@songsid/agend 2.2.0-beta.1 → 2.2.0-beta.10
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 +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/agent-cli-instructions.md +16 -9
- package/dist/agent-cli.js +15 -7
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +21 -9
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +47 -0
- package/dist/agent-whats-new.js +147 -0
- package/dist/agent-whats-new.js.map +1 -0
- package/dist/auth-api.d.ts +2 -2
- package/dist/auth-api.js +4 -2
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +3 -0
- package/dist/backend/antigravity.js +9 -0
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-carry.d.ts +22 -0
- package/dist/backend/claude-carry.js +170 -0
- package/dist/backend/claude-carry.js.map +1 -0
- package/dist/backend/claude-code.d.ts +65 -2
- package/dist/backend/claude-code.js +226 -33
- 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/credential-profile.d.ts +40 -3
- package/dist/backend/credential-profile.js +113 -5
- package/dist/backend/credential-profile.js.map +1 -1
- package/dist/backend/effort-metadata.d.ts +1 -1
- package/dist/backend/effort-metadata.js +3 -2
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.d.ts +4 -0
- package/dist/backend/grok.js +14 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro-engine-ledger.d.ts +5 -0
- package/dist/backend/kiro-engine-ledger.js +20 -0
- package/dist/backend/kiro-engine-ledger.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +6 -0
- package/dist/backend/kiro-identity.js +19 -5
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +39 -3
- package/dist/backend/kiro.js +143 -19
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.d.ts +33 -0
- package/dist/backend/launch-effort.js +123 -0
- package/dist/backend/launch-effort.js.map +1 -0
- package/dist/backend/muse.d.ts +84 -0
- package/dist/backend/muse.js +449 -53
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +33 -4
- package/dist/backend/types.js +2 -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/background-work-ended.d.ts +18 -0
- package/dist/background-work-ended.js +30 -0
- package/dist/background-work-ended.js.map +1 -0
- package/dist/cache-service.d.ts +2 -0
- package/dist/cache-service.js +2 -1
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +24 -1
- package/dist/channel/adapters/discord.js +87 -2
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +24 -2
- package/dist/channel/adapters/telegram.js +118 -20
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-server.js +9 -1
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +11 -0
- package/dist/channel/mcp-tools.js +24 -13
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/message-edit-errors.d.ts +5 -0
- package/dist/channel/message-edit-errors.js +24 -0
- package/dist/channel/message-edit-errors.js.map +1 -0
- package/dist/channel/telegram-markdown.d.ts +6 -0
- package/dist/channel/telegram-markdown.js +104 -0
- package/dist/channel/telegram-markdown.js.map +1 -0
- package/dist/channel/tool-router.js +9 -3
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +39 -3
- package/dist/cli.js +133 -28
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +2 -2
- package/dist/command-table.js +6 -3
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +59 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +15 -2
- package/dist/cross-instance-envelope.js +29 -5
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +156 -1
- package/dist/daemon.js +764 -89
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +62 -7
- package/dist/delivery-outbox.js +309 -22
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +18 -1
- package/dist/delivery-reconciliation.js +67 -2
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/fleet-context.d.ts +10 -1
- package/dist/fleet-manager.d.ts +345 -10
- package/dist/fleet-manager.js +1868 -174
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +92 -21
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +27 -3
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +9 -9
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
- package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
- package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +8 -3
- package/dist/general-knowledge/steering/core-rules.md +8 -7
- package/dist/host-memory.d.ts +10 -0
- package/dist/host-memory.js +33 -1
- package/dist/host-memory.js.map +1 -1
- package/dist/human-delivery-receipts.d.ts +83 -0
- package/dist/human-delivery-receipts.js +194 -0
- package/dist/human-delivery-receipts.js.map +1 -0
- package/dist/instance-avatar.d.ts +36 -0
- package/dist/instance-avatar.js +57 -0
- package/dist/instance-avatar.js.map +1 -0
- package/dist/instance-config-impact.js +6 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +29 -6
- package/dist/instance-lifecycle.js +79 -28
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +3 -1
- package/dist/instructions.js +11 -8
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-log-gate.d.ts +42 -0
- package/dist/kiro-log-gate.js +39 -0
- package/dist/kiro-log-gate.js.map +1 -0
- package/dist/locale.js +200 -44
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +99 -0
- package/dist/log-service.js +243 -0
- package/dist/log-service.js.map +1 -0
- package/dist/log-steps.d.ts +169 -0
- package/dist/log-steps.js +858 -0
- package/dist/log-steps.js.map +1 -0
- package/dist/login-controller.d.ts +20 -3
- package/dist/login-controller.js +71 -19
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +5 -2
- package/dist/login-flows.js +5 -4
- package/dist/login-flows.js.map +1 -1
- package/dist/memory-pressure.d.ts +13 -1
- package/dist/memory-pressure.js +50 -10
- package/dist/memory-pressure.js.map +1 -1
- package/dist/needs-you-hub.js +2 -0
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.d.ts +6 -0
- package/dist/needs-you-live.js +23 -3
- package/dist/needs-you-live.js.map +1 -1
- package/dist/next-steps.js +1 -1
- package/dist/next-steps.js.map +1 -1
- package/dist/outbound-handlers.d.ts +100 -0
- package/dist/outbound-handlers.js +263 -96
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +26 -15
- package/dist/outbound-schemas.js +24 -7
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-link-progress.d.ts +1 -1
- package/dist/public-link-progress.js +8 -2
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-preview.d.ts +101 -0
- package/dist/public-preview.js +236 -0
- package/dist/public-preview.js.map +1 -0
- package/dist/public-web-gateway.js +11 -4
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +24 -1
- package/dist/public-web-link.js +63 -9
- package/dist/public-web-link.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +3 -0
- package/dist/reply-buttons-controller.js +3 -2
- package/dist/reply-buttons-controller.js.map +1 -1
- 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 +33 -0
- package/dist/restart-progress.js +30 -0
- package/dist/restart-progress.js.map +1 -1
- package/dist/service-activation.d.ts +15 -7
- package/dist/service-activation.js +46 -29
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +31 -0
- package/dist/service-installer.js +41 -2
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.js +85 -25
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +72 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +28 -4
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +7 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +1 -1
- package/dist/spawn-gate.js +5 -1
- package/dist/spawn-gate.js.map +1 -1
- package/dist/spawn-hold-reply.d.ts +12 -0
- package/dist/spawn-hold-reply.js +39 -0
- package/dist/spawn-hold-reply.js.map +1 -0
- package/dist/status-emojis.d.ts +10 -3
- package/dist/status-emojis.js +11 -2
- package/dist/status-emojis.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/tips.js +4 -4
- package/dist/tips.js.map +1 -1
- package/dist/tool-progress.js +3 -2
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.d.ts +42 -2
- package/dist/topic-commands.js +92 -7
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.d.ts +3 -3
- package/dist/transcript-delta-reader.js +8 -8
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/transcript-jsonl.d.ts +80 -3
- package/dist/transcript-jsonl.js +147 -10
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-monitor.d.ts +19 -0
- package/dist/transcript-monitor.js +87 -15
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +78 -0
- package/dist/transcript-sources.js +375 -64
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +15 -2
- package/dist/transcript-turns.js +15 -2
- package/dist/transcript-turns.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +7 -15
- package/dist/tunnel/cloudflared.js +36 -1
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/purpose-lane.d.ts +5 -1
- package/dist/tunnel/purpose-lane.js +14 -7
- package/dist/tunnel/purpose-lane.js.map +1 -1
- package/dist/tunnel/types.d.ts +5 -1
- package/dist/tunnel/types.js.map +1 -1
- package/dist/turn-resume.d.ts +2 -0
- package/dist/turn-resume.js +1 -1
- package/dist/turn-resume.js.map +1 -1
- package/dist/types.d.ts +25 -0
- package/dist/ui/chat-commands.js +1 -0
- package/dist/ui/chat-emojis.js +33 -0
- package/dist/ui/chat-mentions.js +63 -0
- package/dist/ui/chat-render.js +42 -2
- package/dist/ui/chat-store.js +2 -2
- package/dist/ui/chat-strings.js +38 -20
- package/dist/ui/chat-thread.js +69 -12
- package/dist/ui/chat-time.js +80 -0
- package/dist/ui/panel-chat.js +39 -10
- package/dist/ui/panel-details.js +2 -0
- package/dist/ui/panel-fleet.js +199 -57
- package/dist/ui/panel-log.js +428 -0
- package/dist/ui/panel-needs.js +6 -2
- package/dist/ui/panel-settings.js +137 -32
- package/dist/ui/preview-panel.js +55 -15
- package/dist/ui/preview-status.js +28 -0
- package/dist/ui/preview.js +44 -3
- package/dist/ui/settings-dialogs.js +7 -2
- package/dist/ui/settings-model.js +2 -1
- package/dist/ui/settings-strings.js +46 -4
- package/dist/ui/settings-token.js +1 -1
- package/dist/ui/shared/app-i18n.js +43 -17
- package/dist/ui/shared/app-prefs.js +250 -0
- package/dist/ui/shared/app-route.js +7 -2
- package/dist/ui/shared/app-shell.js +165 -8
- package/dist/ui/shared/app-store.js +5 -1
- package/dist/ui/shared/app-stream.js +23 -5
- package/dist/ui/shared/app.css +155 -22
- package/dist/ui/shared/app.js +11 -1
- package/dist/ui/shared/fonts/README.md +1 -1
- package/dist/ui/shared/header-tools.js +55 -5
- package/dist/ui/shared/instance-nav.js +241 -45
- package/dist/ui/shared/instance-switch.js +4 -4
- package/dist/ui/shared/panel-view.js +6 -0
- package/dist/ui/shared/signin.js +7 -7
- package/dist/ui/shared/theme.js +39 -7
- package/dist/ui/shared/tokens.css +1 -1
- package/dist/ui/shared/ui-icons.js +1 -0
- package/dist/ui/shared/usage-dialog.js +23 -5
- package/dist/ui/shared/view-strings.js +32 -4
- package/dist/ui/signin.html +1 -1
- package/dist/unrecognised-prompt.d.ts +23 -0
- package/dist/unrecognised-prompt.js +74 -0
- package/dist/unrecognised-prompt.js.map +1 -0
- package/dist/update-install.d.ts +28 -2
- package/dist/update-install.js +22 -4
- package/dist/update-install.js.map +1 -1
- package/dist/update-marker.d.ts +51 -0
- package/dist/update-marker.js +61 -0
- package/dist/update-marker.js.map +1 -1
- package/dist/update-preflight.d.ts +100 -0
- package/dist/update-preflight.js +264 -0
- package/dist/update-preflight.js.map +1 -0
- package/dist/updater-caller.d.ts +31 -0
- package/dist/updater-caller.js +109 -0
- package/dist/updater-caller.js.map +1 -0
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +34 -5
- package/dist/usage/providers.js +160 -27
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/statusline-usage.d.ts +6 -2
- package/dist/usage/statusline-usage.js +10 -4
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/view-api.d.ts +1 -1
- package/dist/view-api.js +38 -8
- package/dist/view-api.js.map +1 -1
- package/dist/wake-coordinator.d.ts +11 -0
- package/dist/wake-coordinator.js +38 -4
- package/dist/wake-coordinator.js.map +1 -1
- package/dist/web-api.d.ts +49 -2
- package/dist/web-api.js +421 -31
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +2 -2
- package/dist/web-auth.js +9 -4
- package/dist/web-auth.js.map +1 -1
- package/dist/web-commands.d.ts +27 -4
- package/dist/web-commands.js +19 -4
- package/dist/web-commands.js.map +1 -1
- package/dist/web-host-guard.d.ts +3 -11
- package/dist/web-host-guard.js +3 -12
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-images.d.ts +47 -0
- package/dist/web-images.js +169 -0
- package/dist/web-images.js.map +1 -0
- package/dist/web-mentions.d.ts +44 -0
- package/dist/web-mentions.js +75 -0
- package/dist/web-mentions.js.map +1 -0
- package/dist/web-prefs.d.ts +63 -0
- package/dist/web-prefs.js +265 -0
- package/dist/web-prefs.js.map +1 -0
- package/dist/web-preview.d.ts +14 -1
- package/dist/web-preview.js +35 -13
- package/dist/web-preview.js.map +1 -1
- package/dist/web-reply-files.d.ts +45 -0
- package/dist/web-reply-files.js +223 -0
- package/dist/web-reply-files.js.map +1 -0
- package/dist/web-schedules.d.ts +12 -0
- package/dist/web-schedules.js +34 -0
- package/dist/web-schedules.js.map +1 -0
- package/dist/web-server-drain.d.ts +10 -0
- package/dist/web-server-drain.js +50 -0
- package/dist/web-server-drain.js.map +1 -0
- package/dist/web-session.d.ts +36 -1
- package/dist/web-session.js +53 -6
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +23 -0
- package/dist/web-settings-patch.js +77 -0
- package/dist/web-settings-patch.js.map +1 -0
- package/dist/web-shell-routes.d.ts +5 -1
- package/dist/web-shell-routes.js +10 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-upload.d.ts +17 -4
- package/dist/web-upload.js +64 -13
- package/dist/web-upload.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +1 -1
- package/templates/launchd.plist.ejs +3 -1
- package/templates/systemd.service.ejs +2 -1
package/dist/web-api.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { webHumanOrigin } from "./human-delivery-receipts.js";
|
|
2
|
+
import { withSpawnHoldReply } from "./spawn-hold-reply.js";
|
|
1
3
|
import { binaryProbe } from "./binary-probe.js";
|
|
2
4
|
import { settingsRequestExecution, settingsWrite, isSettingsReplay } from "./settings-request-capability.js";
|
|
3
5
|
import { readBoundedWebBody } from "./web-body.js";
|
|
@@ -16,7 +18,9 @@ import { shellRoute } from "./web-shell-routes.js";
|
|
|
16
18
|
import { readFileSync } from "node:fs";
|
|
17
19
|
import { join, dirname } from "node:path";
|
|
18
20
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
import { CreateInstanceArgs, validateArgs } from "./outbound-schemas.js";
|
|
21
|
+
import { CreateInstanceArgs, CreateScheduleArgs, UpdateScheduleArgs, validateArgs } from "./outbound-schemas.js";
|
|
22
|
+
import { scheduleView } from "./web-schedules.js";
|
|
23
|
+
import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
|
|
20
24
|
import { readStatuslineModel, resolveInstanceContext } from "./topic-commands.js";
|
|
21
25
|
import { z } from "zod";
|
|
22
26
|
import { evaluateWebRequest, isPassiveWebRead, isSecureRequest, isWebRequestAuthorized, WEB_TOKEN_INVALID_MESSAGE } from "./web-auth.js";
|
|
@@ -24,8 +28,12 @@ import { dashboardOriginFor } from "./web-preview.js";
|
|
|
24
28
|
import { newWebMessageId, parseLastEventId } from "./web-chat-history.js";
|
|
25
29
|
import { attachmentDelivery, displayName, INLINE_MIME, isFileId, MEDIA_RANGE_MAX, publicAttachment, sniffUpload, UPLOAD_LIMITS, wellFormed } from "./web-upload.js";
|
|
26
30
|
import { getAgendHome } from "./paths.js";
|
|
31
|
+
import { OPERATOR } from "./web-prefs.js";
|
|
32
|
+
import { commitTeamCreate, commitTeamUpdate } from "./outbound-handlers.js";
|
|
33
|
+
import { parseMentionQuery } from "./web-mentions.js";
|
|
27
34
|
import { authorizeExplicitInstanceRemoval } from "./instance-removal.js";
|
|
28
35
|
import { measureSyncWork } from "./sync-work-attribution.js";
|
|
36
|
+
import { resolveTz } from "./tz-utils.js";
|
|
29
37
|
// ── Strict public-facing schemas ────────────────────────────────────────────
|
|
30
38
|
// web-api endpoints must reject unknown fields so the dashboard cannot inject
|
|
31
39
|
// internal-only flags that would reach handleCreate/scheduler/config writers.
|
|
@@ -45,18 +53,17 @@ const TaskUpdateSchema = z.object({
|
|
|
45
53
|
description: z.string().max(MAX_TEXT).optional(),
|
|
46
54
|
priority: z.string().max(64).optional(),
|
|
47
55
|
}).strict();
|
|
48
|
-
const ScheduleCreateSchema = z.object({
|
|
49
|
-
cron: z.string().min(1).max(256),
|
|
50
|
-
message: z.string().min(1).max(MAX_TEXT),
|
|
51
|
-
target: z.string().min(1).max(256),
|
|
52
|
-
label: z.string().max(256).optional(),
|
|
53
|
-
timezone: z.string().max(128).optional(),
|
|
54
|
-
}).strict();
|
|
55
56
|
const TeamCreateSchema = z.object({
|
|
56
57
|
name: z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/),
|
|
57
58
|
members: z.array(z.string().min(1).max(256)).min(1).max(256),
|
|
58
59
|
description: z.string().max(MAX_TEXT).optional(),
|
|
59
60
|
}).strict();
|
|
61
|
+
// #1689: an edit of a team — its members as they should be, and its description ("" clears it). No name: a team is
|
|
62
|
+
// not renamed.
|
|
63
|
+
const TeamEditSchema = z.object({
|
|
64
|
+
members: z.array(z.string().min(1).max(256)).min(1).max(256),
|
|
65
|
+
description: z.string().max(MAX_TEXT).optional(),
|
|
66
|
+
}).strict();
|
|
60
67
|
const ConfigUpdateSchema = z.object({
|
|
61
68
|
channel: z.object({
|
|
62
69
|
group_id: z.union([z.number(), z.string()]).optional(),
|
|
@@ -130,6 +137,18 @@ function json(res, status, data) {
|
|
|
130
137
|
res.writeHead(status);
|
|
131
138
|
res.end(JSON.stringify(data));
|
|
132
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* #1630 (#1635 review): an answer that waited on something (a list, a download) is sent only if the request is still
|
|
142
|
+
* allowed — the session not revoked meanwhile, a public link's exposure still current. Otherwise 401, and nothing.
|
|
143
|
+
*/
|
|
144
|
+
function stillAuthorized(req, res, url, ctx) {
|
|
145
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
146
|
+
return false;
|
|
147
|
+
if (isWebRequestAuthorized(req, url, ctx.webToken, ctx.webSessions, { touch: false }))
|
|
148
|
+
return true;
|
|
149
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
133
152
|
/**
|
|
134
153
|
* Handle a Web UI request. Returns true if handled, false to pass through.
|
|
135
154
|
*/
|
|
@@ -328,6 +347,45 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
328
347
|
json(res, 200, { messages: ctx.webChatHistory?.list(instance, limit) ?? [], boot: ctx.webChatHistory?.boot ?? null, lastId: ctx.webChatHistory?.lastId ?? 0 });
|
|
329
348
|
return true;
|
|
330
349
|
}
|
|
350
|
+
// ── #1218: the Log tab ─────────────────────────────────
|
|
351
|
+
// An instance's steps, read on demand from its own transcript (log-service.ts). Read-only, and offered on the public
|
|
352
|
+
// link on purpose (the user's decision): the same masking applies to every viewer. Passive (web-auth.ts).
|
|
353
|
+
if (method === "GET" && path === "/ui/steps") {
|
|
354
|
+
const p = url.searchParams;
|
|
355
|
+
const instance = p.get("instance") ?? "";
|
|
356
|
+
const num = (k) => {
|
|
357
|
+
const v = p.get(k);
|
|
358
|
+
if (v === null)
|
|
359
|
+
return undefined;
|
|
360
|
+
const n = Number(v);
|
|
361
|
+
return /^\d{1,15}$/.test(v) && Number.isSafeInteger(n) ? n : null;
|
|
362
|
+
};
|
|
363
|
+
const after = num("after"), before = num("before"), ev = num("ev"), limit = num("limit");
|
|
364
|
+
const key = p.get("key");
|
|
365
|
+
if (!instance || instance.length > 128 || after === null || before === null || ev === null || limit === null
|
|
366
|
+
|| (limit !== undefined && (limit < 1 || limit > 200)) || (key !== null && !/^\d{1,20}-\d{1,20}$/.test(key))) {
|
|
367
|
+
json(res, 400, { error: "instance (1-128 chars); after, before, ev: offsets; limit 1-200; key: <dev>-<ino>" });
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
if (!ctx.logSteps) {
|
|
371
|
+
json(res, 404, { error: "not available" });
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
ctx.logSteps(instance, {
|
|
375
|
+
...(after !== undefined ? { after } : {}), ...(before !== undefined ? { before } : {}), ...(ev !== undefined ? { ev } : {}),
|
|
376
|
+
...(limit !== undefined ? { limit } : {}), ...(key !== null ? { key } : {}),
|
|
377
|
+
skip: p.get("skip") === "1", watch: p.get("watch") === "1",
|
|
378
|
+
}).then(answer => {
|
|
379
|
+
if (answer === null)
|
|
380
|
+
json(res, 404, { error: `Instance not found: ${instance}` });
|
|
381
|
+
else
|
|
382
|
+
json(res, 200, answer);
|
|
383
|
+
}).catch(err => {
|
|
384
|
+
ctx.logger.error({ err: err.message, instance }, "Log steps read failed");
|
|
385
|
+
json(res, 500, { error: "could not read the steps" });
|
|
386
|
+
});
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
331
389
|
// ── Send message ───────────────────────────────────────
|
|
332
390
|
if (method === "POST" && path === "/ui/send") {
|
|
333
391
|
handleSendMessage(req, res, ctx);
|
|
@@ -365,6 +423,66 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
365
423
|
res.end(bytes);
|
|
366
424
|
return true;
|
|
367
425
|
}
|
|
426
|
+
// ── #1630: avatars and Discord server emoji, same-origin (the page never loads a platform's CDN; img-src stays 'self') ──
|
|
427
|
+
const imageMatch = path.match(/^\/ui\/img\/(bot|avatar|emoji)\/([^/]+)$/);
|
|
428
|
+
if (method === "GET" && imageMatch) {
|
|
429
|
+
const kind = imageMatch[1];
|
|
430
|
+
let key;
|
|
431
|
+
try {
|
|
432
|
+
key = decodeURIComponent(imageMatch[2]);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
json(res, 404, { error: "No such image" });
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
const send = (image) => {
|
|
439
|
+
if (res.headersSent || res.destroyed)
|
|
440
|
+
return;
|
|
441
|
+
if (!stillAuthorized(req, res, url, ctx))
|
|
442
|
+
return;
|
|
443
|
+
if (!image) {
|
|
444
|
+
json(res, 404, { error: "No such image" });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
res.setHeader("Content-Type", image.mime);
|
|
448
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
449
|
+
// A changed avatar is a new URL (?v=); an emoji id is one picture for good.
|
|
450
|
+
res.setHeader("Cache-Control", "private, max-age=86400");
|
|
451
|
+
res.setHeader("Content-Security-Policy", "default-src 'none'; sandbox");
|
|
452
|
+
res.writeHead(200);
|
|
453
|
+
res.end(image.bytes);
|
|
454
|
+
};
|
|
455
|
+
if (kind === "emoji") {
|
|
456
|
+
(ctx.webEmoji ? ctx.webEmoji(key) : Promise.resolve(null)).then(send, () => send(null));
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
send(ctx.webImage?.(kind, key) ?? null);
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
if (method === "GET" && path === "/ui/emojis") {
|
|
463
|
+
const answer = (emojis) => { if (!res.headersSent && !res.destroyed && stillAuthorized(req, res, url, ctx))
|
|
464
|
+
json(res, 200, { emojis }); };
|
|
465
|
+
(ctx.webEmojiList ? ctx.webEmojiList() : Promise.resolve([])).then(answer, () => answer([]));
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
// #1682: the names of the mention ids a page found in what it rendered (at most MAX_MENTION_IDS, digits only) — only
|
|
469
|
+
// those, so no page (the public link included) ever gets a directory; from memory, never a request.
|
|
470
|
+
if (method === "GET" && path === "/ui/mentions") {
|
|
471
|
+
const parsed = parseMentionQuery(url.searchParams);
|
|
472
|
+
if (!parsed.ok) {
|
|
473
|
+
json(res, 400, { error: parsed.error });
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
let names = { u: {}, r: {}, c: {} };
|
|
477
|
+
try {
|
|
478
|
+
names = ctx.webMentionNames?.(parsed.query) ?? names;
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
ctx.logger.debug({ err: err.message }, "mention names unavailable");
|
|
482
|
+
}
|
|
483
|
+
json(res, 200, names);
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
368
486
|
// ── Fleet prompts (C4): the hang / exit / interactive-prompt buttons, answerable here too ──
|
|
369
487
|
if (method === "GET" && path === "/ui/prompts") {
|
|
370
488
|
json(res, 200, { prompts: ctx.listWebPrompts?.() ?? [] });
|
|
@@ -444,6 +562,97 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
444
562
|
});
|
|
445
563
|
return true;
|
|
446
564
|
}
|
|
565
|
+
// #1723: the public link page's preview, asked while it says "Preview is starting…" — its state and, once up, its
|
|
566
|
+
// origin and boot. Public link only: a local page's preview is decided by its own load.
|
|
567
|
+
if (path === "/ui/preview-status" && method === "GET") {
|
|
568
|
+
const gateway = gatewayRequestContext(req);
|
|
569
|
+
if (!gateway || !ctx.publicPreviewForUi) {
|
|
570
|
+
json(res, 404, { error: "not offered here" });
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
const p = ctx.publicPreviewForUi(gateway.exposureId, gateway.expectedOrigin);
|
|
574
|
+
json(res, 200, { code: p.code, previewOrigin: p.previewOrigin, boot: p.previewOrigin ? p.boot : null, detail: p.detail ?? null });
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
// #1665: the web preferences that follow the person between devices. Read by any page of the panel, the public link
|
|
578
|
+
// included (it reads what is in force); written only by a signed-in admin session or the CLI's header token — never
|
|
579
|
+
// over the public link (absent from its route manifest, and refused here too), never by a read-only session.
|
|
580
|
+
if (path === "/ui/prefs" && method === "GET") {
|
|
581
|
+
if (!ctx.webPrefs) {
|
|
582
|
+
json(res, 404, { error: "Preferences are not kept on this fleet" });
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
json(res, 200, ctx.webPrefs.read(OPERATOR));
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
if (path === "/ui/prefs" && method === "POST") {
|
|
589
|
+
const prefs = ctx.webPrefs;
|
|
590
|
+
if (!prefs) {
|
|
591
|
+
json(res, 404, { error: "Preferences are not kept on this fleet" });
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
if (gatewayRequestContext(req)) {
|
|
595
|
+
json(res, 403, { error: "prefs_not_over_public_link" });
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
// Who may write, asked now and again once the body is in: a session revoked or expired, or a token rotated, while
|
|
599
|
+
// the body was on its way writes nothing and announces nothing.
|
|
600
|
+
const writer = () => {
|
|
601
|
+
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
602
|
+
if (verdict.kind !== "allow")
|
|
603
|
+
return 401;
|
|
604
|
+
return verdict.via === "session" && verdict.session?.tier !== "admin" ? 403 : 200;
|
|
605
|
+
};
|
|
606
|
+
const refuse = (code) => json(res, code, code === 401 ? { error: WEB_TOKEN_INVALID_MESSAGE } : { error: "prefs_read_only" });
|
|
607
|
+
const first = writer();
|
|
608
|
+
if (first !== 200) {
|
|
609
|
+
refuse(first);
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
(async () => {
|
|
613
|
+
let body;
|
|
614
|
+
try {
|
|
615
|
+
body = await parseBody(req);
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
json(res, 400, { error: "Invalid JSON" });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
622
|
+
return;
|
|
623
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
624
|
+
json(res, 400, { error: "set required" });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const parsed = prefs.parsePatch(body.set);
|
|
628
|
+
if (!parsed.ok) {
|
|
629
|
+
json(res, 400, { error: parsed.error });
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const now = writer();
|
|
633
|
+
if (now !== 200) {
|
|
634
|
+
refuse(now);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const onlyIfUnset = body.onlyIfUnset === true;
|
|
638
|
+
const written = prefs.write(OPERATOR, parsed.patch, { onlyIfUnset });
|
|
639
|
+
if (!written.ok) {
|
|
640
|
+
json(res, written.status, { error: written.error });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
// Another device sees the new rev in the next status frame — at once on the live stream.
|
|
644
|
+
if (written.changed.length)
|
|
645
|
+
ctx.emitSseEvent("status", ctx.getUiStatus());
|
|
646
|
+
json(res, 200, { ...written.view, changed: written.changed });
|
|
647
|
+
})().catch(err => {
|
|
648
|
+
ctx.logger.error({ err: err.message }, "Saving web preferences failed");
|
|
649
|
+
try {
|
|
650
|
+
json(res, 500, { error: "Saving preferences failed" });
|
|
651
|
+
}
|
|
652
|
+
catch { /* already answered */ }
|
|
653
|
+
});
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
447
656
|
// #1386: Acknowledge a "Needs you" delivery item — a session write (CSRF, checked by the gate) or the CLI's header
|
|
448
657
|
// token. Who did it is recorded by principal: the session's public handle, never the session id or a credential.
|
|
449
658
|
if (method === "POST" && path === "/ui/needs/ack") {
|
|
@@ -586,13 +795,18 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
586
795
|
}
|
|
587
796
|
const topicMode = ctx.fleetConfig?.channel?.mode === "topic";
|
|
588
797
|
(async () => {
|
|
798
|
+
let held = false;
|
|
589
799
|
try {
|
|
590
|
-
await ctx.startInstance(name, config, topicMode ?? false);
|
|
800
|
+
await withSpawnHoldReply(async (message) => { held = true; json(res, 202, { held: true, message, instance: name }); }, () => ctx.startInstance(name, config, topicMode ?? false));
|
|
591
801
|
ctx.emitSseEvent("status", ctx.getUiStatus());
|
|
592
|
-
|
|
802
|
+
if (!held)
|
|
803
|
+
json(res, 200, { started: name });
|
|
593
804
|
}
|
|
594
805
|
catch (err) {
|
|
595
|
-
|
|
806
|
+
if (!held)
|
|
807
|
+
json(res, 500, { error: err.message });
|
|
808
|
+
else
|
|
809
|
+
ctx.logger.error({ err, name }, "Held instance action failed");
|
|
596
810
|
}
|
|
597
811
|
})();
|
|
598
812
|
return true;
|
|
@@ -705,14 +919,19 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
705
919
|
if (method === "POST" && restartMatch) {
|
|
706
920
|
const name = decodeURIComponent(restartMatch[1]);
|
|
707
921
|
(async () => {
|
|
922
|
+
let held = false;
|
|
708
923
|
try {
|
|
709
|
-
await ctx.restartSingleInstance(name, { explicit: true });
|
|
924
|
+
await withSpawnHoldReply(async (message) => { held = true; json(res, 202, { held: true, message, instance: name }); }, () => ctx.restartSingleInstance(name, { explicit: true }));
|
|
710
925
|
ctx.emitSseEvent("status", ctx.getUiStatus());
|
|
711
|
-
|
|
926
|
+
if (!held)
|
|
927
|
+
json(res, 200, { restarted: name });
|
|
712
928
|
}
|
|
713
929
|
catch (err) {
|
|
714
930
|
const status = err.message.includes("not found") ? 404 : 500;
|
|
715
|
-
|
|
931
|
+
if (!held)
|
|
932
|
+
json(res, status, { error: err.message });
|
|
933
|
+
else
|
|
934
|
+
ctx.logger.error({ err, name }, "Held instance action failed");
|
|
716
935
|
}
|
|
717
936
|
})();
|
|
718
937
|
return true;
|
|
@@ -830,10 +1049,28 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
830
1049
|
json(res, 200, { schedules: [] });
|
|
831
1050
|
return true;
|
|
832
1051
|
}
|
|
833
|
-
|
|
1052
|
+
// #1724: each row with its next run and one-shot time in the fleet's zone — worked out here, not on the page.
|
|
1053
|
+
const scheduler = ctx.scheduler;
|
|
1054
|
+
const now = Date.now();
|
|
1055
|
+
json(res, 200, {
|
|
1056
|
+
schedules: scheduler.list().map((s) => scheduleView(s, scheduler.getRetry?.(s.id) ?? null, now)),
|
|
1057
|
+
// The zone a new schedule (and a "Once" date and time typed without one) is read in.
|
|
1058
|
+
default_timezone: ctx.fleetConfig?.defaults?.scheduler?.default_timezone
|
|
1059
|
+
?? DEFAULT_SCHEDULER_CONFIG.default_timezone,
|
|
1060
|
+
});
|
|
834
1061
|
return true;
|
|
835
1062
|
}
|
|
1063
|
+
// #1724: who may write a schedule, asked again once the body is in (a session revoked meanwhile, a non-admin tier).
|
|
1064
|
+
const scheduleWriterGone = () => {
|
|
1065
|
+
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
1066
|
+
return verdict.kind !== "allow" || (verdict.via === "session" && verdict.session?.tier !== "admin");
|
|
1067
|
+
};
|
|
836
1068
|
if (method === "POST" && path === "/ui/schedules") {
|
|
1069
|
+
// #1724: schedules are not created, edited or deleted over the public link (absent from its manifest too, #1689).
|
|
1070
|
+
if (gatewayRequestContext(req)) {
|
|
1071
|
+
json(res, 403, { error: "schedule_not_over_public_link" });
|
|
1072
|
+
return true;
|
|
1073
|
+
}
|
|
837
1074
|
if (!ctx.scheduler) {
|
|
838
1075
|
json(res, 500, { error: "Scheduler not initialized" });
|
|
839
1076
|
return true;
|
|
@@ -843,10 +1080,23 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
843
1080
|
const body = await parseBody(req);
|
|
844
1081
|
if (!permitWebContinuation(req, res, ctx))
|
|
845
1082
|
return;
|
|
846
|
-
|
|
1083
|
+
if (scheduleWriterGone()) {
|
|
1084
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
// #1724: the create_schedule tool's own schema — cron or at (exactly one) — plus a target, which the web names.
|
|
1088
|
+
const parsed = parseOrReject(CreateScheduleArgs, body, res);
|
|
847
1089
|
if (!parsed)
|
|
848
1090
|
return;
|
|
849
|
-
|
|
1091
|
+
if (typeof parsed.target !== "string" || !parsed.target) {
|
|
1092
|
+
json(res, 400, { error: "target: an instance is required" });
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const schedule = ctx.scheduler.create({
|
|
1096
|
+
cron: parsed.cron, at: parsed.at, message: parsed.message, target: parsed.target, label: parsed.label, timezone: parsed.timezone,
|
|
1097
|
+
...(parsed.silent !== undefined ? { silent: parsed.silent } : {}),
|
|
1098
|
+
source: "web", reply_chat_id: "", reply_thread_id: null,
|
|
1099
|
+
});
|
|
850
1100
|
json(res, 200, schedule);
|
|
851
1101
|
}
|
|
852
1102
|
catch (err) {
|
|
@@ -856,7 +1106,48 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
856
1106
|
return true;
|
|
857
1107
|
}
|
|
858
1108
|
const schedDelMatch = path.match(/^\/ui\/schedules\/(.+)$/);
|
|
1109
|
+
// #1724: Edit — the update_schedule tool's validation (UpdateScheduleArgs) and its call (scheduler.update): cron and at
|
|
1110
|
+
// clear each other there; silent is not touched.
|
|
1111
|
+
if (method === "PATCH" && schedDelMatch) {
|
|
1112
|
+
if (gatewayRequestContext(req)) {
|
|
1113
|
+
json(res, 403, { error: "schedule_not_over_public_link" });
|
|
1114
|
+
return true;
|
|
1115
|
+
}
|
|
1116
|
+
if (!ctx.scheduler || typeof ctx.scheduler.update !== "function") {
|
|
1117
|
+
json(res, 500, { error: "Scheduler not initialized" });
|
|
1118
|
+
return true;
|
|
1119
|
+
}
|
|
1120
|
+
const id = decodeURIComponent(schedDelMatch[1]);
|
|
1121
|
+
(async () => {
|
|
1122
|
+
try {
|
|
1123
|
+
const body = await parseBody(req);
|
|
1124
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
1125
|
+
return;
|
|
1126
|
+
if (scheduleWriterGone()) {
|
|
1127
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || Object.hasOwn(body, "id")) {
|
|
1131
|
+
json(res, 400, { error: "expected the fields to change (the id is the path's)" });
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const parsed = parseOrReject(UpdateScheduleArgs, { ...body, id }, res);
|
|
1135
|
+
if (!parsed)
|
|
1136
|
+
return;
|
|
1137
|
+
const { id: _id, ...changes } = parsed;
|
|
1138
|
+
json(res, 200, ctx.scheduler.update(id, changes));
|
|
1139
|
+
}
|
|
1140
|
+
catch (err) {
|
|
1141
|
+
json(res, 400, { error: err.message });
|
|
1142
|
+
}
|
|
1143
|
+
})();
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
859
1146
|
if (method === "DELETE" && schedDelMatch) {
|
|
1147
|
+
if (gatewayRequestContext(req)) {
|
|
1148
|
+
json(res, 403, { error: "schedule_not_over_public_link" });
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
860
1151
|
if (!ctx.scheduler) {
|
|
861
1152
|
json(res, 500, { error: "Scheduler not initialized" });
|
|
862
1153
|
return true;
|
|
@@ -893,26 +1184,82 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
893
1184
|
return true;
|
|
894
1185
|
}
|
|
895
1186
|
if (method === "POST" && path === "/ui/teams") {
|
|
1187
|
+
// #1689: not over the public link (absent from its manifest too), like editing and deleting a team.
|
|
1188
|
+
if (gatewayRequestContext(req)) {
|
|
1189
|
+
json(res, 403, { error: "teams_not_over_public_link" });
|
|
1190
|
+
return true;
|
|
1191
|
+
}
|
|
896
1192
|
(async () => {
|
|
897
1193
|
try {
|
|
898
1194
|
const body = await parseBody(req);
|
|
899
1195
|
if (!permitWebContinuation(req, res, ctx))
|
|
900
1196
|
return;
|
|
1197
|
+
// #1692: who may write, asked again once the body is in (as the edit does).
|
|
1198
|
+
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
1199
|
+
if (verdict.kind !== "allow" || (verdict.via === "session" && verdict.session?.tier !== "admin")) {
|
|
1200
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
901
1203
|
const parsed = parseOrReject(TeamCreateSchema, body, res);
|
|
902
1204
|
if (!parsed)
|
|
903
1205
|
return;
|
|
904
|
-
|
|
905
|
-
|
|
1206
|
+
// #1692: the create_team path — members checked, an existing team never overwritten, a failed save undone.
|
|
1207
|
+
const r = commitTeamCreate(ctx.fleetConfig, parsed, () => ctx.saveFleetConfig());
|
|
1208
|
+
if (!r.ok) {
|
|
1209
|
+
json(res, r.status, { error: r.error });
|
|
906
1210
|
return;
|
|
907
1211
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1212
|
+
json(res, 200, r.result);
|
|
1213
|
+
}
|
|
1214
|
+
catch (err) {
|
|
1215
|
+
json(res, 400, { error: err.message });
|
|
1216
|
+
}
|
|
1217
|
+
})();
|
|
1218
|
+
return true;
|
|
1219
|
+
}
|
|
1220
|
+
// #1689: edit a team — the same update as the update_team tool (applyTeamUpdate), from the members the page sends:
|
|
1221
|
+
// what to add and remove is worked out against the team as it is now. Never over the public link.
|
|
1222
|
+
const teamEditMatch = path.match(/^\/ui\/teams\/(.+)$/);
|
|
1223
|
+
if ((method === "PATCH" || method === "PUT") && teamEditMatch) {
|
|
1224
|
+
if (gatewayRequestContext(req)) {
|
|
1225
|
+
json(res, 403, { error: "teams_not_over_public_link" });
|
|
1226
|
+
return true;
|
|
1227
|
+
}
|
|
1228
|
+
const name = decodeURIComponent(teamEditMatch[1]);
|
|
1229
|
+
// Who may write, asked again once the body is in (#1693 review): a session revoked or a token rotated meanwhile
|
|
1230
|
+
// writes nothing.
|
|
1231
|
+
const writerNow = () => {
|
|
1232
|
+
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
1233
|
+
return verdict.kind === "allow" && !(verdict.via === "session" && verdict.session?.tier !== "admin");
|
|
1234
|
+
};
|
|
1235
|
+
(async () => {
|
|
1236
|
+
try {
|
|
1237
|
+
const body = await parseBody(req);
|
|
1238
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
1239
|
+
return;
|
|
1240
|
+
if (!writerNow()) {
|
|
1241
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
const parsed = parseOrReject(TeamEditSchema, body, res);
|
|
1245
|
+
if (!parsed)
|
|
1246
|
+
return;
|
|
1247
|
+
const teams = ctx.fleetConfig?.teams;
|
|
1248
|
+
const current = teams && Object.hasOwn(teams, name) ? teams[name].members : null;
|
|
1249
|
+
if (!current) {
|
|
1250
|
+
json(res, 404, { error: `Team not found: ${name}` });
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const want = [...new Set(parsed.members)];
|
|
1254
|
+
// The update and its save as one: a failed save leaves the team as it was (500).
|
|
1255
|
+
const r = commitTeamUpdate(ctx.fleetConfig, {
|
|
1256
|
+
name, add: want.filter(m => !current.includes(m)), remove: current.filter(m => !want.includes(m)), description: parsed.description,
|
|
1257
|
+
}, () => ctx.saveFleetConfig());
|
|
1258
|
+
if (!r.ok) {
|
|
1259
|
+
json(res, r.status, { error: r.error });
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
json(res, 200, { updated: r.result });
|
|
916
1263
|
}
|
|
917
1264
|
catch (err) {
|
|
918
1265
|
json(res, 400, { error: err.message });
|
|
@@ -922,8 +1269,13 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
922
1269
|
}
|
|
923
1270
|
const teamDelMatch = path.match(/^\/ui\/teams\/(.+)$/);
|
|
924
1271
|
if (method === "DELETE" && teamDelMatch) {
|
|
1272
|
+
if (gatewayRequestContext(req)) {
|
|
1273
|
+
json(res, 403, { error: "teams_not_over_public_link" });
|
|
1274
|
+
return true;
|
|
1275
|
+
}
|
|
925
1276
|
const name = decodeURIComponent(teamDelMatch[1]);
|
|
926
|
-
|
|
1277
|
+
// Own keys (#1689): a team is one fleet.yaml defines — "__proto__" or "constructor" is not found.
|
|
1278
|
+
if (!ctx.fleetConfig?.teams || !Object.hasOwn(ctx.fleetConfig.teams, name)) {
|
|
927
1279
|
json(res, 404, { error: `Team not found: ${name}` });
|
|
928
1280
|
return true;
|
|
929
1281
|
}
|
|
@@ -1092,7 +1444,10 @@ function handleSendMessage(req, res, ctx) {
|
|
|
1092
1444
|
}) : undefined;
|
|
1093
1445
|
let delivered;
|
|
1094
1446
|
try {
|
|
1095
|
-
|
|
1447
|
+
const deliver = (name, payload) => ctx.deliverHumanToInstance
|
|
1448
|
+
? ctx.deliverHumanToInstance(name, payload, webHumanOrigin(), typed)
|
|
1449
|
+
: ctx.deliverToInstance(name, payload);
|
|
1450
|
+
delivered = await deliver(instance, {
|
|
1096
1451
|
type: "fleet_inbound",
|
|
1097
1452
|
content: delivery.text,
|
|
1098
1453
|
targetSession: instance,
|
|
@@ -1129,6 +1484,13 @@ function handleSendMessage(req, res, ctx) {
|
|
|
1129
1484
|
}
|
|
1130
1485
|
settleEcho?.(true);
|
|
1131
1486
|
settleClassicEcho?.(true);
|
|
1487
|
+
// #1666: the same cancel/progress bubble a platform inbound starts, in the topic the echo went to — a second turn
|
|
1488
|
+
// on the echo's lane, so the topic reads "echo, then bubble" and a cancel in between drops it (the lane's epoch).
|
|
1489
|
+
// Not a copy (#1668): a slow bubble never drops the next message's echo, and a slow echo never drops the bubble.
|
|
1490
|
+
if (settleEcho && ctx.startWebChatProgress) {
|
|
1491
|
+
const startProgress = ctx.startWebChatProgress.bind(ctx);
|
|
1492
|
+
ctx.reserveWebChannelEcho?.(instance, () => startProgress(instance), { copy: false })(true);
|
|
1493
|
+
}
|
|
1132
1494
|
ctx.webFiles?.commit(files);
|
|
1133
1495
|
ctx.lastInboundUser.set(instance, "web-user");
|
|
1134
1496
|
ctx.eventLog?.logActivity("message", "web-user", (message || `[${files.length} file(s)]`).slice(0, 200), instance);
|
|
@@ -1209,8 +1571,12 @@ function handleUpload(req, res, url, ctx) {
|
|
|
1209
1571
|
});
|
|
1210
1572
|
}
|
|
1211
1573
|
function shellPreview(req, ctx, mode) {
|
|
1212
|
-
if (mode !== "full"
|
|
1574
|
+
if (mode !== "full")
|
|
1213
1575
|
return null;
|
|
1576
|
+
// #1723: a page of the public link asks its own exposure's preview — never the local listener's Host-based choice.
|
|
1577
|
+
const gateway = gatewayRequestContext(req);
|
|
1578
|
+
if (gateway)
|
|
1579
|
+
return ctx.publicPreviewForUi?.(gateway.exposureId, gateway.expectedOrigin) ?? null;
|
|
1214
1580
|
return ctx.previewForUi?.(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req)) ?? null;
|
|
1215
1581
|
}
|
|
1216
1582
|
/**
|
|
@@ -1259,6 +1625,21 @@ function serveMediaRange(req, res, files, id, file) {
|
|
|
1259
1625
|
res.writeHead(ranged ? 206 : 200);
|
|
1260
1626
|
res.end(got.bytes);
|
|
1261
1627
|
}
|
|
1628
|
+
/**
|
|
1629
|
+
* #1665: `<html>` carrying the host's preferences (data-prefs: `{"rev":n,"values":{…}}`, attribute-escaped). theme.js (a
|
|
1630
|
+
* blocking script in <head>) and the app's dictionary take the theme and language from it over the browser's cache,
|
|
1631
|
+
* and app-prefs.js copies every value into that cache before any panel reads it — so no device paints a stale theme
|
|
1632
|
+
* or replays a tour already seen, a new device included. Only for the signed-in app (the public link reads what is in
|
|
1633
|
+
* force too); the anonymous View reader and the sign-in page keep the browser's own.
|
|
1634
|
+
*/
|
|
1635
|
+
function shellHtmlTag(ctx, mode) {
|
|
1636
|
+
if (mode !== "full" || !ctx.webPrefs)
|
|
1637
|
+
return '<html lang="en">';
|
|
1638
|
+
const view = ctx.webPrefs.read(OPERATOR);
|
|
1639
|
+
const lang = view.values.lang === "zh-TW" ? "zh-Hant" : "en";
|
|
1640
|
+
const attr = (v) => v.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
|
1641
|
+
return `<html lang="${lang}" data-prefs="${attr(JSON.stringify({ rev: view.rev, values: view.values }))}">`;
|
|
1642
|
+
}
|
|
1262
1643
|
function shellBodyTag(req, ctx, mode) {
|
|
1263
1644
|
const p = shellPreview(req, ctx, mode);
|
|
1264
1645
|
const attr = (v) => String(v ?? "").replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
|
@@ -1266,8 +1647,14 @@ function shellBodyTag(req, ctx, mode) {
|
|
|
1266
1647
|
+ ` data-preview-boot="${attr(p?.previewOrigin ? p.boot : "")}" data-preview-reason="${attr(p ? p.reason : "Previews are not available on this fleet.")}"`
|
|
1267
1648
|
// #1554: no availability asked (the public link, or not the full app) still says why, in the page's language.
|
|
1268
1649
|
+ ` data-preview-reason-code="${attr(p ? p.code ?? "" : gatewayRequestContext(req) ? "publicLink" : "notOffered")}"`
|
|
1650
|
+
// #1723: why the public link's preview tunnel failed (a fixed reason the page words), when it did.
|
|
1651
|
+
+ `${p?.detail ? ` data-preview-detail="${attr(p.detail)}"` : ""}`
|
|
1269
1652
|
// #1523 N3: whether the fleet serves /api/ai-usage (web.usage_panel), for every page's ◔.
|
|
1270
1653
|
+ ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false ? "0" : "1"}"`
|
|
1654
|
+
// #1218: whether the Log tab is on (web.log_tab; off by default): the tab says so and offers to turn it on.
|
|
1655
|
+
+ ` data-log-tab="${ctx.fleetConfig?.web?.log_tab === true ? "1" : "0"}"`
|
|
1656
|
+
// #1770: the fleet's zone (chat-logs' and fleet.log's), in which the chat reads its message times and days.
|
|
1657
|
+
+ ` data-fleet-tz="${attr(resolveTz())}"`
|
|
1271
1658
|
// #1589: the public link shows attachments as downloads only (no inline text or media): its scope is unchanged.
|
|
1272
1659
|
+ `${gatewayRequestContext(req) ? ' data-public-link="1"' : ""}>`;
|
|
1273
1660
|
}
|
|
@@ -1282,7 +1669,10 @@ export function serveAppShell(req, res, ctx, mode) {
|
|
|
1282
1669
|
const own = mode === "full" && !gatewayRequestContext(req)
|
|
1283
1670
|
? dashboardOriginFor(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req)) : null;
|
|
1284
1671
|
const mediaSrc = own ? `${own}/ui/file/` : "'none'";
|
|
1285
|
-
|
|
1672
|
+
// A public-link page frames what its exposure's preview names (the quick-tunnel wildcard: its host is not known when
|
|
1673
|
+
// the page may load); every other page frames exactly the origin this load chose (#1723).
|
|
1674
|
+
const frameSrc = p && "frameSrc" in p ? p.frameSrc : p?.previewOrigin ? `${p.previewOrigin}/frame` : null;
|
|
1675
|
+
sendPanelHtml(res, html.replace('<html lang="en">', shellHtmlTag(ctx, mode)).replace("<body>", shellBodyTag(req, ctx, mode)), 200, {}, { ...(frameSrc ? { frameSrc } : {}), mediaSrc });
|
|
1286
1676
|
}
|
|
1287
1677
|
catch {
|
|
1288
1678
|
json(res, 500, { error: "app.html not found" });
|