ework-aio 0.2.5 → 0.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-aio",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "All-in-one installer for ework (issue tracker) + ework-daemon (AI bridge) + opencode-ework (plugin). One command: npm i -g ework-aio && ework-aio install.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli.ts CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  type ConfigArgs,
36
36
  } from "./commands/config.ts";
37
37
 
38
- const VERSION = "0.2.5-dev";
38
+ const VERSION = "0.2.6-dev";
39
39
 
40
40
  const USAGE = `ework-aio <command> [options]
41
41
 
@@ -357,6 +357,25 @@ export async function runInstall(
357
357
  await patchEnvKey(paths.daemonEnvFile, "GITEA_TOKEN", botToken);
358
358
  }
359
359
 
360
+ // 10.5 Reconcile webhook secret across web and daemon .env files.
361
+ // Pre-v0.2.6 installs generated two independent hex(20) values for
362
+ // WORK_DAEMON_WEBHOOK_SECRET (web) and GITEA_WEBHOOK_SECRET (daemon),
363
+ // so signature verification always failed. Forward-fill won't fix this
364
+ // because both keys already exist. We treat web as the source of truth
365
+ // (ework-web's DB has webhook rows signed with whatever
366
+ // WORK_DAEMON_WEBHOOK_SECRET was at creation time) and overwrite daemon.
367
+ {
368
+ const webSecret = await readEnvKey(paths.webEnvFile, "WORK_DAEMON_WEBHOOK_SECRET");
369
+ const daemonSecret = await readEnvKey(paths.daemonEnvFile, "GITEA_WEBHOOK_SECRET");
370
+ if (webSecret && daemonSecret && webSecret !== daemonSecret) {
371
+ await patchEnvKey(paths.daemonEnvFile, "GITEA_WEBHOOK_SECRET", webSecret);
372
+ logger.warn(`webhook secret mismatch — overwrote daemon GITEA_WEBHOOK_SECRET to match web`);
373
+ } else if (webSecret && !daemonSecret) {
374
+ await patchEnvKey(paths.daemonEnvFile, "GITEA_WEBHOOK_SECRET", webSecret);
375
+ logger.ok(`propagated WORK_DAEMON_WEBHOOK_SECRET → GITEA_WEBHOOK_SECRET`);
376
+ }
377
+ }
378
+
360
379
  // 11. (Systemd only) Write + install daemon unit.
361
380
  if (opts.useSystemd && paths.daemonUnitFile && systemdOk && !opts.noStart) {
362
381
  const userInfo = os.userInfo();
package/src/config.ts CHANGED
@@ -38,6 +38,26 @@ export interface EnvKeySpec {
38
38
 
39
39
  const hex = (bytes: number): string => randomBytes(bytes).toString("hex");
40
40
 
41
+ // ework-web signs outgoing webhook POSTs with WORK_DAEMON_WEBHOOK_SECRET
42
+ // and ework-daemon verifies them with GITEA_WEBHOOK_SECRET. They MUST be
43
+ // the same value — pre-v0.2.6 each key had an independent hex(20) generator,
44
+ // so the two secrets never matched and every webhook was rejected with
45
+ // "invalid signature" on the daemon side.
46
+ //
47
+ // The closure-memoized `sharedWebhookSecret` returns the same value across
48
+ // both calls within a single install run, so first install is correct.
49
+ // Reconciliation for already-broken installs lives in install.ts (it reads
50
+ // both .env files and, if they differ, overwrites the daemon side from the
51
+ // web side — web is the source of truth because ework-web's DB has webhooks
52
+ // signed with whatever WORK_DAEMON_WEBHOOK_SECRET was at creation time).
53
+ let _sharedWebhookSecret: string | null = null;
54
+ function sharedWebhookSecret(): string {
55
+ if (_sharedWebhookSecret === null) {
56
+ _sharedWebhookSecret = hex(20);
57
+ }
58
+ return _sharedWebhookSecret;
59
+ }
60
+
41
61
  export const WEB_ENV_KEYS: readonly EnvKeySpec[] = [
42
62
  { envVar: "WORK_PORT", file: "web", generate: (c) => String(c.workPort) },
43
63
  { envVar: "WORK_HOST", file: "web", generate: () => "127.0.0.1" },
@@ -48,9 +68,13 @@ export const WEB_ENV_KEYS: readonly EnvKeySpec[] = [
48
68
  { envVar: "WORK_DB_PATH", file: "web", generate: (c) => c.paths.webDbPath },
49
69
  { envVar: "WORK_ATTACHMENT_ROOT", file: "web", generate: (c) => c.paths.webAttachmentRoot },
50
70
  { envVar: "WORK_FILE_ROOTS", file: "web", generate: (c) => `/tmp,${c.paths.dataDir}` },
71
+ // Without this, ework-web falls back to /tmp/ework-access.log which is
72
+ // owned by whichever user touched it first. On shared boxes the runtime
73
+ // user can't append → "EACCES: permission denied" on every request.
74
+ { envVar: "WORK_ACCESS_LOG", file: "web", generate: (c) => `${c.paths.runDir}/web-access.log` },
51
75
  { envVar: "WORK_DAEMON_BOT_LOGIN", file: "web", generate: (c) => c.botName },
52
76
  { envVar: "WORK_DAEMON_WEBHOOK_URL", file: "web", generate: (c) => `http://127.0.0.1:${c.daemonPort}` },
53
- { envVar: "WORK_DAEMON_WEBHOOK_SECRET", file: "web", secret: true, generate: () => hex(20) },
77
+ { envVar: "WORK_DAEMON_WEBHOOK_SECRET", file: "web", secret: true, generate: sharedWebhookSecret },
54
78
  ] as const;
55
79
 
56
80
  export const DAEMON_ENV_KEYS: readonly EnvKeySpec[] = [
@@ -62,7 +86,7 @@ export const DAEMON_ENV_KEYS: readonly EnvKeySpec[] = [
62
86
  // These tokens come from the bot bootstrap flow — empty placeholder here,
63
87
  // filled in by write_daemon_env after PAT is minted.
64
88
  { envVar: "GITEA_TOKEN", file: "daemon", secret: true, generate: () => "" },
65
- { envVar: "GITEA_WEBHOOK_SECRET", file: "daemon", secret: true, generate: (c) => hex(20) },
89
+ { envVar: "GITEA_WEBHOOK_SECRET", file: "daemon", secret: true, generate: sharedWebhookSecret },
66
90
  { envVar: "BOT_USERNAME", file: "daemon", generate: (c) => c.botName },
67
91
  { envVar: "BOT_TOKEN", file: "daemon", secret: true, generate: () => "" },
68
92
  { envVar: "OPENCODE_BINARY", file: "daemon", generate: (c) => c.opencodeBin },