@cosmicdrift/kumiko-bundled-features 0.339.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.
Files changed (166) hide show
  1. package/dist/auth-email-password/bootstrap.d.ts +74 -0
  2. package/dist/auth-email-password/bootstrap.js +165 -0
  3. package/dist/auth-email-password/constants.d.ts +2 -0
  4. package/dist/auth-email-password/constants.js +5 -0
  5. package/dist/auth-email-password/errors.d.ts +1 -0
  6. package/dist/auth-email-password/errors.js +6 -0
  7. package/dist/auth-email-password/feature.js +2 -1
  8. package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +9 -1
  9. package/dist/auth-email-password/handlers/invite-accept.write.js +8 -0
  10. package/dist/auth-email-password/handlers/invite-create.write.d.ts +11 -0
  11. package/dist/auth-email-password/handlers/invite-create.write.js +134 -87
  12. package/dist/auth-email-password/handlers/invite-signup-complete.write.js +5 -1
  13. package/dist/auth-email-password/handlers/signup-request.write.js +1 -0
  14. package/dist/auth-email-password/handlers/token-request-handler.js +1 -0
  15. package/dist/auth-email-password/index.d.ts +2 -0
  16. package/dist/auth-email-password/index.js +1 -0
  17. package/dist/auth-email-password/invite-token-store.d.ts +3 -0
  18. package/dist/auth-email-password/invite-token-store.js +5 -0
  19. package/dist/auth-email-password/magic-link-mail.js +1 -0
  20. package/dist/auth-mfa/config.d.ts +1 -1
  21. package/dist/auth-mfa/config.js +8 -3
  22. package/dist/auth-mfa/feature.d.ts +2 -0
  23. package/dist/auth-mfa/feature.js +2 -1
  24. package/dist/auth-mfa/mfa-status-checker.d.ts +2 -0
  25. package/dist/auth-mfa/mfa-status-checker.js +1 -1
  26. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +1 -0
  27. package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -0
  28. package/dist/billing-foundation/consumer-protection/termination-notify.js +1 -0
  29. package/dist/billing-foundation/consumer-protection/termination-pages.d.ts +8 -1
  30. package/dist/billing-foundation/consumer-protection/termination-pages.js +59 -32
  31. package/dist/billing-foundation/consumer-protection/termination-request.js +2 -0
  32. package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -0
  33. package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -1
  34. package/dist/billing-foundation/projection.js +23 -40
  35. package/dist/channel-discord/discord-channel.d.ts +4 -0
  36. package/dist/channel-discord/discord-channel.js +4 -0
  37. package/dist/channel-discord/feature.js +7 -2
  38. package/dist/channel-discord/index.d.ts +1 -1
  39. package/dist/channel-discord/index.js +1 -1
  40. package/dist/channel-email/email-channel.js +1 -0
  41. package/dist/channel-slack/feature.js +4 -2
  42. package/dist/channel-slack/index.d.ts +1 -1
  43. package/dist/channel-slack/index.js +1 -1
  44. package/dist/channel-slack/slack-channel.d.ts +4 -0
  45. package/dist/channel-slack/slack-channel.js +4 -0
  46. package/dist/channel-teams/feature.js +4 -2
  47. package/dist/channel-teams/index.d.ts +1 -1
  48. package/dist/channel-teams/index.js +1 -1
  49. package/dist/channel-teams/teams-channel.d.ts +4 -0
  50. package/dist/channel-teams/teams-channel.js +4 -0
  51. package/dist/channel-telegram/feature.js +4 -2
  52. package/dist/channel-telegram/index.d.ts +1 -1
  53. package/dist/channel-telegram/index.js +1 -1
  54. package/dist/channel-telegram/telegram-channel.d.ts +8 -0
  55. package/dist/channel-telegram/telegram-channel.js +28 -12
  56. package/dist/delivery/chat-webhook-channel.d.ts +5 -1
  57. package/dist/delivery/chat-webhook-channel.js +10 -5
  58. package/dist/delivery/chat-webhook-sender.d.ts +6 -2
  59. package/dist/delivery/chat-webhook-sender.js +13 -8
  60. package/dist/delivery/delivery-service.d.ts +4 -3
  61. package/dist/delivery/delivery-service.js +53 -33
  62. package/dist/delivery/index.d.ts +2 -2
  63. package/dist/delivery/index.js +2 -2
  64. package/dist/delivery/jobs.js +3 -1
  65. package/dist/delivery/types.d.ts +5 -3
  66. package/dist/inbound-mail-foundation/projection.js +37 -45
  67. package/dist/legal-pages/feature.d.ts +3 -15
  68. package/dist/legal-pages/feature.js +20 -50
  69. package/dist/legal-pages/lib/boot-check.d.ts +10 -0
  70. package/dist/legal-pages/lib/boot-check.js +33 -0
  71. package/dist/legal-pages/lib/validate-routes.d.ts +2 -0
  72. package/dist/legal-pages/lib/validate-routes.js +17 -0
  73. package/dist/page-render/index.d.ts +2 -1
  74. package/dist/page-render/index.js +1 -0
  75. package/dist/page-render/layout.d.ts +7 -0
  76. package/dist/page-render/layout.js +5 -0
  77. package/dist/page-render/trailing-slash.d.ts +2 -0
  78. package/dist/page-render/trailing-slash.js +15 -0
  79. package/dist/renderer-simple/feature.js +1 -0
  80. package/dist/renderer-simple/index.d.ts +1 -1
  81. package/dist/renderer-simple/index.js +1 -1
  82. package/dist/renderer-simple/simple-renderer.d.ts +12 -5
  83. package/dist/renderer-simple/simple-renderer.js +66 -12
  84. package/dist/secrets/announce-secret-change.d.ts +8 -0
  85. package/dist/secrets/announce-secret-change.js +18 -0
  86. package/dist/secrets/feature.js +1 -1
  87. package/dist/secrets/handlers/delete.write.js +2 -0
  88. package/dist/secrets/handlers/set.write.js +6 -1
  89. package/dist/secrets/i18n.d.ts +4 -0
  90. package/dist/secrets/i18n.js +4 -0
  91. package/dist/secrets/secrets-context.d.ts +2 -0
  92. package/dist/secrets/secrets-context.js +3 -0
  93. package/dist/secrets/write-gate.d.ts +5 -1
  94. package/dist/secrets/write-gate.js +24 -1
  95. package/dist/tenant/constants.d.ts +1 -0
  96. package/dist/tenant/constants.js +1 -0
  97. package/dist/tenant/handlers/add-member.write.js +14 -3
  98. package/dist/tenant/handlers/invitations.query.d.ts +8 -0
  99. package/dist/tenant/handlers/invitations.query.js +15 -1
  100. package/dist/tenant/index.d.ts +1 -0
  101. package/dist/tenant/index.js +1 -0
  102. package/dist/tenant/invitation-table.d.ts +2 -0
  103. package/dist/tenant/invitation-table.js +10 -1
  104. package/dist/tenant-lifecycle/lifecycle-gate.d.ts +1 -0
  105. package/dist/tenant-lifecycle/lifecycle-gate.js +2 -0
  106. package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
  107. package/dist/tier-engine/feature.js +83 -14
  108. package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +3 -3
  109. package/dist/tier-engine/handlers/set-tenant-tier.write.js +17 -10
  110. package/dist/user/handlers/update-roles.js +7 -18
  111. package/dist/user/invited-global-roles.d.ts +14 -0
  112. package/dist/user/invited-global-roles.js +35 -0
  113. package/dist/user-data-rights/feature.js +3 -0
  114. package/dist/waitlist/constants.d.ts +33 -0
  115. package/dist/waitlist/constants.js +35 -0
  116. package/dist/waitlist/entity.d.ts +433 -0
  117. package/dist/waitlist/entity.js +64 -0
  118. package/dist/waitlist/feature.d.ts +3 -0
  119. package/dist/waitlist/feature.js +38 -0
  120. package/dist/waitlist/handlers/invite.write.d.ts +9 -0
  121. package/dist/waitlist/handlers/invite.write.js +119 -0
  122. package/dist/waitlist/handlers/list.query.d.ts +1 -0
  123. package/dist/waitlist/handlers/list.query.js +10 -0
  124. package/dist/waitlist/handlers/reject.write.d.ts +7 -0
  125. package/dist/waitlist/handlers/reject.write.js +80 -0
  126. package/dist/waitlist/handlers/submit.write.d.ts +13 -0
  127. package/dist/waitlist/handlers/submit.write.js +112 -0
  128. package/dist/waitlist/i18n.d.ts +6 -0
  129. package/dist/waitlist/i18n.js +31 -0
  130. package/dist/waitlist/index.d.ts +5 -0
  131. package/dist/waitlist/index.js +4 -0
  132. package/dist/waitlist/lib.d.ts +10 -0
  133. package/dist/waitlist/lib.js +24 -0
  134. package/dist/waitlist/mail.d.ts +15 -0
  135. package/dist/waitlist/mail.js +73 -0
  136. package/dist/waitlist/options.d.ts +29 -0
  137. package/dist/waitlist/options.js +8 -0
  138. package/dist/waitlist/payloads.d.ts +14 -0
  139. package/dist/waitlist/payloads.js +47 -0
  140. package/dist/waitlist/screens.d.ts +2 -0
  141. package/dist/waitlist/screens.js +42 -0
  142. package/dist/waitlist/user-data-hooks.d.ts +3 -0
  143. package/dist/waitlist/user-data-hooks.js +71 -0
  144. package/dist/workflow-runner/db/queries/due-runs.d.ts +4 -5
  145. package/dist/workflow-runner/db/queries/due-runs.js +5 -4
  146. package/dist/workflow-runner/feature.js +4 -6
  147. package/package.json +17 -9
  148. package/src/auth-email-password/changes.json +18 -0
  149. package/src/auth-mfa/changes.json +6 -0
  150. package/src/billing-foundation/changes.json +13 -0
  151. package/src/channel-telegram/changes.json +9 -0
  152. package/src/delivery/changes.json +7 -0
  153. package/src/files/changes.json +13 -1
  154. package/src/inbound-mail-foundation/changes.json +7 -0
  155. package/src/renderer-simple/changes.json +6 -0
  156. package/src/secrets/changes.json +12 -0
  157. package/src/tenant/changes.json +6 -0
  158. package/src/user/changes.json +13 -6
  159. package/src/waitlist/changes.json +17 -0
  160. package/src/workflow-runner/changes.json +7 -0
  161. package/dist/billing-foundation/db/queries/payment-projection.d.ts +0 -2
  162. package/dist/billing-foundation/db/queries/payment-projection.js +0 -13
  163. package/dist/billing-foundation/db/queries/subscription-projection.d.ts +0 -2
  164. package/dist/billing-foundation/db/queries/subscription-projection.js +0 -7
  165. package/dist/inbound-mail-foundation/db/queries/inbound-projections.d.ts +0 -7
  166. package/dist/inbound-mail-foundation/db/queries/inbound-projections.js +0 -24
