@cosmicdrift/kumiko-bundled-features 0.341.0 → 0.343.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 (117) hide show
  1. package/dist/audit/constants.d.ts +7 -0
  2. package/dist/audit/constants.js +7 -0
  3. package/dist/audit/escape-hatch-audit-sink.d.ts +1 -0
  4. package/dist/audit/escape-hatch-audit-sink.js +1 -0
  5. package/dist/audit/feature.js +34 -7
  6. package/dist/audit/run-escape-hatch-retention.d.ts +11 -0
  7. package/dist/audit/run-escape-hatch-retention.js +91 -0
  8. package/dist/auth-email-password/bootstrap.d.ts +8 -2
  9. package/dist/auth-email-password/bootstrap.js +7 -2
  10. package/dist/auth-email-password/email-templates.d.ts +4 -0
  11. package/dist/auth-email-password/email-templates.js +69 -16
  12. package/dist/auth-email-password/handlers/invite-create.write.js +4 -1
  13. package/dist/auth-email-password/handlers/signup-request.write.js +4 -1
  14. package/dist/auth-email-password/handlers/token-request-handler.js +3 -0
  15. package/dist/auth-email-password/magic-link-mail.d.ts +2 -0
  16. package/dist/auth-email-password/magic-link-mail.js +2 -0
  17. package/dist/auth-email-password/web/auth-form-primitives.d.ts +7 -1
  18. package/dist/auth-email-password/web/auth-form-primitives.js +5 -5
  19. package/dist/auth-email-password/web/confirm-account-unlock-screen.js +3 -3
  20. package/dist/auth-email-password/web/forgot-password-screen.js +2 -2
  21. package/dist/auth-email-password/web/invite-accept-screen.js +9 -9
  22. package/dist/auth-email-password/web/login-screen.js +8 -8
  23. package/dist/auth-email-password/web/request-account-unlock-screen.js +2 -2
  24. package/dist/auth-email-password/web/reset-password-screen.js +3 -3
  25. package/dist/auth-email-password/web/session-bootstrap-error.js +3 -3
  26. package/dist/auth-email-password/web/signup-complete-screen.js +3 -3
  27. package/dist/auth-email-password/web/signup-screen.js +2 -2
  28. package/dist/auth-email-password/web/verify-email-screen.js +3 -3
  29. package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +5 -5
  30. package/dist/auth-mfa/web/mfa-verify-screen.js +3 -3
  31. package/dist/channel-discord/feature.js +1 -0
  32. package/dist/channel-email/email-channel.js +7 -1
  33. package/dist/channel-email/feature.js +1 -0
  34. package/dist/channel-email/pii-guard.js +6 -5
  35. package/dist/channel-email/smtp-transport.js +22 -1
  36. package/dist/channel-email/types.d.ts +1 -0
  37. package/dist/channel-in-app/feature.js +1 -0
  38. package/dist/channel-push/feature.js +1 -0
  39. package/dist/channel-slack/feature.js +1 -0
  40. package/dist/channel-teams/feature.js +1 -0
  41. package/dist/channel-teams/teams-channel.d.ts +2 -1
  42. package/dist/channel-teams/teams-channel.js +12 -0
  43. package/dist/channel-telegram/feature.js +1 -0
  44. package/dist/delivery/chat-webhook-channel.d.ts +2 -1
  45. package/dist/delivery/chat-webhook-channel.js +5 -2
  46. package/dist/delivery/chat-webhook-sender.d.ts +8 -1
  47. package/dist/delivery/chat-webhook-sender.js +45 -4
  48. package/dist/delivery/constants.d.ts +1 -1
  49. package/dist/delivery/constants.js +1 -1
  50. package/dist/delivery/delivery-service.d.ts +2 -1
  51. package/dist/delivery/delivery-service.js +55 -4
  52. package/dist/delivery/events.d.ts +19 -1
  53. package/dist/delivery/events.js +11 -1
  54. package/dist/delivery/feature.js +29 -5
  55. package/dist/delivery/handlers/log.query.d.ts +4 -2
  56. package/dist/delivery/handlers/log.query.js +46 -18
  57. package/dist/delivery/i18n.js +36 -0
  58. package/dist/delivery/index.d.ts +2 -1
  59. package/dist/delivery/index.js +1 -0
  60. package/dist/delivery/jobs.js +24 -10
  61. package/dist/delivery/notify-factory.d.ts +5 -0
  62. package/dist/delivery/notify-factory.js +6 -0
  63. package/dist/delivery/public-names.d.ts +4 -0
  64. package/dist/delivery/public-names.js +4 -0
  65. package/dist/delivery/tables.js +3 -0
  66. package/dist/delivery/testing.d.ts +2 -0
  67. package/dist/delivery/testing.js +2 -1
  68. package/dist/delivery/types.d.ts +5 -3
  69. package/dist/delivery/web/client-plugin.js +9 -1
  70. package/dist/delivery/web/delivery-channel-cell.d.ts +3 -0
  71. package/dist/delivery/web/delivery-channel-cell.js +10 -0
  72. package/dist/delivery/web/delivery-error-cell.d.ts +3 -0
  73. package/dist/delivery/web/delivery-error-cell.js +16 -0
  74. package/dist/delivery/web/delivery-status-cell.d.ts +1 -1
  75. package/dist/delivery/web/delivery-status-cell.js +11 -1
  76. package/dist/delivery/web/delivery-time-cell.d.ts +3 -0
  77. package/dist/delivery/web/delivery-time-cell.js +6 -0
  78. package/dist/delivery/web/delivery-type-cell.d.ts +3 -0
  79. package/dist/delivery/web/delivery-type-cell.js +17 -0
  80. package/dist/secrets/constants.d.ts +6 -0
  81. package/dist/secrets/constants.js +8 -0
  82. package/dist/secrets/feature.d.ts +3 -0
  83. package/dist/secrets/feature.js +3 -0
  84. package/dist/secrets/i18n.js +4 -3
  85. package/dist/secrets/index.d.ts +3 -2
  86. package/dist/secrets/index.js +3 -2
  87. package/dist/secrets/write-gate.d.ts +1 -0
  88. package/dist/secrets/write-gate.js +14 -6
  89. package/dist/shared/index.d.ts +1 -0
  90. package/dist/shared/index.js +1 -0
  91. package/dist/shared/redact.d.ts +4 -0
  92. package/dist/shared/redact.js +21 -0
  93. package/dist/step-dispatcher/feature.js +2 -1
  94. package/dist/step-dispatcher/mail-runner.d.ts +0 -1
  95. package/dist/step-dispatcher/mail-runner.js +0 -5
  96. package/dist/tenant/web/member-roles-cell.js +1 -1
  97. package/dist/tenant/web/member-status-cell.js +1 -1
  98. package/dist/tenant-lifecycle/feature.js +8 -2
  99. package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +2 -1
  100. package/dist/tenant-lifecycle/run-tenant-destroy.js +16 -7
  101. package/dist/user/index.d.ts +1 -0
  102. package/dist/user/index.js +1 -0
  103. package/dist/user/resolve-display-names.d.ts +2 -0
  104. package/dist/user/resolve-display-names.js +24 -0
  105. package/dist/waitlist/handlers/submit.write.js +1 -1
  106. package/dist/waitlist/options.d.ts +3 -2
  107. package/package.json +9 -9
  108. package/src/audit/changes.json +6 -0
  109. package/src/auth-email-password/changes.json +19 -0
  110. package/src/channel-email/changes.json +6 -0
  111. package/src/channel-teams/changes.json +8 -0
  112. package/src/delivery/changes.json +32 -0
  113. package/src/jobs/changes.json +6 -0
  114. package/src/secrets/changes.json +6 -0
  115. package/src/waitlist/changes.json +6 -0
  116. /package/dist/{tenant → shared}/web/translate-or-raw.d.ts +0 -0
  117. /package/dist/{tenant → shared}/web/translate-or-raw.js +0 -0
