Skip to content

Commit 7bfb910

Browse files
committed
sentinel: Add hotpatch and GitHub webhook handling
1 parent e04c2f8 commit 7bfb910

7 files changed

Lines changed: 72 additions & 15 deletions

File tree

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"no-console": "error",
4949
"no-restricted-imports": ["error", { "patterns": [".*"] }],
5050
"no-unreachable": "error",
51-
"no-unused-expressions": "error",
51+
"no-unused-expressions": ["error", { "allowTaggedTemplates": true }],
5252
"no-var": "error",
5353
"prefer-const": [
5454
"error",

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"express": "^5.0.1",
3232
"flat-cache": "^6.1.1",
3333
"mongoose": "^8.12.1",
34-
"ps-client": "^4.5.0",
34+
"ps-client": "^4.5.2",
3535
"react": "^18.2.0",
3636
"react-dom": "^18.2.0",
3737
"tailwindcss": "^4.0.8",

src/eval.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as _cache from '@/cache';
66
import * as _Tools from '@/tools';
77
import { ansiToHtml } from '@/utils/ansiToHtml';
88
import { cachebuster as _cachebuster } from '@/utils/cachebuster';
9+
import { $ as _$ } from '@/utils/child_process';
910
import { fsPath as _fsPath } from '@/utils/fsPath';
1011
import { log as _log } from '@/utils/logger';
1112

@@ -21,9 +22,10 @@ const fsPath = _fsPath;
2122
const log = _log;
2223
const path = _path;
2324
const Tools = _Tools;
25+
const $ = _$;
2426

2527
// Storing in context for eval()
26-
const _evalContext = [cache, cachebuster, fs, fsSync, fsPath, log, path, Tools];
28+
const _evalContext = [cache, cachebuster, fs, fsSync, fsPath, log, path, Tools, $];
2729

2830
export type EvalModes = 'COLOR_OUTPUT' | 'FULL_OUTPUT' | 'ABBR_OUTPUT' | 'NO_OUTPUT';
2931
export type EvalOutput = {

src/sentinel/hotpatch.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { promises as fs } from 'fs';
2+
import path from 'path';
3+
import { update } from 'ps-client/tools';
4+
5+
import { registers } from '@/sentinel/registers';
6+
import { $ } from '@/utils/child_process';
7+
import { fsPath } from '@/utils/fsPath';
8+
import { errorLog, log } from '@/utils/logger';
9+
10+
export type HotpatchType = 'code' | 'data' | string;
11+
12+
export async function hotpatch(hotpatchType: HotpatchType, by: string | symbol): Promise<void> {
13+
if (!hotpatchType) throw new TypeError('Missing hotpatchType');
14+
try {
15+
// Hardcoded variants
16+
switch (hotpatchType) {
17+
case 'code': {
18+
$`git pull`;
19+
break;
20+
}
21+
case 'data': {
22+
await update();
23+
// TODO: cachebust
24+
break;
25+
}
26+
default:
27+
const register = registers.list.find(register => register.label === hotpatchType);
28+
if (register) {
29+
const allFiles = (await fs.readdir(fsPath(), { recursive: true, withFileTypes: true }))
30+
.filter(entry => entry.isFile())
31+
.map(entry => path.join(entry.parentPath, entry.name));
32+
await register.reload(allFiles.filter(file => register.pattern.test(file)));
33+
}
34+
}
35+
log(`${hotpatchType} was hotpatched ${typeof by === 'symbol' ? `(${Symbol.keyFor(by) ?? '-'})` : `by ${by}`}`);
36+
} catch (error) {
37+
if (error instanceof Error) {
38+
log('Failed to hotpatch', hotpatchType, by, error.message);
39+
errorLog(error);
40+
}
41+
throw error;
42+
}
43+
}

src/utils/child_process.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { execSync } from 'child_process';
2+
3+
import type { ExecSyncOptionsWithStringEncoding } from 'child_process';
4+
5+
export function $(input: TemplateStringsArray | string, options?: ExecSyncOptionsWithStringEncoding): string {
6+
const command = typeof input === 'string' ? input : input.raw.join(' ');
7+
return execSync(command, { encoding: 'utf8', ...options });
8+
}

src/web/api/github.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
import crypto from 'crypto';
22

3-
import { log } from '@/utils/logger';
3+
import { hotpatch } from '@/sentinel/hotpatch';
44
import { WebError } from '@/utils/webError';
55

66
import type { RequestHandler } from 'express';
77

88
export const verb = 'post';
99

10-
function gitHubHash(body: unknown): string {
11-
return crypto
10+
function gitHubHash(body: string): string {
11+
const hash = crypto
1212
.createHmac('sha256', process.env.WEB_GITHUB_SECRET ?? 'No key provided.')
13-
.update(JSON.stringify(body))
13+
.update(body)
1414
.digest('hex');
15+
return `sha256=${hash}`;
1516
}
1617

17-
export const handler: RequestHandler = async (req, _res) => {
18+
export const handler: RequestHandler = async (req, res) => {
1819
const signature = req.header('X-Hub-Signature-256');
19-
const checksum = gitHubHash(req.body);
20-
log({ req, body: req.body, checksum, signature });
20+
const checksum = gitHubHash(JSON.stringify(req.body));
2121
if (!signature) throw new WebError('Signature not provided.');
22+
// Not going to bother with protecting against timing attacks
23+
// TODO: Maybe use crypto.timingSafeEqual? Honestly just looks way too cluttered
2224
if (signature !== checksum) throw new WebError('Signature invalid.');
23-
throw new WebError('Not added yet');
25+
26+
await hotpatch('code', Symbol.for('GitHub'));
27+
res.send('Code updated.');
2428
};

0 commit comments

Comments
 (0)