@cosmicdrift/kumiko-bundled-features 0.338.0 → 0.339.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -2
- package/dist/auth-email-password/handlers/invite-accept.write.js +2 -4
- package/dist/auth-email-password/handlers/invite-info.query.js +3 -1
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +2 -4
- package/dist/auth-email-password/handlers/signup-confirm.write.js +1 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +1 -1
- package/dist/auth-email-password/web/auth-gate.d.ts +12 -2
- package/dist/auth-email-password/web/auth-gate.js +25 -1
- package/dist/auth-email-password/web/auth-redirect.d.ts +5 -0
- package/dist/auth-email-password/web/auth-redirect.js +58 -0
- package/dist/auth-email-password/web/client-plugin.d.ts +8 -1
- package/dist/auth-email-password/web/client-plugin.js +2 -0
- package/dist/auth-email-password/web/index.d.ts +2 -1
- package/dist/auth-email-password/web/index.js +1 -0
- package/dist/auth-email-password/web/session.d.ts +3 -1
- package/dist/auth-email-password/web/session.js +8 -2
- package/dist/auth-mfa/feature.js +1 -1
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/enable-start-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/verify.write.js +1 -1
- package/dist/auth-mfa/mfa-code-verifier.js +1 -1
- package/dist/auth-mfa/mfa-status-checker.js +1 -1
- package/dist/billing-foundation/constants.d.ts +7 -0
- package/dist/billing-foundation/constants.js +8 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +3 -3
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +3 -3
- package/dist/billing-foundation/consumer-protection/consent-gate.js +10 -2
- package/dist/billing-foundation/consumer-protection/consent-text.js +2 -2
- package/dist/billing-foundation/consumer-protection/register-termination.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/register-termination.js +31 -3
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +3 -1
- package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -1
- package/dist/billing-foundation/consumer-protection/termination-notify.d.ts +18 -0
- package/dist/billing-foundation/consumer-protection/termination-notify.js +21 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.js +31 -2
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +24 -2
- package/dist/billing-foundation/consumer-protection/termination-record.js +102 -34
- package/dist/billing-foundation/consumer-protection/termination-request.js +10 -42
- package/dist/billing-foundation/consumer-protection/termination-texts.js +20 -20
- package/dist/billing-foundation/events.d.ts +19 -0
- package/dist/billing-foundation/events.js +12 -0
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +2 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +4 -2
- package/dist/billing-foundation/i18n.js +3 -0
- package/dist/billing-foundation/tenant-destroy-hook.js +4 -4
- package/dist/billing-foundation/types.d.ts +6 -1
- package/dist/billing-foundation/validate-options.js +4 -0
- package/dist/channel-discord/discord-channel.d.ts +6 -0
- package/dist/channel-discord/discord-channel.js +19 -0
- package/dist/channel-discord/feature.d.ts +3 -0
- package/dist/channel-discord/feature.js +30 -0
- package/dist/channel-discord/index.d.ts +2 -0
- package/dist/channel-discord/index.js +2 -0
- package/dist/channel-slack/feature.d.ts +3 -0
- package/dist/channel-slack/feature.js +30 -0
- package/dist/channel-slack/index.d.ts +2 -0
- package/dist/channel-slack/index.js +2 -0
- package/dist/channel-slack/slack-channel.d.ts +5 -0
- package/dist/channel-slack/slack-channel.js +16 -0
- package/dist/channel-teams/feature.d.ts +3 -0
- package/dist/channel-teams/feature.js +34 -0
- package/dist/channel-teams/index.d.ts +2 -0
- package/dist/channel-teams/index.js +2 -0
- package/dist/channel-teams/teams-channel.d.ts +5 -0
- package/dist/channel-teams/teams-channel.js +38 -0
- package/dist/channel-telegram/feature.d.ts +3 -0
- package/dist/channel-telegram/feature.js +28 -0
- package/dist/channel-telegram/index.d.ts +2 -0
- package/dist/channel-telegram/index.js +2 -0
- package/dist/channel-telegram/telegram-channel.d.ts +8 -0
- package/dist/channel-telegram/telegram-channel.js +54 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.js +6 -6
- package/dist/custom-fields/handlers/clear-custom-field.write.js +1 -1
- package/dist/custom-fields/handlers/define-tenant-field.write.js +1 -1
- package/dist/custom-fields/handlers/set-custom-field.write.js +1 -1
- package/dist/custom-fields/wire-user-data-rights.js +1 -1
- package/dist/data-retention/feature.js +1 -1
- package/dist/delivery/channel-context.d.ts +2 -1
- package/dist/delivery/channel-context.js +2 -2
- package/dist/delivery/chat-webhook-channel.d.ts +19 -0
- package/dist/delivery/chat-webhook-channel.js +48 -0
- package/dist/delivery/chat-webhook-sender.d.ts +21 -0
- package/dist/delivery/chat-webhook-sender.js +102 -0
- package/dist/delivery/delivery-service.js +3 -0
- package/dist/delivery/index.d.ts +2 -0
- package/dist/delivery/index.js +2 -0
- package/dist/delivery/jobs.js +1 -1
- package/dist/delivery/testing.d.ts +2 -0
- package/dist/delivery/types.d.ts +3 -1
- package/dist/delivery/types.js +1 -2
- package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
- package/dist/files-tenant-data/index.js +1 -1
- package/dist/form-draft/feature.js +1 -1
- package/dist/form-draft/handlers/discard.write.js +1 -1
- package/dist/form-draft/handlers/save.write.js +1 -1
- package/dist/inbound-mail-foundation/connect-routes.d.ts +7 -0
- package/dist/inbound-mail-foundation/connect-routes.js +13 -8
- package/dist/inbound-mail-foundation/feature.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +1 -1
- package/dist/inbound-mail-foundation/index.d.ts +1 -0
- package/dist/inbound-mail-foundation/index.js +1 -0
- package/dist/inbound-mail-foundation/oauth-access-token.d.ts +28 -0
- package/dist/inbound-mail-foundation/oauth-access-token.js +131 -0
- package/dist/inbound-mail-foundation/tenant-destroy-hook.js +1 -1
- package/dist/inbound-mail-foundation/types.d.ts +18 -1
- package/dist/inbound-mail-foundation/watch-supervisor.d.ts +5 -0
- package/dist/inbound-mail-foundation/watch-supervisor.js +42 -14
- package/dist/managed-pages/handlers/set.write.js +1 -1
- package/dist/notes-history/handlers/add-note.write.js +1 -1
- package/dist/renderer-simple/feature.d.ts +8 -2
- package/dist/renderer-simple/feature.js +8 -5
- package/dist/renderer-simple/index.d.ts +2 -2
- package/dist/renderer-simple/index.js +1 -1
- package/dist/renderer-simple/simple-renderer.d.ts +15 -0
- package/dist/renderer-simple/simple-renderer.js +74 -27
- package/dist/secrets/feature.js +1 -1
- package/dist/sessions/feature.js +1 -1
- package/dist/sessions/handlers/revoke-all-for-user.write.js +1 -1
- package/dist/tenant-handover/handlers/claim.write.js +1 -2
- package/dist/tenant-lifecycle/feature.js +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +3 -1
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +1 -1
- package/dist/tier-engine/feature.js +1 -1
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +1 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -1
- package/dist/user-data-rights/feature.js +2 -2
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
- package/dist/user-data-rights/handlers/download-by-job.query.js +1 -1
- package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
- package/dist/user-data-rights/handlers/export-status.query.js +1 -1
- package/dist/user-data-rights/handlers/lift-restriction.write.js +2 -4
- package/dist/user-data-rights/handlers/my-audit-log.query.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/request-export.write.js +1 -1
- package/dist/user-data-rights/handlers/restrict-account.write.js +2 -2
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +1 -1
- package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
- package/dist/workflow-runner/event-subscriber.js +6 -2
- package/dist/workflow-runner/event-trigger.js +5 -0
- package/dist/workflow-runner/handlers/resume-run.write.js +147 -59
- package/dist/workflow-runner/pending-projection.js +1 -0
- package/dist/workflow-runner/runner.d.ts +7 -1
- package/dist/workflow-runner/runner.js +1 -1
- package/dist/workflow-runner/tables.js +8 -1
- package/package.json +41 -9
- package/src/auth-email-password/changes.json +7 -0
- package/src/billing-foundation/changes.json +34 -0
- package/src/delivery/changes.json +7 -0
- package/src/inbound-mail-foundation/changes.json +7 -0
- package/src/renderer-simple/changes.json +9 -1
- package/src/workflow-runner/changes.json +7 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type { HandlerContext, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
5
|
-
import { type BillingEventKinds, type BillingPlanAction, SUBSCRIPTION_PROVIDER_EXTENSION, type SubscriptionCancelTiming, type SubscriptionEventType, type SubscriptionStatus } from "./constants.js";
|
|
5
|
+
import { type BillingEventKinds, type BillingPlanAction, SUBSCRIPTION_PROVIDER_EXTENSION, type SubscriptionCancelTiming, type SubscriptionEventType, type SubscriptionStatus, type TerminationScope } from "./constants.js";
|
|
6
6
|
export type SubscriptionEvent = {
|
|
7
7
|
/** Discriminator against PaymentEvent. Optional (instead of required) so
|
|
8
8
|
* existing plugins like subscription-mollie, which don't set this field,
|
|
@@ -289,6 +289,11 @@ export type ConsumerProtectionOptions = {
|
|
|
289
289
|
/** Per-locale VAT note; must carry at least `de` and `en`. */
|
|
290
290
|
readonly vatNote: Readonly<Record<string, string>>;
|
|
291
291
|
readonly operatorEmail: string;
|
|
292
|
+
/** Where the public § 312k declaration is reachable. `"tenant-host"`
|
|
293
|
+
* (default): on a host that resolves a tenant. `"platform"`: also on a
|
|
294
|
+
* host that resolves none (the platform apex) — the declaration then runs
|
|
295
|
+
* without a request tenant and finds the contract by the declarant's email. */
|
|
296
|
+
readonly terminationScope?: TerminationScope;
|
|
292
297
|
/** Root-relative paths or absolute https URLs. */
|
|
293
298
|
readonly legalLinks: {
|
|
294
299
|
readonly terms: string;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// concerns (app-feature-structure guard's 300-line budget). Internal — not
|
|
4
4
|
// re-exported from index.ts.
|
|
5
5
|
import * as z from "zod";
|
|
6
|
+
import { isTerminationScope, TERMINATION_SCOPES } from "./constants.js";
|
|
6
7
|
function isRootRelativePath(path) {
|
|
7
8
|
return path.startsWith("/") && !path.startsWith("//");
|
|
8
9
|
}
|
|
@@ -28,6 +29,9 @@ function validateConsumerProtection(cp) {
|
|
|
28
29
|
if (!z.email().safeParse(cp.operatorEmail).success) {
|
|
29
30
|
throw new Error(`createBillingFoundationFeature: consumerProtection.operatorEmail "${cp.operatorEmail}" is not a valid email address.`);
|
|
30
31
|
}
|
|
32
|
+
if (cp.terminationScope !== undefined && !isTerminationScope(cp.terminationScope)) {
|
|
33
|
+
throw new Error(`createBillingFoundationFeature: consumerProtection.terminationScope "${String(cp.terminationScope)}" must be one of ${TERMINATION_SCOPES.map((scope) => `"${scope}"`).join(", ")}.`);
|
|
34
|
+
}
|
|
31
35
|
for (const [key, link] of Object.entries(cp.legalLinks)) {
|
|
32
36
|
if (!isRootRelativeOrHttpsUrl(link)) {
|
|
33
37
|
throw new Error(`createBillingFoundationFeature: consumerProtection.legalLinks.${key} "${link}" must be a root-relative path ("/..." not "//") or an absolute https URL.`);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ChatWebhookChannelOptions, type DeliveryChannel } from "../delivery/index.js";
|
|
2
|
+
export type DiscordChannelOptions = ChatWebhookChannelOptions;
|
|
3
|
+
export declare const DISCORD_DEFAULT_ALLOWED_HOSTS: readonly ["discord.com", "discordapp.com"];
|
|
4
|
+
export declare const DISCORD_WEBHOOK_PATH_PREFIX = "/api/webhooks/";
|
|
5
|
+
export declare const DISCORD_CONTENT_MAX_CHARS = 2000;
|
|
6
|
+
export declare function createDiscordChannel(options: DiscordChannelOptions, keyFor: (connection: string) => string): DeliveryChannel;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { chatMessageText, createChatWebhookChannel, truncateChars, } from "../delivery/index.js";
|
|
2
|
+
export const DISCORD_DEFAULT_ALLOWED_HOSTS = ["discord.com", "discordapp.com"];
|
|
3
|
+
export const DISCORD_WEBHOOK_PATH_PREFIX = "/api/webhooks/";
|
|
4
|
+
export const DISCORD_CONTENT_MAX_CHARS = 2000;
|
|
5
|
+
export function createDiscordChannel(options, keyFor) {
|
|
6
|
+
return createChatWebhookChannel({
|
|
7
|
+
name: "discord",
|
|
8
|
+
featureName: "channel-discord",
|
|
9
|
+
defaultAllowedHosts: DISCORD_DEFAULT_ALLOWED_HOSTS,
|
|
10
|
+
requiredPathPrefix: DISCORD_WEBHOOK_PATH_PREFIX,
|
|
11
|
+
keyFor,
|
|
12
|
+
// parse: [] keeps @everyone / @here / role and user mentions in the text inert.
|
|
13
|
+
buildBody: (message) => ({
|
|
14
|
+
content: truncateChars(chatMessageText(message), DISCORD_CONTENT_MAX_CHARS),
|
|
15
|
+
allowed_mentions: { parse: [] },
|
|
16
|
+
}),
|
|
17
|
+
options,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { chatConnectionNameSchema, DELIVERY_CHANNEL_EXTENSION, } from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
2
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { createDiscordChannel } from "./discord-channel.js";
|
|
4
|
+
export function createChannelDiscordFeature(options = {}) {
|
|
5
|
+
return defineFeature("channel-discord", (r) => {
|
|
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.');
|
|
7
|
+
r.uiHints({
|
|
8
|
+
displayLabel: "Discord Channel",
|
|
9
|
+
category: "notifications",
|
|
10
|
+
recommended: false,
|
|
11
|
+
});
|
|
12
|
+
r.requires("delivery");
|
|
13
|
+
r.requires("secrets");
|
|
14
|
+
const webhooks = r.secretNamespace("webhooks", {
|
|
15
|
+
label: { en: "Discord webhooks", de: "Discord-Webhooks", es: "Webhooks de Discord" },
|
|
16
|
+
hint: {
|
|
17
|
+
en: 'One secret per connection name; the value is the Discord webhook URL. Address it with route: { discord: "<connection name>" }.',
|
|
18
|
+
de: 'Ein Secret pro Verbindungsname; der Wert ist die Discord-Webhook-URL. Adressierung mit route: { discord: "<Verbindungsname>" }.',
|
|
19
|
+
es: 'Un secreto por nombre de conexión; el valor es la URL del webhook de Discord. Se usa con route: { discord: "<nombre de conexión>" }.',
|
|
20
|
+
},
|
|
21
|
+
scope: "tenant",
|
|
22
|
+
nameSchema: chatConnectionNameSchema,
|
|
23
|
+
});
|
|
24
|
+
const channel = createDiscordChannel(options, webhooks.keyFor);
|
|
25
|
+
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "discord", {
|
|
26
|
+
mode: channel.mode,
|
|
27
|
+
send: channel.send,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { chatConnectionNameSchema, DELIVERY_CHANNEL_EXTENSION, } from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
2
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { createSlackChannel } from "./slack-channel.js";
|
|
4
|
+
export function createChannelSlackFeature(options = {}) {
|
|
5
|
+
return defineFeature("channel-slack", (r) => {
|
|
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.');
|
|
7
|
+
r.uiHints({
|
|
8
|
+
displayLabel: "Slack Channel",
|
|
9
|
+
category: "notifications",
|
|
10
|
+
recommended: false,
|
|
11
|
+
});
|
|
12
|
+
r.requires("delivery");
|
|
13
|
+
r.requires("secrets");
|
|
14
|
+
const webhooks = r.secretNamespace("webhooks", {
|
|
15
|
+
label: { en: "Slack webhooks", de: "Slack-Webhooks", es: "Webhooks de Slack" },
|
|
16
|
+
hint: {
|
|
17
|
+
en: 'One secret per connection name; the value is the Slack webhook URL. Address it with route: { slack: "<connection name>" }.',
|
|
18
|
+
de: 'Ein Secret pro Verbindungsname; der Wert ist die Slack-Webhook-URL. Adressierung mit route: { slack: "<Verbindungsname>" }.',
|
|
19
|
+
es: 'Un secreto por nombre de conexión; el valor es la URL del webhook de Slack. Se usa con route: { slack: "<nombre de conexión>" }.',
|
|
20
|
+
},
|
|
21
|
+
scope: "tenant",
|
|
22
|
+
nameSchema: chatConnectionNameSchema,
|
|
23
|
+
});
|
|
24
|
+
const channel = createSlackChannel(options, webhooks.keyFor);
|
|
25
|
+
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "slack", {
|
|
26
|
+
mode: channel.mode,
|
|
27
|
+
send: channel.send,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ChatWebhookChannelOptions, type DeliveryChannel } from "../delivery/index.js";
|
|
2
|
+
export type SlackChannelOptions = ChatWebhookChannelOptions;
|
|
3
|
+
export declare const SLACK_DEFAULT_ALLOWED_HOSTS: readonly ["hooks.slack.com"];
|
|
4
|
+
export declare function escapeSlackText(text: string): string;
|
|
5
|
+
export declare function createSlackChannel(options: SlackChannelOptions, keyFor: (connection: string) => string): DeliveryChannel;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { chatMessageText, createChatWebhookChannel, } from "../delivery/index.js";
|
|
2
|
+
export const SLACK_DEFAULT_ALLOWED_HOSTS = ["hooks.slack.com"];
|
|
3
|
+
// Slack mrkdwn treats & < > as control characters (links, @-mentions via <!channel>).
|
|
4
|
+
export function escapeSlackText(text) {
|
|
5
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
6
|
+
}
|
|
7
|
+
export function createSlackChannel(options, keyFor) {
|
|
8
|
+
return createChatWebhookChannel({
|
|
9
|
+
name: "slack",
|
|
10
|
+
featureName: "channel-slack",
|
|
11
|
+
defaultAllowedHosts: SLACK_DEFAULT_ALLOWED_HOSTS,
|
|
12
|
+
keyFor,
|
|
13
|
+
buildBody: (message) => ({ text: escapeSlackText(chatMessageText(message)) }),
|
|
14
|
+
options,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { chatConnectionNameSchema, DELIVERY_CHANNEL_EXTENSION, } from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
2
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { createTeamsChannel } from "./teams-channel.js";
|
|
4
|
+
export function createChannelTeamsFeature(options = {}) {
|
|
5
|
+
return defineFeature("channel-teams", (r) => {
|
|
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.');
|
|
7
|
+
r.uiHints({
|
|
8
|
+
displayLabel: "Microsoft Teams Channel",
|
|
9
|
+
category: "notifications",
|
|
10
|
+
recommended: false,
|
|
11
|
+
});
|
|
12
|
+
r.requires("delivery");
|
|
13
|
+
r.requires("secrets");
|
|
14
|
+
const webhooks = r.secretNamespace("webhooks", {
|
|
15
|
+
label: {
|
|
16
|
+
en: "Microsoft Teams webhooks",
|
|
17
|
+
de: "Microsoft Teams-Webhooks",
|
|
18
|
+
es: "Webhooks de Microsoft Teams",
|
|
19
|
+
},
|
|
20
|
+
hint: {
|
|
21
|
+
en: 'One secret per connection name; the value is the Microsoft Teams webhook URL. Address it with route: { teams: "<connection name>" }.',
|
|
22
|
+
de: 'Ein Secret pro Verbindungsname; der Wert ist die Microsoft Teams-Webhook-URL. Adressierung mit route: { teams: "<Verbindungsname>" }.',
|
|
23
|
+
es: 'Un secreto por nombre de conexión; el valor es la URL del webhook de Microsoft Teams. Se usa con route: { teams: "<nombre de conexión>" }.',
|
|
24
|
+
},
|
|
25
|
+
scope: "tenant",
|
|
26
|
+
nameSchema: chatConnectionNameSchema,
|
|
27
|
+
});
|
|
28
|
+
const channel = createTeamsChannel(options, webhooks.keyFor);
|
|
29
|
+
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "teams", {
|
|
30
|
+
mode: channel.mode,
|
|
31
|
+
send: channel.send,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ChatWebhookChannelOptions, type DeliveryChannel } from "../delivery/index.js";
|
|
2
|
+
export type TeamsChannelOptions = ChatWebhookChannelOptions;
|
|
3
|
+
export declare const TEAMS_DEFAULT_ALLOWED_HOSTS: readonly [".webhook.office.com", ".logic.azure.com", ".powerplatform.com"];
|
|
4
|
+
export declare function buildTeamsAdaptiveCard(title: string, body: string | undefined): unknown;
|
|
5
|
+
export declare function createTeamsChannel(options: TeamsChannelOptions, keyFor: (connection: string) => string): DeliveryChannel;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createChatWebhookChannel, } from "../delivery/index.js";
|
|
2
|
+
export const TEAMS_DEFAULT_ALLOWED_HOSTS = [
|
|
3
|
+
".webhook.office.com",
|
|
4
|
+
".logic.azure.com",
|
|
5
|
+
".powerplatform.com",
|
|
6
|
+
];
|
|
7
|
+
// Text only ever goes into TextBlocks (no markup interpretation of titles/bodies
|
|
8
|
+
// beyond Adaptive Card's own text rendering).
|
|
9
|
+
export function buildTeamsAdaptiveCard(title, body) {
|
|
10
|
+
return {
|
|
11
|
+
type: "message",
|
|
12
|
+
attachments: [
|
|
13
|
+
{
|
|
14
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
15
|
+
contentUrl: null,
|
|
16
|
+
content: {
|
|
17
|
+
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
18
|
+
type: "AdaptiveCard",
|
|
19
|
+
version: "1.4",
|
|
20
|
+
body: [
|
|
21
|
+
{ type: "TextBlock", text: title, weight: "Bolder", wrap: true },
|
|
22
|
+
...(body ? [{ type: "TextBlock", text: body, wrap: true }] : []),
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function createTeamsChannel(options, keyFor) {
|
|
30
|
+
return createChatWebhookChannel({
|
|
31
|
+
name: "teams",
|
|
32
|
+
featureName: "channel-teams",
|
|
33
|
+
defaultAllowedHosts: TEAMS_DEFAULT_ALLOWED_HOSTS,
|
|
34
|
+
keyFor,
|
|
35
|
+
buildBody: (message) => buildTeamsAdaptiveCard(message.title, message.body),
|
|
36
|
+
options,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DELIVERY_CHANNEL_EXTENSION } from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
2
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { createTelegramChannel } from "./telegram-channel.js";
|
|
4
|
+
export function createChannelTelegramFeature(options = {}) {
|
|
5
|
+
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.uiHints({
|
|
8
|
+
displayLabel: "Telegram Channel",
|
|
9
|
+
category: "notifications",
|
|
10
|
+
recommended: false,
|
|
11
|
+
});
|
|
12
|
+
r.requires("delivery");
|
|
13
|
+
r.requires("secrets");
|
|
14
|
+
const botToken = r.secret("botToken", {
|
|
15
|
+
label: { en: "Telegram bot token" },
|
|
16
|
+
hint: {
|
|
17
|
+
en: 'Token from @BotFather. Used for every Telegram chat the tenant addresses via route: { telegram: "<chat id>" }.',
|
|
18
|
+
},
|
|
19
|
+
redact: () => "••••••••",
|
|
20
|
+
scope: "tenant",
|
|
21
|
+
});
|
|
22
|
+
const channel = createTelegramChannel(options, botToken.name);
|
|
23
|
+
r.useExtension(DELIVERY_CHANNEL_EXTENSION, "telegram", {
|
|
24
|
+
mode: channel.mode,
|
|
25
|
+
send: channel.send,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type DeliveryChannel } from "../delivery/index.js";
|
|
2
|
+
export type TelegramChannelOptions = {
|
|
3
|
+
readonly apiBaseUrl?: string;
|
|
4
|
+
readonly timeoutMs?: number;
|
|
5
|
+
};
|
|
6
|
+
export declare const TELEGRAM_DEFAULT_API_BASE_URL = "https://api.telegram.org";
|
|
7
|
+
export declare const TELEGRAM_TEXT_MAX_CHARS = 4096;
|
|
8
|
+
export declare function createTelegramChannel(options: TelegramChannelOptions, botTokenKey: string): DeliveryChannel;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { SYSTEM_USER_ID } from "@cosmicdrift/kumiko-types/identifiers";
|
|
2
|
+
import { chatMessageText, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, toChannelResult, truncateChars, } from "../delivery/index.js";
|
|
3
|
+
export const TELEGRAM_DEFAULT_API_BASE_URL = "https://api.telegram.org";
|
|
4
|
+
export const TELEGRAM_TEXT_MAX_CHARS = 4096;
|
|
5
|
+
// Numeric chat/group id (groups and channels are negative) or a public @channelname.
|
|
6
|
+
const TELEGRAM_CHAT_ID_PATTERN = /^(-?\d{1,20}|@[A-Za-z][A-Za-z0-9_]{3,31})$/;
|
|
7
|
+
// `<bot id>:<secret>`; the token is spliced into the URL path, so only the real shape passes.
|
|
8
|
+
const TELEGRAM_BOT_TOKEN_PATTERN = /^\d{1,20}:[A-Za-z0-9_-]{10,}$/;
|
|
9
|
+
export function createTelegramChannel(options, botTokenKey) {
|
|
10
|
+
const apiBaseUrl = options.apiBaseUrl ?? TELEGRAM_DEFAULT_API_BASE_URL;
|
|
11
|
+
return {
|
|
12
|
+
name: "telegram",
|
|
13
|
+
mode: "queued",
|
|
14
|
+
async send(address, message, ctx) {
|
|
15
|
+
if (!TELEGRAM_CHAT_ID_PATTERN.test(address)) {
|
|
16
|
+
return toChannelResult(address, { ok: false, code: "invalid_address" });
|
|
17
|
+
}
|
|
18
|
+
let token;
|
|
19
|
+
try {
|
|
20
|
+
const revealed = await ctx.secrets?.get(ctx.tenantId, botTokenKey, {
|
|
21
|
+
userId: SYSTEM_USER_ID,
|
|
22
|
+
handlerName: "channel-telegram:send",
|
|
23
|
+
});
|
|
24
|
+
token = revealed?.reveal();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
token = undefined;
|
|
28
|
+
}
|
|
29
|
+
if (!token || !TELEGRAM_BOT_TOKEN_PATTERN.test(token)) {
|
|
30
|
+
return toChannelResult(address, { ok: false, code: "missing_credentials" });
|
|
31
|
+
}
|
|
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
|
+
const result = await postChatWebhook({
|
|
41
|
+
url: `${apiBaseUrl.replace(/\/+$/, "")}/bot${token}/sendMessage`,
|
|
42
|
+
allowedHosts: [baseHost],
|
|
43
|
+
requireHttps: apiBaseUrl.startsWith("https:"),
|
|
44
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_CHAT_TIMEOUT_MS,
|
|
45
|
+
// No parse_mode: Telegram renders the text literally.
|
|
46
|
+
body: {
|
|
47
|
+
chat_id: address,
|
|
48
|
+
text: truncateChars(chatMessageText(message), TELEGRAM_TEXT_MAX_CHARS),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
return toChannelResult(address, result);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -248,17 +248,17 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
248
248
|
? { kind: "tenant", tenantId: raw.tenantId }
|
|
249
249
|
: { kind: "record", entity: raw.entity, id: raw.id };
|
|
250
250
|
const subjectKey = subjectIdToKey(subject);
|
|
251
|
-
const tenantScopeDenial = await resolveTenantScopeDenial(ctx.db.unsafeRaw(
|
|
251
|
+
const tenantScopeDenial = await resolveTenantScopeDenial(ctx.db.unsafeRaw(), ctx.registry.features, event.user, raw);
|
|
252
252
|
if (tenantScopeDenial) {
|
|
253
|
-
const auditFailure = await appendDenialAuditEvent(ctx, event, subjectKey, raw.kind, denialReasonOf(tenantScopeDenial), ctx.dbOutsideTransaction?.unsafeRaw(
|
|
253
|
+
const auditFailure = await appendDenialAuditEvent(ctx, event, subjectKey, raw.kind, denialReasonOf(tenantScopeDenial), ctx.dbOutsideTransaction?.unsafeRaw());
|
|
254
254
|
return auditFailure ?? tenantScopeDenial;
|
|
255
255
|
}
|
|
256
256
|
// Runs after the tenant gate but before any key is touched: a retention
|
|
257
257
|
// check ahead of the tenant gate would leak a foreign entity's retention
|
|
258
258
|
// posture to a cross-tenant prober.
|
|
259
|
-
const retentionDenial = await resolveRetentionDenial(ctx, raw, () => ctx.db.unsafeRaw(
|
|
259
|
+
const retentionDenial = await resolveRetentionDenial(ctx, raw, () => ctx.db.unsafeRaw());
|
|
260
260
|
if (retentionDenial) {
|
|
261
|
-
const auditFailure = await appendDenialAuditEvent(ctx, event, subjectKey, raw.kind, TARGET_RECORD_RETENTION_BLOCK_DELETE, ctx.dbOutsideTransaction?.unsafeRaw(
|
|
261
|
+
const auditFailure = await appendDenialAuditEvent(ctx, event, subjectKey, raw.kind, TARGET_RECORD_RETENTION_BLOCK_DELETE, ctx.dbOutsideTransaction?.unsafeRaw());
|
|
262
262
|
return auditFailure ?? retentionDenial;
|
|
263
263
|
}
|
|
264
264
|
// Erase BEFORE the audit append: if the append throws, the key is gone
|
|
@@ -272,7 +272,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
272
272
|
});
|
|
273
273
|
// Blind-index sweep (#818): nulls bidx columns now so the deterministic
|
|
274
274
|
// HMAC doesn't stay equality-matchable; raw because the ciphertext prefix addresses the subject across tenants.
|
|
275
|
-
const crossTenantSubjectRunner = ctx.db.unsafeRaw(
|
|
275
|
+
const crossTenantSubjectRunner = ctx.db.unsafeRaw();
|
|
276
276
|
await nullBlindIndexesForSubject(crossTenantSubjectRunner, ctx.registry.features, subjectKey);
|
|
277
277
|
// Derived search index still holds plaintext (#1610) — purge next to the
|
|
278
278
|
// blind-index sweep. No adapter → no-op (apps without search).
|
|
@@ -300,7 +300,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
300
300
|
// update is best-effort for real users.
|
|
301
301
|
if (raw.kind === "user" && ctx.registry.features.has("user")) {
|
|
302
302
|
try {
|
|
303
|
-
const userLifecycleRunner = ctx.db.unsafeRaw(
|
|
303
|
+
const userLifecycleRunner = ctx.db.unsafeRaw();
|
|
304
304
|
await updateUserLifecycle(userLifecycleRunner, raw.userId, { status: USER_STATUS.Deleted });
|
|
305
305
|
if (ctx.registry.features.has("personal-access-tokens")) {
|
|
306
306
|
await revokeAllPatTokensForUser(userLifecycleRunner, raw.userId);
|
|
@@ -31,7 +31,7 @@ export const clearCustomFieldHandler = {
|
|
|
31
31
|
},
|
|
32
32
|
handler: async (event, ctx) => {
|
|
33
33
|
const payload = event.payload; // @cast-boundary engine-payload
|
|
34
|
-
const accessCheck = await checkFieldAccessForWrite(ctx.db.unsafeRaw(
|
|
34
|
+
const accessCheck = await checkFieldAccessForWrite(ctx.db.unsafeRaw(), event.user.tenantId, payload.entityName, payload.fieldKey, event.user.roles);
|
|
35
35
|
if (!accessCheck.ok) {
|
|
36
36
|
if (accessCheck.reason === "field_definition_not_found") {
|
|
37
37
|
return failNotFound("fieldDefinition", payload.fieldKey);
|
|
@@ -26,7 +26,7 @@ export function createDefineTenantFieldHandler(opts = {}) {
|
|
|
26
26
|
throw new Error("define-tenant-field: tenantId is SYSTEM_TENANT_ID — use define-system-field for system-scope definitions");
|
|
27
27
|
}
|
|
28
28
|
if (limit !== undefined) {
|
|
29
|
-
const current = await countTenantFieldDefinitions(ctx.db.unsafeRaw(
|
|
29
|
+
const current = await countTenantFieldDefinitions(ctx.db.unsafeRaw(), tenantId);
|
|
30
30
|
if (current >= limit) {
|
|
31
31
|
return failUnprocessable("cap_exceeded", {
|
|
32
32
|
capName: "customFields.fieldDefinition.count",
|
|
@@ -54,7 +54,7 @@ export const setCustomFieldHandler = {
|
|
|
54
54
|
},
|
|
55
55
|
handler: async (event, ctx) => {
|
|
56
56
|
const payload = event.payload; // @cast-boundary engine-payload
|
|
57
|
-
const loaded = await loadFieldDefinition(ctx.db.unsafeRaw(
|
|
57
|
+
const loaded = await loadFieldDefinition(ctx.db.unsafeRaw(), event.user.tenantId, payload.entityName, payload.fieldKey);
|
|
58
58
|
if (!loaded.found) {
|
|
59
59
|
return failNotFound("fieldDefinition", payload.fieldKey);
|
|
60
60
|
}
|
|
@@ -46,7 +46,7 @@ export function wireCustomFieldsUserDataRightsFor(r, opts) {
|
|
|
46
46
|
const tableName = extractTableName(opts.entityTable, "custom-fields/wire-user-data-rights");
|
|
47
47
|
// biome-ignore lint/correctness/useHookAtTopLevel: r.useExtension is a registrar API, not a React hook.
|
|
48
48
|
r.useExtension(EXT_USER_DATA, opts.entityName, {
|
|
49
|
-
export: async (ctx) => exportCustomFields(ctx.db.unsafeRaw(
|
|
49
|
+
export: async (ctx) => exportCustomFields(ctx.db.unsafeRaw(), ctx, tableName, opts),
|
|
50
50
|
escapeHatch: { reason: CUSTOM_FIELDS_EXPORT_REASON },
|
|
51
51
|
});
|
|
52
52
|
}
|
|
@@ -64,7 +64,7 @@ export function createDataRetentionFeature() {
|
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
|
|
67
|
-
const cleanupDb = ctx.db.unsafeRaw(
|
|
67
|
+
const cleanupDb = ctx.db.unsafeRaw(); // @cast-boundary db-operator — jobs never run inside a DbTx
|
|
68
68
|
const tenantPreset = await resolveTenantRetentionPreset({
|
|
69
69
|
db: cleanupDb,
|
|
70
70
|
registry: ctx.registry,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import { type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type { Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
4
5
|
import type { ChannelContext } from "./types.js";
|
|
5
|
-
export declare function buildChannelContext(db: DbConnection, registry: Registry, sseBroker: SseBroker | undefined, tenantId: TenantId): ChannelContext;
|
|
6
|
+
export declare function buildChannelContext(db: DbConnection, registry: Registry, sseBroker: SseBroker | undefined, tenantId: TenantId, secrets?: SecretsContext): ChannelContext;
|
|
@@ -3,6 +3,6 @@ import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
|
3
3
|
// Shared by the synchronous delivery-service path and the async job handlers
|
|
4
4
|
// (which pass sseBroker=undefined — only the inline inApp channel uses SSE,
|
|
5
5
|
// and it never runs through a job).
|
|
6
|
-
export function buildChannelContext(db, registry, sseBroker, tenantId) {
|
|
7
|
-
return { db: createTenantDb(db, tenantId), registry, sseBroker, tenantId };
|
|
6
|
+
export function buildChannelContext(db, registry, sseBroker, tenantId, secrets) {
|
|
7
|
+
return { db: createTenantDb(db, tenantId), registry, sseBroker, tenantId, secrets };
|
|
8
8
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ChatSendResult } from "./chat-webhook-sender.js";
|
|
2
|
+
import type { ChannelMessage, ChannelResult, DeliveryChannel } from "./types.js";
|
|
3
|
+
export type ChatWebhookChannelOptions = {
|
|
4
|
+
readonly allowedHosts?: readonly string[];
|
|
5
|
+
readonly requireHttps?: boolean;
|
|
6
|
+
readonly timeoutMs?: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function toChannelResult(address: string, result: ChatSendResult): ChannelResult;
|
|
9
|
+
export declare function chatMessageText(message: ChannelMessage): string;
|
|
10
|
+
export type ChatWebhookChannelSpec = {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly featureName: string;
|
|
13
|
+
readonly defaultAllowedHosts: readonly string[];
|
|
14
|
+
readonly requiredPathPrefix?: string;
|
|
15
|
+
readonly keyFor: (connection: string) => string;
|
|
16
|
+
readonly buildBody: (message: ChannelMessage) => unknown;
|
|
17
|
+
readonly options: ChatWebhookChannelOptions;
|
|
18
|
+
};
|
|
19
|
+
export declare function createChatWebhookChannel(spec: ChatWebhookChannelSpec): DeliveryChannel;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { SYSTEM_USER_ID } from "@cosmicdrift/kumiko-types/identifiers";
|
|
2
|
+
import { chatConnectionNameSchema, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, } from "./chat-webhook-sender.js";
|
|
3
|
+
export function toChannelResult(address, result) {
|
|
4
|
+
return result.ok
|
|
5
|
+
? { status: "sent", address }
|
|
6
|
+
: { status: "failed", error: result.code, address };
|
|
7
|
+
}
|
|
8
|
+
export function chatMessageText(message) {
|
|
9
|
+
return message.body ? `${message.title}\n${message.body}` : message.title;
|
|
10
|
+
}
|
|
11
|
+
// Slack/Discord/Teams: the route address is a tenant-chosen connection name, the
|
|
12
|
+
// webhook URL lives in the secrets store under that name. Route-only (no resolve).
|
|
13
|
+
export function createChatWebhookChannel(spec) {
|
|
14
|
+
const { options } = spec;
|
|
15
|
+
return {
|
|
16
|
+
name: spec.name,
|
|
17
|
+
mode: "queued",
|
|
18
|
+
async send(address, message, ctx) {
|
|
19
|
+
if (!chatConnectionNameSchema.safeParse(address).success) {
|
|
20
|
+
return toChannelResult(address, { ok: false, code: "invalid_address" });
|
|
21
|
+
}
|
|
22
|
+
let url;
|
|
23
|
+
try {
|
|
24
|
+
const revealed = await ctx.secrets?.get(ctx.tenantId, spec.keyFor(address), {
|
|
25
|
+
userId: SYSTEM_USER_ID,
|
|
26
|
+
handlerName: `${spec.featureName}:send`,
|
|
27
|
+
});
|
|
28
|
+
url = revealed?.reveal();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
url = undefined;
|
|
32
|
+
}
|
|
33
|
+
if (!url)
|
|
34
|
+
return toChannelResult(address, { ok: false, code: "missing_credentials" });
|
|
35
|
+
const result = await postChatWebhook({
|
|
36
|
+
url,
|
|
37
|
+
allowedHosts: options.allowedHosts ?? spec.defaultAllowedHosts,
|
|
38
|
+
requireHttps: options.requireHttps ?? true,
|
|
39
|
+
...(spec.requiredPathPrefix !== undefined && {
|
|
40
|
+
requiredPathPrefix: spec.requiredPathPrefix,
|
|
41
|
+
}),
|
|
42
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_CHAT_TIMEOUT_MS,
|
|
43
|
+
body: spec.buildBody(message),
|
|
44
|
+
});
|
|
45
|
+
return toChannelResult(address, result);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
export type ChatSendFailureCode = `http_${number}` | "timeout" | "network_error" | "redirect_blocked" | "host_not_allowed" | "missing_credentials" | "invalid_address";
|
|
3
|
+
export type ChatSendResult = {
|
|
4
|
+
readonly ok: true;
|
|
5
|
+
} | {
|
|
6
|
+
readonly ok: false;
|
|
7
|
+
readonly code: ChatSendFailureCode;
|
|
8
|
+
};
|
|
9
|
+
export declare const DEFAULT_CHAT_TIMEOUT_MS = 10000;
|
|
10
|
+
export declare const chatConnectionNameSchema: z.ZodString;
|
|
11
|
+
export type ChatWebhookRequest = {
|
|
12
|
+
readonly url: string;
|
|
13
|
+
readonly allowedHosts: readonly string[];
|
|
14
|
+
readonly requireHttps: boolean;
|
|
15
|
+
readonly requiredPathPrefix?: string;
|
|
16
|
+
readonly timeoutMs: number;
|
|
17
|
+
readonly body: unknown;
|
|
18
|
+
readonly send?: (url: string, init: RequestInit) => Promise<Response>;
|
|
19
|
+
};
|
|
20
|
+
export declare function postChatWebhook(request: ChatWebhookRequest): Promise<ChatSendResult>;
|
|
21
|
+
export declare function truncateChars(text: string, maxChars: number): string;
|