hotsheet 0.7.0 → 0.9.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/dist/cli.js CHANGED
@@ -519,10 +519,17 @@ async function isChannelAlive(dataDir2) {
519
519
  async function triggerChannel(dataDir2, serverPort, message) {
520
520
  const port2 = getChannelPort(dataDir2);
521
521
  if (!port2) return false;
522
+ let secretHeader = "";
523
+ try {
524
+ const { readFileSettings: readFileSettings2 } = await Promise.resolve().then(() => (init_file_settings(), file_settings_exports));
525
+ const settings = readFileSettings2(dataDir2);
526
+ if (settings.secret) secretHeader = ` -H "X-Hotsheet-Secret: ${settings.secret}"`;
527
+ } catch {
528
+ }
522
529
  const doneSignal = `
523
530
 
524
531
  When you are completely finished (or if there was nothing to do), signal completion by running:
525
- curl -s -X POST http://localhost:${serverPort}/api/channel/done`;
532
+ curl -s -X POST http://localhost:${serverPort}/api/channel/done${secretHeader}`;
526
533
  const content = message ? message + doneSignal : "Process the Hot Sheet worklist. Run /hotsheet to work through the current Up Next items." + doneSignal;
527
534
  try {
528
535
  const res = await fetch(`http://127.0.0.1:${port2}/trigger`, {
@@ -545,7 +552,7 @@ var init_channel_config = __esm({
545
552
  // src/cli.ts
546
553
  import { mkdirSync as mkdirSync6 } from "fs";
547
554
  import { tmpdir } from "os";
548
- import { join as join12, resolve as resolve3 } from "path";
555
+ import { join as join12, resolve as resolve4 } from "path";
549
556
 
550
557
  // src/backup.ts
551
558
  init_connection();
@@ -2387,7 +2394,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
2387
2394
  // src/routes/api.ts
2388
2395
  import { existsSync as existsSync7, mkdirSync as mkdirSync4, rmSync as rmSync5 } from "fs";
2389
2396
  import { Hono } from "hono";
2390
- import { basename, extname, join as join9, relative as relative2 } from "path";
2397
+ import { basename, extname, join as join9, relative as relative2, resolve as resolve3 } from "path";
2391
2398
 
2392
2399
  // src/skills.ts
2393
2400
  init_file_settings();
@@ -2879,8 +2886,8 @@ function notifyChange() {
2879
2886
  changeVersion++;
2880
2887
  const waiters = pollWaiters;
2881
2888
  pollWaiters = [];
2882
- for (const resolve4 of waiters) {
2883
- resolve4(changeVersion);
2889
+ for (const resolve5 of waiters) {
2890
+ resolve5(changeVersion);
2884
2891
  }
2885
2892
  }
2886
2893
  apiRoutes.get("/poll", async (c) => {
@@ -2889,11 +2896,11 @@ apiRoutes.get("/poll", async (c) => {
2889
2896
  return c.json({ version: changeVersion });
2890
2897
  }
2891
2898
  const version = await Promise.race([
2892
- new Promise((resolve4) => {
2893
- pollWaiters.push(resolve4);
2899
+ new Promise((resolve5) => {
2900
+ pollWaiters.push(resolve5);
2894
2901
  }),
2895
- new Promise((resolve4) => {
2896
- setTimeout(() => resolve4(changeVersion), 3e4);
2902
+ new Promise((resolve5) => {
2903
+ setTimeout(() => resolve5(changeVersion), 3e4);
2897
2904
  })
2898
2905
  ]);
2899
2906
  return c.json({ version });
@@ -3133,7 +3140,11 @@ apiRoutes.post("/attachments/:id/reveal", async (c) => {
3133
3140
  apiRoutes.get("/attachments/file/*", async (c) => {
3134
3141
  const filePath = c.req.path.replace("/api/attachments/file/", "");
3135
3142
  const dataDir2 = c.get("dataDir");
3136
- const fullPath = join9(dataDir2, "attachments", filePath);
3143
+ const attachDir = resolve3(join9(dataDir2, "attachments"));
3144
+ const fullPath = resolve3(join9(attachDir, filePath));
3145
+ if (!fullPath.startsWith(attachDir + "/") && fullPath !== attachDir) {
3146
+ return c.json({ error: "Invalid path" }, 403);
3147
+ }
3137
3148
  if (!existsSync7(fullPath)) {
3138
3149
  return c.json({ error: "File not found" }, 404);
3139
3150
  }
@@ -3206,7 +3217,8 @@ apiRoutes.patch("/settings", async (c) => {
3206
3217
  apiRoutes.get("/file-settings", async (c) => {
3207
3218
  const { readFileSettings: readFileSettings2 } = await Promise.resolve().then(() => (init_file_settings(), file_settings_exports));
3208
3219
  const dataDir2 = c.get("dataDir");
3209
- return c.json(readFileSettings2(dataDir2));
3220
+ const { secret, secretPathHash, port: port2, ...safe } = readFileSettings2(dataDir2);
3221
+ return c.json(safe);
3210
3222
  });
3211
3223
  apiRoutes.patch("/file-settings", async (c) => {
3212
3224
  const { writeFileSettings: writeFileSettings2 } = await Promise.resolve().then(() => (init_file_settings(), file_settings_exports));
@@ -3837,7 +3849,7 @@ pageRoutes.get("/", (c) => {
3837
3849
  /* @__PURE__ */ jsx("div", { id: "detail-attachments", className: "detail-attachments" }),
3838
3850
  /* @__PURE__ */ jsx("label", { className: "btn btn-sm upload-btn", children: [
3839
3851
  "Attach File",
3840
- /* @__PURE__ */ jsx("input", { type: "file", id: "detail-file-input", style: "display:none" })
3852
+ /* @__PURE__ */ jsx("input", { type: "file", id: "detail-file-input", style: "display:none", multiple: true })
3841
3853
  ] })
3842
3854
  ] }),
3843
3855
  /* @__PURE__ */ jsx("div", { className: "detail-field detail-field-full", id: "detail-notes-section", children: [
@@ -3862,7 +3874,7 @@ pageRoutes.get("/", (c) => {
3862
3874
  /* @__PURE__ */ jsx("kbd", { children: "Enter" }),
3863
3875
  " new ticket"
3864
3876
  ] }),
3865
- /* @__PURE__ */ jsx("span", { children: [
3877
+ /* @__PURE__ */ jsx("span", { "data-hint": "category", children: [
3866
3878
  /* @__PURE__ */ jsx("kbd", { children: [
3867
3879
  "\u2318",
3868
3880
  "I/B/F/R/K/G"
@@ -3958,7 +3970,10 @@ pageRoutes.get("/", (c) => {
3958
3970
  /* @__PURE__ */ jsx("div", { className: "settings-tab-panel active", "data-panel": "general", children: [
3959
3971
  /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
3960
3972
  /* @__PURE__ */ jsx("label", { children: "App name" }),
3961
- /* @__PURE__ */ jsx("input", { type: "text", id: "settings-app-name", placeholder: "Hot Sheet" }),
3973
+ /* @__PURE__ */ jsx("div", { className: "settings-app-name-row", children: [
3974
+ /* @__PURE__ */ jsx("button", { className: "app-icon-picker-btn", id: "app-icon-picker-btn", title: "Change app icon", children: /* @__PURE__ */ jsx("img", { id: "app-icon-preview", src: "/static/assets/icon-default.png", width: "28", height: "28" }) }),
3975
+ /* @__PURE__ */ jsx("input", { type: "text", id: "settings-app-name", placeholder: "Hot Sheet" })
3976
+ ] }),
3962
3977
  /* @__PURE__ */ jsx("span", { className: "settings-hint", id: "settings-app-name-hint", children: "Custom name shown in the title bar. Leave empty for default." })
3963
3978
  ] }),
3964
3979
  /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
@@ -4062,10 +4077,10 @@ pageRoutes.get("/", (c) => {
4062
4077
 
4063
4078
  // src/server.ts
4064
4079
  function tryServe(fetch2, port2) {
4065
- return new Promise((resolve4, reject) => {
4080
+ return new Promise((resolve5, reject) => {
4066
4081
  const server = serve({ fetch: fetch2, port: port2 });
4067
4082
  server.on("listening", () => {
4068
- resolve4(port2);
4083
+ resolve5(port2);
4069
4084
  });
4070
4085
  server.on("error", (err) => {
4071
4086
  reject(err);
@@ -4098,15 +4113,33 @@ async function startServer(port2, dataDir2, options) {
4098
4113
  return new Response(content, { headers: { "Content-Type": mimeTypes[ext || ""] || "application/octet-stream", "Cache-Control": "max-age=86400" } });
4099
4114
  });
4100
4115
  app.use("/api/*", async (c, next) => {
4116
+ const settings = readFileSettings(dataDir2);
4117
+ const expectedSecret = settings.secret;
4118
+ if (!expectedSecret) {
4119
+ await next();
4120
+ return;
4121
+ }
4101
4122
  const headerSecret = c.req.header("X-Hotsheet-Secret");
4123
+ const method = c.req.method;
4124
+ const isMutation = method === "POST" || method === "PATCH" || method === "PUT" || method === "DELETE";
4102
4125
  if (headerSecret) {
4103
- const settings = readFileSettings(dataDir2);
4104
- if (settings.secret && headerSecret !== settings.secret) {
4126
+ if (headerSecret !== expectedSecret) {
4105
4127
  return c.json({
4106
4128
  error: "Secret mismatch \u2014 you may be connecting to the wrong Hot Sheet instance.",
4107
4129
  recovery: "Re-read .hotsheet/settings.json to get the correct port and secret, and re-read your skill files (e.g. .claude/skills/hotsheet/SKILL.md) for updated instructions."
4108
4130
  }, 403);
4109
4131
  }
4132
+ } else if (isMutation) {
4133
+ const origin = c.req.header("Origin");
4134
+ const referer = c.req.header("Referer");
4135
+ const localhostPattern = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?(\/|$)/;
4136
+ const isSameOrigin = origin && localhostPattern.test(origin) || referer && localhostPattern.test(referer);
4137
+ if (!isSameOrigin) {
4138
+ return c.json({
4139
+ error: "Missing X-Hotsheet-Secret header. Read .hotsheet/settings.json for the correct port and secret.",
4140
+ recovery: "Re-read .hotsheet/settings.json to get the correct port and secret, and re-read your skill files for updated instructions."
4141
+ }, 403);
4142
+ }
4110
4143
  }
4111
4144
  await next();
4112
4145
  });
@@ -4186,10 +4219,10 @@ function isFirstUseToday() {
4186
4219
  return last !== today;
4187
4220
  }
4188
4221
  function fetchLatestVersion() {
4189
- return new Promise((resolve4) => {
4222
+ return new Promise((resolve5) => {
4190
4223
  const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
4191
4224
  if (res.statusCode !== 200) {
4192
- resolve4(null);
4225
+ resolve5(null);
4193
4226
  return;
4194
4227
  }
4195
4228
  let data = "";
@@ -4198,18 +4231,18 @@ function fetchLatestVersion() {
4198
4231
  });
4199
4232
  res.on("end", () => {
4200
4233
  try {
4201
- resolve4(JSON.parse(data).version);
4234
+ resolve5(JSON.parse(data).version);
4202
4235
  } catch {
4203
- resolve4(null);
4236
+ resolve5(null);
4204
4237
  }
4205
4238
  });
4206
4239
  });
4207
4240
  req.on("error", () => {
4208
- resolve4(null);
4241
+ resolve5(null);
4209
4242
  });
4210
4243
  req.on("timeout", () => {
4211
4244
  req.destroy();
4212
- resolve4(null);
4245
+ resolve5(null);
4213
4246
  });
4214
4247
  });
4215
4248
  }
@@ -4311,7 +4344,7 @@ function parseArgs(argv) {
4311
4344
  }
4312
4345
  break;
4313
4346
  case "--data-dir":
4314
- dataDir2 = resolve3(args[++i]);
4347
+ dataDir2 = resolve4(args[++i]);
4315
4348
  break;
4316
4349
  case "--check-for-updates":
4317
4350
  forceUpdateCheck = true;