@cosmicdrift/kumiko-bundled-features 0.274.0 → 0.276.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 (47) hide show
  1. package/package.json +9 -9
  2. package/src/__tests__/cross-tenant-jobs-escape-hatch.test.ts +45 -0
  3. package/src/audit/feature.ts +99 -95
  4. package/src/auth-mfa/__tests__/reencrypt-job.integration.test.ts +21 -13
  5. package/src/auth-mfa/feature.ts +16 -1
  6. package/src/auth-mfa/handlers/reencrypt.job.ts +7 -9
  7. package/src/config/__tests__/reencrypt-job-concurrent-write-race.integration.test.ts +7 -2
  8. package/src/config/__tests__/reencrypt-job-kek-rotation.integration.test.ts +9 -3
  9. package/src/config/__tests__/reencrypt-job-tenant-checks.integration.test.ts +8 -2
  10. package/src/config/handlers/reencrypt.job.ts +8 -5
  11. package/src/crypto-shredding/feature.ts +18 -14
  12. package/src/data-retention/__tests__/retention-cleanup.integration.test.ts +4 -1
  13. package/src/data-retention/feature.ts +21 -15
  14. package/src/files-tenant-data/__tests__/hooks.integration.test.ts +4 -2
  15. package/src/files-tenant-data/__tests__/sweep-orphaned-derivatives.job.integration.test.ts +8 -2
  16. package/src/files-tenant-data/changes.json +9 -1
  17. package/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +8 -13
  18. package/src/files-tenant-data/index.ts +17 -8
  19. package/src/form-draft/feature.ts +13 -1
  20. package/src/form-draft/handlers/cleanup.job.ts +7 -11
  21. package/src/inbound-mail-foundation/feature.ts +18 -14
  22. package/src/jobs/__tests__/job-system-user.integration.test.ts +39 -30
  23. package/src/jobs/__tests__/reindex-entity-job.integration.test.ts +3 -3
  24. package/src/jobs/handlers/retention-cleanup.job.ts +5 -3
  25. package/src/jobs/handlers/stale-run-sweep.job.ts +5 -3
  26. package/src/presets/__tests__/security-baseline.test.ts +60 -1
  27. package/src/presets/security-baseline.ts +1 -2
  28. package/src/rate-limiting/feature.ts +15 -11
  29. package/src/secrets/__tests__/rotate.integration.test.ts +4 -4
  30. package/src/secrets/changes.json +7 -0
  31. package/src/secrets/feature.ts +19 -4
  32. package/src/secrets/handlers/rotate.job.ts +7 -9
  33. package/src/sessions/__tests__/cleanup.integration.test.ts +5 -5
  34. package/src/sessions/feature.ts +142 -128
  35. package/src/sessions/handlers/cleanup.job.ts +7 -10
  36. package/src/tenant-lifecycle/feature.ts +13 -10
  37. package/src/tenant-settings/__tests__/tenant-currency-read-access.integration.test.ts +50 -0
  38. package/src/tenant-settings/constants.ts +6 -1
  39. package/src/user-data-rights/__tests__/file-binary-forget-cleanup.integration.test.ts +4 -2
  40. package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +4 -1
  41. package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +4 -1
  42. package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +11 -2
  43. package/src/user-data-rights/__tests__/tenant-model-erasure.integration.test.ts +3 -1
  44. package/src/user-data-rights/feature.ts +26 -20
  45. package/src/workflow-runner/__tests__/event-wakeup.integration.test.ts +8 -2
  46. package/src/workflow-runner/__tests__/resume-loop.integration.test.ts +8 -2
  47. package/src/workflow-runner/feature.ts +10 -3
@@ -529,21 +529,24 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
529
529
  });
530
530
 
531
531
  // S2.U3 Atom 3b — Worker fuer Async Export-Pipeline. Cron-getriggert.
