@cosmicdrift/kumiko-bundled-features 0.334.0 → 0.335.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 (242) hide show
  1. package/dist/admin-shell/overview-panels.js +2 -2
  2. package/dist/agent-tools/agent-doc-lint.d.ts +1 -0
  3. package/dist/agent-tools/agent-doc-lint.js +33 -3
  4. package/dist/agent-tools/agent-manifest.js +47 -6
  5. package/dist/agent-tools/filter-ops.d.ts +2 -0
  6. package/dist/agent-tools/filter-ops.js +4 -0
  7. package/dist/agent-tools/tool-catalog.js +1 -4
  8. package/dist/agent-tools/tool-dispatch.js +1 -4
  9. package/dist/audit/constants.d.ts +1 -1
  10. package/dist/audit/constants.js +1 -1
  11. package/dist/auth-email-password/handlers/change-password.write.js +2 -0
  12. package/dist/auth-email-password/handlers/invite-create.write.js +12 -2
  13. package/dist/auth-email-password/handlers/login.write.js +2 -0
  14. package/dist/auth-email-password/handlers/self-registration-status.query.js +1 -1
  15. package/dist/auth-email-password/i18n.js +2 -0
  16. package/dist/auth-email-password/web/auth-client.d.ts +1 -0
  17. package/dist/auth-email-password/web/auth-client.js +1 -1
  18. package/dist/auth-email-password/web/auth-gate.js +6 -8
  19. package/dist/auth-email-password/web/index.d.ts +1 -1
  20. package/dist/auth-email-password/web/index.js +1 -1
  21. package/dist/auth-email-password/web/invite-accept-screen.js +16 -12
  22. package/dist/auth-email-password/web/login-screen.js +1 -1
  23. package/dist/auth-email-password/web/session-bootstrap-error.d.ts +5 -1
  24. package/dist/auth-email-password/web/session-bootstrap-error.js +2 -2
  25. package/dist/auth-email-password/web/session.d.ts +4 -0
  26. package/dist/auth-email-password/web/session.js +15 -0
  27. package/dist/auth-mfa/feature.js +3 -2
  28. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +2 -2
  29. package/dist/auth-mfa/handlers/verify.write.js +2 -0
  30. package/dist/auth-mfa/hmac-token-codec.js +3 -0
  31. package/dist/auth-mfa/mfa-challenge-token.js +4 -0
  32. package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -0
  33. package/dist/auth-mfa/web/mfa-client.d.ts +1 -1
  34. package/dist/auth-mfa/web/mfa-client.js +4 -10
  35. package/dist/billing-foundation/billing-info-query.js +1 -1
  36. package/dist/billing-foundation/checkout-core.d.ts +5 -5
  37. package/dist/billing-foundation/checkout-core.js +10 -6
  38. package/dist/billing-foundation/handlers/switch-plan.write.js +5 -8
  39. package/dist/cap-counter/book-cap-usage.js +15 -5
  40. package/dist/cap-counter/enforce-cap.js +9 -3
  41. package/dist/cap-counter/feature.js +3 -2
  42. package/dist/cap-counter/i18n.d.ts +1 -0
  43. package/dist/cap-counter/i18n.js +6 -5
  44. package/dist/cap-overview/handlers/caps-usage.query.js +9 -16
  45. package/dist/cap-overview/handlers/tenant-caps-list.query.js +41 -15
  46. package/dist/cap-overview/i18n.js +5 -0
  47. package/dist/cap-overview/web/cap-cards-panel.js +4 -1
  48. package/dist/cap-overview/web/cap-usage-bar.js +1 -1
  49. package/dist/channel-email/email-channel.js +12 -1
  50. package/dist/channel-in-app/tables.js +2 -6
  51. package/dist/compliance-profiles/handlers/sub-processors.query.js +1 -1
  52. package/dist/config/db/queries/resolver.js +3 -1
  53. package/dist/config/handlers/set.write.js +8 -4
  54. package/dist/config/web/i18n.js +1 -0
  55. package/dist/config/write-helpers.d.ts +1 -1
  56. package/dist/config/write-helpers.js +11 -5
  57. package/dist/crypto-shredding/handlers/forget-subject.write.js +15 -8
  58. package/dist/custom-fields/constants.js +1 -4
  59. package/dist/data-retention/resolve-for-tenant.d.ts +2 -3
  60. package/dist/data-retention/run-retention-cleanup.js +100 -44
  61. package/dist/delivery/types.d.ts +2 -1
  62. package/dist/delivery/types.js +7 -0
  63. package/dist/delivery/unsubscribe.js +5 -11
  64. package/dist/derivatives-sharp/render.js +27 -9
  65. package/dist/document-ingest-foundation/feature.js +5 -5
  66. package/dist/document-ingest-foundation/providers.d.ts +1 -0
  67. package/dist/document-ingest-foundation/providers.js +6 -0
  68. package/dist/document-ingest-foundation/write-document-extract.d.ts +2 -2
  69. package/dist/document-ingest-foundation/write-document-extract.js +10 -0
  70. package/dist/feature-toggles/global-feature-state-table.js +2 -7
  71. package/dist/feature-toggles/toggle-sync-signal.d.ts +2 -1
  72. package/dist/feature-toggles/toggle-sync-signal.js +3 -2
  73. package/dist/files-tenant-data/index.js +3 -2
  74. package/dist/folders/handlers/clear-folder.write.js +4 -11
  75. package/dist/folders/handlers/set-folder.write.js +4 -10
  76. package/dist/form-draft/feature.js +3 -2
  77. package/dist/form-draft/handlers/discard.write.js +7 -8
  78. package/dist/form-draft/handlers/get.query.js +7 -1
  79. package/dist/foundation-shared/host-allowlist.d.ts +4 -0
  80. package/dist/foundation-shared/host-allowlist.js +14 -0
  81. package/dist/foundation-shared/index.d.ts +1 -0
  82. package/dist/foundation-shared/index.js +1 -0
  83. package/dist/foundation-shared/mail-host-policy.d.ts +0 -2
  84. package/dist/foundation-shared/mail-host-policy.js +3 -13
  85. package/dist/inbound-mail-foundation/connect-routes.js +7 -5
  86. package/dist/inbound-mail-foundation/watch-supervisor.js +47 -43
  87. package/dist/jobs/handlers/list.query.d.ts +7 -2
  88. package/dist/jobs/handlers/list.query.js +9 -2
  89. package/dist/jobs/handlers/retry.write.js +3 -2
  90. package/dist/jobs/handlers/tenant-failures.query.js +3 -9
  91. package/dist/jobs/job-run-logger.js +18 -2
  92. package/dist/jobs/tenant-job-failure-table.js +2 -6
  93. package/dist/ledger/handlers/confirm-schedule-period.write.js +2 -2
  94. package/dist/ledger/handlers/reverse-transaction.write.js +2 -2
  95. package/dist/ledger/subject-reference.d.ts +4 -0
  96. package/dist/ledger/subject-reference.js +7 -0
  97. package/dist/legal-pages/feature.js +8 -1
  98. package/dist/managed-pages/handlers/branding.query.js +1 -1
  99. package/dist/managed-pages/handlers/by-slug.query.js +1 -1
  100. package/dist/managed-pages/handlers/by-tenant-published.query.js +1 -1
  101. package/dist/metrics/compute.js +7 -1
  102. package/dist/metrics/window.js +5 -0
  103. package/dist/notes-history/handlers/add-note.write.js +4 -9
  104. package/dist/notes-history-user-data/hooks.js +8 -1
  105. package/dist/personal-access-tokens/constants.d.ts +1 -0
  106. package/dist/personal-access-tokens/constants.js +1 -0
  107. package/dist/personal-access-tokens/feature.js +2 -2
  108. package/dist/personal-access-tokens/handlers/create.write.js +2 -2
  109. package/dist/personal-access-tokens/handlers/revoke.write.js +2 -2
  110. package/dist/personal-access-tokens/i18n.js +12 -13
  111. package/dist/personal-access-tokens/schema/api-token.js +2 -1
  112. package/dist/personal-access-tokens/screens.js +5 -1
  113. package/dist/rate-limiting/feature.js +2 -0
  114. package/dist/rate-limiting/handlers/bucket-access.js +15 -7
  115. package/dist/rate-limiting/i18n.d.ts +10 -0
  116. package/dist/rate-limiting/i18n.js +10 -0
  117. package/dist/secrets/feature.js +3 -2
  118. package/dist/seo/handlers/seo-config.query.js +1 -1
  119. package/dist/sessions/constants.d.ts +1 -0
  120. package/dist/sessions/constants.js +1 -0
  121. package/dist/sessions/feature.js +5 -3
  122. package/dist/sessions/handlers/revoke-all-for-user.write.d.ts +1 -0
  123. package/dist/sessions/handlers/revoke-all-for-user.write.js +12 -2
  124. package/dist/sessions/handlers/revoke.write.js +2 -2
  125. package/dist/sessions/schema/user-session.js +2 -1
  126. package/dist/sessions/screens.js +5 -1
  127. package/dist/sessions/session-callbacks.js +2 -2
  128. package/dist/shared/index.d.ts +1 -1
  129. package/dist/shared/index.js +1 -1
  130. package/dist/shared/lockout-counter.js +1 -1
  131. package/dist/shared/parent-visibility.d.ts +5 -0
  132. package/dist/shared/parent-visibility.js +17 -1
  133. package/dist/shared/row-bound-grant.js +20 -9
  134. package/dist/shared/single-use-token-store.js +5 -13
  135. package/dist/step-dispatcher/webhook-runner.d.ts +0 -2
  136. package/dist/step-dispatcher/webhook-runner.js +18 -17
  137. package/dist/subscription-mollie/plugin-methods.d.ts +3 -2
  138. package/dist/subscription-mollie/plugin-methods.js +7 -1
  139. package/dist/subscription-mollie/verify-webhook.js +6 -0
  140. package/dist/subscription-stripe/plugin-methods.js +25 -6
  141. package/dist/subscription-stripe/verify-webhook.js +13 -6
  142. package/dist/tags/handlers/assign-tag.write.js +4 -10
  143. package/dist/tags/handlers/remove-tag.write.js +4 -11
  144. package/dist/template-resolver/api.d.ts +2 -1
  145. package/dist/template-resolver/api.js +1 -1
  146. package/dist/template-resolver/handlers/by-slug.query.d.ts +1 -1
  147. package/dist/template-resolver/handlers/by-slug.query.js +2 -2
  148. package/dist/template-resolver/handlers/by-tenant.query.d.ts +3 -2
  149. package/dist/template-resolver/handlers/by-tenant.query.js +2 -2
  150. package/dist/template-resolver/handlers/collection-item.query.d.ts +1 -1
  151. package/dist/template-resolver/handlers/collection-list.query.d.ts +1 -1
  152. package/dist/template-resolver/handlers/collection-shared.d.ts +3 -2
  153. package/dist/template-resolver/handlers/collection-shared.js +1 -1
  154. package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -1
  155. package/dist/template-resolver/handlers/find-by-id.query.js +1 -1
  156. package/dist/template-resolver/handlers/list.query.d.ts +1 -1
  157. package/dist/template-resolver/handlers/list.query.js +1 -1
  158. package/dist/template-resolver/seeding.js +41 -39
  159. package/dist/template-resolver/table.d.ts +8 -5
  160. package/dist/template-resolver/testing.js +2 -2
  161. package/dist/template-resolver/user-content-table.d.ts +2 -5
  162. package/dist/template-resolver/web/client-plugin.d.ts +1 -1
  163. package/dist/template-resolver-user-data/hooks.js +1 -1
  164. package/dist/tenant/cancel-pending-invitation.d.ts +8 -0
  165. package/dist/tenant/cancel-pending-invitation.js +22 -0
  166. package/dist/tenant/feature.js +4 -2
  167. package/dist/tenant/handlers/cancel-invitation.write.js +6 -21
  168. package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
  169. package/dist/tenant/handlers/member-directory.query.js +17 -4
  170. package/dist/tenant/handlers/remove-member.write.js +9 -14
  171. package/dist/tenant/handlers/team-list.query.js +2 -2
  172. package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
  173. package/dist/tenant/handlers/tenant-directory.query.js +13 -3
  174. package/dist/tenant/handlers/update-member-roles.write.d.ts +1 -1
  175. package/dist/tenant/handlers/update-member-roles.write.js +28 -12
  176. package/dist/tenant/i18n.js +4 -0
  177. package/dist/tenant/index.d.ts +1 -1
  178. package/dist/tenant/index.js +1 -1
  179. package/dist/tenant/last-tenant-admin.d.ts +1 -1
  180. package/dist/tenant/last-tenant-admin.js +5 -3
  181. package/dist/tenant/seeding.d.ts +4 -0
  182. package/dist/tenant/seeding.js +9 -2
  183. package/dist/tenant-lifecycle/boot-checks.d.ts +1 -0
  184. package/dist/tenant-lifecycle/boot-checks.js +27 -1
  185. package/dist/tenant-lifecycle/feature.js +2 -1
  186. package/dist/tenant-lifecycle/stages.js +2 -2
  187. package/dist/tier-engine/feature.js +3 -2
  188. package/dist/user/command-schemas.d.ts +2 -2
  189. package/dist/user/handlers/create.write.d.ts +1 -1
  190. package/dist/user/handlers/create.write.js +8 -2
  191. package/dist/user/handlers/update.write.d.ts +1 -1
  192. package/dist/user/handlers/update.write.js +8 -2
  193. package/dist/user-data-rights/db/queries/export-jobs.d.ts +2 -0
  194. package/dist/user-data-rights/feature.js +22 -9
  195. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +4 -7
  196. package/dist/user-data-rights/handlers/deletion-grace-period.d.ts +3 -2
  197. package/dist/user-data-rights/handlers/deletion-grace-period.js +30 -8
  198. package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
  199. package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +1 -1
  200. package/dist/user-data-rights/handlers/request-deletion.write.js +3 -6
  201. package/dist/user-data-rights/lib/update-user-lifecycle.js +7 -14
  202. package/dist/user-data-rights/run-export-jobs.d.ts +1 -0
  203. package/dist/user-data-rights/run-export-jobs.js +74 -41
  204. package/dist/user-data-rights/run-forget-cleanup.js +2 -2
  205. package/dist/user-data-rights/run-user-export.js +2 -2
  206. package/dist/user-data-rights-defaults/hooks/file-ref.userdata-hook.js +4 -1
  207. package/dist/user-profile/feature.js +6 -1
  208. package/dist/user-profile/handlers/change-email.write.js +2 -0
  209. package/dist/workflow-runner/db/queries/due-runs.js +6 -9
  210. package/dist/workflow-runner/event-subscriber.js +29 -9
  211. package/dist/workflow-runner/handlers/resume-run.write.js +8 -5
  212. package/dist/workflow-runner/pending-projection.js +8 -1
  213. package/package.json +11 -9
  214. package/src/agent-tools/changes.json +12 -0
  215. package/src/auth-email-password/changes.json +18 -0
  216. package/src/auth-mfa/changes.json +6 -0
  217. package/src/billing-foundation/changes.json +7 -0
  218. package/src/cap-counter/changes.json +7 -0
  219. package/src/cap-overview/changes.json +12 -0
  220. package/src/channel-email/changes.json +6 -0
  221. package/src/config/changes.json +6 -0
  222. package/src/crypto-shredding/changes.json +6 -0
  223. package/src/data-retention/changes.json +12 -0
  224. package/src/delivery/changes.json +7 -0
  225. package/src/derivatives-sharp/changes.json +11 -0
  226. package/src/document-ingest-foundation/changes.json +12 -0
  227. package/src/inbound-mail-foundation/changes.json +12 -0
  228. package/src/jobs/changes.json +13 -0
  229. package/src/legal-pages/changes.json +8 -1
  230. package/src/managed-pages/changes.json +5 -0
  231. package/src/metrics/changes.json +6 -0
  232. package/src/rate-limiting/changes.json +5 -0
  233. package/src/secrets/changes.json +7 -0
  234. package/src/sessions/changes.json +5 -0
  235. package/src/shared/changes.json +6 -0
  236. package/src/step-dispatcher/changes.json +5 -0
  237. package/src/subscription-mollie/changes.json +7 -0
  238. package/src/subscription-stripe/changes.json +11 -0
  239. package/src/template-resolver/changes.json +7 -0
  240. package/src/tenant/changes.json +29 -0
  241. package/src/user-data-rights/changes.json +22 -0
  242. package/src/workflow-runner/changes.json +6 -0
