@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.
- package/dist/audit/constants.d.ts +7 -0
- package/dist/audit/constants.js +7 -0
- package/dist/audit/escape-hatch-audit-sink.d.ts +1 -0
- package/dist/audit/escape-hatch-audit-sink.js +1 -0
- package/dist/audit/feature.js +34 -7
- package/dist/audit/run-escape-hatch-retention.d.ts +11 -0
- package/dist/audit/run-escape-hatch-retention.js +91 -0
- package/dist/auth-email-password/bootstrap.d.ts +8 -2
- package/dist/auth-email-password/bootstrap.js +7 -2
- package/dist/auth-email-password/email-templates.d.ts +4 -0
- package/dist/auth-email-password/email-templates.js +69 -16
- package/dist/auth-email-password/handlers/invite-create.write.js +4 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +4 -1
- package/dist/auth-email-password/handlers/token-request-handler.js +3 -0
- package/dist/auth-email-password/magic-link-mail.d.ts +2 -0
- package/dist/auth-email-password/magic-link-mail.js +2 -0
- package/dist/auth-email-password/web/auth-form-primitives.d.ts +7 -1
- package/dist/auth-email-password/web/auth-form-primitives.js +5 -5
- package/dist/auth-email-password/web/confirm-account-unlock-screen.js +3 -3
- package/dist/auth-email-password/web/forgot-password-screen.js +2 -2
- package/dist/auth-email-password/web/invite-accept-screen.js +9 -9
- package/dist/auth-email-password/web/login-screen.js +8 -8
- package/dist/auth-email-password/web/request-account-unlock-screen.js +2 -2
- package/dist/auth-email-password/web/reset-password-screen.js +3 -3
- package/dist/auth-email-password/web/session-bootstrap-error.js +3 -3
- package/dist/auth-email-password/web/signup-complete-screen.js +3 -3
- package/dist/auth-email-password/web/signup-screen.js +2 -2
- package/dist/auth-email-password/web/verify-email-screen.js +3 -3
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +5 -5
- package/dist/auth-mfa/web/mfa-verify-screen.js +3 -3
- package/dist/channel-discord/feature.js +1 -0
- package/dist/channel-email/email-channel.js +7 -1
- package/dist/channel-email/feature.js +1 -0
- package/dist/channel-email/pii-guard.js +6 -5
- package/dist/channel-email/smtp-transport.js +22 -1
- package/dist/channel-email/types.d.ts +1 -0
- package/dist/channel-in-app/feature.js +1 -0
- package/dist/channel-push/feature.js +1 -0
- package/dist/channel-slack/feature.js +1 -0
- package/dist/channel-teams/feature.js +1 -0
- package/dist/channel-teams/teams-channel.d.ts +2 -1
- package/dist/channel-teams/teams-channel.js +12 -0
- package/dist/channel-telegram/feature.js +1 -0
- package/dist/delivery/chat-webhook-channel.d.ts +2 -1
- package/dist/delivery/chat-webhook-channel.js +5 -2
- package/dist/delivery/chat-webhook-sender.d.ts +8 -1
- package/dist/delivery/chat-webhook-sender.js +45 -4
- package/dist/delivery/constants.d.ts +1 -1
- package/dist/delivery/constants.js +1 -1
- package/dist/delivery/delivery-service.d.ts +2 -1
- package/dist/delivery/delivery-service.js +55 -4
- package/dist/delivery/events.d.ts +19 -1
- package/dist/delivery/events.js +11 -1
- package/dist/delivery/feature.js +29 -5
- package/dist/delivery/handlers/log.query.d.ts +4 -2
- package/dist/delivery/handlers/log.query.js +46 -18
- package/dist/delivery/i18n.js +36 -0
- package/dist/delivery/index.d.ts +2 -1
- package/dist/delivery/index.js +1 -0
- package/dist/delivery/jobs.js +24 -10
- package/dist/delivery/notify-factory.d.ts +5 -0
- package/dist/delivery/notify-factory.js +6 -0
- package/dist/delivery/public-names.d.ts +4 -0
- package/dist/delivery/public-names.js +4 -0
- package/dist/delivery/tables.js +3 -0
- package/dist/delivery/testing.d.ts +2 -0
- package/dist/delivery/testing.js +2 -1
- package/dist/delivery/types.d.ts +5 -3
- package/dist/delivery/web/client-plugin.js +9 -1
- package/dist/delivery/web/delivery-channel-cell.d.ts +3 -0
- package/dist/delivery/web/delivery-channel-cell.js +10 -0
- package/dist/delivery/web/delivery-error-cell.d.ts +3 -0
- package/dist/delivery/web/delivery-error-cell.js +16 -0
- package/dist/delivery/web/delivery-status-cell.d.ts +1 -1
- package/dist/delivery/web/delivery-status-cell.js +11 -1
- package/dist/delivery/web/delivery-time-cell.d.ts +3 -0
- package/dist/delivery/web/delivery-time-cell.js +6 -0
- package/dist/delivery/web/delivery-type-cell.d.ts +3 -0
- package/dist/delivery/web/delivery-type-cell.js +17 -0
- package/dist/secrets/constants.d.ts +6 -0
- package/dist/secrets/constants.js +8 -0
- package/dist/secrets/feature.d.ts +3 -0
- package/dist/secrets/feature.js +3 -0
- package/dist/secrets/i18n.js +4 -3
- package/dist/secrets/index.d.ts +3 -2
- package/dist/secrets/index.js +3 -2
- package/dist/secrets/write-gate.d.ts +1 -0
- package/dist/secrets/write-gate.js +14 -6
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/redact.d.ts +4 -0
- package/dist/shared/redact.js +21 -0
- package/dist/step-dispatcher/feature.js +2 -1
- package/dist/step-dispatcher/mail-runner.d.ts +0 -1
- package/dist/step-dispatcher/mail-runner.js +0 -5
- package/dist/tenant/web/member-roles-cell.js +1 -1
- package/dist/tenant/web/member-status-cell.js +1 -1
- package/dist/tenant-lifecycle/feature.js +8 -2
- package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +2 -1
- package/dist/tenant-lifecycle/run-tenant-destroy.js +16 -7
- package/dist/user/index.d.ts +1 -0
- package/dist/user/index.js +1 -0
- package/dist/user/resolve-display-names.d.ts +2 -0
- package/dist/user/resolve-display-names.js +24 -0
- package/dist/waitlist/handlers/submit.write.js +1 -1
- package/dist/waitlist/options.d.ts +3 -2
- package/package.json +9 -9
- package/src/audit/changes.json +6 -0
- package/src/auth-email-password/changes.json +19 -0
- package/src/channel-email/changes.json +6 -0
- package/src/channel-teams/changes.json +8 -0
- package/src/delivery/changes.json +32 -0
- package/src/jobs/changes.json +6 -0
- package/src/secrets/changes.json +6 -0
- package/src/waitlist/changes.json +6 -0
- /package/dist/{tenant → shared}/web/translate-or-raw.d.ts +0 -0
- /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 (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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(
|
|
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 (
|
|
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(
|
|
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:
|
|
28
|
-
|
|
29
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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(
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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:
|
|
56
|
-
|
|
57
|
-
|
|
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 {
|
|
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
|
-
["
|
|
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}
|
|
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,
|
|
@@ -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
|
-
|
|
4
|
+
if (!result.ok)
|
|
5
|
+
return { status: "failed", error: result.code, address };
|
|
6
|
+
return result.confirmed
|
|
5
7
|
? { status: "sent", address }
|
|
6
|
-
: { status: "
|
|
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
|
|
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
|
|
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
|
|
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;
|