@cosmicdrift/kumiko-samples 0.338.0 → 0.339.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 (138) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +21 -1
  3. package/packages/bundled-features/src/auth-email-password/changes.json +7 -0
  4. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -6
  5. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +2 -6
  6. package/packages/bundled-features/src/auth-email-password/handlers/invite-info.query.ts +3 -5
  7. package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +2 -8
  8. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
  9. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +1 -1
  10. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +35 -3
  11. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +60 -0
  12. package/packages/bundled-features/src/auth-email-password/web/client-plugin.ts +10 -0
  13. package/packages/bundled-features/src/auth-email-password/web/index.ts +8 -0
  14. package/packages/bundled-features/src/auth-email-password/web/session.tsx +15 -2
  15. package/packages/bundled-features/src/auth-mfa/feature.ts +1 -1
  16. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -7
  17. package/packages/bundled-features/src/auth-mfa/handlers/enable-start-preauth.write.ts +1 -5
  18. package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +1 -7
  19. package/packages/bundled-features/src/auth-mfa/mfa-code-verifier.ts +1 -5
  20. package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +1 -7
  21. package/packages/bundled-features/src/billing-foundation/changes.json +34 -0
  22. package/packages/bundled-features/src/billing-foundation/constants.ts +11 -0
  23. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +3 -3
  24. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +11 -3
  25. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-text.ts +2 -2
  26. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +48 -2
  27. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +10 -3
  28. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +44 -0
  29. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +32 -2
  30. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +129 -33
  31. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +13 -59
  32. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +20 -21
  33. package/packages/bundled-features/src/billing-foundation/events.ts +18 -0
  34. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +4 -5
  35. package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +6 -5
  36. package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
  37. package/packages/bundled-features/src/billing-foundation/tenant-destroy-hook.ts +4 -4
  38. package/packages/bundled-features/src/billing-foundation/types.ts +6 -0
  39. package/packages/bundled-features/src/billing-foundation/validate-options.ts +6 -0
  40. package/packages/bundled-features/src/channel-discord/discord-channel.ts +32 -0
  41. package/packages/bundled-features/src/channel-discord/feature.ts +40 -0
  42. package/packages/bundled-features/src/channel-discord/index.ts +2 -0
  43. package/packages/bundled-features/src/channel-slack/feature.ts +38 -0
  44. package/packages/bundled-features/src/channel-slack/index.ts +2 -0
  45. package/packages/bundled-features/src/channel-slack/slack-channel.ts +29 -0
  46. package/packages/bundled-features/src/channel-teams/feature.ts +42 -0
  47. package/packages/bundled-features/src/channel-teams/index.ts +2 -0
  48. package/packages/bundled-features/src/channel-teams/teams-channel.ts +50 -0
  49. package/packages/bundled-features/src/channel-telegram/feature.ts +35 -0
  50. package/packages/bundled-features/src/channel-telegram/index.ts +2 -0
  51. package/packages/bundled-features/src/channel-telegram/telegram-channel.ts +74 -0
  52. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +6 -18
  53. package/packages/bundled-features/src/custom-fields/handlers/clear-custom-field.write.ts +1 -1
  54. package/packages/bundled-features/src/custom-fields/handlers/define-tenant-field.write.ts +1 -4
  55. package/packages/bundled-features/src/custom-fields/handlers/set-custom-field.write.ts +1 -1
  56. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +1 -1
  57. package/packages/bundled-features/src/data-retention/feature.ts +2 -3
  58. package/packages/bundled-features/src/delivery/changes.json +7 -0
  59. package/packages/bundled-features/src/delivery/channel-context.ts +3 -1
  60. package/packages/bundled-features/src/delivery/chat-webhook-channel.ts +76 -0
  61. package/packages/bundled-features/src/delivery/chat-webhook-sender.ts +135 -0
  62. package/packages/bundled-features/src/delivery/delivery-service.ts +3 -0
  63. package/packages/bundled-features/src/delivery/index.ts +15 -0
  64. package/packages/bundled-features/src/delivery/jobs.ts +1 -1
  65. package/packages/bundled-features/src/delivery/testing.ts +2 -0
  66. package/packages/bundled-features/src/delivery/types.ts +8 -3
  67. package/packages/bundled-features/src/file-derivatives/handlers/public-variant-by-file-ref.query.ts +1 -1
  68. package/packages/bundled-features/src/files-tenant-data/index.ts +1 -1
  69. package/packages/bundled-features/src/form-draft/feature.ts +1 -1
  70. package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
  71. package/packages/bundled-features/src/form-draft/handlers/save.write.ts +1 -5
  72. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +7 -0
  73. package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +29 -13
  74. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
  75. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -1
  76. package/packages/bundled-features/src/inbound-mail-foundation/index.ts +7 -0
  77. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +201 -0
  78. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +1 -1
  79. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +17 -1
  80. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +74 -36
  81. package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +1 -1
  82. package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +7 -10
  83. package/packages/bundled-features/src/renderer-simple/changes.json +9 -1
  84. package/packages/bundled-features/src/renderer-simple/feature.ts +14 -4
  85. package/packages/bundled-features/src/renderer-simple/index.ts +2 -2
  86. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +94 -31
  87. package/packages/bundled-features/src/secrets/feature.ts +1 -1
  88. package/packages/bundled-features/src/sessions/feature.ts +1 -1
  89. package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
  90. package/packages/bundled-features/src/tenant-handover/handlers/claim.write.ts +1 -4
  91. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +1 -3
  92. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +3 -5
  93. package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -1
  94. package/packages/bundled-features/src/tier-engine/feature.ts +1 -1
  95. package/packages/bundled-features/src/tier-engine/handlers/get-tenant-tier.query.ts +1 -1
  96. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +1 -1
  97. package/packages/bundled-features/src/user-data-rights/feature.ts +4 -6
  98. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +1 -1
  99. package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +1 -3
  100. package/packages/bundled-features/src/user-data-rights/handlers/download-by-job.query.ts +1 -1
  101. package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
  102. package/packages/bundled-features/src/user-data-rights/handlers/export-status.query.ts +1 -1
  103. package/packages/bundled-features/src/user-data-rights/handlers/lift-restriction.write.ts +2 -6
  104. package/packages/bundled-features/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
  105. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion-by-email.write.ts +1 -1
  106. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
  107. package/packages/bundled-features/src/user-data-rights/handlers/request-export.write.ts +1 -1
  108. package/packages/bundled-features/src/user-data-rights/handlers/restrict-account.write.ts +2 -8
  109. package/packages/bundled-features/src/user-data-rights/handlers/run-forget-cleanup.write.ts +1 -1
  110. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +1 -1
  111. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  112. package/packages/bundled-features/src/user-data-rights/run-user-export.ts +1 -1
  113. package/packages/bundled-features/src/workflow-runner/changes.json +7 -0
  114. package/packages/bundled-features/src/workflow-runner/event-subscriber.ts +7 -2
  115. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +5 -0
  116. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +231 -77
  117. package/packages/bundled-features/src/workflow-runner/pending-projection.ts +1 -0
  118. package/packages/bundled-features/src/workflow-runner/runner.ts +12 -2
  119. package/packages/bundled-features/src/workflow-runner/tables.ts +8 -1
  120. package/samples/apps/styleguide/src/features/widgets/i18n.ts +5 -0
  121. package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +11 -0
  122. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +16 -0
  123. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +152 -0
  124. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +82 -0
  125. package/samples/apps/use-all-bundled/feature-manifest.json +146 -1
  126. package/samples/apps/use-all-bundled/package.json +1 -0
  127. package/samples/apps/use-all-bundled/playwright.billing-gallery.config.ts +14 -0
  128. package/samples/apps/use-all-bundled/schema/generate.ts +20 -0
  129. package/samples/apps/use-all-bundled/scripts/gen-feature-manifest.ts +1 -0
  130. package/samples/apps/use-all-bundled/src/run-config.ts +10 -0
  131. package/samples/apps/use-all-bundled/tsconfig.json +1 -0
  132. package/samples/recipes/chat-channels/README.md +19 -0
  133. package/samples/recipes/chat-channels/package.json +9 -0
  134. package/samples/recipes/chat-channels/src/feature.ts +40 -0
  135. package/samples/recipes/public-share-token/src/feature.ts +1 -1
  136. package/samples/recipes/workflow-engine/src/event-trigger.ts +5 -0
  137. package/samples/recipes/workflow-engine/src/postgres-resume-loop.ts +50 -9
  138. package/samples/recipes/workflow-engine/src/workflow-runner.ts +12 -2