@@ -113,12 +113,12 @@ export function LoginScreen({ title, subtitle, submitLabel, forgotPasswordHref,
113
113
  };
114
114
  const effectiveTitle = title ?? t("auth.login.title");
115
115
  const effectiveSubmit = submitLabel ?? t("auth.login.submit");
116
- return (_jsx(AuthCard, { title: effectiveTitle, subtitle: subtitle, children: _jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "login-email", label: t("auth.login.email"), required: true, children: _jsx(Input, { kind: "email", id: "login-email", name: "login-email", value: email, onChange: setEmail, disabled: submitting, required: true, autoComplete: "email" }) }), _jsx(Field, { id: "login-password", label: t("auth.login.password"), required: true, children: _jsx(Input, { kind: "password", id: "login-password", name: "login-password", value: password, onChange: setPassword, disabled: submitting, required: true, autoComplete: "current-password" }) }), session.signedOutReason === "session-ended" && (_jsx(Banner, { variant: "info", children: t("auth.login.sessionEnded") })), resendStatus.kind === "success" ? (_jsx(Banner, { variant: "info", children: t("auth.login.resendSuccess") })) : error !== null ? (_jsx(Banner, { variant: "error", children: _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { children: (() => {
117
- const { key, params } = reasonToKey(error);
118
- return t(key, params);
119
- })() }), error.reason === "email_not_verified" &&
120
- email.trim().length > 0 &&
121
- email === failedLoginEmail && (_jsx("span", { className: "self-start", children: _jsx(Button, { variant: "link", onClick: () => void onResend(), disabled: resendStatus.kind === "sending", children: resendStatus.kind === "sending"
122
- ? t("auth.login.submitting")
123
- : t("auth.login.resendVerification") }) })), error.reason === "account_locked" && unlockAccountHref !== undefined && (_jsx("span", { className: "self-start", children: _jsx(Link, { href: unlockAccountHref, variant: "default", children: t("auth.login.unlockAccount") }) })), resendStatus.kind === "rateLimited" && (_jsx("span", { className: "text-xs", children: t("auth.login.resendRateLimited") })), resendStatus.kind === "error" && (_jsx("span", { className: "text-xs", children: t("auth.login.resendError") }))] }) })) : null, _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.login.submitting") : effectiveSubmit })] }), forgotPasswordHref !== undefined && (_jsx(Link, { href: forgotPasswordHref, variant: "muted", className: "self-center", children: t("auth.login.forgotPassword") })), signupHref !== undefined && (_jsx(Link, { href: signupHref, variant: "muted", className: "self-center", children: t("auth.signup.title") })), legalLinks !== undefined && legalLinks.length > 0 && (_jsx("nav", { "data-testid": "login-legal-links", className: "flex items-center justify-center gap-3 pt-2 border-t border-border/50", children: legalLinks.map((link) => (_jsx(Link, { href: link.href, variant: "muted", className: "text-xs", children: link.label }, link.href))) }))] }) }));
116
+ return (_jsxs(AuthCard, { title: effectiveTitle, subtitle: subtitle, children: [_jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "login-email", label: t("auth.login.email"), required: true, children: _jsx(Input, { kind: "email", id: "login-email", name: "login-email", value: email, onChange: setEmail, disabled: submitting, required: true, autoComplete: "email" }) }), _jsx(Field, { id: "login-password", label: t("auth.login.password"), required: true, children: _jsx(Input, { kind: "password", id: "login-password", name: "login-password", value: password, onChange: setPassword, disabled: submitting, required: true, autoComplete: "current-password" }) }), session.signedOutReason === "session-ended" && (_jsx(Banner, { variant: "info", children: t("auth.login.sessionEnded") })), resendStatus.kind === "success" ? (_jsx(Banner, { variant: "info", children: t("auth.login.resendSuccess") })) : error !== null ? (_jsx(Banner, { variant: "error", children: _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { children: (() => {
117
+ const { key, params } = reasonToKey(error);
118
+ return t(key, params);
119
+ })() }), error.reason === "email_not_verified" &&
120
+ email.trim().length > 0 &&
121
+ email === failedLoginEmail && (_jsx("span", { className: "self-start", children: _jsx(Button, { variant: "link", onClick: () => void onResend(), disabled: resendStatus.kind === "sending", children: resendStatus.kind === "sending"
122
+ ? t("auth.login.submitting")
123
+ : t("auth.login.resendVerification") }) })), error.reason === "account_locked" && unlockAccountHref !== undefined && (_jsx("span", { className: "self-start", children: _jsx(Link, { href: unlockAccountHref, variant: "default", children: t("auth.login.unlockAccount") }) })), resendStatus.kind === "rateLimited" && (_jsx("span", { className: "text-xs", children: t("auth.login.resendRateLimited") })), resendStatus.kind === "error" && (_jsx("span", { className: "text-xs", children: t("auth.login.resendError") }))] }) })) : null, _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.login.submitting") : effectiveSubmit })] }), forgotPasswordHref !== undefined && (_jsx(Link, { href: forgotPasswordHref, variant: "muted", className: "self-center", children: t("auth.login.forgotPassword") })), signupHref !== undefined && (_jsx(Link, { href: signupHref, variant: "muted", className: "self-center", children: t("auth.signup.title") })), legalLinks !== undefined && legalLinks.length > 0 && (_jsx("nav", { "data-testid": "login-legal-links", className: "flex items-center justify-center gap-3 pt-2 border-t border-border/50", children: legalLinks.map((link) => (_jsx(Link, { href: link.href, variant: "muted", className: "text-xs", children: link.label }, link.href))) }))] }));
124
124
  }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
3
  // RequestAccountUnlockScreen — form with an email input. Submit triggers
4
4
  // /api/auth/request-account-unlock (silent-success, no account
