patchwork-os 0.2.0-beta.5 → 0.2.0-beta.5.canary.10
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 +2 -0
- package/dist/analyticsConfig.d.ts +29 -0
- package/dist/analyticsConfig.js +89 -0
- package/dist/analyticsConfig.js.map +1 -0
- package/dist/analyticsSend.d.ts +17 -1
- package/dist/analyticsSend.js +63 -5
- package/dist/analyticsSend.js.map +1 -1
- package/dist/bridge.js +9 -0
- package/dist/bridge.js.map +1 -1
- package/dist/commands/analytics.d.ts +8 -0
- package/dist/commands/analytics.js +134 -0
- package/dist/commands/analytics.js.map +1 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/connectors/googleCalendar.js +1 -1
- package/dist/connectors/googleCalendar.js.map +1 -1
- package/dist/httpBodyValidation.d.ts +41 -0
- package/dist/httpBodyValidation.js +45 -0
- package/dist/httpBodyValidation.js.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/patchworkConfig.d.ts +26 -1
- package/dist/patchworkConfig.js +15 -0
- package/dist/patchworkConfig.js.map +1 -1
- package/dist/recipeRoutes.js +16 -6
- package/dist/recipeRoutes.js.map +1 -1
- package/dist/server.js +196 -125
- package/dist/server.js.map +1 -1
- package/package.json +2 -1
package/dist/server.js
CHANGED
|
@@ -10,6 +10,7 @@ import { tryHandleConnectorRoute, tryHandlePublicConnectorRoute, } from "./conne
|
|
|
10
10
|
import { timingSafeStringEqual } from "./crypto.js";
|
|
11
11
|
import { renderDashboardHtml } from "./dashboard.js";
|
|
12
12
|
import { EnvLockedFlagError, getEnvLockedValue, isEnvLockedFor, isWriteKillSwitchActive, KILL_SWITCH_WRITES, setFlag, } from "./featureFlags.js";
|
|
13
|
+
import { respondIfUnknownBodyKeys } from "./httpBodyValidation.js";
|
|
13
14
|
import { respond500 } from "./httpErrorResponse.js";
|
|
14
15
|
import { tryHandleInboxRoute } from "./inboxRoutes.js";
|
|
15
16
|
import { tryHandleMcpRoute } from "./mcpRoutes.js";
|
|
@@ -1174,55 +1175,59 @@ export class Server extends EventEmitter {
|
|
|
1174
1175
|
try {
|
|
1175
1176
|
{
|
|
1176
1177
|
const body = parsed.value ?? {};
|
|
1178
|
+
if (respondIfUnknownBodyKeys(res, body, [
|
|
1179
|
+
"webhookUrl",
|
|
1180
|
+
"approvalGate",
|
|
1181
|
+
"enableTimeOfDayAnomaly",
|
|
1182
|
+
"driver",
|
|
1183
|
+
"model",
|
|
1184
|
+
"localEndpoint",
|
|
1185
|
+
"localModel",
|
|
1186
|
+
"apiKey",
|
|
1187
|
+
"pushServiceUrl",
|
|
1188
|
+
"pushServiceToken",
|
|
1189
|
+
"pushServiceBaseUrl",
|
|
1190
|
+
"ntfyTopic",
|
|
1191
|
+
"ntfyServer",
|
|
1192
|
+
])) {
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
// PHASE 1 — validate ALL fields up front. No disk writes, no
|
|
1196
|
+
// secure-store writes, no live-state mutations until every input
|
|
1197
|
+
// has passed. Prevents the "valid driver + invalid ntfyTopic"
|
|
1198
|
+
// class of bug where a 400 still leaves a partial side-effect on
|
|
1199
|
+
// disk.
|
|
1200
|
+
const respond400 = (error) => {
|
|
1201
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1202
|
+
res.end(JSON.stringify({ error }));
|
|
1203
|
+
};
|
|
1204
|
+
// webhookUrl
|
|
1177
1205
|
const hasWebhookUpdate = body.webhookUrl !== undefined;
|
|
1178
|
-
const
|
|
1206
|
+
const webhookRaw = hasWebhookUpdate
|
|
1179
1207
|
? (body.webhookUrl?.trim() ?? "")
|
|
1180
1208
|
: undefined;
|
|
1181
|
-
if (
|
|
1182
|
-
|
|
1183
|
-
|
|
1209
|
+
if (webhookRaw !== undefined &&
|
|
1210
|
+
webhookRaw !== "" &&
|
|
1211
|
+
!/^https:\/\/.+/.test(webhookRaw)) {
|
|
1212
|
+
respond400("webhookUrl must be HTTPS");
|
|
1184
1213
|
return;
|
|
1185
1214
|
}
|
|
1215
|
+
// approvalGate
|
|
1186
1216
|
const gateRaw = body.approvalGate;
|
|
1187
1217
|
if (gateRaw !== undefined &&
|
|
1188
1218
|
gateRaw !== "off" &&
|
|
1189
1219
|
gateRaw !== "high" &&
|
|
1190
1220
|
gateRaw !== "all") {
|
|
1191
|
-
|
|
1192
|
-
res.end(JSON.stringify({
|
|
1193
|
-
error: 'approvalGate must be "off", "high", or "all"',
|
|
1194
|
-
}));
|
|
1221
|
+
respond400('approvalGate must be "off", "high", or "all"');
|
|
1195
1222
|
return;
|
|
1196
1223
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
pushNotifications: cfg.dashboard?.pushNotifications ?? false,
|
|
1203
|
-
webhookUrl: hasWebhookUpdate
|
|
1204
|
-
? raw || undefined
|
|
1205
|
-
: cfg.dashboard?.webhookUrl,
|
|
1206
|
-
};
|
|
1207
|
-
if (gateRaw !== undefined) {
|
|
1208
|
-
cfg.approvalGate = gateRaw;
|
|
1209
|
-
this.approvalGate = gateRaw;
|
|
1210
|
-
}
|
|
1211
|
-
// h10 toggle: must be boolean if present. Persists to
|
|
1212
|
-
// ~/.patchwork/config.json AND live-mutates the Server
|
|
1213
|
-
// field so the next /approvals POST honors it without
|
|
1214
|
-
// needing a bridge restart.
|
|
1215
|
-
if (body.enableTimeOfDayAnomaly !== undefined) {
|
|
1216
|
-
if (typeof body.enableTimeOfDayAnomaly !== "boolean") {
|
|
1217
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1218
|
-
res.end(JSON.stringify({
|
|
1219
|
-
error: "enableTimeOfDayAnomaly must be a boolean",
|
|
1220
|
-
}));
|
|
1221
|
-
return;
|
|
1222
|
-
}
|
|
1223
|
-
cfg.enableTimeOfDayAnomaly = body.enableTimeOfDayAnomaly;
|
|
1224
|
-
this.enableTimeOfDayAnomaly = body.enableTimeOfDayAnomaly;
|
|
1224
|
+
// enableTimeOfDayAnomaly
|
|
1225
|
+
if (body.enableTimeOfDayAnomaly !== undefined &&
|
|
1226
|
+
typeof body.enableTimeOfDayAnomaly !== "boolean") {
|
|
1227
|
+
respond400("enableTimeOfDayAnomaly must be a boolean");
|
|
1228
|
+
return;
|
|
1225
1229
|
}
|
|
1230
|
+
// driver
|
|
1226
1231
|
const driverRaw = body.driver;
|
|
1227
1232
|
if (driverRaw !== undefined) {
|
|
1228
1233
|
const validDrivers = [
|
|
@@ -1236,26 +1241,11 @@ export class Server extends EventEmitter {
|
|
|
1236
1241
|
"none",
|
|
1237
1242
|
];
|
|
1238
1243
|
if (!validDrivers.includes(driverRaw)) {
|
|
1239
|
-
|
|
1240
|
-
res.end(JSON.stringify({
|
|
1241
|
-
error: `driver must be one of: ${validDrivers.join(", ")}`,
|
|
1242
|
-
}));
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
const driver = driverRaw;
|
|
1246
|
-
cfg.driver = driver;
|
|
1247
|
-
try {
|
|
1248
|
-
saveBridgeConfigDriver(driver, this.bridgeConfigPath);
|
|
1249
|
-
}
|
|
1250
|
-
catch (writeErr) {
|
|
1251
|
-
this.logger.error(`[/config/patchwork] saveBridgeConfigDriver failed: ${writeErr instanceof Error ? (writeErr.stack ?? writeErr.message) : String(writeErr)}`);
|
|
1252
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1253
|
-
res.end(JSON.stringify({
|
|
1254
|
-
error: "Failed to write bridge driver config",
|
|
1255
|
-
}));
|
|
1244
|
+
respond400(`driver must be one of: ${validDrivers.join(", ")}`);
|
|
1256
1245
|
return;
|
|
1257
1246
|
}
|
|
1258
1247
|
}
|
|
1248
|
+
// model
|
|
1259
1249
|
if (body.model !== undefined) {
|
|
1260
1250
|
const validModels = [
|
|
1261
1251
|
"claude",
|
|
@@ -1265,40 +1255,137 @@ export class Server extends EventEmitter {
|
|
|
1265
1255
|
"local",
|
|
1266
1256
|
];
|
|
1267
1257
|
if (!validModels.includes(body.model)) {
|
|
1268
|
-
|
|
1269
|
-
res.end(JSON.stringify({
|
|
1270
|
-
error: `model must be one of: ${validModels.join(", ")}`,
|
|
1271
|
-
}));
|
|
1258
|
+
respond400(`model must be one of: ${validModels.join(", ")}`);
|
|
1272
1259
|
return;
|
|
1273
1260
|
}
|
|
1274
|
-
cfg.model = body.model;
|
|
1275
|
-
if (body.model === "local") {
|
|
1276
|
-
if (body.localEndpoint !== undefined)
|
|
1277
|
-
cfg.localEndpoint = body.localEndpoint.trim() || undefined;
|
|
1278
|
-
if (body.localModel !== undefined)
|
|
1279
|
-
cfg.localModel = body.localModel.trim() || undefined;
|
|
1280
|
-
}
|
|
1281
1261
|
}
|
|
1262
|
+
// apiKey
|
|
1282
1263
|
if (body.apiKey) {
|
|
1283
1264
|
const { provider, key } = body.apiKey;
|
|
1284
1265
|
const validProviders = ["anthropic", "openai", "google", "xai"];
|
|
1285
1266
|
if (!validProviders.includes(provider) ||
|
|
1286
1267
|
typeof key !== "string") {
|
|
1287
|
-
|
|
1288
|
-
|
|
1268
|
+
respond400("Invalid apiKey provider or key");
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
// pushServiceUrl
|
|
1273
|
+
const pushUrlTrimmed = body.pushServiceUrl !== undefined
|
|
1274
|
+
? body.pushServiceUrl.trim()
|
|
1275
|
+
: undefined;
|
|
1276
|
+
if (pushUrlTrimmed !== undefined &&
|
|
1277
|
+
pushUrlTrimmed !== "" &&
|
|
1278
|
+
!pushUrlTrimmed.startsWith("https://")) {
|
|
1279
|
+
respond400("pushServiceUrl must be HTTPS");
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
// pushServiceBaseUrl — bridge callback origin embedded in SW
|
|
1283
|
+
// approveUrl/rejectUrl. http:// or attacker host = approval token
|
|
1284
|
+
// exfiltration. Validate before persisting.
|
|
1285
|
+
const pushBaseTrimmed = body.pushServiceBaseUrl !== undefined
|
|
1286
|
+
? body.pushServiceBaseUrl.trim()
|
|
1287
|
+
: undefined;
|
|
1288
|
+
if (pushBaseTrimmed !== undefined &&
|
|
1289
|
+
pushBaseTrimmed !== "" &&
|
|
1290
|
+
!pushBaseTrimmed.startsWith("https://")) {
|
|
1291
|
+
respond400("pushServiceBaseUrl must be HTTPS");
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
// ntfyTopic — bearer-token on public ntfy.sh; charset-restricted.
|
|
1295
|
+
const ntfyTopicTrimmed = body.ntfyTopic !== undefined ? body.ntfyTopic.trim() : undefined;
|
|
1296
|
+
if (ntfyTopicTrimmed !== undefined &&
|
|
1297
|
+
ntfyTopicTrimmed !== "" &&
|
|
1298
|
+
!/^[A-Za-z0-9_-]{1,64}$/.test(ntfyTopicTrimmed)) {
|
|
1299
|
+
respond400("ntfyTopic must match [A-Za-z0-9_-]{1,64}");
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
// ntfyServer — same threat model as pushServiceBaseUrl.
|
|
1303
|
+
const ntfyServerTrimmed = body.ntfyServer !== undefined
|
|
1304
|
+
? body.ntfyServer.trim()
|
|
1305
|
+
: undefined;
|
|
1306
|
+
if (ntfyServerTrimmed !== undefined &&
|
|
1307
|
+
ntfyServerTrimmed !== "" &&
|
|
1308
|
+
!ntfyServerTrimmed.startsWith("https://")) {
|
|
1309
|
+
respond400("ntfyServer must be HTTPS");
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
// PHASE 2 — load config and apply all in-memory edits to `cfg`.
|
|
1313
|
+
// Still no disk writes; if anything throws here the request
|
|
1314
|
+
// returns 500 with no side effects.
|
|
1315
|
+
const configPath = patchworkConfigPath();
|
|
1316
|
+
const cfg = loadPatchworkConfig(configPath);
|
|
1317
|
+
cfg.dashboard = {
|
|
1318
|
+
port: cfg.dashboard?.port ?? 3200,
|
|
1319
|
+
requireApproval: cfg.dashboard?.requireApproval ?? ["high"],
|
|
1320
|
+
pushNotifications: cfg.dashboard?.pushNotifications ?? false,
|
|
1321
|
+
webhookUrl: hasWebhookUpdate
|
|
1322
|
+
? webhookRaw || undefined
|
|
1323
|
+
: cfg.dashboard?.webhookUrl,
|
|
1324
|
+
};
|
|
1325
|
+
if (gateRaw !== undefined) {
|
|
1326
|
+
cfg.approvalGate = gateRaw;
|
|
1327
|
+
}
|
|
1328
|
+
if (body.enableTimeOfDayAnomaly !== undefined) {
|
|
1329
|
+
cfg.enableTimeOfDayAnomaly = body.enableTimeOfDayAnomaly;
|
|
1330
|
+
}
|
|
1331
|
+
if (driverRaw !== undefined) {
|
|
1332
|
+
cfg.driver = driverRaw;
|
|
1333
|
+
}
|
|
1334
|
+
if (body.model !== undefined) {
|
|
1335
|
+
cfg.model = body.model;
|
|
1336
|
+
}
|
|
1337
|
+
// localEndpoint / localModel persist regardless of whether
|
|
1338
|
+
// `model` is sent. Previously they were silently dropped when
|
|
1339
|
+
// the dashboard updated only the endpoint of an already-local
|
|
1340
|
+
// install, which left the running driver pointed at the old
|
|
1341
|
+
// host until the user happened to re-pick "Local LLM" in the
|
|
1342
|
+
// UI.
|
|
1343
|
+
if (body.localEndpoint !== undefined) {
|
|
1344
|
+
cfg.localEndpoint = body.localEndpoint.trim() || undefined;
|
|
1345
|
+
}
|
|
1346
|
+
if (body.localModel !== undefined) {
|
|
1347
|
+
cfg.localModel = body.localModel.trim() || undefined;
|
|
1348
|
+
}
|
|
1349
|
+
// Push / ntfy fields used to be set only on `this.*` and were
|
|
1350
|
+
// lost on bridge restart. Persist alongside the rest.
|
|
1351
|
+
if (pushUrlTrimmed !== undefined) {
|
|
1352
|
+
cfg.pushServiceUrl = pushUrlTrimmed || undefined;
|
|
1353
|
+
}
|
|
1354
|
+
if (body.pushServiceToken !== undefined) {
|
|
1355
|
+
cfg.pushServiceToken = body.pushServiceToken.trim() || undefined;
|
|
1356
|
+
}
|
|
1357
|
+
if (pushBaseTrimmed !== undefined) {
|
|
1358
|
+
cfg.pushServiceBaseUrl = pushBaseTrimmed || undefined;
|
|
1359
|
+
}
|
|
1360
|
+
if (ntfyTopicTrimmed !== undefined) {
|
|
1361
|
+
cfg.ntfyTopic = ntfyTopicTrimmed || undefined;
|
|
1362
|
+
}
|
|
1363
|
+
if (ntfyServerTrimmed !== undefined) {
|
|
1364
|
+
cfg.ntfyServer = ntfyServerTrimmed || undefined;
|
|
1365
|
+
}
|
|
1366
|
+
// PHASE 3 — disk + secure-store writes. Order: secure store →
|
|
1367
|
+
// bridge driver config → patchwork config. Each rolls back what
|
|
1368
|
+
// it can on later failure (left to PR #02; for now log + 500).
|
|
1369
|
+
if (body.apiKey) {
|
|
1370
|
+
try {
|
|
1371
|
+
saveApiKeyToSecureStore(body.apiKey.provider, body.apiKey.key);
|
|
1372
|
+
}
|
|
1373
|
+
catch (writeErr) {
|
|
1374
|
+
this.logger.error(`[/config/patchwork] saveApiKeyToSecureStore failed: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}`);
|
|
1375
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1376
|
+
res.end(JSON.stringify({ error: "Failed to write provider API key" }));
|
|
1289
1377
|
return;
|
|
1290
1378
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
// ~/.patchwork/config.json. Empty string clears.
|
|
1379
|
+
}
|
|
1380
|
+
if (driverRaw !== undefined) {
|
|
1294
1381
|
try {
|
|
1295
|
-
|
|
1382
|
+
saveBridgeConfigDriver(driverRaw, this.bridgeConfigPath);
|
|
1296
1383
|
}
|
|
1297
1384
|
catch (writeErr) {
|
|
1298
|
-
this.logger.error(`[/config/patchwork]
|
|
1385
|
+
this.logger.error(`[/config/patchwork] saveBridgeConfigDriver failed: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}`);
|
|
1299
1386
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1300
1387
|
res.end(JSON.stringify({
|
|
1301
|
-
error: "Failed to write
|
|
1388
|
+
error: "Failed to write bridge driver config",
|
|
1302
1389
|
}));
|
|
1303
1390
|
return;
|
|
1304
1391
|
}
|
|
@@ -1307,73 +1394,47 @@ export class Server extends EventEmitter {
|
|
|
1307
1394
|
savePatchworkConfig(cfg, configPath);
|
|
1308
1395
|
}
|
|
1309
1396
|
catch (writeErr) {
|
|
1310
|
-
this.logger.error(`[/config/patchwork] savePatchworkConfig failed: ${writeErr instanceof Error ?
|
|
1397
|
+
this.logger.error(`[/config/patchwork] savePatchworkConfig failed: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}`);
|
|
1311
1398
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1312
|
-
res.end(JSON.stringify({
|
|
1313
|
-
error: "Failed to write patchwork config",
|
|
1314
|
-
}));
|
|
1399
|
+
res.end(JSON.stringify({ error: "Failed to write patchwork config" }));
|
|
1315
1400
|
return;
|
|
1316
1401
|
}
|
|
1402
|
+
// PHASE 4 — live state. Only after every persistence step
|
|
1403
|
+
// succeeded; ensures live state never diverges from disk.
|
|
1404
|
+
if (gateRaw !== undefined) {
|
|
1405
|
+
this.approvalGate = gateRaw;
|
|
1406
|
+
}
|
|
1407
|
+
if (body.enableTimeOfDayAnomaly !== undefined) {
|
|
1408
|
+
this.enableTimeOfDayAnomaly = body.enableTimeOfDayAnomaly;
|
|
1409
|
+
}
|
|
1317
1410
|
if (hasWebhookUpdate) {
|
|
1318
|
-
this.approvalWebhookUrl =
|
|
1411
|
+
this.approvalWebhookUrl = webhookRaw || undefined;
|
|
1319
1412
|
}
|
|
1320
|
-
if (
|
|
1321
|
-
|
|
1322
|
-
if (pushUrl && !pushUrl.startsWith("https://")) {
|
|
1323
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1324
|
-
res.end(JSON.stringify({ error: "pushServiceUrl must be HTTPS" }));
|
|
1325
|
-
return;
|
|
1326
|
-
}
|
|
1327
|
-
this.pushServiceUrl = pushUrl || undefined;
|
|
1413
|
+
if (pushUrlTrimmed !== undefined) {
|
|
1414
|
+
this.pushServiceUrl = pushUrlTrimmed || undefined;
|
|
1328
1415
|
}
|
|
1329
1416
|
if (body.pushServiceToken !== undefined) {
|
|
1330
1417
|
this.pushServiceToken = body.pushServiceToken.trim() || undefined;
|
|
1331
1418
|
}
|
|
1332
|
-
if (
|
|
1333
|
-
|
|
1334
|
-
// pushServiceBaseUrl is the bridge callback origin embedded in
|
|
1335
|
-
// the SW's approveUrl/rejectUrl. If it can be set to plain
|
|
1336
|
-
// http:// or to a host the operator didn't intend, the SW will
|
|
1337
|
-
// POST the one-shot approvalToken there — letting an attacker
|
|
1338
|
-
// who sets this redirect every approval to attacker.tld and
|
|
1339
|
-
// replay tokens to the real bridge for silent auto-approve.
|
|
1340
|
-
if (baseUrl && !baseUrl.startsWith("https://")) {
|
|
1341
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1342
|
-
res.end(JSON.stringify({ error: "pushServiceBaseUrl must be HTTPS" }));
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
this.pushServiceBaseUrl = baseUrl || undefined;
|
|
1419
|
+
if (pushBaseTrimmed !== undefined) {
|
|
1420
|
+
this.pushServiceBaseUrl = pushBaseTrimmed || undefined;
|
|
1346
1421
|
}
|
|
1347
|
-
if (
|
|
1348
|
-
|
|
1349
|
-
// Topic acts as a bearer token on the public ntfy.sh server —
|
|
1350
|
-
// anyone subscribed sees the approval payload + single-use
|
|
1351
|
-
// approvalToken. Reject empty / whitespace / control chars to
|
|
1352
|
-
// avoid silent misconfiguration.
|
|
1353
|
-
if (topic && !/^[A-Za-z0-9_-]{1,64}$/.test(topic)) {
|
|
1354
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1355
|
-
res.end(JSON.stringify({
|
|
1356
|
-
error: "ntfyTopic must match [A-Za-z0-9_-]{1,64}",
|
|
1357
|
-
}));
|
|
1358
|
-
return;
|
|
1359
|
-
}
|
|
1360
|
-
this.ntfyTopic = topic || undefined;
|
|
1422
|
+
if (ntfyTopicTrimmed !== undefined) {
|
|
1423
|
+
this.ntfyTopic = ntfyTopicTrimmed || undefined;
|
|
1361
1424
|
}
|
|
1362
|
-
if (
|
|
1363
|
-
|
|
1364
|
-
// Same reasoning as pushServiceBaseUrl — the bridge sends the
|
|
1365
|
-
// single-use token to this URL. http:// would expose it on the
|
|
1366
|
-
// wire; a malicious value would exfiltrate every approval.
|
|
1367
|
-
if (server && !server.startsWith("https://")) {
|
|
1368
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1369
|
-
res.end(JSON.stringify({ error: "ntfyServer must be HTTPS" }));
|
|
1370
|
-
return;
|
|
1371
|
-
}
|
|
1372
|
-
this.ntfyServer = server || undefined;
|
|
1425
|
+
if (ntfyServerTrimmed !== undefined) {
|
|
1426
|
+
this.ntfyServer = ntfyServerTrimmed || undefined;
|
|
1373
1427
|
}
|
|
1428
|
+
// restartRequired covers fields the bridge only reads at boot:
|
|
1429
|
+
// driver/model/apiKey (env injection + driver factory), plus
|
|
1430
|
+
// localEndpoint/localModel which LocalApiDriver reads at
|
|
1431
|
+
// construction time. Push/ntfy fields are live-mutated on
|
|
1432
|
+
// `this.*` in PHASE 4 below, so they do not require restart.
|
|
1374
1433
|
const restartRequired = driverRaw !== undefined ||
|
|
1375
1434
|
body.apiKey !== undefined ||
|
|
1376
|
-
body.model !== undefined
|
|
1435
|
+
body.model !== undefined ||
|
|
1436
|
+
body.localEndpoint !== undefined ||
|
|
1437
|
+
body.localModel !== undefined;
|
|
1377
1438
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1378
1439
|
res.end(JSON.stringify({ ok: true, restartRequired }));
|
|
1379
1440
|
}
|
|
@@ -1432,6 +1493,9 @@ export class Server extends EventEmitter {
|
|
|
1432
1493
|
return;
|
|
1433
1494
|
}
|
|
1434
1495
|
const body = parsed.value ?? {};
|
|
1496
|
+
if (respondIfUnknownBodyKeys(res, body, ["engage", "reason"])) {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1435
1499
|
if (typeof body.engage !== "boolean") {
|
|
1436
1500
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1437
1501
|
res.end(JSON.stringify({
|
|
@@ -1534,6 +1598,13 @@ export class Server extends EventEmitter {
|
|
|
1534
1598
|
return;
|
|
1535
1599
|
}
|
|
1536
1600
|
const body = parsed.value ?? {};
|
|
1601
|
+
if (respondIfUnknownBodyKeys(res, body, [
|
|
1602
|
+
"crashReports",
|
|
1603
|
+
"usageStats",
|
|
1604
|
+
"localDiagnostics",
|
|
1605
|
+
])) {
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1537
1608
|
const update = {};
|
|
1538
1609
|
if (typeof body.crashReports === "boolean") {
|
|
1539
1610
|
update.crashReports = body.crashReports;
|