@cosmicdrift/kumiko-bundled-features 0.338.0 → 0.340.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 (166) hide show
  1. package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -2
  2. package/dist/auth-email-password/handlers/invite-accept.write.js +2 -4
  3. package/dist/auth-email-password/handlers/invite-info.query.js +3 -1
  4. package/dist/auth-email-password/handlers/invite-signup-complete.write.js +2 -4
  5. package/dist/auth-email-password/handlers/signup-confirm.write.js +1 -1
  6. package/dist/auth-email-password/handlers/signup-request.write.js +1 -1
  7. package/dist/auth-email-password/web/auth-gate.d.ts +12 -2
  8. package/dist/auth-email-password/web/auth-gate.js +25 -1
  9. package/dist/auth-email-password/web/auth-redirect.d.ts +5 -0
  10. package/dist/auth-email-password/web/auth-redirect.js +58 -0
  11. package/dist/auth-email-password/web/client-plugin.d.ts +8 -1
  12. package/dist/auth-email-password/web/client-plugin.js +2 -0
  13. package/dist/auth-email-password/web/index.d.ts +2 -1
  14. package/dist/auth-email-password/web/index.js +1 -0
  15. package/dist/auth-email-password/web/session.d.ts +3 -1
  16. package/dist/auth-email-password/web/session.js +8 -2
  17. package/dist/auth-mfa/feature.js +1 -1
  18. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
  19. package/dist/auth-mfa/handlers/enable-start-preauth.write.js +1 -1
  20. package/dist/auth-mfa/handlers/verify.write.js +1 -1
  21. package/dist/auth-mfa/mfa-code-verifier.js +1 -1
  22. package/dist/auth-mfa/mfa-status-checker.js +1 -1
  23. package/dist/billing-foundation/constants.d.ts +7 -0
  24. package/dist/billing-foundation/constants.js +8 -0
  25. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +3 -3
  26. package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +3 -3
  27. package/dist/billing-foundation/consumer-protection/consent-gate.js +10 -2
  28. package/dist/billing-foundation/consumer-protection/consent-text.js +2 -2
  29. package/dist/billing-foundation/consumer-protection/register-termination.d.ts +1 -1
  30. package/dist/billing-foundation/consumer-protection/register-termination.js +31 -3
  31. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +3 -1
  32. package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -1
  33. package/dist/billing-foundation/consumer-protection/termination-notify.d.ts +18 -0
  34. package/dist/billing-foundation/consumer-protection/termination-notify.js +21 -0
  35. package/dist/billing-foundation/consumer-protection/termination-pages.js +31 -2
  36. package/dist/billing-foundation/consumer-protection/termination-record.d.ts +24 -2
  37. package/dist/billing-foundation/consumer-protection/termination-record.js +102 -34
  38. package/dist/billing-foundation/consumer-protection/termination-request.js +10 -42
  39. package/dist/billing-foundation/consumer-protection/termination-texts.js +20 -20
  40. package/dist/billing-foundation/events.d.ts +19 -0
  41. package/dist/billing-foundation/events.js +12 -0
  42. package/dist/billing-foundation/handlers/create-checkout-session.write.js +2 -2
  43. package/dist/billing-foundation/handlers/start-plan-checkout.write.js +4 -2
  44. package/dist/billing-foundation/i18n.js +3 -0
  45. package/dist/billing-foundation/projection.js +23 -40
  46. package/dist/billing-foundation/tenant-destroy-hook.js +4 -4
  47. package/dist/billing-foundation/types.d.ts +6 -1
  48. package/dist/billing-foundation/validate-options.js +4 -0
  49. package/dist/channel-discord/discord-channel.d.ts +6 -0
  50. package/dist/channel-discord/discord-channel.js +19 -0
  51. package/dist/channel-discord/feature.d.ts +3 -0
  52. package/dist/channel-discord/feature.js +30 -0
  53. package/dist/channel-discord/index.d.ts +2 -0
  54. package/dist/channel-discord/index.js +2 -0
  55. package/dist/channel-slack/feature.d.ts +3 -0
  56. package/dist/channel-slack/feature.js +30 -0
  57. package/dist/channel-slack/index.d.ts +2 -0
  58. package/dist/channel-slack/index.js +2 -0
  59. package/dist/channel-slack/slack-channel.d.ts +5 -0
  60. package/dist/channel-slack/slack-channel.js +16 -0
  61. package/dist/channel-teams/feature.d.ts +3 -0
  62. package/dist/channel-teams/feature.js +34 -0
  63. package/dist/channel-teams/index.d.ts +2 -0
  64. package/dist/channel-teams/index.js +2 -0
  65. package/dist/channel-teams/teams-channel.d.ts +5 -0
  66. package/dist/channel-teams/teams-channel.js +38 -0
  67. package/dist/channel-telegram/feature.d.ts +3 -0
  68. package/dist/channel-telegram/feature.js +28 -0
  69. package/dist/channel-telegram/index.d.ts +2 -0
  70. package/dist/channel-telegram/index.js +2 -0
  71. package/dist/channel-telegram/telegram-channel.d.ts +8 -0
  72. package/dist/channel-telegram/telegram-channel.js +54 -0
  73. package/dist/crypto-shredding/handlers/forget-subject.write.js +6 -6
  74. package/dist/custom-fields/handlers/clear-custom-field.write.js +1 -1
  75. package/dist/custom-fields/handlers/define-tenant-field.write.js +1 -1
  76. package/dist/custom-fields/handlers/set-custom-field.write.js +1 -1
  77. package/dist/custom-fields/wire-user-data-rights.js +1 -1
  78. package/dist/data-retention/feature.js +1 -1
  79. package/dist/delivery/channel-context.d.ts +2 -1
  80. package/dist/delivery/channel-context.js +2 -2
  81. package/dist/delivery/chat-webhook-channel.d.ts +19 -0
  82. package/dist/delivery/chat-webhook-channel.js +48 -0
  83. package/dist/delivery/chat-webhook-sender.d.ts +21 -0
  84. package/dist/delivery/chat-webhook-sender.js +102 -0
  85. package/dist/delivery/delivery-service.js +3 -0
  86. package/dist/delivery/index.d.ts +2 -0
  87. package/dist/delivery/index.js +2 -0
  88. package/dist/delivery/jobs.js +1 -1
  89. package/dist/delivery/testing.d.ts +2 -0
  90. package/dist/delivery/types.d.ts +3 -1
  91. package/dist/delivery/types.js +1 -2
  92. package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
  93. package/dist/files-tenant-data/index.js +1 -1
  94. package/dist/form-draft/feature.js +1 -1
  95. package/dist/form-draft/handlers/discard.write.js +1 -1
  96. package/dist/form-draft/handlers/save.write.js +1 -1
  97. package/dist/inbound-mail-foundation/connect-routes.d.ts +7 -0
  98. package/dist/inbound-mail-foundation/connect-routes.js +13 -8
  99. package/dist/inbound-mail-foundation/feature.js +1 -1
  100. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +1 -1
  101. package/dist/inbound-mail-foundation/index.d.ts +1 -0
  102. package/dist/inbound-mail-foundation/index.js +1 -0
  103. package/dist/inbound-mail-foundation/oauth-access-token.d.ts +28 -0
  104. package/dist/inbound-mail-foundation/oauth-access-token.js +131 -0
  105. package/dist/inbound-mail-foundation/projection.js +37 -45
  106. package/dist/inbound-mail-foundation/tenant-destroy-hook.js +1 -1
  107. package/dist/inbound-mail-foundation/types.d.ts +18 -1
  108. package/dist/inbound-mail-foundation/watch-supervisor.d.ts +5 -0
  109. package/dist/inbound-mail-foundation/watch-supervisor.js +42 -14
  110. package/dist/managed-pages/handlers/set.write.js +1 -1
  111. package/dist/notes-history/handlers/add-note.write.js +1 -1
  112. package/dist/renderer-simple/feature.d.ts +8 -2
  113. package/dist/renderer-simple/feature.js +8 -5
  114. package/dist/renderer-simple/index.d.ts +2 -2
  115. package/dist/renderer-simple/index.js +1 -1
  116. package/dist/renderer-simple/simple-renderer.d.ts +15 -0
  117. package/dist/renderer-simple/simple-renderer.js +74 -27
  118. package/dist/secrets/feature.js +1 -1
  119. package/dist/sessions/feature.js +1 -1
  120. package/dist/sessions/handlers/revoke-all-for-user.write.js +1 -1
  121. package/dist/tenant-handover/handlers/claim.write.js +1 -2
  122. package/dist/tenant-lifecycle/feature.js +1 -1
  123. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +3 -1
  124. package/dist/tenant-lifecycle/handlers/request-destruction.write.js +1 -1
  125. package/dist/tier-engine/feature.js +1 -1
  126. package/dist/tier-engine/handlers/get-tenant-tier.query.js +1 -1
  127. package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -1
  128. package/dist/user/handlers/update-roles.js +7 -18
  129. package/dist/user-data-rights/feature.js +2 -2
  130. package/dist/user-data-rights/handlers/cancel-deletion.write.js +1 -1
  131. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
  132. package/dist/user-data-rights/handlers/download-by-job.query.js +1 -1
  133. package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
  134. package/dist/user-data-rights/handlers/export-status.query.js +1 -1
  135. package/dist/user-data-rights/handlers/lift-restriction.write.js +2 -4
  136. package/dist/user-data-rights/handlers/my-audit-log.query.js +1 -1
  137. package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +1 -1
  138. package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
  139. package/dist/user-data-rights/handlers/request-export.write.js +1 -1
  140. package/dist/user-data-rights/handlers/restrict-account.write.js +2 -2
  141. package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +1 -1
  142. package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
  143. package/dist/workflow-runner/db/queries/due-runs.d.ts +4 -5
  144. package/dist/workflow-runner/db/queries/due-runs.js +5 -4
  145. package/dist/workflow-runner/event-subscriber.js +6 -2
  146. package/dist/workflow-runner/event-trigger.js +5 -0
  147. package/dist/workflow-runner/feature.js +4 -6
  148. package/dist/workflow-runner/handlers/resume-run.write.js +147 -59
  149. package/dist/workflow-runner/pending-projection.js +1 -0
  150. package/dist/workflow-runner/runner.d.ts +7 -1
  151. package/dist/workflow-runner/runner.js +1 -1
  152. package/dist/workflow-runner/tables.js +8 -1
  153. package/package.json +41 -9
  154. package/src/auth-email-password/changes.json +7 -0
  155. package/src/billing-foundation/changes.json +41 -0
  156. package/src/delivery/changes.json +7 -0
  157. package/src/inbound-mail-foundation/changes.json +14 -0
  158. package/src/renderer-simple/changes.json +9 -1
  159. package/src/user/changes.json +13 -6
  160. package/src/workflow-runner/changes.json +14 -0
  161. package/dist/billing-foundation/db/queries/payment-projection.d.ts +0 -2
  162. package/dist/billing-foundation/db/queries/payment-projection.js +0 -13
  163. package/dist/billing-foundation/db/queries/subscription-projection.d.ts +0 -2
  164. package/dist/billing-foundation/db/queries/subscription-projection.js +0 -7
  165. package/dist/inbound-mail-foundation/db/queries/inbound-projections.d.ts +0 -7
  166. package/dist/inbound-mail-foundation/db/queries/inbound-projections.js +0 -24
