@zoulabo/line-hive 0.1.20 → 0.1.21

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 CHANGED
@@ -13,7 +13,7 @@
13
13
  <a href="https://www.npmjs.com/package/@zoulabo/line-hive"><img src="https://img.shields.io/npm/v/@zoulabo/line-hive?color=cb3837&label=npm" alt="npm version"></a>
14
14
  <img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen" alt="Node.js ≥18">
15
15
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License"></a>
16
- <img src="https://img.shields.io/badge/tests-246%20passing-brightgreen" alt="246 tests passing">
16
+ <img src="https://img.shields.io/badge/tests-339%20passing-brightgreen" alt="339 tests passing">
17
17
  </p>
18
18
 
19
19
  <p align="center">
@@ -34,7 +34,8 @@
34
34
  - 🔍 **PDF intelligence** — text extraction, scanned PDF→image rendering, per-page navigation
35
35
  - 🌐 **Multi-language** — English and Japanese, auto-detected from LINE profile
36
36
  - 🔧 **Zero config tunnel** — built-in ngrok management (or bring your own tunnel)
37
- - 🛡️ **Security hardened** — PDF JS disabled, workspace-scoped file access, no shell injection
37
+ - 🛡️ **Security hardened** — workspace-scoped file access, path containment, sandboxed PDF parsing
38
+ - 🔐 **Access control** — first user is auto-registered as primary admin; restricted mode activates on follow or status check, blocking other users by default
38
39
 
39
40
  ## Contents
40
41
 
@@ -266,7 +267,7 @@ npx @zoulabo/line-hive init --editor cursor # switch editor type
266
267
  | `line_check_messages` | Fetch unclaimed inbound messages | No |
267
268
  | `line_list_agents` | List all active agents across editor windows | No |
268
269
  | `line_read_file` | Read content from a received file (PDF text, scanned PDF→image, image resize, text decode) | No |
269
- | `line_render_markdown` | Convert Markdown to HTML (built-in, no library install needed) | No |
270
+ | `line_render_markdown` | Convert Markdown HTML with auto-injected CDN (Mermaid, highlight.js, KaTeX). TTL-managed. | No |
270
271
 
271
272
  ### Recommended Pattern
272
273
 
@@ -304,8 +305,8 @@ Auto-detected from the user's LINE profile. Currently **English** (default) and
304
305
 
305
306
  - **Push quota** — free tier: 200/month. Status queries via `?` use reply tokens (free). Send `?` periodically to replenish the token pool.
306
307
  - **Reply token expiry** — ~15 min. Falls back to push when exhausted.
307
- - **Files** — receive images & files (stored as base64 in SQLite). Send via `imageBase64`, `imageFilePath`, or `filePath`. Stickers, video, and audio are currently ignored.
308
- - **Single user** — designed for one developer per LINE channel.
308
+ - **Files** — receive images & files (stored on disk in `.line-hive-tmp/incoming/`, 24h TTL). Send via `imageBase64`, `imageFilePath`, or `filePath`. Served files have a configurable TTL (default 2h, max 24h) with auto-cleanup. Stickers are currently ignored; video and audio are stored as files.
309
+ - **Single user** — designed for one primary user (admin) per LINE channel. The first follow or status check auto-activates restricted mode; additional users require manual allowlisting via store methods.
309
310
  - **Single webhook URL** — all agent instances share one tunnel endpoint.
310
311
  - **Multi-workspace** — only the first editor claims the webhook port/tunnel. Others piggyback via shared SQLite.
311
312
  - **Claude Code** — `line_ask` can block up to 2 hours; ensure `MCP_TOOL_TIMEOUT` is not too short. Always use `--editor claude-code` for init.
@@ -350,7 +351,7 @@ For standalone `npm start`:
350
351
 
351
352
  Designed for **AI-agent-driven development**. The repo includes skill files (`.github/skills/`) that teach agents the codebase. Point your agent at the repo and let it work.
352
353
 
353
- - `npm test` must pass (~246 tests, <1s)
354
+ - `npm test` must pass (~339 tests, <1s)
354
355
  - `npx tsc --noEmit` must be clean
355
356
  - Let the agents read skills first, then write the code
356
357
 
@@ -400,6 +401,7 @@ Don't use `--editor vscode` — that's for GitHub Copilot.
400
401
  - **SQLite** (WAL mode) shared across all agent instances at `~/.line-hive/line-hive.db`
