Skip to content

Commit 2c783d8

Browse files
authored
Vercel hook move to subdir (#5)
* Implement web hook for routing config * fix: fix route for serverless fn * Fix build error * Move service to its own subfolder
1 parent 86eeefe commit 2c783d8

File tree

7 files changed

+189
-1
lines changed

7 files changed

+189
-1
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
node_modules
1+
node_modules
2+
.vercel

service/api/notifyRouter.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Router secret: Used to authenticate requests to the router to secure the hook
2+
const ROUTER_SECRET = process.env.ROUTER_SECRET;
3+
4+
// Telegram bot's token for the alerts
5+
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN!;
6+
7+
// NEAR Telegram chat IDs
8+
const TELEGRAM_CHAT_NEAR = process.env.TELEGRAM_CHAT_NEAR;
9+
10+
// Assert environment variables are set
11+
if (!ROUTER_SECRET || !TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_NEAR) {
12+
throw new Error(
13+
"Missing environment variables: ROUTER_SECRET, TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_NEAR"
14+
);
15+
}
16+
17+
// Map by endpoint name prefix to Telegram chat ID
18+
const NAME_PREFIX_TO_TELEGRAM_CHAT_ID: Record<
19+
string,
20+
string | string[] | undefined
21+
> = {
22+
"Bridging - Near": TELEGRAM_CHAT_NEAR,
23+
"TEST - Intentional Failure": TELEGRAM_CHAT_NEAR, // For testing. Remove after.
24+
};
25+
26+
/**
27+
* Send a message to a Telegram chat
28+
*
29+
* @param token Telegram bot's token
30+
* @param chatId Telegram chat ID
31+
* @param text Message to send
32+
*/
33+
async function sendTelegramMessage(
34+
token: string,
35+
chatId: string,
36+
text: string
37+
) {
38+
const url = `https://api.telegram.org/bot${token}/sendMessage`;
39+
const body = {
40+
chat_id: chatId,
41+
text,
42+
parse_mode: "MarkdownV2",
43+
disable_web_page_preview: true,
44+
};
45+
const res = await fetch(url, {
46+
method: "POST",
47+
headers: { "content-type": "application/json" },
48+
body: JSON.stringify(body),
49+
});
50+
if (!res.ok) throw new Error(await res.text());
51+
}
52+
53+
/**
54+
* Escape a string for MarkdownV2
55+
*
56+
* @param s String to escape
57+
* @returns Escaped string
58+
*/
59+
function escapeMdV2(s: string) {
60+
// Minimal MarkdownV2 escaping for common symbols
61+
return s.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
62+
}
63+
64+
export default {
65+
/**
66+
* Handle the request to notify the Telegram chat
67+
*
68+
* @param request Request object
69+
* @returns Response object
70+
*/
71+
async fetch(request: Request) {
72+
try {
73+
const url = new URL(request.url);
74+
if (url.searchParams.get("key") !== ROUTER_SECRET)
75+
return new Response("unauthorized", { status: 401 });
76+
77+
const payload = await request.json().catch(() => ({} as any));
78+
const textPayload =
79+
typeof payload === "string" ? payload : JSON.stringify(payload);
80+
81+
// Try to extract a site name; fall back if not present
82+
const siteName =
83+
/"site(Name)?"\s*:\s*"(?<name>[^"]+)"/i.exec(textPayload)?.groups
84+
?.name ||
85+
/(^|>)\s*(?<name>[^<(]+)\s*\(/.exec(textPayload)?.groups?.name ||
86+
"Unknown Site";
87+
88+
const match = Object.entries(NAME_PREFIX_TO_TELEGRAM_CHAT_ID).find(
89+
([k]) => siteName.startsWith(k)
90+
)?.[1];
91+
92+
// If no matching route found, skip notification silently
93+
if (!match) {
94+
return new Response("no route configured - skipping", { status: 204 });
95+
}
96+
97+
const siteEsc = escapeMdV2(siteName);
98+
const rawEsc = escapeMdV2(textPayload.slice(0, 3500));
99+
const msg = `🚨 *Upptime alert*\n• *Site:* ${siteEsc}\n• *Raw:* \`${rawEsc}\``;
100+
101+
const targets = Array.isArray(match) ? match : [match];
102+
await Promise.all(
103+
targets
104+
.filter(Boolean)
105+
.map((chatId) =>
106+
sendTelegramMessage(TELEGRAM_BOT_TOKEN, chatId!, msg)
107+
)
108+
);
109+
110+
return new Response("ok");
111+
} catch (e: any) {
112+
return new Response(`error: ${e?.message || e}`, { status: 500 });
113+
}
114+
},
115+
};

service/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "uptime-periphery",
3+
"private": true,
4+
"version": "1.0.0",
5+
"devDependencies": {
6+
"@types/node": "^18.0.0",
7+
"typescript": "^5.0.0"
8+
}
9+
}

service/tsconfig.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"module": "commonjs",
5+
"lib": ["ES2020"],
6+
"moduleResolution": "node",
7+
"esModuleInterop": true,
8+
"skipLibCheck": true,
9+
"strict": true,
10+
"resolveJsonModule": true,
11+
"allowSyntheticDefaultImports": true,
12+
"forceConsistentCasingInFileNames": true,
13+
"types": ["node"]
14+
},
15+
"include": ["api/**/*"],
16+
"exclude": ["node_modules"]
17+
}

service/vercel-ignore.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Vercel provides these; fall back safely if missing
5+
PREV="${VERCEL_GIT_PREVIOUS_SHA:-}"
6+
CURR="${VERCEL_GIT_COMMIT_SHA:-}"
7+
8+
# If we don't have a previous SHA (first deploy), build.
9+
if [ -z "$PREV" ] || [ -z "$CURR" ]; then
10+
echo "No previous SHA; allow build"
11+
exit 1
12+
fi
13+
14+
# Only build when router-related files changed
15+
if git diff --name-only "$PREV" "$CURR" -- \
16+
| grep -E '^(api/|vercel\.json|package\.json)'; then
17+
echo "Router files changed; allow build"
18+
exit 1
19+
else
20+
echo "No router changes; skip build"
21+
exit 0
22+
fi

service/vercel.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"version": 2,
3+
"ignoreCommand": "bash ./vercel-ignore.sh"
4+
}

service/yarn.lock

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2+
# yarn lockfile v1
3+
4+
5+
"@types/node@^18.0.0":
6+
version "18.19.130"
7+
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.130.tgz#da4c6324793a79defb7a62cba3947ec5add00d59"
8+
integrity sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==
9+
dependencies:
10+
undici-types "~5.26.4"
11+
12+
typescript@^5.0.0:
13+
version "5.9.3"
14+
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
15+
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
16+
17+
undici-types@~5.26.4:
18+
version "5.26.5"
19+
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
20+
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==

0 commit comments

Comments
 (0)