@@ -54,5 +54,5 @@ export function RequestAccountUnlockScreen({ title, subtitle, loginHref = "/logi
54
54
  void doSubmit();
55
55
  };
56
56
  const effectiveTitle = title ?? t("auth.requestUnlock.title");
57
- return (_jsx(AuthCard, { title: effectiveTitle, subtitle: subtitle, children: done ? (_jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsxs(Banner, { variant: "info", children: [_jsx("p", { className: "font-medium text-foreground", children: t("auth.requestUnlock.successTitle") }), _jsx("p", { className: "mt-1", children: t("auth.requestUnlock.successBody") })] }), _jsx(Link, { href: loginHref, variant: "muted", children: t("auth.requestUnlock.backToLogin") })] })) : (_jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.requestUnlock.intro") }), _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "unlock-email", label: t("auth.requestUnlock.email"), required: true, children: _jsx(Input, { kind: "text", id: "unlock-email", name: "unlock-email", value: email, onChange: setEmail, disabled: submitting, required: true }) }), error !== null && _jsx(Banner, { variant: "error", children: error }), _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.requestUnlock.submitting") : t("auth.requestUnlock.submit") })] }), _jsx(Link, { href: loginHref, variant: "muted", className: "self-center", children: t("auth.requestUnlock.backToLogin") })] })) }));
57
+ return (_jsx(AuthCard, { title: effectiveTitle, subtitle: subtitle, children: done ? (_jsxs(_Fragment, { children: [_jsxs(Banner, { variant: "info", children: [_jsx("p", { className: "font-medium text-foreground", children: t("auth.requestUnlock.successTitle") }), _jsx("p", { className: "mt-1", children: t("auth.requestUnlock.successBody") })] }), _jsx(Link, { href: loginHref, variant: "muted", children: t("auth.requestUnlock.backToLogin") })] })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.requestUnlock.intro") }), _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "unlock-email", label: t("auth.requestUnlock.email"), required: true, children: _jsx(Input, { kind: "text", id: "unlock-email", name: "unlock-email", value: email, onChange: setEmail, disabled: submitting, required: true }) }), error !== null && _jsx(Banner, { variant: "error", children: error }), _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.requestUnlock.submitting") : t("auth.requestUnlock.submit") })] }), _jsx(Link, { href: loginHref, variant: "muted", className: "self-center", children: t("auth.requestUnlock.backToLogin") })] })) }));
58
58
  }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
3
  // ResetPasswordScreen — liest `?token=...` aus der URL, zeigt Form mit
4
4
  // new + confirm-password. Submit triggert /api/auth/reset-password mit
@@ -62,7 +62,7 @@ export function ResetPasswordScreen({ title, token: tokenProp, loginHref = "/log
62
62
  // submitten würde nur den invalidResetToken-Error zeigen — das ist
63
63
  // verwirrend. Lieber upfront eine klare Message.
64
64
  if (token === "") {
65
- return (_jsx(AuthCard, { title: effectiveTitle, children: _jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.resetPassword.missingToken") }), _jsx(Link, { href: loginHref, variant: "muted", children: t("auth.resetPassword.goToLogin") })] }) }));
65
+ return (_jsxs(AuthCard, { title: effectiveTitle, children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.resetPassword.missingToken") }), _jsx(Link, { href: loginHref, variant: "muted", children: t("auth.resetPassword.goToLogin") })] }));
66
66
  }
67
- return (_jsx(AuthCard, { title: effectiveTitle, children: done ? (_jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsxs(Banner, { variant: "info", children: [_jsx("p", { className: "font-medium text-foreground", children: t("auth.resetPassword.successTitle") }), _jsx("p", { className: "mt-1", children: t("auth.resetPassword.successBody") })] }), _jsx(Link, { href: loginHref, variant: "button", children: t("auth.resetPassword.goToLogin") })] })) : (_jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.resetPassword.intro") }), _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "reset-new-password", label: t("auth.resetPassword.newPassword"), required: true, children: _jsx(Input, { kind: "password", id: "reset-new-password", name: "reset-new-password", value: newPassword, onChange: setNewPassword, disabled: submitting, required: true, autoComplete: "new-password" }) }), _jsx(Field, { id: "reset-confirm-password", label: t("auth.resetPassword.confirmPassword"), required: true, children: _jsx(Input, { kind: "password", id: "reset-confirm-password", name: "reset-confirm-password", value: confirmPassword, onChange: setConfirmPassword, disabled: submitting, required: true, autoComplete: "new-password" }) }), error !== null && _jsx(Banner, { variant: "error", children: error }), _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.resetPassword.submitting") : t("auth.resetPassword.submit") })] })] })) }));
67
+ return (_jsx(AuthCard, { title: effectiveTitle, children: done ? (_jsxs(_Fragment, { children: [_jsxs(Banner, { variant: "info", children: [_jsx("p", { className: "font-medium text-foreground", children: t("auth.resetPassword.successTitle") }), _jsx("p", { className: "mt-1", children: t("auth.resetPassword.successBody") })] }), _jsx(Link, { href: loginHref, variant: "button", children: t("auth.resetPassword.goToLogin") })] })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.resetPassword.intro") }), _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "reset-new-password", label: t("auth.resetPassword.newPassword"), required: true, children: _jsx(Input, { kind: "password", id: "reset-new-password", name: "reset-new-password", value: newPassword, onChange: setNewPassword, disabled: submitting, required: true, autoComplete: "new-password" }) }), _jsx(Field, { id: "reset-confirm-password", label: t("auth.resetPassword.confirmPassword"), required: true, children: _jsx(Input, { kind: "password", id: "reset-confirm-password", name: "reset-confirm-password", value: confirmPassword, onChange: setConfirmPassword, disabled: submitting, required: true, autoComplete: "new-password" }) }), error !== null && _jsx(Banner, { variant: "error", children: error }), _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.resetPassword.submitting") : t("auth.resetPassword.submit") })] })] })) }));
68
68
  }
@@ -24,7 +24,7 @@ export function SessionBootstrapErrorScreen({ failure, onRetry, onSignOut, }) {
24
24
  .then(onRetry)
25
25
  .then(() => setRetrying(false));
26
26
  };
27
- return (_jsx("div", { "data-testid": "session-bootstrap-error", "data-http-status": failure.httpStatus ?? "network", children: _jsx(AuthCard, { title: t("auth.sessionBootstrap.errorTitle"), children: _jsxs("div", { className: "flex flex-col gap-4 px-6 pb-4", children: [_jsx(Banner, { variant: "error", children: failure.httpStatus === 429
28
- ? t("auth.sessionBootstrap.rateLimitedBody")
29
- : t("auth.sessionBootstrap.errorBody") }), _jsx(Button, { variant: "primary", onClick: handleRetry, disabled: retrying, children: retrying ? t("auth.sessionBootstrap.retrying") : t("auth.sessionBootstrap.retry") }), onSignOut !== undefined && (_jsx(Button, { variant: "secondary", onClick: () => void onSignOut(), disabled: retrying, children: t("auth.sessionBootstrap.signOut") }))] }) }) }));
27
+ return (_jsx("div", { "data-testid": "session-bootstrap-error", "data-http-status": failure.httpStatus ?? "network", children: _jsxs(AuthCard, { title: t("auth.sessionBootstrap.errorTitle"), bodyClassName: "pb-4", children: [_jsx(Banner, { variant: "error", children: failure.httpStatus === 429
28
+ ? t("auth.sessionBootstrap.rateLimitedBody")
29
+ : t("auth.sessionBootstrap.errorBody") }), _jsx(Button, { variant: "primary", onClick: handleRetry, disabled: retrying, children: retrying ? t("auth.sessionBootstrap.retrying") : t("auth.sessionBootstrap.retry") }), onSignOut !== undefined && (_jsx(Button, { variant: "secondary", onClick: () => void onSignOut(), disabled: retrying, children: t("auth.sessionBootstrap.signOut") }))] }) }));
30
30
  }
