@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.
Files changed (81) hide show
  1. package/README.md +4 -4
  2. package/dist/adapters/ai-sdk.d.ts +12 -0
  3. package/dist/adapters/ai-sdk.d.ts.map +1 -0
  4. package/dist/adapters/ai-sdk.js +27 -0
  5. package/dist/adapters/ai-sdk.js.map +1 -0
  6. package/dist/adapters/cloudflare.d.ts.map +1 -1
  7. package/dist/adapters/cloudflare.js +9 -83
  8. package/dist/adapters/cloudflare.js.map +1 -1
  9. package/dist/adapters/express.d.ts +6 -2
  10. package/dist/adapters/express.d.ts.map +1 -1
  11. package/dist/adapters/express.js +102 -30
  12. package/dist/adapters/express.js.map +1 -1
  13. package/dist/adapters/index.d.ts +1 -0
  14. package/dist/adapters/index.d.ts.map +1 -1
  15. package/dist/adapters/index.js +1 -0
  16. package/dist/adapters/index.js.map +1 -1
  17. package/dist/adapters/vercel.d.ts.map +1 -1
  18. package/dist/adapters/vercel.js +7 -74
  19. package/dist/adapters/vercel.js.map +1 -1
  20. package/dist/client-ip.d.ts +37 -0
  21. package/dist/client-ip.d.ts.map +1 -0
  22. package/dist/client-ip.js +57 -0
  23. package/dist/client-ip.js.map +1 -0
  24. package/dist/config.d.ts +136 -8
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +15 -1
  27. package/dist/config.js.map +1 -1
  28. package/dist/examples/cloudflare-worker.js +1 -1
  29. package/dist/examples/cloudflare-worker.js.map +1 -1
  30. package/dist/handler.d.ts +52 -1
  31. package/dist/handler.d.ts.map +1 -1
  32. package/dist/handler.js +417 -136
  33. package/dist/handler.js.map +1 -1
  34. package/dist/http.d.ts +69 -0
  35. package/dist/http.d.ts.map +1 -0
  36. package/dist/http.js +262 -0
  37. package/dist/http.js.map +1 -0
  38. package/dist/index.d.ts +4 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/knowledge.d.ts +18 -0
  43. package/dist/knowledge.d.ts.map +1 -0
  44. package/dist/knowledge.js +32 -0
  45. package/dist/knowledge.js.map +1 -0
  46. package/dist/rate-limit.d.ts +5 -0
  47. package/dist/rate-limit.d.ts.map +1 -1
  48. package/dist/rate-limit.js +5 -0
  49. package/dist/rate-limit.js.map +1 -1
  50. package/dist/security/origin-policy.d.ts +19 -0
  51. package/dist/security/origin-policy.d.ts.map +1 -0
  52. package/dist/security/origin-policy.js +60 -0
  53. package/dist/security/origin-policy.js.map +1 -0
  54. package/dist/security/origin.d.ts +5 -0
  55. package/dist/security/origin.d.ts.map +1 -0
  56. package/dist/security/origin.js +47 -0
  57. package/dist/security/origin.js.map +1 -0
  58. package/dist/security/session-token.d.ts +65 -0
  59. package/dist/security/session-token.d.ts.map +1 -0
  60. package/dist/security/session-token.js +338 -0
  61. package/dist/security/session-token.js.map +1 -0
  62. package/dist/security/sessions.d.ts +72 -0
  63. package/dist/security/sessions.d.ts.map +1 -0
  64. package/dist/security/sessions.js +216 -0
  65. package/dist/security/sessions.js.map +1 -0
  66. package/dist/security/startup-checks.d.ts +45 -0
  67. package/dist/security/startup-checks.d.ts.map +1 -0
  68. package/dist/security/startup-checks.js +124 -0
  69. package/dist/security/startup-checks.js.map +1 -0
  70. package/dist/security/turnstile.d.ts +24 -0
  71. package/dist/security/turnstile.d.ts.map +1 -0
  72. package/dist/security/turnstile.js +93 -0
  73. package/dist/security/turnstile.js.map +1 -0
  74. package/dist/visitor-context.d.ts.map +1 -1
  75. package/dist/visitor-context.js +7 -0
  76. package/dist/visitor-context.js.map +1 -1
  77. package/dist/webhook.d.ts +3 -2
  78. package/dist/webhook.d.ts.map +1 -1
  79. package/dist/webhook.js +5 -2
  80. package/dist/webhook.js.map +1 -1
  81. 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":"AAAA,OAAO,EAAE,aAAa,EAAe,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAA0C,MAAM,gBAAgB,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,WAAW,EAAE,aAAa,EAAE,sBAAsB,EAAsB,MAAM,oBAAoB,CAAC;AAC/I,OAAO,EAAE,eAAe,EAAwB,MAAM,gBAAgB,CAAC;AAEvE;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,sFAAsF;AACtF,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAC1D,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAC5B,MAAM,aAAa,GAAG,GAAG,CAAC;AAqC1B;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAqB,EACrB,SAAgE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAC/F,MAAoB,IAAI,CAAC,GAAG;IAE5B,MAAM,OAAO,GAAG,MAAM,CAAC,cAAc,CAAC;IACtC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,IAAI,MAAM,CAAC,cAAc;YAAE,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAC;QAC3G,IAAI,MAAM,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,sFAAsF,CAAC,CAAC;QAC9H,IAAI,MAAM,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;QAC3G,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,cAAc,GAAG,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC/C,IAAI,MAAM,CAAC,QAAQ,IAAI,CAAC,OAAO,cAAc,KAAK,QAAQ,IAAI,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,UAAU,GAAG,gBAAgB,CAAC,EAAE,CAAC;QACtI,MAAM,IAAI,KAAK,CAAC,sCAAsC,gBAAgB,kCAAkC,CAAC,CAAC;IAC5G,CAAC;IACD,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;IACnC,IAAI,SAAS,IAAI,CAAC,SAAS,CAAC,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IAC7F,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACpH,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QAC5B,MAAM,KAAK,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3F,IAAI,KAAK,GAAG,gBAAgB,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,QAAQ,KAAK,6BAA6B,gBAAgB,kDAAkD,CAAC,CAAC;QACnJ,CAAC;IACH,CAAC,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,MAAM,CAAC,aAAa,IAAI,uBAAuB,CAAC;IAC/D,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAE7H,4EAA4E;IAC5E,IAAI,IAAsC,CAAC;IAC3C,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAChG,IAAI,WAA2C,CAAC;IAChD,MAAM,cAAc,GAAG,GAAG,EAAE,CAAC,CAAC,WAAW,KAAK,gBAAgB,CAAC,cAAwB,CAAC,CAAC,CAAC;IAC1F,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAG,MAAM,CAAC,cAAc,IAAI,IAAI,CAAC;IAE/C,OAAO;QACL,QAAQ;QAER,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ;YAC/B,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;YACtC,IAAI,CAAC,OAAO;gBAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,iBAAiB,EAAE,EAAE,OAAO,EAAE,yBAAyB,EAAE,CAAC,EAAE,CAAC;YACjH,MAAM,GAAG,GAAG,MAAM,IAAI,EAAE,CAAC;YACzB,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC;YAEtB,qEAAqE;YACrE,mEAAmE;YACnE,oEAAoE;YACpE,wEAAwE;YACxE,yEAAyE;YACzE,sDAAsD;YACtD,IAAI,OAAkC,CAAC;YACvC,IAAI,GAAuB,CAAC;YAC5B,IAAI,OAAO,CAAC,cAAc,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;gBACpD,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,aAAa,EAAE,MAAM,OAAO,EAAE,EAAE;oBACzE,MAAM,EAAE,GAAG;oBACX,cAAc,EAAE,OAAO,CAAC,cAAc;oBACtC,GAAG,EAAE,IAAI;oBACT,YAAY,EAAE,wBAAwB;iBACvC,CAAC,CAAC;gBACH,IAAI,MAAM,CAAC,EAAE;oBAAE,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC;YACzC,CAAC;YACD,uEAAuE;YACvE,oEAAoE;YACpE,IAAI,IAA4B,CAAC;YACjC,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;gBACpC,IAAI,CAAC,cAAc;oBAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,iBAAiB,EAAE,EAAE,OAAO,EAAE,6CAA6C,EAAE,CAAC,EAAE,CAAC;gBAC5I,IAAI,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,cAAc,EAAE,EAAE,IAAI,CAAC,CAAC;gBAC9E,IAAI,CAAC,IAAI;oBAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,kBAAkB,EAAE,EAAE,OAAO,EAAE,0CAA0C,EAAE,CAAC,EAAE,CAAC;YAClI,CAAC;YACD,+CAA+C;YAC/C,IAAI,OAAO,EAAE,GAAG,IAAI,OAAO,CAAC,GAAG,KAAK,IAAI,EAAE,GAAG;gBAAE,OAAO,GAAG,SAAS,CAAC;YAEnE,uEAAuE;YACvE,+BAA+B;YAC/B,IAAI,SAAS,IAAI,CAAC,OAAO,EAAE,CAAC;gBAC1B,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,cAAc,EAAE,SAAS,EAAE;oBACtE,QAAQ,EAAE,QAAQ;oBAClB,cAAc,EAAE,OAAO,CAAC,cAAc;iBACvC,CAAC,CAAC;gBACH,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;oBACf,IAAI,MAAM,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;wBAClC,CAAC,MAAM,CAAC,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,IAAI,CAAC,uBAAuB,EAAE,iEAAiE,CAAC,CAAC;wBACpI,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,kBAAkB,EAAE,EAAE,OAAO,EAAE,4CAA4C,EAAE,CAAC,EAAE,CAAC;oBACzH,CAAC;oBACD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,kBAAkB,EAAE,EAAE,OAAO,EAAE,6BAA6B,EAAE,CAAC,EAAE,CAAC;gBAC1G,CAAC;YACH,CAAC;YAED,IAAI,OAAO;gBAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;iBAC1B,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;gBAChC,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;gBAClD,iEAAiE;gBACjE,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG,CAAC;oBAAE,GAAG,GAAG,OAAO,CAAC,cAAc,CAAC;YACnF,CAAC;YAED,MAAM,MAAM,GAAkB;gBAC5B,CAAC,EAAE,CAAC;gBACJ,GAAG,EAAE,OAAO,EAAE,GAAG,IAAI,MAAM,CAAC,UAAU,EAAE;gBACxC,GAAG,EAAE,GAAG,IAAI,MAAM,CAAC,UAAU,EAAE;gBAC/B,GAAG;gBACH,GAAG,EAAE,IAAI;gBACT,GAAG,EAAE,IAAI,GAAG,MAAM;gBAClB,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC9E,CAAC;YACF,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,MAAM,EAAE,MAAM,OAAO,EAAE,CAAC,CAAC;YACzD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,GAAG,IAAI,EAAE,cAAc,EAAE,MAAM,CAAC,GAAG,EAAE,EAAE,CAAC;QACpG,CAAC;QAED,KAAK,CAAC,KAAK,CAAC,aAAa,EAAE,MAAM,EAAE,cAAc;YAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC;YACpC,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,OAAO,QAAQ;oBACb,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,8DAA8D,EAAE;oBAClH,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;YACnB,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,MAAM,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,IAAI,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;YACpH,IAAI,MAAM,CAAC,EAAE;gBAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;YAC1D,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;gBACpB,KAAK,SAAS;oBACZ,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,kEAAkE,EAAE,CAAC;gBAC3H,KAAK,uBAAuB;oBAC1B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,uBAAuB,EAAE,OAAO,EAAE,oDAAoD,EAAE,CAAC;gBACrH;oBACE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,iEAAiE,EAAE,CAAC;YAC/H,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,MAAM,CAAC,MAAiC;IAC/C,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IACjD,MAAM,KAAK,GAAG,sBAAsB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAClD,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC;AAYD,SAAS,eAAe,CAAC,IAAa;IACpC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IACnD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACtE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,EAAE,SAAS,EAAE,GAAG,IAA+B,CAAC;IACrH,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,MAAM,GAAG,IAAI;YAAE,OAAO,SAAS,CAAC;QAC/E,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAChC,CAAC;IACD,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;QACjC,IAAI,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,CAAC,MAAM,GAAG,IAAI;YAAE,OAAO,SAAS,CAAC;QACzF,OAAO,CAAC,cAAc,GAAG,cAAc,CAAC;IAC1C,CAAC;IACD,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;QACjC,IAAI,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,CAAC,MAAM,GAAG,GAAG;YAAE,OAAO,SAAS,CAAC;QACxF,OAAO,CAAC,cAAc,GAAG,cAAc,CAAC;IAC1C,CAAC;IACD,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;QACjC,IAAI,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,IAAI,cAAc,CAAC,MAAM,GAAG,aAAa;YAAE,OAAO,SAAS,CAAC;QACjI,OAAO,CAAC,cAAc,GAAG,cAAc,CAAC;IAC1C,CAAC;IACD,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;QAChC,IAAI,OAAO,aAAa,KAAK,QAAQ,IAAI,aAAa,CAAC,MAAM,GAAG,IAAI;YAAE,OAAO,SAAS,CAAC;QACvF,OAAO,CAAC,aAAa,GAAG,aAAa,CAAC;IACxC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAOD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,KAAa,EAAE,GAAc,EAAE,MAAc;IACjF,IAAI,CAAC,CAAC,MAAM,sBAAsB,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACpE,IAAI,OAAgC,CAAC;IACrC,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,OAAO,IAAI,EAAE,CAAC;IACzC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,GAAG;QAAE,OAAO,SAAS,CAAC;IACtF,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,EAAE,GAAG,MAAM;QAAE,OAAO,SAAS,CAAC;IAC5F,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QAAE,OAAO,SAAS,CAAC;IAC3F,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AAChE,CAAC"}
@@ -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;AAa9C,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,oBAAoB,EAAE,aAAa,GAAG,UAAU,CAAC,GAAG,MAAM,GAAG,IAAI,CAqB9G;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"}
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"}
@@ -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":"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"}
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 { WebhookConfig } from '@chatcops/core';
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
- constructor(configs: WebhookConfig[], timeoutMs?: number);
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
  }
@@ -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;AAEpD;;;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;gBAEd,OAAO,EAAE,aAAa,EAAE,EAAE,SAAS,GAAE,MAAmC;IAQ9E,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;YAkB7C,IAAI;CA4CnB;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"}
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
- constructor(configs, timeoutMs = DEFAULT_WEBHOOK_TIMEOUT_MS) {
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
- console.error(`[chatcops] Webhook delivery failed: ${envelope.type} (${envelope.id}) to ${new URL(config.url).origin}: ${lastProblem}`);
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) {