532
- r.job(
533
- "run-export-jobs",
534
- { trigger: { cron: "0 3 * * *" }, concurrency: "skip" },
535
- async (_payload, ctx) => {
536
- if (!ctx.db || !ctx.registry) {
537
- throw new Error(
538
- "run-export-jobs: ctx.db + ctx.registry required (JobContext incomplete)",
539
- );
532
+ const RUN_EXPORT_JOBS_REASON = "processes pending export jobs across every tenant";
533
+ r.job({
534
+ name: "run-export-jobs",
535
+ trigger: { cron: "0 3 * * *" },
536
+ concurrency: "skip",
537
+ escapeHatch: { reason: RUN_EXPORT_JOBS_REASON },
538
+ handler: async (_payload, ctx) => {
539
+ if (!ctx.registry) {
540
+ throw new Error("run-export-jobs: ctx.registry required (JobContext incomplete)");
540
541
  }
541
542
  const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
542
543
  // SYSTEM_USER_ID ist die framework-weite Konvention. Der job-
543
544
  // Discriminator wird via handlerName="user-data-rights:run-export-
544
545
  // jobs" im Secret-Read-Audit erfasst.
545
546
  const exportUserId = ctx._userId ?? SYSTEM_USER_ID;
546
- const exportDb = ctx.db as import("@cosmicdrift/kumiko-framework/db").DbConnection; // @cast-boundary db-operator
547
+ const exportDb = ctx.db.unsafeRaw(
548
+ RUN_EXPORT_JOBS_REASON,
549
+ ) as import("@cosmicdrift/kumiko-framework/db").DbConnection; // @cast-boundary db-operator — jobs never run inside a DbTx
547
550
  const exportRegistry = ctx.registry;
548
551
 
549
552
  // C6 — ohne eigene send*Email-Opts aber mit gemountetem mail-transport
@@ -609,25 +612,28 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
609
612
  }),
610
613
  });
611
614
  },
612
- );
615
+ });
613
616
 
614
617
  // Autonomer Art.17-Forget-Cron. Spiegelt run-export-jobs: nach Ablauf der
615
618
  // Grace-Period laeuft runForgetCleanup unbeaufsichtigt (der manuelle
616
619
  // userDataRights.runForget-API bleibt fuer Operator-Runs). Ohne diesen
617
620
  // Cron bleibt jeder Loesch-Antrag fuer immer in DeletionRequested haengen
618
621
  // — Art.17 wuerde nie ausgefuehrt.
619
- r.job(
620
- "run-forget-cleanup",
621
- { trigger: { cron: "0 3 * * *" }, concurrency: "skip" },
622
- async (_payload, ctx) => {
623
- if (!ctx.db || !ctx.registry) {
624
- throw new Error(
625
- "run-forget-cleanup: ctx.db + ctx.registry required (JobContext incomplete)",
626
- );
622
+ const RUN_FORGET_CLEANUP_REASON = "executes overdue Art.17 forget requests across every tenant";
623
+ r.job({
624
+ name: "run-forget-cleanup",
625
+ trigger: { cron: "0 3 * * *" },
626
+ concurrency: "skip",
627
+ escapeHatch: { reason: RUN_FORGET_CLEANUP_REASON },
628
+ handler: async (_payload, ctx) => {
629
+ if (!ctx.registry) {
630
+ throw new Error("run-forget-cleanup: ctx.registry required (JobContext incomplete)");
627
631
  }
628
632
  const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
629
633
  const forgetUserId = ctx._userId ?? SYSTEM_USER_ID;
630
- const forgetDb = ctx.db as import("@cosmicdrift/kumiko-framework/db").DbConnection; // @cast-boundary db-operator
634
+ const forgetDb = ctx.db.unsafeRaw(
635
+ RUN_FORGET_CLEANUP_REASON,
636
+ ) as import("@cosmicdrift/kumiko-framework/db").DbConnection; // @cast-boundary db-operator — jobs never run inside a DbTx
631
637
  const forgetRegistry = ctx.registry;
632
638
  const tenantModel = await resolveAppTenantModel({
633
639
  registry: forgetRegistry,
@@ -686,7 +692,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
686
692
  );
687
693
  }
688
694
  },
689
- );
695
+ });
690
696
  });
