@xopcai/xopc 0.0.84 → 0.0.85

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.
Files changed (410) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/feishu/src/outbound/media-load.js +1 -1
  3. package/dist/extensions/feishu/src/plugin.d.ts +2 -0
  4. package/dist/extensions/feishu/src/plugin.js +10 -0
  5. package/dist/extensions/feishu/src/plugin.js.map +1 -1
  6. package/dist/extensions/feishu/src/workflow-progress.d.ts +27 -0
  7. package/dist/extensions/feishu/src/workflow-progress.js +99 -0
  8. package/dist/extensions/feishu/src/workflow-progress.js.map +1 -0
  9. package/dist/extensions/telegram/src/plugin.d.ts +2 -0
  10. package/dist/extensions/telegram/src/plugin.js +11 -1
  11. package/dist/extensions/telegram/src/plugin.js.map +1 -1
  12. package/dist/extensions/telegram/src/routing-integration.js +2 -2
  13. package/dist/extensions/telegram/src/workflow-progress.d.ts +24 -0
  14. package/dist/extensions/telegram/src/workflow-progress.js +73 -0
  15. package/dist/extensions/telegram/src/workflow-progress.js.map +1 -0
  16. package/dist/extensions/telegram/xopc.extension.json +1 -1
  17. package/dist/extensions/weixin/src/__tests__/workflow-progress.test.js +158 -0
  18. package/dist/extensions/weixin/src/__tests__/workflow-progress.test.js.map +1 -0
  19. package/dist/extensions/weixin/src/api/api.js +2 -2
  20. package/dist/extensions/weixin/src/auth/accounts.js +1 -1
  21. package/dist/extensions/weixin/src/cdn/upload.js +1 -1
  22. package/dist/extensions/weixin/src/media/data-url.js +1 -1
  23. package/dist/extensions/weixin/src/messaging/debug-mode.js +1 -1
  24. package/dist/extensions/weixin/src/messaging/inbound.js +1 -1
  25. package/dist/extensions/weixin/src/messaging/process-message.js +1 -1
  26. package/dist/extensions/weixin/src/plugin.d.ts +2 -0
  27. package/dist/extensions/weixin/src/plugin.js +11 -1
  28. package/dist/extensions/weixin/src/plugin.js.map +1 -1
  29. package/dist/extensions/weixin/src/storage/sync-buf.js +1 -1
  30. package/dist/extensions/weixin/src/workflow-progress.d.ts +26 -0
  31. package/dist/extensions/weixin/src/workflow-progress.js +99 -0
  32. package/dist/extensions/weixin/src/workflow-progress.js.map +1 -0
  33. package/dist/gateway/static/root/assets/agents-D3_-kNlZ.js +222 -0
  34. package/dist/gateway/static/root/assets/apps-page-D7v7649T.js +1 -0
  35. package/dist/gateway/static/root/assets/channels-settings-nCaMb0a7.js +1 -0
  36. package/dist/gateway/static/root/assets/channels-status-swr-C1gZBcJV.js +8 -0
  37. package/dist/gateway/static/root/assets/createLucideIcon-DPHK1VkS.js +1 -0
  38. package/dist/gateway/static/root/assets/cron-api-CoYK0hlm.js +1 -0
  39. package/dist/gateway/static/root/assets/cron-page-DeGo-Vjc.js +1 -0
  40. package/dist/gateway/static/root/assets/dist-BTWC-BTN.js +45 -0
  41. package/dist/gateway/static/root/assets/{dist-CqNMNhJM.js → dist-DaK4dsss.js} +1 -1
  42. package/dist/gateway/static/root/assets/{extension-debug-page-gf2L0kY_.js → extension-debug-page-BZngZWbO.js} +1 -1
  43. package/dist/gateway/static/root/assets/extension-page-D6JSyV27.js +1 -0
  44. package/dist/gateway/static/root/assets/extension-settings-page-8PZcmWI7.js +1 -0
  45. package/dist/gateway/static/root/assets/fetch-B2MYHbWg.js +1 -0
  46. package/dist/gateway/static/root/assets/{field-primitives-DTtlp-l8.js → field-primitives-Zzl22MvN.js} +1 -1
  47. package/dist/gateway/static/root/assets/heartbeat-config-api-BtIcpG0O.js +1 -0
  48. package/dist/gateway/static/root/assets/index-D4vM3-P7.js +4700 -0
  49. package/dist/gateway/static/root/assets/index-ew_2L2We.css +1 -0
  50. package/dist/gateway/static/root/assets/logs-page-_d4UJ-qQ.js +1 -0
  51. package/dist/gateway/static/root/assets/sessions-page-5N4aF2Wk.js +1 -0
  52. package/dist/gateway/static/root/assets/settings-form-section-D_tgb8r2.js +1 -0
  53. package/dist/gateway/static/root/assets/settings-page-C18xBt4X.js +3 -0
  54. package/dist/gateway/static/root/assets/share-preview-page-D4EG_vM1.js +2 -0
  55. package/dist/gateway/static/root/assets/skills-page-sPAXhh8w.js +2 -0
  56. package/dist/gateway/static/root/assets/theme-store-DryYl3qD.js +1 -0
  57. package/dist/gateway/static/root/assets/url-BwNL6Rgk.js +3 -0
  58. package/dist/gateway/static/root/assets/utils-CYO9eTCM.js +1 -0
  59. package/dist/gateway/static/root/assets/voice-api-key-field-Ds51havm.js +1 -0
  60. package/dist/gateway/static/root/index.html +7 -6
  61. package/dist/package.js +1 -1
  62. package/dist/src/agent/agent-manager.js +7 -7
  63. package/dist/src/agent/bootstrap/load-bootstrap-files.js +1 -1
  64. package/dist/src/agent/context/workspace-seed.js +3 -3
  65. package/dist/src/agent/embedded/map-stream-events.js +6 -0
  66. package/dist/src/agent/embedded/map-stream-events.js.map +1 -1
  67. package/dist/src/agent/embedded/subscribe-session.js +24 -0
  68. package/dist/src/agent/embedded/subscribe-session.js.map +1 -1
  69. package/dist/src/agent/embedded/types.d.ts +19 -0
  70. package/dist/src/agent/goals/goal-locale.js +2 -2
  71. package/dist/src/agent/goals/goal-run-store.js +4 -4
  72. package/dist/src/agent/goals/persistent-goal-service.js +1 -1
  73. package/dist/src/agent/goals/post-turn.js +2 -2
  74. package/dist/src/agent/image/load-image-media.js +2 -2
  75. package/dist/src/agent/ipc/bus.js +1 -1
  76. package/dist/src/agent/ipc/inbox.js +2 -2
  77. package/dist/src/agent/ipc/socket.js +1 -1
  78. package/dist/src/agent/memory/builtin-memory-store.js +1 -1
  79. package/dist/src/agent/memory/dreaming/deep-promotion.js +1 -1
  80. package/dist/src/agent/memory/dreaming/events.js +1 -1
  81. package/dist/src/agent/memory/dreaming/last-run.js +1 -1
  82. package/dist/src/agent/memory/dreaming/light-sweep.js +1 -1
  83. package/dist/src/agent/memory/dreaming/preview.js +1 -1
  84. package/dist/src/agent/memory/dreaming/rem-patterns.js +1 -1
  85. package/dist/src/agent/memory/dreaming/short-term-store.js +1 -1
  86. package/dist/src/agent/memory/dreaming/utils.js +1 -1
  87. package/dist/src/agent/memory/plugin-discovery.js +1 -1
  88. package/dist/src/agent/models/manager.js +1 -1
  89. package/dist/src/agent/prompt/service-prompt-builder.js +2 -2
  90. package/dist/src/agent/reply/post-compaction-context.js +1 -1
  91. package/dist/src/agent/reply/startup-context.d.ts +3 -0
  92. package/dist/src/agent/reply/startup-context.js +25 -2
  93. package/dist/src/agent/reply/startup-context.js.map +1 -1
  94. package/dist/src/agent/reply/workspace-boundary-read.js +1 -1
  95. package/dist/src/agent/sandbox/path-policy.js +2 -2
  96. package/dist/src/agent/service/build-direct-message-content.js +1 -1
  97. package/dist/src/agent/service.d.ts +1 -0
  98. package/dist/src/agent/service.js +10 -4
  99. package/dist/src/agent/service.js.map +1 -1
  100. package/dist/src/agent/session/session-inspector.js +1 -1
  101. package/dist/src/agent/skills/config.js +1 -1
  102. package/dist/src/agent/skills/hub-hash.js +2 -2
  103. package/dist/src/agent/skills/hub-lock.js +1 -1
  104. package/dist/src/agent/skills/hub-pull.js +3 -3
  105. package/dist/src/agent/skills/index.js +1 -1
  106. package/dist/src/agent/skills/managed-store.js +1 -1
  107. package/dist/src/agent/skills/scanner.js +1 -1
  108. package/dist/src/agent/skills/skill-manage-ops.js +1 -1
  109. package/dist/src/agent/skills/skill-manager.js +1 -1
  110. package/dist/src/agent/tools/create-share-tool.d.ts +27 -0
  111. package/dist/src/agent/tools/create-share-tool.js +237 -0
  112. package/dist/src/agent/tools/create-share-tool.js.map +1 -0
  113. package/dist/src/agent/tools/dreaming-tool.js +1 -1
  114. package/dist/src/agent/tools/factory.js +35 -1
  115. package/dist/src/agent/tools/factory.js.map +1 -1
  116. package/dist/src/agent/tools/image-generate-tool.js +1 -1
  117. package/dist/src/agent/tools/index.d.ts +2 -0
  118. package/dist/src/agent/tools/index.js +3 -1
  119. package/dist/src/agent/tools/send-media.js +1 -1
  120. package/dist/src/agent/tools/skill-manage-tool.js +1 -1
  121. package/dist/src/agent/tools/workflow-tool.d.ts +41 -0
  122. package/dist/src/agent/tools/workflow-tool.js +271 -0
  123. package/dist/src/agent/tools/workflow-tool.js.map +1 -0
  124. package/dist/src/agent/tools/write.js +1 -1
  125. package/dist/src/agent/workflow/builtins/audit-repo.d.ts +9 -0
  126. package/dist/src/agent/workflow/builtins/audit-repo.js +115 -0
  127. package/dist/src/agent/workflow/builtins/audit-repo.js.map +1 -0
  128. package/dist/src/agent/workflow/builtins/index.d.ts +15 -0
  129. package/dist/src/agent/workflow/builtins/index.js +28 -0
  130. package/dist/src/agent/workflow/builtins/index.js.map +1 -0
  131. package/dist/src/agent/workflow/builtins/multi-perspective-review.d.ts +9 -0
  132. package/dist/src/agent/workflow/builtins/multi-perspective-review.js +113 -0
  133. package/dist/src/agent/workflow/builtins/multi-perspective-review.js.map +1 -0
  134. package/dist/src/agent/workflow/builtins/research.d.ts +9 -0
  135. package/dist/src/agent/workflow/builtins/research.js +129 -0
  136. package/dist/src/agent/workflow/builtins/research.js.map +1 -0
  137. package/dist/src/agent/workflow/catalog.d.ts +51 -0
  138. package/dist/src/agent/workflow/catalog.js +155 -0
  139. package/dist/src/agent/workflow/catalog.js.map +1 -0
  140. package/dist/src/agent/workflow/channel-capability.d.ts +76 -0
  141. package/dist/src/agent/workflow/channel-capability.js +1 -0
  142. package/dist/src/agent/workflow/index.d.ts +11 -0
  143. package/dist/src/agent/workflow/index.js +10 -0
  144. package/dist/src/agent/workflow/last-run-memory.d.ts +42 -0
  145. package/dist/src/agent/workflow/last-run-memory.js +60 -0
  146. package/dist/src/agent/workflow/last-run-memory.js.map +1 -0
  147. package/dist/src/agent/workflow/parser.d.ts +20 -0
  148. package/dist/src/agent/workflow/parser.js +137 -0
  149. package/dist/src/agent/workflow/parser.js.map +1 -0
  150. package/dist/src/agent/workflow/progress-broker.d.ts +80 -0
  151. package/dist/src/agent/workflow/progress-broker.js +263 -0
  152. package/dist/src/agent/workflow/progress-broker.js.map +1 -0
  153. package/dist/src/agent/workflow/runtime.d.ts +31 -0
  154. package/dist/src/agent/workflow/runtime.js +301 -0
  155. package/dist/src/agent/workflow/runtime.js.map +1 -0
  156. package/dist/src/agent/workflow/snapshot.d.ts +18 -0
  157. package/dist/src/agent/workflow/snapshot.js +144 -0
  158. package/dist/src/agent/workflow/snapshot.js.map +1 -0
  159. package/dist/src/agent/workflow/structured-output-tool.d.ts +33 -0
  160. package/dist/src/agent/workflow/structured-output-tool.js +58 -0
  161. package/dist/src/agent/workflow/structured-output-tool.js.map +1 -0
  162. package/dist/src/agent/workflow/subagent-runner.d.ts +42 -0
  163. package/dist/src/agent/workflow/subagent-runner.js +104 -0
  164. package/dist/src/agent/workflow/subagent-runner.js.map +1 -0
  165. package/dist/src/agent/workflow/types.d.ts +137 -0
  166. package/dist/src/agent/workflow/types.js +1 -0
  167. package/dist/src/auth/credentials.js +3 -3
  168. package/dist/src/auth/profiles/store.js +1 -1
  169. package/dist/src/auth/sync-provider-auth.js +1 -1
  170. package/dist/src/browser/cache-dir-policy.js +1 -1
  171. package/dist/src/browser/cdp-local-launcher.js +2 -2
  172. package/dist/src/browser/providers/browser-ext-install.js +4 -4
  173. package/dist/src/browser/providers/cloakbrowser.js +4 -4
  174. package/dist/src/browser/providers/playwright-doctor.js +1 -1
  175. package/dist/src/browser/stealth.js +1 -1
  176. package/dist/src/channels/attachments/inbound-persist.js +1 -1
  177. package/dist/src/channels/attachments/outbound-tts-persist.js +1 -1
  178. package/dist/src/channels/outbound/persist-store.js +1 -1
  179. package/dist/src/channels/pairing/allow-from-file.js +1 -1
  180. package/dist/src/channels/pairing/pairing-store.js +2 -2
  181. package/dist/src/chat-commands/builtins/config.js +2 -2
  182. package/dist/src/chat-commands/builtins/model.js +40 -23
  183. package/dist/src/chat-commands/builtins/model.js.map +1 -1
  184. package/dist/src/chat-commands/builtins/system.js +30 -15
  185. package/dist/src/chat-commands/builtins/system.js.map +1 -1
  186. package/dist/src/chat-commands/builtins/workflow.d.ts +18 -0
  187. package/dist/src/chat-commands/builtins/workflow.js +167 -0
  188. package/dist/src/chat-commands/builtins/workflow.js.map +1 -0
  189. package/dist/src/chat-commands/context.js +1 -1
  190. package/dist/src/chat-commands/format-output.d.ts +28 -0
  191. package/dist/src/chat-commands/format-output.js +45 -0
  192. package/dist/src/chat-commands/format-output.js.map +1 -0
  193. package/dist/src/chat-commands/index.d.ts +1 -0
  194. package/dist/src/chat-commands/index.js +3 -1
  195. package/dist/src/chat-commands/index.js.map +1 -1
  196. package/dist/src/cli/commands/config.js +2 -2
  197. package/dist/src/cli/commands/doctor/checks/config-health.js +1 -1
  198. package/dist/src/cli/commands/doctor/checks/provider-auth.js +1 -1
  199. package/dist/src/cli/commands/doctor/checks/session-integrity.js +1 -1
  200. package/dist/src/cli/commands/doctor/checks/state-integrity.js +1 -1
  201. package/dist/src/cli/commands/doctor/checks/workspace-status.js +1 -1
  202. package/dist/src/cli/commands/extension-dev.js +1 -1
  203. package/dist/src/cli/commands/extension-marketplace.js +1 -1
  204. package/dist/src/cli/commands/extension-pack.js +1 -1
  205. package/dist/src/cli/commands/gateway/lifecycle.js +10 -4
  206. package/dist/src/cli/commands/gateway/lifecycle.js.map +1 -1
  207. package/dist/src/cli/commands/gateway/shared.js +1 -1
  208. package/dist/src/cli/commands/image.js +1 -1
  209. package/dist/src/cli/commands/init.js +4 -4
  210. package/dist/src/cli/commands/onboard.js +2 -2
  211. package/dist/src/cli/commands/tunnel.js +2 -2
  212. package/dist/src/cli/utils/gateway-client.js +1 -1
  213. package/dist/src/cli/utils/init-workspace-core.js +2 -2
  214. package/dist/src/config/agent-profile.js +1 -1
  215. package/dist/src/config/gateway-bind.js +1 -1
  216. package/dist/src/config/index.js +5 -5
  217. package/dist/src/config/loader.js +2 -2
  218. package/dist/src/config/models-json.js +2 -2
  219. package/dist/src/config/paths-state.js +1 -1
  220. package/dist/src/config/profile.js +2 -2
  221. package/dist/src/config/public-url.d.ts +28 -0
  222. package/dist/src/config/public-url.js +103 -0
  223. package/dist/src/config/public-url.js.map +1 -0
  224. package/dist/src/config/schema.d.ts +82 -0
  225. package/dist/src/config/schema.js +130 -1
  226. package/dist/src/config/schema.js.map +1 -1
  227. package/dist/src/config/workspace-path.js +1 -1
  228. package/dist/src/cron/executor.js +2 -2
  229. package/dist/src/cron/persistence.js +1 -1
  230. package/dist/src/cron/run-log-store.js +1 -1
  231. package/dist/src/daemon/constants.js +1 -1
  232. package/dist/src/daemon/install-plan.js +3 -3
  233. package/dist/src/daemon/install-plan.js.map +1 -1
  234. package/dist/src/daemon/launchd.js +2 -2
  235. package/dist/src/daemon/schtasks.js +38 -1
  236. package/dist/src/daemon/schtasks.js.map +1 -1
  237. package/dist/src/daemon/systemd.js +2 -2
  238. package/dist/src/extensions/bundle-mcp.js +1 -1
  239. package/dist/src/extensions/discover-extensions.js +1 -1
  240. package/dist/src/extensions/health.js +1 -1
  241. package/dist/src/extensions/loader.js +1 -1
  242. package/dist/src/extensions/lockfile.js +2 -2
  243. package/dist/src/gateway/agents-admin.js +2 -2
  244. package/dist/src/gateway/file-path-classifier.js +2 -2
  245. package/dist/src/gateway/hono/app.js +33 -2
  246. package/dist/src/gateway/hono/app.js.map +1 -1
  247. package/dist/src/gateway/hono/lib/config-payload.js +1 -1
  248. package/dist/src/gateway/hono/lib/extension-store.js +2 -2
  249. package/dist/src/gateway/hono/lib/static-ui.js +2 -2
  250. package/dist/src/gateway/hono/oauth.js +1 -1
  251. package/dist/src/gateway/hono/routes/agents.js +1 -1
  252. package/dist/src/gateway/hono/routes/auth-registry-extensions.js +1 -1
  253. package/dist/src/gateway/hono/routes/config-patch/misc.js +1 -1
  254. package/dist/src/gateway/hono/routes/dreaming.js +1 -1
  255. package/dist/src/gateway/hono/routes/host-fs.js +2 -2
  256. package/dist/src/gateway/hono/routes/lazy-bundles.js +8 -0
  257. package/dist/src/gateway/hono/routes/lazy-bundles.js.map +1 -1
  258. package/dist/src/gateway/hono/routes/models.js +1 -1
  259. package/dist/src/gateway/hono/routes/shares.js +631 -34
  260. package/dist/src/gateway/hono/routes/shares.js.map +1 -1
  261. package/dist/src/gateway/hono/routes/site-shares.d.ts +3 -0
  262. package/dist/src/gateway/hono/routes/site-shares.js +228 -0
  263. package/dist/src/gateway/hono/routes/site-shares.js.map +1 -0
  264. package/dist/src/gateway/hono/routes/tunnel.js +97 -8
  265. package/dist/src/gateway/hono/routes/tunnel.js.map +1 -1
  266. package/dist/src/gateway/hono/routes/workspace.js +5 -5
  267. package/dist/src/gateway/hono/sse.js +2 -2
  268. package/dist/src/gateway/host.d.ts +3 -1
  269. package/dist/src/gateway/host.js +3 -1
  270. package/dist/src/gateway/host.js.map +1 -1
  271. package/dist/src/gateway/lock.js +3 -3
  272. package/dist/src/gateway/ports.d.ts +6 -0
  273. package/dist/src/gateway/ports.js +38 -2
  274. package/dist/src/gateway/ports.js.map +1 -1
  275. package/dist/src/gateway/public-url.d.ts +8 -0
  276. package/dist/src/gateway/public-url.js +10 -0
  277. package/dist/src/gateway/public-url.js.map +1 -0
  278. package/dist/src/gateway/security/origin-check.d.ts +9 -1
  279. package/dist/src/gateway/security/origin-check.js +4 -0
  280. package/dist/src/gateway/security/origin-check.js.map +1 -1
  281. package/dist/src/gateway/server.js +15 -0
  282. package/dist/src/gateway/server.js.map +1 -1
  283. package/dist/src/gateway/service/agent-runner.js +2 -2
  284. package/dist/src/gateway/service/marketplace-service.js +2 -2
  285. package/dist/src/gateway/service/run-gateway-agent.js +2 -2
  286. package/dist/src/gateway/service.js +3 -2
  287. package/dist/src/gateway/service.js.map +1 -1
  288. package/dist/src/gateway/workspace-fs-file-list.js +1 -1
  289. package/dist/src/i18n/goals-bundle.js +1 -1
  290. package/dist/src/i18n/index.d.ts +1 -0
  291. package/dist/src/i18n/index.js +2 -1
  292. package/dist/src/i18n/locales/share-tool.en.js +15 -0
  293. package/dist/src/i18n/locales/share-tool.en.js.map +1 -0
  294. package/dist/src/i18n/locales/share-tool.zh.js +15 -0
  295. package/dist/src/i18n/locales/share-tool.zh.js.map +1 -0
  296. package/dist/src/i18n/share-tool-bundle.d.ts +20 -0
  297. package/dist/src/i18n/share-tool-bundle.js +56 -0
  298. package/dist/src/i18n/share-tool-bundle.js.map +1 -0
  299. package/dist/src/infra/gateway-processes.js +1 -0
  300. package/dist/src/infra/gateway-processes.js.map +1 -1
  301. package/dist/src/infra/restart.js +2 -2
  302. package/dist/src/infra/update-check.js +1 -1
  303. package/dist/src/infra/update-lock.js +3 -3
  304. package/dist/src/infra/update-runner.js +1 -1
  305. package/dist/src/infra/update-startup.js +2 -2
  306. package/dist/src/infra/write-file-atomic.js +2 -2
  307. package/dist/src/providers/auth-runtime/auth-profile-store.js +1 -1
  308. package/dist/src/providers/index.js +2 -2
  309. package/dist/src/providers/model-registry.js +1 -1
  310. package/dist/src/session/config-store.js +2 -2
  311. package/dist/src/session/parity/jsonl-transcript-io.js +2 -2
  312. package/dist/src/session/parity/sessions-json-file.js +1 -1
  313. package/dist/src/session/parity/transcript-file-lock.js +2 -2
  314. package/dist/src/session/parity/transcript-paths.js +1 -1
  315. package/dist/src/session/search-index-cache.js +1 -1
  316. package/dist/src/session/search-index.js +1 -1
  317. package/dist/src/session/session-title.js +3 -2
  318. package/dist/src/session/session-title.js.map +1 -1
  319. package/dist/src/session/store.js +5 -5
  320. package/dist/src/share/share-auto.d.ts +74 -0
  321. package/dist/src/share/share-auto.js +247 -0
  322. package/dist/src/share/share-auto.js.map +1 -0
  323. package/dist/src/share/share-config.js +63 -4
  324. package/dist/src/share/share-config.js.map +1 -1
  325. package/dist/src/share/share-landing.d.ts +28 -2
  326. package/dist/src/share/share-landing.js +155 -34
  327. package/dist/src/share/share-landing.js.map +1 -1
  328. package/dist/src/share/share-store.d.ts +48 -4
  329. package/dist/src/share/share-store.js +322 -51
  330. package/dist/src/share/share-store.js.map +1 -1
  331. package/dist/src/share/share-thumbnail.d.ts +35 -0
  332. package/dist/src/share/share-thumbnail.js +277 -0
  333. package/dist/src/share/share-thumbnail.js.map +1 -0
  334. package/dist/src/share/share-types.d.ts +68 -10
  335. package/dist/src/share/share-types.js +18 -1
  336. package/dist/src/share/share-types.js.map +1 -1
  337. package/dist/src/share/share-url.js +1 -1
  338. package/dist/src/share/share-zip.d.ts +35 -0
  339. package/dist/src/share/share-zip.js +303 -0
  340. package/dist/src/share/share-zip.js.map +1 -0
  341. package/dist/src/share/site-proxy.d.ts +35 -0
  342. package/dist/src/share/site-proxy.js +234 -0
  343. package/dist/src/share/site-proxy.js.map +1 -0
  344. package/dist/src/share/site-share-config.d.ts +11 -0
  345. package/dist/src/share/site-share-config.js +103 -0
  346. package/dist/src/share/site-share-config.js.map +1 -0
  347. package/dist/src/share/site-share-router.d.ts +23 -0
  348. package/dist/src/share/site-share-router.js +147 -0
  349. package/dist/src/share/site-share-router.js.map +1 -0
  350. package/dist/src/share/site-share-store.d.ts +53 -0
  351. package/dist/src/share/site-share-store.js +400 -0
  352. package/dist/src/share/site-share-store.js.map +1 -0
  353. package/dist/src/share/site-share-types.d.ts +103 -0
  354. package/dist/src/share/site-share-types.js +41 -0
  355. package/dist/src/share/site-share-types.js.map +1 -0
  356. package/dist/src/share/site-static-serve.d.ts +10 -0
  357. package/dist/src/share/site-static-serve.js +145 -0
  358. package/dist/src/share/site-static-serve.js.map +1 -0
  359. package/dist/src/tui/clipboard-image.js +3 -3
  360. package/dist/src/tui/theme-manager.js +1 -1
  361. package/dist/src/tui/tui-commands.js +18 -0
  362. package/dist/src/tui/tui-commands.js.map +1 -1
  363. package/dist/src/tui/tui-keybindings-file.js +1 -1
  364. package/dist/src/tui/tui-scoped-models.js +2 -2
  365. package/dist/src/tui/tui-settings.js +1 -1
  366. package/dist/src/tui/tui-workflow-slash.d.ts +32 -0
  367. package/dist/src/tui/tui-workflow-slash.js +63 -0
  368. package/dist/src/tui/tui-workflow-slash.js.map +1 -0
  369. package/dist/src/tui/tui.js +2 -2
  370. package/dist/src/tunnel/enable-lan-pairing.js +1 -1
  371. package/dist/src/tunnel/frpc-binary.js +3 -3
  372. package/dist/src/tunnel/frpc-config.js +1 -1
  373. package/dist/src/tunnel/frpc-extract.js +1 -1
  374. package/dist/src/tunnel/index.js +2 -2
  375. package/dist/src/tunnel/pair-context.d.ts +7 -1
  376. package/dist/src/tunnel/pair-context.js +25 -9
  377. package/dist/src/tunnel/pair-context.js.map +1 -1
  378. package/dist/src/tunnel/pair-url.d.ts +14 -1
  379. package/dist/src/tunnel/pair-url.js +14 -1
  380. package/dist/src/tunnel/pair-url.js.map +1 -1
  381. package/dist/src/tunnel/tunnel-service.js +2 -2
  382. package/dist/src/tunnel/tunnel-state.js +1 -1
  383. package/dist/src/utils/logger/audit.js +1 -1
  384. package/dist/src/utils/logger/log-store.js +1 -1
  385. package/dist/src/utils/logger/rotation.js +1 -1
  386. package/dist/src/voice/tts/audio.js +1 -1
  387. package/dist/src/voice/tts/providers/edge-speech.js +2 -2
  388. package/package.json +3 -2
  389. package/dist/gateway/static/root/assets/agents-tR-nNP04.js +0 -222
  390. package/dist/gateway/static/root/assets/apps-page-BDw6SP-d.js +0 -1
  391. package/dist/gateway/static/root/assets/button-KafIU8dx.js +0 -1
  392. package/dist/gateway/static/root/assets/channels-settings-DEFd-jj1.js +0 -1
  393. package/dist/gateway/static/root/assets/channels-status-swr-DI5FHdGe.js +0 -8
  394. package/dist/gateway/static/root/assets/cron-api-BSqY8LwW.js +0 -1
  395. package/dist/gateway/static/root/assets/cron-page-D7lVDjcR.js +0 -1
  396. package/dist/gateway/static/root/assets/dist-C57OMHW8.js +0 -48
  397. package/dist/gateway/static/root/assets/extension-page-CQo2Xsmg.js +0 -1
  398. package/dist/gateway/static/root/assets/extension-settings-page-CZf0WoZg.js +0 -1
  399. package/dist/gateway/static/root/assets/fetch-2iRFmd3n.js +0 -3
  400. package/dist/gateway/static/root/assets/heartbeat-config-api-B0drdQEJ.js +0 -1
  401. package/dist/gateway/static/root/assets/index-0Gt3TG4j.js +0 -4693
  402. package/dist/gateway/static/root/assets/index-BuFldCsB.css +0 -1
  403. package/dist/gateway/static/root/assets/logs-page-DMuORLfC.js +0 -1
  404. package/dist/gateway/static/root/assets/sessions-page-_UO8g6NN.js +0 -1
  405. package/dist/gateway/static/root/assets/settings-form-section-DkmHkknc.js +0 -1
  406. package/dist/gateway/static/root/assets/settings-page-Cz8FoW_A.js +0 -3
  407. package/dist/gateway/static/root/assets/skills-page-HrUOxF7H.js +0 -2
  408. package/dist/gateway/static/root/assets/theme-store-D01dJt95.js +0 -1
  409. package/dist/gateway/static/root/assets/utils-BFwcR6pL.js +0 -1
  410. package/dist/gateway/static/root/assets/voice-api-key-field-JF8-aqc5.js +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"share-store.js","names":[],"sources":["../../../src/share/share-store.ts"],"sourcesContent":["import { randomBytes, randomUUID } from 'node:crypto';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { stat, lstat, realpath } from 'node:fs/promises';\n\nimport { resolveStateDir } from '../config/paths.js';\nimport { isPathUnderWorkspace } from '../gateway/workspace-editor-path.js';\nimport { createLogger } from '../utils/logger.js';\nimport { logShareAudit } from './share-audit.js';\nimport type { ShareRecord, ShareStoreData, ShareConfig, CreateShareParams } from './share-types.js';\nimport { SHARE_CONFIG_DEFAULTS } from './share-types.js';\n\nconst log = createLogger('ShareStore');\n\nconst SHARES_FILE = 'shares.json';\nconst CLEANUP_INTERVAL_MS = 10 * 60_000;\nconst EXPIRED_RETENTION_MS = 24 * 60 * 60_000;\nconst MAX_STORED_RECORDS = 500;\nconst TRUNCATE_TO = 200;\nconst VIEW_COUNT_DEBOUNCE_MS = 2_000;\n\nfunction resolveSharesPath(): string {\n return join(resolveStateDir(), SHARES_FILE);\n}\n\nexport class ShareStore {\n private shares = new Map<string, ShareRecord>();\n private tokenIndex = new Map<string, string>();\n private viewCountDirty = false;\n private viewCountTimer: ReturnType<typeof setTimeout> | null = null;\n private cleanupTimer: ReturnType<typeof setInterval> | null = null;\n private config: ShareConfig;\n\n constructor(config?: Partial<ShareConfig>) {\n this.config = { ...SHARE_CONFIG_DEFAULTS, ...config };\n this.load();\n this.startCleanupTimer();\n }\n\n updateConfig(config: Partial<ShareConfig>): void {\n this.config = { ...this.config, ...config };\n }\n\n getConfig(): ShareConfig {\n return { ...this.config };\n }\n\n // ── CRUD ────────────────────────────────────────────────────────────────────\n\n async create(params: CreateShareParams & {\n workspaceRoot: string;\n gatewayTokenHash: string;\n }): Promise<ShareRecord> {\n if (!this.config.enabled) {\n throw new Error('File sharing is disabled');\n }\n\n const activeCount = this.getActiveShares().length;\n if (activeCount >= this.config.maxActiveShares) {\n throw new Error(`Maximum active shares reached (${this.config.maxActiveShares})`);\n }\n\n const { path: relPath, workspaceRoot, gatewayTokenHash } = params;\n const ttlMs = params.ttlMs ?? this.config.defaultTtlMs;\n\n if (ttlMs < 60_000 || ttlMs > this.config.maxTtlMs) {\n throw new Error(`TTL must be between 60s and ${this.config.maxTtlMs / 1000}s`);\n }\n if (params.maxViews !== undefined && params.maxViews !== null) {\n if (params.maxViews < 1 || params.maxViews > 1000) {\n throw new Error('maxViews must be between 1 and 1000');\n }\n }\n\n const absolutePath = await this.resolveAndValidatePath(relPath, workspaceRoot);\n const fileStat = await stat(absolutePath);\n\n if (!fileStat.isFile()) {\n throw new Error('Path is not a regular file');\n }\n if (fileStat.size > this.config.maxFileSize) {\n const maxMb = (this.config.maxFileSize / 1_048_576).toFixed(0);\n throw new Error(`File size exceeds maximum (${maxMb} MB)`);\n }\n\n const linkStat = await lstat(absolutePath);\n if (linkStat.isSymbolicLink()) {\n const realPath = await realpath(absolutePath);\n if (!isPathUnderWorkspace(workspaceRoot, realPath)) {\n throw new Error('Symlink target is outside workspace');\n }\n }\n\n const id = randomUUID();\n const token = randomBytes(32).toString('base64url');\n const fileName = relPath.split('/').pop() ?? relPath;\n const mimeType = resolveMimeType(fileName);\n const now = new Date();\n\n const record: ShareRecord = {\n id,\n token,\n absolutePath,\n workspaceRelativePath: relPath,\n workspaceRoot,\n inode: fileStat.ino,\n isDirectory: false,\n fileName,\n fileSize: fileStat.size,\n mimeType,\n createdAt: now.toISOString(),\n expiresAt: new Date(now.getTime() + ttlMs).toISOString(),\n maxViews: params.maxViews ?? null,\n viewCount: 0,\n revoked: false,\n createdByTokenHash: gatewayTokenHash,\n description: params.description,\n };\n\n this.shares.set(id, record);\n this.tokenIndex.set(token, id);\n this.persistSync();\n\n logShareAudit(\n 'share.create',\n { shareId: id, tokenPrefix: token.slice(0, 8), fileName, fileSize: fileStat.size, ttlMs },\n `Share created: ${fileName}`,\n );\n\n return record;\n }\n\n getById(id: string): ShareRecord | null {\n return this.shares.get(id) ?? null;\n }\n\n getByToken(token: string): ShareRecord | null {\n const id = this.tokenIndex.get(token);\n if (!id) return null;\n return this.shares.get(id) ?? null;\n }\n\n /** Validate a share is still accessible for download. Returns null reason if valid. */\n validateAccess(record: ShareRecord): { valid: boolean; reason?: string } {\n if (record.revoked) return { valid: false, reason: 'revoked' };\n if (Date.now() >= new Date(record.expiresAt).getTime()) return { valid: false, reason: 'expired' };\n if (record.maxViews !== null && record.viewCount >= record.maxViews) {\n return { valid: false, reason: 'max_views' };\n }\n return { valid: true };\n }\n\n /** Increment view count (debounced persist). */\n incrementViewCount(id: string): void {\n const record = this.shares.get(id);\n if (!record) return;\n record.viewCount++;\n this.scheduleDebouncedPersist();\n }\n\n /** Check if the file still exists and inode matches. */\n async validateFileIntegrity(record: ShareRecord): Promise<{ valid: boolean; reason?: string }> {\n try {\n const realPath = await realpath(record.absolutePath);\n if (!isPathUnderWorkspace(record.workspaceRoot, realPath)) {\n return { valid: false, reason: 'file_deleted' };\n }\n const fileStat = await stat(record.absolutePath);\n if (fileStat.ino !== record.inode) {\n logShareAudit(\n 'share.path_changed',\n { shareId: record.id, tokenPrefix: record.token.slice(0, 8), oldInode: record.inode, newInode: fileStat.ino },\n `Share file replaced (inode changed): ${record.fileName}`,\n );\n return { valid: false, reason: 'file_deleted' };\n }\n return { valid: true };\n } catch {\n return { valid: false, reason: 'file_deleted' };\n }\n }\n\n revoke(id: string): boolean {\n const record = this.shares.get(id);\n if (!record) return false;\n record.revoked = true;\n this.persistSync();\n logShareAudit(\n 'share.revoke',\n { shareId: id, tokenPrefix: record.token.slice(0, 8), fileName: record.fileName },\n `Share revoked: ${record.fileName}`,\n );\n return true;\n }\n\n revokeMany(ids: string[]): number {\n let count = 0;\n for (const id of ids) {\n const record = this.shares.get(id);\n if (record && !record.revoked) {\n record.revoked = true;\n count++;\n logShareAudit(\n 'share.revoke',\n { shareId: id, tokenPrefix: record.token.slice(0, 8), fileName: record.fileName },\n `Share revoked (batch): ${record.fileName}`,\n );\n }\n }\n if (count > 0) this.persistSync();\n return count;\n }\n\n revokeExpired(): number {\n const now = Date.now();\n let count = 0;\n for (const record of this.shares.values()) {\n if (!record.revoked && now >= new Date(record.expiresAt).getTime()) {\n record.revoked = true;\n count++;\n }\n }\n if (count > 0) this.persistSync();\n return count;\n }\n\n update(id: string, patch: { extendTtlMs?: number; maxViews?: number | null }): ShareRecord | null {\n const record = this.shares.get(id);\n if (!record) return null;\n\n if (patch.extendTtlMs !== undefined) {\n const newExpiry = new Date(Date.now() + patch.extendTtlMs);\n record.expiresAt = newExpiry.toISOString();\n }\n if (patch.maxViews !== undefined) {\n record.maxViews = patch.maxViews;\n }\n\n this.persistSync();\n logShareAudit(\n 'share.update',\n { shareId: id, tokenPrefix: record.token.slice(0, 8), patch },\n `Share updated: ${record.fileName}`,\n );\n return record;\n }\n\n getActiveShares(): ShareRecord[] {\n const now = Date.now();\n return [...this.shares.values()].filter(\n (r) => !r.revoked && now < new Date(r.expiresAt).getTime(),\n );\n }\n\n getAllShares(): ShareRecord[] {\n return [...this.shares.values()].sort(\n (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),\n );\n }\n\n // ── Persistence ─────────────────────────────────────────────────────────────\n\n private load(): void {\n const path = resolveSharesPath();\n if (!existsSync(path)) return;\n try {\n const raw = readFileSync(path, 'utf8');\n const data = JSON.parse(raw) as ShareStoreData;\n if (data.version !== 1 || !Array.isArray(data.shares)) return;\n\n const now = Date.now();\n let cleaned = 0;\n for (const record of data.shares) {\n const expiredMs = now - new Date(record.expiresAt).getTime();\n if (expiredMs > EXPIRED_RETENTION_MS) {\n cleaned++;\n continue;\n }\n this.shares.set(record.id, record);\n this.tokenIndex.set(record.token, record.id);\n }\n if (cleaned > 0) {\n log.info({ cleaned }, `Cleaned ${cleaned} expired share records on load`);\n this.persistSync();\n }\n } catch (err) {\n log.warn({ err }, 'Failed to load shares.json');\n }\n }\n\n private persistSync(): void {\n const path = resolveSharesPath();\n mkdirSync(resolveStateDir(), { recursive: true });\n\n let records = [...this.shares.values()];\n if (records.length > MAX_STORED_RECORDS) {\n records.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());\n const active = records.filter((r) => !r.revoked && Date.now() < new Date(r.expiresAt).getTime());\n records = active.slice(0, TRUNCATE_TO);\n\n this.shares.clear();\n this.tokenIndex.clear();\n for (const r of records) {\n this.shares.set(r.id, r);\n this.tokenIndex.set(r.token, r.id);\n }\n }\n\n const data: ShareStoreData = { version: 1, shares: records };\n writeFileSync(path, `${JSON.stringify(data, null, 2)}\\n`, 'utf8');\n }\n\n private scheduleDebouncedPersist(): void {\n this.viewCountDirty = true;\n if (this.viewCountTimer) return;\n this.viewCountTimer = setTimeout(() => {\n this.viewCountTimer = null;\n if (this.viewCountDirty) {\n this.viewCountDirty = false;\n this.persistSync();\n }\n }, VIEW_COUNT_DEBOUNCE_MS);\n this.viewCountTimer.unref?.();\n }\n\n // ── Cleanup ─────────────────────────────────────────────────────────────────\n\n private startCleanupTimer(): void {\n this.cleanupTimer = setInterval(() => {\n this.cleanupExpired();\n }, CLEANUP_INTERVAL_MS);\n this.cleanupTimer.unref?.();\n }\n\n private cleanupExpired(): void {\n const now = Date.now();\n let removed = 0;\n for (const [id, record] of this.shares) {\n const expiredMs = now - new Date(record.expiresAt).getTime();\n if (expiredMs > EXPIRED_RETENTION_MS) {\n this.shares.delete(id);\n this.tokenIndex.delete(record.token);\n removed++;\n }\n }\n if (removed > 0) {\n this.persistSync();\n log.debug({ removed }, `Cleaned ${removed} expired shares`);\n }\n }\n\n shutdown(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n if (this.viewCountTimer) {\n clearTimeout(this.viewCountTimer);\n this.viewCountTimer = null;\n }\n if (this.viewCountDirty) {\n this.viewCountDirty = false;\n this.persistSync();\n }\n }\n\n // ── Helpers ─────────────────────────────────────────────────────────────────\n\n private async resolveAndValidatePath(relPath: string, workspaceRoot: string): Promise<string> {\n const trimmed = relPath.trim().replace(/\\\\/g, '/').replace(/^\\/+/, '');\n if (!trimmed) throw new Error('Empty path');\n if (trimmed.includes('..')) throw new Error('Path traversal not allowed');\n if (trimmed.includes('\\0')) throw new Error('Invalid path');\n\n const { resolve } = await import('node:path');\n const { relative } = await import('node:path');\n\n const abs = resolve(workspaceRoot, trimmed);\n const root = resolve(workspaceRoot);\n const relToRoot = relative(root, abs);\n if (relToRoot.startsWith('..') || relToRoot.split(/[/\\\\]/).includes('..')) {\n throw new Error('Path is outside workspace');\n }\n return abs;\n }\n}\n\n// ── MIME resolution ─────────────────────────────────────────────────────────\n\nconst MIME_BY_EXT: Record<string, string> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n pdf: 'application/pdf',\n txt: 'text/plain',\n md: 'text/markdown',\n json: 'application/json',\n html: 'text/html',\n css: 'text/css',\n js: 'text/javascript',\n ts: 'text/typescript',\n xml: 'application/xml',\n csv: 'text/csv',\n zip: 'application/zip',\n gz: 'application/gzip',\n tar: 'application/x-tar',\n mp3: 'audio/mpeg',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n mp4: 'video/mp4',\n webm: 'video/webm',\n mov: 'video/quicktime',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n};\n\nfunction resolveMimeType(fileName: string): string {\n const ext = fileName.split('.').pop()?.toLowerCase() ?? '';\n return MIME_BY_EXT[ext] || 'application/octet-stream';\n}\n\n// ── Singleton ─────────────────────────────────────────────────────────────────\n\nlet singleton: ShareStore | null = null;\n\nexport function getShareStore(config?: Partial<ShareConfig>): ShareStore {\n if (!singleton) singleton = new ShareStore(config);\n return singleton;\n}\n\nexport function resetShareStoreForTests(): void {\n singleton?.shutdown();\n singleton = null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"share-store.js","names":["resolvePath","relPathPosix"],"sources":["../../../src/share/share-store.ts"],"sourcesContent":["import { randomBytes, randomUUID } from 'node:crypto';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, relative as relPathPosix, resolve as resolvePath } from 'node:path';\nimport { stat, lstat, realpath, readdir } from 'node:fs/promises';\n\nimport { resolveStateDir } from '../config/paths.js';\nimport { isPathUnderWorkspace } from '../gateway/workspace-editor-path.js';\nimport { createLogger } from '../utils/logger.js';\nimport { logShareAudit } from './share-audit.js';\nimport type {\n ShareRecord,\n ShareStoreData,\n ShareConfig,\n CreateShareParams,\n ShareKind,\n} from './share-types.js';\nimport { SHARE_CONFIG_DEFAULTS } from './share-types.js';\n\nconst log = createLogger('ShareStore');\n\nconst SHARES_FILE = 'shares.json';\nconst CLEANUP_INTERVAL_MS = 10 * 60_000;\nconst EXPIRED_RETENTION_MS = 24 * 60 * 60_000;\nconst MAX_STORED_RECORDS = 500;\nconst TRUNCATE_TO = 200;\nconst COUNTER_DEBOUNCE_MS = 2_000;\n\nfunction resolveSharesPath(): string {\n return join(resolveStateDir(), SHARES_FILE);\n}\n\nexport interface DirectoryListingEntry {\n name: string;\n /** Workspace/share-relative POSIX path. */\n path: string;\n isDirectory: boolean;\n size: number;\n mtime: string;\n mimeType: string;\n}\n\nexport interface DirectoryListing {\n /** Share-relative path of the listed dir ('' for root). */\n path: string;\n entries: DirectoryListingEntry[];\n truncated: boolean;\n}\n\ninterface DirectoryScanSummary {\n entryCount: number;\n totalSize: number;\n}\n\nexport class ShareStore {\n private shares = new Map<string, ShareRecord>();\n private tokenIndex = new Map<string, string>();\n private dirty = false;\n private debounceTimer: ReturnType<typeof setTimeout> | null = null;\n private cleanupTimer: ReturnType<typeof setInterval> | null = null;\n private config: ShareConfig;\n private listingCache = new Map<string, { listing: DirectoryListing; expiresAt: number }>();\n /** Optional cleanup hook invoked when a share record is dropped (e.g. delete thumbnail). */\n private onCleanup: ((record: ShareRecord) => void) | null = null;\n\n constructor(config?: Partial<ShareConfig>) {\n this.config = { ...SHARE_CONFIG_DEFAULTS, ...config };\n this.load();\n this.startCleanupTimer();\n }\n\n updateConfig(config: Partial<ShareConfig>): void {\n this.config = { ...this.config, ...config };\n }\n\n /** Register a cleanup hook (idempotent — last wins). */\n setCleanupHook(hook: (record: ShareRecord) => void): void {\n this.onCleanup = hook;\n }\n\n getConfig(): ShareConfig {\n return { ...this.config };\n }\n\n // ── CRUD ────────────────────────────────────────────────────────────────────\n\n async create(\n params: CreateShareParams & {\n workspaceRoot: string;\n gatewayTokenHash: string;\n },\n ): Promise<ShareRecord> {\n if (!this.config.enabled) {\n throw new Error('File sharing is disabled');\n }\n\n const activeCount = this.getActiveShares().length;\n if (activeCount >= this.config.maxActiveShares) {\n throw new Error(`Maximum active shares reached (${this.config.maxActiveShares})`);\n }\n\n const { path: relPath, workspaceRoot, gatewayTokenHash } = params;\n const ttlMs = params.ttlMs ?? this.config.defaultTtlMs;\n\n if (ttlMs < 60_000 || ttlMs > this.config.maxTtlMs) {\n throw new Error(`TTL must be between 60s and ${this.config.maxTtlMs / 1000}s`);\n }\n if (params.maxViews !== undefined && params.maxViews !== null) {\n if (params.maxViews < 1 || params.maxViews > 1000) {\n throw new Error('maxViews must be between 1 and 1000');\n }\n }\n\n const absolutePath = await this.resolveAndValidatePath(relPath, workspaceRoot);\n const fileStat = await stat(absolutePath);\n\n const detectedKind: ShareKind = fileStat.isDirectory() ? 'directory' : 'file';\n const requestedKind = params.kind ?? detectedKind;\n if (requestedKind !== detectedKind) {\n throw new Error(\n `Requested share kind '${requestedKind}' does not match filesystem (got '${detectedKind}')`,\n );\n }\n\n if (requestedKind === 'file') {\n return this.createFileShare({\n params,\n absolutePath,\n fileStat,\n relPath,\n workspaceRoot,\n ttlMs,\n gatewayTokenHash,\n });\n }\n\n return this.createDirectoryShare({\n params,\n absolutePath,\n fileStat,\n relPath,\n workspaceRoot,\n ttlMs,\n gatewayTokenHash,\n });\n }\n\n private async createFileShare(args: {\n params: CreateShareParams;\n absolutePath: string;\n fileStat: import('node:fs').Stats;\n relPath: string;\n workspaceRoot: string;\n ttlMs: number;\n gatewayTokenHash: string;\n }): Promise<ShareRecord> {\n const { params, absolutePath, fileStat, relPath, workspaceRoot, ttlMs, gatewayTokenHash } = args;\n\n if (!fileStat.isFile()) {\n throw new Error('Path is not a regular file');\n }\n if (fileStat.size > this.config.maxFileSize) {\n const maxMb = (this.config.maxFileSize / 1_048_576).toFixed(0);\n throw new Error(`File size exceeds maximum (${maxMb} MB)`);\n }\n\n const linkStat = await lstat(absolutePath);\n if (linkStat.isSymbolicLink()) {\n const real = await realpath(absolutePath);\n if (!isPathUnderWorkspace(workspaceRoot, real)) {\n throw new Error('Symlink target is outside workspace');\n }\n }\n\n const fileName = relPath.split('/').pop() || relPath;\n const mimeType = resolveMimeType(fileName);\n const record = this.buildRecord({\n kind: 'file',\n absolutePath,\n workspaceRoot,\n workspaceRelativePath: relPath,\n inode: fileStat.ino,\n fileName,\n fileSize: fileStat.size,\n mimeType,\n ttlMs,\n maxViews: params.maxViews,\n description: params.description,\n gatewayTokenHash,\n });\n\n this.persistAndAudit(record, 'share.create', `Share created: ${record.fileName}`, {\n fileName: record.fileName,\n fileSize: record.fileSize,\n ttlMs,\n });\n\n return record;\n }\n\n private async createDirectoryShare(args: {\n params: CreateShareParams;\n absolutePath: string;\n fileStat: import('node:fs').Stats;\n relPath: string;\n workspaceRoot: string;\n ttlMs: number;\n gatewayTokenHash: string;\n }): Promise<ShareRecord> {\n const { params, absolutePath, fileStat, relPath, workspaceRoot, ttlMs, gatewayTokenHash } = args;\n const dirCfg = this.config.directory;\n if (!dirCfg.enabled) {\n throw new Error('Directory sharing is disabled');\n }\n\n const followSymlinks = params.followSymlinks ?? false;\n const maxDepth = params.maxDepth ?? dirCfg.maxDepth;\n const maxFileCount = Math.min(params.maxFileCount ?? dirCfg.maxFileCount, dirCfg.maxFileCount);\n const maxFolderSize = Math.min(\n params.maxFolderSize ?? dirCfg.maxFolderSize,\n dirCfg.maxFolderSize,\n );\n\n const summary = await scanDirectory(absolutePath, {\n workspaceRoot,\n followSymlinks,\n maxDepth,\n maxFileCount,\n maxFolderSize,\n });\n\n const fileName = relPath.split('/').pop() || relPath || 'shared';\n const record = this.buildRecord({\n kind: 'directory',\n absolutePath,\n workspaceRoot,\n workspaceRelativePath: relPath,\n inode: fileStat.ino,\n fileName,\n fileSize: summary.totalSize,\n mimeType: 'application/x-directory',\n ttlMs,\n maxViews: params.maxViews,\n description: params.description,\n gatewayTokenHash,\n directory: {\n mode: params.directoryMode ?? 'browse',\n entryCount: summary.entryCount,\n followSymlinks,\n maxDepth,\n },\n });\n\n this.persistAndAudit(record, 'share.create', `Folder share created: ${record.fileName}`, {\n fileName: record.fileName,\n entryCount: summary.entryCount,\n totalSize: summary.totalSize,\n ttlMs,\n mode: record.directory?.mode,\n });\n\n return record;\n }\n\n private buildRecord(input: {\n kind: ShareKind;\n absolutePath: string;\n workspaceRoot: string;\n workspaceRelativePath: string;\n inode: number;\n fileName: string;\n fileSize: number;\n mimeType: string;\n ttlMs: number;\n maxViews?: number | null;\n description?: string;\n gatewayTokenHash: string;\n directory?: ShareRecord['directory'];\n }): ShareRecord {\n const id = randomUUID();\n const token = randomBytes(32).toString('base64url');\n const now = new Date();\n const record: ShareRecord = {\n id,\n token,\n absolutePath: input.absolutePath,\n workspaceRelativePath: input.workspaceRelativePath,\n workspaceRoot: input.workspaceRoot,\n inode: input.inode,\n kind: input.kind,\n fileName: input.fileName,\n fileSize: input.fileSize,\n mimeType: input.mimeType,\n createdAt: now.toISOString(),\n expiresAt: new Date(now.getTime() + input.ttlMs).toISOString(),\n maxViews: input.maxViews ?? null,\n downloadCount: 0,\n revoked: false,\n createdByTokenHash: input.gatewayTokenHash,\n description: input.description,\n directory: input.directory,\n };\n\n this.shares.set(id, record);\n this.tokenIndex.set(token, id);\n return record;\n }\n\n private persistAndAudit(\n record: ShareRecord,\n event: Parameters<typeof logShareAudit>[0],\n message: string,\n extra: Record<string, unknown>,\n ): void {\n this.persistSync();\n logShareAudit(event, { shareId: record.id, tokenPrefix: record.token.slice(0, 8), ...extra }, message);\n }\n\n getById(id: string): ShareRecord | null {\n return this.shares.get(id) ?? null;\n }\n\n getByToken(token: string): ShareRecord | null {\n const id = this.tokenIndex.get(token);\n if (!id) return null;\n return this.shares.get(id) ?? null;\n }\n\n /** Validate a share is still accessible for download. Returns null reason if valid. */\n validateAccess(record: ShareRecord): { valid: boolean; reason?: string } {\n if (record.revoked) return { valid: false, reason: 'revoked' };\n if (Date.now() >= new Date(record.expiresAt).getTime()) return { valid: false, reason: 'expired' };\n if (record.maxViews !== null && record.downloadCount >= record.maxViews) {\n return { valid: false, reason: 'max_views' };\n }\n return { valid: true };\n }\n\n /** Increment download counter (used by directory & file downloads). Debounced persist. */\n incrementDownloadCount(id: string): void {\n const record = this.shares.get(id);\n if (!record) return;\n record.downloadCount++;\n this.scheduleDebouncedPersist();\n }\n\n /** Check if the file still exists and inode matches. */\n async validateFileIntegrity(record: ShareRecord): Promise<{ valid: boolean; reason?: string }> {\n try {\n const realPath = await realpath(record.absolutePath);\n if (!isPathUnderWorkspace(record.workspaceRoot, realPath)) {\n return { valid: false, reason: 'file_deleted' };\n }\n const fileStat = await stat(record.absolutePath);\n if (fileStat.ino !== record.inode) {\n logShareAudit(\n 'share.path_changed',\n {\n shareId: record.id,\n tokenPrefix: record.token.slice(0, 8),\n oldInode: record.inode,\n newInode: fileStat.ino,\n },\n `Share path replaced (inode changed): ${record.fileName}`,\n );\n return { valid: false, reason: 'file_deleted' };\n }\n return { valid: true };\n } catch {\n return { valid: false, reason: 'file_deleted' };\n }\n }\n\n /**\n * Resolve a child path inside a directory share. Returns the absolute path\n * if it stays within both the share root and the workspace.\n */\n async resolveDirectoryChild(\n record: ShareRecord,\n relativePath: string,\n ): Promise<{ ok: true; absolutePath: string } | { ok: false; reason: string }> {\n if (record.kind !== 'directory') {\n return { ok: false, reason: 'not_directory' };\n }\n const trimmed = (relativePath ?? '').replace(/^\\/+/, '').replace(/\\\\/g, '/');\n if (trimmed.includes('..') || trimmed.includes('\\0')) {\n return { ok: false, reason: 'path_traversal' };\n }\n const abs = resolvePath(record.absolutePath, trimmed);\n const relToShare = relPathPosix(record.absolutePath, abs);\n if (relToShare.startsWith('..') || relToShare.split(/[/\\\\]/).includes('..')) {\n return { ok: false, reason: 'path_outside_share' };\n }\n try {\n const real = await realpath(abs);\n if (!isPathUnderWorkspace(record.workspaceRoot, real)) {\n return { ok: false, reason: 'path_outside_workspace' };\n }\n const relToShareReal = relPathPosix(record.absolutePath, real);\n if (relToShareReal.startsWith('..') || relToShareReal.split(/[/\\\\]/).includes('..')) {\n return { ok: false, reason: 'path_outside_share' };\n }\n return { ok: true, absolutePath: real };\n } catch {\n return { ok: false, reason: 'not_found' };\n }\n }\n\n /** List a single directory level (cached, share-root-relative). */\n async listDirectory(record: ShareRecord, relativePath: string): Promise<DirectoryListing> {\n if (record.kind !== 'directory') throw new Error('Not a directory share');\n const trimmed = (relativePath ?? '').replace(/^\\/+/, '');\n const cacheKey = `${record.id}::${trimmed}`;\n const cacheTtl = this.config.directory.listingCacheMs;\n const now = Date.now();\n const cached = this.listingCache.get(cacheKey);\n if (cached && cached.expiresAt > now) return cached.listing;\n\n const resolved = await this.resolveDirectoryChild(record, trimmed);\n if (resolved.ok !== true) throw new Error(resolved.reason);\n\n const absDir = resolved.absolutePath;\n const stats = await stat(absDir);\n if (!stats.isDirectory()) throw new Error('not_directory');\n\n const followSymlinks = record.directory?.followSymlinks ?? false;\n const dirents = await readdir(absDir, { withFileTypes: true });\n const entries: DirectoryListingEntry[] = [];\n let truncated = false;\n const limit = 2_000;\n\n for (const dirent of dirents) {\n if (entries.length >= limit) {\n truncated = true;\n break;\n }\n const childAbs = resolvePath(absDir, dirent.name);\n try {\n const childLstat = await lstat(childAbs);\n if (childLstat.isSymbolicLink()) {\n if (!followSymlinks) continue;\n const real = await realpath(childAbs);\n if (!isPathUnderWorkspace(record.workspaceRoot, real)) continue;\n const relToShare = relPathPosix(record.absolutePath, real);\n if (relToShare.startsWith('..')) continue;\n }\n const childStat = childLstat.isSymbolicLink() ? await stat(childAbs) : childLstat;\n const childRel = trimmed ? `${trimmed}/${dirent.name}` : dirent.name;\n entries.push({\n name: dirent.name,\n path: childRel,\n isDirectory: childStat.isDirectory(),\n size: childStat.isFile() ? childStat.size : 0,\n mtime: childStat.mtime.toISOString(),\n mimeType: childStat.isDirectory() ? 'application/x-directory' : resolveMimeType(dirent.name),\n });\n } catch {\n /* skip unreadable entries */\n }\n }\n\n entries.sort((a, b) => {\n if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;\n return a.name.localeCompare(b.name);\n });\n\n const listing: DirectoryListing = { path: trimmed, entries, truncated };\n if (cacheTtl > 0) {\n this.listingCache.set(cacheKey, { listing, expiresAt: now + cacheTtl });\n }\n return listing;\n }\n\n /** Update thumbnail status. Persists immediately so generator restart is safe. */\n setThumbnailStatus(id: string, status: 'pending' | 'ready' | 'failed'): void {\n const record = this.shares.get(id);\n if (!record) return;\n record.thumbnailStatus = status;\n if (status === 'ready') record.thumbnailGeneratedAt = new Date().toISOString();\n if (status === 'failed') record.thumbnailFailedAt = new Date().toISOString();\n this.persistSync();\n }\n\n /** Drop the listing cache for a share (used on revoke/update). */\n invalidateListingCache(shareId: string): void {\n for (const key of this.listingCache.keys()) {\n if (key.startsWith(`${shareId}::`)) this.listingCache.delete(key);\n }\n }\n\n revoke(id: string): boolean {\n const record = this.shares.get(id);\n if (!record) return false;\n record.revoked = true;\n this.invalidateListingCache(id);\n this.persistSync();\n logShareAudit(\n 'share.revoke',\n { shareId: id, tokenPrefix: record.token.slice(0, 8), fileName: record.fileName },\n `Share revoked: ${record.fileName}`,\n );\n return true;\n }\n\n revokeMany(ids: string[]): number {\n let count = 0;\n for (const id of ids) {\n const record = this.shares.get(id);\n if (record && !record.revoked) {\n record.revoked = true;\n this.invalidateListingCache(id);\n count++;\n logShareAudit(\n 'share.revoke',\n { shareId: id, tokenPrefix: record.token.slice(0, 8), fileName: record.fileName },\n `Share revoked (batch): ${record.fileName}`,\n );\n }\n }\n if (count > 0) this.persistSync();\n return count;\n }\n\n revokeExpired(): number {\n const now = Date.now();\n let count = 0;\n for (const record of this.shares.values()) {\n if (!record.revoked && now >= new Date(record.expiresAt).getTime()) {\n record.revoked = true;\n this.invalidateListingCache(record.id);\n count++;\n }\n }\n if (count > 0) this.persistSync();\n return count;\n }\n\n update(id: string, patch: { extendTtlMs?: number; maxViews?: number | null }): ShareRecord | null {\n const record = this.shares.get(id);\n if (!record) return null;\n\n if (patch.extendTtlMs !== undefined) {\n const newExpiry = new Date(Date.now() + patch.extendTtlMs);\n record.expiresAt = newExpiry.toISOString();\n }\n if (patch.maxViews !== undefined) {\n record.maxViews = patch.maxViews;\n }\n\n this.persistSync();\n logShareAudit(\n 'share.update',\n { shareId: id, tokenPrefix: record.token.slice(0, 8), patch },\n `Share updated: ${record.fileName}`,\n );\n return record;\n }\n\n getActiveShares(): ShareRecord[] {\n const now = Date.now();\n return [...this.shares.values()].filter(\n (r) => !r.revoked && now < new Date(r.expiresAt).getTime(),\n );\n }\n\n getAllShares(): ShareRecord[] {\n return [...this.shares.values()].sort(\n (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),\n );\n }\n\n // ── Persistence ─────────────────────────────────────────────────────────────\n\n private load(): void {\n const path = resolveSharesPath();\n if (!existsSync(path)) return;\n try {\n const raw = readFileSync(path, 'utf8');\n const data = JSON.parse(raw) as ShareStoreData;\n if (data.version !== 1 || !Array.isArray(data.shares)) return;\n\n const now = Date.now();\n let cleaned = 0;\n for (const record of data.shares) {\n const expiredMs = now - new Date(record.expiresAt).getTime();\n if (expiredMs > EXPIRED_RETENTION_MS) {\n cleaned++;\n continue;\n }\n this.shares.set(record.id, record);\n this.tokenIndex.set(record.token, record.id);\n }\n if (cleaned > 0) {\n log.info({ cleaned }, `Cleaned ${cleaned} expired share records on load`);\n this.persistSync();\n }\n } catch (err) {\n log.warn({ err }, 'Failed to load shares.json');\n }\n }\n\n private persistSync(): void {\n const path = resolveSharesPath();\n mkdirSync(resolveStateDir(), { recursive: true });\n\n let records = [...this.shares.values()];\n if (records.length > MAX_STORED_RECORDS) {\n records.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());\n const active = records.filter((r) => !r.revoked && Date.now() < new Date(r.expiresAt).getTime());\n records = active.slice(0, TRUNCATE_TO);\n\n this.shares.clear();\n this.tokenIndex.clear();\n for (const r of records) {\n this.shares.set(r.id, r);\n this.tokenIndex.set(r.token, r.id);\n }\n }\n\n const data: ShareStoreData = { version: 1, shares: records };\n writeFileSync(path, `${JSON.stringify(data, null, 2)}\\n`, 'utf8');\n }\n\n private scheduleDebouncedPersist(): void {\n this.dirty = true;\n if (this.debounceTimer) return;\n this.debounceTimer = setTimeout(() => {\n this.debounceTimer = null;\n if (this.dirty) {\n this.dirty = false;\n this.persistSync();\n }\n }, COUNTER_DEBOUNCE_MS);\n this.debounceTimer.unref?.();\n }\n\n // ── Cleanup ─────────────────────────────────────────────────────────────────\n\n private startCleanupTimer(): void {\n this.cleanupTimer = setInterval(() => {\n this.cleanupExpired();\n }, CLEANUP_INTERVAL_MS);\n this.cleanupTimer.unref?.();\n }\n\n private cleanupExpired(): void {\n const now = Date.now();\n let removed = 0;\n for (const [id, record] of this.shares) {\n const expiredMs = now - new Date(record.expiresAt).getTime();\n if (expiredMs > EXPIRED_RETENTION_MS) {\n this.shares.delete(id);\n this.tokenIndex.delete(record.token);\n this.invalidateListingCache(id);\n if (this.onCleanup) {\n try {\n this.onCleanup(record);\n } catch (err) {\n log.warn({ err, shareId: id }, 'Share cleanup hook threw');\n }\n }\n removed++;\n }\n }\n if (removed > 0) {\n this.persistSync();\n log.debug({ removed }, `Cleaned ${removed} expired shares`);\n }\n }\n\n shutdown(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer);\n this.debounceTimer = null;\n }\n if (this.dirty) {\n this.dirty = false;\n this.persistSync();\n }\n this.listingCache.clear();\n }\n\n // ── Helpers ─────────────────────────────────────────────────────────────────\n\n private async resolveAndValidatePath(relPath: string, workspaceRoot: string): Promise<string> {\n const trimmed = relPath.trim().replace(/\\\\/g, '/').replace(/^\\/+/, '');\n if (!trimmed) throw new Error('Empty path');\n if (trimmed.includes('..')) throw new Error('Path traversal not allowed');\n if (trimmed.includes('\\0')) throw new Error('Invalid path');\n\n const abs = resolvePath(workspaceRoot, trimmed);\n const root = resolvePath(workspaceRoot);\n const relToRoot = relPathPosix(root, abs);\n if (relToRoot.startsWith('..') || relToRoot.split(/[/\\\\]/).includes('..')) {\n throw new Error('Path is outside workspace');\n }\n return abs;\n }\n}\n\n// ── Directory scan helpers ────────────────────────────────────────────────────\n\nasync function scanDirectory(\n root: string,\n opts: {\n workspaceRoot: string;\n followSymlinks: boolean;\n maxDepth: number;\n maxFileCount: number;\n maxFolderSize: number;\n },\n): Promise<DirectoryScanSummary> {\n let entryCount = 0;\n let totalSize = 0;\n\n async function walk(dir: string, depth: number): Promise<void> {\n if (depth > opts.maxDepth) return;\n const dirents = await readdir(dir, { withFileTypes: true });\n for (const dirent of dirents) {\n if (entryCount >= opts.maxFileCount) {\n throw new Error(`Folder exceeds maxFileCount (${opts.maxFileCount})`);\n }\n const childAbs = resolvePath(dir, dirent.name);\n const childLstat = await lstat(childAbs);\n let effectiveStat = childLstat;\n if (childLstat.isSymbolicLink()) {\n if (!opts.followSymlinks) continue;\n const real = await realpath(childAbs);\n if (!isPathUnderWorkspace(opts.workspaceRoot, real)) {\n throw new Error('Symlink target escapes workspace');\n }\n effectiveStat = await stat(childAbs);\n }\n if (effectiveStat.isFile()) {\n entryCount++;\n totalSize += effectiveStat.size;\n if (totalSize > opts.maxFolderSize) {\n const maxMb = (opts.maxFolderSize / 1_048_576).toFixed(0);\n throw new Error(`Folder exceeds maxFolderSize (${maxMb} MB)`);\n }\n } else if (effectiveStat.isDirectory()) {\n entryCount++;\n await walk(childAbs, depth + 1);\n }\n }\n }\n\n await walk(root, 0);\n return { entryCount, totalSize };\n}\n\n// ── MIME resolution ─────────────────────────────────────────────────────────\n\nconst MIME_BY_EXT: Record<string, string> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n pdf: 'application/pdf',\n txt: 'text/plain',\n md: 'text/markdown',\n json: 'application/json',\n html: 'text/html',\n css: 'text/css',\n js: 'text/javascript',\n mjs: 'text/javascript',\n ts: 'text/typescript',\n xml: 'application/xml',\n csv: 'text/csv',\n zip: 'application/zip',\n gz: 'application/gzip',\n tar: 'application/x-tar',\n mp3: 'audio/mpeg',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n mp4: 'video/mp4',\n webm: 'video/webm',\n mov: 'video/quicktime',\n wasm: 'application/wasm',\n woff: 'font/woff',\n woff2: 'font/woff2',\n ttf: 'font/ttf',\n otf: 'font/otf',\n ico: 'image/x-icon',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n};\n\nexport function resolveMimeType(fileName: string): string {\n const ext = fileName.split('.').pop()?.toLowerCase() ?? '';\n return MIME_BY_EXT[ext] || 'application/octet-stream';\n}\n\n/** HTTP Content-Type with charset for text-like bodies (browser inline preview). */\nexport function shareResponseContentType(mime: string): string {\n if (/;\\s*charset=/i.test(mime)) return mime;\n if (\n mime.startsWith('text/') ||\n mime === 'application/json' ||\n mime === 'application/javascript' ||\n mime === 'application/xml' ||\n mime === 'image/svg+xml'\n ) {\n return `${mime}; charset=utf-8`;\n }\n return mime;\n}\n\n// ── Singleton ─────────────────────────────────────────────────────────────────\n\nlet singleton: ShareStore | null = null;\n\nexport function getShareStore(config?: Partial<ShareConfig>): ShareStore {\n if (!singleton) singleton = new ShareStore(config);\n return singleton;\n}\n\nexport function resetShareStoreForTests(): void {\n singleton?.shutdown();\n singleton = null;\n}\n"],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { ShareThumbnailConfig } from './share-types.js';
2
+ type ThumbnailScope = 'file' | 'site';
3
+ interface ThumbnailTaskInput {
4
+ scope: ThumbnailScope;
5
+ token: string;
6
+ /** Stored record id (used to update status). */
7
+ recordId: string;
8
+ }
9
+ interface ThumbnailRenderContext {
10
+ config: ShareThumbnailConfig;
11
+ /** Loopback gateway base URL (e.g. http://127.0.0.1:18790) used by the renderer. */
12
+ internalBaseUrl: string;
13
+ }
14
+ export declare function getThumbnailPath(token: string): string;
15
+ export declare function thumbnailExists(token: string): Promise<boolean>;
16
+ export declare function readThumbnail(token: string): Promise<Buffer | null>;
17
+ /** Delete the cached thumbnail (called from share-store cleanup hook). */
18
+ export declare function deleteThumbnail(token: string): Promise<void>;
19
+ /**
20
+ * Schedule generation. Returns the current effective status without waiting.
21
+ * If a recent failure is within cooldown, returns 'failed' and skips work.
22
+ */
23
+ export declare function scheduleThumbnail(task: ThumbnailTaskInput, ctx: ThumbnailRenderContext): 'pending' | 'ready' | 'failed' | 'disabled';
24
+ export declare function shutdownThumbnailBrowser(): Promise<void>;
25
+ type PlaceholderKind = 'file' | 'folder' | 'html' | 'image' | 'video' | 'audio' | 'pdf' | 'archive' | 'text' | 'doc' | 'sheet' | 'slides';
26
+ /** Generate a static placeholder card as an SVG-then-jpeg surrogate. Since we
27
+ * have no native SVG-to-raster encoder available without extra deps, we emit a
28
+ * tiny inline-SVG that browsers render directly. Callers store .jpg but the
29
+ * route content-type is set to the actual returned mime. */
30
+ export declare function placeholderSvg(fileName: string, kind: PlaceholderKind): Buffer;
31
+ /** Determine if the cached file is a real jpeg or a placeholder SVG by sniffing. */
32
+ export declare function thumbnailContentType(bytes: Buffer): string;
33
+ /** Helper for tests + cleanup. */
34
+ export declare function resetThumbnailStateForTests(): void;
35
+ export {};
@@ -0,0 +1,277 @@
1
+ import { resolveStateDir } from "../config/paths-state.js";
2
+ import { createLogger } from "../utils/logger/index.js";
3
+ import { init_logger } from "../utils/logger.js";
4
+ import { init_paths } from "../config/paths.js";
5
+ import { getShareStore } from "./share-store.js";
6
+ import { getSiteShareStore } from "./site-share-store.js";
7
+ import { loadPlaywrightCoreModule } from "../browser/providers/playwright-doctor.js";
8
+ import { join } from "node:path";
9
+ import { mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
10
+ //#region src/share/share-thumbnail.ts
11
+ /**
12
+ * Thumbnail generator for shares.
13
+ *
14
+ * Responsibilities:
15
+ * - Render a 1200x630 jpeg preview for shareable artefacts.
16
+ * - HTML / sites: launch its own Playwright browser (does NOT share the user
17
+ * BrowserManager — the user-facing browser is for the agent and we mustn't
18
+ * pollute its context).
19
+ * - Images: pass through with size cap; downscale only if oversized.
20
+ * - Anything else: emit an SVG placeholder card with the file name + icon.
21
+ * - On-disk cache keyed by token, in `<stateDir>/share-thumbnails/`.
22
+ * - Process-wide concurrency cap; failure cooldown.
23
+ */
24
+ init_paths();
25
+ init_logger();
26
+ const log = createLogger("share-thumbnail");
27
+ const THUMBNAIL_DIR = "share-thumbnails";
28
+ let queue = [];
29
+ let inflight = 0;
30
+ const lastFailureAt = /* @__PURE__ */ new Map();
31
+ let browserPromise = null;
32
+ function getThumbnailPath(token) {
33
+ return join(resolveStateDir(), THUMBNAIL_DIR, `${safeToken(token)}.jpg`);
34
+ }
35
+ async function thumbnailExists(token) {
36
+ try {
37
+ const st = await stat(getThumbnailPath(token));
38
+ return st.isFile() && st.size > 0;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ async function readThumbnail(token) {
44
+ try {
45
+ return await readFile(getThumbnailPath(token));
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ /** Delete the cached thumbnail (called from share-store cleanup hook). */
51
+ async function deleteThumbnail(token) {
52
+ try {
53
+ await unlink(getThumbnailPath(token));
54
+ } catch {}
55
+ }
56
+ /**
57
+ * Schedule generation. Returns the current effective status without waiting.
58
+ * If a recent failure is within cooldown, returns 'failed' and skips work.
59
+ */
60
+ function scheduleThumbnail(task, ctx) {
61
+ if (!ctx.config.enabled) return "disabled";
62
+ const failedAt = lastFailureAt.get(task.token);
63
+ if (failedAt && Date.now() - failedAt < ctx.config.failureCooldownMs) return "failed";
64
+ queue.push({
65
+ task,
66
+ ctx
67
+ });
68
+ if (queue.length > 100) queue = queue.slice(-100);
69
+ pumpQueue();
70
+ return "pending";
71
+ }
72
+ function pumpQueue() {
73
+ while (queue.length > 0 && inflight < currentConcurrency()) {
74
+ const next = queue.shift();
75
+ if (!next) break;
76
+ inflight++;
77
+ runThumbnail(next.task, next.ctx).catch((err) => log.warn({
78
+ err,
79
+ token: next.task.token
80
+ }, "Thumbnail task threw")).finally(() => {
81
+ inflight--;
82
+ pumpQueue();
83
+ });
84
+ }
85
+ }
86
+ function currentConcurrency() {
87
+ return Math.max(1, queue[0]?.ctx.config.concurrency ?? 2);
88
+ }
89
+ async function runThumbnail(task, ctx) {
90
+ await mkdir(join(resolveStateDir(), THUMBNAIL_DIR), { recursive: true });
91
+ const target = task.scope === "file" ? getShareStore().getById(task.recordId) : getSiteShareStore().getById(task.recordId);
92
+ if (!target) return;
93
+ try {
94
+ let bytes;
95
+ if (task.scope === "file") bytes = await renderForFile(target, ctx);
96
+ else bytes = await renderForSite(target, ctx);
97
+ if (bytes.length > ctx.config.maxBytes) bytes = bytes.subarray(0, ctx.config.maxBytes);
98
+ await writeFile(getThumbnailPath(task.token), bytes);
99
+ if (task.scope === "file") getShareStore().setThumbnailStatus(task.recordId, "ready");
100
+ else getSiteShareStore().setThumbnailStatus(task.recordId, "ready");
101
+ lastFailureAt.delete(task.token);
102
+ log.debug({
103
+ token: task.token.slice(0, 8),
104
+ scope: task.scope,
105
+ bytes: bytes.length
106
+ }, "Thumbnail generated");
107
+ } catch (err) {
108
+ lastFailureAt.set(task.token, Date.now());
109
+ if (task.scope === "file") getShareStore().setThumbnailStatus(task.recordId, "failed");
110
+ else getSiteShareStore().setThumbnailStatus(task.recordId, "failed");
111
+ log.warn({
112
+ err,
113
+ token: task.token.slice(0, 8),
114
+ scope: task.scope
115
+ }, "Thumbnail generation failed");
116
+ }
117
+ }
118
+ async function renderForFile(record, ctx) {
119
+ if (record.kind === "directory") return placeholderPng(record.fileName, "folder");
120
+ const mime = record.mimeType;
121
+ if (mime === "text/html") return await renderUrlScreenshot(`${ctx.internalBaseUrl.replace(/\/+$/, "")}/s/${record.token}?inline=1`, ctx);
122
+ if (mime.startsWith("image/")) return await readImageWithSizeCap(record.absolutePath, ctx.config.maxBytes);
123
+ return placeholderPng(record.fileName, classifyByMime(mime));
124
+ }
125
+ async function renderForSite(record, ctx) {
126
+ return await renderUrlScreenshot(`${ctx.internalBaseUrl.replace(/\/+$/, "")}/site/${record.token}/`, ctx);
127
+ }
128
+ async function renderUrlScreenshot(url, ctx) {
129
+ const browser = await getOrLaunchBrowser();
130
+ if (!browser) throw new Error("playwright-core / chromium unavailable");
131
+ const context = await browser.newContext({
132
+ viewport: {
133
+ width: ctx.config.viewportWidth,
134
+ height: ctx.config.viewportHeight
135
+ },
136
+ deviceScaleFactor: 1,
137
+ javaScriptEnabled: true,
138
+ bypassCSP: true,
139
+ ignoreHTTPSErrors: true,
140
+ userAgent: "xopc-thumbnail/1.0 (compatible; share preview)"
141
+ });
142
+ await context.route("**/*", (route) => {
143
+ const reqUrl = route.request().url();
144
+ if (reqUrl.startsWith("data:") || reqUrl.startsWith("blob:") || reqUrl.startsWith(ctx.internalBaseUrl) || reqUrl.startsWith("http://127.0.0.1") || reqUrl.startsWith("http://localhost") || reqUrl.startsWith("http://[::1]")) {
145
+ route.continue();
146
+ return;
147
+ }
148
+ route.abort();
149
+ });
150
+ try {
151
+ const page = await context.newPage();
152
+ await page.goto(url, {
153
+ waitUntil: "load",
154
+ timeout: ctx.config.generationTimeoutMs
155
+ });
156
+ try {
157
+ await page.waitForLoadState("networkidle", { timeout: Math.min(ctx.config.generationTimeoutMs, 3e3) });
158
+ } catch {}
159
+ const bytes = await page.screenshot({
160
+ type: "jpeg",
161
+ quality: 80,
162
+ fullPage: false,
163
+ clip: {
164
+ x: 0,
165
+ y: 0,
166
+ width: ctx.config.viewportWidth,
167
+ height: ctx.config.viewportHeight
168
+ }
169
+ });
170
+ return Buffer.from(bytes);
171
+ } finally {
172
+ await context.close().catch(() => {});
173
+ }
174
+ }
175
+ async function getOrLaunchBrowser() {
176
+ if (!browserPromise) browserPromise = (async () => {
177
+ try {
178
+ const pw = await loadPlaywrightCoreModule();
179
+ const chromium = pw.chromium ?? pw.default?.chromium;
180
+ if (!chromium?.launch) return null;
181
+ return await chromium.launch({
182
+ headless: true,
183
+ args: ["--no-sandbox"]
184
+ });
185
+ } catch (err) {
186
+ log.warn({ err }, "Playwright launch failed for thumbnail renderer");
187
+ return null;
188
+ }
189
+ })();
190
+ return browserPromise;
191
+ }
192
+ async function shutdownThumbnailBrowser() {
193
+ if (!browserPromise) return;
194
+ try {
195
+ const b = await browserPromise;
196
+ if (b) await b.close();
197
+ } catch {}
198
+ browserPromise = null;
199
+ queue = [];
200
+ inflight = 0;
201
+ lastFailureAt.clear();
202
+ }
203
+ async function readImageWithSizeCap(absolutePath, cap) {
204
+ const buf = await readFile(absolutePath);
205
+ if (buf.length <= cap) return buf;
206
+ throw new Error(`image exceeds thumbnail size cap (${buf.length} > ${cap})`);
207
+ }
208
+ function classifyByMime(mime) {
209
+ if (mime.startsWith("image/")) return "image";
210
+ if (mime.startsWith("video/")) return "video";
211
+ if (mime.startsWith("audio/")) return "audio";
212
+ if (mime === "application/pdf") return "pdf";
213
+ if (mime === "application/zip" || mime === "application/gzip" || mime === "application/x-tar") return "archive";
214
+ if (mime === "text/html") return "html";
215
+ if (mime.startsWith("text/")) return "text";
216
+ if (mime.includes("spreadsheet")) return "sheet";
217
+ if (mime.includes("presentation")) return "slides";
218
+ if (mime.includes("wordprocessing")) return "doc";
219
+ return "file";
220
+ }
221
+ const ICONS = {
222
+ file: "📄",
223
+ folder: "📁",
224
+ html: "🌐",
225
+ image: "🖼",
226
+ video: "🎬",
227
+ audio: "🎵",
228
+ pdf: "📕",
229
+ archive: "🗜",
230
+ text: "📝",
231
+ doc: "📘",
232
+ sheet: "📊",
233
+ slides: "📽"
234
+ };
235
+ function safeToken(token) {
236
+ return token.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
237
+ }
238
+ /** Generate a static placeholder card as an SVG-then-jpeg surrogate. Since we
239
+ * have no native SVG-to-raster encoder available without extra deps, we emit a
240
+ * tiny inline-SVG that browsers render directly. Callers store .jpg but the
241
+ * route content-type is set to the actual returned mime. */
242
+ function placeholderSvg(fileName, kind) {
243
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
244
+ <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
245
+ <stop offset="0%" stop-color="#0f172a"/><stop offset="100%" stop-color="#1e293b"/></linearGradient></defs>
246
+ <rect width="1200" height="630" fill="url(#g)"/>
247
+ <text x="600" y="300" font-size="200" text-anchor="middle" dominant-baseline="central" font-family="apple color emoji,segoe ui emoji,noto color emoji,sans-serif">${ICONS[kind] ?? ICONS.file}</text>
248
+ <text x="600" y="460" font-size="40" fill="#e2e8f0" text-anchor="middle" font-family="-apple-system,Segoe UI,Roboto,sans-serif" font-weight="600">${escapeXml(fileName.length > 36 ? `${fileName.slice(0, 33)}...` : fileName)}</text>
249
+ <text x="600" y="520" font-size="24" fill="#94a3b8" text-anchor="middle" font-family="-apple-system,Segoe UI,Roboto,sans-serif">Shared via xopc</text>
250
+ </svg>`;
251
+ return Buffer.from(svg, "utf8");
252
+ }
253
+ function placeholderPng(fileName, kind) {
254
+ return placeholderSvg(fileName, kind);
255
+ }
256
+ function escapeXml(text) {
257
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
258
+ }
259
+ /** Determine if the cached file is a real jpeg or a placeholder SVG by sniffing. */
260
+ function thumbnailContentType(bytes) {
261
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
262
+ if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80) return "image/png";
263
+ if (bytes.length >= 4 && bytes[0] === 71 && bytes[1] === 73) return "image/gif";
264
+ if (bytes.length >= 12 && bytes.subarray(0, 4).toString("ascii") === "RIFF" && bytes.subarray(8, 12).toString("ascii") === "WEBP") return "image/webp";
265
+ return "image/svg+xml";
266
+ }
267
+ /** Helper for tests + cleanup. */
268
+ function resetThumbnailStateForTests() {
269
+ queue = [];
270
+ inflight = 0;
271
+ lastFailureAt.clear();
272
+ browserPromise = null;
273
+ }
274
+ //#endregion
275
+ export { deleteThumbnail, getThumbnailPath, placeholderSvg, readThumbnail, resetThumbnailStateForTests, scheduleThumbnail, shutdownThumbnailBrowser, thumbnailContentType, thumbnailExists };
276
+
277
+ //# sourceMappingURL=share-thumbnail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share-thumbnail.js","names":[],"sources":["../../../src/share/share-thumbnail.ts"],"sourcesContent":["/**\n * Thumbnail generator for shares.\n *\n * Responsibilities:\n * - Render a 1200x630 jpeg preview for shareable artefacts.\n * - HTML / sites: launch its own Playwright browser (does NOT share the user\n * BrowserManager — the user-facing browser is for the agent and we mustn't\n * pollute its context).\n * - Images: pass through with size cap; downscale only if oversized.\n * - Anything else: emit an SVG placeholder card with the file name + icon.\n * - On-disk cache keyed by token, in `<stateDir>/share-thumbnails/`.\n * - Process-wide concurrency cap; failure cooldown.\n */\nimport { mkdir, readFile, stat, unlink, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { resolveStateDir } from '../config/paths.js';\nimport { createLogger } from '../utils/logger.js';\nimport { loadPlaywrightCoreModule } from '../browser/providers/playwright-doctor.js';\nimport { getShareStore } from './share-store.js';\nimport { getSiteShareStore } from './site-share-store.js';\nimport type { ShareRecord, ShareThumbnailConfig } from './share-types.js';\nimport type { SiteShareRecord } from './site-share-types.js';\n\nconst log = createLogger('share-thumbnail');\n\nconst THUMBNAIL_DIR = 'share-thumbnails';\n\ntype ThumbnailScope = 'file' | 'site';\n\ninterface ThumbnailTaskInput {\n scope: ThumbnailScope;\n token: string;\n /** Stored record id (used to update status). */\n recordId: string;\n}\n\ninterface ThumbnailRenderContext {\n config: ShareThumbnailConfig;\n /** Loopback gateway base URL (e.g. http://127.0.0.1:18790) used by the renderer. */\n internalBaseUrl: string;\n}\n\nlet queue: Array<{ task: ThumbnailTaskInput; ctx: ThumbnailRenderContext }> = [];\nlet inflight = 0;\nconst lastFailureAt = new Map<string, number>();\nlet browserPromise: Promise<import('playwright-core').Browser | null> | null = null;\n\nexport function getThumbnailPath(token: string): string {\n return join(resolveStateDir(), THUMBNAIL_DIR, `${safeToken(token)}.jpg`);\n}\n\nexport async function thumbnailExists(token: string): Promise<boolean> {\n try {\n const st = await stat(getThumbnailPath(token));\n return st.isFile() && st.size > 0;\n } catch {\n return false;\n }\n}\n\nexport async function readThumbnail(token: string): Promise<Buffer | null> {\n try {\n return await readFile(getThumbnailPath(token));\n } catch {\n return null;\n }\n}\n\n/** Delete the cached thumbnail (called from share-store cleanup hook). */\nexport async function deleteThumbnail(token: string): Promise<void> {\n try {\n await unlink(getThumbnailPath(token));\n } catch {\n /* missing file is fine */\n }\n}\n\n/**\n * Schedule generation. Returns the current effective status without waiting.\n * If a recent failure is within cooldown, returns 'failed' and skips work.\n */\nexport function scheduleThumbnail(\n task: ThumbnailTaskInput,\n ctx: ThumbnailRenderContext,\n): 'pending' | 'ready' | 'failed' | 'disabled' {\n if (!ctx.config.enabled) return 'disabled';\n const failedAt = lastFailureAt.get(task.token);\n if (failedAt && Date.now() - failedAt < ctx.config.failureCooldownMs) {\n return 'failed';\n }\n queue.push({ task, ctx });\n // Cap queue depth so a misbehaving client cannot pin memory.\n if (queue.length > 100) queue = queue.slice(-100);\n pumpQueue();\n return 'pending';\n}\n\nfunction pumpQueue(): void {\n while (queue.length > 0 && inflight < currentConcurrency()) {\n const next = queue.shift();\n if (!next) break;\n inflight++;\n void runThumbnail(next.task, next.ctx)\n .catch((err) => log.warn({ err, token: next.task.token }, 'Thumbnail task threw'))\n .finally(() => {\n inflight--;\n pumpQueue();\n });\n }\n}\n\nfunction currentConcurrency(): number {\n return Math.max(1, queue[0]?.ctx.config.concurrency ?? 2);\n}\n\nasync function runThumbnail(\n task: ThumbnailTaskInput,\n ctx: ThumbnailRenderContext,\n): Promise<void> {\n await mkdir(join(resolveStateDir(), THUMBNAIL_DIR), { recursive: true });\n const target = task.scope === 'file' ? getShareStore().getById(task.recordId) : getSiteShareStore().getById(task.recordId);\n if (!target) return;\n try {\n let bytes: Buffer;\n if (task.scope === 'file') {\n bytes = await renderForFile(target as ShareRecord, ctx);\n } else {\n bytes = await renderForSite(target as SiteShareRecord, ctx);\n }\n if (bytes.length > ctx.config.maxBytes) {\n // Re-encode lossily by re-running through a smaller viewport if possible\n bytes = bytes.subarray(0, ctx.config.maxBytes);\n }\n await writeFile(getThumbnailPath(task.token), bytes);\n if (task.scope === 'file') getShareStore().setThumbnailStatus(task.recordId, 'ready');\n else getSiteShareStore().setThumbnailStatus(task.recordId, 'ready');\n lastFailureAt.delete(task.token);\n log.debug({ token: task.token.slice(0, 8), scope: task.scope, bytes: bytes.length }, 'Thumbnail generated');\n } catch (err) {\n lastFailureAt.set(task.token, Date.now());\n if (task.scope === 'file') getShareStore().setThumbnailStatus(task.recordId, 'failed');\n else getSiteShareStore().setThumbnailStatus(task.recordId, 'failed');\n log.warn({ err, token: task.token.slice(0, 8), scope: task.scope }, 'Thumbnail generation failed');\n }\n}\n\nasync function renderForFile(record: ShareRecord, ctx: ThumbnailRenderContext): Promise<Buffer> {\n if (record.kind === 'directory') {\n return placeholderPng(record.fileName, 'folder');\n }\n const mime = record.mimeType;\n if (mime === 'text/html') {\n const url = `${ctx.internalBaseUrl.replace(/\\/+$/, '')}/s/${record.token}?inline=1`;\n return await renderUrlScreenshot(url, ctx);\n }\n if (mime.startsWith('image/')) {\n return await readImageWithSizeCap(record.absolutePath, ctx.config.maxBytes);\n }\n return placeholderPng(record.fileName, classifyByMime(mime));\n}\n\nasync function renderForSite(record: SiteShareRecord, ctx: ThumbnailRenderContext): Promise<Buffer> {\n const url = `${ctx.internalBaseUrl.replace(/\\/+$/, '')}/site/${record.token}/`;\n return await renderUrlScreenshot(url, ctx);\n}\n\nasync function renderUrlScreenshot(url: string, ctx: ThumbnailRenderContext): Promise<Buffer> {\n const browser = await getOrLaunchBrowser();\n if (!browser) {\n throw new Error('playwright-core / chromium unavailable');\n }\n const context = await browser.newContext({\n viewport: { width: ctx.config.viewportWidth, height: ctx.config.viewportHeight },\n deviceScaleFactor: 1,\n javaScriptEnabled: true,\n bypassCSP: true,\n ignoreHTTPSErrors: true,\n userAgent: 'xopc-thumbnail/1.0 (compatible; share preview)',\n });\n // Block non-loopback network so a malicious share cannot use us to scan/contact external hosts.\n await context.route('**/*', (route) => {\n const reqUrl = route.request().url();\n if (\n reqUrl.startsWith('data:') ||\n reqUrl.startsWith('blob:') ||\n reqUrl.startsWith(ctx.internalBaseUrl) ||\n reqUrl.startsWith('http://127.0.0.1') ||\n reqUrl.startsWith('http://localhost') ||\n reqUrl.startsWith('http://[::1]')\n ) {\n void route.continue();\n return;\n }\n void route.abort();\n });\n try {\n const page = await context.newPage();\n await page.goto(url, { waitUntil: 'load', timeout: ctx.config.generationTimeoutMs });\n // Best-effort wait for network idle, capped by generationTimeoutMs.\n try {\n await page.waitForLoadState('networkidle', { timeout: Math.min(ctx.config.generationTimeoutMs, 3_000) });\n } catch {\n /* networkidle may never settle for animation pages — ignore */\n }\n const bytes = await page.screenshot({\n type: 'jpeg',\n quality: 80,\n fullPage: false,\n clip: { x: 0, y: 0, width: ctx.config.viewportWidth, height: ctx.config.viewportHeight },\n });\n return Buffer.from(bytes);\n } finally {\n await context.close().catch(() => {});\n }\n}\n\nasync function getOrLaunchBrowser(): Promise<import('playwright-core').Browser | null> {\n if (!browserPromise) {\n browserPromise = (async () => {\n try {\n const pw = await loadPlaywrightCoreModule();\n const chromium = pw.chromium\n ?? (pw as { default?: { chromium?: (typeof pw)['chromium'] } }).default?.chromium;\n if (!chromium?.launch) return null;\n return await chromium.launch({ headless: true, args: ['--no-sandbox'] });\n } catch (err) {\n log.warn({ err }, 'Playwright launch failed for thumbnail renderer');\n return null;\n }\n })();\n }\n return browserPromise;\n}\n\nexport async function shutdownThumbnailBrowser(): Promise<void> {\n if (!browserPromise) return;\n try {\n const b = await browserPromise;\n if (b) await b.close();\n } catch {\n /* ignore */\n }\n browserPromise = null;\n queue = [];\n inflight = 0;\n lastFailureAt.clear();\n}\n\nasync function readImageWithSizeCap(absolutePath: string, cap: number): Promise<Buffer> {\n const buf = await readFile(absolutePath);\n if (buf.length <= cap) return buf;\n // We don't ship a real raster encoder here; for tiny budgets the recipient\n // sees a placeholder rather than a truncated/broken jpeg.\n throw new Error(`image exceeds thumbnail size cap (${buf.length} > ${cap})`);\n}\n\nfunction classifyByMime(mime: string): PlaceholderKind {\n if (mime.startsWith('image/')) return 'image';\n if (mime.startsWith('video/')) return 'video';\n if (mime.startsWith('audio/')) return 'audio';\n if (mime === 'application/pdf') return 'pdf';\n if (mime === 'application/zip' || mime === 'application/gzip' || mime === 'application/x-tar') return 'archive';\n if (mime === 'text/html') return 'html';\n if (mime.startsWith('text/')) return 'text';\n if (mime.includes('spreadsheet')) return 'sheet';\n if (mime.includes('presentation')) return 'slides';\n if (mime.includes('wordprocessing')) return 'doc';\n return 'file';\n}\n\ntype PlaceholderKind =\n | 'file' | 'folder' | 'html' | 'image' | 'video' | 'audio'\n | 'pdf' | 'archive' | 'text' | 'doc' | 'sheet' | 'slides';\n\nconst ICONS: Record<PlaceholderKind, string> = {\n file: '📄', folder: '📁', html: '🌐', image: '🖼', video: '🎬', audio: '🎵',\n pdf: '📕', archive: '🗜', text: '📝', doc: '📘', sheet: '📊', slides: '📽',\n};\n\nfunction safeToken(token: string): string {\n return token.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);\n}\n\n/** Generate a static placeholder card as an SVG-then-jpeg surrogate. Since we\n * have no native SVG-to-raster encoder available without extra deps, we emit a\n * tiny inline-SVG that browsers render directly. Callers store .jpg but the\n * route content-type is set to the actual returned mime. */\nexport function placeholderSvg(fileName: string, kind: PlaceholderKind): Buffer {\n const icon = ICONS[kind] ?? ICONS.file;\n const safe = escapeXml(fileName.length > 36 ? `${fileName.slice(0, 33)}...` : fileName);\n const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">\n<defs><linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n<stop offset=\"0%\" stop-color=\"#0f172a\"/><stop offset=\"100%\" stop-color=\"#1e293b\"/></linearGradient></defs>\n<rect width=\"1200\" height=\"630\" fill=\"url(#g)\"/>\n<text x=\"600\" y=\"300\" font-size=\"200\" text-anchor=\"middle\" dominant-baseline=\"central\" font-family=\"apple color emoji,segoe ui emoji,noto color emoji,sans-serif\">${icon}</text>\n<text x=\"600\" y=\"460\" font-size=\"40\" fill=\"#e2e8f0\" text-anchor=\"middle\" font-family=\"-apple-system,Segoe UI,Roboto,sans-serif\" font-weight=\"600\">${safe}</text>\n<text x=\"600\" y=\"520\" font-size=\"24\" fill=\"#94a3b8\" text-anchor=\"middle\" font-family=\"-apple-system,Segoe UI,Roboto,sans-serif\">Shared via xopc</text>\n</svg>`;\n return Buffer.from(svg, 'utf8');\n}\n\nfunction placeholderPng(fileName: string, kind: PlaceholderKind): Buffer {\n // Returned as SVG bytes — caller's content-type must be `image/svg+xml`.\n return placeholderSvg(fileName, kind);\n}\n\nfunction escapeXml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\n/** Determine if the cached file is a real jpeg or a placeholder SVG by sniffing. */\nexport function thumbnailContentType(bytes: Buffer): string {\n if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return 'image/jpeg';\n if (bytes.length >= 8 && bytes[0] === 0x89 && bytes[1] === 0x50) return 'image/png';\n if (bytes.length >= 4 && bytes[0] === 0x47 && bytes[1] === 0x49) return 'image/gif';\n if (bytes.length >= 12 && bytes.subarray(0, 4).toString('ascii') === 'RIFF' && bytes.subarray(8, 12).toString('ascii') === 'WEBP') return 'image/webp';\n return 'image/svg+xml';\n}\n\n/** Helper for tests + cleanup. */\nexport function resetThumbnailStateForTests(): void {\n queue = [];\n inflight = 0;\n lastFailureAt.clear();\n browserPromise = null;\n}\n"],"mappings":"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"}
@@ -1,3 +1,14 @@
1
+ export type ShareKind = 'file' | 'directory';
2
+ export interface ShareDirectoryMeta {
3
+ /** 'browse' shows a file tree; 'zip-only' hides structure and only offers ZIP. */
4
+ mode: 'browse' | 'zip-only';
5
+ /** Snapshot entry count at creation time (display + sanity guard). */
6
+ entryCount: number;
7
+ /** Whether the symlink filter at scan time was permissive. */
8
+ followSymlinks: boolean;
9
+ /** Maximum traversal depth (defense against recursive symlinks). */
10
+ maxDepth: number;
11
+ }
1
12
  export interface ShareRecord {
2
13
  /** Unique identifier (UUIDv4) for management. */
3
14
  id: string;
@@ -9,37 +20,48 @@ export interface ShareRecord {
9
20
  workspaceRelativePath: string;
10
21
  /** Workspace root at creation time (for download-time re-validation). */
11
22
  workspaceRoot: string;
12
- /** File inode at creation time (TOCTOU protection). */
23
+ /** File or directory inode at creation time (TOCTOU protection). */
13
24
  inode: number;
14
- /** Whether this share targets a directory. */
15
- isDirectory: boolean;
16
- /** Original file name (basename). */
25
+ /** Discriminator for downstream branching ('file' | 'directory'). */
26
+ kind: ShareKind;
27
+ /** Original file/folder name (basename). */
17
28
  fileName: string;
18
- /** File size in bytes (snapshot at creation). */
29
+ /**
30
+ * For files: file size in bytes (snapshot at creation).
31
+ * For directories: sum of file sizes at scan time (display only).
32
+ */
19
33
  fileSize: number;
20
- /** MIME type. */
34
+ /** MIME type (directories: 'application/x-directory'). */
21
35
  mimeType: string;
22
36
  /** ISO creation timestamp. */
23
37
  createdAt: string;
24
38
  /** ISO expiration timestamp. */
25
39
  expiresAt: string;
26
- /** Maximum allowed views (null = unlimited). */
40
+ /** Maximum allowed downloads (null = unlimited). Counts file/zip/preview events. */
27
41
  maxViews: number | null;
28
- /** Current view count. */
29
- viewCount: number;
42
+ /** Download counter (file-bytes / zip-bytes / preview events). Landing-page renders never count. */
43
+ downloadCount: number;
30
44
  /** Whether manually revoked. */
31
45
  revoked: boolean;
32
46
  /** Gateway token SHA-256 hash prefix (12 chars) of the creator. */
33
47
  createdByTokenHash: string;
34
48
  /** Optional human-readable description. */
35
49
  description?: string;
50
+ /** Directory-only extra fields. */
51
+ directory?: ShareDirectoryMeta;
52
+ /** Thumbnail generation status (lazy + scheduled). */
53
+ thumbnailStatus?: 'pending' | 'ready' | 'failed';
54
+ /** ISO timestamp of the last successful thumbnail render. */
55
+ thumbnailGeneratedAt?: string;
56
+ /** ISO timestamp of the last failed attempt (used for cooldown). */
57
+ thumbnailFailedAt?: string;
36
58
  }
37
59
  export interface CreateShareParams {
38
60
  /** Workspace-relative file path. */
39
61
  path: string;
40
62
  /** Time-to-live in milliseconds (default: 24h). */
41
63
  ttlMs?: number;
42
- /** Maximum view count (null = unlimited). */
64
+ /** Maximum view/download count (null = unlimited). */
43
65
  maxViews?: number | null;
44
66
  /** Optional description shown on the landing page. */
45
67
  description?: string;
@@ -47,6 +69,18 @@ export interface CreateShareParams {
47
69
  sessionKey?: string;
48
70
  /** Agent id to resolve workspace root. */
49
71
  agentId?: string;
72
+ /** Force directory share semantics (overrides auto-detection). */
73
+ kind?: ShareKind;
74
+ /** Directory: browse vs zip-only. Defaults to 'browse'. */
75
+ directoryMode?: 'browse' | 'zip-only';
76
+ /** Directory: cap on entry count at scan time. */
77
+ maxFileCount?: number;
78
+ /** Directory: cap on total folder size in bytes. */
79
+ maxFolderSize?: number;
80
+ /** Directory: whether to follow symlinks (subject to workspace boundary). */
81
+ followSymlinks?: boolean;
82
+ /** Directory: walk depth cap (default 20). */
83
+ maxDepth?: number;
50
84
  }
51
85
  export interface ShareStoreData {
52
86
  version: 1;
@@ -59,6 +93,28 @@ export interface ResolvedShareUrl {
59
93
  reachability: ShareReachability;
60
94
  reachabilityHint: string | null;
61
95
  }
96
+ export interface ShareDirectoryConfig {
97
+ enabled: boolean;
98
+ maxFolderSize: number;
99
+ maxFileCount: number;
100
+ maxDepth: number;
101
+ listingCacheMs: number;
102
+ zipConcurrency: number;
103
+ }
104
+ export interface ShareThumbnailConfig {
105
+ enabled: boolean;
106
+ concurrency: number;
107
+ /** Hard cap on emitted jpeg bytes. */
108
+ maxBytes: number;
109
+ viewportWidth: number;
110
+ viewportHeight: number;
111
+ /** Per-render timeout (Playwright navigation + screenshot). */
112
+ generationTimeoutMs: number;
113
+ /** Cooldown before a failed token is retried. */
114
+ failureCooldownMs: number;
115
+ /** Optional override for the loopback URL used by the headless renderer. */
116
+ internalGatewayUrl?: string;
117
+ }
62
118
  export interface ShareConfig {
63
119
  enabled: boolean;
64
120
  defaultTtlMs: number;
@@ -66,6 +122,8 @@ export interface ShareConfig {
66
122
  maxActiveShares: number;
67
123
  maxFileSize: number;
68
124
  inlinePreviewMimes: string[];
125
+ directory: ShareDirectoryConfig;
126
+ thumbnail: ShareThumbnailConfig;
69
127
  }
70
128
  /** Default share configuration values. */
71
129
  export declare const SHARE_CONFIG_DEFAULTS: ShareConfig;