@songsid/agend 2.2.0-beta.9 → 2.3.0-beta.1
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 +40 -24
- package/README.zh-TW.md +40 -24
- package/dist/agent-cli.js +248 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +28 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +30 -0
- package/dist/agent-file-read.js +129 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.js +20 -1
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/atomic-write.js +9 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.d.ts +3 -1
- package/dist/auth-api.js +32 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +33 -0
- package/dist/backend/antigravity.js +129 -21
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +3 -0
- package/dist/backend/claude-code.js +9 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +33 -0
- package/dist/backend/codex-metadata.js +122 -18
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex.d.ts +3 -0
- package/dist/backend/codex.js +17 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/effort-metadata.d.ts +37 -1
- package/dist/backend/effort-metadata.js +86 -15
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.d.ts +3 -0
- package/dist/backend/grok.js +9 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -1
- package/dist/backend/kiro.js +7 -3
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.d.ts +61 -0
- package/dist/backend/launch-effort.js +170 -0
- package/dist/backend/launch-effort.js.map +1 -0
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +24 -0
- package/dist/backend/types.js +12 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-command.d.ts +91 -0
- package/dist/backend-command.js +122 -0
- package/dist/backend-command.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +14 -0
- package/dist/channel/adapters/discord.js +66 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +26 -1
- package/dist/channel/adapters/telegram.js +98 -7
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +36 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +2 -0
- package/dist/channel/tool-router.js +1 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +31 -0
- package/dist/cli.js +124 -24
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +2 -2
- package/dist/command-table.js +5 -2
- package/dist/command-table.js.map +1 -1
- package/dist/compact-watch.d.ts +86 -0
- package/dist/compact-watch.js +288 -0
- package/dist/compact-watch.js.map +1 -0
- package/dist/config-validator.js +55 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +9 -0
- package/dist/daemon.js +394 -29
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +8 -1
- package/dist/delivery-outbox.js +6 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/doctor.js +7 -0
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-context.d.ts +12 -0
- package/dist/fleet-manager.d.ts +247 -9
- package/dist/fleet-manager.js +1227 -142
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +4 -3
- package/dist/forged-envelope-scan.js +4 -3
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
- package/dist/install-lock.d.ts +18 -1
- package/dist/install-lock.js +30 -3
- package/dist/install-lock.js.map +1 -1
- package/dist/instance-avatar.d.ts +5 -4
- package/dist/instance-avatar.js +15 -8
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +3 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +9 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +2 -2
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-compact-pane.d.ts +38 -0
- package/dist/kiro-compact-pane.js +57 -0
- package/dist/kiro-compact-pane.js.map +1 -0
- package/dist/launchd-handoff.d.ts +43 -0
- package/dist/launchd-handoff.js +56 -0
- package/dist/launchd-handoff.js.map +1 -0
- package/dist/locale.js +202 -30
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +10 -2
- package/dist/log-service.js.map +1 -1
- package/dist/login-shell.d.ts +21 -0
- package/dist/login-shell.js +98 -0
- package/dist/login-shell.js.map +1 -0
- package/dist/needs-you-hub.d.ts +4 -0
- package/dist/needs-you-hub.js +31 -6
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +1 -13
- package/dist/needs-you.js +3 -37
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +32 -0
- package/dist/outbound-handlers.js +150 -45
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +76 -0
- package/dist/outbound-schemas.js +65 -4
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/provider-probe.d.ts +29 -0
- package/dist/provider-probe.js +198 -2
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-web-gateway.d.ts +2 -0
- package/dist/public-web-gateway.js +25 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.d.ts +1 -1
- package/dist/quickstart-api.js +8 -6
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +12 -0
- package/dist/quickstart.js +36 -6
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +86 -5
- package/dist/reply-buttons-controller.js +271 -7
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-buttons.d.ts +48 -3
- package/dist/reply-buttons.js +105 -10
- package/dist/reply-buttons.js.map +1 -1
- package/dist/reply-polls.d.ts +85 -0
- package/dist/reply-polls.js +154 -0
- package/dist/reply-polls.js.map +1 -0
- package/dist/repo-worktree-guard.d.ts +36 -0
- package/dist/repo-worktree-guard.js +143 -0
- package/dist/repo-worktree-guard.js.map +1 -0
- package/dist/restart-guard.d.ts +6 -3
- package/dist/restart-guard.js +6 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/restart-progress.d.ts +0 -2
- package/dist/restart-progress.js +0 -2
- package/dist/restart-progress.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +58 -1
- package/dist/scheduler/scheduler.js +176 -10
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/sd-notify.d.ts +22 -1
- package/dist/sd-notify.js +63 -10
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-activation.d.ts +15 -7
- package/dist/service-activation.js +42 -26
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +24 -0
- package/dist/service-installer.js +49 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +11 -0
- package/dist/service-plan.js +23 -0
- package/dist/service-plan.js.map +1 -1
- package/dist/settings-api.js +74 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +8 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +16 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +5 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/systemd-peer-bus.d.ts +95 -0
- package/dist/systemd-peer-bus.js +430 -0
- package/dist/systemd-peer-bus.js.map +1 -0
- package/dist/systemd-runtime.d.ts +27 -1
- package/dist/systemd-runtime.js +69 -7
- package/dist/systemd-runtime.js.map +1 -1
- package/dist/tool-permissions.js +53 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +72 -2
- package/dist/topic-commands.js +156 -38
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.d.ts +128 -0
- package/dist/topic-link.js +269 -0
- package/dist/topic-link.js.map +1 -0
- package/dist/transcript-jsonl.d.ts +7 -2
- package/dist/transcript-jsonl.js +13 -7
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-sources.js +6 -1
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/ui/app-effects.js +187 -0
- package/dist/ui/chat-commands.js +1 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +72 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +59 -14
- package/dist/ui/chat-store.js +78 -8
- package/dist/ui/chat-strings.js +43 -16
- package/dist/ui/chat-thread.js +175 -31
- package/dist/ui/chat-tour.js +18 -15
- package/dist/ui/effect-trial-controller.js +119 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +37 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +101 -27
- package/dist/ui/panel-details.js +18 -5
- package/dist/ui/panel-fleet.js +18 -5
- package/dist/ui/panel-log.js +10 -10
- package/dist/ui/panel-needs.js +13 -10
- package/dist/ui/panel-settings.js +66 -12
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/settings-appearance.js +325 -0
- package/dist/ui/settings-dialogs.js +11 -6
- package/dist/ui/settings-effects.js +201 -0
- package/dist/ui/settings-model.js +3 -1
- package/dist/ui/settings-mods.js +257 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +269 -10
- package/dist/ui/settings-token.js +7 -0
- package/dist/ui/settings-wizard.js +5 -3
- package/dist/ui/shared/agend-auth.js +27 -3
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +3 -1
- package/dist/ui/shared/app-prefs.js +30 -10
- package/dist/ui/shared/app-route.js +23 -1
- package/dist/ui/shared/app-session.js +1 -1
- package/dist/ui/shared/app-shell.js +29 -16
- package/dist/ui/shared/app-store.js +2 -0
- package/dist/ui/shared/app-stream.js +15 -7
- package/dist/ui/shared/app-themes.js +188 -0
- package/dist/ui/shared/app.css +266 -11
- package/dist/ui/shared/app.js +6 -0
- package/dist/ui/shared/card-strings.js +28 -0
- package/dist/ui/shared/effect-app-runtime.js +248 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +154 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +127 -0
- package/dist/ui/shared/effect-geometry.js +122 -0
- package/dist/ui/shared/effect-host-style.js +10 -0
- package/dist/ui/shared/effect-owner-regions.js +74 -0
- package/dist/ui/shared/effect-owner-source.js +115 -0
- package/dist/ui/shared/effect-policy.js +137 -0
- package/dist/ui/shared/effect-poster-reader.js +98 -0
- package/dist/ui/shared/effect-posters.js +117 -0
- package/dist/ui/shared/effect-profiler.js +162 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +496 -0
- package/dist/ui/shared/effect-runtime-loader.js +234 -0
- package/dist/ui/shared/effect-safety.js +153 -0
- package/dist/ui/shared/effect-stock-snapshot.js +34 -0
- package/dist/ui/shared/effect-stock-trust.js +165 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +147 -0
- package/dist/ui/shared/header-tools.js +35 -15
- package/dist/ui/shared/identity-card.js +202 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +22 -14
- package/dist/ui/shared/instance-switch.js +5 -2
- package/dist/ui/shared/owner-css.js +221 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +96 -12
- package/dist/ui/shared/signin.js +17 -9
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +3 -0
- package/dist/ui/shared/ui-menu.js +1 -1
- package/dist/ui/shared/ui-toast.js +1 -1
- package/dist/ui/shared/view-strings.js +40 -2
- package/dist/ui/signin.html +9 -3
- package/dist/update-check.d.ts +31 -0
- package/dist/update-check.js +47 -0
- package/dist/update-check.js.map +1 -1
- package/dist/update-launch.d.ts +48 -1
- package/dist/update-launch.js +159 -5
- package/dist/update-launch.js.map +1 -1
- package/dist/usage/providers.d.ts +28 -8
- package/dist/usage/providers.js +96 -41
- package/dist/usage/providers.js.map +1 -1
- package/dist/view-api.d.ts +29 -0
- package/dist/view-api.js +214 -17
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +86 -2
- package/dist/web-api.js +1160 -92
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-limits.d.ts +82 -0
- package/dist/web-appearance-limits.js +83 -0
- package/dist/web-appearance-limits.js.map +1 -0
- package/dist/web-appearance-policy.d.ts +66 -0
- package/dist/web-appearance-policy.js +94 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +50 -0
- package/dist/web-appearance-tools.js +105 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +99 -0
- package/dist/web-appearance-view.js +163 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +79 -0
- package/dist/web-appearance.js +298 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +98 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +17 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +96 -0
- package/dist/web-card.js +232 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-chat-history.d.ts +39 -1
- package/dist/web-chat-history.js +69 -9
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +39 -3
- package/dist/web-commands.js +22 -5
- package/dist/web-commands.js.map +1 -1
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +126 -0
- package/dist/web-effect-bundle.js.map +1 -0
- package/dist/web-effect-canonical.d.ts +8 -0
- package/dist/web-effect-canonical.js +27 -0
- package/dist/web-effect-canonical.js.map +1 -0
- package/dist/web-effect-library.d.ts +114 -0
- package/dist/web-effect-library.js +530 -0
- package/dist/web-effect-library.js.map +1 -0
- package/dist/web-effect-poster.d.ts +8 -0
- package/dist/web-effect-poster.js +52 -0
- package/dist/web-effect-poster.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +118 -0
- package/dist/web-effect-profiles.js +180 -0
- package/dist/web-effect-profiles.js.map +1 -0
- package/dist/web-effect-service.d.ts +86 -0
- package/dist/web-effect-service.js +312 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +47 -0
- package/dist/web-effect-tools.js +126 -0
- package/dist/web-effect-tools.js.map +1 -0
- package/dist/web-effect-trials.d.ts +86 -0
- package/dist/web-effect-trials.js +204 -0
- package/dist/web-effect-trials.js.map +1 -0
- package/dist/web-effect-upload.d.ts +29 -0
- package/dist/web-effect-upload.js +190 -0
- package/dist/web-effect-upload.js.map +1 -0
- package/dist/web-effect-worker.d.ts +8 -0
- package/dist/web-effect-worker.js +170 -0
- package/dist/web-effect-worker.js.map +1 -0
- package/dist/web-effect.d.ts +89 -0
- package/dist/web-effect.js +132 -0
- package/dist/web-effect.js.map +1 -0
- package/dist/web-identity-color.d.ts +87 -0
- package/dist/web-identity-color.js +209 -0
- package/dist/web-identity-color.js.map +1 -0
- package/dist/web-identity-palette.d.ts +113 -0
- package/dist/web-identity-palette.js +84 -0
- package/dist/web-identity-palette.js.map +1 -0
- package/dist/web-image-meta.d.ts +25 -0
- package/dist/web-image-meta.js +200 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-login.d.ts +14 -0
- package/dist/web-login.js +4 -2
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +203 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +144 -0
- package/dist/web-mod-bundle.js +464 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +59 -0
- package/dist/web-mod-export.js +79 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +283 -0
- package/dist/web-mod-store.js +624 -0
- package/dist/web-mod-store.js.map +1 -0
- package/dist/web-org.d.ts +3 -3
- package/dist/web-org.js +11 -17
- package/dist/web-org.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +180 -0
- package/dist/web-owner-css-store.js +384 -0
- package/dist/web-owner-css-store.js.map +1 -0
- package/dist/web-owner-css.d.ts +58 -0
- package/dist/web-owner-css.js +552 -0
- package/dist/web-owner-css.js.map +1 -0
- package/dist/web-prefs.d.ts +66 -0
- package/dist/web-prefs.js +243 -35
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +9 -1
- package/dist/web-preview.js +24 -7
- package/dist/web-preview.js.map +1 -1
- package/dist/web-profile.d.ts +32 -0
- package/dist/web-profile.js +132 -0
- package/dist/web-profile.js.map +1 -0
- package/dist/web-session.d.ts +14 -0
- package/dist/web-session.js +10 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +18 -0
- package/dist/web-settings-patch.js +51 -0
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +218 -0
- package/dist/web-theme-store.js +516 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +121 -0
- package/dist/web-theme.js +407 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-upload.d.ts +119 -4
- package/dist/web-upload.js +252 -39
- package/dist/web-upload.js.map +1 -1
- package/dist/web-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/launcher/install-admission.cjs +61 -6
- package/package.json +3 -3
package/dist/web-upload.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { openSync } from "node:fs";
|
|
2
|
+
/** #1937: the defaults; `web.upload_max_file_mb` / `web.upload_max_total_mb` change the two sizes (uploadLimits()). */
|
|
1
3
|
export declare const UPLOAD_LIMITS: {
|
|
2
4
|
/** One file. */
|
|
3
5
|
readonly maxFileBytes: number;
|
|
@@ -6,10 +8,79 @@ export declare const UPLOAD_LIMITS: {
|
|
|
6
8
|
/** All files on one message together. */
|
|
7
9
|
readonly maxTotalBytes: number;
|
|
8
10
|
};
|
|
11
|
+
/** #1937: what `web.upload_max_file_mb` and `web.upload_max_total_mb` may be — whole MB (MiB). */
|
|
12
|
+
export declare const UPLOAD_MB_RANGE: {
|
|
13
|
+
readonly min: 1;
|
|
14
|
+
readonly max: 500;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* #1937: one file over the public link. A Cloudflare quick tunnel carries a request body of about 100 MB at most, and
|
|
18
|
+
* one upload is one request, so whatever the config says, a file through the tunnel stays under that.
|
|
19
|
+
*/
|
|
20
|
+
export declare const PUBLIC_LINK_MAX_FILE_BYTES: number;
|
|
21
|
+
export interface UploadLimits {
|
|
22
|
+
maxFileBytes: number;
|
|
23
|
+
maxFiles: number;
|
|
24
|
+
maxTotalBytes: number;
|
|
25
|
+
/** The public link lowered the per-file cap below the configured one (its 413 says why). */
|
|
26
|
+
publicLinkCapped?: true;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* #1937: the limits in force — the one source the upload route, the send, the served-file cap, the page's own check
|
|
30
|
+
* and its words all read. A file is never allowed more than a whole message is; over the public link a file is at most
|
|
31
|
+
* PUBLIC_LINK_MAX_FILE_BYTES. Read on every request, so `agend reload` applies a change to the next upload.
|
|
32
|
+
*/
|
|
33
|
+
export declare function uploadLimits(web: {
|
|
34
|
+
upload_max_file_mb?: unknown;
|
|
35
|
+
upload_max_total_mb?: unknown;
|
|
36
|
+
} | null | undefined, opts?: {
|
|
37
|
+
publicLink?: boolean;
|
|
38
|
+
}): UploadLimits;
|
|
39
|
+
/** A size in MB as the limits are worded ("50", "2.5"). */
|
|
40
|
+
export declare function mbText(bytes: number): string;
|
|
41
|
+
/** #1937: the 413 for one file, from the limits in force (never a constant's number). */
|
|
42
|
+
export declare function fileTooLargeText(limits: UploadLimits): string;
|
|
43
|
+
/** #1937: how long an upload may go without a byte before it is dropped (its partial file with it). */
|
|
44
|
+
export declare const UPLOAD_STALL_MS = 60000;
|
|
45
|
+
/** #1937: the longest one upload may take, start to end (at 95 MB, about 54 KB/s on average). */
|
|
46
|
+
export declare const UPLOAD_MAX_MS: number;
|
|
47
|
+
/** #1937: what a listener allows for receiving one request — above UPLOAD_MAX_MS, so an upload's own clocks decide. */
|
|
48
|
+
export declare const UPLOAD_REQUEST_TIMEOUT_MS: number;
|
|
49
|
+
/** #1937: the one route whose body may take long to arrive (its own clocks bound it). */
|
|
50
|
+
export declare function isUploadRequest(req: {
|
|
51
|
+
method?: string;
|
|
52
|
+
url?: string;
|
|
53
|
+
}): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* #1937: every other request keeps the listener's old bound on receiving it — not whole within `ms`, its connection is
|
|
56
|
+
* dropped (what `server.requestTimeout` did before it was raised for uploads).
|
|
57
|
+
*/
|
|
58
|
+
export declare function limitRequestReceive(req: {
|
|
59
|
+
complete?: boolean;
|
|
60
|
+
socket?: {
|
|
61
|
+
destroy(): void;
|
|
62
|
+
} | null;
|
|
63
|
+
once?(event: "close", fn: () => void): unknown;
|
|
64
|
+
}, ms: number): void;
|
|
9
65
|
/** How long an uploaded file waits to be sent with a message before its id stops working. */
|
|
10
66
|
export declare const UPLOAD_TTL_MS: number;
|
|
11
67
|
/** An upload no message has taken yet is stored under this prefix; it drops the prefix when a message takes it. */
|
|
12
68
|
export declare const PENDING_PREFIX = "web-pending-";
|
|
69
|
+
/** #1937: an upload's bytes stream into this file first (pending prefix: a restart's sweep removes one left behind). */
|
|
70
|
+
export declare const PART_SUFFIX = ".part";
|
|
71
|
+
/**
|
|
72
|
+
* #1937: a new, empty part file in `inboxDir` (made private, 0700), created exclusively, 0600, never through a link.
|
|
73
|
+
* Its bytes are written through `fd` (never by reopening the name); the caller closes it. `discard()` removes the part
|
|
74
|
+
* and the directories made for it, when they are still empty — an upload that ends in nothing leaves nothing behind
|
|
75
|
+
* (an instance deleted while its body was arriving gets no inbox made back, #1252 review P2-3).
|
|
76
|
+
*/
|
|
77
|
+
export declare function createPartFile(inboxDir: string, open?: typeof openSync): {
|
|
78
|
+
path: string;
|
|
79
|
+
fd: number;
|
|
80
|
+
discard(): void;
|
|
81
|
+
};
|
|
82
|
+
/** Remove a file of ours, if it is still there; never throws. */
|
|
83
|
+
export declare function removeQuietly(path: string): void;
|
|
13
84
|
/** The largest file the dashboard will serve back (a reply attachment can be anything the agent made). */
|
|
14
85
|
export declare const MAX_SERVED_BYTES: number;
|
|
15
86
|
export type UploadKind = "photo" | "document";
|
|
@@ -18,12 +89,31 @@ export interface SniffedType {
|
|
|
18
89
|
mime: string;
|
|
19
90
|
ext: string;
|
|
20
91
|
}
|
|
92
|
+
/** #1900: any other file, as Discord and Telegram take it — a download (application/octet-stream), never shown. */
|
|
93
|
+
export declare const OTHER_FILE_MIME = "application/octet-stream";
|
|
94
|
+
/** The extension an upload of an unread type is stored under: its own, lower-cased, if short and plain; else .bin. */
|
|
95
|
+
export declare function storedExtension(name: string): string;
|
|
21
96
|
/**
|
|
22
|
-
* The type of an upload, from its bytes. Images by signature (PNG, JPEG, GIF, WebP), PDF by `%PDF-`,
|
|
23
|
-
*
|
|
24
|
-
*
|
|
97
|
+
* The type of an upload, from its bytes. Images by signature (PNG, JPEG, GIF, WebP), PDF by `%PDF-`, text when it is
|
|
98
|
+
* valid UTF-8 with no NUL byte and not markup. #1900: anything else (an archive, an office file, a binary, HTML, SVG …)
|
|
99
|
+
* is taken too, as a document the dashboard only offers as a download. Null only for an empty file.
|
|
25
100
|
*/
|
|
26
101
|
export declare function sniffUpload(bytes: Uint8Array, name: string): SniffedType | null;
|
|
102
|
+
/**
|
|
103
|
+
* #1937: sniffUpload() as the bytes stream in — the upload is written to disk chunk by chunk, never held whole. Keeps
|
|
104
|
+
* the first bytes (every signature is in the first 12), whether a NUL was seen, and whether everything so far is
|
|
105
|
+
* valid UTF-8 (a streaming decoder: a character split across two chunks is still one character).
|
|
106
|
+
*/
|
|
107
|
+
export declare class UploadSniffer {
|
|
108
|
+
private head;
|
|
109
|
+
private size;
|
|
110
|
+
private nul;
|
|
111
|
+
private utf8;
|
|
112
|
+
private readonly decoder;
|
|
113
|
+
push(chunk: Uint8Array): this;
|
|
114
|
+
/** The type of everything pushed (call once, at the end); null for an empty file. */
|
|
115
|
+
result(name: string): SniffedType | null;
|
|
116
|
+
}
|
|
27
117
|
/** A file name as a label only: no directory, no control characters, no leading dots, bounded. */
|
|
28
118
|
export declare function displayName(raw: string | undefined | null, fallback: string): string;
|
|
29
119
|
/** Any lone surrogate replaced by U+FFFD, so the name always encodes. */
|
|
@@ -81,10 +171,15 @@ export declare class WebFileLedger {
|
|
|
81
171
|
private readonly served;
|
|
82
172
|
private readonly now;
|
|
83
173
|
private readonly maxServed;
|
|
174
|
+
/** #1937: the limits in force (the fleet's config, read each time): the send's count and total, the served-file cap. */
|
|
175
|
+
readonly limits: () => UploadLimits;
|
|
84
176
|
constructor(opts?: {
|
|
85
177
|
now?: () => number;
|
|
86
178
|
maxServed?: number;
|
|
179
|
+
limits?: () => UploadLimits;
|
|
87
180
|
});
|
|
181
|
+
/** #1937: the largest file served back — MAX_SERVED_BYTES, or more when a file that large may be uploaded. */
|
|
182
|
+
maxServedBytes(): number;
|
|
88
183
|
/**
|
|
89
184
|
* Store an upload in `inboxDir` under a name chosen here and register it. The directory is made
|
|
90
185
|
* private (0700), the file 0600, written exclusively (never over an existing file).
|
|
@@ -96,6 +191,16 @@ export declare class WebFileLedger {
|
|
|
96
191
|
name: string;
|
|
97
192
|
type: SniffedType;
|
|
98
193
|
}): UploadEntry;
|
|
194
|
+
/**
|
|
195
|
+
* #1937: register an upload that was streamed to `partPath` (createPartFile(), in the inbox it belongs to): it is
|
|
196
|
+
* linked under its stored name — never over an existing file — and the part goes. The part is removed on failure too.
|
|
197
|
+
*/
|
|
198
|
+
storeUploadedFile(input: {
|
|
199
|
+
instance: string;
|
|
200
|
+
partPath: string;
|
|
201
|
+
name: string;
|
|
202
|
+
type: SniffedType;
|
|
203
|
+
}): UploadEntry;
|
|
99
204
|
/**
|
|
100
205
|
* The uploads a message names, all or nothing: each must exist, belong to this instance, not be
|
|
101
206
|
* expired and be waiting for a message; together within the per-message limits. On success they are
|
|
@@ -143,6 +248,16 @@ export declare class WebFileLedger {
|
|
|
143
248
|
file: ServedFile;
|
|
144
249
|
bytes: Buffer;
|
|
145
250
|
} | null;
|
|
251
|
+
/**
|
|
252
|
+
* #1937: a served file opened for streaming whole, with read()'s checks on the open descriptor (no symlink followed,
|
|
253
|
+
* a regular file, the device + inode and size it was registered with). The caller closes `fd`. Null when any fails.
|
|
254
|
+
*/
|
|
255
|
+
open(id: string): {
|
|
256
|
+
file: ServedFile;
|
|
257
|
+
fd: number;
|
|
258
|
+
} | null;
|
|
259
|
+
/** #1937: what is served under an id (no file is opened: open() and read() check the file itself). */
|
|
260
|
+
servedFile(id: string): ServedFile | null;
|
|
146
261
|
/** #1589: a served file that is audio or video (MEDIA_MIME), or null — the file route plays those by range. */
|
|
147
262
|
mediaOf(id: string): ServedFile | null;
|
|
148
263
|
/**
|
|
@@ -174,7 +289,7 @@ export declare class WebFileLedger {
|
|
|
174
289
|
* symlink, and a regular file within MAX_SERVED_BYTES; its real path inside this instance's inbox
|
|
175
290
|
* (`workspaces/<instance>/inbox`, itself not a link elsewhere) — or (#1770) inside its reply snapshots
|
|
176
291
|
* (`webFilesDir`, `workspaces/<instance>/web-files`, the same checks), then served as a snapshot again. Null when any
|
|
177
|
-
* check fails (the message then shows the name as unavailable). Fetched by id only, as ever: the stored path is the
|
|
292
|
+
* check fails (the message then shows the name as unavailable). (Within maxServedBytes(): #1937.) Fetched by id only, as ever: the stored path is the
|
|
178
293
|
* fleet's own file, never a request's.
|
|
179
294
|
*/
|
|
180
295
|
restoreServed(entry: {
|
package/dist/web-upload.js
CHANGED
|
@@ -12,58 +12,206 @@
|
|
|
12
12
|
* label, and a file can be fetched back only by an id this process issued for it — never by a path.
|
|
13
13
|
*/
|
|
14
14
|
import { randomBytes } from "node:crypto";
|
|
15
|
-
import { constants as fsConstants, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { constants as fsConstants, closeSync, existsSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { basename, dirname, extname, join, sep } from "node:path";
|
|
17
|
+
const MB = 1024 * 1024;
|
|
18
|
+
/** #1937: the defaults; `web.upload_max_file_mb` / `web.upload_max_total_mb` change the two sizes (uploadLimits()). */
|
|
17
19
|
export const UPLOAD_LIMITS = {
|
|
18
20
|
/** One file. */
|
|
19
|
-
maxFileBytes:
|
|
21
|
+
maxFileBytes: 50 * MB,
|
|
20
22
|
/** Files on one message. */
|
|
21
23
|
maxFiles: 5,
|
|
22
24
|
/** All files on one message together. */
|
|
23
|
-
maxTotalBytes:
|
|
25
|
+
maxTotalBytes: 100 * MB,
|
|
24
26
|
};
|
|
27
|
+
/** #1937: what `web.upload_max_file_mb` and `web.upload_max_total_mb` may be — whole MB (MiB). */
|
|
28
|
+
export const UPLOAD_MB_RANGE = { min: 1, max: 500 };
|
|
29
|
+
/**
|
|
30
|
+
* #1937: one file over the public link. A Cloudflare quick tunnel carries a request body of about 100 MB at most, and
|
|
31
|
+
* one upload is one request, so whatever the config says, a file through the tunnel stays under that.
|
|
32
|
+
*/
|
|
33
|
+
export const PUBLIC_LINK_MAX_FILE_BYTES = 95 * MB;
|
|
34
|
+
/** A configured size in MB, or the default when it is absent or not a whole number in UPLOAD_MB_RANGE (the validator says so). */
|
|
35
|
+
function configuredMb(v, fallback) {
|
|
36
|
+
return typeof v === "number" && Number.isInteger(v) && v >= UPLOAD_MB_RANGE.min && v <= UPLOAD_MB_RANGE.max ? v * MB : fallback;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* #1937: the limits in force — the one source the upload route, the send, the served-file cap, the page's own check
|
|
40
|
+
* and its words all read. A file is never allowed more than a whole message is; over the public link a file is at most
|
|
41
|
+
* PUBLIC_LINK_MAX_FILE_BYTES. Read on every request, so `agend reload` applies a change to the next upload.
|
|
42
|
+
*/
|
|
43
|
+
export function uploadLimits(web, opts = {}) {
|
|
44
|
+
const total = configuredMb(web?.upload_max_total_mb, UPLOAD_LIMITS.maxTotalBytes);
|
|
45
|
+
const file = Math.min(configuredMb(web?.upload_max_file_mb, UPLOAD_LIMITS.maxFileBytes), total);
|
|
46
|
+
const capped = opts.publicLink === true && file > PUBLIC_LINK_MAX_FILE_BYTES;
|
|
47
|
+
return { maxFileBytes: capped ? PUBLIC_LINK_MAX_FILE_BYTES : file, maxFiles: UPLOAD_LIMITS.maxFiles, maxTotalBytes: total, ...(capped ? { publicLinkCapped: true } : {}) };
|
|
48
|
+
}
|
|
49
|
+
/** A size in MB as the limits are worded ("50", "2.5"). */
|
|
50
|
+
export function mbText(bytes) { return String(Math.round((bytes / MB) * 10) / 10); }
|
|
51
|
+
/** #1937: the 413 for one file, from the limits in force (never a constant's number). */
|
|
52
|
+
export function fileTooLargeText(limits) {
|
|
53
|
+
return limits.publicLinkCapped
|
|
54
|
+
? `a file can be at most ${mbText(limits.maxFileBytes)} MB over the public link (its Cloudflare tunnel carries about 100 MB per request)`
|
|
55
|
+
: `a file can be at most ${mbText(limits.maxFileBytes)} MB`;
|
|
56
|
+
}
|
|
57
|
+
/** #1937: how long an upload may go without a byte before it is dropped (its partial file with it). */
|
|
58
|
+
export const UPLOAD_STALL_MS = 60_000;
|
|
59
|
+
/** #1937: the longest one upload may take, start to end (at 95 MB, about 54 KB/s on average). */
|
|
60
|
+
export const UPLOAD_MAX_MS = 30 * 60 * 1000;
|
|
61
|
+
/** #1937: what a listener allows for receiving one request — above UPLOAD_MAX_MS, so an upload's own clocks decide. */
|
|
62
|
+
export const UPLOAD_REQUEST_TIMEOUT_MS = UPLOAD_MAX_MS + 60_000;
|
|
63
|
+
/** #1937: the one route whose body may take long to arrive (its own clocks bound it). */
|
|
64
|
+
export function isUploadRequest(req) {
|
|
65
|
+
return req.method === "POST" && (req.url ?? "").split("?")[0] === "/ui/upload";
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* #1937: every other request keeps the listener's old bound on receiving it — not whole within `ms`, its connection is
|
|
69
|
+
* dropped (what `server.requestTimeout` did before it was raised for uploads).
|
|
70
|
+
*/
|
|
71
|
+
export function limitRequestReceive(req, ms) {
|
|
72
|
+
if (req.complete !== false)
|
|
73
|
+
return; // whole already (a request object always says)
|
|
74
|
+
const t = setTimeout(() => { if (req.complete === false)
|
|
75
|
+
req.socket?.destroy(); }, ms);
|
|
76
|
+
t.unref?.();
|
|
77
|
+
req.once?.("close", () => clearTimeout(t));
|
|
78
|
+
}
|
|
25
79
|
/** How long an uploaded file waits to be sent with a message before its id stops working. */
|
|
26
80
|
export const UPLOAD_TTL_MS = 30 * 60 * 1000;
|
|
27
81
|
/** An upload no message has taken yet is stored under this prefix; it drops the prefix when a message takes it. */
|
|
28
82
|
export const PENDING_PREFIX = "web-pending-";
|
|
29
83
|
function sentPath(p) { const b = basename(p); return b.startsWith(PENDING_PREFIX) ? join(dirname(p), "web-" + b.slice(PENDING_PREFIX.length)) : p; }
|
|
30
84
|
function pendingPath(p) { const b = basename(p); return b.startsWith("web-") && !b.startsWith(PENDING_PREFIX) ? join(dirname(p), PENDING_PREFIX + b.slice(4)) : p; }
|
|
85
|
+
/** #1937: an upload's bytes stream into this file first (pending prefix: a restart's sweep removes one left behind). */
|
|
86
|
+
export const PART_SUFFIX = ".part";
|
|
87
|
+
/**
|
|
88
|
+
* #1937: a new, empty part file in `inboxDir` (made private, 0700), created exclusively, 0600, never through a link.
|
|
89
|
+
* Its bytes are written through `fd` (never by reopening the name); the caller closes it. `discard()` removes the part
|
|
90
|
+
* and the directories made for it, when they are still empty — an upload that ends in nothing leaves nothing behind
|
|
91
|
+
* (an instance deleted while its body was arriving gets no inbox made back, #1252 review P2-3).
|
|
92
|
+
*/
|
|
93
|
+
export function createPartFile(inboxDir, open = openSync) {
|
|
94
|
+
const made = mkdirSync(inboxDir, { recursive: true, mode: 0o700 });
|
|
95
|
+
const path = join(inboxDir, `${PENDING_PREFIX}${Date.now()}-${randomBytes(6).toString("hex")}${PART_SUFFIX}`);
|
|
96
|
+
const discard = () => {
|
|
97
|
+
removeQuietly(path);
|
|
98
|
+
if (!made)
|
|
99
|
+
return;
|
|
100
|
+
// From the inbox up to the first directory made here; rmdir only removes an empty one (never one that was there).
|
|
101
|
+
for (let d = inboxDir; d.length >= made.length; d = dirname(d)) {
|
|
102
|
+
try {
|
|
103
|
+
rmdirSync(d);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (d === made)
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
// Prism #1941 P3-1: an open that fails (EMFILE, ENOSPC …) still takes back the directories made for it.
|
|
113
|
+
let fd;
|
|
114
|
+
try {
|
|
115
|
+
fd = open(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW, 0o600);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
discard();
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
return { path, fd, discard };
|
|
122
|
+
}
|
|
123
|
+
/** Remove a file of ours, if it is still there; never throws. */
|
|
124
|
+
export function removeQuietly(path) { try {
|
|
125
|
+
unlinkSync(path);
|
|
126
|
+
}
|
|
127
|
+
catch { /* already gone */ } }
|
|
31
128
|
/** The largest file the dashboard will serve back (a reply attachment can be anything the agent made). */
|
|
32
129
|
export const MAX_SERVED_BYTES = 50 * 1024 * 1024;
|
|
33
130
|
/** Text files keep a known extension from their name; anything else is stored as .txt. */
|
|
34
131
|
const TEXT_EXTENSIONS = new Set([
|
|
35
|
-
".txt", ".md", ".csv", ".tsv", ".json", ".log", ".yaml", ".yml", ".xml", ".
|
|
132
|
+
".txt", ".md", ".csv", ".tsv", ".json", ".log", ".yaml", ".yml", ".xml", ".css",
|
|
36
133
|
".js", ".ts", ".tsx", ".jsx", ".py", ".rb", ".go", ".rs", ".java", ".kt", ".c", ".h", ".cpp", ".sh", ".sql", ".toml", ".ini", ".diff", ".patch",
|
|
37
134
|
]);
|
|
135
|
+
/** #1900: markup a browser would render or run. Valid text or not, it is stored under its own name and only downloaded. */
|
|
136
|
+
const MARKUP_EXTENSIONS = new Set([".html", ".htm", ".xhtml", ".shtml", ".svg", ".svgz", ".mht", ".mhtml", ".xsl", ".xslt"]);
|
|
137
|
+
/** Extensions of the types read from the bytes above: a file whose bytes are not that type does not keep the name. */
|
|
138
|
+
const SNIFFED_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".pdf"]);
|
|
139
|
+
/** #1900: any other file, as Discord and Telegram take it — a download (application/octet-stream), never shown. */
|
|
140
|
+
export const OTHER_FILE_MIME = "application/octet-stream";
|
|
141
|
+
/** The extension an upload of an unread type is stored under: its own, lower-cased, if short and plain; else .bin. */
|
|
142
|
+
export function storedExtension(name) {
|
|
143
|
+
const ext = extname(name).toLowerCase();
|
|
144
|
+
return /^\.[a-z0-9]{1,10}$/.test(ext) && !SNIFFED_EXTENSIONS.has(ext) ? ext : ".bin";
|
|
145
|
+
}
|
|
38
146
|
const startsWith = (b, sig, at = 0) => sig.every((v, i) => b[at + i] === v);
|
|
39
147
|
/**
|
|
40
|
-
* The type of an upload, from its bytes. Images by signature (PNG, JPEG, GIF, WebP), PDF by `%PDF-`,
|
|
41
|
-
*
|
|
42
|
-
*
|
|
148
|
+
* The type of an upload, from its bytes. Images by signature (PNG, JPEG, GIF, WebP), PDF by `%PDF-`, text when it is
|
|
149
|
+
* valid UTF-8 with no NUL byte and not markup. #1900: anything else (an archive, an office file, a binary, HTML, SVG …)
|
|
150
|
+
* is taken too, as a document the dashboard only offers as a download. Null only for an empty file.
|
|
43
151
|
*/
|
|
44
152
|
export function sniffUpload(bytes, name) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
153
|
+
return new UploadSniffer().push(bytes).result(name);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* #1937: sniffUpload() as the bytes stream in — the upload is written to disk chunk by chunk, never held whole. Keeps
|
|
157
|
+
* the first bytes (every signature is in the first 12), whether a NUL was seen, and whether everything so far is
|
|
158
|
+
* valid UTF-8 (a streaming decoder: a character split across two chunks is still one character).
|
|
159
|
+
*/
|
|
160
|
+
export class UploadSniffer {
|
|
161
|
+
head = Buffer.alloc(0);
|
|
162
|
+
size = 0;
|
|
163
|
+
nul = false;
|
|
164
|
+
utf8 = true;
|
|
165
|
+
decoder = new TextDecoder("utf-8", { fatal: true });
|
|
166
|
+
push(chunk) {
|
|
167
|
+
if (chunk.length === 0)
|
|
168
|
+
return this;
|
|
169
|
+
this.size += chunk.length;
|
|
170
|
+
if (this.head.length < 16)
|
|
171
|
+
this.head = Buffer.concat([this.head, Buffer.from(chunk.subarray(0, 16 - this.head.length))]);
|
|
172
|
+
if (!this.nul && chunk.includes(0))
|
|
173
|
+
this.nul = true;
|
|
174
|
+
if (this.utf8 && !this.nul) {
|
|
175
|
+
try {
|
|
176
|
+
this.decoder.decode(chunk, { stream: true });
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
this.utf8 = false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return this;
|
|
61
183
|
}
|
|
62
|
-
|
|
63
|
-
|
|
184
|
+
/** The type of everything pushed (call once, at the end); null for an empty file. */
|
|
185
|
+
result(name) {
|
|
186
|
+
if (this.size === 0)
|
|
187
|
+
return null;
|
|
188
|
+
const b = this.head;
|
|
189
|
+
if (startsWith(b, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
|
|
190
|
+
return { kind: "photo", mime: "image/png", ext: ".png" };
|
|
191
|
+
if (startsWith(b, [0xff, 0xd8, 0xff]))
|
|
192
|
+
return { kind: "photo", mime: "image/jpeg", ext: ".jpg" };
|
|
193
|
+
if (startsWith(b, [0x47, 0x49, 0x46, 0x38]))
|
|
194
|
+
return { kind: "photo", mime: "image/gif", ext: ".gif" };
|
|
195
|
+
if (startsWith(b, [0x52, 0x49, 0x46, 0x46]) && startsWith(b, [0x57, 0x45, 0x42, 0x50], 8))
|
|
196
|
+
return { kind: "photo", mime: "image/webp", ext: ".webp" };
|
|
197
|
+
if (startsWith(b, [0x25, 0x50, 0x44, 0x46, 0x2d]))
|
|
198
|
+
return { kind: "document", mime: "application/pdf", ext: ".pdf" };
|
|
199
|
+
const other = { kind: "document", mime: OTHER_FILE_MIME, ext: storedExtension(name) };
|
|
200
|
+
const ext = extname(name).toLowerCase();
|
|
201
|
+
if (MARKUP_EXTENSIONS.has(ext) || this.nul)
|
|
202
|
+
return other;
|
|
203
|
+
if (this.utf8) {
|
|
204
|
+
try {
|
|
205
|
+
this.decoder.decode();
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
this.utf8 = false;
|
|
209
|
+
} // a character cut off at the very end
|
|
210
|
+
}
|
|
211
|
+
if (!this.utf8)
|
|
212
|
+
return other;
|
|
213
|
+
return { kind: "document", mime: "text/plain; charset=utf-8", ext: TEXT_EXTENSIONS.has(ext) ? ext : ".txt" };
|
|
64
214
|
}
|
|
65
|
-
const ext = extname(name).toLowerCase();
|
|
66
|
-
return { kind: "document", mime: "text/plain; charset=utf-8", ext: TEXT_EXTENSIONS.has(ext) ? ext : ".txt" };
|
|
67
215
|
}
|
|
68
216
|
/** A file name as a label only: no directory, no control characters, no leading dots, bounded. */
|
|
69
217
|
export function displayName(raw, fallback) {
|
|
@@ -117,28 +265,58 @@ export class WebFileLedger {
|
|
|
117
265
|
served = new Map();
|
|
118
266
|
now;
|
|
119
267
|
maxServed;
|
|
268
|
+
/** #1937: the limits in force (the fleet's config, read each time): the send's count and total, the served-file cap. */
|
|
269
|
+
limits;
|
|
120
270
|
constructor(opts = {}) {
|
|
121
271
|
// Elapsed time, like the sweep timer below: a wall-clock jump must neither expire an upload early nor keep
|
|
122
272
|
// one past the timer that was meant to remove it (#1252 review).
|
|
123
273
|
this.now = opts.now ?? (() => performance.now());
|
|
124
274
|
this.maxServed = opts.maxServed ?? 2000;
|
|
275
|
+
this.limits = opts.limits ?? (() => uploadLimits(null));
|
|
125
276
|
}
|
|
277
|
+
/** #1937: the largest file served back — MAX_SERVED_BYTES, or more when a file that large may be uploaded. */
|
|
278
|
+
maxServedBytes() { return Math.max(MAX_SERVED_BYTES, this.limits().maxFileBytes); }
|
|
126
279
|
/**
|
|
127
280
|
* Store an upload in `inboxDir` under a name chosen here and register it. The directory is made
|
|
128
281
|
* private (0700), the file 0600, written exclusively (never over an existing file).
|
|
129
282
|
*/
|
|
130
283
|
storeUpload(input) {
|
|
131
|
-
|
|
284
|
+
const part = createPartFile(input.inboxDir);
|
|
285
|
+
try {
|
|
286
|
+
writeFileSync(part.fd, input.bytes);
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
removeQuietly(part.path);
|
|
290
|
+
throw err;
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
closeSync(part.fd);
|
|
294
|
+
}
|
|
295
|
+
return this.storeUploadedFile({ instance: input.instance, partPath: part.path, name: input.name, type: input.type });
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* #1937: register an upload that was streamed to `partPath` (createPartFile(), in the inbox it belongs to): it is
|
|
299
|
+
* linked under its stored name — never over an existing file — and the part goes. The part is removed on failure too.
|
|
300
|
+
*/
|
|
301
|
+
storeUploadedFile(input) {
|
|
132
302
|
const id = randomBytes(16).toString("hex");
|
|
133
303
|
// Stored as web-pending-…: the name says on disk that no message has taken it yet, so a sweep after a restart
|
|
134
304
|
// (which loses this ledger) can still tell an abandoned upload from one an agent got (#1273).
|
|
135
|
-
const path = join(input.
|
|
136
|
-
|
|
305
|
+
const path = join(dirname(input.partPath), `${PENDING_PREFIX}${Date.now()}-${id.slice(0, 8)}${input.type.ext}`);
|
|
306
|
+
try {
|
|
307
|
+
const part = lstatSync(input.partPath);
|
|
308
|
+
if (!part.isFile())
|
|
309
|
+
throw new Error("the upload's part file is not a regular file");
|
|
310
|
+
linkSync(input.partPath, path);
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
removeQuietly(input.partPath);
|
|
314
|
+
}
|
|
315
|
+
const st = lstatSync(path);
|
|
137
316
|
const entry = {
|
|
138
317
|
id, instance: input.instance, path, kind: input.type.kind, mime: input.type.mime,
|
|
139
|
-
name: displayName(input.name, "file"), size:
|
|
318
|
+
name: displayName(input.name, "file"), size: st.size, expiresAt: this.now() + UPLOAD_TTL_MS, state: "pending",
|
|
140
319
|
};
|
|
141
|
-
const st = lstatSync(path);
|
|
142
320
|
this.written.set(entry, { dev: st.dev, ino: st.ino });
|
|
143
321
|
this.prune();
|
|
144
322
|
this.uploads.set(id, entry);
|
|
@@ -155,8 +333,9 @@ export class WebFileLedger {
|
|
|
155
333
|
* says how the delivery went: commit() when it was delivered, release() when it definitely was not.
|
|
156
334
|
*/
|
|
157
335
|
takeForMessage(instance, ids) {
|
|
158
|
-
|
|
159
|
-
|
|
336
|
+
const limits = this.limits();
|
|
337
|
+
if (ids.length > limits.maxFiles)
|
|
338
|
+
return { ok: false, error: `at most ${limits.maxFiles} files per message` };
|
|
160
339
|
if (new Set(ids).size !== ids.length)
|
|
161
340
|
return { ok: false, error: "the same file twice" };
|
|
162
341
|
const entries = [];
|
|
@@ -169,8 +348,8 @@ export class WebFileLedger {
|
|
|
169
348
|
total += e.size;
|
|
170
349
|
entries.push(e);
|
|
171
350
|
}
|
|
172
|
-
if (total >
|
|
173
|
-
return { ok: false, error: `the files together are over ${
|
|
351
|
+
if (total > limits.maxTotalBytes)
|
|
352
|
+
return { ok: false, error: `the files together are over ${mbText(limits.maxTotalBytes)} MB` };
|
|
174
353
|
// Each file gets its sent name now, before the delivery names it to the agent; all or nothing.
|
|
175
354
|
const moved = [];
|
|
176
355
|
for (const e of entries) {
|
|
@@ -270,7 +449,7 @@ export class WebFileLedger {
|
|
|
270
449
|
try {
|
|
271
450
|
realPath = realpathSync(input.path);
|
|
272
451
|
const st = statSync(realPath);
|
|
273
|
-
if (!st.isFile() || st.size >
|
|
452
|
+
if (!st.isFile() || st.size > this.maxServedBytes())
|
|
274
453
|
return null;
|
|
275
454
|
({ size, dev, ino } = st);
|
|
276
455
|
}
|
|
@@ -312,6 +491,40 @@ export class WebFileLedger {
|
|
|
312
491
|
const got = this.readSlice(id, 0, Number.MAX_SAFE_INTEGER);
|
|
313
492
|
return got && got.start === 0 && got.bytes.length === got.file.size ? { file: got.file, bytes: got.bytes } : null;
|
|
314
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* #1937: a served file opened for streaming whole, with read()'s checks on the open descriptor (no symlink followed,
|
|
496
|
+
* a regular file, the device + inode and size it was registered with). The caller closes `fd`. Null when any fails.
|
|
497
|
+
*/
|
|
498
|
+
open(id) {
|
|
499
|
+
const file = isFileId(id) ? this.served.get(id) : undefined;
|
|
500
|
+
if (!file)
|
|
501
|
+
return null;
|
|
502
|
+
let fd = null;
|
|
503
|
+
try {
|
|
504
|
+
fd = openSync(file.realPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
505
|
+
const st = fstatSync(fd);
|
|
506
|
+
if (st.isFile() && st.size === file.size && st.dev === file.dev && st.ino === file.ino) {
|
|
507
|
+
const out = { file, fd };
|
|
508
|
+
fd = null;
|
|
509
|
+
return out;
|
|
510
|
+
}
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
finally {
|
|
517
|
+
if (fd !== null)
|
|
518
|
+
try {
|
|
519
|
+
closeSync(fd);
|
|
520
|
+
}
|
|
521
|
+
catch { /* closed */ }
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/** #1937: what is served under an id (no file is opened: open() and read() check the file itself). */
|
|
525
|
+
servedFile(id) {
|
|
526
|
+
return (isFileId(id) ? this.served.get(id) : undefined) ?? null;
|
|
527
|
+
}
|
|
315
528
|
/** #1589: a served file that is audio or video (MEDIA_MIME), or null — the file route plays those by range. */
|
|
316
529
|
mediaOf(id) {
|
|
317
530
|
const file = isFileId(id) ? this.served.get(id) : undefined;
|
|
@@ -396,7 +609,7 @@ export class WebFileLedger {
|
|
|
396
609
|
* symlink, and a regular file within MAX_SERVED_BYTES; its real path inside this instance's inbox
|
|
397
610
|
* (`workspaces/<instance>/inbox`, itself not a link elsewhere) — or (#1770) inside its reply snapshots
|
|
398
611
|
* (`webFilesDir`, `workspaces/<instance>/web-files`, the same checks), then served as a snapshot again. Null when any
|
|
399
|
-
* check fails (the message then shows the name as unavailable). Fetched by id only, as ever: the stored path is the
|
|
612
|
+
* check fails (the message then shows the name as unavailable). (Within maxServedBytes(): #1937.) Fetched by id only, as ever: the stored path is the
|
|
400
613
|
* fleet's own file, never a request's.
|
|
401
614
|
*/
|
|
402
615
|
restoreServed(entry, instance, inboxDir, webFilesDir) {
|
|
@@ -409,7 +622,7 @@ export class WebFileLedger {
|
|
|
409
622
|
let snapshot = false;
|
|
410
623
|
try {
|
|
411
624
|
own = lstatSync(entry.path);
|
|
412
|
-
if (own.isSymbolicLink() || !own.isFile() || own.size >
|
|
625
|
+
if (own.isSymbolicLink() || !own.isFile() || own.size > this.maxServedBytes())
|
|
413
626
|
return null;
|
|
414
627
|
real = realpathSync(entry.path);
|
|
415
628
|
// The directory, resolved, must be itself (not a link elsewhere); the file's real path directly under one of them.
|