401
402
  - **Heartbeat** every 10s — dead agents auto-cleaned after 2 min
402
403
  - **Reply token pooling** — webhook saves tokens for free replies (saves push quota)
404
+ - **TTL-managed file serving** — images, HTML, and documents served from disk with configurable TTL (default 2h, max 24h); expired files auto-cleaned every 60s
403
405
  - **Session correlation** — `line_ask` creates a session, webhook fulfills it on reply
404
406
  - **Multi-agent routing** — user selects agent via quick reply buttons, or auto-routes to `needs_input` agent
405
407
  - **Embedded tunnel** — ngrok auto-managed as child process (optional; any tunnel to localhost:19780 works)
package/dist/constants.js CHANGED
@@ -10,6 +10,12 @@ export const MAX_TIMEOUT_MS = 86400000; // 24 hours
10
10
  export const POLL_INTERVAL_MS = 2000;
11
11
  // Reply token pool
12
12
  export const REPLY_TOKEN_MAX_AGE_MS = 900000; // 15 min
13
+ // Served file TTL
14
+ export const DEFAULT_FILE_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours
15
+ export const MAX_FILE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
16
+ // Incoming media disk storage
17
+ export const INCOMING_MEDIA_DIR = ".line-hive-tmp/incoming";
13
18
  // Config keys
14
19
  export const CONFIG_TARGETED_AGENT = "targeted_agent_id";
20
+ export const CONFIG_ALLOWED_USERS = "allowed_users";
15
21
  //# sourceMappingURL=constants.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,kBAAkB;AAClB,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAEzC,mBAAmB;AACnB,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAClC,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC,CAAC,WAAW;AAEnD,wCAAwC;AACxC,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAErC,mBAAmB;AACnB,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC,CAAC,SAAS;AAEvD,cAAc;AACd,MAAM,CAAC,MAAM,qBAAqB,GAAG,mBAAmB,CAAC"}
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,kBAAkB;AAClB,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAEzC,mBAAmB;AACnB,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAClC,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC,CAAC,WAAW;AAEnD,wCAAwC;AACxC,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAErC,mBAAmB;AACnB,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC,CAAC,SAAS;AAEvD,kBAAkB;AAClB,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAW,UAAU;AAC3E,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAS,WAAW;AAEvE,8BAA8B;AAC9B,MAAM,CAAC,MAAM,kBAAkB,GAAG,yBAAyB,CAAC;AAE5D,cAAc;AACd,MAAM,CAAC,MAAM,qBAAqB,GAAG,mBAAmB,CAAC;AACzD,MAAM,CAAC,MAAM,oBAAoB,GAAG,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -82,8 +82,8 @@ async function main() {
82
82
  webhookServer = null;
83
83
  });
84
84
  }
85
- catch {
86
- // Port still in use — will retry on next heartbeat
85
+ catch (err) {
86
+ logger.debug({ error: err instanceof Error ? err.message : err }, "Port still in use — will retry on next heartbeat");
87
87
  }
88
88
  }
89
89
  // Initial attempt to claim the port
@@ -147,9 +147,39 @@ async function main() {
147
147
  }
148
148
  }, config.heartbeatIntervalMs);
149
149
  const cleanupTimer = setInterval(() => {
150
- messageStore.cleanup();
150
+ const incomingMediaPaths = messageStore.cleanup();
151
151
  messageStore.expireSessions(Date.now());
152
152
  messageStore.cleanupDeadAgentSessions(config.heartbeatTimeoutMs);
153
+ // Clean up incoming media files from disk (returned by cleanup())
154
+ let incomingDeleted = 0;
155
+ for (const diskPath of incomingMediaPaths) {
156
+ try {
157
+ const fullPath = path.resolve(diskPath);
158
+ if (fs.existsSync(fullPath)) {
159
+ fs.unlinkSync(fullPath);
160
+ incomingDeleted++;
161
+ }
162
+ }
163
+ catch { /* ignore cleanup errors */ }
164
+ }
165
+ if (incomingDeleted > 0) {
166
+ logger.info({ count: incomingDeleted }, "Cleaned up incoming media files");
167
+ }
168
+ // Clean up expired served files from disk
169
+ const expiredFiles = messageStore.cleanupExpiredServedFiles();
170
+ const serveDir = path.resolve(".line-hive-tmp");
171
+ for (const filename of expiredFiles) {
172
+ try {
173
+ const filePath = path.join(serveDir, filename);
174
+ if (fs.existsSync(filePath)) {
175
+ fs.unlinkSync(filePath);
176
+ }
177
+ }
178
+ catch { /* ignore cleanup errors */ }
179
+ }
180
+ if (expiredFiles.length > 0) {
181
+ logger.info({ count: expiredFiles.length }, "Cleaned up expired served files");
182
+ }
153
183
  }, config.cleanupIntervalMs);
