@chatcops/server 0.5.0 → 0.7.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/README.md +4 -4
- package/dist/adapters/ai-sdk.d.ts +12 -0
- package/dist/adapters/ai-sdk.d.ts.map +1 -0
- package/dist/adapters/ai-sdk.js +27 -0
- package/dist/adapters/ai-sdk.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +9 -83
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/express.d.ts +6 -2
- package/dist/adapters/express.d.ts.map +1 -1
- package/dist/adapters/express.js +102 -30
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/vercel.d.ts.map +1 -1
- package/dist/adapters/vercel.js +7 -74
- package/dist/adapters/vercel.js.map +1 -1
- package/dist/client-ip.d.ts +37 -0
- package/dist/client-ip.d.ts.map +1 -0
- package/dist/client-ip.js +57 -0
- package/dist/client-ip.js.map +1 -0
- package/dist/config.d.ts +136 -8
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -1
- package/dist/config.js.map +1 -1
- package/dist/examples/cloudflare-worker.js +1 -1
- package/dist/examples/cloudflare-worker.js.map +1 -1
- package/dist/handler.d.ts +52 -1
- package/dist/handler.d.ts.map +1 -1
- package/dist/handler.js +417 -136
- package/dist/handler.js.map +1 -1
- package/dist/http.d.ts +69 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +262 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/knowledge.d.ts +18 -0
- package/dist/knowledge.d.ts.map +1 -0
- package/dist/knowledge.js +32 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/rate-limit.d.ts +5 -0
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +5 -0
- package/dist/rate-limit.js.map +1 -1
- package/dist/security/origin-policy.d.ts +19 -0
- package/dist/security/origin-policy.d.ts.map +1 -0
- package/dist/security/origin-policy.js +60 -0
- package/dist/security/origin-policy.js.map +1 -0
- package/dist/security/origin.d.ts +5 -0
- package/dist/security/origin.d.ts.map +1 -0
- package/dist/security/origin.js +47 -0
- package/dist/security/origin.js.map +1 -0
- package/dist/security/session-token.d.ts +65 -0
- package/dist/security/session-token.d.ts.map +1 -0
- package/dist/security/session-token.js +338 -0
- package/dist/security/session-token.js.map +1 -0
- package/dist/security/sessions.d.ts +72 -0
- package/dist/security/sessions.d.ts.map +1 -0
- package/dist/security/sessions.js +216 -0
- package/dist/security/sessions.js.map +1 -0
- package/dist/security/startup-checks.d.ts +45 -0
- package/dist/security/startup-checks.d.ts.map +1 -0
- package/dist/security/startup-checks.js +124 -0
- package/dist/security/startup-checks.js.map +1 -0
- package/dist/security/turnstile.d.ts +24 -0
- package/dist/security/turnstile.d.ts.map +1 -0
- package/dist/security/turnstile.js +93 -0
- package/dist/security/turnstile.js.map +1 -0
- package/dist/visitor-context.d.ts.map +1 -1
- package/dist/visitor-context.js +7 -0
- package/dist/visitor-context.js.map +1 -1
- package/dist/webhook.d.ts +3 -2
- package/dist/webhook.d.ts.map +1 -1
- package/dist/webhook.js +5 -2
- package/dist/webhook.js.map +1 -1
- package/package.json +22 -5
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { consoleLogger } from '@chatcops/core';
|
|
2
|
+
import { errorBody } from '@chatcops/core';
|
|
3
|
+
import { base64UrlDecode, importSessionKey, signSession, verifySession, verifySessionSignature } from './session-token.js';
|
|
4
|
+
import { verifyTurnstile } from './turnstile.js';
|
|
5
|
+
/**
|
|
6
|
+
* Session tokens for the chat endpoint. `POST /session` gives the widget a
|
|
7
|
+
* short-lived token bound to its origin and one conversation id, and chat
|
|
8
|
+
* requests send it as `Authorization: Bearer <token>`. A visitor who guesses
|
|
9
|
+
* another conversation's id can't get a token for it, so the id alone no
|
|
10
|
+
* longer reads or writes someone else's conversation.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_SESSION_TTL_SEC = 900;
|
|
13
|
+
/** An expired token still resumes its conversation for this long after it expires. */
|
|
14
|
+
export const SESSION_RESUME_GRACE_SEC = 30 * 24 * 60 * 60;
|
|
15
|
+
const MIN_SECRET_BYTES = 32;
|
|
16
|
+
const MAX_ID_LENGTH = 128;
|
|
17
|
+
/**
|
|
18
|
+
* `undefined` when `sessionSecrets` isn't set. Throws at startup on a bad
|
|
19
|
+
* config. `lookup` says what's stored under a conversation id.
|
|
20
|
+
*/
|
|
21
|
+
export function createSessions(config, lookup = async () => ({ unused: false }), now = Date.now) {
|
|
22
|
+
const secrets = config.sessionSecrets;
|
|
23
|
+
if (secrets === undefined) {
|
|
24
|
+
if (config.requireSession)
|
|
25
|
+
throw new Error('`requireSession` needs `sessionSecrets` to sign tokens with.');
|
|
26
|
+
if (config.turnstile)
|
|
27
|
+
throw new Error('`turnstile` is checked when a session token is issued, so it needs `sessionSecrets`.');
|
|
28
|
+
if (config.identity)
|
|
29
|
+
throw new Error('`identity` goes into session tokens, so it needs `sessionSecrets`.');
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const identitySecret = config.identity?.secret;
|
|
33
|
+
if (config.identity && (typeof identitySecret !== 'string' || new TextEncoder().encode(identitySecret).byteLength < MIN_SECRET_BYTES)) {
|
|
34
|
+
throw new Error(`\`identity.secret\` needs at least ${MIN_SECRET_BYTES} bytes, like \`sessionSecrets\`.`);
|
|
35
|
+
}
|
|
36
|
+
const turnstile = config.turnstile;
|
|
37
|
+
if (turnstile && !turnstile.secretKey)
|
|
38
|
+
throw new Error('`turnstile` needs its `secretKey`.');
|
|
39
|
+
if (!Array.isArray(secrets) || secrets.length === 0)
|
|
40
|
+
throw new Error('`sessionSecrets` needs at least one secret.');
|
|
41
|
+
secrets.forEach((secret, i) => {
|
|
42
|
+
const bytes = typeof secret === 'string' ? new TextEncoder().encode(secret).byteLength : 0;
|
|
43
|
+
if (bytes < MIN_SECRET_BYTES) {
|
|
44
|
+
throw new Error(`sessionSecrets[${i}] is ${bytes} bytes; it needs at least ${MIN_SECRET_BYTES}. Generate one with \`openssl rand -base64 32\`.`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
const ttlSec = config.sessionTtlSec ?? DEFAULT_SESSION_TTL_SEC;
|
|
48
|
+
if (!(Number.isInteger(ttlSec) && ttlSec > 0))
|
|
49
|
+
throw new Error('`sessionTtlSec` must be a whole number of seconds above 0.');
|
|
50
|
+
// Imported on first use: Workers don't allow crypto.subtle at global scope.
|
|
51
|
+
let keys;
|
|
52
|
+
const getKeys = () => (keys ??= Promise.all(secrets.map((secret) => importSessionKey(secret))));
|
|
53
|
+
let identityKey;
|
|
54
|
+
const getIdentityKey = () => (identityKey ??= importSessionKey(identitySecret));
|
|
55
|
+
const nowSec = () => Math.floor(now() / 1000);
|
|
56
|
+
const required = config.requireSession ?? true;
|
|
57
|
+
return {
|
|
58
|
+
required,
|
|
59
|
+
async mint(body, origin, clientIp) {
|
|
60
|
+
const request = readMintRequest(body);
|
|
61
|
+
if (!request)
|
|
62
|
+
return { status: 400, body: errorBody('invalid_request', { message: 'Invalid session request' }) };
|
|
63
|
+
const org = origin ?? '';
|
|
64
|
+
const time = nowSec();
|
|
65
|
+
// Resuming a conversation needs a token that was issued for it, from
|
|
66
|
+
// this origin. Expired is fine within the grace window. An id with
|
|
67
|
+
// nothing stored yet (the widget makes its own) is given out as is,
|
|
68
|
+
// since there's nothing in it to read, and so is a signed-in user's own
|
|
69
|
+
// conversation (with a \`userToken\` for them). Anything else gets a new
|
|
70
|
+
// conversation, so a guessed or leaked id is useless.
|
|
71
|
+
let resumed;
|
|
72
|
+
let cid;
|
|
73
|
+
if (request.conversationId && request.previousToken) {
|
|
74
|
+
const result = await verifySession(request.previousToken, await getKeys(), {
|
|
75
|
+
origin: org,
|
|
76
|
+
conversationId: request.conversationId,
|
|
77
|
+
now: time,
|
|
78
|
+
clockSkewSec: SESSION_RESUME_GRACE_SEC,
|
|
79
|
+
});
|
|
80
|
+
if (result.ok)
|
|
81
|
+
resumed = result.claims;
|
|
82
|
+
}
|
|
83
|
+
// A visitor the host site signed in. A token that doesn't verify is an
|
|
84
|
+
// error, not an anonymous session, so a bug on the host site shows.
|
|
85
|
+
let user;
|
|
86
|
+
if (request.userToken !== undefined) {
|
|
87
|
+
if (!identitySecret)
|
|
88
|
+
return { status: 400, body: errorBody('invalid_request', { message: '`userToken` needs `identity` on the server.' }) };
|
|
89
|
+
user = await verifyUserToken(request.userToken, await getIdentityKey(), time);
|
|
90
|
+
if (!user)
|
|
91
|
+
return { status: 401, body: errorBody('session_required', { message: 'The userToken is invalid or has expired.' }) };
|
|
92
|
+
}
|
|
93
|
+
// A token for another user doesn't carry over.
|
|
94
|
+
if (resumed?.uid && resumed.uid !== user?.sub)
|
|
95
|
+
resumed = undefined;
|
|
96
|
+
// A refresh was checked when its first token was issued; a new session
|
|
97
|
+
// has to pass Turnstile first.
|
|
98
|
+
if (turnstile && !resumed) {
|
|
99
|
+
const result = await verifyTurnstile(request.turnstileToken, turnstile, {
|
|
100
|
+
remoteIp: clientIp,
|
|
101
|
+
idempotencyKey: request.idempotencyKey,
|
|
102
|
+
});
|
|
103
|
+
if (!result.ok) {
|
|
104
|
+
if (result.code === 'unavailable') {
|
|
105
|
+
(config.logger ?? consoleLogger()).warn('turnstile_unavailable', 'Turnstile could not be reached, so no session token was issued.');
|
|
106
|
+
return { status: 503, body: errorBody('turnstile_failed', { message: 'Turnstile could not be reached. Try again.' }) };
|
|
107
|
+
}
|
|
108
|
+
return { status: 401, body: errorBody('turnstile_failed', { message: 'The Turnstile check failed.' }) };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (resumed)
|
|
112
|
+
cid = resumed.cid;
|
|
113
|
+
else if (request.conversationId) {
|
|
114
|
+
const info = await lookup(request.conversationId);
|
|
115
|
+
// Nothing stored yet, or this signed-in user's own conversation.
|
|
116
|
+
if (info.unused || (user && info.uid === user.sub))
|
|
117
|
+
cid = request.conversationId;
|
|
118
|
+
}
|
|
119
|
+
const claims = {
|
|
120
|
+
v: 1,
|
|
121
|
+
sid: resumed?.sid ?? crypto.randomUUID(),
|
|
122
|
+
cid: cid ?? crypto.randomUUID(),
|
|
123
|
+
org,
|
|
124
|
+
iat: time,
|
|
125
|
+
exp: time + ttlSec,
|
|
126
|
+
...(user ? { uid: user.sub, ...(user.tier ? { tier: user.tier } : {}) } : {}),
|
|
127
|
+
};
|
|
128
|
+
const token = await signSession(claims, await getKeys());
|
|
129
|
+
return { status: 200, body: { token, expiresAt: claims.exp * 1000, conversationId: claims.cid } };
|
|
130
|
+
},
|
|
131
|
+
async check(authorization, origin, conversationId) {
|
|
132
|
+
const token = bearer(authorization);
|
|
133
|
+
if (!token) {
|
|
134
|
+
return required
|
|
135
|
+
? { ok: false, code: 'session_required', message: 'This chat needs a session token. Get one from POST /session.' }
|
|
136
|
+
: { ok: true };
|
|
137
|
+
}
|
|
138
|
+
const result = await verifySession(token, await getKeys(), { origin: origin ?? '', conversationId, now: nowSec() });
|
|
139
|
+
if (result.ok)
|
|
140
|
+
return { ok: true, claims: result.claims };
|
|
141
|
+
switch (result.code) {
|
|
142
|
+
case 'expired':
|
|
143
|
+
return { ok: false, code: 'token_expired', message: 'The session token has expired. Get a new one from POST /session.' };
|
|
144
|
+
case 'conversation_mismatch':
|
|
145
|
+
return { ok: false, code: 'conversation_mismatch', message: 'The session token is for a different conversation.' };
|
|
146
|
+
default:
|
|
147
|
+
return { ok: false, code: 'session_required', message: 'The session token is invalid. Get a new one from POST /session.' };
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function bearer(header) {
|
|
153
|
+
if (typeof header !== 'string')
|
|
154
|
+
return undefined;
|
|
155
|
+
const match = /^Bearer\s+(\S+)\s*$/i.exec(header);
|
|
156
|
+
return match?.[1];
|
|
157
|
+
}
|
|
158
|
+
function readMintRequest(body) {
|
|
159
|
+
if (body === undefined || body === null)
|
|
160
|
+
return {};
|
|
161
|
+
if (typeof body !== 'object' || Array.isArray(body))
|
|
162
|
+
return undefined;
|
|
163
|
+
const { conversationId, previousToken, turnstileToken, idempotencyKey, userToken } = body;
|
|
164
|
+
const request = {};
|
|
165
|
+
if (userToken !== undefined) {
|
|
166
|
+
if (typeof userToken !== 'string' || userToken.length > 4096)
|
|
167
|
+
return undefined;
|
|
168
|
+
request.userToken = userToken;
|
|
169
|
+
}
|
|
170
|
+
if (turnstileToken !== undefined) {
|
|
171
|
+
if (typeof turnstileToken !== 'string' || turnstileToken.length > 4096)
|
|
172
|
+
return undefined;
|
|
173
|
+
request.turnstileToken = turnstileToken;
|
|
174
|
+
}
|
|
175
|
+
if (idempotencyKey !== undefined) {
|
|
176
|
+
if (typeof idempotencyKey !== 'string' || idempotencyKey.length > 128)
|
|
177
|
+
return undefined;
|
|
178
|
+
request.idempotencyKey = idempotencyKey;
|
|
179
|
+
}
|
|
180
|
+
if (conversationId !== undefined) {
|
|
181
|
+
if (typeof conversationId !== 'string' || conversationId.length === 0 || conversationId.length > MAX_ID_LENGTH)
|
|
182
|
+
return undefined;
|
|
183
|
+
request.conversationId = conversationId;
|
|
184
|
+
}
|
|
185
|
+
if (previousToken !== undefined) {
|
|
186
|
+
if (typeof previousToken !== 'string' || previousToken.length > 4096)
|
|
187
|
+
return undefined;
|
|
188
|
+
request.previousToken = previousToken;
|
|
189
|
+
}
|
|
190
|
+
return request;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* A host site's `userToken`: an HS256 JWT signed with `identity.secret`, with
|
|
194
|
+
* `sub` (the user id, up to 128 characters), `exp` in seconds and an optional
|
|
195
|
+
* `tier`. `undefined` when it doesn't verify or has expired.
|
|
196
|
+
*/
|
|
197
|
+
export async function verifyUserToken(token, key, nowSec) {
|
|
198
|
+
if (!(await verifySessionSignature(token, [key])))
|
|
199
|
+
return undefined;
|
|
200
|
+
let payload;
|
|
201
|
+
try {
|
|
202
|
+
payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(token.split('.')[1] ?? '')));
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
const { sub, exp, tier } = payload ?? {};
|
|
208
|
+
if (typeof sub !== 'string' || sub.length === 0 || sub.length > 128)
|
|
209
|
+
return undefined;
|
|
210
|
+
if (typeof exp !== 'number' || !Number.isFinite(exp) || exp + 60 < nowSec)
|
|
211
|
+
return undefined;
|
|
212
|
+
if (tier !== undefined && (typeof tier !== 'string' || tier.length > 64))
|
|
213
|
+
return undefined;
|
|
214
|
+
return { sub, ...(typeof tier === 'string' ? { tier } : {}) };
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/security/sessions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type Logger } from '@chatcops/core';
|
|
2
|
+
/**
|
|
3
|
+
* Startup checks for the security settings the server is about to add: the
|
|
4
|
+
* allowed origins and the secrets that sign session tokens.
|
|
5
|
+
*
|
|
6
|
+
* These are the mistakes that fail silently. A secret that is ten characters
|
|
7
|
+
* long works, and so does a secret that is also a `NEXT_PUBLIC_` variable, so
|
|
8
|
+
* nothing breaks until the day it does. A function that finds them before the
|
|
9
|
+
* server starts is worth more than one that finds them during a request.
|
|
10
|
+
*
|
|
11
|
+
* Nothing here logs or throws. The caller decides what to do with a problem.
|
|
12
|
+
*/
|
|
13
|
+
/** The security settings a server is configured with. */
|
|
14
|
+
export interface SecuritySettings {
|
|
15
|
+
/** The origins the server accepts requests from. `*` allows every origin. */
|
|
16
|
+
allowedOrigins?: string[];
|
|
17
|
+
/** The secrets that sign session tokens. */
|
|
18
|
+
sessionSecrets?: string[];
|
|
19
|
+
}
|
|
20
|
+
/** One thing that is wrong with the security settings. */
|
|
21
|
+
export interface SecurityProblem {
|
|
22
|
+
/** An error stops the server from starting. A warning is worth a look. */
|
|
23
|
+
level: 'error' | 'warning';
|
|
24
|
+
/** What is wrong, and what to do about it. Never contains a secret. */
|
|
25
|
+
message: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Finds the mistakes in the security settings, most serious first.
|
|
29
|
+
*
|
|
30
|
+
* `env` is the environment to look in, and defaults to `process.env`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function checkSecuritySettings(settings: SecuritySettings, env?: Record<string, string | undefined>): SecurityProblem[];
|
|
33
|
+
/** What a server's security settings come to, for the startup log. */
|
|
34
|
+
export interface SecuritySummary {
|
|
35
|
+
allowedOrigins: string[];
|
|
36
|
+
/** Seconds, when session tokens are on. */
|
|
37
|
+
sessionTtlSec?: number;
|
|
38
|
+
requireSession?: boolean;
|
|
39
|
+
turnstile: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** The one line the server logs at startup about its security settings. */
|
|
42
|
+
export declare function describeSecurity(summary: SecuritySummary): string;
|
|
43
|
+
/** Logs the problems `checkSecuritySettings` finds, errors first, then the summary line. */
|
|
44
|
+
export declare function logSecurityAtStartup(settings: SecuritySettings & Omit<SecuritySummary, 'allowedOrigins'>, logger?: Logger): void;
|
|
45
|
+
//# sourceMappingURL=startup-checks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"startup-checks.d.ts","sourceRoot":"","sources":["../../src/security/startup-checks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC5D;;;;;;;;;;GAUG;AAEH,yDAAyD;AACzD,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,0DAA0D;AAC1D,MAAM,WAAW,eAAe;IAC9B,0EAA0E;IAC1E,KAAK,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;CACjB;AAwBD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,gBAAgB,EAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACvC,eAAe,EAAE,CAkFnB;AAED,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CASjE;AAED,4FAA4F;AAC5F,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC,eAAe,EAAE,gBAAgB,CAAC,EACpE,MAAM,GAAE,MAAwB,GAC/B,IAAI,CAMN"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { consoleLogger } from '@chatcops/core';
|
|
2
|
+
/** How many bytes a session secret needs to be worth signing with. */
|
|
3
|
+
const MIN_SECRET_BYTES = 32;
|
|
4
|
+
/**
|
|
5
|
+
* The env var prefixes whose value is shipped to the browser, so a secret
|
|
6
|
+
* that shares a value with one of them is in the client bundle.
|
|
7
|
+
*/
|
|
8
|
+
const PUBLIC_ENV_PREFIXES = ['NEXT_PUBLIC_', 'VITE_', 'PUBLIC_', 'NUXT_PUBLIC_', 'EXPO_PUBLIC_', 'REACT_APP_'];
|
|
9
|
+
/**
|
|
10
|
+
* The environment to read, which is `process.env` where there is one. Workers
|
|
11
|
+
* have no `process`, so the checks still work there with an empty environment.
|
|
12
|
+
*/
|
|
13
|
+
function defaultEnv() {
|
|
14
|
+
return globalThis.process?.env ?? {};
|
|
15
|
+
}
|
|
16
|
+
/** How many bytes a string takes in UTF-8, so a 32 byte secret is 32 bytes. */
|
|
17
|
+
function utf8Length(value) {
|
|
18
|
+
return new TextEncoder().encode(value).length;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Finds the mistakes in the security settings, most serious first.
|
|
22
|
+
*
|
|
23
|
+
* `env` is the environment to look in, and defaults to `process.env`.
|
|
24
|
+
*/
|
|
25
|
+
export function checkSecuritySettings(settings, env) {
|
|
26
|
+
const problems = [];
|
|
27
|
+
const secrets = settings.sessionSecrets ?? [];
|
|
28
|
+
const origins = settings.allowedOrigins ?? [];
|
|
29
|
+
const environment = env ?? defaultEnv();
|
|
30
|
+
for (const [index, secret] of secrets.entries()) {
|
|
31
|
+
if (utf8Length(secret) < MIN_SECRET_BYTES) {
|
|
32
|
+
problems.push({
|
|
33
|
+
level: 'error',
|
|
34
|
+
message: `Session secret ${index + 1} is shorter than ${MIN_SECRET_BYTES} bytes.`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const [index, secret] of secrets.entries()) {
|
|
39
|
+
for (const name of Object.keys(environment)) {
|
|
40
|
+
if (!PUBLIC_ENV_PREFIXES.some((prefix) => name.startsWith(prefix)))
|
|
41
|
+
continue;
|
|
42
|
+
if (environment[name] !== secret)
|
|
43
|
+
continue;
|
|
44
|
+
problems.push({
|
|
45
|
+
level: 'error',
|
|
46
|
+
message: `Session secret ${index + 1} is also the value of the ${name} environment variable, ` +
|
|
47
|
+
'which is sent to the browser. Use a different secret.',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const [index, secret] of secrets.entries()) {
|
|
52
|
+
const first = secrets.indexOf(secret);
|
|
53
|
+
if (first !== index) {
|
|
54
|
+
problems.push({
|
|
55
|
+
level: 'warning',
|
|
56
|
+
message: `Session secrets ${first + 1} and ${index + 1} are the same.`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const origin of origins) {
|
|
61
|
+
if (origin === '*')
|
|
62
|
+
continue;
|
|
63
|
+
if (!origin.startsWith('http://') && !origin.startsWith('https://')) {
|
|
64
|
+
problems.push({
|
|
65
|
+
level: 'error',
|
|
66
|
+
message: `Allowed origin "${origin}" must start with http:// or https://.`,
|
|
67
|
+
});
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = new URL(origin);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
problems.push({
|
|
76
|
+
level: 'error',
|
|
77
|
+
message: `Allowed origin "${origin}" is not a valid URL.`,
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (parsed.pathname !== '/' || parsed.search !== '' || parsed.hash !== '' || /[/?#]$/.test(origin)) {
|
|
82
|
+
problems.push({
|
|
83
|
+
level: 'error',
|
|
84
|
+
message: `Allowed origin "${origin}" must not have a path, a query, a fragment or a trailing slash, ` +
|
|
85
|
+
`because browsers send the origin without one. Use "${parsed.origin}".`,
|
|
86
|
+
});
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (origin.startsWith('http://') && parsed.hostname !== 'localhost' && parsed.hostname !== '127.0.0.1') {
|
|
90
|
+
problems.push({
|
|
91
|
+
level: 'warning',
|
|
92
|
+
message: `Allowed origin "${origin}" uses http://. Use https://, unless it is a local development server.`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (origins.includes('*') && origins.length > 1) {
|
|
97
|
+
problems.push({
|
|
98
|
+
level: 'warning',
|
|
99
|
+
message: 'Allowed origins include "*", which allows every site, so the other entries do nothing.',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return problems;
|
|
103
|
+
}
|
|
104
|
+
/** The one line the server logs at startup about its security settings. */
|
|
105
|
+
export function describeSecurity(summary) {
|
|
106
|
+
const origins = summary.allowedOrigins.includes('*')
|
|
107
|
+
? 'any site (allowedOrigins is "*"; list your sites in production)'
|
|
108
|
+
: summary.allowedOrigins.join(', ');
|
|
109
|
+
const sessions = summary.sessionTtlSec === undefined
|
|
110
|
+
? 'session tokens off'
|
|
111
|
+
: `session tokens on (${summary.sessionTtlSec}s${summary.requireSession === false ? ', not required' : ''})`;
|
|
112
|
+
return `Security: chat allowed from ${origins}; ${sessions}; Turnstile ${summary.turnstile ? 'on' : 'off'}.`;
|
|
113
|
+
}
|
|
114
|
+
/** Logs the problems `checkSecuritySettings` finds, errors first, then the summary line. */
|
|
115
|
+
export function logSecurityAtStartup(settings, logger = consoleLogger()) {
|
|
116
|
+
const allowedOrigins = settings.allowedOrigins ?? ['*'];
|
|
117
|
+
const problems = checkSecuritySettings({ ...settings, allowedOrigins });
|
|
118
|
+
for (const problem of problems.filter((p) => p.level === 'error'))
|
|
119
|
+
logger.error('insecure_config', `ERROR: ${problem.message}`);
|
|
120
|
+
for (const problem of problems.filter((p) => p.level === 'warning'))
|
|
121
|
+
logger.warn('insecure_config', problem.message);
|
|
122
|
+
logger.info('security', describeSecurity({ ...settings, allowedOrigins }));
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=startup-checks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"startup-checks.js","sourceRoot":"","sources":["../../src/security/startup-checks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface TurnstileConfig {
|
|
2
|
+
secretKey: string;
|
|
3
|
+
/** Expected action to validate against the Turnstile response. */
|
|
4
|
+
action?: string;
|
|
5
|
+
/** Expected hostnames where the challenge may be solved. */
|
|
6
|
+
expectedHostnames?: string[];
|
|
7
|
+
/** Timeout in milliseconds for the verification request. Defaults to 5000 ms. */
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
export type TurnstileResult = {
|
|
11
|
+
ok: true;
|
|
12
|
+
hostname?: string;
|
|
13
|
+
action?: string;
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
code: 'missing_token' | 'invalid_token' | 'hostname_mismatch' | 'action_mismatch' | 'unavailable';
|
|
17
|
+
errorCodes?: string[];
|
|
18
|
+
};
|
|
19
|
+
export declare function verifyTurnstile(token: string | undefined, config: TurnstileConfig, opts?: {
|
|
20
|
+
remoteIp?: string;
|
|
21
|
+
idempotencyKey?: string;
|
|
22
|
+
fetch?: typeof fetch;
|
|
23
|
+
}): Promise<TurnstileResult>;
|
|
24
|
+
//# sourceMappingURL=turnstile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnstile.d.ts","sourceRoot":"","sources":["../../src/security/turnstile.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAChD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,IAAI,EACA,eAAe,GACf,eAAe,GACf,mBAAmB,GACnB,iBAAiB,GACjB,aAAa,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB,CAAC;AAKN,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,MAAM,EAAE,eAAe,EACvB,IAAI,CAAC,EAAE;IACL,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB,GACA,OAAO,CAAC,eAAe,CAAC,CA+G1B"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const SITEVERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
3
|
+
export async function verifyTurnstile(token, config, opts) {
|
|
4
|
+
if (token === undefined || token === null || typeof token !== 'string' || token.trim() === '') {
|
|
5
|
+
return { ok: false, code: 'missing_token' };
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
const body = {
|
|
9
|
+
secret: config.secretKey,
|
|
10
|
+
response: token,
|
|
11
|
+
};
|
|
12
|
+
if (opts?.remoteIp !== undefined && opts.remoteIp !== '') {
|
|
13
|
+
body.remoteip = opts.remoteIp;
|
|
14
|
+
}
|
|
15
|
+
if (opts?.idempotencyKey !== undefined && opts.idempotencyKey !== '') {
|
|
16
|
+
body.idempotency_key = opts.idempotencyKey;
|
|
17
|
+
}
|
|
18
|
+
const fetchFn = opts?.fetch ?? globalThis.fetch;
|
|
19
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
20
|
+
let response;
|
|
21
|
+
try {
|
|
22
|
+
response = await fetchFn(SITEVERIFY_URL, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { ok: false, code: 'unavailable' };
|
|
33
|
+
}
|
|
34
|
+
let data;
|
|
35
|
+
try {
|
|
36
|
+
data = await response.json();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return { ok: false, code: 'unavailable' };
|
|
40
|
+
}
|
|
41
|
+
if (typeof data !== 'object' ||
|
|
42
|
+
data === null ||
|
|
43
|
+
Array.isArray(data) ||
|
|
44
|
+
typeof data.success !== 'boolean') {
|
|
45
|
+
return { ok: false, code: 'unavailable' };
|
|
46
|
+
}
|
|
47
|
+
const payload = data;
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
if (response.status >= 500 || payload.success !== false) {
|
|
50
|
+
return { ok: false, code: 'unavailable' };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!payload.success) {
|
|
54
|
+
const errorCodes = Array.isArray(payload['error-codes'])
|
|
55
|
+
? payload['error-codes'].map(String)
|
|
56
|
+
: undefined;
|
|
57
|
+
const isUnavailable = errorCodes?.includes('internal-error');
|
|
58
|
+
const result = {
|
|
59
|
+
ok: false,
|
|
60
|
+
code: isUnavailable ? 'unavailable' : 'invalid_token',
|
|
61
|
+
};
|
|
62
|
+
if (errorCodes !== undefined) {
|
|
63
|
+
result.errorCodes = errorCodes;
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
const returnedHostname = typeof payload.hostname === 'string' ? payload.hostname : undefined;
|
|
68
|
+
const returnedAction = typeof payload.action === 'string' ? payload.action : undefined;
|
|
69
|
+
if (config.expectedHostnames !== undefined) {
|
|
70
|
+
if (!returnedHostname ||
|
|
71
|
+
!config.expectedHostnames.some((expected) => expected.toLowerCase() === returnedHostname.toLowerCase())) {
|
|
72
|
+
return { ok: false, code: 'hostname_mismatch' };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (config.action !== undefined) {
|
|
76
|
+
if (returnedAction !== config.action) {
|
|
77
|
+
return { ok: false, code: 'action_mismatch' };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const result = { ok: true };
|
|
81
|
+
if (returnedHostname !== undefined) {
|
|
82
|
+
result.hostname = returnedHostname;
|
|
83
|
+
}
|
|
84
|
+
if (returnedAction !== undefined) {
|
|
85
|
+
result.action = returnedAction;
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return { ok: false, code: 'unavailable' };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=turnstile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnstile.js","sourceRoot":"","sources":["../../src/security/turnstile.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"visitor-context.d.ts","sourceRoot":"","sources":["../src/visitor-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGxD;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,kSACyP,CAAC;AAElS,yFAAyF;AACzF,eAAO,MAAM,yBAAyB,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"visitor-context.d.ts","sourceRoot":"","sources":["../src/visitor-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGxD;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,kSACyP,CAAC;AAElS,yFAAyF;AACzF,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAc9C,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,oBAAoB,EAAE,aAAa,GAAG,UAAU,CAAC,GAAG,MAAM,GAAG,IAAI,CA0B9G;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,EAAE,CAOjG;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD"}
|
package/dist/visitor-context.js
CHANGED
|
@@ -12,6 +12,7 @@ const TAG = 'visitor_context';
|
|
|
12
12
|
/** One line of plain text: no control characters, no line breaks, no tags that could close the block. */
|
|
13
13
|
function clean(value) {
|
|
14
14
|
return value
|
|
15
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: sanitize control characters
|
|
15
16
|
.replace(/[\u0000-\u001f\u007f]+/g, ' ')
|
|
16
17
|
.replace(new RegExp(`<\\s*/?\\s*${TAG}[^>]*>`, 'gi'), '')
|
|
17
18
|
.replace(/\s+/g, ' ')
|
|
@@ -26,6 +27,12 @@ export function buildVisitorContext(req) {
|
|
|
26
27
|
lines.push(`Page title: ${clean(req.pageContext.title)}`);
|
|
27
28
|
if (req.pageContext.description)
|
|
28
29
|
lines.push(`Page description: ${clean(req.pageContext.description)}`);
|
|
30
|
+
const added = Object.entries(req.pageContext.data ?? {}).filter(([, value]) => clean(value));
|
|
31
|
+
if (added.length > 0) {
|
|
32
|
+
lines.push('Details the page added (not verified):');
|
|
33
|
+
for (const [key, value] of added)
|
|
34
|
+
lines.push(`- ${clean(key)}: ${clean(value)}`);
|
|
35
|
+
}
|
|
29
36
|
}
|
|
30
37
|
const entries = Object.entries(req.userData ?? {}).filter(([, value]) => clean(value));
|
|
31
38
|
if (entries.length > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"visitor-context.js","sourceRoot":"","sources":["../src/visitor-context.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,2BAA2B,GACtC,+RAA+R,CAAC;AAElS,yFAAyF;AACzF,MAAM,CAAC,MAAM,yBAAyB,GAAG,IAAI,CAAC;AAE9C,MAAM,GAAG,GAAG,iBAAiB,CAAC;AAE9B,yGAAyG;AACzG,SAAS,KAAK,CAAC,KAAa;IAC1B,OAAO,KAAK;
|
|
1
|
+
{"version":3,"file":"visitor-context.js","sourceRoot":"","sources":["../src/visitor-context.ts"],"names":[],"mappings":"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"}
|
package/dist/webhook.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Logger, type WebhookConfig } from '@chatcops/core';
|
|
2
2
|
/**
|
|
3
3
|
* The JSON body of every delivery. `event` and `timestamp` repeat `type` and
|
|
4
4
|
* `created` for receivers written against the old format.
|
|
@@ -34,7 +34,8 @@ export declare function assertSafeWebhookUrl(rawUrl: string, allowPrivateNetwork
|
|
|
34
34
|
export declare class WebhookDispatcher {
|
|
35
35
|
private configs;
|
|
36
36
|
private timeoutMs;
|
|
37
|
-
|
|
37
|
+
private logger;
|
|
38
|
+
constructor(configs: WebhookConfig[], timeoutMs?: number, logger?: Logger);
|
|
38
39
|
dispatch(event: string, data: unknown): Promise<void>;
|
|
39
40
|
private send;
|
|
40
41
|
}
|
package/dist/webhook.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../src/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../src/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEhF;;;GAGG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO;IACvC,uFAAuF;IACvF,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,CAAC,CAAC;IACR,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAC/C,eAAO,MAAM,gBAAgB,yBAAyB,CAAC;AACvD,6EAA6E;AAC7E,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAKnD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,mBAAmB,UAAQ,GAAG,GAAG,CAiBrF;AAqCD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,SAAS,CAAS;IAE1B,OAAO,CAAC,MAAM,CAAS;gBAEX,OAAO,EAAE,aAAa,EAAE,EAAE,SAAS,GAAE,MAAmC,EAAE,MAAM,GAAE,MAAwB;IAShH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;YAkB7C,IAAI;CA8CnB;AAoBD;;;GAGG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,SAAgC,GAAG,OAAO,CAAC,MAAM,CAAC,CAE7H;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,OAAO,CAAC,CAkBnB"}
|
package/dist/webhook.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { consoleLogger } from '@chatcops/core';
|
|
1
2
|
export const DEFAULT_WEBHOOK_TIMEOUT_MS = 5000;
|
|
2
3
|
export const SIGNATURE_HEADER = 'X-ChatCops-Signature';
|
|
3
4
|
/** Signatures older than this are rejected by `verifyWebhook` by default. */
|
|
@@ -66,7 +67,9 @@ function isPrivateHost(hostname) {
|
|
|
66
67
|
export class WebhookDispatcher {
|
|
67
68
|
configs;
|
|
68
69
|
timeoutMs;
|
|
69
|
-
|
|
70
|
+
logger;
|
|
71
|
+
constructor(configs, timeoutMs = DEFAULT_WEBHOOK_TIMEOUT_MS, logger = consoleLogger()) {
|
|
72
|
+
this.logger = logger;
|
|
70
73
|
for (const config of configs) {
|
|
71
74
|
assertSafeWebhookUrl(config.url, config.allowPrivateNetwork);
|
|
72
75
|
}
|
|
@@ -126,7 +129,7 @@ export class WebhookDispatcher {
|
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
// Never log the body or the secret; they can hold lead details.
|
|
129
|
-
|
|
132
|
+
this.logger.error('webhook_failed', `Webhook delivery failed: ${envelope.type} (${envelope.id}) to ${new URL(config.url).origin}: ${lastProblem}`, { webhookEvent: envelope.type, deliveryId: envelope.id });
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
function parseRetryAfter(value) {
|