@@ -1,6 +1,62 @@
1
- import { escapeHtml } from "@cosmicdrift/kumiko-headless";
1
+ import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
2
2
  import type { NotificationRenderer } from "../delivery/index.js";
3
3
 
4
+ export type MailBranding = {
5
+ /** Shown as text when there is no logo, and as the logo alt text. */
6
+ readonly productName?: string;
7
+ /** Hex color (#rgb, #rrggbb, #rrggbbaa); used for buttons and the header accent. */
8
+ readonly primaryColor?: string;
9
+ /** Absolute http(s) URL; anything else is dropped. */
10
+ readonly logoUrl?: string;
11
+ readonly footerText?: string;
12
+ /** Absolute http(s) URLs only; other entries are dropped. */
13
+ readonly footerLinks?: readonly { readonly label: string; readonly url: string }[];
14
+ };
15
+
16
+ const DEFAULT_PRIMARY_COLOR = "#2563eb";
17
+ const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
18
+
19
+ function isHttpUrl(value: string): boolean {
20
+ try {
21
+ const { protocol } = new URL(value);
22
+ return protocol === "http:" || protocol === "https:";
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ function resolvePrimaryColor(branding: MailBranding | undefined): string {
29
+ const color = branding?.primaryColor;
30
+ return color && HEX_COLOR_PATTERN.test(color) ? color : DEFAULT_PRIMARY_COLOR;
31
+ }
32
+
33
+ function renderBrandingHeader(branding: MailBranding | undefined, primaryColor: string): string {
34
+ if (!branding) return "";
35
+ const logoUrl = branding.logoUrl && isHttpUrl(branding.logoUrl) ? branding.logoUrl : undefined;
36
+ if (!logoUrl && !branding.productName) return "";
37
+ const logo = logoUrl
38
+ ? `<img src="${escapeHtmlAttr(logoUrl)}" alt="${escapeHtmlAttr(branding.productName ?? "")}" style="display:block;max-height:40px;max-width:200px;border:0" />`
39
+ : `<span style="font-size:18px;font-weight:700;color:${escapeHtmlAttr(primaryColor)}">${escapeHtml(branding.productName ?? "")}</span>`;
40
+ return `<div style="margin:0 0 24px;padding:0 0 16px;border-bottom:3px solid ${escapeHtmlAttr(primaryColor)}">${logo}</div>`;
41
+ }
42
+
43
+ function renderBrandingFooter(branding: MailBranding | undefined): string {
44
+ if (!branding) return "";
45
+ const links = (branding.footerLinks ?? [])
46
+ .filter((link) => isHttpUrl(link.url))
47
+ .map(
48
+ (link) =>
49
+ `<a href="${escapeHtmlAttr(link.url)}" style="color:#999">${escapeHtml(link.label)}</a>`,
50
+ );
51
+ const footerPartsHtml = [
52
+ branding.footerText ? escapeHtml(branding.footerText) : "",
53
+ links.join(" · "),
54
+ ].filter((part) => part !== "");
55
+ if (footerPartsHtml.length === 0) return "";
56
+ const footerHtml = footerPartsHtml.join("<br />");
57
+ return `<p style="margin:16px 0 0;color:#999;font-size:12px">${footerHtml}</p>`;
58
+ }
59
+
4
60
  type Section =
5
61
  | { readonly text: string }
6
62
  | { readonly button: { readonly label: string; readonly url: string } };
@@ -15,51 +71,58 @@ type EmailTemplateData = {
15
71
  readonly body?: string;
16
72
  };
17
73
 
18
- function renderSection(section: Section): string {
74
+ function renderSection(section: Section, primaryColor: string): string {
19
75
  if ("text" in section) {
20
76
  return `<p style="margin:0 0 16px;color:#333;font-size:14px;line-height:1.5">${escapeHtml(section.text)}</p>`;
21
77
  }
22
78
  if ("button" in section) {
23
- return `<p style="margin:0 0 16px"><a href="${escapeHtml(section.button.url)}" style="display:inline-block;padding:10px 24px;background:#2563eb;color:#fff;text-decoration:none;border-radius:4px;font-size:14px">${escapeHtml(section.button.label)}</a></p>`;
79
+ return `<p style="margin:0 0 16px"><a href="${escapeHtml(section.button.url)}" style="display:inline-block;padding:10px 24px;background:${escapeHtmlAttr(primaryColor)};color:#fff;text-decoration:none;border-radius:4px;font-size:14px">${escapeHtml(section.button.label)}</a></p>`;
24
80
  }
25
81
  return "";
26
82
  }
27
83
 
28
84
  // Simple Renderer: turns structured email template data into HTML with inline CSS.
29
85
  // No external dependencies, no template engine — just string concatenation.
30
- export const simpleRenderer: NotificationRenderer = {
31
- name: "simple",
86
+ export function createSimpleRenderer(branding?: MailBranding): NotificationRenderer {
87
+ const primaryColor = resolvePrimaryColor(branding);
88
+ return {
89
+ name: "simple",
90
+
91
+ async render(input) {
92
+ const data = input.variables as EmailTemplateData; // @cast-boundary render-helper
32
93
 
33
- async render(input) {
34
- const data = input.variables as EmailTemplateData; // @cast-boundary render-helper
94
+ // Fallback: if no structured fields, use title + body as header + single text section
95
+ const header = data.header ?? data.title;
96
+ const sections = data.sections ?? (data.body ? [{ text: data.body }] : undefined);
35
97
 
36
- // Fallback: if no structured fields, use title + body as header + single text section
37
- const header = data.header ?? data.title;
38
- const sections = data.sections ?? (data.body ? [{ text: data.body }] : undefined);
98
+ const parts: string[] = [];
99
+ parts.push('<!DOCTYPE html><html><body style="margin:0;padding:0;font-family:sans-serif">');
100
+ parts.push('<div style="max-width:600px;margin:0 auto;padding:24px">');
101
+ parts.push(renderBrandingHeader(branding, primaryColor));
39
102
 
40
- const parts: string[] = [];
41
- parts.push('<!DOCTYPE html><html><body style="margin:0;padding:0;font-family:sans-serif">');
42
- parts.push('<div style="max-width:600px;margin:0 auto;padding:24px">');
103
+ if (header) {
104
+ parts.push(
105
+ `<h1 style="margin:0 0 24px;color:#111;font-size:20px;font-weight:600">${escapeHtml(header)}</h1>`,
106
+ );
107
+ }
43
108
 
44
- if (header) {
45
- parts.push(
46
- `<h1 style="margin:0 0 24px;color:#111;font-size:20px;font-weight:600">${escapeHtml(header)}</h1>`,
47
- );
48
- }
109
+ if (sections) {
110
+ for (const section of sections) {
111
+ parts.push(renderSection(section, primaryColor));
112
+ }
113
+ }
49
114
 
50
- if (sections) {
51
- for (const section of sections) {
52
- parts.push(renderSection(section));
115
+ if (data.footer) {
116
+ parts.push(
117
+ `<p style="margin:24px 0 0;color:#999;font-size:12px;border-top:1px solid #eee;padding-top:16px">${escapeHtml(data.footer)}</p>`,
118
+ );
53
119
  }
54
- }
55
120
 
56
- if (data.footer) {
57
- parts.push(
58
- `<p style="margin:24px 0 0;color:#999;font-size:12px;border-top:1px solid #eee;padding-top:16px">${escapeHtml(data.footer)}</p>`,
59
- );
60
- }
121
+ parts.push(renderBrandingFooter(branding));
122
+ parts.push("</div></body></html>");
123
+ return parts.join("");
124
+ },
125
+ };
126
+ }
61
127
 
62
- parts.push("</div></body></html>");
63
- return parts.join("");
64
- },
65
- };
128
+ export const simpleRenderer: NotificationRenderer = createSimpleRenderer();
@@ -183,7 +183,7 @@ export function createSecretsFeature(opts: SecretsFeatureOptions = {}): FeatureD
183
183
  rotateJob(
184
184
  payload,
185
185
  ctx,
186
- ctx.db.unsafeRaw(ROTATE_ESCAPE_HATCH_REASON) as DbConnection, // @cast-boundary db-operator — jobs never run inside a DbTx
186
+ ctx.db.unsafeRaw() as DbConnection, // @cast-boundary db-operator — jobs never run inside a DbTx
187
187
  ),
188
188
  });
189
189
 
@@ -177,7 +177,7 @@ export function createSessionsFeature(options?: SessionsFeatureOptions): Feature
177
177
  cleanupJob(
178
178
  payload,
179
179
  ctx,
180
- ctx.db.unsafeRaw(CLEANUP_ESCAPE_HATCH_REASON) as DbConnection, // @cast-boundary db-operator — jobs never run inside a DbTx
180
+ ctx.db.unsafeRaw() as DbConnection, // @cast-boundary db-operator — jobs never run inside a DbTx
181
181
  ),
182
182
  });