154
184
  let shuttingDown = false;
155
185
  const shutdown = () => {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,10 @@
1
1
  import http from "http";
2
2
  import crypto from "crypto";
3
+ import fs from "fs";
4
+ import path from "path";
3
5
  import { buildDeleteQueueReply, errorMessage, parsePostbackData, textWithQuickReply, textWithQueueQuickReply, formatTimeAgo, STATUS_ICONS, getAgentDisplayName, } from "./messages.js";
4
6
  import { sendWithTokenPool } from "../util/sendWithTokenPool.js";
5
- import { CONFIG_TARGETED_AGENT } from "../constants.js";
7
+ import { CONFIG_TARGETED_AGENT, INCOMING_MEDIA_DIR } from "../constants.js";
6
8
  import { t, resolveLocale } from "../i18n.js";
7
9
  export function verifySignature(secret, body, signature) {
8
10
  if (!secret || !signature) {
@@ -42,6 +44,18 @@ async function resolveUserLocale(deps, userId) {
42
44
  }
43
45
  return "en";
44
46
  }
47
+ /**
48
+ * Register the user as default (if first user) and auto-enable restricted mode.
49
+ * When the first user interacts with the bot and no allowed_users list exists yet,
50
+ * this switches from open mode to restricted mode with the first user auto-allowed.
51
+ */
52
+ function registerAndAutoAllow(deps, userId) {
53
+ const isFirst = deps.messageStore.registerDefaultUserIfMissing(userId);
54
+ if (isFirst && deps.messageStore.getAllowedUsers().length === 0) {
55
+ deps.messageStore.addAllowedUser(userId);
56
+ deps.logger?.info({ userId: userId.slice(0, 8) }, "First user auto-allowed, restricted mode enabled");
57
+ }
58
+ }
45
59
  /** Format a single agent's status into a line of text */
46
60
  export function formatAgentStatus(agent, locale = "en") {
47
61
  const m = t(locale);
@@ -136,8 +150,8 @@ async function replyViaPool(deps, replyToken, message, eventTimestamp) {
136
150
  }
137
151
  }
138
152
  /**
139
- * Download an image preview from the LINE Content API and return as base64.
140
- * Uses the /preview endpoint (~240×240, ~30KB) to keep payloads small.
153
+ * Download an image from the LINE Content API at full resolution.
154
+ * Uses the full content endpoint (not preview) to get the original image.
141
155
  * For external images (forwarded), fetches from the original URL directly.
142
156
  */
143
157
  async function downloadImagePreview(deps, event) {
@@ -154,7 +168,8 @@ async function downloadImagePreview(deps, event) {
154
168
  url = provider.originalContentUrl;
155
169
  }
156
170
  else if (messageId) {
157
- url = `https://api-data.line.me/v2/bot/message/${messageId}/content/preview`;
171
+ // Full-resolution content endpoint (not /preview)
172
+ url = `https://api-data.line.me/v2/bot/message/${messageId}/content`;
158
173
  }
159
174
  else {
160
175
  return null;
@@ -168,16 +183,15 @@ async function downloadImagePreview(deps, event) {
168
183
  deps.logger?.warn({ status: res.status, messageId }, "Image download failed");
169
184
  return null;
170
185
  }
171
- // Limit download size to 5MB to prevent memory exhaustion
186
+ // Limit download size to 10MB to prevent memory exhaustion
172
187
  const contentLength = res.headers.get("content-length");
173
- if (contentLength && parseInt(contentLength, 10) > 5 * 1024 * 1024) {
188
+ if (contentLength && parseInt(contentLength, 10) > 10 * 1024 * 1024) {
174
189
  deps.logger?.warn({ contentLength }, "Image too large, skipping download");
175
190
  return null;
176
191
  }
177
192
  const buffer = Buffer.from(await res.arrayBuffer());
178
- const base64 = buffer.toString("base64");
179
193
  const mimeType = res.headers.get("content-type") || "image/jpeg";
180
- return { base64, mimeType };
194
+ return { buffer, mimeType };
181
195
  }
182
196
  catch (error) {
183
197
  deps.logger?.warn({ error }, "Image download error");
@@ -209,10 +223,9 @@ async function downloadFileContent(deps, event) {
209
223
  return null;
210
224
  }
211
225
  const buffer = Buffer.from(await res.arrayBuffer());
212
- const base64 = buffer.toString("base64");
213
226
  const mimeType = res.headers.get("content-type") || "application/octet-stream";
214
227
  return {
215
- base64,
228
+ buffer,
216
229
  mimeType,
217
230
  fileName: event.message?.fileName,
218
231
  fileSize: event.message?.fileSize,
@@ -223,6 +236,36 @@ async function downloadFileContent(deps, event) {
223
236
  return null;
224
237
  }
225
238
  }
239
+ /**
240
+ * Write downloaded media to disk under `.line-hive-tmp/incoming/`.
241
+ * Returns the relative disk path (from project root) for storage in media_json.
242
+ *
243
+ * Naming: `{lineMessageId}-{sanitizedFilename}` or `{lineMessageId}.{ext}`
244
+ */
245
+ function writeIncomingMedia(lineMessageId, buffer, mimeType, fileName) {
246
+ const dir = path.resolve(INCOMING_MEDIA_DIR);
247
+ fs.mkdirSync(dir, { recursive: true });
248
+ // Derive extension from filename or mime type
249
+ const mimeToExt = {
250
+ "image/jpeg": ".jpg", "image/png": ".png", "image/gif": ".gif",
251
+ "image/webp": ".webp", "audio/mpeg": ".mp3", "audio/m4a": ".m4a",
252
+ "audio/ogg": ".ogg", "video/mp4": ".mp4", "application/pdf": ".pdf",
253
+ };
254
+ let safeName;
255
+ if (fileName) {
256
+ // Sanitize user-provided filename: keep alphanumeric, dots, hyphens, underscores
257
+ safeName = fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
258
+ }
259
+ else {
260
+ const ext = mimeToExt[mimeType] || "";
261
+ safeName = `${lineMessageId}${ext}`;
262
+ }
263
+ const diskName = `${lineMessageId}-${safeName}`;
264
+ const diskPath = path.join(dir, diskName);
265
+ fs.writeFileSync(diskPath, buffer);
266
+ // Return path relative to project root (for storage in media_json)
267
+ return path.join(INCOMING_MEDIA_DIR, diskName);
268
+ }
226
269
  async function handleFileEvent(deps, event, userId) {
227
270
  const replyToken = event.replyToken;
228
271
  const webhookEventId = getWebhookEventId(event);
@@ -230,7 +273,17 @@ async function handleFileEvent(deps, event, userId) {
230
273
  const fileSize = event.message?.fileSize;
231
274
  // Download file content immediately (LINE auto-deletes content after a period)
232
275
  const media = await downloadFileContent(deps, event);
233
- const mediaJson = media ? JSON.stringify(media) : null;
276
+ let mediaJson = null;
277
+ if (media) {
278
+ const lineMessageId = event.message?.id ?? webhookEventId;
279
+ const diskPath = writeIncomingMedia(lineMessageId, media.buffer, media.mimeType, media.fileName);
280
+ mediaJson = JSON.stringify({
281
+ mimeType: media.mimeType,
282
+ diskPath,
283
+ fileName: media.fileName,
284
+ fileSize: media.fileSize,
285
+ });
286
+ }
234
287
  const locale = await resolveUserLocale(deps, userId);
235
288
  const chatTargetedAgentId = deps.messageStore.getConfig(CONFIG_TARGETED_AGENT);
236
289
  const sizeStr = fileSize ? ` (${Math.round(fileSize / 1024)}KB)` : "";
@@ -261,7 +314,12 @@ async function handleImageEvent(deps, event, userId) {
261
314
  const webhookEventId = getWebhookEventId(event);
262
315
  // Download image preview immediately (LINE auto-deletes content after a period)
263
316
  const media = await downloadImagePreview(deps, event);
264
- const mediaJson = media ? JSON.stringify({ mimeType: media.mimeType, base64: media.base64 }) : null;
317
+ let mediaJson = null;
318
+ if (media) {
319
+ const lineMessageId = event.message?.id ?? webhookEventId;
320
+ const diskPath = writeIncomingMedia(lineMessageId, media.buffer, media.mimeType);
321
+ mediaJson = JSON.stringify({ mimeType: media.mimeType, diskPath });
322
+ }
265
323
  // Resolve user locale
266
324
  const locale = await resolveUserLocale(deps, userId);
267
325
  // For chat messages, use targeted routing if user has selected an agent
@@ -277,7 +335,7 @@ async function handleImageEvent(deps, event, userId) {
277
335
  }, { targetedAgentId: chatTargetedAgentId });
278
336
  if (messageResult.deduped)
279
337
  return;
280
- // Auto-register userId
338
+ // Auto-register userId (no auto-allow for image events)
281
339
  deps.messageStore.registerDefaultUserIfMissing(userId);
282
340
  if (!replyToken)
283
341
  return;
@@ -331,8 +389,13 @@ async function handleTextEvent(deps, event, text, userId) {
331
389
  if (isControlCommand && messageResult.messageId) {
332
390
  deps.messageStore.markMessageConsumed(messageResult.messageId);
333
391
  }
334
- // Auto-register userId from incoming webhook (DB-only, no env/file writes)
335
- deps.messageStore.registerDefaultUserIfMissing(userId);
392
+ // Auto-register: only auto-allow on status command ("?"), not on chat/other
393
+ if (command.type === "status") {
394
+ registerAndAutoAllow(deps, userId);
395
+ }
396
+ else {
397
+ deps.messageStore.registerDefaultUserIfMissing(userId);
398
+ }
336
399
  if (!replyToken) {
337
400
  return;
338
401
  }
@@ -356,7 +419,7 @@ async function handlePostbackEvent(deps, event, data, userId) {
356
419
  const replyToken = event.replyToken;
357
420
  if (!replyToken)
358
421
  return;
359
- // Auto-register userId if needed
422
+ // Auto-register userId (no auto-allow for postback events)
360
423
  deps.messageStore.registerDefaultUserIfMissing(userId);
361
424
  // Resolve user locale
362
425
  const locale = await resolveUserLocale(deps, userId);
@@ -600,7 +663,8 @@ async function handleCommandReply(deps, command, replyToken, messageId, eventTim
600
663
  deps.logger?.info({ targetAgent: agentDisplayName }, "Chat routed to targeted agent");
601
664
  // If agent is actively waiting (has active session), save token silently —
602
665
  // agent will respond within seconds via line_ask polling.
603
- if (deps.messageStore.hasWaitingSession()) {
666
+ // Scope to THIS agent to avoid stale sessions from dead agents suppressing acks.
667
+ if (deps.messageStore.getWaitingSessionForAgent(targetAgent.agentId)) {
604
668
  deps.messageStore.saveReplyToken(replyToken, eventTimestamp ?? Date.now());
605
669
  return;
606
670
  }
@@ -645,6 +709,11 @@ export async function handleWebhook(deps, body, signature) {
645
709
  const userId = event.source?.userId;
646
710
  if (!userId)
647
711
  continue;
712
+ // Access control: reject messages from unauthorized users
713
+ if (!deps.messageStore.isUserAllowed(userId)) {
714
+ deps.logger?.info({ userId: userId.slice(0, 8) }, "Blocked message from unauthorized user");
715
+ continue;
716
+ }
648
717
  if (event.type === "message" && event.message?.type === "text") {
649
718
  const text = event.message?.text ?? "";
650
719
  await handleTextEvent(deps, event, text, userId);
@@ -667,6 +736,14 @@ export async function handleWebhook(deps, body, signature) {
667
736
  const removed = deps.messageStore.deleteByLineMessageId(event.unsend.messageId);
668
737
  deps.logger?.info({ lineMessageId: event.unsend.messageId, removed }, "Unsend: message deleted");
669
738
  }
739
+ else if (event.type === "follow") {
740
+ // New user added bot as friend — auto-register and auto-allow
741
+ registerAndAutoAllow(deps, userId);
742
+ // Save reply token if available
743
+ if (event.replyToken) {
744
+ deps.messageStore.saveReplyToken(event.replyToken, event.timestamp);
745
+ }
746
+ }
670
747
  }
671
748
  return { statusCode: 200, body: "OK" };
672
749
  }
@@ -681,37 +758,65 @@ export function startWebhookServer(deps) {
681
758
  const prefix = url.startsWith("/files/") ? "/files/" : "/images/";
682
759
  // Strip query string and decode percent-encoding for clean ID extraction
683
760
  const rawId = url.slice(prefix.length).split("?")[0];
684
- // Strip file extension (e.g. "abc123.html" → "abc123") for store lookup
685
- const imageId = decodeURIComponent(rawId).replace(/\.[^.]+$/, "");
686
- if (!imageId || imageId.includes("/") || imageId.includes("..")) {
761
+ const decodedRawId = decodeURIComponent(rawId);
762
+ if (!decodedRawId || decodedRawId.includes("/") || decodedRawId.includes("..")) {
687
763
  res.statusCode = 404;
688
764
  res.end("Not found");
689
765
  return;
690
766
  }
691
- const image = deps.messageStore.getOutgoingImage(imageId);
692
- if (!image) {
693
- res.statusCode = 404;
694
- res.end("Not found");
767
+ // Check TTL — all disk-served files MUST be tracked.
768
+ // Untracked files on disk return 404 (strict mode).
769
+ // Race: cleanup may delete between check and readFile → 404 (acceptable).
770
+ const fileExpiry = deps.messageStore.getServedFileExpiry(decodedRawId);
771
+ if (fileExpiry?.isExpired) {
772
+ res.statusCode = 410;
773
+ res.end("Gone — file expired");
695
774
  return;
696
775
  }
697
- // Add charset for text-based content types
698
- const contentType = image.mimeType.startsWith("text/")
699
- ? `${image.mimeType}; charset=utf-8`
700
- : image.mimeType;
701
- res.setHeader("Content-Type", contentType);
702
- res.setHeader("Content-Length", image.data.length);
703
- res.setHeader("Cache-Control", "public, max-age=3600");
704
- // Prevent MIME sniffing (defense-in-depth)
705
- res.setHeader("X-Content-Type-Options", "nosniff");
706
- // Images and browser-viewable types display inline; others trigger download
707
- if (image.mimeType.startsWith("image/") || image.mimeType === "text/html" || image.mimeType === "application/pdf") {
708
- res.setHeader("Content-Disposition", "inline");
709
- }
710
- else {
711
- res.setHeader("Content-Disposition", "attachment");
776
+ // Try disk (files MUST be tracked in served_files table)
777
+ const serveDir = path.resolve(".line-hive-tmp");
778
+ const diskPath = path.resolve(".line-hive-tmp", decodedRawId);
779
+ if (fileExpiry && diskPath.startsWith(serveDir + path.sep) && fs.existsSync(diskPath)) {
780
+ try {
781
+ const fileBuffer = fs.readFileSync(diskPath);
782
+ const ext = path.extname(diskPath).toLowerCase();
783
+ const mimeMap = {
784
+ ".html": "text/html",
785
+ ".css": "text/css",
786
+ ".js": "application/javascript",
787
+ ".json": "application/json",
788
+ ".png": "image/png",
789
+ ".jpg": "image/jpeg",
790
+ ".jpeg": "image/jpeg",
791
+ ".gif": "image/gif",
792
+ ".webp": "image/webp",
793
+ ".svg": "image/svg+xml",
794
+ ".pdf": "application/pdf",
795
+ };
796
+ const mimeType = mimeMap[ext] || "application/octet-stream";
797
+ const contentType = mimeType.startsWith("text/") ? `${mimeType}; charset=utf-8` : mimeType;
798
+ res.setHeader("Content-Type", contentType);
799
+ res.setHeader("Content-Length", fileBuffer.length);
800
+ res.setHeader("Cache-Control", "public, max-age=60");
801
+ res.setHeader("X-Content-Type-Options", "nosniff");
802
+ if (mimeType.startsWith("image/") || mimeType === "text/html" || mimeType === "application/pdf") {
803
+ res.setHeader("Content-Disposition", "inline");
804
+ }
805
+ else {
806
+ res.setHeader("Content-Disposition", "attachment");
807
+ }
808
+ res.statusCode = 200;
809
+ res.end(fileBuffer);
810
+ }
811
+ catch (err) {
812
+ deps.logger?.warn({ error: err instanceof Error ? err.message : err, file: decodedRawId }, "Failed to serve file");
813
+ res.statusCode = 500;
814
+ res.end("Internal error");
815
+ }
816
+ return;
712
817
  }
713
- res.statusCode = 200;
714
- res.end(image.data);
818
+ res.statusCode = 404;
819
+ res.end("Not found");
715
820
  return;
716
821
  }
717
822
  if (method !== "POST" || url !== deps.config.webhookPath) {