@@ -77,10 +77,10 @@ export function SignupCompleteScreen({ title, token: tokenProp, loggedInHref = "
77
77
  // Kein Token in der URL → klare Message statt Form ohne Token
78
78
  // (würde nur invalidSignupToken zeigen, verwirrend).
79
79
  if (token === "") {
80
- return (_jsx(AuthCard, { title: effectiveTitle, children: _jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.signupComplete.missingToken") }), _jsx(Link, { href: loginHref, variant: "muted", children: t("auth.signup.haveAccount") })] }) }));
80
+ return (_jsxs(AuthCard, { title: effectiveTitle, children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.signupComplete.missingToken") }), _jsx(Link, { href: loginHref, variant: "muted", children: t("auth.signup.haveAccount") })] }));
81
81
  }
82
82
  if (continueHref !== null) {
83
- return (_jsx(AuthCard, { title: effectiveTitle, children: _jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", role: "status", children: t("auth.signupComplete.activated") }), _jsx(Link, { href: continueHref, variant: "button", children: t("auth.signupComplete.continue") })] }) }));
83
+ return (_jsxs(AuthCard, { title: effectiveTitle, children: [_jsx("p", { className: "text-sm text-muted-foreground", role: "status", children: t("auth.signupComplete.activated") }), _jsx(Link, { href: continueHref, variant: "button", children: t("auth.signupComplete.continue") })] }));
84
84
  }
85
- return (_jsx(AuthCard, { title: effectiveTitle, children: _jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.signupComplete.intro") }), _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "signup-password", label: t("auth.signupComplete.password"), required: true, children: _jsx(Input, { kind: "password", id: "signup-password", name: "signup-password", value: password, onChange: setPassword, disabled: submitting, required: true, autoComplete: "new-password" }) }), _jsx(Field, { id: "signup-confirm-password", label: t("auth.signupComplete.confirmPassword"), required: true, children: _jsx(Input, { kind: "password", id: "signup-confirm-password", name: "signup-confirm-password", value: confirmPassword, onChange: setConfirmPassword, disabled: submitting, required: true, autoComplete: "new-password" }) }), error !== null && _jsx(Banner, { variant: "error", children: error }), _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.signupComplete.submitting") : t("auth.signupComplete.submit") })] })] }) }));
85
+ return (_jsxs(AuthCard, { title: effectiveTitle, children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.signupComplete.intro") }), _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "signup-password", label: t("auth.signupComplete.password"), required: true, children: _jsx(Input, { kind: "password", id: "signup-password", name: "signup-password", value: password, onChange: setPassword, disabled: submitting, required: true, autoComplete: "new-password" }) }), _jsx(Field, { id: "signup-confirm-password", label: t("auth.signupComplete.confirmPassword"), required: true, children: _jsx(Input, { kind: "password", id: "signup-confirm-password", name: "signup-confirm-password", value: confirmPassword, onChange: setConfirmPassword, disabled: submitting, required: true, autoComplete: "new-password" }) }), error !== null && _jsx(Banner, { variant: "error", children: error }), _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.signupComplete.submitting") : t("auth.signupComplete.submit") })] })] }));
86
86
  }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
3
  // SignupScreen — Magic-Link-Self-Signup, Step 1.
4
4
  //
@@ -67,5 +67,5 @@ export function SignupScreen({ title, subtitle, loginHref = "/login", }) {
67
67
  }
68
68
  };
69
69
  const effectiveTitle = title ?? t("auth.signup.title");
70
- return (_jsx(AuthCard, { title: effectiveTitle, subtitle: subtitle, children: done ? (_jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsxs(Banner, { variant: "info", children: [_jsx("p", { className: "font-medium text-foreground", children: t("auth.signup.successTitle") }), _jsx("p", { className: "mt-1", children: t("auth.signup.successBody") })] }), _jsx(Button, { variant: "secondary", onClick: onResend, disabled: submitting, children: t("auth.signup.resend") }), _jsx(Link, { href: loginHref, variant: "muted", children: t("auth.signup.haveAccount") })] })) : (_jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.signup.intro") }), _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "signup-email", label: t("auth.signup.email"), required: true, children: _jsx(Input, { kind: "email", id: "signup-email", name: "signup-email", value: email, onChange: setEmail, disabled: submitting, required: true, autoComplete: "email" }) }), error !== null && _jsx(Banner, { variant: "error", children: error }), _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.signup.submitting") : t("auth.signup.submit") })] }), _jsx(Link, { href: loginHref, variant: "muted", className: "self-center", children: t("auth.signup.haveAccount") })] })) }));
70
+ return (_jsx(AuthCard, { title: effectiveTitle, subtitle: subtitle, children: done ? (_jsxs(_Fragment, { children: [_jsxs(Banner, { variant: "info", children: [_jsx("p", { className: "font-medium text-foreground", children: t("auth.signup.successTitle") }), _jsx("p", { className: "mt-1", children: t("auth.signup.successBody") })] }), _jsx(Button, { variant: "secondary", onClick: onResend, disabled: submitting, children: t("auth.signup.resend") }), _jsx(Link, { href: loginHref, variant: "muted", children: t("auth.signup.haveAccount") })] })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.signup.intro") }), _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "signup-email", label: t("auth.signup.email"), required: true, children: _jsx(Input, { kind: "email", id: "signup-email", name: "signup-email", value: email, onChange: setEmail, disabled: submitting, required: true, autoComplete: "email" }) }), error !== null && _jsx(Banner, { variant: "error", children: error }), _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.signup.submitting") : t("auth.signup.submit") })] }), _jsx(Link, { href: loginHref, variant: "muted", className: "self-center", children: t("auth.signup.haveAccount") })] })) }));
71
71
  }
@@ -33,14 +33,14 @@ export function VerifyEmailScreen({ title, token: tokenProp, loginHref = "/login
33
33
  })();
34
34
  }, [status, token]);