183
183
 
@@ -49,7 +49,7 @@ export const revokeAllForUserWrite = defineWriteHandler({
49
49
  reason: REVOKE_ALL_SESSIONS_REASON,
50
50
  },
51
51
  handler: async (event, ctx) => {
52
- const runner = ctx.db.unsafeRaw(REVOKE_ALL_SESSIONS_REASON);
52
+ const runner = ctx.db.unsafeRaw();
53
53
  const updated = await updateMany<{ id: string }>(
54
54
  runner,
55
55
  userSessionTable,
@@ -51,10 +51,7 @@ export function createClaimTenantHandoverHandler(opts: ClaimTenantHandoverOption
51
51
  agent: { expose: false },
52
52
  rateLimit: { per: "user", limit: 10, windowSeconds: 60 },
53
53
  handler: async (event, ctx) => {
54
- const db = ctx.db.unsafeRaw(
55
- "tenant-handover claim: cross-tenant read + ownership write, legitimized by the " +
56
- "row-bound grant redeemed below",
57
- );
54
+ const db = ctx.db.unsafeRaw();
58
55
  const rootLocation = resolveRootAnchorLocation(ctx.registry, db, event.payload.entityType);
59
56
  if (!rootLocation) return notTransferable(event.payload.entityType);
60
57
  const { rootTableName, rootIdCol, rootTenantCol, loadAnchor } = rootLocation;
@@ -126,9 +126,7 @@ export function createTenantLifecycleFeature(): FeatureDefinition {
126
126
  }
127
127
  const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
128
128
  await runTenantDestructionSweep({
129
- db: ctx.db.unsafeRaw(
130
- RUN_TENANT_DESTRUCTION_REASON,
131
- ) as import("@cosmicdrift/kumiko-framework/db").DbConnection, // @cast-boundary db-operator — jobs never run inside a DbTx
129
+ db: ctx.db.unsafeRaw() as import("@cosmicdrift/kumiko-framework/db").DbConnection, // @cast-boundary db-operator — jobs never run inside a DbTx
132
130
  registry: ctx.registry,
133
131
  now: T.Now.instant(),
134
132
  log: (message) => ctx.log?.warn(message),
@@ -30,11 +30,9 @@ export const cancelDestructionWrite = defineWriteHandler({
30
30
  },
31
31
  handler: async (event, ctx) => {
32
32
  const tenantId = event.user.tenantId;
33
- const row = await fetchOne<TenantLifecycleRow>(
34
- ctx.db.unsafeRaw(CANCEL_DESTRUCTION_TENANT_ROW_REASON),
35
- tenantTable,
36
- { id: tenantId },
37
- );
33
+ const row = await fetchOne<TenantLifecycleRow>(ctx.db.unsafeRaw(), tenantTable, {
34
+ id: tenantId,
35
+ });
38
36
  if (!row) {
39
37
  return writeFailure(new UnprocessableError("tenant_not_found", { details: { tenantId } }));
40
38
  }
@@ -34,7 +34,7 @@ export const requestDestructionWrite = defineWriteHandler({
34
34
  },
35
35
  handler: async (event, ctx) => {
36
36
  const tenantId = event.user.tenantId;
37
- const runner = ctx.db.unsafeRaw(REQUEST_DESTRUCTION_TENANT_ROW_REASON);
37
+ const runner = ctx.db.unsafeRaw();
38
38
  const row = await fetchOne<TenantLifecycleRow>(runner, tenantTable, { id: tenantId });
39
39
  if (!row) {
40
40
  return writeFailure(new UnprocessableError("tenant_not_found", { details: { tenantId } }));
@@ -399,7 +399,7 @@ export function createTierEngineFeature<
399
399
  const rawDb = ctx.systemDb
400
400
  ? ctx.systemDb.unsafeRaw(autoDefaultTierHookReason)
401
401
  : ctx.db && "unsafeRaw" in ctx.db
402
- ? ctx.db.unsafeRaw(autoDefaultTierHookReason)
402
+ ? ctx.db.unsafeRaw()
403
403
  : undefined;
404
404
  // skip: defensive — inTransaction phase always sets db, but AppContext's
405
405
  // type makes it optional. Throwing would be overreach (lifecycle
@@ -25,7 +25,7 @@ export const getTenantTierQuery = defineQueryHandler({
25
25
  },
26
26
  handler: async (query, ctx) => {
27
27
  const tenantId = query.payload.tenantId as TenantId; // @cast-boundary engine-bridge
28
- const tdb = createTenantDb(ctx.db.unsafeRaw(GET_TENANT_TIER_REASON), tenantId, "system");
28
+ const tdb = createTenantDb(ctx.db.unsafeRaw(), tenantId, "system");
29
29
  const row = await fetchOne<TierAssignmentRow>(tdb, tierAssignmentTable, { tenantId });
30
30
  return row ?? null;
31
31
  },
@@ -73,7 +73,7 @@ export function createSetTenantTierWrite(opts: SetTenantTierOptions = {}) {
73
73
  },
74
74
  handler: async (event, ctx) => {
75
75
  const tenantId = event.payload.tenantId as TenantId; // @cast-boundary engine-bridge
76
- const rawDb = ctx.db.unsafeRaw(SET_TENANT_TIER_REASON);
76
+ const rawDb = ctx.db.unsafeRaw();
77
77
  const tdb = createTenantDb(rawDb, tenantId, "system");
78
78
  const systemUser = { ...event.user, tenantId };
79
79
  const tier = event.payload.tier;
@@ -588,9 +588,8 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
588
588
  // Discriminator wird via handlerName="user-data-rights:run-export-
589
589
  // jobs" im Secret-Read-Audit erfasst.
590
590
  const exportUserId = ctx._userId ?? SYSTEM_USER_ID;
591
- const exportDb = ctx.db.unsafeRaw(
592
- RUN_EXPORT_JOBS_REASON,
593
- ) as import("@cosmicdrift/kumiko-framework/db").DbConnection; // @cast-boundary db-operator — jobs never run inside a DbTx
591
+ const exportDb =
592
+ ctx.db.unsafeRaw() as import("@cosmicdrift/kumiko-framework/db").DbConnection; // @cast-boundary db-operator — jobs never run inside a DbTx
594
593
  const exportRegistry = ctx.registry;
595
594
  // JobContext carries ctx.meter (raw), not ctx.metrics — that bound
596
595
  // handle only exists on HandlerContext (built per write/query call
@@ -684,9 +683,8 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
684
683
  }
685
684
  const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
686
685
  const forgetUserId = ctx._userId ?? SYSTEM_USER_ID;
687
- const forgetDb = ctx.db.unsafeRaw(
688
- RUN_FORGET_CLEANUP_REASON,
689
- ) as import("@cosmicdrift/kumiko-framework/db").DbConnection; // @cast-boundary db-operator — jobs never run inside a DbTx
686
+ const forgetDb =
687
+ ctx.db.unsafeRaw() as import("@cosmicdrift/kumiko-framework/db").DbConnection; // @cast-boundary db-operator — jobs never run inside a DbTx
690
688
  const forgetRegistry = ctx.registry;
691
689
  const tenantModel = await resolveAppTenantModel({
692
690
  registry: forgetRegistry,
@@ -61,7 +61,7 @@ export const cancelDeletionWrite = defineWriteHandler({
61
61
  return writeFailure(new UnprocessableError("grace_period_expired"));
62
62
  }
63
63
 
64
- await updateUserLifecycle(ctx.db.unsafeRaw(APPEND_LIFECYCLE_EVENT_REASON), event.user.id, {
64
+ await updateUserLifecycle(ctx.db.unsafeRaw(), event.user.id, {
65
65
  status: USER_STATUS.Active,
66
66
  gracePeriodEnd: null,
67
67
  // #354/1: closes the replay-after-cancel window — a still-TTL-valid email
@@ -83,9 +83,7 @@ export function createConfirmDeletionByTokenHandler(opts: ConfirmDeletionByToken
83
83
  ctx,
84
84
  userId,
85
85
  () => resolveGracePeriod(ctx, event.user.tenantId),
86
- ctx.db.unsafeRaw(
87
- "appends the user lifecycle event on the SYSTEM_TENANT_ID user stream",
88
- ),
86
+ ctx.db.unsafeRaw(),
89
87
  { pendingDeletionRequestId: requestId },
90
88
  );
91
89
  if (!res.ok) return false;
@@ -87,7 +87,7 @@ export const downloadByJobQuery = defineQueryHandler({
87
87
  // Wert (603/2).
88
88
  const auditIp = requestContext.get()?.ip ?? null;
89
89
  const auditUa = requestContext.get()?.userAgent ?? null;
90
- const runner = ctx.db.unsafeRaw(DOWNLOAD_BY_JOB_REASON);
90
+ const runner = ctx.db.unsafeRaw();
91
91
 
92
92
  // Step 1-2: job-lookup + cross-user-isolation
93
93
  const jobRow = await fetchOne<JobRow>(runner, exportJobsTable, { id: jobId });
@@ -92,7 +92,7 @@ export const downloadByTokenQuery = defineQueryHandler({
92
92
  handler: async (query, ctx) => {
93
93
  const T = getTemporal();
94
94
  const now = T.Now.instant();
95
- const runner = ctx.db.unsafeRaw(DOWNLOAD_BY_TOKEN_REASON);
95
+ const runner = ctx.db.unsafeRaw();
96
96
 
97
97
  // Step 1: hash + lookup
98
98
  const hash = await hashDownloadToken(query.payload.token);
@@ -50,7 +50,7 @@ export const exportStatusQuery = defineQueryHandler({
50
50
  },
51
51
  handler: async (query, ctx) => {
52
52
  const rows = await selectMany<ExportJobRow>(
53
- ctx.db.unsafeRaw(EXPORT_STATUS_REASON),
53
+ ctx.db.unsafeRaw(),
54
54
  exportJobsTable,
55
55
  { userId: query.user.id },
56
56
  { limit: 1, orderBy: { col: "requestedAt", direction: "desc" } },
@@ -7,10 +7,6 @@ import { updateUserLifecycle } from "../lib/update-user-lifecycle.js";
7
7
 
8
8
  const LIFT_RESTRICTION_ESCAPE_HATCH_REASON =
9
9
  "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.";
10
- const CHECK_TARGET_MEMBERSHIP_REASON =
11
- "checks the target user's membership in the admin's tenant via the DbRunner helper";
12
- const APPEND_LIFECYCLE_EVENT_REASON =
13
- "appends the user lifecycle event on the SYSTEM_TENANT_ID user stream";
14
10
 
15
11
  // POST /api/user/lift-restriction (S2.U6) — DSGVO Art. 18 Reverse.
16
12
  //
@@ -46,7 +42,7 @@ export const liftRestrictionWrite = defineWriteHandler({
46
42
  const targetUserId = event.payload.userId;
47
43
 
48
44
  const outside = await denyIfTargetOutsideAdminTenant(
49
- ctx.db.unsafeRaw(CHECK_TARGET_MEMBERSHIP_REASON),
45
+ ctx.db.unsafeRaw(),
50
46
  event.user,
51
47
  targetUserId,
52
48
  );
@@ -73,7 +69,7 @@ export const liftRestrictionWrite = defineWriteHandler({
73
69
  );
74
70
  }
75
71
 
76
- await updateUserLifecycle(ctx.db.unsafeRaw(APPEND_LIFECYCLE_EVENT_REASON), targetUserId, {
72
+ await updateUserLifecycle(ctx.db.unsafeRaw(), targetUserId, {
77
73
  status: USER_STATUS.Active,
78
74
  });
79
75
 
@@ -64,7 +64,7 @@ export const myAuditLogQuery = defineQueryHandler({
64
64
  type: string;
65
65
  payload: Record<string, unknown>;
66
66
  created_at: unknown;
67
- }>(ctx.db.unsafeRaw(MY_AUDIT_LOG_REASON), eventsTable, where, {
67
+ }>(ctx.db.unsafeRaw(), eventsTable, where, {
68
68
  orderBy: { col: "id", direction: "desc" },
69
69
  limit: p.limit,
70
70
  });
@@ -92,7 +92,7 @@ export function createRequestDeletionByEmailHandler(opts: RequestDeletionByEmail
92
92
  // user-Row landet und in die Token-HMAC-Purpose gefaltet wird. cancel
93
93
  // nullt sie → ein nach Cancel nachgespieltes Token verifiziert nicht mehr.
94
94
  const requestId = crypto.randomUUID();
95
- await updateUserLifecycle(ctx.db.unsafeRaw(APPEND_LIFECYCLE_EVENT_REASON), userRow["id"], {
95
+ await updateUserLifecycle(ctx.db.unsafeRaw(), userRow["id"], {
96
96
  pendingDeletionRequestId: requestId,
97
97
  });
98
98
 
@@ -65,7 +65,7 @@ export function createRequestDeletionHandler(opts: RequestDeletionOptions = {})
65
65
  ctx,
66
66
  event.user.id,
67
67
  () => resolveGracePeriod(ctx, event.user.tenantId),
68
- ctx.db.unsafeRaw("appends the user lifecycle event on the SYSTEM_TENANT_ID user stream"),
68
+ ctx.db.unsafeRaw(),
69
69
  );
70
70
  if (!res.ok) return writeFailure(res.error);
71
71
  const { gracePeriodEnd, userEmail, userLocale } = res;
@@ -77,7 +77,7 @@ export const requestExportWrite = defineWriteHandler({
77
77
  const userId = event.user.id;
78
78
  const T = getTemporal();
79
79
  const now = T.Now.instant();
80
- const exportJobRunner = ctx.db.unsafeRaw(REQUEST_EXPORT_REASON);
80
+ const exportJobRunner = ctx.db.unsafeRaw();
81
81
 
82
82
  // Pre-Check: exportJobRunner bypasses the TenantDb-Filter — the
83
83
  // TenantDb-Wrapper would hide cross-tenant jobs.
@@ -70,9 +70,7 @@ export const restrictAccountWrite = defineWriteHandler({
70
70
  );
71
71
  }
72
72
  const outside = await denyIfTargetOutsideAdminTenant(
73
- ctx.db.unsafeRaw(
74
- "checks the target user's membership in the admin's tenant via the DbRunner helper",
75
- ),
73
+ ctx.db.unsafeRaw(),
76
74
  event.user,
77
75
  targetUserId,
78
76
  );
@@ -107,11 +105,7 @@ export const restrictAccountWrite = defineWriteHandler({
107
105
  );
108
106
  }
109
107
 
110
- await updateUserLifecycle(
111
- ctx.db.unsafeRaw("appends the user lifecycle event on the SYSTEM_TENANT_ID user stream"),
112
- targetUserId,
113
- { status: USER_STATUS.Restricted },
114
- );
108
+ await updateUserLifecycle(ctx.db.unsafeRaw(), targetUserId, { status: USER_STATUS.Restricted });
115
109
 
116
110
  // Cross-Feature: alle live sessions revoken — sonst koennte der User
117
111
  // mit existierendem JWT bis zur Token-Expiry weiter schreiben.
@@ -48,7 +48,7 @@ export function createRunForgetCleanupHandler(opts: RunForgetCleanupOptions = {}
48
48
  }
49
49
 
50
50
  const T = getTemporal();
51
- const runner = ctx.db.unsafeRaw(RUN_FORGET_CLEANUP_REASON);
51
+ const runner = ctx.db.unsafeRaw();
52
52
  // Operator-triggered forget must also erase binaries, not just rows —
53
53
  // it flips users to Deleted, after which the cron never re-processes
54
54
  // them, so a row-only delete here would permanently leak the binaries.
@@ -29,7 +29,7 @@ export type UpdateUserLifecycleOptions = {
29
29
  readonly expect?: Readonly<Record<string, string | number | boolean | null>>;
30
30
  };
31
31
 
32
- // `conn` is ctx.db.unsafeRaw(reason) (regular handlers) or the open tx
32
+ // `conn` is ctx.db.unsafeRaw() (regular handlers) or the open tx
33
33
  // (forget-cleanup sub-tx) — keeps the event append atomic with the write.
34
34
  //
35
35
  // Returns `{ applied: false }` instead of throwing only when `options.expect`
@@ -155,7 +155,7 @@ export interface RunForgetCleanupArgs {
155
155
 
156
156
  // fw#2914 — sourced from the owning job's ctx (_escapeHatchAuditSink,
157
157
  // systemUser.id); attributes+audits any EXT_USER_DATA usage's declared
158
- // escapeHatch when its delete hook calls ctx.db.unsafeRaw(reason).
158
+ // escapeHatch when its delete hook calls ctx.db.unsafeRaw().
159
159
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
160
160
  readonly actor?: string;
161
161
  }
@@ -60,7 +60,7 @@ export interface RunUserExportArgs {
60
60
  readonly now: Instant;
61
61
  // fw#2914 — sourced from the owning job's ctx (_escapeHatchAuditSink,
62
62
  // systemUser.id); attributes+audits any EXT_USER_DATA usage's declared
63
- // escapeHatch when its export hook calls ctx.db.unsafeRaw(reason).
63
+ // escapeHatch when its export hook calls ctx.db.unsafeRaw().
64
64
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
65
65
  readonly actor?: string;
66
66
  }
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.339.0",
4
+ "type": "breaking",
5
+ "title": "workflow-run stream stores a reference to the trigger event instead of its payload",
6
+ "detail": "`workflow.run-started` now holds `triggerEventRef: { eventId, aggregateId, version }` and no `triggerPayload`; the wait, waitForEvent and retry step events no longer embed it either. `workflow_run_pending` gets a `trigger_event_ref` column and the event-subscriber stops writing `trigger_payload`. `resume-run` loads the trigger and the awaited event through `ctx.loadAggregate` and fails the run with `trigger_event_unavailable` or `awaited_event_unavailable` when one is gone.",
7
+ "migration": "Own code that reads `triggerPayload` from `workflow.run-started` must read `triggerEventRef` and load the event with `ctx.loadAggregate(ref.aggregateId)`. Apps generate the migration for the new `workflow_run_pending.trigger_event_ref` column with `kumiko schema generate`. The step events `workflow.step.waiting`, `workflow.step.waiting-for-event` and `workflow.retry.scheduled` no longer carry `triggerPayload`; a custom resume loop that read it must load the trigger event through the `triggerEventRef` on its own run-started event (see `samples/recipes/workflow-engine/src/postgres-resume-loop.ts`). Stored run-started events and pending rows keep their old copy; this release rewrites no events, and runs started before it resume from that copy."
8
+ },
2
9
  {
3
10
  "version": "0.337.0",
4
11
  "type": "fix",
@@ -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
 
@@ -34,6 +34,7 @@ import type {
34
34
  import { evaluateEventMatch } from "@cosmicdrift/kumiko-framework/engine";
35
35
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
36
36
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
37
+ import type { TriggerEventRef } from "./runner.js";
37
38
  import { workflowRunPendingTable } from "./tables.js";
38
39
 
39
40
  type CandidateRow = {
@@ -87,7 +88,11 @@ const wakeupApply: MultiStreamApplyFn = async (event, tx) => {
87
88
  workflowRunPendingTable,
88
89
  {
89
90
  triggerEventType: event.type,
90
- triggerPayload: event.payload,
91
+ triggerEventRef: {
92
+ eventId: event.id,
93
+ aggregateId: event.aggregateId,
94
+ version: event.version,
95
+ } satisfies TriggerEventRef,
91
96
  wakeAt: getTemporal().Now.instant().toString(),
92
97
  },
93
98
  { tenantId: row.tenantId, runId: row.runId, stepIndex: row.stepIndex },
@@ -85,6 +85,11 @@ export function registerEventTrigger(r: FeatureRegistrar, workflow: WorkflowDefi
85
85
  runId,
86
86
  workflow,
87
87
  triggerEvent,
88
+ triggerEventRef: {
89
+ eventId: event.id,
90
+ aggregateId: event.aggregateId,
91
+ version: event.version,
92
+ },
88
93
  ...(idempotencyKey && { idempotencyKey }),
89
94
  // @cast-boundary msp-to-handler-ctx — MultiStreamApplyContext only
90
95
  // exposes unsafeAppendEvent/loadAggregate, a subset of