@@ -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 { createDiscordChannel } from "./discord-channel.js";
3
+ import { createDiscordChannel, DISCORD_DEFAULT_ALLOWED_HOSTS, DISCORD_WEBHOOK_PATH_PREFIX, } from "./discord-channel.js";
4
4
  export function createChannelDiscordFeature(options = {}) {
5
5
  return defineFeature("channel-discord", (r) => {
6
6
  r.describe('Posts delivery notifications to a Discord webhook, registered as the `discord` channel in the delivery system. Tenant-owned chat target: reach it only via `ctx.notify(type, { route: { discord: "<connection name>" } })`. The webhook URL is a tenant secret under `channel-discord:webhooks.<connection name>`; only the connection name is logged. Mentions are disabled and content is cut to 2000 characters. Requires `delivery` and `secrets`. Host allowlist (discord.com, discordapp.com, path /api/webhooks/), https-only and the request timeout are app options of `createChannelDiscordFeature(opts)`, never tenant config.');
@@ -11,6 +11,10 @@ export function createChannelDiscordFeature(options = {}) {
11
11
  });
12
12
  r.requires("delivery");
13
13
  r.requires("secrets");
14
+ const webhookTarget = resolveChatWebhookTarget({
15
+ defaultAllowedHosts: DISCORD_DEFAULT_ALLOWED_HOSTS,
16
+ requiredPathPrefix: DISCORD_WEBHOOK_PATH_PREFIX,
17
+ }, options);
14
18
  const webhooks = r.secretNamespace("webhooks", {
15
19
  label: { en: "Discord webhooks", de: "Discord-Webhooks", es: "Webhooks de Discord" },
16
20
  hint: {
@@ -20,6 +24,7 @@ export function createChannelDiscordFeature(options = {}) {
20
24
  },
21
25
  scope: "tenant",
22
26
  nameSchema: chatConnectionNameSchema,
27
+ valueSchema: chatWebhookUrlSchema(webhookTarget),
23
28
  });
24
29
  const channel = createDiscordChannel(options, webhooks.keyFor);
25
30
  r.useExtension(DELIVERY_CHANNEL_EXTENSION, "discord", {
@@ -1,2 +1,2 @@
1
- export { createDiscordChannel, type DiscordChannelOptions } from "./discord-channel.js";
1
+ export { createDiscordChannel, DISCORD_DEFAULT_ALLOWED_HOSTS, DISCORD_SECRET_KEYS, DISCORD_WEBHOOK_PATH_PREFIX, type DiscordChannelOptions, } from "./discord-channel.js";
2
2
  export { createChannelDiscordFeature } from "./feature.js";
@@ -1,2 +1,2 @@
1
- export { createDiscordChannel } from "./discord-channel.js";
1
+ export { createDiscordChannel, DISCORD_DEFAULT_ALLOWED_HOSTS, DISCORD_SECRET_KEYS, DISCORD_WEBHOOK_PATH_PREFIX, } from "./discord-channel.js";
2
2
  export { createChannelDiscordFeature } from "./feature.js";
@@ -73,6 +73,7 @@ export function createEmailChannel(options) {
73
73
  const html = await renderer.render({
74
74
  template: message.notificationType,
75
75
  variables,
76
+ locale: message.locale,
76
77
  });
77
78
  const subject = variables["subject"] ?? message.title; // @cast-boundary dynamic-key
78
79
  return { html, subject };
@@ -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 { createSlackChannel } from "./slack-channel.js";
3
+ import { createSlackChannel, SLACK_DEFAULT_ALLOWED_HOSTS, } from "./slack-channel.js";
4
4
  export function createChannelSlackFeature(options = {}) {
5
5
  return defineFeature("channel-slack", (r) => {
6
6
  r.describe('Posts delivery notifications to a Slack incoming webhook, registered as the `slack` channel in the delivery system. Tenant-owned chat target: reach it only via `ctx.notify(type, { route: { slack: "<connection name>" } })`. The webhook URL is a tenant secret under `channel-slack:webhooks.<connection name>`; only the connection name is logged. Requires `delivery` and `secrets`. Host allowlist (hooks.slack.com), https-only and the request timeout are app options of `createChannelSlackFeature(opts)`, never tenant config.');
@@ -11,6 +11,7 @@ export function createChannelSlackFeature(options = {}) {
11
11
  });
12
12
  r.requires("delivery");
13
13
  r.requires("secrets");
14
+ const webhookTarget = resolveChatWebhookTarget({ defaultAllowedHosts: SLACK_DEFAULT_ALLOWED_HOSTS }, options);
14
15
  const webhooks = r.secretNamespace("webhooks", {
15
16
  label: { en: "Slack webhooks", de: "Slack-Webhooks", es: "Webhooks de Slack" },
16
17
  hint: {
@@ -20,6 +21,7 @@ export function createChannelSlackFeature(options = {}) {
20
21
  },
21
22
  scope: "tenant",
22
23
  nameSchema: chatConnectionNameSchema,
24
+ valueSchema: chatWebhookUrlSchema(webhookTarget),
23
25
  });
24
26
  const channel = createSlackChannel(options, webhooks.keyFor);
25
27
  r.useExtension(DELIVERY_CHANNEL_EXTENSION, "slack", {
@@ -1,2 +1,2 @@
1
1
  export { createChannelSlackFeature } from "./feature.js";
2
- export { createSlackChannel, type SlackChannelOptions } from "./slack-channel.js";
2
+ export { createSlackChannel, SLACK_DEFAULT_ALLOWED_HOSTS, SLACK_SECRET_KEYS, type SlackChannelOptions, } from "./slack-channel.js";
@@ -1,2 +1,2 @@
1
1
  export { createChannelSlackFeature } from "./feature.js";
2
- export { createSlackChannel } from "./slack-channel.js";
2
+ export { createSlackChannel, SLACK_DEFAULT_ALLOWED_HOSTS, SLACK_SECRET_KEYS, } from "./slack-channel.js";
@@ -1,5 +1,9 @@
1
1
  import { type ChatWebhookChannelOptions, type DeliveryChannel } from "../delivery/index.js";
2
2
  export type SlackChannelOptions = ChatWebhookChannelOptions;
3
+ export declare const SLACK_SECRET_KEYS: {
4
+ readonly webhookPrefix: "channel-slack:webhooks.";
5
+ readonly webhookKeyFor: (connection: string) => string;
6
+ };
3
7
  export declare const SLACK_DEFAULT_ALLOWED_HOSTS: readonly ["hooks.slack.com"];
4
8
  export declare function escapeSlackText(text: string): string;
5
9
  export declare function createSlackChannel(options: SlackChannelOptions, keyFor: (connection: string) => string): DeliveryChannel;
@@ -1,4 +1,8 @@
1
1
  import { chatMessageText, createChatWebhookChannel, } from "../delivery/index.js";
2
+ export const SLACK_SECRET_KEYS = {
3
+ webhookPrefix: "channel-slack:webhooks.",
4
+ webhookKeyFor: (connection) => `channel-slack:webhooks.${connection}`,
5
+ };
2
6
  export const SLACK_DEFAULT_ALLOWED_HOSTS = ["hooks.slack.com"];
3
7
  // Slack mrkdwn treats & < > as control characters (links, @-mentions via <!channel>).
4
8
  export function escapeSlackText(text) {
@@ -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 { createTelegramChannel } from "./telegram-channel.js";
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 (!TELEGRAM_CHAT_ID_PATTERN.test(address)) {
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 || !TELEGRAM_BOT_TOKEN_PATTERN.test(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: [baseHost],
43
- requireHttps: apiBaseUrl.startsWith("https:"),
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
- allowedHosts: options.allowedHosts ?? spec.defaultAllowedHosts,
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 ChatWebhookRequest = {
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 checkTarget(request) {
30
+ export function checkChatWebhookTarget(target, url) {
31
31
  let parsed;
32
32
  try {
33
- parsed = new URL(request.url);
33
+ parsed = new URL(url);
34
34
  }
35
35
  catch {
36
36
  return undefined;
37
37
  }
38
- const protocolAllowed = parsed.protocol === "https:" || (!request.requireHttps && parsed.protocol === "http:");
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 (request.requireHttps && parsed.port !== "")
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 (!request.allowedHosts.some((allowed) => hostMatches(hostname, allowed)))
47
+ if (!target.allowedHosts.some((allowed) => hostMatches(hostname, allowed)))
48
48
  return undefined;
49
- if (request.requiredPathPrefix !== undefined &&
50
- !parsed.pathname.startsWith(request.requiredPathPrefix)) {
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 = checkTarget(request);
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?: JobRunner;
23
+ readonly jobRunner?: NotifyJobDispatcher;
24
+ readonly secrets?: SecretsContext;
24
25
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
25
26
  readonly log?: Logger;
26
27
  };