@cogenta/channels 0.1.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/adapter.d.ts +111 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +2 -0
- package/dist/adapter.js.map +1 -0
- package/dist/approvals/commands.d.ts +28 -0
- package/dist/approvals/commands.d.ts.map +1 -0
- package/dist/approvals/commands.js +66 -0
- package/dist/approvals/commands.js.map +1 -0
- package/dist/approvals/dispatch.d.ts +22 -0
- package/dist/approvals/dispatch.d.ts.map +1 -0
- package/dist/approvals/dispatch.js +22 -0
- package/dist/approvals/dispatch.js.map +1 -0
- package/dist/approvals/message.d.ts +18 -0
- package/dist/approvals/message.d.ts.map +1 -0
- package/dist/approvals/message.js +40 -0
- package/dist/approvals/message.js.map +1 -0
- package/dist/approvals/signed-link.d.ts +19 -0
- package/dist/approvals/signed-link.d.ts.map +1 -0
- package/dist/approvals/signed-link.js +42 -0
- package/dist/approvals/signed-link.js.map +1 -0
- package/dist/approvals/store.d.ts +41 -0
- package/dist/approvals/store.d.ts.map +1 -0
- package/dist/approvals/store.js +41 -0
- package/dist/approvals/store.js.map +1 -0
- package/dist/approvals/token.d.ts +7 -0
- package/dist/approvals/token.d.ts.map +1 -0
- package/dist/approvals/token.js +40 -0
- package/dist/approvals/token.js.map +1 -0
- package/dist/formats/alert.d.ts +18 -0
- package/dist/formats/alert.d.ts.map +1 -0
- package/dist/formats/alert.js +49 -0
- package/dist/formats/alert.js.map +1 -0
- package/dist/formats/budget.d.ts +14 -0
- package/dist/formats/budget.d.ts.map +1 -0
- package/dist/formats/budget.js +13 -0
- package/dist/formats/budget.js.map +1 -0
- package/dist/formats/notification.d.ts +8 -0
- package/dist/formats/notification.d.ts.map +1 -0
- package/dist/formats/notification.js +26 -0
- package/dist/formats/notification.js.map +1 -0
- package/dist/formats/report.d.ts +18 -0
- package/dist/formats/report.d.ts.map +1 -0
- package/dist/formats/report.js +44 -0
- package/dist/formats/report.js.map +1 -0
- package/dist/inbound/authorize.d.ts +57 -0
- package/dist/inbound/authorize.d.ts.map +1 -0
- package/dist/inbound/authorize.js +32 -0
- package/dist/inbound/authorize.js.map +1 -0
- package/dist/inbound/router.d.ts +67 -0
- package/dist/inbound/router.d.ts.map +1 -0
- package/dist/inbound/router.js +69 -0
- package/dist/inbound/router.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/linking/codes.d.ts +6 -0
- package/dist/linking/codes.d.ts.map +1 -0
- package/dist/linking/codes.js +37 -0
- package/dist/linking/codes.js.map +1 -0
- package/dist/linking/store.d.ts +39 -0
- package/dist/linking/store.d.ts.map +1 -0
- package/dist/linking/store.js +77 -0
- package/dist/linking/store.js.map +1 -0
- package/dist/linking/tables.d.ts +14 -0
- package/dist/linking/tables.d.ts.map +1 -0
- package/dist/linking/tables.js +52 -0
- package/dist/linking/tables.js.map +1 -0
- package/dist/preferences/dispatcher.d.ts +37 -0
- package/dist/preferences/dispatcher.d.ts.map +1 -0
- package/dist/preferences/dispatcher.js +103 -0
- package/dist/preferences/dispatcher.js.map +1 -0
- package/dist/preferences/quiet-hours.d.ts +12 -0
- package/dist/preferences/quiet-hours.d.ts.map +1 -0
- package/dist/preferences/quiet-hours.js +22 -0
- package/dist/preferences/quiet-hours.js.map +1 -0
- package/dist/preferences/store.d.ts +8 -0
- package/dist/preferences/store.d.ts.map +1 -0
- package/dist/preferences/store.js +60 -0
- package/dist/preferences/store.js.map +1 -0
- package/dist/preferences/tables.d.ts +12 -0
- package/dist/preferences/tables.d.ts.map +1 -0
- package/dist/preferences/tables.js +55 -0
- package/dist/preferences/tables.js.map +1 -0
- package/dist/preferences/types.d.ts +37 -0
- package/dist/preferences/types.d.ts.map +1 -0
- package/dist/preferences/types.js +31 -0
- package/dist/preferences/types.js.map +1 -0
- package/dist/providers/discord/adapter.d.ts +38 -0
- package/dist/providers/discord/adapter.d.ts.map +1 -0
- package/dist/providers/discord/adapter.js +97 -0
- package/dist/providers/discord/adapter.js.map +1 -0
- package/dist/providers/discord/client.d.ts +67 -0
- package/dist/providers/discord/client.d.ts.map +1 -0
- package/dist/providers/discord/client.js +65 -0
- package/dist/providers/discord/client.js.map +1 -0
- package/dist/providers/discord/gateway.d.ts +55 -0
- package/dist/providers/discord/gateway.d.ts.map +1 -0
- package/dist/providers/discord/gateway.js +114 -0
- package/dist/providers/discord/gateway.js.map +1 -0
- package/dist/providers/discord/inbound.d.ts +22 -0
- package/dist/providers/discord/inbound.d.ts.map +1 -0
- package/dist/providers/discord/inbound.js +75 -0
- package/dist/providers/discord/inbound.js.map +1 -0
- package/dist/providers/discord/render.d.ts +9 -0
- package/dist/providers/discord/render.d.ts.map +1 -0
- package/dist/providers/discord/render.js +73 -0
- package/dist/providers/discord/render.js.map +1 -0
- package/dist/providers/email/adapter.d.ts +18 -0
- package/dist/providers/email/adapter.d.ts.map +1 -0
- package/dist/providers/email/adapter.js +41 -0
- package/dist/providers/email/adapter.js.map +1 -0
- package/dist/providers/email/file-transport.d.ts +14 -0
- package/dist/providers/email/file-transport.d.ts.map +1 -0
- package/dist/providers/email/file-transport.js +33 -0
- package/dist/providers/email/file-transport.js.map +1 -0
- package/dist/providers/email/render.d.ts +20 -0
- package/dist/providers/email/render.d.ts.map +1 -0
- package/dist/providers/email/render.js +89 -0
- package/dist/providers/email/render.js.map +1 -0
- package/dist/providers/email/transport.d.ts +25 -0
- package/dist/providers/email/transport.d.ts.map +1 -0
- package/dist/providers/email/transport.js +2 -0
- package/dist/providers/email/transport.js.map +1 -0
- package/dist/providers/slack/adapter.d.ts +35 -0
- package/dist/providers/slack/adapter.d.ts.map +1 -0
- package/dist/providers/slack/adapter.js +91 -0
- package/dist/providers/slack/adapter.js.map +1 -0
- package/dist/providers/slack/client.d.ts +45 -0
- package/dist/providers/slack/client.d.ts.map +1 -0
- package/dist/providers/slack/client.js +54 -0
- package/dist/providers/slack/client.js.map +1 -0
- package/dist/providers/slack/inbound.d.ts +23 -0
- package/dist/providers/slack/inbound.d.ts.map +1 -0
- package/dist/providers/slack/inbound.js +76 -0
- package/dist/providers/slack/inbound.js.map +1 -0
- package/dist/providers/slack/render.d.ts +8 -0
- package/dist/providers/slack/render.d.ts.map +1 -0
- package/dist/providers/slack/render.js +72 -0
- package/dist/providers/slack/render.js.map +1 -0
- package/dist/providers/slack/socket.d.ts +34 -0
- package/dist/providers/slack/socket.d.ts.map +1 -0
- package/dist/providers/slack/socket.js +70 -0
- package/dist/providers/slack/socket.js.map +1 -0
- package/dist/providers/telegram/adapter.d.ts +35 -0
- package/dist/providers/telegram/adapter.d.ts.map +1 -0
- package/dist/providers/telegram/adapter.js +109 -0
- package/dist/providers/telegram/adapter.js.map +1 -0
- package/dist/providers/telegram/client.d.ts +78 -0
- package/dist/providers/telegram/client.d.ts.map +1 -0
- package/dist/providers/telegram/client.js +53 -0
- package/dist/providers/telegram/client.js.map +1 -0
- package/dist/providers/telegram/inbound.d.ts +21 -0
- package/dist/providers/telegram/inbound.d.ts.map +1 -0
- package/dist/providers/telegram/inbound.js +72 -0
- package/dist/providers/telegram/inbound.js.map +1 -0
- package/dist/providers/telegram/render.d.ts +15 -0
- package/dist/providers/telegram/render.d.ts.map +1 -0
- package/dist/providers/telegram/render.js +87 -0
- package/dist/providers/telegram/render.js.map +1 -0
- package/dist/providers/webhook/adapter.d.ts +40 -0
- package/dist/providers/webhook/adapter.d.ts.map +1 -0
- package/dist/providers/webhook/adapter.js +71 -0
- package/dist/providers/webhook/adapter.js.map +1 -0
- package/dist/providers/webhook/render.d.ts +26 -0
- package/dist/providers/webhook/render.d.ts.map +1 -0
- package/dist/providers/webhook/render.js +41 -0
- package/dist/providers/webhook/render.js.map +1 -0
- package/dist/providers/webhook/signing.d.ts +59 -0
- package/dist/providers/webhook/signing.d.ts.map +1 -0
- package/dist/providers/webhook/signing.js +108 -0
- package/dist/providers/webhook/signing.js.map +1 -0
- package/dist/registry.d.ts +16 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +39 -0
- package/dist/registry.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal, hand-typed client for the four Telegram Bot API methods this
|
|
3
|
+
* adapter needs. Telegram's Bot API is plain HTTPS/JSON — no official SDK
|
|
4
|
+
* is required, and this codebase's established precedent (`@cogenta/import`'s
|
|
5
|
+
* hand-rolled WXR parser, `@cogenta/mcp`'s hand-rolled JSON-RPC subset)
|
|
6
|
+
* favours a small, zero-dependency client over a general-purpose SDK for an
|
|
7
|
+
* API this simple, rather than adding a new direct dependency for four HTTP
|
|
8
|
+
* calls.
|
|
9
|
+
*/
|
|
10
|
+
export interface TelegramInlineButton {
|
|
11
|
+
readonly text: string;
|
|
12
|
+
readonly callback_data: string;
|
|
13
|
+
}
|
|
14
|
+
export interface TelegramReplyMarkup {
|
|
15
|
+
readonly inline_keyboard: readonly (readonly TelegramInlineButton[])[];
|
|
16
|
+
}
|
|
17
|
+
export interface TelegramSendMessageParams {
|
|
18
|
+
readonly chat_id: string;
|
|
19
|
+
readonly text: string;
|
|
20
|
+
readonly parse_mode?: 'MarkdownV2';
|
|
21
|
+
readonly reply_markup?: TelegramReplyMarkup;
|
|
22
|
+
}
|
|
23
|
+
export interface TelegramEditMessageParams {
|
|
24
|
+
readonly chat_id: string;
|
|
25
|
+
readonly message_id: number;
|
|
26
|
+
readonly text: string;
|
|
27
|
+
readonly parse_mode?: 'MarkdownV2';
|
|
28
|
+
readonly reply_markup?: TelegramReplyMarkup;
|
|
29
|
+
}
|
|
30
|
+
export interface TelegramMessage {
|
|
31
|
+
readonly message_id: number;
|
|
32
|
+
readonly chat: {
|
|
33
|
+
readonly id: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export interface TelegramUser {
|
|
37
|
+
readonly id: number;
|
|
38
|
+
}
|
|
39
|
+
export interface TelegramIncomingMessage {
|
|
40
|
+
readonly message_id: number;
|
|
41
|
+
readonly chat: {
|
|
42
|
+
readonly id: number;
|
|
43
|
+
};
|
|
44
|
+
readonly from?: TelegramUser;
|
|
45
|
+
readonly text?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface TelegramCallbackQuery {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
readonly from: TelegramUser;
|
|
50
|
+
readonly message?: TelegramIncomingMessage;
|
|
51
|
+
readonly data?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface TelegramUpdate {
|
|
54
|
+
readonly update_id: number;
|
|
55
|
+
readonly message?: TelegramIncomingMessage;
|
|
56
|
+
readonly callback_query?: TelegramCallbackQuery;
|
|
57
|
+
}
|
|
58
|
+
export interface TelegramClientConfig {
|
|
59
|
+
readonly token: string;
|
|
60
|
+
/** Overridable for tests; defaults to the real Telegram API host. */
|
|
61
|
+
readonly baseUrl?: string;
|
|
62
|
+
readonly fetchImpl?: typeof fetch;
|
|
63
|
+
/** Overridable for tests, so a 429 retry doesn't really wait. */
|
|
64
|
+
readonly sleepImpl?: (ms: number) => Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
export interface TelegramClient {
|
|
67
|
+
sendMessage(params: TelegramSendMessageParams): Promise<TelegramMessage>;
|
|
68
|
+
editMessageText(params: TelegramEditMessageParams): Promise<void>;
|
|
69
|
+
answerCallbackQuery(callbackQueryId: string, text?: string): Promise<void>;
|
|
70
|
+
getUpdates(offset: number, timeoutSeconds: number): Promise<readonly TelegramUpdate[]>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `createTelegramClient` retries a rate-limited call using Telegram's own
|
|
74
|
+
* `retry_after` value (seconds) rather than a guessed backoff — "Prévoir la
|
|
75
|
+
* file, le backoff et le regroupement dès le premier adaptateur" (lot doc).
|
|
76
|
+
*/
|
|
77
|
+
export declare function createTelegramClient(config: TelegramClientConfig): TelegramClient;
|
|
78
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/providers/telegram/client.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC,SAAS,oBAAoB,EAAE,CAAC,EAAE,CAAA;CACvE;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,UAAU,CAAC,EAAE,YAAY,CAAA;IAClC,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAA;CAC5C;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,UAAU,CAAC,EAAE,YAAY,CAAA;IAClC,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAA;CAC5C;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;CACvC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;IACtC,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,CAAA;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,uBAAuB,CAAA;IAC1C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,uBAAuB,CAAA;IAC1C,QAAQ,CAAC,cAAc,CAAC,EAAE,qBAAqB,CAAA;CAChD;AAgBD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACjC,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACnD;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IACxE,eAAe,CAAC,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjE,mBAAmB,CAAC,eAAe,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1E,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,cAAc,EAAE,CAAC,CAAA;CACvF;AAQD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,cAAc,CAkDjF"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { CogentaError } from '@cogenta/core';
|
|
2
|
+
const DEFAULT_BASE_URL = 'https://api.telegram.org';
|
|
3
|
+
function defaultSleep(ms) {
|
|
4
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* `createTelegramClient` retries a rate-limited call using Telegram's own
|
|
8
|
+
* `retry_after` value (seconds) rather than a guessed backoff — "Prévoir la
|
|
9
|
+
* file, le backoff et le regroupement dès le premier adaptateur" (lot doc).
|
|
10
|
+
*/
|
|
11
|
+
export function createTelegramClient(config) {
|
|
12
|
+
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
13
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
14
|
+
const sleepImpl = config.sleepImpl ?? defaultSleep;
|
|
15
|
+
async function call(method, body) {
|
|
16
|
+
for (;;) {
|
|
17
|
+
const response = await fetchImpl(`${baseUrl}/bot${config.token}/${method}`, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'content-type': 'application/json' },
|
|
20
|
+
body: JSON.stringify(body),
|
|
21
|
+
});
|
|
22
|
+
const json = (await response.json());
|
|
23
|
+
if (response.status === 429 && !json.ok) {
|
|
24
|
+
const retryAfterSeconds = json.parameters?.retry_after ?? 1;
|
|
25
|
+
await sleepImpl(retryAfterSeconds * 1000);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!json.ok) {
|
|
29
|
+
throw new CogentaError({
|
|
30
|
+
code: 'CHANNEL_TELEGRAM_API_ERROR',
|
|
31
|
+
message: `Telegram API call "${method}" failed: ${json.description}`,
|
|
32
|
+
hint: 'Check the bot token, and that the chat or message this call references still exists.',
|
|
33
|
+
details: { method, errorCode: json.error_code },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return json.result;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
sendMessage: (params) => call('sendMessage', params),
|
|
41
|
+
async editMessageText(params) {
|
|
42
|
+
await call('editMessageText', params);
|
|
43
|
+
},
|
|
44
|
+
async answerCallbackQuery(callbackQueryId, text) {
|
|
45
|
+
await call('answerCallbackQuery', {
|
|
46
|
+
callback_query_id: callbackQueryId,
|
|
47
|
+
...(text === undefined ? {} : { text }),
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
getUpdates: (offset, timeoutSeconds) => call('getUpdates', { offset, timeout: timeoutSeconds }),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/providers/telegram/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AA+F5C,MAAM,gBAAgB,GAAG,0BAA0B,CAAA;AAEnD,SAAS,YAAY,CAAC,EAAU;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;AAC1D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAA4B;IAC/D,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,gBAAgB,CAAA;IAClD,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,IAAI,KAAK,CAAA;IAC3C,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,IAAI,YAAY,CAAA;IAElD,KAAK,UAAU,IAAI,CAAI,MAAc,EAAE,IAAa;QAClD,SAAS,CAAC;YACR,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,OAAO,OAAO,MAAM,CAAC,KAAK,IAAI,MAAM,EAAE,EAAE;gBAC1E,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;aAC3B,CAAC,CAAA;YACF,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA2B,CAAA;YAE9D,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;gBACxC,MAAM,iBAAiB,GAAG,IAAI,CAAC,UAAU,EAAE,WAAW,IAAI,CAAC,CAAA;gBAC3D,MAAM,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAA;gBACzC,SAAQ;YACV,CAAC;YAED,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;gBACb,MAAM,IAAI,YAAY,CAAC;oBACrB,IAAI,EAAE,4BAA4B;oBAClC,OAAO,EAAE,sBAAsB,MAAM,aAAa,IAAI,CAAC,WAAW,EAAE;oBACpE,IAAI,EAAE,sFAAsF;oBAC5F,OAAO,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE;iBAChD,CAAC,CAAA;YACJ,CAAC;YAED,OAAO,IAAI,CAAC,MAAM,CAAA;QACpB,CAAC;IACH,CAAC;IAED,OAAO;QACL,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAkB,aAAa,EAAE,MAAM,CAAC;QAErE,KAAK,CAAC,eAAe,CAAC,MAAM;YAC1B,MAAM,IAAI,CAAyB,iBAAiB,EAAE,MAAM,CAAC,CAAA;QAC/D,CAAC;QAED,KAAK,CAAC,mBAAmB,CAAC,eAAe,EAAE,IAAI;YAC7C,MAAM,IAAI,CAAO,qBAAqB,EAAE;gBACtC,iBAAiB,EAAE,eAAe;gBAClC,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;aACxC,CAAC,CAAA;QACJ,CAAC;QAED,UAAU,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,EAAE,CACrC,IAAI,CAA4B,YAAY,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC;KACrF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { InboundHandler } from '../../adapter.js';
|
|
2
|
+
import type { CommandRouter } from '../../inbound/router.js';
|
|
3
|
+
import type { ChannelLinkStore } from '../../linking/store.js';
|
|
4
|
+
import type { TelegramClient, TelegramUpdate } from './client.js';
|
|
5
|
+
export interface TelegramInboundDeps {
|
|
6
|
+
readonly client: TelegramClient;
|
|
7
|
+
readonly linkStore: ChannelLinkStore;
|
|
8
|
+
readonly router: CommandRouter;
|
|
9
|
+
/** The handler registered via the adapter's `onInbound()`, if any. */
|
|
10
|
+
getExternalHandler(): InboundHandler | undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The real routing/authorization wiring for one inbound Telegram update —
|
|
14
|
+
* extracted from the adapter so it is directly testable without spinning
|
|
15
|
+
* the real long-polling loop. Handles both a typed text command and a
|
|
16
|
+
* button press (`callback_query`, whose `data` is itself a command string
|
|
17
|
+
* rendered by `render.ts`) through the exact same `CommandRouter.route()`,
|
|
18
|
+
* never a second, parallel authorization path.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createTelegramInboundHandler(deps: TelegramInboundDeps): (update: TelegramUpdate) => Promise<void>;
|
|
21
|
+
//# sourceMappingURL=inbound.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../../../src/providers/telegram/inbound.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AASjE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAA;IACpC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;IAC9B,sEAAsE;IACtE,kBAAkB,IAAI,cAAc,GAAG,SAAS,CAAA;CACjD;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,mBAAmB,GACxB,CAAC,MAAM,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAgE3C"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const CHANNEL_NAME = 'telegram';
|
|
2
|
+
const MARKDOWN_V2_SPECIAL = /[_*[\]()~`>#+\-=|{}.!\\]/g;
|
|
3
|
+
function escapePlainReply(text) {
|
|
4
|
+
return text.replace(MARKDOWN_V2_SPECIAL, (char) => `\\${char}`);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The real routing/authorization wiring for one inbound Telegram update —
|
|
8
|
+
* extracted from the adapter so it is directly testable without spinning
|
|
9
|
+
* the real long-polling loop. Handles both a typed text command and a
|
|
10
|
+
* button press (`callback_query`, whose `data` is itself a command string
|
|
11
|
+
* rendered by `render.ts`) through the exact same `CommandRouter.route()`,
|
|
12
|
+
* never a second, parallel authorization path.
|
|
13
|
+
*/
|
|
14
|
+
export function createTelegramInboundHandler(deps) {
|
|
15
|
+
async function resolveIdentity(channelUserId) {
|
|
16
|
+
return deps.linkStore.resolveIdentity(CHANNEL_NAME, channelUserId);
|
|
17
|
+
}
|
|
18
|
+
async function reply(chatId, text) {
|
|
19
|
+
await deps.client.sendMessage({ chat_id: chatId, text: escapePlainReply(text) });
|
|
20
|
+
}
|
|
21
|
+
async function handleText(chatId, channelUserId, text) {
|
|
22
|
+
let identity = await resolveIdentity(channelUserId);
|
|
23
|
+
if (identity.linkedUserId === null) {
|
|
24
|
+
// An unlinked identity gets exactly one thing to try: the message AS
|
|
25
|
+
// a linking code. Any other failure stays silent — "Une identité de
|
|
26
|
+
// canal non liée à un compte est ignorée, sans réponse."
|
|
27
|
+
try {
|
|
28
|
+
await deps.linkStore.verifyCode(text.trim(), CHANNEL_NAME, channelUserId);
|
|
29
|
+
identity = await resolveIdentity(channelUserId);
|
|
30
|
+
await reply(chatId, '✅ Compte lié. Vous pouvez maintenant utiliser les commandes.');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Wrong/expired/nonexistent code from an unlinked identity: silence.
|
|
34
|
+
}
|
|
35
|
+
const handler = deps.getExternalHandler();
|
|
36
|
+
if (handler !== undefined)
|
|
37
|
+
await handler({ text, identity });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const handler = deps.getExternalHandler();
|
|
41
|
+
if (handler !== undefined)
|
|
42
|
+
await handler({ text, identity });
|
|
43
|
+
const result = await deps.router.route(text, identity);
|
|
44
|
+
if (!result.shouldReply)
|
|
45
|
+
return;
|
|
46
|
+
if (result.kind === 'forbidden') {
|
|
47
|
+
await reply(chatId, "🚫 Vous n'avez pas la permission d'exécuter cette commande.");
|
|
48
|
+
}
|
|
49
|
+
else if (result.kind === 'unrecognized') {
|
|
50
|
+
await reply(chatId, `Commande inconnue : ${result.commandName}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return async function handleUpdate(update) {
|
|
54
|
+
if (update.message?.text !== undefined && update.message.from !== undefined) {
|
|
55
|
+
await handleText(String(update.message.chat.id), String(update.message.from.id), update.message.text);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (update.callback_query !== undefined) {
|
|
59
|
+
const query = update.callback_query;
|
|
60
|
+
const chatId = query.message === undefined ? undefined : String(query.message.chat.id);
|
|
61
|
+
const channelUserId = String(query.from.id);
|
|
62
|
+
const data = query.data ?? '';
|
|
63
|
+
if (chatId !== undefined && data.length > 0) {
|
|
64
|
+
await handleText(chatId, channelUserId, data);
|
|
65
|
+
}
|
|
66
|
+
// Dismiss the button's loading state regardless of outcome — a UI
|
|
67
|
+
// affordance, not a disclosure.
|
|
68
|
+
await deps.client.answerCallbackQuery(query.id);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=inbound.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbound.js","sourceRoot":"","sources":["../../../src/providers/telegram/inbound.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ChannelMessage } from '../../adapter.js';
|
|
2
|
+
import type { TelegramReplyMarkup } from './client.js';
|
|
3
|
+
export declare function escapeMarkdownV2(text: string): string;
|
|
4
|
+
export interface RenderedTelegramMessage {
|
|
5
|
+
readonly text: string;
|
|
6
|
+
readonly replyMarkup?: TelegramReplyMarkup;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Renders one of the lot's three fixed message levels into Telegram
|
|
10
|
+
* MarkdownV2 text plus (for an alert with actions) inline buttons. Every
|
|
11
|
+
* piece of user-authored text is escaped; only the structural markup this
|
|
12
|
+
* function itself adds (bold, bullets) is left unescaped.
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderTelegramMessage(message: ChannelMessage): RenderedTelegramMessage;
|
|
15
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../src/providers/telegram/render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,cAAc,EAAmB,MAAM,kBAAkB,CAAA;AACtF,OAAO,KAAK,EAAwB,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAY5E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAkCD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,mBAAmB,CAAA;CAC3C;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,cAAc,GAAG,uBAAuB,CA+CtF"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "On n'écrit pas de Markdown Telegram dans le code métier." All
|
|
3
|
+
* Telegram-specific rendering lives here, entirely inside this adapter —
|
|
4
|
+
* `ChannelMessage` itself never knows Telegram's escaping rules exist.
|
|
5
|
+
*/
|
|
6
|
+
// Telegram's MarkdownV2 requires escaping these characters outside of an
|
|
7
|
+
// entity (https://core.telegram.org/bots/api#markdownv2-style).
|
|
8
|
+
const MARKDOWN_V2_SPECIAL = /[_*[\]()~`>#+\-=|{}.!\\]/g;
|
|
9
|
+
export function escapeMarkdownV2(text) {
|
|
10
|
+
return text.replace(MARKDOWN_V2_SPECIAL, (char) => `\\${char}`);
|
|
11
|
+
}
|
|
12
|
+
const SEVERITY_PREFIX = {
|
|
13
|
+
info: 'ℹ️',
|
|
14
|
+
warning: '⚠️',
|
|
15
|
+
critical: '🔴',
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Telegram's real, hard API limit on `sendMessage`'s `text`
|
|
19
|
+
* (https://core.telegram.org/bots/api#sendmessage) — not the abstract
|
|
20
|
+
* screen-budget `buildReport` (`../../formats/report.js`) already enforces
|
|
21
|
+
* at construction time. That budget is deliberately smaller and about
|
|
22
|
+
* readability; this one is a last-resort safety net so a `ReportChannelMessage`
|
|
23
|
+
* assembled without going through `buildReport` can never produce a request
|
|
24
|
+
* Telegram would reject outright.
|
|
25
|
+
*/
|
|
26
|
+
const TELEGRAM_MAX_TEXT_CHARS = 4096;
|
|
27
|
+
const TRUNCATION_MARK = '…';
|
|
28
|
+
function renderActions(actions) {
|
|
29
|
+
if (actions === undefined || actions.length === 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
const buttons = actions.map((action) => ({
|
|
32
|
+
text: action.label,
|
|
33
|
+
// The callback_data IS the command text a button press routes as —
|
|
34
|
+
// reusing the exact same `CommandRouter.route()` a typed command goes
|
|
35
|
+
// through, never a second, parallel authorization path.
|
|
36
|
+
callback_data: action.id,
|
|
37
|
+
}));
|
|
38
|
+
return { inline_keyboard: [buttons] };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Renders one of the lot's three fixed message levels into Telegram
|
|
42
|
+
* MarkdownV2 text plus (for an alert with actions) inline buttons. Every
|
|
43
|
+
* piece of user-authored text is escaped; only the structural markup this
|
|
44
|
+
* function itself adds (bold, bullets) is left unescaped.
|
|
45
|
+
*/
|
|
46
|
+
export function renderTelegramMessage(message) {
|
|
47
|
+
if (message.level === 'notification') {
|
|
48
|
+
// "Information sans action. Une ligne."
|
|
49
|
+
return { text: escapeMarkdownV2(message.text) };
|
|
50
|
+
}
|
|
51
|
+
if (message.level === 'alert') {
|
|
52
|
+
const prefix = SEVERITY_PREFIX[message.severity];
|
|
53
|
+
const lines = [
|
|
54
|
+
`${prefix} *${escapeMarkdownV2(message.title)}*`,
|
|
55
|
+
escapeMarkdownV2(message.context),
|
|
56
|
+
`➡️ ${escapeMarkdownV2(message.expectedAction)}`,
|
|
57
|
+
`[${escapeMarkdownV2('Voir dans l’admin')}](${message.adminUrl})`,
|
|
58
|
+
];
|
|
59
|
+
const replyMarkup = renderActions(message.actions);
|
|
60
|
+
return { text: lines.join('\n\n'), ...(replyMarkup === undefined ? {} : { replyMarkup }) };
|
|
61
|
+
}
|
|
62
|
+
// 'report' — "Chiffres clés en tête, détail ensuite, jamais plus d'un
|
|
63
|
+
// écran sans repli."
|
|
64
|
+
const lines = [`*${escapeMarkdownV2(message.title)}*`];
|
|
65
|
+
if (message.keyFigures.length > 0) {
|
|
66
|
+
lines.push(message.keyFigures
|
|
67
|
+
.map((figure) => `*${escapeMarkdownV2(figure.value)}* ${escapeMarkdownV2(figure.label)}`)
|
|
68
|
+
.join(' · '));
|
|
69
|
+
}
|
|
70
|
+
for (const section of message.sections) {
|
|
71
|
+
const heading = section.heading === undefined ? '' : `*${escapeMarkdownV2(section.heading)}*\n`;
|
|
72
|
+
lines.push(`${heading}${escapeMarkdownV2(section.body)}`);
|
|
73
|
+
}
|
|
74
|
+
const footer = message.moreUrl === undefined
|
|
75
|
+
? ''
|
|
76
|
+
: `\n\n[${escapeMarkdownV2('Voir le détail')}](${message.moreUrl})`;
|
|
77
|
+
const body = lines.join('\n\n');
|
|
78
|
+
// Telegram's real limit applies to `body + footer` together — the footer
|
|
79
|
+
// (the reader's only way to the full detail) must never itself be the
|
|
80
|
+
// part that gets cut off, so the body is what shrinks.
|
|
81
|
+
const budget = TELEGRAM_MAX_TEXT_CHARS - footer.length;
|
|
82
|
+
const truncated = body.length > budget
|
|
83
|
+
? `${body.slice(0, Math.max(0, budget - TRUNCATION_MARK.length))}${TRUNCATION_MARK}`
|
|
84
|
+
: body;
|
|
85
|
+
return { text: `${truncated}${footer}` };
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../../src/providers/telegram/render.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ChannelAdapter } from '../../adapter.js';
|
|
2
|
+
import { type WebhookActionLinkOptions } from './render.js';
|
|
3
|
+
export type WebhookFetch = (url: string, init: {
|
|
4
|
+
method: 'POST';
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
body: string;
|
|
7
|
+
}) => Promise<{
|
|
8
|
+
readonly ok: boolean;
|
|
9
|
+
readonly status: number;
|
|
10
|
+
}>;
|
|
11
|
+
export interface WebhookAdapterOptions {
|
|
12
|
+
/** Shared secret signing every outbound request — the receiver's `verifyIncomingWebhook` must use the same value. */
|
|
13
|
+
readonly secret: string;
|
|
14
|
+
/** Required only if messages with actions (e.g. approvals) are ever sent — see `render.ts`. */
|
|
15
|
+
readonly actionLinks?: WebhookActionLinkOptions;
|
|
16
|
+
/** Defaults to the global `fetch`; overridable for tests. */
|
|
17
|
+
readonly fetchImpl?: WebhookFetch;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* "Webhook générique signé" (L6 task 11) — the general-purpose "any HTTP
|
|
21
|
+
* endpoint" channel. `target.id` is the receiving URL itself (this is what
|
|
22
|
+
* `ChannelTarget`'s "platform-side identifier" means for a channel whose
|
|
23
|
+
* platform IS arbitrary HTTP): one adapter instance, one shared signing
|
|
24
|
+
* secret, any number of destination URLs.
|
|
25
|
+
*
|
|
26
|
+
* Outbound-only for real command execution: `capabilities.buttons` is
|
|
27
|
+
* `false` (no UI to click, hence the signed-link rendering, same pattern as
|
|
28
|
+
* the email adapter, L6 task 8) and `capabilities.inbound` is `false` —
|
|
29
|
+
* building real inbound COMMAND execution for an arbitrary third-party
|
|
30
|
+
* caller is a materially different, larger undertaking (it would need a
|
|
31
|
+
* real HTTP route wired to a live site, per-integration identity linking
|
|
32
|
+
* decisions, and its own authorization story) than this task's actual,
|
|
33
|
+
* security-critical deliverable: the signing/verification PRIMITIVE
|
|
34
|
+
* (`signing.ts`), which is real, complete, and exercised end-to-end here by
|
|
35
|
+
* `send()`. `verifyIdentity` is an honest refusal, exactly matching the
|
|
36
|
+
* email adapter's precedent, rather than a half-built linking flow with no
|
|
37
|
+
* real inbound transport to carry it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createWebhookAdapter(options: WebhookAdapterOptions): ChannelAdapter;
|
|
40
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/providers/webhook/adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EAKf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAwB,KAAK,wBAAwB,EAAE,MAAM,aAAa,CAAA;AASjF,MAAM,MAAM,YAAY,GAAG,CACzB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KACpE,OAAO,CAAC;IACX,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB,CAAC,CAAA;AAEF,MAAM,WAAW,qBAAqB;IACpC,qHAAqH;IACrH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,+FAA+F;IAC/F,QAAQ,CAAC,WAAW,CAAC,EAAE,wBAAwB,CAAA;IAC/C,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,CAAC,EAAE,YAAY,CAAA;CAClC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CAoDnF"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { CogentaError } from '@cogenta/core';
|
|
2
|
+
import { renderWebhookPayload } from './render.js';
|
|
3
|
+
import { signOutgoingWebhook, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, } from './signing.js';
|
|
4
|
+
const CHANNEL_NAME = 'webhook';
|
|
5
|
+
/**
|
|
6
|
+
* "Webhook générique signé" (L6 task 11) — the general-purpose "any HTTP
|
|
7
|
+
* endpoint" channel. `target.id` is the receiving URL itself (this is what
|
|
8
|
+
* `ChannelTarget`'s "platform-side identifier" means for a channel whose
|
|
9
|
+
* platform IS arbitrary HTTP): one adapter instance, one shared signing
|
|
10
|
+
* secret, any number of destination URLs.
|
|
11
|
+
*
|
|
12
|
+
* Outbound-only for real command execution: `capabilities.buttons` is
|
|
13
|
+
* `false` (no UI to click, hence the signed-link rendering, same pattern as
|
|
14
|
+
* the email adapter, L6 task 8) and `capabilities.inbound` is `false` —
|
|
15
|
+
* building real inbound COMMAND execution for an arbitrary third-party
|
|
16
|
+
* caller is a materially different, larger undertaking (it would need a
|
|
17
|
+
* real HTTP route wired to a live site, per-integration identity linking
|
|
18
|
+
* decisions, and its own authorization story) than this task's actual,
|
|
19
|
+
* security-critical deliverable: the signing/verification PRIMITIVE
|
|
20
|
+
* (`signing.ts`), which is real, complete, and exercised end-to-end here by
|
|
21
|
+
* `send()`. `verifyIdentity` is an honest refusal, exactly matching the
|
|
22
|
+
* email adapter's precedent, rather than a half-built linking flow with no
|
|
23
|
+
* real inbound transport to carry it.
|
|
24
|
+
*/
|
|
25
|
+
export function createWebhookAdapter(options) {
|
|
26
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
27
|
+
return {
|
|
28
|
+
name: CHANNEL_NAME,
|
|
29
|
+
capabilities: {
|
|
30
|
+
richText: false,
|
|
31
|
+
buttons: false,
|
|
32
|
+
threads: false,
|
|
33
|
+
attachments: false,
|
|
34
|
+
inbound: false,
|
|
35
|
+
},
|
|
36
|
+
async send(target, message) {
|
|
37
|
+
const payload = renderWebhookPayload(message, options.actionLinks);
|
|
38
|
+
const rawBody = JSON.stringify(payload);
|
|
39
|
+
const signed = signOutgoingWebhook(options.secret, rawBody);
|
|
40
|
+
const response = await fetchImpl(target.id, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
'content-type': 'application/json',
|
|
44
|
+
[WEBHOOK_TIMESTAMP_HEADER]: signed.timestamp,
|
|
45
|
+
[WEBHOOK_SIGNATURE_HEADER]: signed.signature,
|
|
46
|
+
},
|
|
47
|
+
body: rawBody,
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new CogentaError({
|
|
51
|
+
code: 'CHANNEL_WEBHOOK_DELIVERY_FAILED',
|
|
52
|
+
message: `Webhook delivery to ${target.id} failed with status ${response.status}.`,
|
|
53
|
+
hint: 'Check that the target URL is reachable and returns a 2xx status for a valid signed request.',
|
|
54
|
+
details: { url: target.id, status: response.status },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// A generic webhook receiver has no message-id concept of its own to
|
|
58
|
+
// hand back — the timestamp+signature pair is the only real,
|
|
59
|
+
// independently-verifiable identifier this send actually produced.
|
|
60
|
+
return `${signed.timestamp}:${signed.signature}`;
|
|
61
|
+
},
|
|
62
|
+
async verifyIdentity() {
|
|
63
|
+
throw new CogentaError({
|
|
64
|
+
code: 'CHANNEL_WEBHOOK_INBOUND_UNSUPPORTED',
|
|
65
|
+
message: 'The generic webhook adapter does not support identity linking — it is outbound-only.',
|
|
66
|
+
hint: 'Link accounts through a channel with capabilities.inbound === true (e.g. Telegram, Slack, Discord).',
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../../../src/providers/webhook/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAQ5C,OAAO,EAAE,oBAAoB,EAAiC,MAAM,aAAa,CAAA;AACjF,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,cAAc,CAAA;AAErB,MAAM,YAAY,GAAG,SAAS,CAAA;AAmB9B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAA8B;IACjE,MAAM,SAAS,GAAiB,OAAO,CAAC,SAAS,IAAK,KAAiC,CAAA;IAEvF,OAAO;QACL,IAAI,EAAE,YAAY;QAClB,YAAY,EAAE;YACZ,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,KAAK;YACd,WAAW,EAAE,KAAK;YAClB,OAAO,EAAE,KAAK;SACf;QAED,KAAK,CAAC,IAAI,CAAC,MAAqB,EAAE,OAAuB;YACvD,MAAM,OAAO,GAAG,oBAAoB,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;YAClE,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;YACvC,MAAM,MAAM,GAAG,mBAAmB,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;YAE3D,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE;gBAC1C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,CAAC,wBAAwB,CAAC,EAAE,MAAM,CAAC,SAAS;oBAC5C,CAAC,wBAAwB,CAAC,EAAE,MAAM,CAAC,SAAS;iBAC7C;gBACD,IAAI,EAAE,OAAO;aACd,CAAC,CAAA;YAEF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,YAAY,CAAC;oBACrB,IAAI,EAAE,iCAAiC;oBACvC,OAAO,EAAE,uBAAuB,MAAM,CAAC,EAAE,uBAAuB,QAAQ,CAAC,MAAM,GAAG;oBAClF,IAAI,EAAE,6FAA6F;oBACnG,OAAO,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE;iBACrD,CAAC,CAAA;YACJ,CAAC;YAED,qEAAqE;YACrE,6DAA6D;YAC7D,mEAAmE;YACnE,OAAO,GAAG,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,EAAE,CAAA;QAClD,CAAC;QAED,KAAK,CAAC,cAAc;YAClB,MAAM,IAAI,YAAY,CAAC;gBACrB,IAAI,EAAE,qCAAqC;gBAC3C,OAAO,EACL,sFAAsF;gBACxF,IAAI,EAAE,qGAAqG;aAC5G,CAAC,CAAA;QACJ,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ChannelMessage } from '../../adapter.js';
|
|
2
|
+
import { type EmailActionLinkOptions } from '../email/render.js';
|
|
3
|
+
export interface WebhookActionLinkOptions extends EmailActionLinkOptions {
|
|
4
|
+
}
|
|
5
|
+
export interface WebhookPayload {
|
|
6
|
+
readonly level: ChannelMessage['level'];
|
|
7
|
+
readonly title: string;
|
|
8
|
+
readonly text: string;
|
|
9
|
+
readonly actions?: readonly {
|
|
10
|
+
readonly label: string;
|
|
11
|
+
readonly url: string;
|
|
12
|
+
}[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A generic webhook receiver has no rendering surface of its own (no
|
|
16
|
+
* Markdown dialect, no Block Kit) — the payload is plain, structured JSON.
|
|
17
|
+
* Actions still can't be buttons ("## Approbations depuis le canal": "Sur un
|
|
18
|
+
* canal sans boutons [...] l'action est un lien signé à usage unique"), so
|
|
19
|
+
* this reuses `email/render.ts`'s real validation (it already throws
|
|
20
|
+
* `CHANNEL_EMAIL_TRANSPORT_ERROR` for an unrenderable action or a missing
|
|
21
|
+
* `actionLinks` config — that check is not duplicated here) for its plain
|
|
22
|
+
* text and only rebuilds the action *links* locally, since the payload shape
|
|
23
|
+
* (label/url pairs) differs from email's HTML anchor rendering.
|
|
24
|
+
*/
|
|
25
|
+
export declare function renderWebhookPayload(message: ChannelMessage, actionLinks?: WebhookActionLinkOptions): WebhookPayload;
|
|
26
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../src/providers/webhook/render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAErE,OAAO,EAAE,KAAK,sBAAsB,EAAsB,MAAM,oBAAoB,CAAA;AAEpF,MAAM,WAAW,wBAAyB,SAAQ,sBAAsB;CAAG;AAE3E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,CAAA;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC/E;AAqBD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,cAAc,EACvB,WAAW,CAAC,EAAE,wBAAwB,GACrC,cAAc,CAgBhB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { buildSignedApprovalLink } from '../../approvals/signed-link.js';
|
|
2
|
+
import { renderEmailMessage } from '../email/render.js';
|
|
3
|
+
/**
|
|
4
|
+
* `action.id` is command text (`"approve <token>"`/`"deny <token>"`, the
|
|
5
|
+
* same shape `email/render.ts`'s `actionToLink` already parses) — mirrored
|
|
6
|
+
* here rather than imported, since that helper is private to its own
|
|
7
|
+
* module; both wrap the one real primitive, `buildSignedApprovalLink`.
|
|
8
|
+
*/
|
|
9
|
+
function actionToUrl(action, links) {
|
|
10
|
+
const match = /^(approve|deny)\s+(.+)$/.exec(action.id);
|
|
11
|
+
const [, word = 'approve', token = action.id] = match ?? [];
|
|
12
|
+
const decision = word === 'approve' ? 'approved' : 'rejected';
|
|
13
|
+
return buildSignedApprovalLink(links.baseUrl, links.signingKey, token, decision, links.expiresInSeconds);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A generic webhook receiver has no rendering surface of its own (no
|
|
17
|
+
* Markdown dialect, no Block Kit) — the payload is plain, structured JSON.
|
|
18
|
+
* Actions still can't be buttons ("## Approbations depuis le canal": "Sur un
|
|
19
|
+
* canal sans boutons [...] l'action est un lien signé à usage unique"), so
|
|
20
|
+
* this reuses `email/render.ts`'s real validation (it already throws
|
|
21
|
+
* `CHANNEL_EMAIL_TRANSPORT_ERROR` for an unrenderable action or a missing
|
|
22
|
+
* `actionLinks` config — that check is not duplicated here) for its plain
|
|
23
|
+
* text and only rebuilds the action *links* locally, since the payload shape
|
|
24
|
+
* (label/url pairs) differs from email's HTML anchor rendering.
|
|
25
|
+
*/
|
|
26
|
+
export function renderWebhookPayload(message, actionLinks) {
|
|
27
|
+
const rendered = renderEmailMessage(message, actionLinks);
|
|
28
|
+
const actions = message.level === 'alert' && message.actions !== undefined && message.actions.length > 0
|
|
29
|
+
? message.actions.map((action) => ({
|
|
30
|
+
label: action.label,
|
|
31
|
+
url: actionToUrl(action, actionLinks),
|
|
32
|
+
}))
|
|
33
|
+
: undefined;
|
|
34
|
+
return {
|
|
35
|
+
level: message.level,
|
|
36
|
+
title: rendered.subject,
|
|
37
|
+
text: rendered.text,
|
|
38
|
+
...(actions === undefined ? {} : { actions }),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../../src/providers/webhook/render.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAA;AACxE,OAAO,EAA+B,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAWpF;;;;;GAKG;AACH,SAAS,WAAW,CAAC,MAAqB,EAAE,KAA+B;IACzE,MAAM,KAAK,GAAG,yBAAyB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IACvD,MAAM,CAAC,EAAE,IAAI,GAAG,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,GAAG,KAAK,IAAI,EAAE,CAAA;IAC3D,MAAM,QAAQ,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAA;IAC7D,OAAO,uBAAuB,CAC5B,KAAK,CAAC,OAAO,EACb,KAAK,CAAC,UAAU,EAChB,KAAK,EACL,QAAQ,EACR,KAAK,CAAC,gBAAgB,CACvB,CAAA;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAuB,EACvB,WAAsC;IAEtC,MAAM,QAAQ,GAAG,kBAAkB,CAAC,OAAO,EAAE,WAAW,CAAC,CAAA;IACzD,MAAM,OAAO,GACX,OAAO,CAAC,KAAK,KAAK,OAAO,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;QACtF,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YAC/B,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,GAAG,EAAE,WAAW,CAAC,MAAM,EAAE,WAAuC,CAAC;SAClE,CAAC,CAAC;QACL,CAAC,CAAC,SAAS,CAAA;IAEf,OAAO;QACL,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,KAAK,EAAE,QAAQ,CAAC,OAAO;QACvB,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;KAC9C,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface SignedWebhookRequest {
|
|
2
|
+
readonly timestamp: string;
|
|
3
|
+
readonly signature: string;
|
|
4
|
+
}
|
|
5
|
+
/** The two headers an outbound signed webhook carries; a receiver names them however it configures its route. */
|
|
6
|
+
export declare const WEBHOOK_TIMESTAMP_HEADER = "X-Cogenta-Timestamp";
|
|
7
|
+
export declare const WEBHOOK_SIGNATURE_HEADER = "X-Cogenta-Signature";
|
|
8
|
+
/** The outbound half: what `createWebhookAdapter`'s `send()` attaches to every request. */
|
|
9
|
+
export declare function signOutgoingWebhook(secret: string, rawBody: string, nowSeconds?: number): SignedWebhookRequest;
|
|
10
|
+
export type WebhookVerificationResult = {
|
|
11
|
+
readonly ok: true;
|
|
12
|
+
} | {
|
|
13
|
+
readonly ok: false;
|
|
14
|
+
readonly reason: 'invalid_signature' | 'expired' | 'replayed';
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Bounded, in-memory replay guard — proportionate to the freshness window a
|
|
18
|
+
* verifier already enforces (an entry older than the window can never pass
|
|
19
|
+
* freshness anyway, so nothing needs to be remembered past it). Not a
|
|
20
|
+
* general-purpose dedup store; one instance per verifying process/secret is
|
|
21
|
+
* the expected usage, matching how a route handler would own it.
|
|
22
|
+
*/
|
|
23
|
+
export declare class WebhookReplayGuard {
|
|
24
|
+
#private;
|
|
25
|
+
/** Returns `true` if `signature` was already recorded within a still-valid window. */
|
|
26
|
+
hasSeen(signature: string, nowSeconds: number, freshnessWindowSeconds: number): boolean;
|
|
27
|
+
remember(signature: string, nowSeconds: number): void;
|
|
28
|
+
}
|
|
29
|
+
export interface VerifyIncomingWebhookInput {
|
|
30
|
+
readonly headers: SignedWebhookRequest;
|
|
31
|
+
readonly rawBody: string;
|
|
32
|
+
readonly secret: string;
|
|
33
|
+
readonly now?: number;
|
|
34
|
+
readonly freshnessWindowSeconds?: number;
|
|
35
|
+
/** Omit to skip replay checking (e.g. a caller that already dedupes upstream). */
|
|
36
|
+
readonly replayGuard?: WebhookReplayGuard;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Three independent checks, all mandatory, in the order that fails cheapest
|
|
40
|
+
* first: a malformed/non-numeric timestamp or a stale one is rejected before
|
|
41
|
+
* ever computing an HMAC over it, and only a request that is both fresh and
|
|
42
|
+
* authentic is checked for replay (recording a signature for a request that
|
|
43
|
+
* never even had a valid signature would let an attacker exhaust the replay
|
|
44
|
+
* guard with garbage).
|
|
45
|
+
*
|
|
46
|
+
* Unlike `linking/codes.ts`'s deliberately uniform `CHANNEL_LINK_CODE_INVALID`
|
|
47
|
+
* (chosen to deny an attacker an enumeration oracle over *which accounts
|
|
48
|
+
* exist*), this returns a *distinguishable* reason. There is no identity to
|
|
49
|
+
* enumerate here — a webhook secret is either configured correctly or it
|
|
50
|
+
* isn't, and telling an operator debugging a misconfigured integration
|
|
51
|
+
* "your signature doesn't match" versus "your timestamp is stale" versus
|
|
52
|
+
* "this exact request was already processed" is a legitimate operational
|
|
53
|
+
* need, not an attack surface: none of the three reasons reveal anything an
|
|
54
|
+
* attacker couldn't already infer from a generic 401.
|
|
55
|
+
*/
|
|
56
|
+
export declare function verifyIncomingWebhook(input: VerifyIncomingWebhookInput): WebhookVerificationResult;
|
|
57
|
+
/** Throws the typed `CogentaError` matching a failed `verifyIncomingWebhook` result; a no-op on success. */
|
|
58
|
+
export declare function assertValidIncomingWebhook(result: WebhookVerificationResult): void;
|
|
59
|
+
//# sourceMappingURL=signing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signing.d.ts","sourceRoot":"","sources":["../../../src/providers/webhook/signing.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,iHAAiH;AACjH,eAAO,MAAM,wBAAwB,wBAAwB,CAAA;AAC7D,eAAO,MAAM,wBAAwB,wBAAwB,CAAA;AAM7D,2FAA2F;AAC3F,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,UAAU,GAAE,MAAsC,GACjD,oBAAoB,CAGtB;AAED,MAAM,MAAM,yBAAyB,GACjC;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;CAAE,GACrB;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,mBAAmB,GAAG,SAAS,GAAG,UAAU,CAAA;CAAE,CAAA;AAEzF;;;;;;GAMG;AACH,qBAAa,kBAAkB;;IAG7B,sFAAsF;IACtF,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,GAAG,OAAO,CAGtF;IAED,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAEpD;CAOF;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAA;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAA;IACxC,kFAAkF;IAClF,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAA;CAC1C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,0BAA0B,GAChC,yBAAyB,CAwB3B;AAQD,4GAA4G;AAC5G,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,CAQlF"}
|