35
35
  if (status === "missing-token") {
36
- return (_jsx(AuthCard, { title: title ?? t("auth.verifyEmail.errorTitle"), children: _jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.verifyEmail.missingToken") }), _jsx(Link, { href: loginHref, variant: "muted", children: t("auth.verifyEmail.goToLogin") })] }) }));
36
+ return (_jsxs(AuthCard, { title: title ?? t("auth.verifyEmail.errorTitle"), children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.verifyEmail.missingToken") }), _jsx(Link, { href: loginHref, variant: "muted", children: t("auth.verifyEmail.goToLogin") })] }));
37
37
  }
38
38
  if (status === "verifying") {
39
39
  return (_jsx(AuthCard, { children: _jsx("div", { className: "p-6", children: _jsx("p", { className: "text-sm text-muted-foreground", role: "status", children: t("auth.verifyEmail.verifying") }) }) }));
40
40
  }
41
41
  if (status === "success") {
42
- return (_jsx(AuthCard, { title: title ?? t("auth.verifyEmail.successTitle"), children: _jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.verifyEmail.successBody") }), _jsx(Link, { href: loginHref, variant: "button", children: t("auth.verifyEmail.goToLogin") })] }) }));
42
+ return (_jsxs(AuthCard, { title: title ?? t("auth.verifyEmail.successTitle"), children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.verifyEmail.successBody") }), _jsx(Link, { href: loginHref, variant: "button", children: t("auth.verifyEmail.goToLogin") })] }));
43
43
  }
44
44
  // status === "error"
45
- return (_jsx(AuthCard, { title: title ?? t("auth.verifyEmail.errorTitle"), children: _jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.verifyEmail.errorBody") }), _jsx(Link, { href: loginHref, variant: "muted", children: t("auth.verifyEmail.goToLogin") })] }) }));
45
+ return (_jsxs(AuthCard, { title: title ?? t("auth.verifyEmail.errorTitle"), children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.verifyEmail.errorBody") }), _jsx(Link, { href: loginHref, variant: "muted", children: t("auth.verifyEmail.goToLogin") })] }));
46
46
  }
@@ -127,9 +127,9 @@ export function MfaSetupPreauthScreen({ preauthSetupToken, accountLabel, title,
127
127
  e?.preventDefault();
128
128
  void confirmSetup();
129
129
  };
130
- return (_jsx(AuthCard, { title: title ?? t("auth.mfa.setup.title"), subtitle: subtitle ?? t("auth.mfa.setup.subtitle"), children: _jsxs("div", { className: "p-6 pt-0 flex flex-col gap-4", children: [error !== null ? (_jsx(Banner, { variant: "error", children: error === "too_many_attempts" && retryAfterSeconds !== null
131
- ? t("auth.mfa.errors.tooManyAttemptsWithSeconds", { seconds: retryAfterSeconds })
132
- : t(reasonToKey(error)) })) : null, !setup && (_jsx(Section, { testId: "mfa-setup-preauth-intro", actions: _jsx(Button, { variant: "primary", onClick: () => void startSetup(), loading: busy, disabled: busy, children: t("auth.mfa.setup.start") }), children: _jsx("span", { className: "text-sm text-muted-foreground", children: t("auth.mfa.setup.intro") }) })), setup && (_jsx(Form, { onSubmit: onSubmit, children: _jsxs(Section, { testId: "mfa-setup-preauth-setup", actions: _jsx(Button, { type: "submit", variant: "primary", loading: busy, disabled: busy || !acknowledged || code.length !== 6, children: t("auth.mfa.setup.confirm") }), children: [_jsxs("div", { className: "flex flex-col items-center gap-2 text-center", children: [_jsx("span", { className: "text-sm font-semibold", children: t("auth.mfa.enable.scanTitle") }), _jsx("div", { className: "h-40 w-40",
133
- // biome-ignore lint/security/noDangerouslySetInnerHtml: qrcode-generated SVG, no user input
134
- dangerouslySetInnerHTML: { __html: setup.qrSvg } }), _jsx("span", { className: "text-xs text-muted-foreground", children: t("auth.mfa.enable.manualEntry") }), _jsx("code", { className: "inline-block break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: setup.secretParam })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { className: "text-sm font-semibold", children: t("auth.mfa.enable.recoveryTitle") }), _jsx("span", { className: "text-xs text-muted-foreground", children: t("auth.mfa.enable.recoveryHint") }), _jsx("code", { className: "block whitespace-pre-wrap break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: setup.recoveryCodes.join("\n") }), _jsx(Field, { id: "mfa-setup-preauth-ack", label: t("auth.mfa.enable.acknowledge"), children: _jsx(Input, { kind: "boolean", id: "mfa-setup-preauth-ack", name: "mfa-setup-preauth-ack", value: acknowledged, onChange: setAcknowledged }) })] }), _jsx(Field, { id: "mfa-setup-preauth-code", label: t("auth.mfa.enable.code"), required: true, children: _jsx(Input, { kind: "text", id: "mfa-setup-preauth-code", name: "mfa-setup-preauth-code", value: code, onChange: setCode, disabled: busy || !acknowledged, autoComplete: "one-time-code" }) })] }) })), onCancel ? (_jsx("div", { className: "flex justify-center", children: _jsx(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: busy, children: t("auth.mfa.verify.backToLogin") }) })) : null] }) }));
130
+ return (_jsxs(AuthCard, { title: title ?? t("auth.mfa.setup.title"), subtitle: subtitle ?? t("auth.mfa.setup.subtitle"), children: [error !== null ? (_jsx(Banner, { variant: "error", children: error === "too_many_attempts" && retryAfterSeconds !== null
131
+ ? t("auth.mfa.errors.tooManyAttemptsWithSeconds", { seconds: retryAfterSeconds })
132
+ : t(reasonToKey(error)) })) : null, !setup && (_jsx(Section, { testId: "mfa-setup-preauth-intro", actions: _jsx(Button, { variant: "primary", onClick: () => void startSetup(), loading: busy, disabled: busy, children: t("auth.mfa.setup.start") }), children: _jsx("span", { className: "text-sm text-muted-foreground", children: t("auth.mfa.setup.intro") }) })), setup && (_jsx(Form, { onSubmit: onSubmit, children: _jsxs(Section, { testId: "mfa-setup-preauth-setup", actions: _jsx(Button, { type: "submit", variant: "primary", loading: busy, disabled: busy || !acknowledged || code.length !== 6, children: t("auth.mfa.setup.confirm") }), children: [_jsxs("div", { className: "flex flex-col items-center gap-2 text-center", children: [_jsx("span", { className: "text-sm font-semibold", children: t("auth.mfa.enable.scanTitle") }), _jsx("div", { className: "h-40 w-40",
133
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: qrcode-generated SVG, no user input
134
+ dangerouslySetInnerHTML: { __html: setup.qrSvg } }), _jsx("span", { className: "text-xs text-muted-foreground", children: t("auth.mfa.enable.manualEntry") }), _jsx("code", { className: "inline-block break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: setup.secretParam })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { className: "text-sm font-semibold", children: t("auth.mfa.enable.recoveryTitle") }), _jsx("span", { className: "text-xs text-muted-foreground", children: t("auth.mfa.enable.recoveryHint") }), _jsx("code", { className: "block whitespace-pre-wrap break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: setup.recoveryCodes.join("\n") }), _jsx(Field, { id: "mfa-setup-preauth-ack", label: t("auth.mfa.enable.acknowledge"), children: _jsx(Input, { kind: "boolean", id: "mfa-setup-preauth-ack", name: "mfa-setup-preauth-ack", value: acknowledged, onChange: setAcknowledged }) })] }), _jsx(Field, { id: "mfa-setup-preauth-code", label: t("auth.mfa.enable.code"), required: true, children: _jsx(Input, { kind: "text", id: "mfa-setup-preauth-code", name: "mfa-setup-preauth-code", value: code, onChange: setCode, disabled: busy || !acknowledged, autoComplete: "one-time-code" }) })] }) })), onCancel ? (_jsx("div", { className: "flex justify-center", children: _jsx(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: busy, children: t("auth.mfa.verify.backToLogin") }) })) : null] }));
135
135
  }
@@ -52,7 +52,7 @@ export function MfaVerifyScreen({ challengeToken, title, subtitle, submitLabel,
52
52
  e?.preventDefault();
53
53
  void doSubmit();
54
54
  };
55
- return (_jsx(AuthCard, { title: title ?? t("auth.mfa.verify.title"), subtitle: subtitle ?? t("auth.mfa.verify.subtitle"), children: _jsx("div", { className: "p-6 pt-0 flex flex-col gap-4", children: _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "mfa-verify-code", label: t("auth.mfa.verify.code"), required: true, children: _jsx(Input, { kind: "text", id: "mfa-verify-code", name: "mfa-verify-code", value: code, onChange: setCode, disabled: submitting, required: true, autoComplete: "one-time-code" }) }), error !== null ? _jsx(Banner, { variant: "error", children: t(reasonToKey(error)) }) : null, _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting
56
- ? t("auth.mfa.verify.submitting")
57
- : (submitLabel ?? t("auth.mfa.verify.submit")) }), onCancel ? (_jsx(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: submitting, children: t("auth.mfa.verify.backToLogin") })) : null] }) }) }));
55
+ return (_jsx(AuthCard, { title: title ?? t("auth.mfa.verify.title"), subtitle: subtitle ?? t("auth.mfa.verify.subtitle"), children: _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "mfa-verify-code", label: t("auth.mfa.verify.code"), required: true, children: _jsx(Input, { kind: "text", id: "mfa-verify-code", name: "mfa-verify-code", value: code, onChange: setCode, disabled: submitting, required: true, autoComplete: "one-time-code" }) }), error !== null ? _jsx(Banner, { variant: "error", children: t(reasonToKey(error)) }) : null, _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting
56
+ ? t("auth.mfa.verify.submitting")
57
+ : (submitLabel ?? t("auth.mfa.verify.submit")) }), onCancel ? (_jsx(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: submitting, children: t("auth.mfa.verify.backToLogin") })) : null] }) }));
58
58
  }
