@driftwatch/autopilot 0.2.1
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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/http.d.ts +3 -0
- package/dist/http.js +20 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/notifiers/slack.d.ts +18 -0
- package/dist/notifiers/slack.js +55 -0
- package/dist/notifiers/telegram.d.ts +18 -0
- package/dist/notifiers/telegram.js +48 -0
- package/dist/notifiers/webhook.d.ts +13 -0
- package/dist/notifiers/webhook.js +15 -0
- package/dist/verify/slack.d.ts +15 -0
- package/dist/verify/slack.js +52 -0
- package/dist/verify/telegram.d.ts +20 -0
- package/dist/verify/telegram.js +55 -0
- package/dist/verify/util.d.ts +3 -0
- package/dist/verify/util.js +10 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Victory Lucky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @driftwatch/autopilot
|
|
2
|
+
|
|
3
|
+
Slack, Telegram, and generic-webhook notifiers, plus inbound-webhook signature
|
|
4
|
+
verification, for [DriftWatch](https://github.com/codewithveek/drift-watch)
|
|
5
|
+
Autopilot. A companion to `@driftwatch/sdk` — bring this in only if you use
|
|
6
|
+
those channels; the SDK's approval/scheduler orchestration works against any
|
|
7
|
+
`Notifier` implementation, including your own.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @driftwatch/sdk @driftwatch/autopilot
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Notifiers
|
|
16
|
+
|
|
17
|
+
Each implements the SDK's `Notifier` interface (`notify(message)`), so they
|
|
18
|
+
plug straight into `NotifierRegistry` / `ApprovalService` / `AutopilotScheduler`
|
|
19
|
+
from `@driftwatch/sdk`:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { SlackNotifier, TelegramNotifier, WebhookNotifier } from '@driftwatch/autopilot';
|
|
23
|
+
import type { NotifierRegistry } from '@driftwatch/sdk';
|
|
24
|
+
|
|
25
|
+
const notifiers: NotifierRegistry = {
|
|
26
|
+
slack: new SlackNotifier(process.env.SLACK_WEBHOOK_URL!),
|
|
27
|
+
telegram: new TelegramNotifier(process.env.TELEGRAM_BOT_TOKEN!, process.env.TELEGRAM_CHAT_ID!),
|
|
28
|
+
webhook: new WebhookNotifier(process.env.DRIFT_WEBHOOK_URL!),
|
|
29
|
+
list: [], // populate with whichever of the above you configured
|
|
30
|
+
};
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Verifying inbound callbacks
|
|
34
|
+
|
|
35
|
+
Approve/Reject buttons post back to your server. These helpers are
|
|
36
|
+
framework-agnostic, it reads the raw body/headers in your HTTP layer and hand them
|
|
37
|
+
over:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { verifySlackSignature, parseSlackInteraction } from '@driftwatch/autopilot';
|
|
41
|
+
|
|
42
|
+
// Fastify example
|
|
43
|
+
fastify.post('/integrations/slack/actions', async (request, reply) => {
|
|
44
|
+
const rawBody = request.body as string; // parsed as a raw string, not JSON
|
|
45
|
+
const ok = verifySlackSignature(
|
|
46
|
+
rawBody,
|
|
47
|
+
request.headers['x-slack-request-timestamp'] as string,
|
|
48
|
+
request.headers['x-slack-signature'] as string,
|
|
49
|
+
process.env.SLACK_SIGNING_SECRET!,
|
|
50
|
+
);
|
|
51
|
+
if (!ok) return reply.code(401).send();
|
|
52
|
+
|
|
53
|
+
const interaction = parseSlackInteraction(rawBody);
|
|
54
|
+
// interaction.approvalId, interaction.decision, interaction.actor
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { verifyTelegramSecretToken, parseTelegramCallback, answerTelegramCallback } from '@driftwatch/autopilot';
|
|
60
|
+
|
|
61
|
+
fastify.post('/integrations/telegram/webhook', async (request, reply) => {
|
|
62
|
+
const ok = verifyTelegramSecretToken(
|
|
63
|
+
request.headers['x-telegram-bot-api-secret-token'] as string,
|
|
64
|
+
process.env.TELEGRAM_SECRET_TOKEN!,
|
|
65
|
+
);
|
|
66
|
+
if (!ok) return reply.code(401).send();
|
|
67
|
+
|
|
68
|
+
const callback = parseTelegramCallback(request.body);
|
|
69
|
+
if (!callback) return reply.send({ ok: true }); // not a button tap
|
|
70
|
+
// callback.approvalId, callback.decision, callback.actor
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Full docs: **[drift-watch-docs.vercel.app](https://drift-watch-docs.vercel.app)**.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT — see [LICENSE](https://github.com/codewithveek/drift-watch/blob/main/LICENSE).
|
package/dist/http.d.ts
ADDED
package/dist/http.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Tiny POST-JSON helper with a hard timeout, shared by the notifiers. */
|
|
2
|
+
export async function postJson(url, body, timeoutMs = 5_000) {
|
|
3
|
+
const controller = new AbortController();
|
|
4
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
5
|
+
try {
|
|
6
|
+
const response = await fetch(url, {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: { 'content-type': 'application/json' },
|
|
9
|
+
body: JSON.stringify(body),
|
|
10
|
+
signal: controller.signal,
|
|
11
|
+
});
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
throw new Error(`HTTP ${response.status} from ${new URL(url).host}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
clearTimeout(timeout);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=http.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { postJson } from './http.js';
|
|
2
|
+
export { SlackNotifier, SLACK_APPROVE_ACTION_ID, SLACK_REJECT_ACTION_ID, } from './notifiers/slack.js';
|
|
3
|
+
export { TelegramNotifier, TELEGRAM_APPROVE_PREFIX, TELEGRAM_REJECT_PREFIX, } from './notifiers/telegram.js';
|
|
4
|
+
export { WebhookNotifier } from './notifiers/webhook.js';
|
|
5
|
+
export { verifySlackSignature, parseSlackInteraction, type ParsedSlackInteraction, } from './verify/slack.js';
|
|
6
|
+
export { verifyTelegramSecretToken, parseTelegramCallback, answerTelegramCallback, type ParsedTelegramCallback, } from './verify/telegram.js';
|
|
7
|
+
export { constantTimeEqualStrings } from './verify/util.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { postJson } from './http.js';
|
|
2
|
+
export { SlackNotifier, SLACK_APPROVE_ACTION_ID, SLACK_REJECT_ACTION_ID, } from './notifiers/slack.js';
|
|
3
|
+
export { TelegramNotifier, TELEGRAM_APPROVE_PREFIX, TELEGRAM_REJECT_PREFIX, } from './notifiers/telegram.js';
|
|
4
|
+
export { WebhookNotifier } from './notifiers/webhook.js';
|
|
5
|
+
export { verifySlackSignature, parseSlackInteraction, } from './verify/slack.js';
|
|
6
|
+
export { verifyTelegramSecretToken, parseTelegramCallback, answerTelegramCallback, } from './verify/telegram.js';
|
|
7
|
+
export { constantTimeEqualStrings } from './verify/util.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack notifier. Posts a Block Kit message to an incoming webhook. When the
|
|
3
|
+
* message carries an approvalId, it renders Approve/Reject buttons whose
|
|
4
|
+
* clicks Slack delivers to your `/integrations/slack/actions`-style route —
|
|
5
|
+
* verify them with `verifySlackSignature` and parse them with
|
|
6
|
+
* `parseSlackInteraction` from `../verify/slack.js`.
|
|
7
|
+
*/
|
|
8
|
+
import type { NotificationMessage, Notifier } from '@driftwatch/sdk';
|
|
9
|
+
export declare const SLACK_APPROVE_ACTION_ID = "dw_approve";
|
|
10
|
+
export declare const SLACK_REJECT_ACTION_ID = "dw_reject";
|
|
11
|
+
export declare class SlackNotifier implements Notifier {
|
|
12
|
+
private readonly webhookUrl;
|
|
13
|
+
readonly channel = "slack";
|
|
14
|
+
constructor(webhookUrl: string);
|
|
15
|
+
notify(message: NotificationMessage): Promise<void>;
|
|
16
|
+
private render;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=slack.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { postJson } from '../http.js';
|
|
2
|
+
export const SLACK_APPROVE_ACTION_ID = 'dw_approve';
|
|
3
|
+
export const SLACK_REJECT_ACTION_ID = 'dw_reject';
|
|
4
|
+
export class SlackNotifier {
|
|
5
|
+
webhookUrl;
|
|
6
|
+
channel = 'slack';
|
|
7
|
+
constructor(webhookUrl) {
|
|
8
|
+
this.webhookUrl = webhookUrl;
|
|
9
|
+
}
|
|
10
|
+
async notify(message) {
|
|
11
|
+
await postJson(this.webhookUrl, this.render(message));
|
|
12
|
+
}
|
|
13
|
+
render(message) {
|
|
14
|
+
const reasons = message.reasons.length
|
|
15
|
+
? message.reasons.map((r) => `• ${r}`).join('\n')
|
|
16
|
+
: '_no specific reasons reported_';
|
|
17
|
+
const blocks = [
|
|
18
|
+
{
|
|
19
|
+
type: 'header',
|
|
20
|
+
text: { type: 'plain_text', text: `🚨 ${message.title}` },
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: 'section',
|
|
24
|
+
text: {
|
|
25
|
+
type: 'mrkdwn',
|
|
26
|
+
text: `*Severity:* ${message.severity}\n*Reasons:*\n${reasons}\n*Recommended:* ${message.recommendedAction}`,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
if (message.approvalId) {
|
|
31
|
+
blocks.push({
|
|
32
|
+
type: 'actions',
|
|
33
|
+
block_id: `dw_approval:${message.approvalId}`,
|
|
34
|
+
elements: [
|
|
35
|
+
{
|
|
36
|
+
type: 'button',
|
|
37
|
+
style: 'primary',
|
|
38
|
+
text: { type: 'plain_text', text: '✅ Approve' },
|
|
39
|
+
action_id: SLACK_APPROVE_ACTION_ID,
|
|
40
|
+
value: message.approvalId,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'button',
|
|
44
|
+
style: 'danger',
|
|
45
|
+
text: { type: 'plain_text', text: '❌ Reject' },
|
|
46
|
+
action_id: SLACK_REJECT_ACTION_ID,
|
|
47
|
+
value: message.approvalId,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return { text: message.title, blocks };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=slack.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram notifier. Sends a message via the Bot API, with an inline keyboard
|
|
3
|
+
* carrying Approve/Reject buttons when an approvalId is present. Button taps
|
|
4
|
+
* arrive as `callback_query` updates at your webhook route — parse them with
|
|
5
|
+
* `parseTelegramCallback` from `../verify/telegram.js`.
|
|
6
|
+
*/
|
|
7
|
+
import type { NotificationMessage, Notifier } from '@driftwatch/sdk';
|
|
8
|
+
export declare const TELEGRAM_APPROVE_PREFIX = "dw_approve:";
|
|
9
|
+
export declare const TELEGRAM_REJECT_PREFIX = "dw_reject:";
|
|
10
|
+
export declare class TelegramNotifier implements Notifier {
|
|
11
|
+
private readonly botToken;
|
|
12
|
+
private readonly chatId;
|
|
13
|
+
readonly channel = "telegram";
|
|
14
|
+
constructor(botToken: string, chatId: string);
|
|
15
|
+
notify(message: NotificationMessage): Promise<void>;
|
|
16
|
+
private render;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=telegram.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { postJson } from '../http.js';
|
|
2
|
+
export const TELEGRAM_APPROVE_PREFIX = 'dw_approve:';
|
|
3
|
+
export const TELEGRAM_REJECT_PREFIX = 'dw_reject:';
|
|
4
|
+
export class TelegramNotifier {
|
|
5
|
+
botToken;
|
|
6
|
+
chatId;
|
|
7
|
+
channel = 'telegram';
|
|
8
|
+
constructor(botToken, chatId) {
|
|
9
|
+
this.botToken = botToken;
|
|
10
|
+
this.chatId = chatId;
|
|
11
|
+
}
|
|
12
|
+
async notify(message) {
|
|
13
|
+
const url = `https://api.telegram.org/bot${this.botToken}/sendMessage`;
|
|
14
|
+
await postJson(url, this.render(message));
|
|
15
|
+
}
|
|
16
|
+
render(message) {
|
|
17
|
+
const reasons = message.reasons.length
|
|
18
|
+
? message.reasons.map((r) => `• ${r}`).join('\n')
|
|
19
|
+
: '_no specific reasons reported_';
|
|
20
|
+
const text = `🚨 *${message.title}*\n` +
|
|
21
|
+
`*Severity:* ${message.severity}\n` +
|
|
22
|
+
`*Reasons:*\n${reasons}\n` +
|
|
23
|
+
`*Recommended:* ${message.recommendedAction}`;
|
|
24
|
+
const payload = {
|
|
25
|
+
chat_id: this.chatId,
|
|
26
|
+
text,
|
|
27
|
+
parse_mode: 'Markdown',
|
|
28
|
+
};
|
|
29
|
+
if (message.approvalId) {
|
|
30
|
+
payload.reply_markup = {
|
|
31
|
+
inline_keyboard: [
|
|
32
|
+
[
|
|
33
|
+
{
|
|
34
|
+
text: '✅ Approve',
|
|
35
|
+
callback_data: `${TELEGRAM_APPROVE_PREFIX}${message.approvalId}`,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
text: '❌ Reject',
|
|
39
|
+
callback_data: `${TELEGRAM_REJECT_PREFIX}${message.approvalId}`,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return payload;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=telegram.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic webhook notifier — posts the raw notification payload as JSON to a
|
|
3
|
+
* caller-supplied URL. This is the extensibility hook: point it at your own
|
|
4
|
+
* incident tooling, a serverless function, PagerDuty's events API, etc.
|
|
5
|
+
*/
|
|
6
|
+
import type { NotificationMessage, Notifier } from '@driftwatch/sdk';
|
|
7
|
+
export declare class WebhookNotifier implements Notifier {
|
|
8
|
+
private readonly webhookUrl;
|
|
9
|
+
readonly channel = "webhook";
|
|
10
|
+
constructor(webhookUrl: string);
|
|
11
|
+
notify(message: NotificationMessage): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=webhook.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { postJson } from '../http.js';
|
|
2
|
+
export class WebhookNotifier {
|
|
3
|
+
webhookUrl;
|
|
4
|
+
channel = 'webhook';
|
|
5
|
+
constructor(webhookUrl) {
|
|
6
|
+
this.webhookUrl = webhookUrl;
|
|
7
|
+
}
|
|
8
|
+
async notify(message) {
|
|
9
|
+
await postJson(this.webhookUrl, {
|
|
10
|
+
source: 'driftwatch-autopilot',
|
|
11
|
+
...message,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=webhook.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify Slack's v0 signature: HMAC-SHA256 of `v0:{ts}:{body}` with the signing
|
|
3
|
+
* secret, compared in constant time. Also rejects stale timestamps to defeat
|
|
4
|
+
* replay attacks. `rawBody` MUST be the exact bytes Slack sent — any
|
|
5
|
+
* re-serialization changes what the HMAC covers.
|
|
6
|
+
*/
|
|
7
|
+
export declare function verifySlackSignature(rawBody: string, timestamp: string, signature: string, signingSecret: string): boolean;
|
|
8
|
+
export interface ParsedSlackInteraction {
|
|
9
|
+
approvalId: string;
|
|
10
|
+
decision: 'approved' | 'rejected';
|
|
11
|
+
actor: string;
|
|
12
|
+
}
|
|
13
|
+
/** Parse a verified Slack interactive-message form body into a decision. */
|
|
14
|
+
export declare function parseSlackInteraction(rawBody: string): ParsedSlackInteraction | undefined;
|
|
15
|
+
//# sourceMappingURL=slack.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verification + parsing for Slack's Interactivity callbacks. Framework-
|
|
3
|
+
* agnostic — your HTTP layer is responsible for reading the raw request body
|
|
4
|
+
* and headers and handing them to these functions; see docs for a Fastify
|
|
5
|
+
* example.
|
|
6
|
+
*/
|
|
7
|
+
import { createHmac } from 'node:crypto';
|
|
8
|
+
import { SLACK_APPROVE_ACTION_ID } from '../notifiers/slack.js';
|
|
9
|
+
import { constantTimeEqualStrings } from './util.js';
|
|
10
|
+
const SLACK_TIMESTAMP_TOLERANCE_SEC = 300;
|
|
11
|
+
/**
|
|
12
|
+
* Verify Slack's v0 signature: HMAC-SHA256 of `v0:{ts}:{body}` with the signing
|
|
13
|
+
* secret, compared in constant time. Also rejects stale timestamps to defeat
|
|
14
|
+
* replay attacks. `rawBody` MUST be the exact bytes Slack sent — any
|
|
15
|
+
* re-serialization changes what the HMAC covers.
|
|
16
|
+
*/
|
|
17
|
+
export function verifySlackSignature(rawBody, timestamp, signature, signingSecret) {
|
|
18
|
+
if (!timestamp || !signature)
|
|
19
|
+
return false;
|
|
20
|
+
const timestampSec = Number(timestamp);
|
|
21
|
+
if (!Number.isFinite(timestampSec))
|
|
22
|
+
return false;
|
|
23
|
+
const skewSec = Math.abs(Date.now() / 1000 - timestampSec);
|
|
24
|
+
if (skewSec > SLACK_TIMESTAMP_TOLERANCE_SEC)
|
|
25
|
+
return false;
|
|
26
|
+
const expected = 'v0=' +
|
|
27
|
+
createHmac('sha256', signingSecret)
|
|
28
|
+
.update(`v0:${timestamp}:${rawBody}`)
|
|
29
|
+
.digest('hex');
|
|
30
|
+
return constantTimeEqualStrings(signature, expected);
|
|
31
|
+
}
|
|
32
|
+
/** Parse a verified Slack interactive-message form body into a decision. */
|
|
33
|
+
export function parseSlackInteraction(rawBody) {
|
|
34
|
+
try {
|
|
35
|
+
const payloadJson = new URLSearchParams(rawBody).get('payload');
|
|
36
|
+
if (!payloadJson)
|
|
37
|
+
return undefined;
|
|
38
|
+
const payload = JSON.parse(payloadJson);
|
|
39
|
+
const action = payload.actions?.[0];
|
|
40
|
+
if (!action?.action_id || !action.value)
|
|
41
|
+
return undefined;
|
|
42
|
+
return {
|
|
43
|
+
approvalId: action.value,
|
|
44
|
+
decision: action.action_id === SLACK_APPROVE_ACTION_ID ? 'approved' : 'rejected',
|
|
45
|
+
actor: payload.user?.username || payload.user?.id || 'slack-user',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=slack.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare the `X-Telegram-Bot-Api-Secret-Token` header against the secret you
|
|
3
|
+
* registered with `setWebhook`, in constant time.
|
|
4
|
+
*/
|
|
5
|
+
export declare function verifyTelegramSecretToken(providedToken: string, expectedToken: string): boolean;
|
|
6
|
+
export interface ParsedTelegramCallback {
|
|
7
|
+
approvalId: string;
|
|
8
|
+
decision: 'approved' | 'rejected';
|
|
9
|
+
actor: string;
|
|
10
|
+
callbackQueryId: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Parse a Telegram update body into an approval decision. Returns undefined
|
|
14
|
+
* for updates that aren't an Approve/Reject callback_query (e.g. a plain
|
|
15
|
+
* message) — the caller should treat that as a no-op, not an error.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseTelegramCallback(body: unknown): ParsedTelegramCallback | undefined;
|
|
18
|
+
/** Acknowledge a callback_query so Telegram stops showing a loading spinner. */
|
|
19
|
+
export declare function answerTelegramCallback(botToken: string, callbackQueryId: string, text: string): Promise<void>;
|
|
20
|
+
//# sourceMappingURL=telegram.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verification + parsing for Telegram's callback_query webhook updates.
|
|
3
|
+
* Framework-agnostic — your HTTP layer reads the request body/headers and
|
|
4
|
+
* hands them to these functions; see docs for a Fastify example.
|
|
5
|
+
*/
|
|
6
|
+
import { TELEGRAM_APPROVE_PREFIX, TELEGRAM_REJECT_PREFIX } from '../notifiers/telegram.js';
|
|
7
|
+
import { constantTimeEqualStrings } from './util.js';
|
|
8
|
+
/**
|
|
9
|
+
* Compare the `X-Telegram-Bot-Api-Secret-Token` header against the secret you
|
|
10
|
+
* registered with `setWebhook`, in constant time.
|
|
11
|
+
*/
|
|
12
|
+
export function verifyTelegramSecretToken(providedToken, expectedToken) {
|
|
13
|
+
return constantTimeEqualStrings(providedToken, expectedToken);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parse a Telegram update body into an approval decision. Returns undefined
|
|
17
|
+
* for updates that aren't an Approve/Reject callback_query (e.g. a plain
|
|
18
|
+
* message) — the caller should treat that as a no-op, not an error.
|
|
19
|
+
*/
|
|
20
|
+
export function parseTelegramCallback(body) {
|
|
21
|
+
const update = body;
|
|
22
|
+
const callbackQuery = update?.callback_query;
|
|
23
|
+
if (!callbackQuery?.id || !callbackQuery.data)
|
|
24
|
+
return undefined;
|
|
25
|
+
const data = callbackQuery.data;
|
|
26
|
+
let decision;
|
|
27
|
+
let approvalId;
|
|
28
|
+
if (data.startsWith(TELEGRAM_APPROVE_PREFIX)) {
|
|
29
|
+
decision = 'approved';
|
|
30
|
+
approvalId = data.slice(TELEGRAM_APPROVE_PREFIX.length);
|
|
31
|
+
}
|
|
32
|
+
else if (data.startsWith(TELEGRAM_REJECT_PREFIX)) {
|
|
33
|
+
decision = 'rejected';
|
|
34
|
+
approvalId = data.slice(TELEGRAM_REJECT_PREFIX.length);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
approvalId,
|
|
41
|
+
decision,
|
|
42
|
+
actor: callbackQuery.from?.username ||
|
|
43
|
+
(callbackQuery.from?.id ? String(callbackQuery.from.id) : 'telegram-user'),
|
|
44
|
+
callbackQueryId: callbackQuery.id,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Acknowledge a callback_query so Telegram stops showing a loading spinner. */
|
|
48
|
+
export async function answerTelegramCallback(botToken, callbackQueryId, text) {
|
|
49
|
+
await fetch(`https://api.telegram.org/bot${botToken}/answerCallbackQuery`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'content-type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({ callback_query_id: callbackQueryId, text }),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=telegram.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/** Length-safe constant-time string comparison. */
|
|
3
|
+
export function constantTimeEqualStrings(a, b) {
|
|
4
|
+
const bufferA = Buffer.from(a);
|
|
5
|
+
const bufferB = Buffer.from(b);
|
|
6
|
+
if (bufferA.length !== bufferB.length)
|
|
7
|
+
return false;
|
|
8
|
+
return timingSafeEqual(bufferA, bufferB);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=util.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@driftwatch/autopilot",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Slack, Telegram, and generic-webhook notifiers plus inbound-webhook signature verification for DriftWatch Autopilot. Companion to @driftwatch/sdk — bring these in only if you use those channels.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Victory Lucky",
|
|
8
|
+
"homepage": "https://github.com/codewithveek/drift-watch#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/codewithveek/drift-watch.git",
|
|
12
|
+
"directory": "packages/autopilot"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/codewithveek/drift-watch/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ai-agent",
|
|
19
|
+
"autopilot",
|
|
20
|
+
"slack",
|
|
21
|
+
"telegram",
|
|
22
|
+
"webhook",
|
|
23
|
+
"drift-detection"
|
|
24
|
+
],
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"!dist/**/*.map"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@driftwatch/sdk": "0.4.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"typescript": "^5.6.0",
|
|
50
|
+
"vitest": "^4.1.10"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc",
|
|
54
|
+
"typecheck": "tsc --noEmit",
|
|
55
|
+
"test": "vitest run --passWithNoTests"
|
|
56
|
+
}
|
|
57
|
+
}
|