@xopcai/xopc 0.0.83 → 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.
- package/README.md +2 -0
- package/README.zh-CN.md +3 -1
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/feishu/src/outbound/media-load.js +1 -1
- package/dist/extensions/feishu/src/plugin.d.ts +2 -0
- package/dist/extensions/feishu/src/plugin.js +10 -0
- package/dist/extensions/feishu/src/plugin.js.map +1 -1
- package/dist/extensions/feishu/src/workflow-progress.d.ts +27 -0
- package/dist/extensions/feishu/src/workflow-progress.js +99 -0
- package/dist/extensions/feishu/src/workflow-progress.js.map +1 -0
- package/dist/extensions/telegram/src/plugin.d.ts +2 -0
- package/dist/extensions/telegram/src/plugin.js +11 -1
- package/dist/extensions/telegram/src/plugin.js.map +1 -1
- package/dist/extensions/telegram/src/routing-integration.js +2 -2
- package/dist/extensions/telegram/src/workflow-progress.d.ts +24 -0
- package/dist/extensions/telegram/src/workflow-progress.js +73 -0
- package/dist/extensions/telegram/src/workflow-progress.js.map +1 -0
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/extensions/weixin/src/__tests__/workflow-progress.test.js +158 -0
- package/dist/extensions/weixin/src/__tests__/workflow-progress.test.js.map +1 -0
- package/dist/extensions/weixin/src/api/api.js +2 -2
- package/dist/extensions/weixin/src/auth/accounts.js +1 -1
- package/dist/extensions/weixin/src/cdn/upload.js +1 -1
- package/dist/extensions/weixin/src/media/data-url.js +1 -1
- package/dist/extensions/weixin/src/messaging/debug-mode.js +1 -1
- package/dist/extensions/weixin/src/messaging/inbound.js +1 -1
- package/dist/extensions/weixin/src/messaging/process-message.js +1 -1
- package/dist/extensions/weixin/src/plugin.d.ts +2 -0
- package/dist/extensions/weixin/src/plugin.js +11 -1
- package/dist/extensions/weixin/src/plugin.js.map +1 -1
- package/dist/extensions/weixin/src/storage/sync-buf.js +1 -1
- package/dist/extensions/weixin/src/workflow-progress.d.ts +26 -0
- package/dist/extensions/weixin/src/workflow-progress.js +99 -0
- package/dist/extensions/weixin/src/workflow-progress.js.map +1 -0
- package/dist/gateway/static/root/assets/agents-D3_-kNlZ.js +222 -0
- package/dist/gateway/static/root/assets/apps-page-D7v7649T.js +1 -0
- package/dist/gateway/static/root/assets/channels-settings-nCaMb0a7.js +1 -0
- package/dist/gateway/static/root/assets/channels-status-swr-C1gZBcJV.js +8 -0
- package/dist/gateway/static/root/assets/createLucideIcon-DPHK1VkS.js +1 -0
- package/dist/gateway/static/root/assets/cron-api-CoYK0hlm.js +1 -0
- package/dist/gateway/static/root/assets/cron-page-DeGo-Vjc.js +1 -0
- package/dist/gateway/static/root/assets/dist-BTWC-BTN.js +45 -0
- package/dist/gateway/static/root/assets/{dist-BpQxde0t.js → dist-DaK4dsss.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-CY27wj_p.js → extension-debug-page-BZngZWbO.js} +1 -1
- package/dist/gateway/static/root/assets/extension-page-D6JSyV27.js +1 -0
- package/dist/gateway/static/root/assets/extension-settings-page-8PZcmWI7.js +1 -0
- package/dist/gateway/static/root/assets/fetch-B2MYHbWg.js +1 -0
- package/dist/gateway/static/root/assets/{field-primitives-fa_hiQcX.js → field-primitives-Zzl22MvN.js} +1 -1
- package/dist/gateway/static/root/assets/heartbeat-config-api-BtIcpG0O.js +1 -0
- package/dist/gateway/static/root/assets/index-D4vM3-P7.js +4700 -0
- package/dist/gateway/static/root/assets/index-ew_2L2We.css +1 -0
- package/dist/gateway/static/root/assets/logs-page-_d4UJ-qQ.js +1 -0
- package/dist/gateway/static/root/assets/sessions-page-5N4aF2Wk.js +1 -0
- package/dist/gateway/static/root/assets/settings-form-section-D_tgb8r2.js +1 -0
- package/dist/gateway/static/root/assets/settings-page-C18xBt4X.js +3 -0
- package/dist/gateway/static/root/assets/share-preview-page-D4EG_vM1.js +2 -0
- package/dist/gateway/static/root/assets/skills-page-sPAXhh8w.js +2 -0
- package/dist/gateway/static/root/assets/theme-store-DryYl3qD.js +1 -0
- package/dist/gateway/static/root/assets/url-BwNL6Rgk.js +3 -0
- package/dist/gateway/static/root/assets/utils-CYO9eTCM.js +1 -0
- package/dist/gateway/static/root/assets/voice-api-key-field-Ds51havm.js +1 -0
- package/dist/gateway/static/root/index.html +7 -6
- package/dist/package.js +1 -1
- package/dist/src/agent/agent-manager.js +7 -7
- package/dist/src/agent/bootstrap/load-bootstrap-files.js +1 -1
- package/dist/src/agent/context/workspace-seed.js +3 -3
- package/dist/src/agent/embedded/map-stream-events.js +6 -0
- package/dist/src/agent/embedded/map-stream-events.js.map +1 -1
- package/dist/src/agent/embedded/subscribe-session.js +24 -0
- package/dist/src/agent/embedded/subscribe-session.js.map +1 -1
- package/dist/src/agent/embedded/types.d.ts +19 -0
- package/dist/src/agent/goals/goal-locale.js +2 -2
- package/dist/src/agent/goals/goal-run-store.js +4 -4
- package/dist/src/agent/goals/persistent-goal-service.js +1 -1
- package/dist/src/agent/goals/post-turn.js +2 -2
- package/dist/src/agent/image/load-image-media.js +2 -2
- package/dist/src/agent/ipc/bus.js +1 -1
- package/dist/src/agent/ipc/inbox.js +2 -2
- package/dist/src/agent/ipc/socket.js +1 -1
- package/dist/src/agent/memory/builtin-memory-store.js +1 -1
- package/dist/src/agent/memory/dreaming/deep-promotion.js +1 -1
- package/dist/src/agent/memory/dreaming/events.js +1 -1
- package/dist/src/agent/memory/dreaming/last-run.js +1 -1
- package/dist/src/agent/memory/dreaming/light-sweep.js +1 -1
- package/dist/src/agent/memory/dreaming/preview.js +1 -1
- package/dist/src/agent/memory/dreaming/rem-patterns.js +1 -1
- package/dist/src/agent/memory/dreaming/short-term-store.js +1 -1
- package/dist/src/agent/memory/dreaming/utils.js +1 -1
- package/dist/src/agent/memory/plugin-discovery.js +1 -1
- package/dist/src/agent/models/manager.js +1 -1
- package/dist/src/agent/prompt/service-prompt-builder.js +2 -2
- package/dist/src/agent/reply/post-compaction-context.js +1 -1
- package/dist/src/agent/reply/startup-context.d.ts +3 -0
- package/dist/src/agent/reply/startup-context.js +25 -2
- package/dist/src/agent/reply/startup-context.js.map +1 -1
- package/dist/src/agent/reply/workspace-boundary-read.js +1 -1
- package/dist/src/agent/sandbox/path-policy.js +2 -2
- package/dist/src/agent/service/build-direct-message-content.js +1 -1
- package/dist/src/agent/service.d.ts +1 -0
- package/dist/src/agent/service.js +10 -4
- package/dist/src/agent/service.js.map +1 -1
- package/dist/src/agent/session/session-inspector.js +1 -1
- package/dist/src/agent/skills/config.js +1 -1
- package/dist/src/agent/skills/hub-hash.js +2 -2
- package/dist/src/agent/skills/hub-lock.js +1 -1
- package/dist/src/agent/skills/hub-pull.js +3 -3
- package/dist/src/agent/skills/index.js +1 -1
- package/dist/src/agent/skills/managed-store.js +1 -1
- package/dist/src/agent/skills/scanner.js +1 -1
- package/dist/src/agent/skills/skill-manage-ops.js +1 -1
- package/dist/src/agent/skills/skill-manager.js +1 -1
- package/dist/src/agent/tools/create-share-tool.d.ts +27 -0
- package/dist/src/agent/tools/create-share-tool.js +237 -0
- package/dist/src/agent/tools/create-share-tool.js.map +1 -0
- package/dist/src/agent/tools/dreaming-tool.js +1 -1
- package/dist/src/agent/tools/factory.js +35 -1
- package/dist/src/agent/tools/factory.js.map +1 -1
- package/dist/src/agent/tools/image-generate-tool.js +1 -1
- package/dist/src/agent/tools/index.d.ts +2 -0
- package/dist/src/agent/tools/index.js +3 -1
- package/dist/src/agent/tools/send-media.js +1 -1
- package/dist/src/agent/tools/skill-manage-tool.js +1 -1
- package/dist/src/agent/tools/workflow-tool.d.ts +41 -0
- package/dist/src/agent/tools/workflow-tool.js +271 -0
- package/dist/src/agent/tools/workflow-tool.js.map +1 -0
- package/dist/src/agent/tools/write.js +1 -1
- package/dist/src/agent/workflow/builtins/audit-repo.d.ts +9 -0
- package/dist/src/agent/workflow/builtins/audit-repo.js +115 -0
- package/dist/src/agent/workflow/builtins/audit-repo.js.map +1 -0
- package/dist/src/agent/workflow/builtins/index.d.ts +15 -0
- package/dist/src/agent/workflow/builtins/index.js +28 -0
- package/dist/src/agent/workflow/builtins/index.js.map +1 -0
- package/dist/src/agent/workflow/builtins/multi-perspective-review.d.ts +9 -0
- package/dist/src/agent/workflow/builtins/multi-perspective-review.js +113 -0
- package/dist/src/agent/workflow/builtins/multi-perspective-review.js.map +1 -0
- package/dist/src/agent/workflow/builtins/research.d.ts +9 -0
- package/dist/src/agent/workflow/builtins/research.js +129 -0
- package/dist/src/agent/workflow/builtins/research.js.map +1 -0
- package/dist/src/agent/workflow/catalog.d.ts +51 -0
- package/dist/src/agent/workflow/catalog.js +155 -0
- package/dist/src/agent/workflow/catalog.js.map +1 -0
- package/dist/src/agent/workflow/channel-capability.d.ts +76 -0
- package/dist/src/agent/workflow/channel-capability.js +1 -0
- package/dist/src/agent/workflow/index.d.ts +11 -0
- package/dist/src/agent/workflow/index.js +10 -0
- package/dist/src/agent/workflow/last-run-memory.d.ts +42 -0
- package/dist/src/agent/workflow/last-run-memory.js +60 -0
- package/dist/src/agent/workflow/last-run-memory.js.map +1 -0
- package/dist/src/agent/workflow/parser.d.ts +20 -0
- package/dist/src/agent/workflow/parser.js +137 -0
- package/dist/src/agent/workflow/parser.js.map +1 -0
- package/dist/src/agent/workflow/progress-broker.d.ts +80 -0
- package/dist/src/agent/workflow/progress-broker.js +263 -0
- package/dist/src/agent/workflow/progress-broker.js.map +1 -0
- package/dist/src/agent/workflow/runtime.d.ts +31 -0
- package/dist/src/agent/workflow/runtime.js +301 -0
- package/dist/src/agent/workflow/runtime.js.map +1 -0
- package/dist/src/agent/workflow/snapshot.d.ts +18 -0
- package/dist/src/agent/workflow/snapshot.js +144 -0
- package/dist/src/agent/workflow/snapshot.js.map +1 -0
- package/dist/src/agent/workflow/structured-output-tool.d.ts +33 -0
- package/dist/src/agent/workflow/structured-output-tool.js +58 -0
- package/dist/src/agent/workflow/structured-output-tool.js.map +1 -0
- package/dist/src/agent/workflow/subagent-runner.d.ts +42 -0
- package/dist/src/agent/workflow/subagent-runner.js +104 -0
- package/dist/src/agent/workflow/subagent-runner.js.map +1 -0
- package/dist/src/agent/workflow/types.d.ts +137 -0
- package/dist/src/agent/workflow/types.js +1 -0
- package/dist/src/auth/credentials.js +3 -3
- package/dist/src/auth/profiles/store.js +1 -1
- package/dist/src/auth/sync-provider-auth.js +1 -1
- package/dist/src/browser/cache-dir-policy.js +1 -1
- package/dist/src/browser/cdp-local-launcher.js +2 -2
- package/dist/src/browser/providers/browser-ext-install.js +4 -4
- package/dist/src/browser/providers/cloakbrowser.js +4 -4
- package/dist/src/browser/providers/playwright-doctor.js +1 -1
- package/dist/src/browser/stealth.js +1 -1
- package/dist/src/channels/attachments/inbound-persist.js +1 -1
- package/dist/src/channels/attachments/outbound-tts-persist.js +1 -1
- package/dist/src/channels/outbound/persist-store.js +1 -1
- package/dist/src/channels/pairing/allow-from-file.js +1 -1
- package/dist/src/channels/pairing/pairing-store.js +2 -2
- package/dist/src/chat-commands/builtins/config.js +2 -2
- package/dist/src/chat-commands/builtins/model.js +40 -23
- package/dist/src/chat-commands/builtins/model.js.map +1 -1
- package/dist/src/chat-commands/builtins/system.js +30 -15
- package/dist/src/chat-commands/builtins/system.js.map +1 -1
- package/dist/src/chat-commands/builtins/workflow.d.ts +18 -0
- package/dist/src/chat-commands/builtins/workflow.js +167 -0
- package/dist/src/chat-commands/builtins/workflow.js.map +1 -0
- package/dist/src/chat-commands/context.js +1 -1
- package/dist/src/chat-commands/format-output.d.ts +28 -0
- package/dist/src/chat-commands/format-output.js +45 -0
- package/dist/src/chat-commands/format-output.js.map +1 -0
- package/dist/src/chat-commands/index.d.ts +1 -0
- package/dist/src/chat-commands/index.js +3 -1
- package/dist/src/chat-commands/index.js.map +1 -1
- package/dist/src/cli/command-catalog.js +110 -8
- package/dist/src/cli/command-catalog.js.map +1 -1
- package/dist/src/cli/command-loaders.js +2 -0
- package/dist/src/cli/command-loaders.js.map +1 -1
- package/dist/src/cli/command-manifest.js +9 -1
- package/dist/src/cli/command-manifest.js.map +1 -1
- package/dist/src/cli/commands/config.js +71 -20
- package/dist/src/cli/commands/config.js.map +1 -1
- package/dist/src/cli/commands/cron-cli.d.ts +2 -0
- package/dist/src/cli/commands/cron-cli.js +15 -0
- package/dist/src/cli/commands/cron-cli.js.map +1 -0
- package/dist/src/cli/commands/cron.d.ts +4 -1
- package/dist/src/cli/commands/cron.js +76 -41
- package/dist/src/cli/commands/cron.js.map +1 -1
- package/dist/src/cli/commands/doctor/checks/channel-config.js +1 -1
- package/dist/src/cli/commands/doctor/checks/channel-config.js.map +1 -1
- package/dist/src/cli/commands/doctor/checks/config-health.js +2 -2
- package/dist/src/cli/commands/doctor/checks/config-health.js.map +1 -1
- package/dist/src/cli/commands/doctor/checks/cron-health.js +1 -1
- package/dist/src/cli/commands/doctor/checks/cron-health.js.map +1 -1
- package/dist/src/cli/commands/doctor/checks/gateway-health.js +2 -2
- package/dist/src/cli/commands/doctor/checks/gateway-health.js.map +1 -1
- package/dist/src/cli/commands/doctor/checks/gateway-service.js +2 -2
- package/dist/src/cli/commands/doctor/checks/gateway-service.js.map +1 -1
- package/dist/src/cli/commands/doctor/checks/provider-auth.js +1 -1
- package/dist/src/cli/commands/doctor/checks/session-integrity.js +1 -1
- package/dist/src/cli/commands/doctor/checks/state-integrity.js +2 -2
- package/dist/src/cli/commands/doctor/checks/state-integrity.js.map +1 -1
- package/dist/src/cli/commands/doctor/checks/workspace-status.js +4 -4
- package/dist/src/cli/commands/doctor/checks/workspace-status.js.map +1 -1
- package/dist/src/cli/commands/extension-dev.js +1 -1
- package/dist/src/cli/commands/extension-marketplace.js +1 -1
- package/dist/src/cli/commands/extension-pack.js +1 -1
- package/dist/src/cli/commands/gateway/index.d.ts +1 -1
- package/dist/src/cli/commands/gateway/index.js +2 -2
- package/dist/src/cli/commands/gateway/lifecycle.js +10 -4
- package/dist/src/cli/commands/gateway/lifecycle.js.map +1 -1
- package/dist/src/cli/commands/gateway/service.d.ts +4 -0
- package/dist/src/cli/commands/gateway/service.js +17 -2
- package/dist/src/cli/commands/gateway/service.js.map +1 -1
- package/dist/src/cli/commands/gateway/shared.js +1 -1
- package/dist/src/cli/commands/gateway/subcommands.js +1 -4
- package/dist/src/cli/commands/gateway/subcommands.js.map +1 -1
- package/dist/src/cli/commands/image.js +1 -1
- package/dist/src/cli/commands/init.js +31 -4
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/models.d.ts +4 -1
- package/dist/src/cli/commands/models.js +86 -74
- package/dist/src/cli/commands/models.js.map +1 -1
- package/dist/src/cli/commands/onboard.js +4 -2
- package/dist/src/cli/commands/onboard.js.map +1 -1
- package/dist/src/cli/commands/profile.d.ts +3 -5
- package/dist/src/cli/commands/profile.js +31 -31
- package/dist/src/cli/commands/profile.js.map +1 -1
- package/dist/src/cli/commands/setup.js +6 -1
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/dist/src/cli/commands/tunnel.js +2 -2
- package/dist/src/cli/gateway-run-argv.js +15 -5
- package/dist/src/cli/gateway-run-argv.js.map +1 -1
- package/dist/src/cli/utils/gateway-client.js +1 -1
- package/dist/src/cli/utils/init-workspace-core.js +2 -2
- package/dist/src/config/agent-profile.js +1 -1
- package/dist/src/config/gateway-bind.js +1 -1
- package/dist/src/config/index.js +5 -5
- package/dist/src/config/loader.js +2 -2
- package/dist/src/config/models-json.js +2 -2
- package/dist/src/config/paths-state.js +1 -1
- package/dist/src/config/profile.js +2 -2
- package/dist/src/config/public-url.d.ts +28 -0
- package/dist/src/config/public-url.js +103 -0
- package/dist/src/config/public-url.js.map +1 -0
- package/dist/src/config/schema.d.ts +82 -0
- package/dist/src/config/schema.js +130 -1
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/config/workspace-path.js +1 -1
- package/dist/src/cron/executor.js +2 -2
- package/dist/src/cron/persistence.js +1 -1
- package/dist/src/cron/run-log-store.js +1 -1
- package/dist/src/daemon/constants.js +1 -1
- package/dist/src/daemon/install-plan.js +3 -3
- package/dist/src/daemon/install-plan.js.map +1 -1
- package/dist/src/daemon/launchd.js +2 -2
- package/dist/src/daemon/schtasks.js +38 -1
- package/dist/src/daemon/schtasks.js.map +1 -1
- package/dist/src/daemon/systemd.js +2 -2
- package/dist/src/extensions/bundle-mcp.js +1 -1
- package/dist/src/extensions/discover-extensions.js +1 -1
- package/dist/src/extensions/health.js +1 -1
- package/dist/src/extensions/loader.js +1 -1
- package/dist/src/extensions/lockfile.js +2 -2
- package/dist/src/gateway/agents-admin.js +2 -2
- package/dist/src/gateway/file-path-classifier.js +2 -2
- package/dist/src/gateway/heartbeat/service.js +1 -1
- package/dist/src/gateway/hono/app.js +33 -2
- package/dist/src/gateway/hono/app.js.map +1 -1
- package/dist/src/gateway/hono/lib/config-payload.js +1 -1
- package/dist/src/gateway/hono/lib/extension-store.js +2 -2
- package/dist/src/gateway/hono/lib/static-ui.js +2 -2
- package/dist/src/gateway/hono/oauth.js +1 -1
- package/dist/src/gateway/hono/routes/agents.js +1 -1
- package/dist/src/gateway/hono/routes/auth-registry-extensions.js +1 -1
- package/dist/src/gateway/hono/routes/config-patch/misc.js +1 -1
- package/dist/src/gateway/hono/routes/dreaming.js +1 -1
- package/dist/src/gateway/hono/routes/host-fs.js +2 -2
- package/dist/src/gateway/hono/routes/lazy-bundles.js +8 -0
- package/dist/src/gateway/hono/routes/lazy-bundles.js.map +1 -1
- package/dist/src/gateway/hono/routes/models.js +1 -1
- package/dist/src/gateway/hono/routes/shares.js +631 -34
- package/dist/src/gateway/hono/routes/shares.js.map +1 -1
- package/dist/src/gateway/hono/routes/site-shares.d.ts +3 -0
- package/dist/src/gateway/hono/routes/site-shares.js +228 -0
- package/dist/src/gateway/hono/routes/site-shares.js.map +1 -0
- package/dist/src/gateway/hono/routes/tunnel.js +97 -8
- package/dist/src/gateway/hono/routes/tunnel.js.map +1 -1
- package/dist/src/gateway/hono/routes/workspace.js +5 -5
- package/dist/src/gateway/hono/sse.js +2 -2
- package/dist/src/gateway/host.d.ts +3 -1
- package/dist/src/gateway/host.js +3 -1
- package/dist/src/gateway/host.js.map +1 -1
- package/dist/src/gateway/lock.js +3 -3
- package/dist/src/gateway/ports.d.ts +6 -0
- package/dist/src/gateway/ports.js +38 -2
- package/dist/src/gateway/ports.js.map +1 -1
- package/dist/src/gateway/public-url.d.ts +8 -0
- package/dist/src/gateway/public-url.js +10 -0
- package/dist/src/gateway/public-url.js.map +1 -0
- package/dist/src/gateway/security/origin-check.d.ts +9 -1
- package/dist/src/gateway/security/origin-check.js +4 -0
- package/dist/src/gateway/security/origin-check.js.map +1 -1
- package/dist/src/gateway/server.js +15 -0
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service/agent-runner.js +2 -2
- package/dist/src/gateway/service/marketplace-service.js +2 -2
- package/dist/src/gateway/service/run-gateway-agent.js +2 -2
- package/dist/src/gateway/service.js +3 -2
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/workspace-fs-file-list.js +1 -1
- package/dist/src/heartbeat/index.js +1 -1
- package/dist/src/i18n/goals-bundle.js +1 -1
- package/dist/src/i18n/index.d.ts +1 -0
- package/dist/src/i18n/index.js +2 -1
- package/dist/src/i18n/locales/share-tool.en.js +15 -0
- package/dist/src/i18n/locales/share-tool.en.js.map +1 -0
- package/dist/src/i18n/locales/share-tool.zh.js +15 -0
- package/dist/src/i18n/locales/share-tool.zh.js.map +1 -0
- package/dist/src/i18n/share-tool-bundle.d.ts +20 -0
- package/dist/src/i18n/share-tool-bundle.js +56 -0
- package/dist/src/i18n/share-tool-bundle.js.map +1 -0
- package/dist/src/infra/gateway-processes.js +1 -0
- package/dist/src/infra/gateway-processes.js.map +1 -1
- package/dist/src/infra/restart.js +2 -2
- package/dist/src/infra/update-check.js +1 -1
- package/dist/src/infra/update-lock.js +3 -3
- package/dist/src/infra/update-runner.js +1 -1
- package/dist/src/infra/update-startup.js +2 -2
- package/dist/src/infra/write-file-atomic.js +2 -2
- package/dist/src/providers/auth-runtime/auth-profile-store.js +1 -1
- package/dist/src/providers/index.js +2 -2
- package/dist/src/providers/model-registry.js +1 -1
- package/dist/src/session/config-store.js +2 -2
- package/dist/src/session/parity/jsonl-transcript-io.js +2 -2
- package/dist/src/session/parity/sessions-json-file.js +1 -1
- package/dist/src/session/parity/transcript-file-lock.js +2 -2
- package/dist/src/session/parity/transcript-paths.js +1 -1
- package/dist/src/session/search-index-cache.js +1 -1
- package/dist/src/session/search-index.js +1 -1
- package/dist/src/session/session-title.js +3 -2
- package/dist/src/session/session-title.js.map +1 -1
- package/dist/src/session/store.js +5 -5
- package/dist/src/share/share-auto.d.ts +74 -0
- package/dist/src/share/share-auto.js +247 -0
- package/dist/src/share/share-auto.js.map +1 -0
- package/dist/src/share/share-config.js +63 -4
- package/dist/src/share/share-config.js.map +1 -1
- package/dist/src/share/share-landing.d.ts +28 -2
- package/dist/src/share/share-landing.js +155 -34
- package/dist/src/share/share-landing.js.map +1 -1
- package/dist/src/share/share-store.d.ts +48 -4
- package/dist/src/share/share-store.js +322 -51
- package/dist/src/share/share-store.js.map +1 -1
- package/dist/src/share/share-thumbnail.d.ts +35 -0
- package/dist/src/share/share-thumbnail.js +277 -0
- package/dist/src/share/share-thumbnail.js.map +1 -0
- package/dist/src/share/share-types.d.ts +68 -10
- package/dist/src/share/share-types.js +18 -1
- package/dist/src/share/share-types.js.map +1 -1
- package/dist/src/share/share-url.js +1 -1
- package/dist/src/share/share-zip.d.ts +35 -0
- package/dist/src/share/share-zip.js +303 -0
- package/dist/src/share/share-zip.js.map +1 -0
- package/dist/src/share/site-proxy.d.ts +35 -0
- package/dist/src/share/site-proxy.js +234 -0
- package/dist/src/share/site-proxy.js.map +1 -0
- package/dist/src/share/site-share-config.d.ts +11 -0
- package/dist/src/share/site-share-config.js +103 -0
- package/dist/src/share/site-share-config.js.map +1 -0
- package/dist/src/share/site-share-router.d.ts +23 -0
- package/dist/src/share/site-share-router.js +147 -0
- package/dist/src/share/site-share-router.js.map +1 -0
- package/dist/src/share/site-share-store.d.ts +53 -0
- package/dist/src/share/site-share-store.js +400 -0
- package/dist/src/share/site-share-store.js.map +1 -0
- package/dist/src/share/site-share-types.d.ts +103 -0
- package/dist/src/share/site-share-types.js +41 -0
- package/dist/src/share/site-share-types.js.map +1 -0
- package/dist/src/share/site-static-serve.d.ts +10 -0
- package/dist/src/share/site-static-serve.js +145 -0
- package/dist/src/share/site-static-serve.js.map +1 -0
- package/dist/src/tui/clipboard-image.js +3 -3
- package/dist/src/tui/theme-manager.js +1 -1
- package/dist/src/tui/tui-commands.js +18 -0
- package/dist/src/tui/tui-commands.js.map +1 -1
- package/dist/src/tui/tui-keybindings-file.js +1 -1
- package/dist/src/tui/tui-scoped-models.js +2 -2
- package/dist/src/tui/tui-settings.js +1 -1
- package/dist/src/tui/tui-workflow-slash.d.ts +32 -0
- package/dist/src/tui/tui-workflow-slash.js +63 -0
- package/dist/src/tui/tui-workflow-slash.js.map +1 -0
- package/dist/src/tui/tui.js +2 -2
- package/dist/src/tunnel/enable-lan-pairing.js +1 -1
- package/dist/src/tunnel/frpc-binary.js +3 -3
- package/dist/src/tunnel/frpc-config.js +1 -1
- package/dist/src/tunnel/frpc-extract.js +1 -1
- package/dist/src/tunnel/index.js +2 -2
- package/dist/src/tunnel/pair-context.d.ts +7 -1
- package/dist/src/tunnel/pair-context.js +25 -9
- package/dist/src/tunnel/pair-context.js.map +1 -1
- package/dist/src/tunnel/pair-url.d.ts +14 -1
- package/dist/src/tunnel/pair-url.js +14 -1
- package/dist/src/tunnel/pair-url.js.map +1 -1
- package/dist/src/tunnel/tunnel-service.js +2 -2
- package/dist/src/tunnel/tunnel-state.js +1 -1
- package/dist/src/utils/logger/audit.js +1 -1
- package/dist/src/utils/logger/log-store.js +1 -1
- package/dist/src/utils/logger/rotation.js +1 -1
- package/dist/src/voice/tts/audio.js +1 -1
- package/dist/src/voice/tts/providers/edge-speech.js +2 -2
- package/package.json +3 -2
- package/dist/gateway/static/root/assets/agents-CrpYTHJS.js +0 -222
- package/dist/gateway/static/root/assets/apps-page-1mcKh5Rh.js +0 -1
- package/dist/gateway/static/root/assets/button-KafIU8dx.js +0 -1
- package/dist/gateway/static/root/assets/channels-settings-zd6QNKPx.js +0 -1
- package/dist/gateway/static/root/assets/channels-status-swr-uRAuhiUo.js +0 -8
- package/dist/gateway/static/root/assets/cron-api-O2Q_ruV6.js +0 -1
- package/dist/gateway/static/root/assets/cron-page-By09AQD-.js +0 -1
- package/dist/gateway/static/root/assets/dist-C57OMHW8.js +0 -48
- package/dist/gateway/static/root/assets/extension-page-C-Ed5ZmP.js +0 -1
- package/dist/gateway/static/root/assets/extension-settings-page-raLux7E7.js +0 -1
- package/dist/gateway/static/root/assets/fetch-2iRFmd3n.js +0 -3
- package/dist/gateway/static/root/assets/heartbeat-config-api-BVl5VHvL.js +0 -1
- package/dist/gateway/static/root/assets/index-BuFldCsB.css +0 -1
- package/dist/gateway/static/root/assets/index-Y-iqo-gL.js +0 -4693
- package/dist/gateway/static/root/assets/logs-page-BdH2n7ZW.js +0 -1
- package/dist/gateway/static/root/assets/sessions-page-Vpchzdp-.js +0 -1
- package/dist/gateway/static/root/assets/settings-form-section-Kk1yAGBl.js +0 -1
- package/dist/gateway/static/root/assets/settings-page-KBm0u6Dz.js +0 -3
- package/dist/gateway/static/root/assets/skills-page-BjeXXaOn.js +0 -2
- package/dist/gateway/static/root/assets/theme-store-D01dJt95.js +0 -1
- package/dist/gateway/static/root/assets/utils-DpTxN4AF.js +0 -1
- package/dist/gateway/static/root/assets/voice-api-key-field-CwO8Cf01.js +0 -1
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { resolveGatewayEffectiveHost } from "../../../config/gateway-bind.js";
|
|
2
|
+
import { SHARE_CONFIG_DEFAULTS } from "../../../share/share-types.js";
|
|
3
|
+
import { resolveSiteShareConfig } from "../../../share/site-share-config.js";
|
|
4
|
+
import { logShareAudit } from "../../../share/share-audit.js";
|
|
5
|
+
import { getShareStore, shareResponseContentType } from "../../../share/share-store.js";
|
|
6
|
+
import { getSiteShareStore } from "../../../share/site-share-store.js";
|
|
7
|
+
import { resolveShareUrl } from "../../../share/share-url.js";
|
|
8
|
+
import { audienceDefaults, cleanupStagedSite, decideShareKind, forgetStagedSite, makeDescription, makeTitle, probeShareTarget, rememberStagedSite, stageSingleHtmlAsSite } from "../../../share/share-auto.js";
|
|
9
|
+
import { deleteThumbnail, placeholderSvg, readThumbnail, scheduleThumbnail, thumbnailContentType, thumbnailExists } from "../../../share/share-thumbnail.js";
|
|
2
10
|
import { extractToken } from "../../auth.js";
|
|
3
11
|
import { getClientIpFromHeaders } from "../../security/loopback.js";
|
|
4
|
-
import {
|
|
5
|
-
import { SHARE_CONFIG_DEFAULTS } from "../../../share/share-types.js";
|
|
6
|
-
import { renderShareExpiredPage, renderShareLandingPage } from "../../../share/share-landing.js";
|
|
12
|
+
import { renderFolderLandingPage, renderShareExpiredPage, renderShareLandingPage } from "../../../share/share-landing.js";
|
|
7
13
|
import { consumeSharePublicLimit } from "../../../share/share-rate-limit.js";
|
|
8
|
-
import {
|
|
9
|
-
import { resolveShareUrl } from "../../../share/share-url.js";
|
|
10
|
-
import { createHash } from "node:crypto";
|
|
14
|
+
import { createZipStream, planDirectoryFiles } from "../../../share/share-zip.js";
|
|
11
15
|
import { createReadStream } from "node:fs";
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
12
17
|
import { stat } from "node:fs/promises";
|
|
13
18
|
import { Readable } from "node:stream";
|
|
14
19
|
//#region src/gateway/hono/routes/shares.ts
|
|
@@ -19,6 +24,18 @@ function getShareUrlContext(service) {
|
|
|
19
24
|
gatewayPort: gateway.port ?? 18790
|
|
20
25
|
};
|
|
21
26
|
}
|
|
27
|
+
function thumbnailRenderContext(service) {
|
|
28
|
+
const cfg = {
|
|
29
|
+
...SHARE_CONFIG_DEFAULTS,
|
|
30
|
+
...resolveShareConfig(service)
|
|
31
|
+
};
|
|
32
|
+
const port = service.currentConfig.gateway.port ?? 18790;
|
|
33
|
+
const internalBaseUrl = cfg.thumbnail.internalGatewayUrl ?? `http://127.0.0.1:${port}`;
|
|
34
|
+
return {
|
|
35
|
+
config: cfg.thumbnail,
|
|
36
|
+
internalBaseUrl
|
|
37
|
+
};
|
|
38
|
+
}
|
|
22
39
|
function resolveShareConfig(service) {
|
|
23
40
|
const raw = service.currentConfig.gateway?.share;
|
|
24
41
|
if (!raw || typeof raw !== "object") return {};
|
|
@@ -29,6 +46,7 @@ function hashGatewayToken(token) {
|
|
|
29
46
|
}
|
|
30
47
|
const MAX_CONCURRENT_DOWNLOADS_PER_TOKEN = 5;
|
|
31
48
|
const activeDownloads = /* @__PURE__ */ new Map();
|
|
49
|
+
const activeZipStreams = /* @__PURE__ */ new Map();
|
|
32
50
|
function acquireDownloadSlot(token) {
|
|
33
51
|
const current = activeDownloads.get(token) ?? 0;
|
|
34
52
|
if (current >= MAX_CONCURRENT_DOWNLOADS_PER_TOKEN) return false;
|
|
@@ -40,9 +58,40 @@ function releaseDownloadSlot(token) {
|
|
|
40
58
|
if (current <= 1) activeDownloads.delete(token);
|
|
41
59
|
else activeDownloads.set(token, current - 1);
|
|
42
60
|
}
|
|
61
|
+
function acquireZipSlot(token, limit) {
|
|
62
|
+
const current = activeZipStreams.get(token) ?? 0;
|
|
63
|
+
if (current >= limit) return false;
|
|
64
|
+
activeZipStreams.set(token, current + 1);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
function releaseZipSlot(token) {
|
|
68
|
+
const current = activeZipStreams.get(token) ?? 0;
|
|
69
|
+
if (current <= 1) activeZipStreams.delete(token);
|
|
70
|
+
else activeZipStreams.set(token, current - 1);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether the browser can render this MIME natively (when served with
|
|
74
|
+
* `Content-Disposition: inline`). Honours the per-deployment whitelist so
|
|
75
|
+
* admins can block specific types.
|
|
76
|
+
*/
|
|
77
|
+
function isPreviewableInline(mime, whitelist) {
|
|
78
|
+
return whitelist.includes(mime);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether the type benefits from being rendered through the SPA preview page
|
|
82
|
+
* (e.g. markdown — the browser would otherwise show raw source). Independent
|
|
83
|
+
* of the inline whitelist: SPA preview pulls the bytes via the share API and
|
|
84
|
+
* renders client-side, so admins control reach via TTL/maxViews, not MIME.
|
|
85
|
+
*/
|
|
86
|
+
function isRichSpaPreviewable(mime) {
|
|
87
|
+
return mime === "text/markdown" || mime === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || mime === "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
88
|
+
}
|
|
89
|
+
function rfc5987ContentDisposition(disposition, fileName) {
|
|
90
|
+
return `${disposition}; filename="${fileName.replace(/[^\x20-\x7e]/g, "_").replace(/"/g, "")}"; filename*=UTF-8''${encodeURIComponent(fileName)}`;
|
|
91
|
+
}
|
|
43
92
|
function registerSharePublicRoutes(app, service) {
|
|
44
93
|
const store = getShareStore(resolveShareConfig(service));
|
|
45
|
-
/** Landing page — does NOT consume
|
|
94
|
+
/** Landing page — does NOT consume downloadCount. */
|
|
46
95
|
app.get("/s/:token", async (c) => {
|
|
47
96
|
const clientIp = getClientIpFromHeaders({ get: (n) => c.req.header(n) ?? void 0 });
|
|
48
97
|
const rateResult = consumeSharePublicLimit(clientIp);
|
|
@@ -63,17 +112,29 @@ function registerSharePublicRoutes(app, service) {
|
|
|
63
112
|
}, `Share access denied: ${validation.reason}`);
|
|
64
113
|
return c.html(renderShareExpiredPage(validation.reason), 410);
|
|
65
114
|
}
|
|
66
|
-
if (
|
|
115
|
+
if (record.kind === "directory") return renderDirectoryLanding(c, store, service, record, token);
|
|
116
|
+
if (c.req.query("dl") === "1") return handleFileDownload(c, store, record, clientIp);
|
|
67
117
|
if (c.req.query("inline") === "1") {
|
|
68
118
|
if ({
|
|
69
119
|
...SHARE_CONFIG_DEFAULTS,
|
|
70
120
|
...resolveShareConfig(service)
|
|
71
|
-
}.inlinePreviewMimes.includes(record.mimeType)) return
|
|
121
|
+
}.inlinePreviewMimes.includes(record.mimeType)) return handleFileDownload(c, store, record, clientIp, true);
|
|
72
122
|
}
|
|
73
123
|
const downloadPath = `/s/${token}/download`;
|
|
74
|
-
|
|
124
|
+
const cfg = {
|
|
125
|
+
...SHARE_CONFIG_DEFAULTS,
|
|
126
|
+
...resolveShareConfig(service)
|
|
127
|
+
};
|
|
128
|
+
const previewable = isPreviewableInline(record.mimeType, cfg.inlinePreviewMimes);
|
|
129
|
+
const richPreviewable = isRichSpaPreviewable(record.mimeType);
|
|
130
|
+
const og = buildLandingOg(service, record);
|
|
131
|
+
return c.html(renderShareLandingPage(record, downloadPath, {
|
|
132
|
+
inlineUrl: previewable ? `/s/${token}?inline=1` : null,
|
|
133
|
+
previewUrl: richPreviewable ? `/#/share/${encodeURIComponent(token)}` : null,
|
|
134
|
+
og
|
|
135
|
+
}));
|
|
75
136
|
});
|
|
76
|
-
/**
|
|
137
|
+
/** Single-file download — POST so unfurl/scrapers don't consume views. */
|
|
77
138
|
app.post("/s/:token/download", async (c) => {
|
|
78
139
|
const clientIp = getClientIpFromHeaders({ get: (n) => c.req.header(n) ?? void 0 });
|
|
79
140
|
const rateResult = consumeSharePublicLimit(clientIp);
|
|
@@ -94,9 +155,80 @@ function registerSharePublicRoutes(app, service) {
|
|
|
94
155
|
}, `Share download denied: ${validation.reason}`);
|
|
95
156
|
return c.html(renderShareExpiredPage(validation.reason), 410);
|
|
96
157
|
}
|
|
97
|
-
|
|
158
|
+
if (record.kind === "directory") return c.html(renderShareExpiredPage("not_found"), 404);
|
|
159
|
+
return handleFileDownload(c, store, record, clientIp);
|
|
160
|
+
});
|
|
161
|
+
/** Directory child file (GET — preview counts as download per product). */
|
|
162
|
+
app.get("/s/:token/file", async (c) => {
|
|
163
|
+
const clientIp = getClientIpFromHeaders({ get: (n) => c.req.header(n) ?? void 0 });
|
|
164
|
+
const rateResult = consumeSharePublicLimit(clientIp);
|
|
165
|
+
if (!rateResult.allowed) {
|
|
166
|
+
c.header("Retry-After", String(Math.ceil(rateResult.retryAfterMs / 1e3)));
|
|
167
|
+
return c.text("Too many requests", 429);
|
|
168
|
+
}
|
|
169
|
+
const token = c.req.param("token");
|
|
170
|
+
const record = store.getByToken(token);
|
|
171
|
+
if (!record) return c.html(renderShareExpiredPage("not_found"), 404);
|
|
172
|
+
if (record.kind !== "directory") return c.html(renderShareExpiredPage("not_found"), 404);
|
|
173
|
+
const validation = store.validateAccess(record);
|
|
174
|
+
if (!validation.valid) return c.html(renderShareExpiredPage(validation.reason), 410);
|
|
175
|
+
return handleDirectoryFile(c, store, service, record, c.req.query("path") ?? "", clientIp, c.req.query("inline") === "1" || c.req.query("dl") !== "1");
|
|
176
|
+
});
|
|
177
|
+
/** Directory JSON listing. */
|
|
178
|
+
app.get("/s/:token/tree", async (c) => {
|
|
179
|
+
const rateResult = consumeSharePublicLimit(getClientIpFromHeaders({ get: (n) => c.req.header(n) ?? void 0 }));
|
|
180
|
+
if (!rateResult.allowed) {
|
|
181
|
+
c.header("Retry-After", String(Math.ceil(rateResult.retryAfterMs / 1e3)));
|
|
182
|
+
return c.text("Too many requests", 429);
|
|
183
|
+
}
|
|
184
|
+
const token = c.req.param("token");
|
|
185
|
+
const record = store.getByToken(token);
|
|
186
|
+
if (!record) return c.json({
|
|
187
|
+
ok: false,
|
|
188
|
+
error: { message: "not_found" }
|
|
189
|
+
}, 404);
|
|
190
|
+
if (record.kind !== "directory") return c.json({
|
|
191
|
+
ok: false,
|
|
192
|
+
error: { message: "not_directory" }
|
|
193
|
+
}, 400);
|
|
194
|
+
const validation = store.validateAccess(record);
|
|
195
|
+
if (!validation.valid) return c.json({
|
|
196
|
+
ok: false,
|
|
197
|
+
error: { message: validation.reason }
|
|
198
|
+
}, 410);
|
|
199
|
+
const path = c.req.query("path") ?? "";
|
|
200
|
+
if ((c.req.header("accept") ?? "").includes("text/html")) return renderDirectoryLanding(c, store, service, record, token, path);
|
|
201
|
+
try {
|
|
202
|
+
const listing = await store.listDirectory(record, path);
|
|
203
|
+
return c.json({
|
|
204
|
+
ok: true,
|
|
205
|
+
payload: listing
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
209
|
+
return c.json({
|
|
210
|
+
ok: false,
|
|
211
|
+
error: { message }
|
|
212
|
+
}, 400);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
/** Folder ZIP download (whole share or sub-path). */
|
|
216
|
+
app.get("/s/:token/zip", async (c) => {
|
|
217
|
+
const clientIp = getClientIpFromHeaders({ get: (n) => c.req.header(n) ?? void 0 });
|
|
218
|
+
const rateResult = consumeSharePublicLimit(clientIp);
|
|
219
|
+
if (!rateResult.allowed) {
|
|
220
|
+
c.header("Retry-After", String(Math.ceil(rateResult.retryAfterMs / 1e3)));
|
|
221
|
+
return c.text("Too many requests", 429);
|
|
222
|
+
}
|
|
223
|
+
const token = c.req.param("token");
|
|
224
|
+
const record = store.getByToken(token);
|
|
225
|
+
if (!record) return c.html(renderShareExpiredPage("not_found"), 404);
|
|
226
|
+
if (record.kind !== "directory") return c.html(renderShareExpiredPage("not_found"), 404);
|
|
227
|
+
const validation = store.validateAccess(record);
|
|
228
|
+
if (!validation.valid) return c.html(renderShareExpiredPage(validation.reason), 410);
|
|
229
|
+
return handleDirectoryZip(c, store, service, record, c.req.query("path") ?? "", clientIp);
|
|
98
230
|
});
|
|
99
|
-
/**
|
|
231
|
+
/** Metadata (for link preview cards / unfurl). Does NOT consume views. */
|
|
100
232
|
app.get("/s/:token/meta", async (c) => {
|
|
101
233
|
const rateResult = consumeSharePublicLimit(getClientIpFromHeaders({ get: (n) => c.req.header(n) ?? void 0 }));
|
|
102
234
|
if (!rateResult.allowed) {
|
|
@@ -107,18 +239,20 @@ function registerSharePublicRoutes(app, service) {
|
|
|
107
239
|
const record = store.getByToken(token);
|
|
108
240
|
if (!record) return c.json({ valid: false }, 404);
|
|
109
241
|
const validation = store.validateAccess(record);
|
|
110
|
-
const remainingViews = record.maxViews !== null ? Math.max(0, record.maxViews - record.
|
|
242
|
+
const remainingViews = record.maxViews !== null ? Math.max(0, record.maxViews - record.downloadCount) : null;
|
|
111
243
|
return c.json({
|
|
244
|
+
kind: record.kind,
|
|
112
245
|
fileName: record.fileName,
|
|
113
246
|
fileSize: record.fileSize,
|
|
114
247
|
mimeType: record.mimeType,
|
|
115
248
|
description: record.description ?? null,
|
|
116
249
|
expiresAt: record.expiresAt,
|
|
117
250
|
remainingViews,
|
|
118
|
-
valid: validation.valid
|
|
251
|
+
valid: validation.valid,
|
|
252
|
+
directory: record.directory ?? null
|
|
119
253
|
});
|
|
120
254
|
});
|
|
121
|
-
/** HEAD check
|
|
255
|
+
/** HEAD check. */
|
|
122
256
|
app.on("HEAD", "/s/:token", async (c) => {
|
|
123
257
|
const token = c.req.param("token");
|
|
124
258
|
const record = store.getByToken(token);
|
|
@@ -126,11 +260,100 @@ function registerSharePublicRoutes(app, service) {
|
|
|
126
260
|
const validation = store.validateAccess(record);
|
|
127
261
|
return c.body(null, validation.valid ? 200 : 410);
|
|
128
262
|
});
|
|
263
|
+
/** Thumbnail (jpeg/png/svg) — does NOT consume views. */
|
|
264
|
+
app.get("/s/:token/thumbnail", async (c) => {
|
|
265
|
+
const rateResult = consumeSharePublicLimit(getClientIpFromHeaders({ get: (n) => c.req.header(n) ?? void 0 }));
|
|
266
|
+
if (!rateResult.allowed) {
|
|
267
|
+
c.header("Retry-After", String(Math.ceil(rateResult.retryAfterMs / 1e3)));
|
|
268
|
+
return c.text("Too many requests", 429);
|
|
269
|
+
}
|
|
270
|
+
const token = c.req.param("token");
|
|
271
|
+
const record = store.getByToken(token);
|
|
272
|
+
if (!record) return c.body(null, 404);
|
|
273
|
+
if (!store.validateAccess(record).valid) return c.body(null, 410);
|
|
274
|
+
const cached = await readThumbnail(token);
|
|
275
|
+
if (cached) return new Response(cached, {
|
|
276
|
+
status: 200,
|
|
277
|
+
headers: {
|
|
278
|
+
"Content-Type": thumbnailContentType(cached),
|
|
279
|
+
"Cache-Control": "public, max-age=300",
|
|
280
|
+
"X-Content-Type-Options": "nosniff"
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
scheduleThumbnail({
|
|
284
|
+
scope: "file",
|
|
285
|
+
token,
|
|
286
|
+
recordId: record.id
|
|
287
|
+
}, thumbnailRenderContext(service));
|
|
288
|
+
const placeholder = placeholderSvg(record.fileName, record.kind === "directory" ? "folder" : "file");
|
|
289
|
+
return new Response(placeholder, {
|
|
290
|
+
status: 200,
|
|
291
|
+
headers: {
|
|
292
|
+
"Content-Type": "image/svg+xml; charset=utf-8",
|
|
293
|
+
"Cache-Control": "public, max-age=10",
|
|
294
|
+
"X-Content-Type-Options": "nosniff"
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
app.on("HEAD", "/s/:token/thumbnail", async (c) => {
|
|
299
|
+
const token = c.req.param("token");
|
|
300
|
+
const record = store.getByToken(token);
|
|
301
|
+
if (!record) return c.body(null, 404);
|
|
302
|
+
if (!store.validateAccess(record).valid) return c.body(null, 410);
|
|
303
|
+
const ready = await thumbnailExists(token);
|
|
304
|
+
return c.body(null, ready ? 200 : 202);
|
|
305
|
+
});
|
|
306
|
+
/** Site-share thumbnail. Shape mirrors /s/:token/thumbnail. */
|
|
307
|
+
app.get("/site/:token/thumbnail", async (c) => {
|
|
308
|
+
const rateResult = consumeSharePublicLimit(getClientIpFromHeaders({ get: (n) => c.req.header(n) ?? void 0 }));
|
|
309
|
+
if (!rateResult.allowed) {
|
|
310
|
+
c.header("Retry-After", String(Math.ceil(rateResult.retryAfterMs / 1e3)));
|
|
311
|
+
return c.text("Too many requests", 429);
|
|
312
|
+
}
|
|
313
|
+
const token = c.req.param("token");
|
|
314
|
+
const siteStore = getSiteShareStore(resolveSiteShareConfig(service));
|
|
315
|
+
const record = siteStore.getByToken(token);
|
|
316
|
+
if (!record) return c.body(null, 404);
|
|
317
|
+
if (!siteStore.validateAccess(record).valid) return c.body(null, 410);
|
|
318
|
+
const cached = await readThumbnail(token);
|
|
319
|
+
if (cached) return new Response(cached, {
|
|
320
|
+
status: 200,
|
|
321
|
+
headers: {
|
|
322
|
+
"Content-Type": thumbnailContentType(cached),
|
|
323
|
+
"Cache-Control": "public, max-age=300",
|
|
324
|
+
"X-Content-Type-Options": "nosniff"
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
scheduleThumbnail({
|
|
328
|
+
scope: "site",
|
|
329
|
+
token,
|
|
330
|
+
recordId: record.id
|
|
331
|
+
}, thumbnailRenderContext(service));
|
|
332
|
+
const placeholder = placeholderSvg(record.description ?? "site share", "html");
|
|
333
|
+
return new Response(placeholder, {
|
|
334
|
+
status: 200,
|
|
335
|
+
headers: {
|
|
336
|
+
"Content-Type": "image/svg+xml; charset=utf-8",
|
|
337
|
+
"Cache-Control": "public, max-age=10",
|
|
338
|
+
"X-Content-Type-Options": "nosniff"
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
app.on("HEAD", "/site/:token/thumbnail", async (c) => {
|
|
343
|
+
const ready = await thumbnailExists(c.req.param("token"));
|
|
344
|
+
return c.body(null, ready ? 200 : 202);
|
|
345
|
+
});
|
|
346
|
+
store.setCleanupHook((rec) => {
|
|
347
|
+
deleteThumbnail(rec.token);
|
|
348
|
+
});
|
|
129
349
|
}
|
|
130
350
|
function registerShareRoutes(authenticated, deps) {
|
|
131
351
|
const { service } = deps;
|
|
132
352
|
const store = getShareStore(resolveShareConfig(service));
|
|
133
|
-
|
|
353
|
+
getSiteShareStore(resolveSiteShareConfig(service)).setCleanupHook((rec) => {
|
|
354
|
+
const dir = forgetStagedSite(rec.id);
|
|
355
|
+
if (dir) cleanupStagedSite(dir);
|
|
356
|
+
});
|
|
134
357
|
authenticated.post("/api/shares", async (c) => {
|
|
135
358
|
const gatewayToken = extractToken({ authorization: c.req.header("authorization") ?? void 0 });
|
|
136
359
|
if (!gatewayToken) return c.json({
|
|
@@ -161,6 +384,12 @@ function registerShareRoutes(authenticated, deps) {
|
|
|
161
384
|
const ttlMs = typeof body.ttlMs === "number" ? body.ttlMs : void 0;
|
|
162
385
|
const maxViews = body.maxViews === null ? null : typeof body.maxViews === "number" ? body.maxViews : void 0;
|
|
163
386
|
const description = typeof body.description === "string" ? body.description.trim() || void 0 : void 0;
|
|
387
|
+
const kind = body.kind === "directory" || body.kind === "file" ? body.kind : void 0;
|
|
388
|
+
const directoryMode = body.directoryMode === "zip-only" ? "zip-only" : body.directoryMode === "browse" ? "browse" : void 0;
|
|
389
|
+
const followSymlinks = body.followSymlinks === true;
|
|
390
|
+
const maxFileCount = typeof body.maxFileCount === "number" ? body.maxFileCount : void 0;
|
|
391
|
+
const maxFolderSize = typeof body.maxFolderSize === "number" ? body.maxFolderSize : void 0;
|
|
392
|
+
const maxDepth = typeof body.maxDepth === "number" ? body.maxDepth : void 0;
|
|
164
393
|
try {
|
|
165
394
|
store.updateConfig(resolveShareConfig(service));
|
|
166
395
|
const record = await store.create({
|
|
@@ -171,7 +400,13 @@ function registerShareRoutes(authenticated, deps) {
|
|
|
171
400
|
sessionKey,
|
|
172
401
|
agentId,
|
|
173
402
|
workspaceRoot,
|
|
174
|
-
gatewayTokenHash: hashGatewayToken(gatewayToken)
|
|
403
|
+
gatewayTokenHash: hashGatewayToken(gatewayToken),
|
|
404
|
+
kind,
|
|
405
|
+
directoryMode,
|
|
406
|
+
followSymlinks,
|
|
407
|
+
maxFileCount,
|
|
408
|
+
maxFolderSize,
|
|
409
|
+
maxDepth
|
|
175
410
|
});
|
|
176
411
|
const urlCtx = getShareUrlContext(service);
|
|
177
412
|
const resolved = resolveShareUrl(record.token, urlCtx);
|
|
@@ -180,6 +415,7 @@ function registerShareRoutes(authenticated, deps) {
|
|
|
180
415
|
payload: {
|
|
181
416
|
id: record.id,
|
|
182
417
|
token: record.token,
|
|
418
|
+
kind: record.kind,
|
|
183
419
|
shareUrl: resolved.shareUrl,
|
|
184
420
|
lanUrl: resolved.lanUrl,
|
|
185
421
|
reachability: resolved.reachability,
|
|
@@ -187,7 +423,8 @@ function registerShareRoutes(authenticated, deps) {
|
|
|
187
423
|
expiresAt: record.expiresAt,
|
|
188
424
|
maxViews: record.maxViews,
|
|
189
425
|
fileName: record.fileName,
|
|
190
|
-
fileSize: record.fileSize
|
|
426
|
+
fileSize: record.fileSize,
|
|
427
|
+
directory: record.directory ?? null
|
|
191
428
|
}
|
|
192
429
|
}, 201);
|
|
193
430
|
} catch (err) {
|
|
@@ -198,7 +435,172 @@ function registerShareRoutes(authenticated, deps) {
|
|
|
198
435
|
}, 400);
|
|
199
436
|
}
|
|
200
437
|
});
|
|
201
|
-
/**
|
|
438
|
+
/**
|
|
439
|
+
* Smart share — picks file vs site, fills sensible defaults, returns the
|
|
440
|
+
* payload the mobile share-sheet needs (title, description, thumbnailUrl,
|
|
441
|
+
* reachability) in a single round-trip.
|
|
442
|
+
*/
|
|
443
|
+
authenticated.post("/api/shares/auto", async (c) => {
|
|
444
|
+
const gatewayToken = extractToken({ authorization: c.req.header("authorization") ?? void 0 });
|
|
445
|
+
if (!gatewayToken) return c.json({
|
|
446
|
+
ok: false,
|
|
447
|
+
error: { message: "Token required" }
|
|
448
|
+
}, 401);
|
|
449
|
+
let body;
|
|
450
|
+
try {
|
|
451
|
+
body = await c.req.json();
|
|
452
|
+
} catch {
|
|
453
|
+
return c.json({
|
|
454
|
+
ok: false,
|
|
455
|
+
error: { message: "Invalid JSON" }
|
|
456
|
+
}, 400);
|
|
457
|
+
}
|
|
458
|
+
const path = typeof body.path === "string" ? body.path.trim() : "";
|
|
459
|
+
if (!path) return c.json({
|
|
460
|
+
ok: false,
|
|
461
|
+
error: { message: "Missing path" }
|
|
462
|
+
}, 400);
|
|
463
|
+
const sessionKey = typeof body.sessionKey === "string" ? body.sessionKey.trim() : void 0;
|
|
464
|
+
const agentId = typeof body.agentId === "string" ? body.agentId.trim() : void 0;
|
|
465
|
+
const mode = typeof body.mode === "string" && [
|
|
466
|
+
"auto",
|
|
467
|
+
"force-file",
|
|
468
|
+
"force-site",
|
|
469
|
+
"force-zip"
|
|
470
|
+
].includes(body.mode) ? body.mode : "auto";
|
|
471
|
+
const audience = body.audience === "friend" || body.audience === "colleague" || body.audience === "public" ? body.audience : void 0;
|
|
472
|
+
const title = typeof body.title === "string" ? body.title : void 0;
|
|
473
|
+
const description = typeof body.description === "string" ? body.description : void 0;
|
|
474
|
+
const ttlOverride = typeof body.ttlMs === "number" ? body.ttlMs : void 0;
|
|
475
|
+
const maxViewsOverride = body.maxViews === null ? null : typeof body.maxViews === "number" ? body.maxViews : void 0;
|
|
476
|
+
const wantThumbnail = body.thumbnail !== false;
|
|
477
|
+
const workspaceRoot = await resolveWorkspaceRootForShare(service, sessionKey, agentId);
|
|
478
|
+
if (!workspaceRoot) return c.json({
|
|
479
|
+
ok: false,
|
|
480
|
+
error: { message: "Workspace not configured" }
|
|
481
|
+
}, 400);
|
|
482
|
+
let probe;
|
|
483
|
+
try {
|
|
484
|
+
probe = await probeShareTarget(workspaceRoot, path);
|
|
485
|
+
} catch (err) {
|
|
486
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
487
|
+
return c.json({
|
|
488
|
+
ok: false,
|
|
489
|
+
error: { message }
|
|
490
|
+
}, 400);
|
|
491
|
+
}
|
|
492
|
+
let decision;
|
|
493
|
+
try {
|
|
494
|
+
decision = decideShareKind(probe, mode);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
497
|
+
return c.json({
|
|
498
|
+
ok: false,
|
|
499
|
+
error: { message }
|
|
500
|
+
}, 400);
|
|
501
|
+
}
|
|
502
|
+
const defaults = audienceDefaults(audience);
|
|
503
|
+
const ttlMs = ttlOverride ?? defaults.ttlMs;
|
|
504
|
+
const maxViews = maxViewsOverride !== void 0 ? maxViewsOverride : defaults.maxViews;
|
|
505
|
+
const tokenHash = hashGatewayToken(gatewayToken);
|
|
506
|
+
const urlCtx = getShareUrlContext(service);
|
|
507
|
+
try {
|
|
508
|
+
if (decision.kind === "site") {
|
|
509
|
+
const siteStore = getSiteShareStore(resolveSiteShareConfig(service));
|
|
510
|
+
let sitePath = path;
|
|
511
|
+
let stagedDir = null;
|
|
512
|
+
if (probe.kind === "file") {
|
|
513
|
+
const staged = await stageSingleHtmlAsSite(workspaceRoot, probe.absolutePath);
|
|
514
|
+
sitePath = staged.relativePath;
|
|
515
|
+
stagedDir = staged.stagingDir;
|
|
516
|
+
}
|
|
517
|
+
const siteRec = await siteStore.create({
|
|
518
|
+
kind: "static",
|
|
519
|
+
path: sitePath,
|
|
520
|
+
ttlMs,
|
|
521
|
+
description,
|
|
522
|
+
subdomain: typeof body.subdomain === "string" ? body.subdomain : void 0,
|
|
523
|
+
spaFallback: true,
|
|
524
|
+
rewriteMode: "html-css",
|
|
525
|
+
sessionKey,
|
|
526
|
+
agentId,
|
|
527
|
+
workspaceRoot,
|
|
528
|
+
gatewayTokenHash: tokenHash
|
|
529
|
+
});
|
|
530
|
+
if (stagedDir) rememberStagedSite(siteRec.id, stagedDir);
|
|
531
|
+
const cfg = siteStore.getConfig();
|
|
532
|
+
const gatewayPort = service.currentConfig.gateway.port ?? 18790;
|
|
533
|
+
const gatewayHost = resolveGatewayEffectiveHost(service.currentConfig);
|
|
534
|
+
const subdomainLabel = siteRec.subdomain ?? siteRec.token;
|
|
535
|
+
const tunnelUp = !!(await import("../../../tunnel/tunnel-state.js")).loadTunnelState();
|
|
536
|
+
const shareUrl = tunnelUp ? `https://${subdomainLabel}.${cfg.publicHostSuffix}/` : `http://${gatewayHost}:${gatewayPort}/site/${siteRec.token}/`;
|
|
537
|
+
const reachability = tunnelUp ? "public" : gatewayHost === "127.0.0.1" || gatewayHost === "localhost" || gatewayHost === "::1" ? "local-only" : "lan";
|
|
538
|
+
const thumbnailUrl = wantThumbnail ? `${tunnelUp ? `https://${subdomainLabel}.${cfg.publicHostSuffix}` : `http://${gatewayHost}:${gatewayPort}`}/site/${siteRec.token}/thumbnail` : "";
|
|
539
|
+
if (wantThumbnail) {
|
|
540
|
+
scheduleThumbnail({
|
|
541
|
+
scope: "site",
|
|
542
|
+
token: siteRec.token,
|
|
543
|
+
recordId: siteRec.id
|
|
544
|
+
}, thumbnailRenderContext(service));
|
|
545
|
+
siteStore.setThumbnailStatus(siteRec.id, "pending");
|
|
546
|
+
}
|
|
547
|
+
const titleOut = makeTitle(probe.kind === "directory" ? path.split("/").pop() || path : path, title);
|
|
548
|
+
return c.json({
|
|
549
|
+
ok: true,
|
|
550
|
+
payload: {
|
|
551
|
+
share: {
|
|
552
|
+
id: siteRec.id,
|
|
553
|
+
kind: "site",
|
|
554
|
+
title: titleOut,
|
|
555
|
+
description: makeDescription({
|
|
556
|
+
audience,
|
|
557
|
+
expiresAt: siteRec.expiresAt,
|
|
558
|
+
override: description
|
|
559
|
+
}),
|
|
560
|
+
shareUrl,
|
|
561
|
+
lanUrl: null,
|
|
562
|
+
reachability,
|
|
563
|
+
reachabilityHint: reachability === "public" ? null : reachability === "lan" ? "当前局域网内可访问,开启远程隧道后对外网可达" : "当前仅本机可访问,开启远程隧道后对外可达",
|
|
564
|
+
expiresAt: siteRec.expiresAt,
|
|
565
|
+
maxViews: null
|
|
566
|
+
},
|
|
567
|
+
thumbnail: {
|
|
568
|
+
url: thumbnailUrl,
|
|
569
|
+
status: wantThumbnail ? "pending" : "unavailable",
|
|
570
|
+
width: SHARE_CONFIG_DEFAULTS.thumbnail.viewportWidth,
|
|
571
|
+
height: SHARE_CONFIG_DEFAULTS.thumbnail.viewportHeight
|
|
572
|
+
},
|
|
573
|
+
routing: {
|
|
574
|
+
reason: decision.reason,
|
|
575
|
+
hint: decision.hint
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}, 201);
|
|
579
|
+
}
|
|
580
|
+
return await createFileShareResponse({
|
|
581
|
+
c,
|
|
582
|
+
service,
|
|
583
|
+
store,
|
|
584
|
+
probe,
|
|
585
|
+
decision,
|
|
586
|
+
ttlMs,
|
|
587
|
+
maxViews,
|
|
588
|
+
title,
|
|
589
|
+
description,
|
|
590
|
+
audience,
|
|
591
|
+
workspaceRoot,
|
|
592
|
+
tokenHash,
|
|
593
|
+
urlCtx,
|
|
594
|
+
wantThumbnail
|
|
595
|
+
});
|
|
596
|
+
} catch (err) {
|
|
597
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
598
|
+
return c.json({
|
|
599
|
+
ok: false,
|
|
600
|
+
error: { message }
|
|
601
|
+
}, 400);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
202
604
|
authenticated.get("/api/shares", async (c) => {
|
|
203
605
|
store.updateConfig(resolveShareConfig(service));
|
|
204
606
|
const shares = store.getAllShares();
|
|
@@ -209,6 +611,7 @@ function registerShareRoutes(authenticated, deps) {
|
|
|
209
611
|
const expired = now >= new Date(r.expiresAt).getTime();
|
|
210
612
|
return {
|
|
211
613
|
id: r.id,
|
|
614
|
+
kind: r.kind,
|
|
212
615
|
fileName: r.fileName,
|
|
213
616
|
workspaceRelativePath: r.workspaceRelativePath,
|
|
214
617
|
shareUrl: resolved.shareUrl,
|
|
@@ -216,13 +619,14 @@ function registerShareRoutes(authenticated, deps) {
|
|
|
216
619
|
reachability: resolved.reachability,
|
|
217
620
|
createdAt: r.createdAt,
|
|
218
621
|
expiresAt: r.expiresAt,
|
|
219
|
-
|
|
622
|
+
downloadCount: r.downloadCount,
|
|
220
623
|
maxViews: r.maxViews,
|
|
221
624
|
revoked: r.revoked,
|
|
222
625
|
expired,
|
|
223
626
|
description: r.description ?? null,
|
|
224
627
|
fileSize: r.fileSize,
|
|
225
|
-
mimeType: r.mimeType
|
|
628
|
+
mimeType: r.mimeType,
|
|
629
|
+
directory: r.directory ?? null
|
|
226
630
|
};
|
|
227
631
|
});
|
|
228
632
|
return c.json({
|
|
@@ -230,7 +634,6 @@ function registerShareRoutes(authenticated, deps) {
|
|
|
230
634
|
payload: { shares: items }
|
|
231
635
|
});
|
|
232
636
|
});
|
|
233
|
-
/** Get single share details. */
|
|
234
637
|
authenticated.get("/api/shares/:id", async (c) => {
|
|
235
638
|
const id = c.req.param("id");
|
|
236
639
|
const record = store.getById(id);
|
|
@@ -253,7 +656,6 @@ function registerShareRoutes(authenticated, deps) {
|
|
|
253
656
|
}
|
|
254
657
|
});
|
|
255
658
|
});
|
|
256
|
-
/** Revoke a share. */
|
|
257
659
|
authenticated.delete("/api/shares/:id", async (c) => {
|
|
258
660
|
const id = c.req.param("id");
|
|
259
661
|
if (!store.revoke(id)) return c.json({
|
|
@@ -262,7 +664,6 @@ function registerShareRoutes(authenticated, deps) {
|
|
|
262
664
|
}, 404);
|
|
263
665
|
return c.json({ ok: true });
|
|
264
666
|
});
|
|
265
|
-
/** Batch revoke. */
|
|
266
667
|
authenticated.delete("/api/shares", async (c) => {
|
|
267
668
|
let body = {};
|
|
268
669
|
try {
|
|
@@ -286,7 +687,6 @@ function registerShareRoutes(authenticated, deps) {
|
|
|
286
687
|
payload: { revokedCount: count }
|
|
287
688
|
});
|
|
288
689
|
});
|
|
289
|
-
/** Update a share (extend TTL or change maxViews). */
|
|
290
690
|
authenticated.patch("/api/shares/:id", async (c) => {
|
|
291
691
|
const id = c.req.param("id");
|
|
292
692
|
let body;
|
|
@@ -319,6 +719,204 @@ function registerShareRoutes(authenticated, deps) {
|
|
|
319
719
|
});
|
|
320
720
|
});
|
|
321
721
|
}
|
|
722
|
+
async function renderDirectoryLanding(c, store, service, record, token, subPath = "") {
|
|
723
|
+
store.updateConfig(resolveShareConfig(service));
|
|
724
|
+
const urls = {
|
|
725
|
+
tree: (p) => p ? `/s/${token}/tree?path=${encodeURIComponent(p)}` : `/s/${token}`,
|
|
726
|
+
file: (p) => `/s/${token}/file?path=${encodeURIComponent(p)}`,
|
|
727
|
+
zip: (p) => p ? `/s/${token}/zip?path=${encodeURIComponent(p)}` : `/s/${token}/zip`
|
|
728
|
+
};
|
|
729
|
+
let listing = null;
|
|
730
|
+
if (record.directory?.mode !== "zip-only") try {
|
|
731
|
+
listing = await store.listDirectory(record, subPath);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
logShareAudit("share.access_denied", {
|
|
734
|
+
shareId: record.id,
|
|
735
|
+
tokenPrefix: record.token.slice(0, 8),
|
|
736
|
+
subPath,
|
|
737
|
+
reason: String(err)
|
|
738
|
+
}, `Directory listing failed`);
|
|
739
|
+
return c.html(renderShareExpiredPage("not_found"), 404);
|
|
740
|
+
}
|
|
741
|
+
const og = buildLandingOg(service, record);
|
|
742
|
+
return c.html(renderFolderLandingPage(record, listing, urls, { og }));
|
|
743
|
+
}
|
|
744
|
+
function buildLandingOg(service, record) {
|
|
745
|
+
const resolved = resolveShareUrl(record.token, getShareUrlContext(service));
|
|
746
|
+
if (resolved.reachability !== "public") return void 0;
|
|
747
|
+
return {
|
|
748
|
+
absoluteShareUrl: resolved.shareUrl,
|
|
749
|
+
absoluteThumbnailUrl: `${resolved.shareUrl}/thumbnail`,
|
|
750
|
+
title: record.fileName,
|
|
751
|
+
description: record.description ?? void 0
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
async function handleDirectoryFile(c, store, service, record, relPath, clientIp, inline) {
|
|
755
|
+
if (!acquireDownloadSlot(record.token)) return new Response("Too many concurrent downloads for this share", { status: 429 });
|
|
756
|
+
try {
|
|
757
|
+
const resolved = await store.resolveDirectoryChild(record, relPath);
|
|
758
|
+
if (resolved.ok !== true) {
|
|
759
|
+
logShareAudit("share.access_denied", {
|
|
760
|
+
shareId: record.id,
|
|
761
|
+
tokenPrefix: record.token.slice(0, 8),
|
|
762
|
+
reason: resolved.reason,
|
|
763
|
+
clientIp,
|
|
764
|
+
relPath
|
|
765
|
+
}, `Directory child resolution failed: ${resolved.reason}`);
|
|
766
|
+
return c.html(renderShareExpiredPage("not_found"), 404);
|
|
767
|
+
}
|
|
768
|
+
const integrity = await store.validateFileIntegrity(record);
|
|
769
|
+
if (!integrity.valid) return c.html(renderShareExpiredPage(integrity.reason ?? "file_deleted"), 410);
|
|
770
|
+
const fileStat = await stat(resolved.absolutePath);
|
|
771
|
+
if (!fileStat.isFile()) return c.html(renderShareExpiredPage("not_found"), 404);
|
|
772
|
+
store.updateConfig(resolveShareConfig(service));
|
|
773
|
+
const cfg = store.getConfig();
|
|
774
|
+
if (fileStat.size > cfg.maxFileSize) return c.html(renderShareExpiredPage("not_found"), 410);
|
|
775
|
+
const cfgPreview = {
|
|
776
|
+
...SHARE_CONFIG_DEFAULTS,
|
|
777
|
+
...resolveShareConfig(service)
|
|
778
|
+
};
|
|
779
|
+
const baseName = relPath.split("/").pop() || record.fileName;
|
|
780
|
+
const { resolveMimeType } = await import("../../../share/share-store.js");
|
|
781
|
+
const mime = resolveMimeType(baseName);
|
|
782
|
+
const useInline = inline && cfgPreview.inlinePreviewMimes.includes(mime);
|
|
783
|
+
store.incrementDownloadCount(record.id);
|
|
784
|
+
logShareAudit("share.access", {
|
|
785
|
+
shareId: record.id,
|
|
786
|
+
tokenPrefix: record.token.slice(0, 8),
|
|
787
|
+
clientIp,
|
|
788
|
+
relPath,
|
|
789
|
+
mode: useInline ? "inline" : "attachment"
|
|
790
|
+
}, `Directory file served: ${baseName}`);
|
|
791
|
+
const stream = createReadStream(resolved.absolutePath);
|
|
792
|
+
const webStream = Readable.toWeb(stream);
|
|
793
|
+
const disposition = rfc5987ContentDisposition(useInline ? "inline" : "attachment", baseName);
|
|
794
|
+
return new Response(webStream, {
|
|
795
|
+
status: 200,
|
|
796
|
+
headers: {
|
|
797
|
+
"Content-Type": shareResponseContentType(mime),
|
|
798
|
+
"Content-Disposition": disposition,
|
|
799
|
+
"Content-Length": String(fileStat.size),
|
|
800
|
+
"Cache-Control": "private, no-store",
|
|
801
|
+
"X-Content-Type-Options": "nosniff",
|
|
802
|
+
"X-Frame-Options": "DENY",
|
|
803
|
+
"Referrer-Policy": "no-referrer"
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
} finally {
|
|
807
|
+
releaseDownloadSlot(record.token);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async function handleDirectoryZip(c, store, service, record, subPath, clientIp) {
|
|
811
|
+
store.updateConfig(resolveShareConfig(service));
|
|
812
|
+
const cfg = store.getConfig();
|
|
813
|
+
const zipLimit = cfg.directory.zipConcurrency;
|
|
814
|
+
if (!acquireZipSlot(record.token, zipLimit)) return new Response("Too many concurrent ZIP streams for this share", { status: 429 });
|
|
815
|
+
try {
|
|
816
|
+
if ((await store.resolveDirectoryChild(record, subPath)).ok !== true) return c.html(renderShareExpiredPage("not_found"), 404);
|
|
817
|
+
const integrity = await store.validateFileIntegrity(record);
|
|
818
|
+
if (!integrity.valid) return c.html(renderShareExpiredPage(integrity.reason ?? "file_deleted"), 410);
|
|
819
|
+
const files = await planDirectoryFiles(record, {
|
|
820
|
+
rootRelativePath: subPath,
|
|
821
|
+
maxFileCount: cfg.directory.maxFileCount,
|
|
822
|
+
maxFolderSize: cfg.directory.maxFolderSize,
|
|
823
|
+
followSymlinks: record.directory?.followSymlinks ?? false,
|
|
824
|
+
maxDepth: record.directory?.maxDepth ?? cfg.directory.maxDepth
|
|
825
|
+
});
|
|
826
|
+
store.incrementDownloadCount(record.id);
|
|
827
|
+
logShareAudit("share.access", {
|
|
828
|
+
shareId: record.id,
|
|
829
|
+
tokenPrefix: record.token.slice(0, 8),
|
|
830
|
+
clientIp,
|
|
831
|
+
mode: "zip",
|
|
832
|
+
subPath,
|
|
833
|
+
fileCount: files.length
|
|
834
|
+
}, `Directory zip served: ${record.fileName}${subPath ? "/" + subPath : ""}`);
|
|
835
|
+
const zipName = subPath ? `${record.fileName}-${subPath.replace(/[\\/]/g, "_")}.zip` : `${record.fileName}.zip`;
|
|
836
|
+
const stream = createZipStream({ files });
|
|
837
|
+
const webStream = Readable.toWeb(stream);
|
|
838
|
+
return new Response(webStream, {
|
|
839
|
+
status: 200,
|
|
840
|
+
headers: {
|
|
841
|
+
"Content-Type": "application/zip",
|
|
842
|
+
"Content-Disposition": rfc5987ContentDisposition("attachment", zipName),
|
|
843
|
+
"Cache-Control": "private, no-store",
|
|
844
|
+
"X-Content-Type-Options": "nosniff",
|
|
845
|
+
"X-Frame-Options": "DENY",
|
|
846
|
+
"Referrer-Policy": "no-referrer"
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
} catch (err) {
|
|
850
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
851
|
+
return new Response(`zip build failed: ${message}`, { status: 500 });
|
|
852
|
+
} finally {
|
|
853
|
+
releaseZipSlot(record.token);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
async function createFileShareResponse(args) {
|
|
857
|
+
const { c, service, store, probe, decision, ttlMs, maxViews, title, description, audience, workspaceRoot, tokenHash, urlCtx, wantThumbnail } = args;
|
|
858
|
+
store.updateConfig(resolveShareConfig(service));
|
|
859
|
+
const record = await store.create({
|
|
860
|
+
path: relPathFromAbs(workspaceRoot, probe.absolutePath),
|
|
861
|
+
workspaceRoot,
|
|
862
|
+
gatewayTokenHash: tokenHash,
|
|
863
|
+
ttlMs,
|
|
864
|
+
maxViews: maxViews === void 0 ? void 0 : maxViews,
|
|
865
|
+
description,
|
|
866
|
+
kind: probe.kind === "directory" ? "directory" : "file",
|
|
867
|
+
directoryMode: decision.kind === "zip" ? "zip-only" : probe.kind === "directory" ? "browse" : void 0
|
|
868
|
+
});
|
|
869
|
+
const resolved = resolveShareUrl(record.token, urlCtx);
|
|
870
|
+
const titleOut = makeTitle(record.fileName, title);
|
|
871
|
+
const descOut = makeDescription({
|
|
872
|
+
audience,
|
|
873
|
+
expiresAt: record.expiresAt,
|
|
874
|
+
override: description
|
|
875
|
+
});
|
|
876
|
+
const thumbnailUrl = wantThumbnail ? `${resolved.shareUrl}/thumbnail` : "";
|
|
877
|
+
if (wantThumbnail) {
|
|
878
|
+
scheduleThumbnail({
|
|
879
|
+
scope: "file",
|
|
880
|
+
token: record.token,
|
|
881
|
+
recordId: record.id
|
|
882
|
+
}, thumbnailRenderContext(service));
|
|
883
|
+
store.setThumbnailStatus(record.id, "pending");
|
|
884
|
+
}
|
|
885
|
+
return c.json({
|
|
886
|
+
ok: true,
|
|
887
|
+
payload: {
|
|
888
|
+
share: {
|
|
889
|
+
id: record.id,
|
|
890
|
+
kind: decision.kind,
|
|
891
|
+
title: titleOut,
|
|
892
|
+
description: descOut,
|
|
893
|
+
shareUrl: resolved.shareUrl,
|
|
894
|
+
lanUrl: resolved.lanUrl,
|
|
895
|
+
reachability: resolved.reachability,
|
|
896
|
+
reachabilityHint: resolved.reachabilityHint,
|
|
897
|
+
expiresAt: record.expiresAt,
|
|
898
|
+
maxViews: record.maxViews
|
|
899
|
+
},
|
|
900
|
+
thumbnail: {
|
|
901
|
+
url: thumbnailUrl,
|
|
902
|
+
status: wantThumbnail ? "pending" : "unavailable",
|
|
903
|
+
width: SHARE_CONFIG_DEFAULTS.thumbnail.viewportWidth,
|
|
904
|
+
height: SHARE_CONFIG_DEFAULTS.thumbnail.viewportHeight
|
|
905
|
+
},
|
|
906
|
+
routing: {
|
|
907
|
+
reason: decision.reason,
|
|
908
|
+
hint: decision.hint
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}, 201);
|
|
912
|
+
}
|
|
913
|
+
function relPathFromAbs(workspaceRoot, abs) {
|
|
914
|
+
const root = workspaceRoot.replace(/[\\/]+$/, "");
|
|
915
|
+
if (abs === root) return "";
|
|
916
|
+
if (abs.startsWith(`${root}/`)) return abs.slice(root.length + 1);
|
|
917
|
+
if (abs.startsWith(`${root}\\`)) return abs.slice(root.length + 1).replace(/\\/g, "/");
|
|
918
|
+
return abs.split(/[\\/]/).pop() ?? abs;
|
|
919
|
+
}
|
|
322
920
|
async function resolveWorkspaceRootForShare(service, sessionKey, agentId) {
|
|
323
921
|
const cfg = service.currentConfig;
|
|
324
922
|
if (sessionKey) try {
|
|
@@ -331,8 +929,8 @@ async function resolveWorkspaceRootForShare(service, sessionKey, agentId) {
|
|
|
331
929
|
if (root) return root;
|
|
332
930
|
return resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
|
|
333
931
|
}
|
|
334
|
-
async function
|
|
335
|
-
if (!acquireDownloadSlot(record.token)) return
|
|
932
|
+
async function handleFileDownload(c, store, record, clientIp, inline = false) {
|
|
933
|
+
if (!acquireDownloadSlot(record.token)) return new Response("Too many concurrent downloads for this share", { status: 429 });
|
|
336
934
|
try {
|
|
337
935
|
const integrity = await store.validateFileIntegrity(record);
|
|
338
936
|
if (!integrity.valid) {
|
|
@@ -342,27 +940,26 @@ async function handleDownload(c, store, record, clientIp, inline = false) {
|
|
|
342
940
|
reason: integrity.reason,
|
|
343
941
|
clientIp
|
|
344
942
|
}, `Share file integrity check failed: ${integrity.reason}`);
|
|
345
|
-
|
|
346
|
-
return new Response(render(integrity.reason), {
|
|
943
|
+
return new Response(renderShareExpiredPage(integrity.reason ?? "file_deleted"), {
|
|
347
944
|
status: 410,
|
|
348
945
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
349
946
|
});
|
|
350
947
|
}
|
|
351
|
-
store.
|
|
948
|
+
store.incrementDownloadCount(record.id);
|
|
352
949
|
logShareAudit("share.access", {
|
|
353
950
|
shareId: record.id,
|
|
354
951
|
tokenPrefix: record.token.slice(0, 8),
|
|
355
952
|
clientIp,
|
|
356
|
-
|
|
953
|
+
downloadCount: record.downloadCount
|
|
357
954
|
}, `Share downloaded: ${record.fileName}`);
|
|
358
955
|
const fileStat = await stat(record.absolutePath);
|
|
359
956
|
const stream = createReadStream(record.absolutePath);
|
|
360
957
|
const webStream = Readable.toWeb(stream);
|
|
361
|
-
const disposition = inline ?
|
|
958
|
+
const disposition = rfc5987ContentDisposition(inline ? "inline" : "attachment", record.fileName);
|
|
362
959
|
return new Response(webStream, {
|
|
363
960
|
status: 200,
|
|
364
961
|
headers: {
|
|
365
|
-
"Content-Type": record.mimeType,
|
|
962
|
+
"Content-Type": shareResponseContentType(record.mimeType),
|
|
366
963
|
"Content-Disposition": disposition,
|
|
367
964
|
"Content-Length": String(fileStat.size),
|
|
368
965
|
"Cache-Control": "private, no-store",
|