@@ -20,7 +20,9 @@ export const cancelDestructionWrite = defineWriteHandler({
20
20
  },
21
21
  handler: async (event, ctx) => {
22
22
  const tenantId = event.user.tenantId;
23
- const row = await fetchOne(ctx.db.unsafeRaw(CANCEL_DESTRUCTION_TENANT_ROW_REASON), tenantTable, { id: tenantId });
23
+ const row = await fetchOne(ctx.db.unsafeRaw(), tenantTable, {
24
+ id: tenantId,
25
+ });
24
26
  if (!row) {
25
27
  return writeFailure(new UnprocessableError("tenant_not_found", { details: { tenantId } }));
26
28
  }
@@ -24,7 +24,7 @@ export const requestDestructionWrite = defineWriteHandler({
24
24
  },
25
25
  handler: async (event, ctx) => {
26
26
  const tenantId = event.user.tenantId;
27
- const runner = ctx.db.unsafeRaw(REQUEST_DESTRUCTION_TENANT_ROW_REASON);
27
+ const runner = ctx.db.unsafeRaw();
28
28
  const row = await fetchOne(runner, tenantTable, { id: tenantId });
29
29
  if (!row) {
30
30
  return writeFailure(new UnprocessableError("tenant_not_found", { details: { tenantId } }));
@@ -303,7 +303,7 @@ export function createTierEngineFeature(opts = {}) {
303
303
  const rawDb = ctx.systemDb
304
304
  ? ctx.systemDb.unsafeRaw(autoDefaultTierHookReason)
305
305
  : ctx.db && "unsafeRaw" in ctx.db
306
- ? ctx.db.unsafeRaw(autoDefaultTierHookReason)
306
+ ? ctx.db.unsafeRaw()
307
307
  : undefined;
308
308
  // skip: defensive — inTransaction phase always sets db, but AppContext's
309
309
  // type makes it optional. Throwing would be overreach (lifecycle
@@ -20,7 +20,7 @@ export const getTenantTierQuery = defineQueryHandler({
20
20
  },
21
21
  handler: async (query, ctx) => {
22
22
  const tenantId = query.payload.tenantId; // @cast-boundary engine-bridge
23
- const tdb = createTenantDb(ctx.db.unsafeRaw(GET_TENANT_TIER_REASON), tenantId, "system");
23
+ const tdb = createTenantDb(ctx.db.unsafeRaw(), tenantId, "system");
24
24
  const row = await fetchOne(tdb, tierAssignmentTable, { tenantId });
25
25
  return row ?? null;
26
26
  },
@@ -51,7 +51,7 @@ export function createSetTenantTierWrite(opts = {}) {
51
51
  },
52
52
  handler: async (event, ctx) => {
53
53
  const tenantId = event.payload.tenantId; // @cast-boundary engine-bridge
54
- const rawDb = ctx.db.unsafeRaw(SET_TENANT_TIER_REASON);
54
+ const rawDb = ctx.db.unsafeRaw();
55
55
  const tdb = createTenantDb(rawDb, tenantId, "system");
56
56
  const systemUser = { ...event.user, tenantId };
57
57
  const tier = event.payload.tier;
@@ -1,4 +1,4 @@
1
- import { asEntityTableMeta, asRawClient, fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
1
+ import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { acquireNamespacedAdvisoryLock, createEventStoreExecutor, } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { createSystemUser, findForbiddenRoleAssignment, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import { AccessDeniedError, ConflictError, InternalError, NotFoundError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
@@ -10,9 +10,6 @@ const REVOKE_ALL_SESSIONS_QN = "sessions:write:user-session:revoke-all-for-user"
10
10
  // Serializes last-SystemAdmin demotion checks globally inside the write TX
11
11
  // (dispatcher batch wraps handlers in transaction — xact lock holds through update).
12
12
  const LAST_SYSTEM_ADMIN_LOCK_NAMESPACE = 0x7361646d; // 'sadm'
13
- function quoteIdent(name) {
14
- return `"${name.replace(/"/g, '""')}"`;
15
- }
16
13
  function isActiveUserRow(row) {
17
14
  if (row.isDeleted === true)
18
15
  return false;
@@ -22,19 +19,11 @@ function isActiveSystemAdminRow(row) {
22
19
  return isActiveUserRow(row) && parseRoles(row.roles).includes("SystemAdmin");
23
20
  }
24
21
  async function countOtherActiveSystemAdmins(db, excludeUserId) {
25
- const tableName = asEntityTableMeta(userTable)?.tableName;
26
- if (!tableName) {
27
- throw new InternalError({ message: "user read table meta missing" });
28
- }
29
- // kumiko-lint-ignore raw-sql jsonb @> prefilter for SystemAdmin roster under advisory lock
30
- const rows = (await asRawClient(db).unsafe(`SELECT id, roles, status, is_deleted AS "isDeleted"
31
- FROM ${quoteIdent(tableName)}
32
- WHERE is_deleted = false
33
- AND (status = $1 OR status IS NULL)
34
- AND roles @> $2::jsonb`,
35
- // Pass a JS array — JSON.stringify + $n::jsonb double-encodes to a jsonb string
36
- // (postgres.js), so `@>` never matches a roles array.
37
- [USER_STATUS.Active, ["SystemAdmin"]]));
22
+ // `roles` is a jsonb multiSelect column: a scalar filter value means `@>` containment.
23
+ const rows = await selectMany(db, userTable, {
24
+ isDeleted: false,
25
+ roles: "SystemAdmin",
26
+ });
38
27
  return rows.filter((u) => u.id !== excludeUserId && isActiveSystemAdminRow(u)).length;
39
28
  }
40
29
  export async function applyUserRolesUpdate(event, ctx, db) {
@@ -68,7 +57,7 @@ export async function applyUserRolesUpdate(event, ctx, db) {
68
57
  // Lock before count+update so two concurrent demotions cannot both
69
58
  // observe otherActiveSystemAdmins >= 1 and leave zero active SystemAdmins.
70
59
  await acquireNamespacedAdvisoryLock(lockRunner, LAST_SYSTEM_ADMIN_LOCK_NAMESPACE, "global");
71
- const otherActiveSystemAdmins = await countOtherActiveSystemAdmins(ctx.systemDb.unsafeRaw("jsonb @> prefilter over the global users table to count remaining active SystemAdmins"), event.payload.id);
60
+ const otherActiveSystemAdmins = await countOtherActiveSystemAdmins(ctx.systemDb.unsafeRaw("count remaining active SystemAdmins across the global users table"), event.payload.id);
72
61
  if (otherActiveSystemAdmins === 0) {
73
62
  return writeFailure(new ConflictError({
74
63
  message: "cannot demote the last active SystemAdmin",
@@ -411,7 +411,7 @@ export function createUserDataRightsFeature(opts = {}) {
411
411
  // Discriminator wird via handlerName="user-data-rights:run-export-
412
412
  // jobs" im Secret-Read-Audit erfasst.
413
413
  const exportUserId = ctx._userId ?? SYSTEM_USER_ID;
414
- const exportDb = ctx.db.unsafeRaw(RUN_EXPORT_JOBS_REASON); // @cast-boundary db-operator — jobs never run inside a DbTx
414
+ const exportDb = ctx.db.unsafeRaw(); // @cast-boundary db-operator — jobs never run inside a DbTx
415
415
  const exportRegistry = ctx.registry;
416
416
  // JobContext carries ctx.meter (raw), not ctx.metrics — that bound
417
417
  // handle only exists on HandlerContext (built per write/query call
@@ -496,7 +496,7 @@ export function createUserDataRightsFeature(opts = {}) {
496
496
  }
497
497
  const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
498
498
  const forgetUserId = ctx._userId ?? SYSTEM_USER_ID;
499
- const forgetDb = ctx.db.unsafeRaw(RUN_FORGET_CLEANUP_REASON); // @cast-boundary db-operator — jobs never run inside a DbTx
499
+ const forgetDb = ctx.db.unsafeRaw(); // @cast-boundary db-operator — jobs never run inside a DbTx
500
500
  const forgetRegistry = ctx.registry;
501
501
  const tenantModel = await resolveAppTenantModel({
502
502
  registry: forgetRegistry,
@@ -45,7 +45,7 @@ export const cancelDeletionWrite = defineWriteHandler({
45
45
  if (!isWithinGracePeriod(row.gracePeriodEnd)) {
46
46
  return writeFailure(new UnprocessableError("grace_period_expired"));
47
47
  }
48
- await updateUserLifecycle(ctx.db.unsafeRaw(APPEND_LIFECYCLE_EVENT_REASON), event.user.id, {
48
+ await updateUserLifecycle(ctx.db.unsafeRaw(), event.user.id, {
49
49
  status: USER_STATUS.Active,
50
50
  gracePeriodEnd: null,
51
51
  // #354/1: closes the replay-after-cancel window — a still-TTL-valid email
@@ -68,7 +68,7 @@ export function createConfirmDeletionByTokenHandler(opts = {}) {
68
68
  secret: opts.deletionTokenSecret,
69
69
  loadPendingRequestId: (userId) => readPendingDeletionRequestId(ctx, userId),
70
70
  commitDeletion: async (userId, requestId) => {
71
- const res = await startDeletionGracePeriod(ctx, userId, () => resolveGracePeriod(ctx, event.user.tenantId), ctx.db.unsafeRaw("appends the user lifecycle event on the SYSTEM_TENANT_ID user stream"), { pendingDeletionRequestId: requestId });
71
+ const res = await startDeletionGracePeriod(ctx, userId, () => resolveGracePeriod(ctx, event.user.tenantId), ctx.db.unsafeRaw(), { pendingDeletionRequestId: requestId });
72
72
  if (!res.ok)
73
73
  return false;
74
74
  gracePeriodEndIso = res.gracePeriodEnd.toString();
@@ -61,7 +61,7 @@ export const downloadByJobQuery = defineQueryHandler({
61
61
  // Wert (603/2).
62
62
  const auditIp = requestContext.get()?.ip ?? null;
63
63
  const auditUa = requestContext.get()?.userAgent ?? null;
64
- const runner = ctx.db.unsafeRaw(DOWNLOAD_BY_JOB_REASON);
64
+ const runner = ctx.db.unsafeRaw();
65
65
  // Step 1-2: job-lookup + cross-user-isolation
66
66
  const jobRow = await fetchOne(runner, exportJobsTable, { id: jobId });
67
67
  if (!jobRow || jobRow.userId !== userId) {
@@ -64,7 +64,7 @@ export const downloadByTokenQuery = defineQueryHandler({
64
64
  handler: async (query, ctx) => {
65
65
  const T = getTemporal();
66
66
  const now = T.Now.instant();
67
- const runner = ctx.db.unsafeRaw(DOWNLOAD_BY_TOKEN_REASON);
67
+ const runner = ctx.db.unsafeRaw();
68
68
  // Step 1: hash + lookup
69
69
  const hash = await hashDownloadToken(query.payload.token);
70
70
  const tokenRow = await fetchOne(runner, exportDownloadTokensTable, {
@@ -30,7 +30,7 @@ export const exportStatusQuery = defineQueryHandler({
30
30
  reason: EXPORT_STATUS_REASON,
31
31
  },
32
32
  handler: async (query, ctx) => {
33
- const rows = await selectMany(ctx.db.unsafeRaw(EXPORT_STATUS_REASON), exportJobsTable, { userId: query.user.id }, { limit: 1, orderBy: { col: "requestedAt", direction: "desc" } });
33
+ const rows = await selectMany(ctx.db.unsafeRaw(), exportJobsTable, { userId: query.user.id }, { limit: 1, orderBy: { col: "requestedAt", direction: "desc" } });
34
34
  const latest = rows[0];
35
35
  if (!latest)
36
36
  return { hasJob: false };
@@ -5,8 +5,6 @@ import { USER_STATUS, userTable } from "../../user/index.js";
5
5
  import { denyIfTargetOutsideAdminTenant } from "../lib/deny-if-target-outside-admin-tenant.js";
6
6
  import { updateUserLifecycle } from "../lib/update-user-lifecycle.js";
7
7
  const LIFT_RESTRICTION_ESCAPE_HATCH_REASON = "checks the target user's membership in the admin's tenant and appends the user lifecycle status change on the SYSTEM_TENANT_ID user stream, both via DbRunner helpers outside the admin's own tenant scope.";
8
- const CHECK_TARGET_MEMBERSHIP_REASON = "checks the target user's membership in the admin's tenant via the DbRunner helper";
9
- const APPEND_LIFECYCLE_EVENT_REASON = "appends the user lifecycle event on the SYSTEM_TENANT_ID user stream";
10
8
  // POST /api/user/lift-restriction (S2.U6) — DSGVO Art. 18 Reverse.
11
9
  //
12
10
  // Operator-only. A Restricted user's own session is unconditionally
@@ -38,7 +36,7 @@ export const liftRestrictionWrite = defineWriteHandler({
38
36
  },
39
37
  handler: async (event, ctx) => {
40
38
  const targetUserId = event.payload.userId;
41
- const outside = await denyIfTargetOutsideAdminTenant(ctx.db.unsafeRaw(CHECK_TARGET_MEMBERSHIP_REASON), event.user, targetUserId);
39
+ const outside = await denyIfTargetOutsideAdminTenant(ctx.db.unsafeRaw(), event.user, targetUserId);
42
40
  if (outside)
43
41
  return outside;
44
42
  const userRow = await ctx.db.global(userTable).fetchOne({
@@ -55,7 +53,7 @@ export const liftRestrictionWrite = defineWriteHandler({
55
53
  details: { currentStatus },
56
54
  }));
57
55
  }
58
- await updateUserLifecycle(ctx.db.unsafeRaw(APPEND_LIFECYCLE_EVENT_REASON), targetUserId, {
56
+ await updateUserLifecycle(ctx.db.unsafeRaw(), targetUserId, {
59
57
  status: USER_STATUS.Active,
60
58
  });
61
59
  return {
@@ -53,7 +53,7 @@ export const myAuditLogQuery = defineQueryHandler({
53
53
  }
54
54
  if (p.before)
55
55
  where["id"] = { lt: BigInt(p.before) };
56
- const rows = await selectMany(ctx.db.unsafeRaw(MY_AUDIT_LOG_REASON), eventsTable, where, {
56
+ const rows = await selectMany(ctx.db.unsafeRaw(), eventsTable, where, {
57
57
  orderBy: { col: "id", direction: "desc" },
58
58
  limit: p.limit,
59
59
  });
@@ -57,7 +57,7 @@ export function createRequestDeletionByEmailHandler(opts = {}) {
57
57
  // user-Row landet und in die Token-HMAC-Purpose gefaltet wird. cancel
58
58
  // nullt sie → ein nach Cancel nachgespieltes Token verifiziert nicht mehr.
59
59
  const requestId = crypto.randomUUID();
60
- await updateUserLifecycle(ctx.db.unsafeRaw(APPEND_LIFECYCLE_EVENT_REASON), userRow["id"], {
60
+ await updateUserLifecycle(ctx.db.unsafeRaw(), userRow["id"], {
61
61
  pendingDeletionRequestId: requestId,
62
62
  });
63
63
  const { token, expiresAt } = signDeletionToken(userRow["id"], requestId, DELETION_VERIFY_TTL_MINUTES, opts.deletionTokenSecret);
@@ -29,7 +29,7 @@ export function createRequestDeletionHandler(opts = {}) {
29
29
  description: "Starts the GDPR Art. 17 deletion of the calling user's own account by arming the grace period from the tenant compliance profile and mailing a confirmation, after which only cancel-deletion can stop the erasure.",
30
30
  agent: { risk: "high" },
31
31
  handler: async (event, ctx) => {
32
- const res = await startDeletionGracePeriod(ctx, event.user.id, () => resolveGracePeriod(ctx, event.user.tenantId), ctx.db.unsafeRaw("appends the user lifecycle event on the SYSTEM_TENANT_ID user stream"));
32
+ const res = await startDeletionGracePeriod(ctx, event.user.id, () => resolveGracePeriod(ctx, event.user.tenantId), ctx.db.unsafeRaw());
33
33
  if (!res.ok)
34
34
  return writeFailure(res.error);
35
35
  const { gracePeriodEnd, userEmail, userLocale } = res;
@@ -58,7 +58,7 @@ export const requestExportWrite = defineWriteHandler({
58
58
  const userId = event.user.id;
59
59
  const T = getTemporal();
60
60
  const now = T.Now.instant();
61
- const exportJobRunner = ctx.db.unsafeRaw(REQUEST_EXPORT_REASON);
61
+ const exportJobRunner = ctx.db.unsafeRaw();
62
62
  // Pre-Check: exportJobRunner bypasses the TenantDb-Filter — the
63
63
  // TenantDb-Wrapper would hide cross-tenant jobs.
64
64
  const existing = await findActiveJob(exportJobRunner, userId);
@@ -59,7 +59,7 @@ export const restrictAccountWrite = defineWriteHandler({
59
59
  details: { reason: "admin_required_for_other_user" },
60
60
  }));
61
61
  }
62
- const outside = await denyIfTargetOutsideAdminTenant(ctx.db.unsafeRaw("checks the target user's membership in the admin's tenant via the DbRunner helper"), event.user, targetUserId);
62
+ const outside = await denyIfTargetOutsideAdminTenant(ctx.db.unsafeRaw(), event.user, targetUserId);
63
63
  if (outside)
64
64
  return outside;
65
65
  }
@@ -82,7 +82,7 @@ export const restrictAccountWrite = defineWriteHandler({
82
82
  details: { currentStatus },
83
83
  }));
84
84
  }
85
- await updateUserLifecycle(ctx.db.unsafeRaw("appends the user lifecycle event on the SYSTEM_TENANT_ID user stream"), targetUserId, { status: USER_STATUS.Restricted });
85
+ await updateUserLifecycle(ctx.db.unsafeRaw(), targetUserId, { status: USER_STATUS.Restricted });
86
86
  // Cross-Feature: alle live sessions revoken — sonst koennte der User
87
87
  // mit existierendem JWT bis zur Token-Expiry weiter schreiben.
88
88
  // ctx.writeAs(systemUser, ...) damit der privileged-Handler die
@@ -36,7 +36,7 @@ export function createRunForgetCleanupHandler(opts = {}) {
36
36
  }));
37
37
  }
38
38
  const T = getTemporal();
39
- const runner = ctx.db.unsafeRaw(RUN_FORGET_CLEANUP_REASON);
39
+ const runner = ctx.db.unsafeRaw();
40
40
  // Operator-triggered forget must also erase binaries, not just rows —
41
41
  // it flips users to Deleted, after which the cron never re-processes
42
42
  // them, so a row-only delete here would permanently leak the binaries.
@@ -18,7 +18,7 @@ import { USER_STATUS, userEntity, userTable } from "../../user/index.js";
18
18
  // NICHT `event.user.tenantId` (das ist der aktive Tenant zur Lifecycle-Zeit und
19
19
  // kann abweichen).
20
20
  const userExecutor = createEventStoreExecutor(userTable, userEntity, { entityName: "user" });
21
- // `conn` is ctx.db.unsafeRaw(reason) (regular handlers) or the open tx
21
+ // `conn` is ctx.db.unsafeRaw() (regular handlers) or the open tx
22
22
  // (forget-cleanup sub-tx) — keeps the event append atomic with the write.
23
23
  //
24
24
  // Returns `{ applied: false }` instead of throwing only when `options.expect`
@@ -1,8 +1,7 @@
1
- import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
- import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { type TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
2
  export type DueRow = {
4
- readonly run_id: string;
5
- readonly step_index: number;
3
+ readonly runId: string;
4
+ readonly stepIndex: number;
6
5
  };
7
6
  export declare const DUE_RUNS_BATCH_SIZE = 100;
8
- export declare function selectDueWorkflowRunPending(db: DbConnection, tenantId: TenantId): Promise<readonly DueRow[]>;
7
+ export declare function selectDueWorkflowRunPending(db: TenantDb): Promise<readonly DueRow[]>;
@@ -1,4 +1,5 @@
1
- import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
1
+ import { sql } from "@cosmicdrift/kumiko-framework/db";
2
+ import { workflowRunPendingTable } from "../../tables.js";
2
3
  // Oldest first and capped, so a backlog after an outage drains over several
3
4
  // ticks instead of holding one tick's `concurrency: "skip"` slot for minutes.
4
5
  export const DUE_RUNS_BATCH_SIZE = 100;
@@ -6,7 +7,7 @@ export const DUE_RUNS_BATCH_SIZE = 100;
6
7
  // (ctx.tryAppendEvent on WORKFLOW_RESUMED) — a second dispatch for the same
7
8
  // row loses that race and no-ops. No row locks: in an autocommit statement
8
9
  // they would be released right after the SELECT and guard nothing.
9
- export async function selectDueWorkflowRunPending(db, tenantId) {
10
- // kumiko-lint-ignore raw-sql tenant-scoped due-row pickup
11
- return (await asRawClient(db).unsafe(`SELECT run_id, step_index FROM workflow_run_pending WHERE tenant_id = $1 AND wake_at < now() ORDER BY wake_at ASC LIMIT ${DUE_RUNS_BATCH_SIZE}`, [tenantId]));
10
+ // `now()` stays the DB clock so job-host clock skew cannot mark rows due early.
11
+ export function selectDueWorkflowRunPending(db) {
12
+ return db.selectMany(workflowRunPendingTable, { wakeAt: { lt: sql `now()` } }, { orderBy: { col: "wakeAt", direction: "asc" }, limit: DUE_RUNS_BATCH_SIZE });
12
13
  }
@@ -20,7 +20,7 @@
20
20
  // This subscriber never resumes a run itself (D3, workflow-resume-loop.md):
21
21
  // the MultiStreamApplyContext it runs in has no callFeature/runStepList
22
22
  // access, only unsafeAppendEvent/loadAggregate. It writes triggerEventType
23
- // + triggerPayload + wakeAt=now() onto the matching pending row(s); the
23
+ // + triggerEventRef + wakeAt=now() onto the matching pending row(s); the
24
24
  // existing resume-due-runs job (Phase 2) picks the row up on its next
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";
@@ -66,7 +66,11 @@ const wakeupApply = async (event, tx) => {
66
66
  continue;
67
67
  await updateMany(tx, workflowRunPendingTable, {
68
68
  triggerEventType: event.type,
69
- triggerPayload: event.payload,
69
+ triggerEventRef: {
70
+ eventId: event.id,
71
+ aggregateId: event.aggregateId,
72
+ version: event.version,
73
+ },
70
74
  wakeAt: getTemporal().Now.instant().toString(),
71
75
  }, { tenantId: row.tenantId, runId: row.runId, stepIndex: row.stepIndex });
72
76
  }
@@ -69,6 +69,11 @@ export function registerEventTrigger(r, workflow) {
69
69
  runId,
70
70
  workflow,
71
71
  triggerEvent,
72
+ triggerEventRef: {
73
+ eventId: event.id,
74
+ aggregateId: event.aggregateId,
75
+ version: event.version,
76
+ },
72
77
  ...(idempotencyKey && { idempotencyKey }),
73
78
  // @cast-boundary msp-to-handler-ctx — MultiStreamApplyContext only
74
79
  // exposes unsafeAppendEvent/loadAggregate, a subset of
@@ -57,21 +57,19 @@ export const workflowRunnerFeature = defineFeature(FEATURE_NAME, (r) => {
57
57
  // skip: cron fired without a perTenant fan-out tenant — nothing scoped
58
58
  return;
59
59
  }
60
- ctx.systemDb.assertTenantMatch(tenantId);
61
- const dueRows = await selectDueWorkflowRunPending(ctx.systemDb.unsafeRaw("due-row pickup needs FOR UPDATE SKIP LOCKED raw SQL; the query filters by this job's tenantId"), // @cast-boundary db-operator — DbRunner narrows to DbConnection, jobs never run inside a DbTx
62
- tenantId);
60
+ const dueRows = await selectDueWorkflowRunPending(ctx.systemDb.assertTenantMatch(tenantId));
63
61
  for (const row of dueRows) {
64
62
  try {
65
63
  await ctx.write("workflow-runner:write:resume-run", {
66
- runId: row.run_id,
67
- stepIndex: row.step_index,
64
+ runId: row.runId,
65
+ stepIndex: row.stepIndex,
68
66
  });
69
67
  }
70
68
  catch (error) {
71
69
  // skip: one row's dispatch failing (infra hiccup, not a business
72
70
  // outcome — resume-run itself never throws for expected cases)
73
71
  // must not stop the rest of this tenant's due rows from resuming.
74
- ctx.log.warn(`[workflow-runner:resume-due-runs] runId=${row.run_id} stepIndex=${row.step_index} dispatch failed: ${String(error)}`);
72
+ ctx.log.warn(`[workflow-runner:resume-due-runs] runId=${row.runId} stepIndex=${row.stepIndex} dispatch failed: ${String(error)}`);
75
73
  }
76
74
  }
77
75
  });