@@ -62,6 +62,10 @@ import { generateDownloadToken } from "./token-helpers.js";
62
62
  // No "_seconds"/"_total" suffix: validateMetricName rejects both on a gauge.
63
63
  // Unit lives in the r.metric() `unit: "seconds"` declaration instead.
64
64
  export const EXPORT_CLEANUP_BACKLOG_AGE_METRIC = "export_cleanup_backlog_age";
65
+ // Unix seconds of the last storage-cleanup pass that ran to completion. A
66
+ // gauge keeps its last value in-process (no TTL), so a dead cron does not make
67
+ // the backlog gauge disappear — alert on `time() - this > interval + buffer`.
68
+ export const EXPORT_CLEANUP_LAST_RUN_TIMESTAMP_METRIC = "export_cleanup_last_run_timestamp";
65
69
  const crud = createEventStoreExecutor(exportJobsTable, exportJobEntity, {
66
70
  entityName: "export-job",
67
71
  });
@@ -429,54 +433,71 @@ async function storageCleanupPass(args) {
429
433
  // via selectMany (db/queries/export-jobs.ts).
430
434
  const candidates = await selectExportJobsForStorageCleanup(db, EXPORT_JOB_STATUS.Done, EXPORT_JOB_STATUS.Failed);
431
435
  const cleaned = [];
432
- // Done-status candidates past expiresAt+grace, keyed by that due-instant —
433
- // used below to report how far cleanup has fallen behind if a candidate
434
- // is still standing (delete threw, or the version-checked null-out lost
435
- // a race) once the pass is done. Failed-status jobs have no expiresAt
436
- // (immediate cleanup, no TTL) so they're out of scope for this signal —
437
- // see EXPORT_CLEANUP_BACKLOG_AGE_METRIC's r.metric() description.
436
+ // Candidates past their due-instant, keyed by that instant — used below to
437
+ // report how far cleanup has fallen behind if a candidate is still standing
438
+ // (delete threw, or the version-checked null-out lost a race) once the pass
439
+ // is done. Done jobs are due at expiresAt+grace; Failed jobs (orphaned ZIPs,
440
+ // no TTL) are due at completedAt ?? startedAt, i.e. from the first pass.
438
441
  const overdueDoneCleanupAfterMs = new Map();
439
- for (const c of candidates) {
440
- if (!c.downloadStorageKey)
441
- continue;
442
- // Done-Jobs brauchen expiresAt+grace-Check. Failed-Jobs gehen direkt
443
- // durch (kein User-Pfad → sofort cleanup).
444
- if (c.status === EXPORT_JOB_STATUS.Done) {
445
- if (!c.expiresAt)
442
+ let passCompleted = false;
443
+ try {
444
+ for (const c of candidates) {
445
+ if (!c.downloadStorageKey)
446
446
  continue;
447
- const profile = await resolveProfileForTenant({
448
- db,
449
- tenantId: c.requestedFromTenantId,
450
- });
451
- const cleanupAfter = c.expiresAt.epochMilliseconds +
452
- profile.profile.userRights.exportStorageCleanupGraceHours * 60 * 60 * 1000;
453
- if (now.epochMilliseconds < cleanupAfter)
447
+ // Done jobs need the expiresAt+grace check. Failed jobs pass straight
448
+ // through (no user path, so clean up immediately).
449
+ if (c.status === EXPORT_JOB_STATUS.Done) {
450
+ if (!c.expiresAt)
451
+ continue;
452
+ // Count the candidate as overdue (lower bound, grace unknown) before the
453
+ // throwing profile lookup, so a failing pass still shows a backlog.
454
+ overdueDoneCleanupAfterMs.set(c.id, c.expiresAt.epochMilliseconds);
455
+ const profile = await resolveProfileForTenant({
456
+ db,
457
+ tenantId: c.requestedFromTenantId,
458
+ });
459
+ const cleanupAfter = c.expiresAt.epochMilliseconds +
460
+ profile.profile.userRights.exportStorageCleanupGraceHours * 60 * 60 * 1000;
461
+ if (now.epochMilliseconds < cleanupAfter) {
462
+ overdueDoneCleanupAfterMs.delete(c.id);
463
+ continue;
464
+ }
465
+ overdueDoneCleanupAfterMs.set(c.id, cleanupAfter);
466
+ }
467
+ // Failed-job branch: no TTL check, clean up immediately.
468
+ else {
469
+ const failedAt = c.completedAt ?? c.startedAt;
470
+ if (failedAt)
471
+ overdueDoneCleanupAfterMs.set(c.id, failedAt.epochMilliseconds);
472
+ }
473
+ // Delete the storage file and null the DB column.
474
+ try {
475
+ const provider = await buildStorageProvider(c.requestedFromTenantId);
476
+ await provider.delete(c.downloadStorageKey);
477
+ }
478
+ catch {
479
+ // best-effort; if the storage delete fails, retry on the next pass
454
480
  continue;
455
- overdueDoneCleanupAfterMs.set(c.id, cleanupAfter);
456
- }
457
- // Failed-Job-Branch: kein TTL-Check, sofort cleanup.
458
- // Storage-Datei loeschen + DB-Spalte nullen.
459
- try {
460
- const provider = await buildStorageProvider(c.requestedFromTenantId);
461
- await provider.delete(c.downloadStorageKey);
462
- }
463
- catch {
464
- // best-effort; wenn Storage-Delete failed, retry beim naechsten Pass
465
- continue;
481
+ }
482
+ const result = await crud.update({
483
+ id: c.id,
484
+ version: c.version,
485
+ changes: { downloadStorageKey: null },
486
+ }, createSystemUser(c.requestedFromTenantId), systemTenantDb(db, c.requestedFromTenantId));
487
+ if (result.isSuccess)
488
+ cleaned.push(c.id);
466
489
  }
467
- const result = await crud.update({
468
- id: c.id,
469
- version: c.version,
470
- changes: { downloadStorageKey: null },
471
- }, createSystemUser(c.requestedFromTenantId), systemTenantDb(db, c.requestedFromTenantId));
472
- if (result.isSuccess)
473
- cleaned.push(c.id);
490
+ passCompleted = true;
491
+ }
492
+ finally {
493
+ reportOldestOverdueAge(metrics, overdueDoneCleanupAfterMs, cleaned, now);
494
+ if (passCompleted)
495
+ reportCleanupLastRun(metrics, now);
474
496
  }
475
- reportOldestOverdueAge(metrics, overdueDoneCleanupAfterMs, cleaned, now);
476
497
  return cleaned;
477
498
  }
478
- // Seconds since the longest-overdue Done-status bundle should have been
479
- // deleted (expiresAt+grace passed) but still isn't — 0 when the pass
499
+ // Seconds since the longest-overdue bundle (Done: expiresAt+grace passed;
500
+ // Failed: completedAt ?? startedAt) should have been deleted but still isn't — 0 when the pass
480
501
  // cleaned everything that was due, or nothing was due.
481
502
  function oldestOverdueAgeSeconds(overdueDoneCleanupAfterMs, cleanedIds, now) {
482
503
  const cleaned = new Set(cleanedIds);
@@ -505,6 +526,18 @@ function reportOldestOverdueAge(metrics, overdueDoneCleanupAfterMs, cleanedIds,
505
526
  console.warn(`[user-data-rights:run-export-jobs] metrics.set(${EXPORT_CLEANUP_BACKLOG_AGE_METRIC}) failed err=${err instanceof Error ? err.message : String(err)}`);
506
527
  }
507
528
  }
529
+ function reportCleanupLastRun(metrics, now) {
530
+ // skip: no meter configured means there is nothing to report.
531
+ if (!metrics)
532
+ return;
533
+ try {
534
+ metrics.set(EXPORT_CLEANUP_LAST_RUN_TIMESTAMP_METRIC, now.epochMilliseconds / 1000);
535
+ }
536
+ catch (err) {
537
+ // biome-ignore lint/suspicious/noConsole: operator-visibility for observability-emit-failure
538
+ console.warn(`[user-data-rights:run-export-jobs] metrics.set(${EXPORT_CLEANUP_LAST_RUN_TIMESTAMP_METRIC}) failed err=${err instanceof Error ? err.message : String(err)}`);
539
+ }
540
+ }
508
541
  // Wrapper: crud.update braucht TenantDb. ExportJob ist tenant-agnostisch
509
542
  // (1 Job pro userId), aber der event-store-Stream-Lookup erwartet einen
510
543
  // Tenant-Context. system-mode TenantDb bypassed den auto-tenant-filter
@@ -35,7 +35,7 @@ import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
35
35
  import { configuredPiiSubjectKms, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
36
36
  import { createTenantDb, entityEventName, executeRawQuery, nullBlindIndexesForSubject, } from "@cosmicdrift/kumiko-framework/db";
37
37
  import { EXT_USER_DATA, EXT_USER_DATA_ORDER, extensionUsageEscapeHatchReason, } from "@cosmicdrift/kumiko-framework/engine";
38
- import { createEscapeHatchReporter } from "@cosmicdrift/kumiko-framework/pipeline";
38
+ import { createEscapeHatchReporter, UNATTRIBUTED_ACTOR, } from "@cosmicdrift/kumiko-framework/pipeline";
39
39
  import { purgeSearchDocumentsForSubject, } from "@cosmicdrift/kumiko-framework/search";
40
40
  import { resolveRetentionPolicyForTenant, resolveTenantRetentionPreset, } from "../data-retention/index.js";
41
41
  import { decryptStoredPii, runInSubTransaction } from "../shared/index.js";
@@ -47,7 +47,7 @@ function buildHookDb(db, tenantId, entry, args) {
47
47
  const report = createEscapeHatchReporter({
48
48
  handler: `${EXT_USER_DATA}:${entry.entityName}`,
49
49
  tenantId,
50
- actor: args.actor ?? "system",
50
+ actor: args.actor ?? UNATTRIBUTED_ACTOR,
51
51
  sink: args.escapeHatchAuditSink,
52
52
  });
53
53
  return createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
@@ -23,14 +23,14 @@ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
23
23
  import { configuredPiiSubjectKms, decryptPiiFieldValues, isPiiCiphertext, } from "@cosmicdrift/kumiko-framework/crypto";
24
24
  import { collectEncryptedFieldNames, configuredEntityFieldEncryption, createTenantDb, decryptEntityFieldValues, } from "@cosmicdrift/kumiko-framework/db";
25
25
  import { EXT_USER_DATA, extensionUsageEscapeHatchReason, maskWriteOnlyFields, } from "@cosmicdrift/kumiko-framework/engine";
26
- import { createEscapeHatchReporter } from "@cosmicdrift/kumiko-framework/pipeline";
26
+ import { createEscapeHatchReporter, UNATTRIBUTED_ACTOR, } from "@cosmicdrift/kumiko-framework/pipeline";
27
27
  import { tenantMembershipsTable } from "../tenant/index.js";
28
28
  import { buildFileRefZipPath } from "./zip-path.js";
29
29
  function buildHookDb(db, tenantId, entry, args) {
30
30
  const report = createEscapeHatchReporter({
31
31
  handler: `${EXT_USER_DATA}:${entry.entityName}`,
32
32
  tenantId,
33
- actor: args.actor ?? "system",
33
+ actor: args.actor ?? UNATTRIBUTED_ACTOR,
34
34
  sink: args.escapeHatchAuditSink,
35
35
  });
36
36
  return createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
@@ -1,6 +1,7 @@
1
1
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { deleteStoredFileAndDerivatives } from "@cosmicdrift/kumiko-framework/derivatives";
3
3
  import { createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
4
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
4
5
  import { fileRefEntity, fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
5
6
  import { assertErased } from "../../shared/index.js";
6
7
  const ERROR_CONTEXT = "user-data-rights-defaults:fileRef";
@@ -222,7 +223,9 @@ export const fileRefDeleteHook = async (ctx, strategy) => {
222
223
  if (storageProvider) {
223
224
  const failedKeys = await deleteBinaries(personalRows, storageProvider);
224
225
  if (failedKeys.length > 0) {
225
- throw new Error(`[user-data-rights-defaults:fileRef] ${failedKeys.length} binary delete(s) failed — aborting forget so the rows are retried next run (keys: ${failedKeys.join(", ")})`);
226
+ throw new InternalError({
227
+ message: `[user-data-rights-defaults:fileRef] ${failedKeys.length} binary delete(s) failed — aborting forget so the rows are retried next run (keys: ${failedKeys.join(", ")})`,
228
+ });
226
229
  }
227
230
  }
228
231
  else {
@@ -42,6 +42,9 @@ export function createUserProfileFeature() {
42
42
  "profile.email.title": { en: "Email address" },
43
43
  "profile.password.title": { en: "Password" },
44
44
  "profile.danger.title": { en: "Delete account" },
45
+ "profile.danger.explainer": {
46
+ en: "Your account will be permanently deleted after a grace period. Until then you can cancel the deletion at any time.",
47
+ },
45
48
  "profile.danger.gracePeriodEnd": { en: "Deletion date" },
46
49
  "profile.danger.delete": { en: "Delete account" },
47
50
  "profile.danger.dialogDescription": {
@@ -94,7 +97,7 @@ export function createUserProfileFeature() {
94
97
  },
95
98
  {
96
99
  title: i18nKey("profile.danger.title"),
97
- description: "profile.danger.explainer",
100
+ description: i18nKey("profile.danger.explainer"),
98
101
  // gracePeriodEnd is only meaningful once a deletion is actually
99
102
  // pending — hide it instead of showing an empty date when status
100
103
  // isn't deletionRequested (same rule as privacy-center).
@@ -123,6 +126,8 @@ export function createUserProfileFeature() {
123
126
  handler: UserDataRightsHandlers.cancelDeletion,
124
127
  visible: { field: "status", eq: "deletionRequested" },
125
128
  style: "secondary",
129
+ // Declared: the id's last segment ("deletion") would otherwise resolve to the destructive trash icon.
130
+ icon: "x",
126
131
  },
127
132
  ],
128
133
  });
@@ -33,6 +33,8 @@ export const changeEmailWrite = defineWriteHandler({
33
33
  "email uniqueness the same way, and writes the new email via ctx.writeAs(SYSTEM, " +
34
34
  "user:update) — field-access on passwordHash/email is privileged-only.",
35
35
  },
36
+ // Payload carries the current password.
37
+ agent: { expose: false },
36
38
  description: "Replaces the signed-in caller's own email address after re-checking their current password, refusing an unchanged or already-taken address and clearing the verified flag so the app can re-run email verification.",
37
39
  handler: async (event, ctx) => {
38
40
  const systemUser = createSystemUser(event.user.tenantId);
@@ -1,12 +1,9 @@
1
1
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
2
- // Standalone statement (no db.begin) — FOR UPDATE SKIP LOCKED only
3
- // needs to survive this one SELECT to reduce redundant dispatches
4
- // under true concurrency; it auto-commits and releases the row locks
5
- // immediately after. The real correctness guarantee is resume-run's
6
- // VersionConflictError-checked claim (ctx.tryAppendEvent on
7
- // WORKFLOW_RESUMED) — a second dispatch for the same row just loses
8
- // that race and no-ops, same as the sample this was adapted from.
2
+ // Dedup happens solely via resume-run's VersionConflictError-checked claim
3
+ // (ctx.tryAppendEvent on WORKFLOW_RESUMED) — a second dispatch for the same
4
+ // row loses that race and no-ops. No row locks: in an autocommit statement
5
+ // they would be released right after the SELECT and guard nothing.
9
6
  export async function selectDueWorkflowRunPending(db, tenantId) {
10
- // kumiko-lint-ignore raw-sql tenant-scoped due-row pickup with FOR UPDATE SKIP LOCKED
11
- return (await asRawClient(db).unsafe(`SELECT run_id, step_index FROM workflow_run_pending WHERE tenant_id = $1 AND wake_at < now() FOR UPDATE SKIP LOCKED`, [tenantId]));
7
+ // kumiko-lint-ignore raw-sql tenant-scoped due-row pickup
8
+ return (await asRawClient(db).unsafe(`SELECT run_id, step_index FROM workflow_run_pending WHERE tenant_id = $1 AND wake_at < now()`, [tenantId]));
12
9
  }
@@ -25,21 +25,41 @@
25
25
  // cron tick — the one resume path stays exactly the one Phase 2 built.
26
26
  import { selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
27
27
  import { evaluateEventMatch } from "@cosmicdrift/kumiko-framework/engine";
28
+ import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
29
+ import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
28
30
  import { workflowRunPendingTable } from "./tables.js";
31
+ const log = createFallbackLogger("workflow-runner");
32
+ // A row whose persisted matchExpr cannot be evaluated (unknown version from a
33
+ // rolling deploy, corrupt AST) must not abort the whole projection: that would
34
+ // block every other waiting run of this event type on the same event forever.
35
+ function rowMatchesEvent(row, eventType, payload) {
36
+ if (!row.matchExpr)
37
+ return true;
38
+ try {
39
+ return evaluateEventMatch(
40
+ // @cast-boundary engine-payload — matchExpr round-trips through
41
+ // jsonb as the EventMatch AST waitForEvent's `match` resolver
42
+ // persisted (see steps/wait-for-event.ts); evaluateEventMatch
43
+ // rejects a malformed shape itself.
44
+ row.matchExpr, payload);
45
+ }
46
+ catch (error) {
47
+ log.error("workflow wakeup skipped a pending row with an unevaluable matchExpr", {
48
+ runId: row.runId,
49
+ stepIndex: row.stepIndex,
50
+ eventType,
51
+ error: error instanceof Error ? error.message : String(error),
52
+ });
53
+ return false;
54
+ }
55
+ }
29
56
  const wakeupApply = async (event, tx) => {
30
57
  const rows = await selectMany(tx, workflowRunPendingTable, {
31
58
  tenantId: event.tenantId,
32
59
  waitEventType: event.type,
33
60
  });
34
61
  for (const row of rows) {
35
- const matches = row.matchExpr
36
- ? evaluateEventMatch(
37
- // @cast-boundary engine-payload — matchExpr round-trips through
38
- // jsonb as the EventMatch AST waitForEvent's `match` resolver
39
- // persisted (see steps/wait-for-event.ts); never an unchecked
40
- // external value.
41
- row.matchExpr, event.payload)
42
- : true;
62
+ const matches = rowMatchesEvent(row, event.type, event.payload);
43
63
  // skip: this row's matchExpr rejected the event — not this row's
44
64
  // resume, leave wakeAt/triggerEventType untouched.
45
65
  if (!matches)
@@ -47,7 +67,7 @@ const wakeupApply = async (event, tx) => {
47
67
  await updateMany(tx, workflowRunPendingTable, {
48
68
  triggerEventType: event.type,
49
69
  triggerPayload: event.payload,
50
- wakeAt: Temporal.Now.instant().toString(),
70
+ wakeAt: getTemporal().Now.instant().toString(),
51
71
  }, { tenantId: row.tenantId, runId: row.runId, stepIndex: row.stepIndex });
52
72
  }
53
73
  };
@@ -36,7 +36,7 @@ import { workflowRunPendingTable } from "../tables.js";
36
36
  import { getWorkflow } from "../workflow-registry.js";
37
37
  const log = createFallbackLogger("workflow-runner");
38
38
  const resumeRunSchema = z.object({
39
- runId: z.string().min(1),
39
+ runId: z.string().uuid(),
40
40
  stepIndex: z.number().int().nonnegative(),
41
41
  });
42
42
  async function appendRunFailed(ctx, runId, failedPayload) {
@@ -170,16 +170,19 @@ export const resumeRunHandler = {
170
170
  return { isSuccess: true, data: { outcome: "already-resumed" } };
171
171
  }
172
172
  const { workflowName } = pending;
173
+ const runEvents = await ctx.loadAggregate(runId);
174
+ // Before the workflow resolution: a racing tick on an already-settled run
175
+ // must not append run-failed after run-completed when the definition has
176
+ // since changed or been removed.
177
+ if (isRunAlreadySettled(runEvents, stepIndex, pending.retryAttempt ?? undefined)) {
178
+ return { isSuccess: true, data: { outcome: "already-resumed" } };
179
+ }
173
180
  const resolved = resolveRunnableWorkflow(workflowName, runId, stepIndex, pending.definitionFingerprint);
174
181
  if (resolved.failure !== null) {
175
182
  await appendRunFailed(ctx, runId, resolved.failure);
176
183
  return { isSuccess: true, data: { outcome: "failed" } };
177
184
  }
178
185
  const workflow = resolved.workflow;
179
- const runEvents = await ctx.loadAggregate(runId);
180
- if (isRunAlreadySettled(runEvents, stepIndex, pending.retryAttempt ?? undefined)) {
181
- return { isSuccess: true, data: { outcome: "already-resumed" } };
182
- }
183
186
  const claim = await ctx.tryAppendEvent({
184
187
  aggregateId: runId,
185
188
  aggregateType: WORKFLOW_AGGREGATE_TYPE,
@@ -11,10 +11,17 @@
11
11
  // No resume logic here — framework#2513 Phase 2 owns the job that reads
12
12
  // this table and dispatches the actual resume. This projection only writes
13
13
  // the rows.
14
- import { deleteMany, upsertOnConflict } from "@cosmicdrift/kumiko-framework/bun-db";
14
+ import { deleteMany, fetchOne, upsertOnConflict } from "@cosmicdrift/kumiko-framework/bun-db";
15
15
  import { WORKFLOW_RESUMED_TYPE, WORKFLOW_RETRY_SCHEDULED_TYPE, WORKFLOW_RUN_COMPLETED_TYPE, WORKFLOW_RUN_FAILED_TYPE, WORKFLOW_WAITING_FOR_EVENT_TYPE, WORKFLOW_WAITING_TYPE, } from "@cosmicdrift/kumiko-framework/engine";
16
16
  import { workflowRunPendingTable } from "./tables.js";
17
17
  async function upsertPending(tx, row) {
18
+ // At-least-once redelivery of a suspension event after the event-subscriber
19
+ // already matched a trigger onto this row must not reset wakeAt/trigger —
20
+ // the run would sleep until timeout and lose the matched payload.
21
+ const existing = await fetchOne(tx, workflowRunPendingTable, { tenantId: row.tenantId, runId: row.runId, stepIndex: row.stepIndex });
22
+ // skip: row already carries a trigger event type, keep it on redelivery
23
+ if (existing?.triggerEventType != null)
24
+ return;
18
25
  await upsertOnConflict(tx, workflowRunPendingTable, {
19
26
  ...row,
20
27
  // Phase 3's event-subscriber writes these when the awaited event
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.334.0",
3
+ "version": "0.335.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>",
@@ -483,18 +483,20 @@
483
483
  }
484
484
  },
485
485
  "dependencies": {
486
- "@cosmicdrift/kumiko-dispatcher-live": "0.334.0",
487
- "@cosmicdrift/kumiko-framework": "0.334.0",
488
- "@cosmicdrift/kumiko-headless": "0.334.0",
489
- "@cosmicdrift/kumiko-renderer": "0.334.0",
490
- "@cosmicdrift/kumiko-renderer-web": "0.334.0",
491
- "@cosmicdrift/kumiko-types": "0.334.0",
486
+ "@cosmicdrift/kumiko-dispatcher-live": "0.335.0",
487
+ "@cosmicdrift/kumiko-framework": "0.335.0",
488
+ "@cosmicdrift/kumiko-headless": "0.335.0",
489
+ "@cosmicdrift/kumiko-renderer": "0.335.0",
490
+ "@cosmicdrift/kumiko-renderer-web": "0.335.0",
491
+ "@cosmicdrift/kumiko-types": "0.335.0",
492
492
  "@mollie/api-client": "^4.5.0",
493
493
  "@node-rs/argon2": "^2.0.2",
494
494
  "@types/mailparser": "^3.4.6",
495
495
  "@types/qrcode": "^1.5.5",
496
496
  "clsx": "^2.1.1",
497
+ "hono": "^4.13.1",
497
498
  "imapflow": "^1.7.8",
499
+ "ioredis": "^5.10.1",
498
500
  "jose": "^6.2.3",
499
501
  "lucide-react": "^1.14.0",
500
502
  "mailparser": "^3.9.32",
@@ -983,8 +985,8 @@
983
985
  ],
984
986
  "devDependencies": {
985
987
  "@testing-library/user-event": "^14.6.1",
986
- "@cosmicdrift/kumiko-locale-de": "0.334.0",
987
- "@cosmicdrift/kumiko-locale-es": "0.334.0",
988
+ "@cosmicdrift/kumiko-locale-de": "0.335.0",
989
+ "@cosmicdrift/kumiko-locale-es": "0.335.0",
988
990
  "jsqr": "^1.4.0"
989
991
  }
990
992
  }
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.335.0",
4
+ "type": "breaking",
5
+ "title": "Manifest rejects unknown denyQns, drops navs under hidden parents; doc lint flags blank and exposed-undescribed handlers",
6
+ "detail": "Agent tools: `buildAgentManifest` now throws when `denyQns` names a handler that is not registered, so a typo can no longer leave a handler exposed. Navs whose parent is hidden from the role are dropped from the manifest, and screens reachable only through them are no longer listed. The agent doc lint reports handlers that are exposed without a description and treats a blank description as a gap.",
7
+ "migration": "`buildAgentManifest` now throws when `denyQns` names a handler that is not registered; remove stale or misspelled entries from your `denyQns` lists."
8
+ },
9
+ {
10
+ "version": "0.335.0",
11
+ "type": "fix",
12
+ "title": "Agent doc lint reports exposed handlers with a non-convertible input schema"
13
+ },
2
14
  {
3
15
  "version": "0.313.0",
4
16
  "type": "fix",
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "version": "0.335.0",
4
+ "type": "fix",
5
+ "title": "Credential-carrying auth handlers are hidden from the agent catalog; additionalAssignableRoles rejects framework-ranked roles",
6
+ "detail": "The credential-carrying handlers `auth-email-password:login`, `change-password`, `user-profile:change-email`, `auth-mfa:verify` and `auth-mfa:enable-confirm-preauth` are now opted out of the agent tool catalog with `agent: { expose: false }`, so passwords, TOTP codes and setup tokens no longer reach an LLM transcript. `invite.additionalAssignableRoles` now throws at construction when it lists a framework-ranked role such as `TenantAdmin`, instead of silently disabling the elevation guard for it."
7
+ },
8
+ {
9
+ "version": "0.335.0",
10
+ "type": "fix",
11
+ "title": "Invite accept handles MFA responses, bootstrap error screen offers Sign out",
12
+ "detail": "`InviteAcceptScreen` no longer redirects to a tenant URL when the accept route answers with an MFA challenge or setup requirement (no session cookie was minted); it sends the user to `loginHref` instead. `SessionBootstrapErrorScreen` accepts an optional `onSignOut` and the auth gate wires it, so a permanent bootstrap failure no longer traps the user behind \"Try again\" (new i18n key `auth.sessionBootstrap.signOut`). Cap bookings back off with a small random delay between version-conflict retries."
13
+ },
14
+ {
15
+ "version": "0.335.0",
16
+ "type": "fix",
17
+ "title": "Ended session leads back to the login screen",
18
+ "detail": "When the server ends a session (revoked, expired), the app now shows the login screen with a short hint instead of a raw error banner. After signing in again the user lands on the same screen as before."
19
+ },
2
20
  {
3
21
  "version": "0.328.1",
4
22
  "type": "fix",
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.335.0",
4
+ "type": "fix",
5
+ "title": "MFA web client shares the login response mapper",
6
+ "detail": "The MFA web client reuses the shared `toLoginResponse` helper (now exported from the auth-email-password web barrel) for `landingPath` handling, so login, MFA verify and MFA setup confirm map the response identically."
7
+ },
2
8
  {
3
9
  "version": "0.329.0",
4
10
  "type": "improvement",
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.335.0",
4
+ "type": "breaking",
5
+ "title": "Unregistered catalog.providerName fails unconfigured; switch-plan checks provider mismatch before lookup",
6
+ "detail": "`billing-plans` now fails with an `unconfigured` error when `catalog.providerName` names a provider that is not registered, instead of rendering billing as disabled. Billing renders as disabled only when no `providerName` is set and no provider is mounted. `switch-plan` answers a subscription on an unmounted provider with the `providerMismatch` conflict instead of a 500.",
7
+ "migration": "If `catalog.providerName` names a provider that is not mounted, billing-plans now fails with `unconfigured` instead of rendering billing as disabled; mount the provider or remove `providerName`."
8
+ },
2
9
  {
3
10
  "version": "0.330.0",
4
11
  "type": "fix",
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.335.0",
4
+ "type": "breaking",
5
+ "title": "Cap helpers validate periodStartIso and amount, surface failed soft-warn writes; German column labels",
6
+ "detail": "`bookCapUsage` and `markCapSoftWarned` now reject a `periodStartIso` that is not an ISO instant with a validation error instead of silently forking a counter row or throwing a `RangeError`. `enforceCap` rejects a non-integer or non-positive `amount` instead of letting it bypass the cap. `enforceCapAndMaybeNotify` surfaces a failed soft-warn write instead of re-sending the notification on the next call. The cap-counter list now has German column labels.",
7
+ "migration": "`bookCapUsage` and `markCapSoftWarned` now reject a `periodStartIso` that is not a full ISO instant, including date-only strings like `2026-10-01`; pass e.g. `2026-10-01T00:00:00.000Z`."
8
+ },
2
9
  {
3
10
  "version": "0.317.0",
4
11
  "type": "fix",
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.335.0",
4
+ "type": "fix",
5
+ "title": "tenant-caps:list validates cursor and filters, parallel usage reads; cap-counter list not searchable",
6
+ "detail": "`tenant-caps:list` now rejects a malformed pagination cursor and unsupported or repeated filters with a validation error instead of returning a wrong page or silently ignoring the filter, and loads per-tenant usage in parallel. The `cap-counter` operator list is no longer searchable, because search resolved against the caller's own tenant instead of all tenants."
7
+ },
8
+ {
9
+ "version": "0.335.0",
10
+ "type": "fix",
11
+ "title": "Unlimited caps show a labelled usage count",
12
+ "detail": "The cap usage bar labels an unlimited cap as \"<used> · unlimited\" through the new `cap-overview.unlimited` key instead of a bare number. The German and Spanish catalogs ship the translation."
13
+ },
2
14
  {
3
15
  "version": "0.332.0",
4
16
  "type": "fix",
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.335.0",
4
+ "type": "fix",
5
+ "title": "Explicit List-Unsubscribe headers replace the automatic pair case-insensitively",
6
+ "detail": "The email channel now matches explicit `data.headers` case-insensitively against the automatic unsubscribe headers. Overriding `List-Unsubscribe` or `List-Unsubscribe-Post` in any spelling drops the whole automatic pair, so a mail client no longer sends a one-click POST to a URL that cannot handle it."
7
+ },
2
8
  {
3
9
  "version": "0.321.0",
4
10
  "type": "improvement",
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.335.0",
4
+ "type": "breaking",
5
+ "title": "config:write:set masks the echoed value for encrypted-at-rest keys; validation errors no longer include the value",
6
+ "migration": "A caller that reads `data.value` from a `config:write:set` result for a key with `encrypted: true` or `backing: \"secrets\"` now gets the mask. Use the value it sent instead. Tests that match the `value` param of a validation error on such a key need to drop that expectation."
7
+ },
2
8
  {
3
9
  "version": "0.326.0",
4
10
  "type": "breaking",
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.335.0",
4
+ "type": "fix",
5
+ "title": "forget-denied audit events record the specific tenant-gate denial reason",
6
+ "detail": "The `forget-denied` audit event now records the specific tenant-gate reason (for example `target_tenant_not_admin_tenant`) instead of the generic `access_denied`. The framework exports `requireEntityTableMeta` from `bun-db`."
7
+ },
2
8
  {
3
9
  "version": "0.332.0",
4
10
  "type": "fix",
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.335.0",
4
+ "type": "fix",
5
+ "title": "Retention hardDelete keeps cross-entity shared files and isolates storage errors per row",
6
+ "detail": "Retention hardDelete no longer deletes a fileRef that a live row of another entity type still references in a file/image field (relevant for unbound uploads). A storage error while deleting a row's files (for example a missing list permission) now skips only that row with `file_delete_failed` instead of aborting the whole tenant's cleanup run."
7
+ },
8
+ {
9
+ "version": "0.335.0",
10
+ "type": "fix",
11
+ "title": "Retention hardDelete pages past stuck file rows and blockDelete reports personal fields without anonymize",
12
+ "detail": "Retention hardDelete on entities with file fields now pages by id, so rows stuck on a storage failure or a missing file storage no longer starve the rows behind them. A `blockDelete` entity with a personal field but no `anonymize` function is reported as `missing_anonymize_fields` again after the hold expires; only entities without any anonymizable subject field stay silent. The framework exports `entityHasAnonymizableSubjectField` for this check."
13
+ },
2
14
  {
3
15
  "version": "0.330.1",
4
16
  "type": "fix",
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.335.0",
4
+ "type": "breaking",
5
+ "title": "Channel plugin guard validates mode and render",
6
+ "detail": "Stricter input guards. A delivery channel plugin must declare `mode` as `inline` or `queued` and a function `render`, otherwise boot fails instead of silently dispatching inline. The derivatives-sharp overlay rejects a non-finite `marginPct` before it reaches sharp. A document-ingest provider can no longer register under the reserved name `unknown`, which marks upcast legacy ingest events. The unsubscribe POST route also accepts a JSON body.",
7
+ "migration": "A delivery channel plugin must now declare `mode` as `\"inline\"` or `\"queued\"` and a function `render`; boot fails otherwise instead of dispatching inline."
8
+ },
2
9
  {
3
10
  "version": "0.330.0",
4
11
  "type": "improvement",
@@ -1,4 +1,15 @@
1
1
  [
2
+ {
3
+ "version": "0.335.0",
4
+ "type": "fix",
5
+ "title": "Overlay and blur intermediates are lossless and QR overlays render crisp at their final size",
6
+ "detail": "The sharp renderer buffers intermediates losslessly before overlays and blur regions, so a JPEG source no longer gains an extra compression generation. QR overlays are generated at their final size and checked against the smaller of output width and height before generation, so a flat output can no longer cache an unscannable QR."
7
+ },
8
+ {
9
+ "version": "0.335.0",
10
+ "type": "fix",
11
+ "title": "Overlay marginPct must be finite"
12
+ },
2
13
  {
3
14
  "version": "0.287.0",
4
15
  "type": "improvement",
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.335.0",
4
+ "type": "fix",
5
+ "title": "Writing a document extract is idempotent per fileRef",
6
+ "detail": "`writeDocumentExtractForLiveFileRef` now returns `{ kind: \"skipped\", reason: \"already_extracted\" }` when the fileRef already has a documentExtract, so a redelivered `documentIngest.requested` or a delete-restore race no longer creates a second extract. Providers need no existence check of their own; code that switches exhaustively on `reason` must handle the new value."
7
+ },
8
+ {
9
+ "version": "0.335.0",
10
+ "type": "breaking",
11
+ "title": "Provider name \"unknown\" is reserved for upcast legacy events",
12
+ "migration": "Registering a document-ingest provider under the name `unknown` now throws; rename any provider that uses it."
13
+ },
2
14
  {
3
15
  "version": "0.312.0",
4
16
  "type": "breaking",