-
-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy path[id].js
More file actions
118 lines (102 loc) · 3.76 KB
/
Copy path[id].js
File metadata and controls
118 lines (102 loc) · 3.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// GET /api/developer/webhooks/:id — webhook details + recent deliveries
// PATCH /api/developer/webhooks/:id — update webhook (url, events, active, description)
// DELETE /api/developer/webhooks/:id — delete webhook
import { cors, error, json, method, readJson, wrap } from '../../_lib/http.js';
import { getSessionUser } from '../../_lib/auth.js';
import { requireCsrf } from '../../_lib/csrf.js';
import { sql } from '../../_lib/db.js';
import { EVENT_TYPES } from '../../_lib/webhook-dispatch.js';
import { assertPublicHttpsUrl } from '../../_lib/ssrf.js';
const URL_MAX_LENGTH = 2048;
export default wrap(async function handler(req, res) {
if (cors(req, res, { methods: 'GET,PATCH,DELETE,OPTIONS', credentials: true })) return;
const user = await getSessionUser(req, res);
if (!user) return error(res, 401, 'unauthorized', 'Sign in required');
const url = new URL(req.url, 'http://x');
const id = url.searchParams.get('id') || extractId(url.pathname);
if (!id) return error(res, 400, 'bad_request', 'Webhook ID required');
const [webhook] = await sql`
select id, url, events, active, description, created_at, updated_at
from developer_webhooks
where id = ${id} and user_id = ${user.id}
`;
if (!webhook) return error(res, 404, 'not_found', 'Webhook not found');
if (req.method === 'GET') {
const deliveries = await sql`
select id, event_type, event_id, status_code, error, attempt, created_at
from webhook_deliveries
where webhook_id = ${id}
order by created_at desc
limit 50
`;
return json(res, 200, { webhook, deliveries });
}
if (req.method === 'PATCH') {
if (!(await requireCsrf(req, res, user.id))) return;
let body;
try {
body = await readJson(req, 5000);
} catch (e) {
return error(res, e.status || 400, 'bad_request', e.message);
}
const updates = {};
if (typeof body.url === 'string') {
const trimmed = body.url.trim();
if (trimmed.length > URL_MAX_LENGTH)
return error(res, 400, 'bad_request', 'URL too long');
try {
const parsed = new URL(trimmed);
if (parsed.protocol !== 'https:')
return error(res, 400, 'bad_request', 'Must use HTTPS');
} catch {
return error(res, 400, 'bad_request', 'Invalid URL');
}
// SSRF: reject a URL that resolves to a non-public address (delivery
// also re-validates and pins per attempt).
try {
await assertPublicHttpsUrl(trimmed);
} catch {
return error(
res,
400,
'bad_request',
'Webhook URL must resolve to a public address',
);
}
updates.url = trimmed;
}
if (Array.isArray(body.events)) {
updates.events = body.events.filter((e) => EVENT_TYPES.includes(e));
}
if (typeof body.active === 'boolean') {
updates.active = body.active;
}
if (typeof body.description === 'string') {
updates.description = body.description.trim().slice(0, 200);
}
if (!Object.keys(updates).length) {
return json(res, 200, { webhook });
}
const [updated] = await sql`
update developer_webhooks set
url = ${updates.url ?? webhook.url},
events = ${updates.events ?? webhook.events},
active = ${updates.active ?? webhook.active},
description = ${updates.description !== undefined ? updates.description : webhook.description},
updated_at = now()
where id = ${id} and user_id = ${user.id}
returning id, url, events, active, description, created_at, updated_at
`;
return json(res, 200, { webhook: updated });
}
if (req.method === 'DELETE') {
if (!(await requireCsrf(req, res, user.id))) return;
await sql`delete from developer_webhooks where id = ${id} and user_id = ${user.id}`;
return json(res, 200, { deleted: true });
}
return method(req, res, ['GET', 'PATCH', 'DELETE']);
});
function extractId(pathname) {
const m = pathname.match(/\/webhooks\/([^/]+)/);
return m ? m[1] : null;
}