691
697
  }
692
698
 
@@ -24,7 +24,12 @@
24
24
 
25
25
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
26
26
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
27
- import { insertOne, selectMany } from "@cosmicdrift/kumiko-framework/db";
27
+ import {
28
+ createTenantDb,
29
+ createUncheckedSystemDb,
30
+ insertOne,
31
+ selectMany,
32
+ } from "@cosmicdrift/kumiko-framework/db";
28
33
  import {
29
34
  type AwaitedEventType,
30
35
  createSystemUser,
@@ -199,7 +204,8 @@ async function runResumeDueRunsJob(tenantId: string): Promise<void> {
199
204
 
200
205
  const systemUser = createSystemUser(tenantId);
201
206
  const ctx: JobContext = {
202
- db: stack.db,
207
+ db: createTenantDb(stack.db, tenantId),
208
+ systemDb: createUncheckedSystemDb(createTenantDb(stack.db, tenantId, "system")),
203
209
  registry: stack.registry,
204
210
  systemUser,
205
211
  log: noopLogger,
@@ -23,7 +23,12 @@
23
23
 
24
24
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
25
25
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
26
- import { insertOne, selectMany } from "@cosmicdrift/kumiko-framework/db";
26
+ import {
27
+ createTenantDb,
28
+ createUncheckedSystemDb,
29
+ insertOne,
30
+ selectMany,
31
+ } from "@cosmicdrift/kumiko-framework/db";
27
32
  import {
28
33
  createSystemUser,
29
34
  defineFeature,
@@ -177,7 +182,8 @@ async function runResumeDueRunsJob(): Promise<void> {
177
182
 
178
183
  const systemUser = createSystemUser(admin.tenantId);
179
184
  const ctx: JobContext = {
180
- db: stack.db,
185
+ db: createTenantDb(stack.db, admin.tenantId),
186
+ systemDb: createUncheckedSystemDb(createTenantDb(stack.db, admin.tenantId, "system")),
181
187
  registry: stack.registry,
182
188
  systemUser,
183
189
  log: noopLogger,
@@ -24,6 +24,7 @@
24
24
  //
25
25
  // wait/retry/waitForEvent suspensions all resume automatically now.
26
26
 
27
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
27
28
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
28
29
  import { selectDueWorkflowRunPending } from "./db/queries/due-runs";
29
30
  import { resumeRunHandler } from "./handlers/resume-run.write";
@@ -58,8 +59,8 @@ export const workflowRunnerFeature = defineFeature(FEATURE_NAME, (r) => {
58
59
  "resume-due-runs",
59
60
  { trigger: { cron: "* * * * *" }, perTenant: true, concurrency: "skip" },
60
61
  async (_payload, ctx) => {
61
- if (!ctx.db) {
62
- throw new Error("resume-due-runs: ctx.db required (JobContext incomplete)");
62
+ if (!ctx.systemDb) {
63
+ throw new Error("resume-due-runs: ctx.systemDb required (r.systemScope() feature)");
63
64
  }
64
65
  const tenantId = ctx.systemUser?.tenantId ?? ctx._tenantId;
65
66
  if (tenantId === undefined) {
@@ -67,7 +68,13 @@ export const workflowRunnerFeature = defineFeature(FEATURE_NAME, (r) => {
67
68
  return;
68
69
  }
69
70
 
70
- const dueRows = await selectDueWorkflowRunPending(ctx.db, tenantId);
71
+ ctx.systemDb.assertTenantMatch(tenantId);
72
+ const dueRows = await selectDueWorkflowRunPending(
73
+ ctx.systemDb.unsafeRaw(
74
+ "due-row pickup needs FOR UPDATE SKIP LOCKED raw SQL; the query filters by this job's tenantId",
75
+ ) as DbConnection, // @cast-boundary db-operator — DbRunner narrows to DbConnection, jobs never run inside a DbTx
76
+ tenantId,
77
+ );
71
78
 
72
79
  for (const row of dueRows) {
73
80
  try {