@cosmicdrift/kumiko-bundled-features 0.340.0 → 0.341.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/auth-email-password/bootstrap.d.ts +74 -0
- package/dist/auth-email-password/bootstrap.js +165 -0
- package/dist/auth-email-password/constants.d.ts +2 -0
- package/dist/auth-email-password/constants.js +5 -0
- package/dist/auth-email-password/errors.d.ts +1 -0
- package/dist/auth-email-password/errors.js +6 -0
- package/dist/auth-email-password/feature.js +2 -1
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +9 -1
- package/dist/auth-email-password/handlers/invite-accept.write.js +8 -0
- package/dist/auth-email-password/handlers/invite-create.write.d.ts +11 -0
- package/dist/auth-email-password/handlers/invite-create.write.js +134 -87
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +5 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +1 -0
- package/dist/auth-email-password/handlers/token-request-handler.js +1 -0
- package/dist/auth-email-password/index.d.ts +2 -0
- package/dist/auth-email-password/index.js +1 -0
- package/dist/auth-email-password/invite-token-store.d.ts +3 -0
- package/dist/auth-email-password/invite-token-store.js +5 -0
- package/dist/auth-email-password/magic-link-mail.js +1 -0
- package/dist/auth-mfa/config.d.ts +1 -1
- package/dist/auth-mfa/config.js +8 -3
- package/dist/auth-mfa/feature.d.ts +2 -0
- package/dist/auth-mfa/feature.js +2 -1
- package/dist/auth-mfa/mfa-status-checker.d.ts +2 -0
- package/dist/auth-mfa/mfa-status-checker.js +1 -1
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +1 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -0
- package/dist/billing-foundation/consumer-protection/termination-notify.js +1 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.d.ts +8 -1
- package/dist/billing-foundation/consumer-protection/termination-pages.js +59 -32
- package/dist/billing-foundation/consumer-protection/termination-request.js +2 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -1
- package/dist/channel-discord/discord-channel.d.ts +4 -0
- package/dist/channel-discord/discord-channel.js +4 -0
- package/dist/channel-discord/feature.js +7 -2
- package/dist/channel-discord/index.d.ts +1 -1
- package/dist/channel-discord/index.js +1 -1
- package/dist/channel-email/email-channel.js +1 -0
- package/dist/channel-slack/feature.js +4 -2
- package/dist/channel-slack/index.d.ts +1 -1
- package/dist/channel-slack/index.js +1 -1
- package/dist/channel-slack/slack-channel.d.ts +4 -0
- package/dist/channel-slack/slack-channel.js +4 -0
- package/dist/channel-teams/feature.js +4 -2
- package/dist/channel-teams/index.d.ts +1 -1
- package/dist/channel-teams/index.js +1 -1
- package/dist/channel-teams/teams-channel.d.ts +4 -0
- package/dist/channel-teams/teams-channel.js +4 -0
- package/dist/channel-telegram/feature.js +4 -2
- package/dist/channel-telegram/index.d.ts +1 -1
- package/dist/channel-telegram/index.js +1 -1
- package/dist/channel-telegram/telegram-channel.d.ts +8 -0
- package/dist/channel-telegram/telegram-channel.js +28 -12
- package/dist/delivery/chat-webhook-channel.d.ts +5 -1
- package/dist/delivery/chat-webhook-channel.js +10 -5
- package/dist/delivery/chat-webhook-sender.d.ts +6 -2
- package/dist/delivery/chat-webhook-sender.js +13 -8
- package/dist/delivery/delivery-service.d.ts +4 -3
- package/dist/delivery/delivery-service.js +53 -33
- package/dist/delivery/index.d.ts +2 -2
- package/dist/delivery/index.js +2 -2
- package/dist/delivery/jobs.js +3 -1
- package/dist/delivery/types.d.ts +5 -3
- package/dist/legal-pages/feature.d.ts +3 -15
- package/dist/legal-pages/feature.js +20 -50
- package/dist/legal-pages/lib/boot-check.d.ts +10 -0
- package/dist/legal-pages/lib/boot-check.js +33 -0
- package/dist/legal-pages/lib/validate-routes.d.ts +2 -0
- package/dist/legal-pages/lib/validate-routes.js +17 -0
- package/dist/page-render/index.d.ts +2 -1
- package/dist/page-render/index.js +1 -0
- package/dist/page-render/layout.d.ts +7 -0
- package/dist/page-render/layout.js +5 -0
- package/dist/page-render/trailing-slash.d.ts +2 -0
- package/dist/page-render/trailing-slash.js +15 -0
- package/dist/renderer-simple/feature.js +1 -0
- package/dist/renderer-simple/index.d.ts +1 -1
- package/dist/renderer-simple/index.js +1 -1
- package/dist/renderer-simple/simple-renderer.d.ts +12 -5
- package/dist/renderer-simple/simple-renderer.js +66 -12
- package/dist/secrets/announce-secret-change.d.ts +8 -0
- package/dist/secrets/announce-secret-change.js +18 -0
- package/dist/secrets/feature.js +1 -1
- package/dist/secrets/handlers/delete.write.js +2 -0
- package/dist/secrets/handlers/set.write.js +6 -1
- package/dist/secrets/i18n.d.ts +4 -0
- package/dist/secrets/i18n.js +4 -0
- package/dist/secrets/secrets-context.d.ts +2 -0
- package/dist/secrets/secrets-context.js +3 -0
- package/dist/secrets/write-gate.d.ts +5 -1
- package/dist/secrets/write-gate.js +24 -1
- package/dist/tenant/constants.d.ts +1 -0
- package/dist/tenant/constants.js +1 -0
- package/dist/tenant/handlers/add-member.write.js +14 -3
- package/dist/tenant/handlers/invitations.query.d.ts +8 -0
- package/dist/tenant/handlers/invitations.query.js +15 -1
- package/dist/tenant/index.d.ts +1 -0
- package/dist/tenant/index.js +1 -0
- package/dist/tenant/invitation-table.d.ts +2 -0
- package/dist/tenant/invitation-table.js +10 -1
- package/dist/tenant-lifecycle/lifecycle-gate.d.ts +1 -0
- package/dist/tenant-lifecycle/lifecycle-gate.js +2 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
- package/dist/tier-engine/feature.js +83 -14
- package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +3 -3
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +17 -10
- package/dist/user/invited-global-roles.d.ts +14 -0
- package/dist/user/invited-global-roles.js +35 -0
- package/dist/user-data-rights/feature.js +3 -0
- package/dist/waitlist/constants.d.ts +33 -0
- package/dist/waitlist/constants.js +35 -0
- package/dist/waitlist/entity.d.ts +433 -0
- package/dist/waitlist/entity.js +64 -0
- package/dist/waitlist/feature.d.ts +3 -0
- package/dist/waitlist/feature.js +38 -0
- package/dist/waitlist/handlers/invite.write.d.ts +9 -0
- package/dist/waitlist/handlers/invite.write.js +119 -0
- package/dist/waitlist/handlers/list.query.d.ts +1 -0
- package/dist/waitlist/handlers/list.query.js +10 -0
- package/dist/waitlist/handlers/reject.write.d.ts +7 -0
- package/dist/waitlist/handlers/reject.write.js +80 -0
- package/dist/waitlist/handlers/submit.write.d.ts +13 -0
- package/dist/waitlist/handlers/submit.write.js +112 -0
- package/dist/waitlist/i18n.d.ts +6 -0
- package/dist/waitlist/i18n.js +31 -0
- package/dist/waitlist/index.d.ts +5 -0
- package/dist/waitlist/index.js +4 -0
- package/dist/waitlist/lib.d.ts +10 -0
- package/dist/waitlist/lib.js +24 -0
- package/dist/waitlist/mail.d.ts +15 -0
- package/dist/waitlist/mail.js +73 -0
- package/dist/waitlist/options.d.ts +29 -0
- package/dist/waitlist/options.js +8 -0
- package/dist/waitlist/payloads.d.ts +14 -0
- package/dist/waitlist/payloads.js +47 -0
- package/dist/waitlist/screens.d.ts +2 -0
- package/dist/waitlist/screens.js +42 -0
- package/dist/waitlist/user-data-hooks.d.ts +3 -0
- package/dist/waitlist/user-data-hooks.js +71 -0
- package/package.json +17 -9
- package/src/auth-email-password/changes.json +18 -0
- package/src/auth-mfa/changes.json +6 -0
- package/src/billing-foundation/changes.json +6 -0
- package/src/channel-telegram/changes.json +9 -0
- package/src/delivery/changes.json +7 -0
- package/src/files/changes.json +13 -1
- package/src/renderer-simple/changes.json +6 -0
- package/src/secrets/changes.json +12 -0
- package/src/tenant/changes.json +6 -0
- package/src/waitlist/changes.json +17 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { chatConnectionNameSchema, DELIVERY_CHANNEL_EXTENSION, } from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
1
|
+
import { chatConnectionNameSchema, chatWebhookUrlSchema, DELIVERY_CHANNEL_EXTENSION, resolveChatWebhookTarget, } from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
2
2
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { createTeamsChannel } from "./teams-channel.js";
|
|
3
|
+
import { createTeamsChannel, TEAMS_DEFAULT_ALLOWED_HOSTS, } from "./teams-channel.js";
|
|
4
4
|
export function createChannelTeamsFeature(options = {}) {
|
|
5
5
|
return defineFeature("channel-teams", (r) => {
|
|
6
6
|
r.describe('Posts delivery notifications to a Microsoft Teams webhook (Workflows or connector) as an Adaptive Card, registered as the `teams` channel in the delivery system. Tenant-owned chat target: reach it only via `ctx.notify(type, { route: { teams: "<connection name>" } })`. The webhook URL is a tenant secret under `channel-teams:webhooks.<connection name>`; only the connection name is logged. Requires `delivery` and `secrets`. Host allowlist (.webhook.office.com, .logic.azure.com, .powerplatform.com), https-only and the request timeout are app options of `createChannelTeamsFeature(opts)`, never tenant config.');
|
|
@@ -11,6 +11,7 @@ export function createChannelTeamsFeature(options = {}) {
|
|
|
11
11
|
});
|
|
12
12
|
r.requires("delivery");
|
|
13
13
|
r.requires("secrets");
|
|
14
|
+
const webhookTarget = resolveChatWebhookTarget({ defaultAllowedHosts: TEAMS_DEFAULT_ALLOWED_HOSTS }, options);
|
|
14
15
|
const webhooks = r.secretNamespace("webhooks", {
|
|
15
16
|
label: {
|
|
16
17
|
en: "Microsoft Teams webhooks",
|
|
@@ -24,6 +25,7 @@ export function createChannelTeamsFeature(options = {}) {
|
|
|
24
25
|
},
|
|
25
26
|
scope: "tenant",
|
|
26
27
|
nameSchema: chatConnectionNameSchema,
|
|
28
|
+
valueSchema: chatWebhookUrlSchema(webhookTarget),
|
|
27
29
|
});
|
|
28
30
|
const channel = createTeamsChannel(options, webhooks.keyFor);
|
|
29
31
|
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "teams", {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createChannelTeamsFeature } from "./feature.js";
|
|
2
|
-
export { createTeamsChannel, type TeamsChannelOptions } from "./teams-channel.js";
|
|
2
|
+
export { createTeamsChannel, TEAMS_DEFAULT_ALLOWED_HOSTS, TEAMS_SECRET_KEYS, type TeamsChannelOptions, } from "./teams-channel.js";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createChannelTeamsFeature } from "./feature.js";
|
|
2
|
-
export { createTeamsChannel } from "./teams-channel.js";
|
|
2
|
+
export { createTeamsChannel, TEAMS_DEFAULT_ALLOWED_HOSTS, TEAMS_SECRET_KEYS, } from "./teams-channel.js";
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type ChatWebhookChannelOptions, type DeliveryChannel } from "../delivery/index.js";
|
|
2
2
|
export type TeamsChannelOptions = ChatWebhookChannelOptions;
|
|
3
|
+
export declare const TEAMS_SECRET_KEYS: {
|
|
4
|
+
readonly webhookPrefix: "channel-teams:webhooks.";
|
|
5
|
+
readonly webhookKeyFor: (connection: string) => string;
|
|
6
|
+
};
|
|
3
7
|
export declare const TEAMS_DEFAULT_ALLOWED_HOSTS: readonly [".webhook.office.com", ".logic.azure.com", ".powerplatform.com"];
|
|
4
8
|
export declare function buildTeamsAdaptiveCard(title: string, body: string | undefined): unknown;
|
|
5
9
|
export declare function createTeamsChannel(options: TeamsChannelOptions, keyFor: (connection: string) => string): DeliveryChannel;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { createChatWebhookChannel, } from "../delivery/index.js";
|
|
2
|
+
export const TEAMS_SECRET_KEYS = {
|
|
3
|
+
webhookPrefix: "channel-teams:webhooks.",
|
|
4
|
+
webhookKeyFor: (connection) => `channel-teams:webhooks.${connection}`,
|
|
5
|
+
};
|
|
2
6
|
export const TEAMS_DEFAULT_ALLOWED_HOSTS = [
|
|
3
7
|
".webhook.office.com",
|
|
4
8
|
".logic.azure.com",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { DELIVERY_CHANNEL_EXTENSION } from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
2
2
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
import { createTelegramChannel, isTelegramBotToken, } from "./telegram-channel.js";
|
|
4
5
|
export function createChannelTelegramFeature(options = {}) {
|
|
5
6
|
return defineFeature("channel-telegram", (r) => {
|
|
6
|
-
r.describe('Sends delivery notifications through the Telegram Bot API (`sendMessage`, plain text), registered as the `telegram` channel in the delivery system. Tenant-owned chat target: reach it only via `ctx.notify(type, { route: { telegram: "<chat id or @channel>" } })`. The bot token is the tenant secret `channel-telegram:secret:bot-token`; the token never reaches logs or delivery attempts. Requires `delivery` and `secrets`. The API base URL and request timeout are app options of `createChannelTelegramFeature(opts)`, never tenant config.');
|
|
7
|
+
r.describe('Sends delivery notifications through the Telegram Bot API (`sendMessage`, plain text), registered as the `telegram` channel in the delivery system. Tenant-owned chat target: reach it only via `ctx.notify(type, { route: { telegram: "<chat id or @channel>" } })`. The bot token is the tenant secret `channel-telegram:secret:bot-token`; the token never reaches logs or delivery attempts. Requires `delivery` and `secrets`. The API base URL, its host allowlist (`allowedHosts`, default `api.telegram.org`), `requireHttps` (default true; boot fails on a mismatch) and the request timeout are app options of `createChannelTelegramFeature(opts)`, never tenant config.');
|
|
7
8
|
r.uiHints({
|
|
8
9
|
displayLabel: "Telegram Channel",
|
|
9
10
|
category: "notifications",
|
|
@@ -18,6 +19,7 @@ export function createChannelTelegramFeature(options = {}) {
|
|
|
18
19
|
},
|
|
19
20
|
redact: () => "••••••••",
|
|
20
21
|
scope: "tenant",
|
|
22
|
+
valueSchema: z.string().refine(isTelegramBotToken, "invalid bot token"),
|
|
21
23
|
});
|
|
22
24
|
const channel = createTelegramChannel(options, botToken.name);
|
|
23
25
|
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "telegram", {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createChannelTelegramFeature } from "./feature.js";
|
|
2
|
-
export { createTelegramChannel, type TelegramChannelOptions } from "./telegram-channel.js";
|
|
2
|
+
export { createTelegramChannel, isTelegramBotToken, isTelegramChatId, TELEGRAM_DEFAULT_API_BASE_URL, TELEGRAM_SECRET_KEYS, type TelegramChannelOptions, } from "./telegram-channel.js";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createChannelTelegramFeature } from "./feature.js";
|
|
2
|
-
export { createTelegramChannel } from "./telegram-channel.js";
|
|
2
|
+
export { createTelegramChannel, isTelegramBotToken, isTelegramChatId, TELEGRAM_DEFAULT_API_BASE_URL, TELEGRAM_SECRET_KEYS, } from "./telegram-channel.js";
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { type DeliveryChannel } from "../delivery/index.js";
|
|
2
2
|
export type TelegramChannelOptions = {
|
|
3
3
|
readonly apiBaseUrl?: string;
|
|
4
|
+
readonly allowedHosts?: readonly string[];
|
|
5
|
+
readonly requireHttps?: boolean;
|
|
4
6
|
readonly timeoutMs?: number;
|
|
5
7
|
};
|
|
6
8
|
export declare const TELEGRAM_DEFAULT_API_BASE_URL = "https://api.telegram.org";
|
|
9
|
+
export declare const TELEGRAM_DEFAULT_ALLOWED_HOSTS: readonly string[];
|
|
7
10
|
export declare const TELEGRAM_TEXT_MAX_CHARS = 4096;
|
|
11
|
+
export declare const TELEGRAM_SECRET_KEYS: {
|
|
12
|
+
readonly botToken: "channel-telegram:secret:bot-token";
|
|
13
|
+
};
|
|
14
|
+
export declare function isTelegramChatId(address: string): boolean;
|
|
15
|
+
export declare function isTelegramBotToken(token: string): boolean;
|
|
8
16
|
export declare function createTelegramChannel(options: TelegramChannelOptions, botTokenKey: string): DeliveryChannel;
|
|
@@ -1,18 +1,42 @@
|
|
|
1
1
|
import { SYSTEM_USER_ID } from "@cosmicdrift/kumiko-types/identifiers";
|
|
2
2
|
import { chatMessageText, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, toChannelResult, truncateChars, } from "../delivery/index.js";
|
|
3
3
|
export const TELEGRAM_DEFAULT_API_BASE_URL = "https://api.telegram.org";
|
|
4
|
+
export const TELEGRAM_DEFAULT_ALLOWED_HOSTS = ["api.telegram.org"];
|
|
4
5
|
export const TELEGRAM_TEXT_MAX_CHARS = 4096;
|
|
6
|
+
export const TELEGRAM_SECRET_KEYS = { botToken: "channel-telegram:secret:bot-token" };
|
|
5
7
|
// Numeric chat/group id (groups and channels are negative) or a public @channelname.
|
|
6
8
|
const TELEGRAM_CHAT_ID_PATTERN = /^(-?\d{1,20}|@[A-Za-z][A-Za-z0-9_]{3,31})$/;
|
|
7
9
|
// `<bot id>:<secret>`; the token is spliced into the URL path, so only the real shape passes.
|
|
8
10
|
const TELEGRAM_BOT_TOKEN_PATTERN = /^\d{1,20}:[A-Za-z0-9_-]{10,}$/;
|
|
11
|
+
export function isTelegramChatId(address) {
|
|
12
|
+
return TELEGRAM_CHAT_ID_PATTERN.test(address);
|
|
13
|
+
}
|
|
14
|
+
export function isTelegramBotToken(token) {
|
|
15
|
+
return TELEGRAM_BOT_TOKEN_PATTERN.test(token);
|
|
16
|
+
}
|
|
9
17
|
export function createTelegramChannel(options, botTokenKey) {
|
|
10
18
|
const apiBaseUrl = options.apiBaseUrl ?? TELEGRAM_DEFAULT_API_BASE_URL;
|
|
19
|
+
const allowedHosts = options.allowedHosts ?? TELEGRAM_DEFAULT_ALLOWED_HOSTS;
|
|
20
|
+
const requireHttps = options.requireHttps ?? true;
|
|
21
|
+
// Fail at boot: a misconfigured base URL must not surface as per-send failures.
|
|
22
|
+
let parsedBase;
|
|
23
|
+
try {
|
|
24
|
+
parsedBase = new URL(apiBaseUrl);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error("channel-telegram: apiBaseUrl is not a valid URL");
|
|
28
|
+
}
|
|
29
|
+
if (!allowedHosts.includes(parsedBase.hostname)) {
|
|
30
|
+
throw new Error(`channel-telegram: apiBaseUrl host "${parsedBase.hostname}" is not in allowedHosts`);
|
|
31
|
+
}
|
|
32
|
+
if (requireHttps && parsedBase.protocol !== "https:") {
|
|
33
|
+
throw new Error("channel-telegram: apiBaseUrl must use https (set requireHttps: false to opt out)");
|
|
34
|
+
}
|
|
11
35
|
return {
|
|
12
36
|
name: "telegram",
|
|
13
37
|
mode: "queued",
|
|
14
38
|
async send(address, message, ctx) {
|
|
15
|
-
if (!
|
|
39
|
+
if (!isTelegramChatId(address)) {
|
|
16
40
|
return toChannelResult(address, { ok: false, code: "invalid_address" });
|
|
17
41
|
}
|
|
18
42
|
let token;
|
|
@@ -26,21 +50,13 @@ export function createTelegramChannel(options, botTokenKey) {
|
|
|
26
50
|
catch {
|
|
27
51
|
token = undefined;
|
|
28
52
|
}
|
|
29
|
-
if (!token || !
|
|
53
|
+
if (!token || !isTelegramBotToken(token)) {
|
|
30
54
|
return toChannelResult(address, { ok: false, code: "missing_credentials" });
|
|
31
55
|
}
|
|
32
|
-
// The base URL is the allowlist: only its own host is reachable.
|
|
33
|
-
let baseHost;
|
|
34
|
-
try {
|
|
35
|
-
baseHost = new URL(apiBaseUrl).hostname;
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return toChannelResult(address, { ok: false, code: "host_not_allowed" });
|
|
39
|
-
}
|
|
40
56
|
const result = await postChatWebhook({
|
|
41
57
|
url: `${apiBaseUrl.replace(/\/+$/, "")}/bot${token}/sendMessage`,
|
|
42
|
-
allowedHosts
|
|
43
|
-
requireHttps
|
|
58
|
+
allowedHosts,
|
|
59
|
+
requireHttps,
|
|
44
60
|
timeoutMs: options.timeoutMs ?? DEFAULT_CHAT_TIMEOUT_MS,
|
|
45
61
|
// No parse_mode: Telegram renders the text literally.
|
|
46
62
|
body: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ChatSendResult } from "./chat-webhook-sender.js";
|
|
1
|
+
import { type ChatSendResult, 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[];
|
|
@@ -7,6 +7,10 @@ export type ChatWebhookChannelOptions = {
|
|
|
7
7
|
};
|
|
8
8
|
export declare function toChannelResult(address: string, result: ChatSendResult): ChannelResult;
|
|
9
9
|
export declare function chatMessageText(message: ChannelMessage): string;
|
|
10
|
+
export declare function resolveChatWebhookTarget(provider: {
|
|
11
|
+
readonly defaultAllowedHosts: readonly string[];
|
|
12
|
+
readonly requiredPathPrefix?: string;
|
|
13
|
+
}, options: ChatWebhookChannelOptions): ChatWebhookTarget;
|
|
10
14
|
export type ChatWebhookChannelSpec = {
|
|
11
15
|
readonly name: string;
|
|
12
16
|
readonly featureName: string;
|
|
@@ -8,6 +8,15 @@ export function toChannelResult(address, result) {
|
|
|
8
8
|
export function chatMessageText(message) {
|
|
9
9
|
return message.body ? `${message.title}\n${message.body}` : message.title;
|
|
10
10
|
}
|
|
11
|
+
export function resolveChatWebhookTarget(provider, options) {
|
|
12
|
+
return {
|
|
13
|
+
allowedHosts: options.allowedHosts ?? provider.defaultAllowedHosts,
|
|
14
|
+
requireHttps: options.requireHttps ?? true,
|
|
15
|
+
...(provider.requiredPathPrefix !== undefined && {
|
|
16
|
+
requiredPathPrefix: provider.requiredPathPrefix,
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
11
20
|
// Slack/Discord/Teams: the route address is a tenant-chosen connection name, the
|
|
12
21
|
// webhook URL lives in the secrets store under that name. Route-only (no resolve).
|
|
13
22
|
export function createChatWebhookChannel(spec) {
|
|
@@ -34,11 +43,7 @@ export function createChatWebhookChannel(spec) {
|
|
|
34
43
|
return toChannelResult(address, { ok: false, code: "missing_credentials" });
|
|
35
44
|
const result = await postChatWebhook({
|
|
36
45
|
url,
|
|
37
|
-
|
|
38
|
-
requireHttps: options.requireHttps ?? true,
|
|
39
|
-
...(spec.requiredPathPrefix !== undefined && {
|
|
40
|
-
requiredPathPrefix: spec.requiredPathPrefix,
|
|
41
|
-
}),
|
|
46
|
+
...resolveChatWebhookTarget(spec, options),
|
|
42
47
|
timeoutMs: options.timeoutMs ?? DEFAULT_CHAT_TIMEOUT_MS,
|
|
43
48
|
body: spec.buildBody(message),
|
|
44
49
|
});
|
|
@@ -8,14 +8,18 @@ export type ChatSendResult = {
|
|
|
8
8
|
};
|
|
9
9
|
export declare const DEFAULT_CHAT_TIMEOUT_MS = 10000;
|
|
10
10
|
export declare const chatConnectionNameSchema: z.ZodString;
|
|
11
|
-
export type
|
|
12
|
-
readonly url: string;
|
|
11
|
+
export type ChatWebhookTarget = {
|
|
13
12
|
readonly allowedHosts: readonly string[];
|
|
14
13
|
readonly requireHttps: boolean;
|
|
15
14
|
readonly requiredPathPrefix?: string;
|
|
15
|
+
};
|
|
16
|
+
export type ChatWebhookRequest = ChatWebhookTarget & {
|
|
17
|
+
readonly url: string;
|
|
16
18
|
readonly timeoutMs: number;
|
|
17
19
|
readonly body: unknown;
|
|
18
20
|
readonly send?: (url: string, init: RequestInit) => Promise<Response>;
|
|
19
21
|
};
|
|
22
|
+
export declare function checkChatWebhookTarget(target: ChatWebhookTarget, url: string): URL | undefined;
|
|
23
|
+
export declare function chatWebhookUrlSchema(target: ChatWebhookTarget): z.ZodType<string>;
|
|
20
24
|
export declare function postChatWebhook(request: ChatWebhookRequest): Promise<ChatSendResult>;
|
|
21
25
|
export declare function truncateChars(text: string, maxChars: number): string;
|
|
@@ -27,31 +27,36 @@ function hostMatches(hostname, allowedHost) {
|
|
|
27
27
|
}
|
|
28
28
|
// Allowlist runs on the parsed URL, never on the raw string: userinfo
|
|
29
29
|
// (`https://hooks.slack.com@evil.com`) and look-alike suffixes must not pass.
|
|
30
|
-
function
|
|
30
|
+
export function checkChatWebhookTarget(target, url) {
|
|
31
31
|
let parsed;
|
|
32
32
|
try {
|
|
33
|
-
parsed = new URL(
|
|
33
|
+
parsed = new URL(url);
|
|
34
34
|
}
|
|
35
35
|
catch {
|
|
36
36
|
return undefined;
|
|
37
37
|
}
|
|
38
|
-
const protocolAllowed = parsed.protocol === "https:" || (!
|
|
38
|
+
const protocolAllowed = parsed.protocol === "https:" || (!target.requireHttps && parsed.protocol === "http:");
|
|
39
39
|
if (!protocolAllowed)
|
|
40
40
|
return undefined;
|
|
41
41
|
// egress() has no port rule; a provider webhook over https never needs a custom port.
|
|
42
|
-
if (
|
|
42
|
+
if (target.requireHttps && parsed.port !== "")
|
|
43
43
|
return undefined;
|
|
44
44
|
if (parsed.username !== "" || parsed.password !== "")
|
|
45
45
|
return undefined;
|
|
46
46
|
const hostname = parsed.hostname.toLowerCase();
|
|
47
|
-
if (!
|
|
47
|
+
if (!target.allowedHosts.some((allowed) => hostMatches(hostname, allowed)))
|
|
48
48
|
return undefined;
|
|
49
|
-
if (
|
|
50
|
-
!parsed.pathname.startsWith(
|
|
49
|
+
if (target.requiredPathPrefix !== undefined &&
|
|
50
|
+
!parsed.pathname.startsWith(target.requiredPathPrefix)) {
|
|
51
51
|
return undefined;
|
|
52
52
|
}
|
|
53
53
|
return parsed;
|
|
54
54
|
}
|
|
55
|
+
export function chatWebhookUrlSchema(target) {
|
|
56
|
+
return z
|
|
57
|
+
.string()
|
|
58
|
+
.refine((url) => checkChatWebhookTarget(target, url) !== undefined, "invalid webhook url");
|
|
59
|
+
}
|
|
55
60
|
function isTimeout(err) {
|
|
56
61
|
return err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
57
62
|
}
|
|
@@ -71,7 +76,7 @@ function classifyFailure(err) {
|
|
|
71
76
|
// Never throws and never surfaces the URL: egress/Bun errors can carry it, so
|
|
72
77
|
// every failure is reduced to a code here.
|
|
73
78
|
export async function postChatWebhook(request) {
|
|
74
|
-
const target =
|
|
79
|
+
const target = checkChatWebhookTarget(request, request.url);
|
|
75
80
|
if (!target)
|
|
76
81
|
return { ok: false, code: "host_not_allowed" };
|
|
77
82
|
let response;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import type { EscapeHatchAuditSink, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
-
import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
|
|
3
|
+
import type { EscapeHatchAuditSink, NotifyJobDispatcher, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
4
|
import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
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
8
|
export type RateLimitConfig = {
|
|
@@ -20,7 +20,8 @@ export type DeliveryServiceOptions = {
|
|
|
20
20
|
readonly rateLimit?: RateLimitConfig;
|
|
21
21
|
readonly isChannelKilled?: KillSwitchResolver;
|
|
22
22
|
readonly idempotencyRedis?: Redis;
|
|
23
|
-
readonly jobRunner?:
|
|
23
|
+
readonly jobRunner?: NotifyJobDispatcher;
|
|
24
|
+
readonly secrets?: SecretsContext;
|
|
24
25
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
25
26
|
readonly log?: Logger;
|
|
26
27
|
};
|
|
@@ -25,7 +25,7 @@ export function collectChannels(registry) {
|
|
|
25
25
|
// nicht aus Extension-Usages). Multi-Kind-Plugin-Pool lebt jetzt im
|
|
26
26
|
// `renderer-foundation`-Bundle via `collectRendererPlugins`.
|
|
27
27
|
export function createDeliveryService(options) {
|
|
28
|
-
const { db, registry, sseBroker, channels, tenantUserIdsQuery, rateLimit, isChannelKilled, idempotencyRedis, jobRunner, escapeHatchAuditSink, log, } = options;
|
|
28
|
+
const { db, registry, sseBroker, channels, tenantUserIdsQuery, rateLimit, isChannelKilled, idempotencyRedis, jobRunner, secrets, escapeHatchAuditSink, log, } = options;
|
|
29
29
|
const idemRedis = idempotencyRedis ?? rateLimit?.redis;
|
|
30
30
|
// Rate limit check: atomic INCR + TTL + over-limit rollback via server-side
|
|
31
31
|
// Lua. Runs single-threaded in Redis, so two parallel clients can't both
|
|
@@ -119,11 +119,21 @@ export function createDeliveryService(options) {
|
|
|
119
119
|
...bridgeStub(),
|
|
120
120
|
}));
|
|
121
121
|
}
|
|
122
|
+
function recordDelivery(run, entry, deliveryAttemptId) {
|
|
123
|
+
run.deliveries.push({
|
|
124
|
+
channel: entry.channel,
|
|
125
|
+
recipientId: entry.recipientId,
|
|
126
|
+
status: entry.status,
|
|
127
|
+
error: entry.error,
|
|
128
|
+
deliveryAttemptId,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
122
131
|
// Single-shot terminal log (inline channels, skips, idempotency dups). Async
|
|
123
132
|
// attempts instead append a queued event up front and a terminal event from
|
|
124
133
|
// the send job — see deliverViaChannel + jobs.ts.
|
|
125
|
-
async function logDelivery(entry) {
|
|
126
|
-
await logAttempt(db, registry, entry);
|
|
134
|
+
async function logDelivery(run, entry) {
|
|
135
|
+
const attemptId = await logAttempt(db, registry, entry);
|
|
136
|
+
recordDelivery(run, entry, attemptId);
|
|
127
137
|
}
|
|
128
138
|
// Deliver one resolved (channel, address) pair. Inline channels (inApp) and
|
|
129
139
|
// the no-job-runner fallback render + send synchronously and log the terminal
|
|
@@ -131,13 +141,13 @@ export function createDeliveryService(options) {
|
|
|
131
141
|
// hand off to the delivery.render (or delivery.send) job, which appends the
|
|
132
142
|
// terminal event on the same attempt stream.
|
|
133
143
|
async function deliverViaChannel(args) {
|
|
134
|
-
const { channel, address, message, channelCtx, tenantId, recipientId, notificationType, priority, } = args;
|
|
135
|
-
if (channel.mode === "queued" &&
|
|
144
|
+
const { run, channel, address, message, channelCtx, tenantId, recipientId, notificationType, priority, } = args;
|
|
145
|
+
if (channel.mode === "queued" && run.jobDispatcher) {
|
|
136
146
|
// Async hand-off: record the attempt as queued, then dispatch the
|
|
137
147
|
// render/send job — the terminal sent/failed event is appended by the
|
|
138
148
|
// job, not here.
|
|
139
149
|
const deliveryAttemptId = generateId();
|
|
140
|
-
|
|
150
|
+
const queuedEntry = {
|
|
141
151
|
tenantId,
|
|
142
152
|
notificationType,
|
|
143
153
|
channel: channel.name,
|
|
@@ -146,8 +156,9 @@ export function createDeliveryService(options) {
|
|
|
146
156
|
status: "queued",
|
|
147
157
|
error: null,
|
|
148
158
|
priority,
|
|
149
|
-
}
|
|
150
|
-
await
|
|
159
|
+
};
|
|
160
|
+
await appendAttemptEvent(db, registry, deliveryAttemptId, queuedEntry);
|
|
161
|
+
await run.jobDispatcher.dispatch(channel.render ? DeliveryJobs.render : DeliveryJobs.send, {
|
|
151
162
|
channelName: channel.name,
|
|
152
163
|
address,
|
|
153
164
|
tenantId,
|
|
@@ -157,12 +168,13 @@ export function createDeliveryService(options) {
|
|
|
157
168
|
priority,
|
|
158
169
|
message,
|
|
159
170
|
}, { priority: deliveryPriorityRank[priority] });
|
|
171
|
+
recordDelivery(run, queuedEntry, deliveryAttemptId);
|
|
160
172
|
}
|
|
161
173
|
else {
|
|
162
174
|
// Inline (inApp) or no-job-runner fallback: render + send synchronously.
|
|
163
175
|
const rendered = channel.render ? await channel.render(message, channelCtx) : undefined;
|
|
164
176
|
const result = await channel.send(address, message, channelCtx, rendered);
|
|
165
|
-
await logDelivery({
|
|
177
|
+
await logDelivery(run, {
|
|
166
178
|
tenantId,
|
|
167
179
|
notificationType,
|
|
168
180
|
channel: channel.name,
|
|
@@ -174,7 +186,7 @@ export function createDeliveryService(options) {
|
|
|
174
186
|
});
|
|
175
187
|
}
|
|
176
188
|
}
|
|
177
|
-
function buildMessage(notificationType, data, channelName) {
|
|
189
|
+
function buildMessage(notificationType, data, channelName, locale) {
|
|
178
190
|
// Look up per-channel template from notification definition
|
|
179
191
|
const notifDef = registry.getAllNotifications().get(notificationType);
|
|
180
192
|
const templateFn = notifDef?.templates?.[channelName];
|
|
@@ -186,6 +198,7 @@ export function createDeliveryService(options) {
|
|
|
186
198
|
title: channelData["title"] ?? data["title"] ?? notificationType,
|
|
187
199
|
body: channelData["body"],
|
|
188
200
|
data: channelData,
|
|
201
|
+
locale,
|
|
189
202
|
};
|
|
190
203
|
}
|
|
191
204
|
// @cast-boundary engine-payload — generic notification.data shape
|
|
@@ -194,6 +207,7 @@ export function createDeliveryService(options) {
|
|
|
194
207
|
title: data?.["title"] ?? notificationType,
|
|
195
208
|
body: data?.["body"],
|
|
196
209
|
data,
|
|
210
|
+
locale,
|
|
197
211
|
};
|
|
198
212
|
}
|
|
199
213
|
// Shared by deliverToUser (resolved account address) and deliverDirect (a
|
|
@@ -226,18 +240,18 @@ export function createDeliveryService(options) {
|
|
|
226
240
|
// and conservative — DB ordering no longer decides the outcome).
|
|
227
241
|
return !prefs.some((p) => p.enabled === false);
|
|
228
242
|
}
|
|
229
|
-
async function deliverToUser(userId, notificationType, data, tenantId, priority) {
|
|
230
|
-
const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId);
|
|
243
|
+
async function deliverToUser(userId, notificationType, data, tenantId, priority, run) {
|
|
244
|
+
const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
|
|
231
245
|
for (const channel of channels) {
|
|
232
246
|
// Route-only channel (no per-user address): not a user-notification target.
|
|
233
247
|
if (!channel.resolve)
|
|
234
248
|
continue;
|
|
235
|
-
const message = buildMessage(notificationType, data, channel.name);
|
|
249
|
+
const message = buildMessage(notificationType, data, channel.name, run.locale);
|
|
236
250
|
// Kill switch: tenant admin disabled this channel entirely
|
|
237
251
|
if (isChannelKilled) {
|
|
238
252
|
const killed = await isChannelKilled(tenantId, channel.name);
|
|
239
253
|
if (killed) {
|
|
240
|
-
await logDelivery({
|
|
254
|
+
await logDelivery(run, {
|
|
241
255
|
tenantId,
|
|
242
256
|
notificationType,
|
|
243
257
|
channel: channel.name,
|
|
@@ -254,7 +268,7 @@ export function createDeliveryService(options) {
|
|
|
254
268
|
if (priority !== "critical") {
|
|
255
269
|
const enabled = await isChannelEnabled(userId, tenantId, notificationType, channel.name);
|
|
256
270
|
if (!enabled) {
|
|
257
|
-
await logDelivery({
|
|
271
|
+
await logDelivery(run, {
|
|
258
272
|
tenantId,
|
|
259
273
|
notificationType,
|
|
260
274
|
channel: channel.name,
|
|
@@ -271,7 +285,7 @@ export function createDeliveryService(options) {
|
|
|
271
285
|
if (rateLimit) {
|
|
272
286
|
const allowed = await checkRateLimit(rateLimit, tenantId, channel.name);
|
|
273
287
|
if (!allowed) {
|
|
274
|
-
await logDelivery({
|
|
288
|
+
await logDelivery(run, {
|
|
275
289
|
tenantId,
|
|
276
290
|
notificationType,
|
|
277
291
|
channel: channel.name,
|
|
@@ -287,7 +301,7 @@ export function createDeliveryService(options) {
|
|
|
287
301
|
try {
|
|
288
302
|
const address = await channel.resolve(userId, channelCtx);
|
|
289
303
|
if (!address) {
|
|
290
|
-
await logDelivery({
|
|
304
|
+
await logDelivery(run, {
|
|
291
305
|
tenantId,
|
|
292
306
|
notificationType,
|
|
293
307
|
channel: channel.name,
|
|
@@ -301,7 +315,7 @@ export function createDeliveryService(options) {
|
|
|
301
315
|
}
|
|
302
316
|
if (priority !== "critical" &&
|
|
303
317
|
(await isAddressSuppressed(address, tenantId, notificationType, channel.name))) {
|
|
304
|
-
await logDelivery({
|
|
318
|
+
await logDelivery(run, {
|
|
305
319
|
tenantId,
|
|
306
320
|
notificationType,
|
|
307
321
|
channel: channel.name,
|
|
@@ -314,6 +328,7 @@ export function createDeliveryService(options) {
|
|
|
314
328
|
continue;
|
|
315
329
|
}
|
|
316
330
|
await deliverViaChannel({
|
|
331
|
+
run,
|
|
317
332
|
channel,
|
|
318
333
|
address,
|
|
319
334
|
message,
|
|
@@ -325,7 +340,7 @@ export function createDeliveryService(options) {
|
|
|
325
340
|
});
|
|
326
341
|
}
|
|
327
342
|
catch (err) {
|
|
328
|
-
await logDelivery({
|
|
343
|
+
await logDelivery(run, {
|
|
329
344
|
tenantId,
|
|
330
345
|
notificationType,
|
|
331
346
|
channel: channel.name,
|
|
@@ -338,20 +353,20 @@ export function createDeliveryService(options) {
|
|
|
338
353
|
}
|
|
339
354
|
}
|
|
340
355
|
}
|
|
341
|
-
async function deliverDirect(route, notificationType, data, tenantId, priority, recipientId) {
|
|
342
|
-
const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId);
|
|
356
|
+
async function deliverDirect(route, notificationType, data, tenantId, priority, recipientId, run) {
|
|
357
|
+
const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
|
|
343
358
|
// Direct routing skips preferences (no user account) but NOT rate limit
|
|
344
359
|
// — direct sends can still be abused (webhook replays, test harnesses).
|
|
345
360
|
for (const channel of channels) {
|
|
346
361
|
const address = route[channel.name];
|
|
347
|
-
const message = buildMessage(notificationType, data, channel.name);
|
|
362
|
+
const message = buildMessage(notificationType, data, channel.name, run.locale);
|
|
348
363
|
if (!address)
|
|
349
364
|
continue;
|
|
350
365
|
// Address opt-out (critical priority skips it, same rule as user
|
|
351
366
|
// preferences).
|
|
352
367
|
if (priority !== "critical" &&
|
|
353
368
|
(await isAddressSuppressed(address, tenantId, notificationType, channel.name))) {
|
|
354
|
-
await logDelivery({
|
|
369
|
+
await logDelivery(run, {
|
|
355
370
|
tenantId,
|
|
356
371
|
notificationType,
|
|
357
372
|
channel: channel.name,
|
|
@@ -367,7 +382,7 @@ export function createDeliveryService(options) {
|
|
|
367
382
|
if (rateLimit) {
|
|
368
383
|
const allowed = await checkRateLimit(rateLimit, tenantId, channel.name);
|
|
369
384
|
if (!allowed) {
|
|
370
|
-
await logDelivery({
|
|
385
|
+
await logDelivery(run, {
|
|
371
386
|
tenantId,
|
|
372
387
|
notificationType,
|
|
373
388
|
channel: channel.name,
|
|
@@ -382,6 +397,7 @@ export function createDeliveryService(options) {
|
|
|
382
397
|
}
|
|
383
398
|
try {
|
|
384
399
|
await deliverViaChannel({
|
|
400
|
+
run,
|
|
385
401
|
channel,
|
|
386
402
|
address,
|
|
387
403
|
message,
|
|
@@ -393,7 +409,7 @@ export function createDeliveryService(options) {
|
|
|
393
409
|
});
|
|
394
410
|
}
|
|
395
411
|
catch (err) {
|
|
396
|
-
await logDelivery({
|
|
412
|
+
await logDelivery(run, {
|
|
397
413
|
tenantId,
|
|
398
414
|
notificationType,
|
|
399
415
|
channel: channel.name,
|
|
@@ -407,13 +423,18 @@ export function createDeliveryService(options) {
|
|
|
407
423
|
}
|
|
408
424
|
}
|
|
409
425
|
return {
|
|
410
|
-
async notify(notificationType, options, _user, tenantId) {
|
|
426
|
+
async notify(notificationType, options, _user, tenantId, jobDispatcher) {
|
|
411
427
|
const { to, route, data, idempotencyKey } = options;
|
|
412
428
|
const priority = options.priority ?? "normal";
|
|
429
|
+
const run = {
|
|
430
|
+
deliveries: [],
|
|
431
|
+
jobDispatcher: options.immediate ? undefined : (jobDispatcher ?? jobRunner),
|
|
432
|
+
locale: options.locale,
|
|
433
|
+
};
|
|
413
434
|
if (idempotencyKey) {
|
|
414
435
|
const first = await claimIdempotency(tenantId, idempotencyKey);
|
|
415
436
|
if (!first) {
|
|
416
|
-
await logDelivery({
|
|
437
|
+
await logDelivery(run, {
|
|
417
438
|
tenantId,
|
|
418
439
|
notificationType,
|
|
419
440
|
channel: "*",
|
|
@@ -423,14 +444,12 @@ export function createDeliveryService(options) {
|
|
|
423
444
|
error: "duplicate_idempotency_key",
|
|
424
445
|
priority,
|
|
425
446
|
});
|
|
426
|
-
|
|
427
|
-
return;
|
|
447
|
+
return { deliveries: run.deliveries };
|
|
428
448
|
}
|
|
429
449
|
}
|
|
430
450
|
if (route) {
|
|
431
|
-
await deliverDirect(route, notificationType, data, tenantId, priority, options.recipientId ?? null);
|
|
432
|
-
|
|
433
|
-
return;
|
|
451
|
+
await deliverDirect(route, notificationType, data, tenantId, priority, options.recipientId ?? null, run);
|
|
452
|
+
return { deliveries: run.deliveries };
|
|
434
453
|
}
|
|
435
454
|
if (to !== undefined) {
|
|
436
455
|
let userIds;
|
|
@@ -444,9 +463,10 @@ export function createDeliveryService(options) {
|
|
|
444
463
|
userIds = to;
|
|
445
464
|
}
|
|
446
465
|
for (const userId of userIds) {
|
|
447
|
-
await deliverToUser(userId, notificationType, data, tenantId, priority);
|
|
466
|
+
await deliverToUser(userId, notificationType, data, tenantId, priority, run);
|
|
448
467
|
}
|
|
449
468
|
}
|
|
469
|
+
return { deliveries: run.deliveries };
|
|
450
470
|
},
|
|
451
471
|
};
|
|
452
472
|
}
|
package/dist/delivery/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
2
|
-
export { type ChatWebhookChannelOptions, chatMessageText, createChatWebhookChannel, toChannelResult, } from "./chat-webhook-channel.js";
|
|
3
|
-
export { type ChatSendFailureCode, type ChatSendResult, type ChatWebhookRequest, chatConnectionNameSchema, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, truncateChars, } from "./chat-webhook-sender.js";
|
|
2
|
+
export { type ChatWebhookChannelOptions, chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
|
|
3
|
+
export { type ChatSendFailureCode, type ChatSendResult, type ChatWebhookRequest, type ChatWebhookTarget, chatConnectionNameSchema, chatWebhookUrlSchema, checkChatWebhookTarget, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, truncateChars, } from "./chat-webhook-sender.js";
|
|
4
4
|
export type { DeliveryStatusValue } from "./constants.js";
|
|
5
5
|
export { DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, } from "./constants.js";
|
|
6
6
|
export { collectChannels, createDeliveryService, type DeliveryServiceOptions, type KillSwitchResolver, type RateLimitConfig, } from "./delivery-service.js";
|
package/dist/delivery/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
2
|
-
export { chatMessageText, createChatWebhookChannel, toChannelResult, } from "./chat-webhook-channel.js";
|
|
3
|
-
export { chatConnectionNameSchema, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, truncateChars, } from "./chat-webhook-sender.js";
|
|
2
|
+
export { chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
|
|
3
|
+
export { chatConnectionNameSchema, chatWebhookUrlSchema, checkChatWebhookTarget, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, truncateChars, } from "./chat-webhook-sender.js";
|
|
4
4
|
export { DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, } from "./constants.js";
|
|
5
5
|
export { collectChannels, createDeliveryService, } from "./delivery-service.js";
|
|
6
6
|
export { createDeliveryFeature } from "./feature.js";
|
package/dist/delivery/jobs.js
CHANGED
|
@@ -15,6 +15,7 @@ const channelMessageSchema = z.object({
|
|
|
15
15
|
title: z.string(),
|
|
16
16
|
body: z.string().optional(),
|
|
17
17
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
18
|
+
locale: z.string().optional(),
|
|
18
19
|
});
|
|
19
20
|
const renderJobPayloadSchema = z.object({
|
|
20
21
|
channelName: z.string(),
|
|
@@ -56,6 +57,7 @@ function toMessage(p) {
|
|
|
56
57
|
title: p.message.title,
|
|
57
58
|
body: p.message.body,
|
|
58
59
|
data: p.message.data,
|
|
60
|
+
locale: p.message.locale,
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
63
|
function entryFor(p, status, error, address) {
|
|
@@ -81,7 +83,7 @@ export const deliveryRenderJob = async (payload, ctx) => {
|
|
|
81
83
|
const tenantId = p.tenantId; // @cast-boundary engine-payload — stream tenant
|
|
82
84
|
const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
|
|
83
85
|
const channel = resolveChannel(registry, p.channelName);
|
|
84
|
-
const channelCtx = buildChannelContext(db, registry, undefined, tenantId);
|
|
86
|
+
const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets);
|
|
85
87
|
try {
|
|
86
88
|
if (!channel.render) {
|
|
87
89
|
throw new Error(`delivery.render: channel "${p.channelName}" has no render step`);
|