@@ -27,6 +27,7 @@ export function createChannelDiscordFeature(options = {}) {
27
27
  valueSchema: chatWebhookUrlSchema(webhookTarget),
28
28
  });
29
29
  const channel = createDiscordChannel(options, webhooks.keyFor);
30
+ r.translations({ keys: { "delivery.channel.discord": { en: "Discord" } } });
30
31
  r.useExtension(DELIVERY_CHANNEL_EXTENSION, "discord", {
31
32
  mode: channel.mode,
32
33
  send: channel.send,
@@ -44,6 +44,7 @@ function emailEnvelopeFrom(data) {
44
44
  if (!data)
45
45
  return {};
46
46
  const from = typeof data["from"] === "string" ? data["from"] : undefined;
47
+ const fromName = typeof data["fromName"] === "string" ? data["fromName"] : undefined;
47
48
  const replyTo = typeof data["replyTo"] === "string" ? data["replyTo"] : undefined;
48
49
  const autoHeaders = listUnsubscribeHeaders(data);
49
50
  const explicitHeaders = stringHeaders(data["headers"]);
@@ -55,7 +56,12 @@ function emailEnvelopeFrom(data) {
55
56
  const headers = (!overridesUnsubscribe && autoHeaders) || explicitHeaders
56
57
  ? { ...(overridesUnsubscribe ? undefined : autoHeaders), ...explicitHeaders }
57
58
  : undefined;
58
- return { ...(from && { from }), ...(replyTo && { replyTo }), ...(headers && { headers }) };
59
+ return {
60
+ ...(from && { from }),
61
+ ...(fromName && { fromName }),
62
+ ...(replyTo && { replyTo }),
63
+ ...(headers && { headers }),
64
+ };
59
65
  }
60
66
  export function createEmailChannel(options) {
61
67
  const { transport, renderer, resolveEmail } = options;
@@ -11,6 +11,7 @@ export function createChannelEmailFeature(options) {
11
11
  recommended: false,
12
12
  });
13
13
  r.requires("delivery");
14
+ r.translations({ keys: { "delivery.channel.email": { en: "Email" } } });
14
15
  r.useExtension(DELIVERY_CHANNEL_EXTENSION, "email", {
15
16
  mode: channel.mode,
16
17
  resolve: channel.resolve,
@@ -9,15 +9,16 @@ const CIPHERTEXT_RE = /kumiko-pii:v\d+:[^"\s<>\\]*/g;
9
9
  // blob); ciphertext in subject/body fails loud in dev and is redacted+logged
10
10
  // in prod.
11
11
  export function guardEmailMessage(message) {
12
- // Every envelope address, not just `to`: a ciphertext From or Reply-To is
12
+ // Every envelope address, not just `to`: a ciphertext From, From-Name or Reply-To is
13
13
  // the same garbage as a ciphertext recipient — refuse rather than mail it.
14
14
  for (const [label, address] of [
15
- ["recipient", message.to],
16
- ["from", message.from],
17
- ["reply-to", message.replyTo],
15
+ ["recipient address", message.to],
16
+ ["from address", message.from],
17
+ ["from name", message.fromName],
18
+ ["reply-to address", message.replyTo],
18
19
  ]) {
19
20
  if (address?.includes(CIPHERTEXT_MARKER)) {
20
- throw new Error(`[channel-email] refusing to send: ${label} address is a PII ciphertext ` +
21
+ throw new Error(`[channel-email] refusing to send: ${label} is a PII ciphertext ` +
21
22
  `("${PII_CIPHERTEXT_PREFIX}…") — decrypt the stored value before mailing (decryptStoredPii).`);
22
23
  }
23
24
  }
@@ -16,6 +16,27 @@
16
16
  // Volume-Apps muss der Caller eine eigene Implementation mit
17
17
  // dedizierter Queue (BullMQ + retry) drüberlegen.
18
18
  import { createTransport } from "nodemailer";
19
+ import addressparser from "nodemailer/lib/addressparser";
20
+ function isControlCharacter(char) {
21
+ const code = char.charCodeAt(0);
22
+ return code <= 0x1f || code === 0x7f;
23
+ }
24
+ // Control characters in a display name would let a caller inject headers.
25
+ function sanitizedDisplayName(name) {
26
+ const cleaned = name
27
+ ?.split("")
28
+ .map((char) => (isControlCharacter(char) ? " " : char))
29
+ .join("")
30
+ .trim();
31
+ return cleaned === undefined || cleaned === "" ? undefined : cleaned;
32
+ }
33
+ function fromHeader(address, fromName) {
34
+ const name = sanitizedDisplayName(fromName);
35
+ if (name === undefined)
36
+ return address;
37
+ const [parsed] = addressparser(address, { flatten: true });
38
+ return parsed?.address ? { name, address: parsed.address } : address;
39
+ }
19
40
  export function createSmtpTransport(options) {
20
41
  const transporter = createTransport({
21
42
  host: options.host,
@@ -29,7 +50,7 @@ export function createSmtpTransport(options) {
29
50
  return {
30
51
  async send(message) {
31
52
  await transporter.sendMail({
32
- from: message.from ?? options.from,
53
+ from: fromHeader(message.from ?? options.from, message.fromName),
33
54
  to: message.to,
34
55
  subject: message.subject,
35
56
  html: message.html,
@@ -3,6 +3,7 @@ export type EmailMessage = {
3
3
  readonly subject: string;
4
4
  readonly html: string;
5
5
  readonly from?: string;
6
+ readonly fromName?: string;
6
7
  readonly replyTo?: string;
7
8
  readonly headers?: Readonly<Record<string, string>>;
8
9
  };
@@ -19,6 +19,7 @@ export function createChannelInAppFeature() {
19
19
  reason: "read_side.in_app_messages",
20
20
  });
21
21
  // Register as delivery channel via extension system
22
+ r.translations({ keys: { "delivery.channel.inApp": { en: "In-app" } } });
22
23
  r.useExtension(DELIVERY_CHANNEL_EXTENSION, "inApp", {
23
24
  mode: inAppChannel.mode,
24
25
  resolve: inAppChannel.resolve,
@@ -11,6 +11,7 @@ export function createChannelPushFeature(options) {
11
11
  recommended: false,
12
12
  });
13
13
  r.requires("delivery");
14
+ r.translations({ keys: { "delivery.channel.push": { en: "Push" } } });
14
15
  r.useExtension(DELIVERY_CHANNEL_EXTENSION, "push", {
15
16
  mode: channel.mode,
16
17
  resolve: channel.resolve,
@@ -24,6 +24,7 @@ export function createChannelSlackFeature(options = {}) {
24
24
  valueSchema: chatWebhookUrlSchema(webhookTarget),
25
25
  });
26
26
  const channel = createSlackChannel(options, webhooks.keyFor);
27
+ r.translations({ keys: { "delivery.channel.slack": { en: "Slack" } } });
27
28
  r.useExtension(DELIVERY_CHANNEL_EXTENSION, "slack", {
28
29
  mode: channel.mode,
29
30
  send: channel.send,
@@ -28,6 +28,7 @@ export function createChannelTeamsFeature(options = {}) {
28
28
  valueSchema: chatWebhookUrlSchema(webhookTarget),
29
29
  });
30
30
  const channel = createTeamsChannel(options, webhooks.keyFor);
31
+ r.translations({ keys: { "delivery.channel.teams": { en: "Microsoft Teams" } } });
31
32
  r.useExtension(DELIVERY_CHANNEL_EXTENSION, "teams", {
32
33
  mode: channel.mode,
33
34
  send: channel.send,
@@ -1,4 +1,4 @@
1
- import { type ChatWebhookChannelOptions, type DeliveryChannel } from "../delivery/index.js";
1
+ import { type ChatSendResult, type ChatWebhookChannelOptions, type ChatWebhookResponse, type DeliveryChannel } from "../delivery/index.js";
2
2
  export type TeamsChannelOptions = ChatWebhookChannelOptions;
3
3
  export declare const TEAMS_SECRET_KEYS: {
4
4
  readonly webhookPrefix: "channel-teams:webhooks.";
@@ -6,4 +6,5 @@ export declare const TEAMS_SECRET_KEYS: {
6
6
  };
7
7
  export declare const TEAMS_DEFAULT_ALLOWED_HOSTS: readonly [".webhook.office.com", ".logic.azure.com", ".powerplatform.com"];
8
8
  export declare function buildTeamsAdaptiveCard(title: string, body: string | undefined): unknown;
9
+ export declare function classifyTeamsResponse(response: ChatWebhookResponse): Promise<ChatSendResult>;
9
10
  export declare function createTeamsChannel(options: TeamsChannelOptions, keyFor: (connection: string) => string): DeliveryChannel;
@@ -30,12 +30,24 @@ export function buildTeamsAdaptiveCard(title, body) {
30
30
  ],
31
31
  };
32
32
  }
33
+ // Classic Office connectors answer 200 with the body "1". Workflow endpoints answer
34
+ // 202 and never say whether the card reached the channel. Any other 2xx (an empty
35
+ // 200 is what a made-up URL returns) is not a Teams success.
36
+ export async function classifyTeamsResponse(response) {
37
+ if (response.status === 202)
38
+ return { ok: true, confirmed: false };
39
+ if (response.status === 200 && (await response.readBodyPrefix()).trim() === "1") {
40
+ return { ok: true, confirmed: true };
41
+ }
42
+ return { ok: false, code: "unexpected_response" };
43
+ }
33
44
  export function createTeamsChannel(options, keyFor) {
34
45
  return createChatWebhookChannel({
35
46
  name: "teams",
36
47
  featureName: "channel-teams",
37
48
  defaultAllowedHosts: TEAMS_DEFAULT_ALLOWED_HOSTS,
38
49
  keyFor,
50
+ classifyResponse: classifyTeamsResponse,
39
51
  buildBody: (message) => buildTeamsAdaptiveCard(message.title, message.body),
40
52
  options,
41
53
  });
@@ -22,6 +22,7 @@ export function createChannelTelegramFeature(options = {}) {
22
22
  valueSchema: z.string().refine(isTelegramBotToken, "invalid bot token"),
23
23
  });
24
24
  const channel = createTelegramChannel(options, botToken.name);
25
+ r.translations({ keys: { "delivery.channel.telegram": { en: "Telegram" } } });
25
26
  r.useExtension(DELIVERY_CHANNEL_EXTENSION, "telegram", {
26
27
  mode: channel.mode,
27
28
  send: channel.send,
@@ -1,4 +1,4 @@
1
- import { type ChatSendResult, type ChatWebhookTarget } from "./chat-webhook-sender.js";
1
+ import { type ChatSendResult, type ChatWebhookRequest, type ChatWebhookTarget } from "./chat-webhook-sender.js";
2
2
  import type { ChannelMessage, ChannelResult, DeliveryChannel } from "./types.js";
3
3
  export type ChatWebhookChannelOptions = {
4
4
  readonly allowedHosts?: readonly string[];
@@ -18,6 +18,7 @@ export type ChatWebhookChannelSpec = {
18
18
  readonly requiredPathPrefix?: string;
19
19
  readonly keyFor: (connection: string) => string;
20
20
  readonly buildBody: (message: ChannelMessage) => unknown;
21
+ readonly classifyResponse?: ChatWebhookRequest["classifyResponse"];
21
22
  readonly options: ChatWebhookChannelOptions;
22
23
  };
23
24
  export declare function createChatWebhookChannel(spec: ChatWebhookChannelSpec): DeliveryChannel;
@@ -1,9 +1,11 @@
1
1
  import { SYSTEM_USER_ID } from "@cosmicdrift/kumiko-types/identifiers";
2
2
  import { chatConnectionNameSchema, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, } from "./chat-webhook-sender.js";
3
3
  export function toChannelResult(address, result) {
4
- return result.ok
4
+ if (!result.ok)
5
+ return { status: "failed", error: result.code, address };
6
+ return result.confirmed
5
7
  ? { status: "sent", address }
6
- : { status: "failed", error: result.code, address };
8
+ : { status: "sent", confirmed: false, address };
7
9
  }
8
10
  export function chatMessageText(message) {
9
11
  return message.body ? `${message.title}\n${message.body}` : message.title;
@@ -46,6 +48,7 @@ export function createChatWebhookChannel(spec) {
46
48
  ...resolveChatWebhookTarget(spec, options),
47
49
  timeoutMs: options.timeoutMs ?? DEFAULT_CHAT_TIMEOUT_MS,
48
50
  body: spec.buildBody(message),
51
+ ...(spec.classifyResponse && { classifyResponse: spec.classifyResponse }),
49
52
  });
50
53
  return toChannelResult(address, result);
51
54
  },
@@ -1,11 +1,17 @@
1
+ import type { ChatSendFailureCode } from "@cosmicdrift/kumiko-framework/engine";
1
2
  import * as z from "zod";
2
- export type ChatSendFailureCode = `http_${number}` | "timeout" | "network_error" | "redirect_blocked" | "host_not_allowed" | "missing_credentials" | "invalid_address";
3
+ export type { ChatSendFailureCode };
3
4
  export type ChatSendResult = {
4
5
  readonly ok: true;
6
+ readonly confirmed: boolean;
5
7
  } | {
6
8
  readonly ok: false;
7
9
  readonly code: ChatSendFailureCode;
8
10
  };
11
+ export type ChatWebhookResponse = {
12
+ readonly status: number;
13
+ readBodyPrefix(): Promise<string>;
14
+ };
9
15
  export declare const DEFAULT_CHAT_TIMEOUT_MS = 10000;
10
16
  export declare const chatConnectionNameSchema: z.ZodString;
11
17
  export type ChatWebhookTarget = {
@@ -18,6 +24,7 @@ export type ChatWebhookRequest = ChatWebhookTarget & {
18
24
  readonly timeoutMs: number;
19
25
  readonly body: unknown;
20
26
  readonly send?: (url: string, init: RequestInit) => Promise<Response>;
27
+ readonly classifyResponse?: (response: ChatWebhookResponse) => Promise<ChatSendResult>;
21
28
  };
22
29
  export declare function checkChatWebhookTarget(target: ChatWebhookTarget, url: string): URL | undefined;
23
30
  export declare function chatWebhookUrlSchema(target: ChatWebhookTarget): z.ZodType<string>;
@@ -1,5 +1,6 @@
1
1
  import { BlockedHostError, egress } from "@cosmicdrift/kumiko-framework/http";
2
2
  import * as z from "zod";
3
+ const MAX_BODY_PREFIX_BYTES = 64;
3
4
  export const DEFAULT_CHAT_TIMEOUT_MS = 10_000;
4
5
  // Tenant-chosen connection name (the route address for webhook channels). Also
5
6
  // the suffix the secrets write gate accepts under the channel's namespace, so
@@ -92,15 +93,55 @@ export async function postChatWebhook(request) {
92
93
  catch (err) {
93
94
  return { ok: false, code: classifyFailure(err) };
94
95
  }
95
- // Free the connection; the body is never read or stored.
96
- await response.body?.cancel().catch(() => undefined);
97
96
  const { status } = response;
98
- if (status >= 300 && status < 400)
97
+ if (status >= 300 && status < 400) {
98
+ await response.body?.cancel().catch(() => undefined);
99
99
  return { ok: false, code: "redirect_blocked" };
100
+ }
100
101
  if (status >= 200 && status < 300)
101
- return { ok: true };
102
+ return classifySuccess(request, response);
103
+ await response.body?.cancel().catch(() => undefined);
102
104
  return { ok: false, code: `http_${status}` };
103
105
  }
106
+ // The body is cancelled afterwards in every case, even when the classifier throws,
107
+ // so the connection is freed and a large body is never read to the end.
108
+ async function classifySuccess(request, response) {
109
+ let reader;
110
+ try {
111
+ if (!request.classifyResponse)
112
+ return { ok: true, confirmed: true };
113
+ return await request.classifyResponse({
114
+ status: response.status,
115
+ async readBodyPrefix() {
116
+ reader ??= response.body?.getReader();
117
+ if (!reader)
118
+ return "";
119
+ const chunks = [];
120
+ let total = 0;
121
+ while (total < MAX_BODY_PREFIX_BYTES) {
122
+ const { done, value } = await reader.read();
123
+ if (done)
124
+ break;
125
+ chunks.push(value);
126
+ total += value.length;
127
+ }
128
+ const joined = new Uint8Array(total);
129
+ let offset = 0;
130
+ for (const chunk of chunks) {
131
+ joined.set(chunk, offset);
132
+ offset += chunk.length;
133
+ }
134
+ return new TextDecoder().decode(joined.subarray(0, MAX_BODY_PREFIX_BYTES));
135
+ },
136
+ });
137
+ }
138
+ catch (err) {
139
+ return { ok: false, code: classifyFailure(err) };
140
+ }
141
+ finally {
142
+ await (reader ? reader.cancel() : response.body?.cancel())?.catch(() => undefined);
143
+ }
144
+ }
104
145
  export function truncateChars(text, maxChars) {
105
146
  const chars = Array.from(text);
106
147
  return chars.length <= maxChars ? text : chars.slice(0, maxChars).join("");
@@ -1,6 +1,6 @@
1
1
  import type { NotifyPriority } from "@cosmicdrift/kumiko-framework/engine";
2
2
  export declare const DELIVERY_CHANNEL_EXTENSION: "deliveryChannel";
3
- export { DELIVERY_ATTEMPT_EVENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, type DeliveryStatusValue, } from "./public-names.js";
3
+ export { DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, type DeliveryStatusValue, } from "./public-names.js";
4
4
  export declare const deliveryPriorityRank: Record<NotifyPriority, number>;
5
5
  export declare const DeliveryJobs: {
6
6
  readonly render: string;
@@ -3,7 +3,7 @@ import { DELIVERY_FEATURE, DeliveryJobNames } from "./public-names.js";
3
3
  // Extension-point name for delivery-channel plugins (channel-email,
4
4
  // channel-in-app, channel-push, ...).
5
5
  export const DELIVERY_CHANNEL_EXTENSION = "deliveryChannel";
6
- export { DELIVERY_ATTEMPT_EVENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, } from "./public-names.js";
6
+ export { DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, } from "./public-names.js";
7
7
  // notify() priority → BullMQ job priority. Lower number = processed first; all
8
8
  // > 0 so prioritised delivery jobs never mix with BullMQ's "0 = unprioritised
9
9
  // FIFO" bucket. critical jobs jump ahead of normal/low in the worker queue.
@@ -1,10 +1,11 @@
1
1
  import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
2
2
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
3
  import type { EscapeHatchAuditSink, NotifyJobDispatcher, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
- import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
4
+ import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
5
5
  import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
6
6
  import type { Redis } from "ioredis";
7
7
  import { type DeliveryChannel, type DeliveryService } from "./types.js";
8
+ export declare function redactedMessageOf(err: unknown): string;
8
9
  export type RateLimitConfig = {
9
10
  readonly redis: Redis;
10
11
  readonly maxPerHour: number;