@songsid/agend 2.3.3-beta.1 → 2.3.3-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/dist/agent-cli.js +13 -112
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +0 -1
- package/dist/agent-endpoint.js +1 -9
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +3 -6
- package/dist/agent-whats-new.js +7 -18
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +0 -7
- package/dist/auth-api.js +1 -15
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/authority-state.js +1 -1
- package/dist/authority/authority-state.js.map +1 -1
- package/dist/authority/web-authority.d.ts +9 -4
- package/dist/authority/web-authority.js +10 -5
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/antigravity.js +6 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +7 -0
- package/dist/backend/claude-code.js +34 -28
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/claude-transcript-errors.d.ts +18 -0
- package/dist/backend/claude-transcript-errors.js +19 -0
- package/dist/backend/claude-transcript-errors.js.map +1 -0
- package/dist/backend/codex-session-lookup.d.ts +16 -3
- package/dist/backend/codex-session-lookup.js +45 -11
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.d.ts +16 -0
- package/dist/backend/codex.js +184 -35
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +58 -12
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.js +2 -0
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +2 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +46 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/bundled-skills.d.ts +4 -6
- package/dist/bundled-skills.js +10 -14
- package/dist/bundled-skills.js.map +1 -1
- package/dist/channel/adapters/discord.js +60 -17
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-server.js +5 -5
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +1 -3
- package/dist/channel/mcp-tools.js +4 -58
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +4 -1
- package/dist/classic-channel-manager.js +21 -7
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +21 -4
- package/dist/cli.js.map +1 -1
- package/dist/config-history.d.ts +50 -0
- package/dist/config-history.js +263 -0
- package/dist/config-history.js.map +1 -0
- package/dist/config-validator.js +7 -7
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +11 -4
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +73 -3
- package/dist/daemon.js +334 -121
- package/dist/daemon.js.map +1 -1
- package/dist/daily-summary.d.ts +8 -1
- package/dist/daily-summary.js +8 -5
- package/dist/daily-summary.js.map +1 -1
- package/dist/dashboard-live.d.ts +1 -1
- package/dist/dashboard-live.js +3 -3
- package/dist/dashboard-live.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +1 -1
- package/dist/dashboard-permissions.js +1 -3
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/delivery-outbox.d.ts +4 -0
- package/dist/delivery-outbox.js +12 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/doctor.d.ts +4 -0
- package/dist/doctor.js +16 -1
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-level-config.js +1 -1
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +70 -54
- package/dist/fleet-manager.js +257 -322
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.d.ts +5 -0
- package/dist/fleet-process-handlers.js +21 -0
- package/dist/fleet-process-handlers.js.map +1 -1
- package/dist/fleet-service-owner.d.ts +11 -0
- package/dist/fleet-service-owner.js +3 -0
- package/dist/fleet-service-owner.js.map +1 -0
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +11 -1
- package/dist/general-knowledge/skills/session-management/SKILL.md +6 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +13 -204
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +29 -67
- package/dist/instance-lifecycle.d.ts +19 -0
- package/dist/instance-lifecycle.js +52 -10
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +92 -42
- package/dist/locale.js.map +1 -1
- package/dist/needs-you-hub.d.ts +1 -0
- package/dist/needs-you-hub.js +6 -3
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.d.ts +3 -1
- package/dist/needs-you-live.js +24 -2
- package/dist/needs-you-live.js.map +1 -1
- package/dist/needs-you.d.ts +13 -2
- package/dist/needs-you.js +11 -1
- package/dist/needs-you.js.map +1 -1
- package/dist/notify-socket.d.ts +10 -0
- package/dist/notify-socket.js +45 -0
- package/dist/notify-socket.js.map +1 -0
- package/dist/outbound-handlers.js +19 -7
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +0 -62
- package/dist/outbound-schemas.js +3 -42
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/parked-notices.d.ts +87 -0
- package/dist/parked-notices.js +194 -0
- package/dist/parked-notices.js.map +1 -0
- package/dist/public-link-progress.d.ts +5 -2
- package/dist/public-link-progress.js +4 -3
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-web-gateway.js +0 -9
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +12 -0
- package/dist/public-web-link.js +29 -5
- package/dist/public-web-link.js.map +1 -1
- package/dist/quota-notice.d.ts +23 -0
- package/dist/quota-notice.js +22 -0
- package/dist/quota-notice.js.map +1 -0
- package/dist/scheduler/task-board.d.ts +0 -13
- package/dist/scheduler/task-board.js +0 -14
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/sd-notify.d.ts +10 -1
- package/dist/sd-notify.js +27 -2
- package/dist/sd-notify.js.map +1 -1
- package/dist/search/search-docs.d.ts +1 -1
- package/dist/search/search-docs.js +2 -2
- package/dist/search/search-docs.js.map +1 -1
- package/dist/settings-api.d.ts +16 -0
- package/dist/settings-api.js +55 -2
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +3 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +17 -3
- package/dist/settings-confirmation.js +8 -5
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-http-confirmation.js +31 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/setup-wizard.d.ts +3 -0
- package/dist/setup-wizard.js +4 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/task-board-waker.d.ts +4 -1
- package/dist/task-board-waker.js +13 -2
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tool-permissions.js +3 -10
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +27 -7
- package/dist/topic-commands.js +95 -36
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.d.ts +7 -0
- package/dist/transcript-monitor.js +48 -3
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +5 -0
- package/dist/tunnel/cloudflared.js +32 -1
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/named-diagnostics.d.ts +2 -1
- package/dist/tunnel/named-diagnostics.js +4 -1
- package/dist/tunnel/named-diagnostics.js.map +1 -1
- package/dist/tunnel/named-link-test.d.ts +90 -0
- package/dist/tunnel/named-link-test.js +270 -0
- package/dist/tunnel/named-link-test.js.map +1 -0
- package/dist/tunnel/purpose-lane.d.ts +3 -1
- package/dist/tunnel/purpose-lane.js +2 -0
- package/dist/tunnel/purpose-lane.js.map +1 -1
- package/dist/types.d.ts +8 -6
- package/dist/ui/chat-ambient.js +4 -4
- package/dist/ui/chat-commands.js +3 -2
- package/dist/ui/chat-store.js +4 -1
- package/dist/ui/chat-strings.js +12 -46
- package/dist/ui/chat-thread.js +51 -59
- package/dist/ui/fleet-office-skin.js +37 -14
- package/dist/ui/fleet-office.js +31 -7
- package/dist/ui/html-attachment.js +22 -8
- package/dist/ui/panel-chat.js +21 -50
- package/dist/ui/panel-needs.js +11 -9
- package/dist/ui/panel-settings.js +5 -3
- package/dist/ui/preview-panel.js +12 -21
- package/dist/ui/preview.js +10 -3
- package/dist/ui/search-dialog.js +30 -14
- package/dist/ui/settings-appearance.js +1 -3
- package/dist/ui/settings-confirm.js +18 -0
- package/dist/ui/settings-guests.js +13 -4
- package/dist/ui/settings-learn.js +1 -1
- package/dist/ui/settings-mods.js +5 -12
- package/dist/ui/settings-named-test.js +144 -0
- package/dist/ui/settings-registry.js +0 -1
- package/dist/ui/settings-strings.js +14 -14
- package/dist/ui/settings-web-authority.js +4 -3
- package/dist/ui/shared/app-i18n.js +15 -4
- package/dist/ui/shared/app-needs.js +5 -1
- package/dist/ui/shared/app-prefs.js +17 -17
- package/dist/ui/shared/app-shell.js +47 -9
- package/dist/ui/shared/app-store.js +6 -2
- package/dist/ui/shared/app.css +33 -39
- package/dist/ui/shared/app.js +5 -4
- package/dist/ui/shared/instance-nav.js +9 -9
- package/dist/ui/shared/instance-switch.js +8 -2
- package/dist/ui/shared/panel-view.js +22 -8
- package/dist/ui/shared/view-strings.js +6 -0
- package/dist/ui/shared/web-authorize.js +35 -13
- package/dist/usage/usage-api.d.ts +3 -0
- package/dist/usage/usage-api.js +5 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.d.ts +2 -0
- package/dist/view-api.js +64 -0
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +9 -17
- package/dist/web-api.js +257 -371
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +0 -2
- package/dist/web-appearance-tools.js +2 -5
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +0 -6
- package/dist/web-appearance-view.js +0 -1
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +14 -7
- package/dist/web-appearance.js +5 -6
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-asset-parts.d.ts +1 -17
- package/dist/web-asset-parts.js +1 -15
- package/dist/web-asset-parts.js.map +1 -1
- package/dist/web-auth.js +1 -7
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +3 -7
- package/dist/web-chat-history.js +10 -17
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +9 -1
- package/dist/web-commands.js +13 -0
- package/dist/web-commands.js.map +1 -1
- package/dist/web-experimental.d.ts +2 -11
- package/dist/web-experimental.js +8 -36
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-guest-access.d.ts +40 -2
- package/dist/web-guest-access.js +50 -14
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-mod-bundle.d.ts +2 -45
- package/dist/web-mod-bundle.js +15 -63
- package/dist/web-mod-bundle.js.map +1 -1
- package/dist/web-mod-store.d.ts +3 -48
- package/dist/web-mod-store.js +30 -97
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-prefs.d.ts +13 -0
- package/dist/web-prefs.js +38 -0
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +6 -13
- package/dist/web-preview.js +12 -23
- package/dist/web-preview.js.map +1 -1
- package/dist/web-session.d.ts +11 -3
- package/dist/web-session.js +22 -11
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +1 -0
- package/dist/web-settings-patch.js +6 -3
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +0 -13
- package/dist/web-settings-tools.js +12 -84
- package/dist/web-settings-tools.js.map +1 -1
- package/launcher/agend-install.cjs +7 -1
- package/launcher/fleet-service-owner.cjs +150 -0
- package/package.json +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +0 -376
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +0 -673
- package/dist/task-board-summary.d.ts +0 -101
- package/dist/task-board-summary.js +0 -177
- package/dist/task-board-summary.js.map +0 -1
- package/dist/ui/app-effects.js +0 -251
- package/dist/ui/chat-effect-posters.js +0 -42
- package/dist/ui/chat-effects.js +0 -167
- package/dist/ui/effect-trial-controller.js +0 -124
- package/dist/ui/effect-trial-offer.js +0 -2
- package/dist/ui/effect-trial-panel.js +0 -40
- package/dist/ui/effects-waker.js +0 -2
- package/dist/ui/settings-effects.js +0 -202
- package/dist/ui/settings-owner-effects.js +0 -82
- package/dist/ui/shared/effect-app-runtime.js +0 -299
- package/dist/ui/shared/effect-baseline.js +0 -64
- package/dist/ui/shared/effect-chat-runtime.js +0 -162
- package/dist/ui/shared/effect-cursor.js +0 -126
- package/dist/ui/shared/effect-display-metadata.js +0 -39
- package/dist/ui/shared/effect-frame.js +0 -146
- package/dist/ui/shared/effect-geometry.js +0 -137
- package/dist/ui/shared/effect-host-style.js +0 -10
- package/dist/ui/shared/effect-owner-regions.js +0 -74
- package/dist/ui/shared/effect-owner-source.js +0 -122
- package/dist/ui/shared/effect-policy.js +0 -151
- package/dist/ui/shared/effect-poster-reader.js +0 -98
- package/dist/ui/shared/effect-posters.js +0 -117
- package/dist/ui/shared/effect-profiler.js +0 -184
- package/dist/ui/shared/effect-profiles.js +0 -65
- package/dist/ui/shared/effect-registry.js +0 -545
- package/dist/ui/shared/effect-runtime-loader.js +0 -236
- package/dist/ui/shared/effect-safety.js +0 -153
- package/dist/ui/shared/effect-stock-snapshot.js +0 -43
- package/dist/ui/shared/effect-stock-trust.js +0 -217
- package/dist/ui/shared/effect-trial-shadow.js +0 -62
- package/dist/ui/shared/effect-trial-source.js +0 -103
- package/dist/ui/shared/effect-trial-stage.js +0 -170
- package/dist/ui/shared/effect-trial.css +0 -40
- package/dist/ui/shared/feature-flags.js +0 -127
- package/dist/ui/shared/owner-effect-prefs.js +0 -117
- package/dist/web-effect-bootstrap.d.ts +0 -12
- package/dist/web-effect-bootstrap.js +0 -49
- package/dist/web-effect-bootstrap.js.map +0 -1
- package/dist/web-effect-bundle.d.ts +0 -28
- package/dist/web-effect-bundle.js +0 -171
- package/dist/web-effect-bundle.js.map +0 -1
- package/dist/web-effect-canonical.d.ts +0 -8
- package/dist/web-effect-canonical.js +0 -27
- package/dist/web-effect-canonical.js.map +0 -1
- package/dist/web-effect-library.d.ts +0 -114
- package/dist/web-effect-library.js +0 -530
- package/dist/web-effect-library.js.map +0 -1
- package/dist/web-effect-playback.d.ts +0 -30
- package/dist/web-effect-playback.js +0 -71
- package/dist/web-effect-playback.js.map +0 -1
- package/dist/web-effect-poster.d.ts +0 -8
- package/dist/web-effect-poster.js +0 -52
- package/dist/web-effect-poster.js.map +0 -1
- package/dist/web-effect-profiles.d.ts +0 -118
- package/dist/web-effect-profiles.js +0 -181
- package/dist/web-effect-profiles.js.map +0 -1
- package/dist/web-effect-service.d.ts +0 -93
- package/dist/web-effect-service.js +0 -335
- package/dist/web-effect-service.js.map +0 -1
- package/dist/web-effect-tools.d.ts +0 -48
- package/dist/web-effect-tools.js +0 -129
- package/dist/web-effect-tools.js.map +0 -1
- package/dist/web-effect-trials.d.ts +0 -86
- package/dist/web-effect-trials.js +0 -204
- package/dist/web-effect-trials.js.map +0 -1
- package/dist/web-effect-upload.d.ts +0 -27
- package/dist/web-effect-upload.js +0 -185
- package/dist/web-effect-upload.js.map +0 -1
- package/dist/web-effect-worker.d.ts +0 -8
- package/dist/web-effect-worker.js +0 -170
- package/dist/web-effect-worker.js.map +0 -1
- package/dist/web-effect.d.ts +0 -103
- package/dist/web-effect.js +0 -153
- package/dist/web-effect.js.map +0 -1
- package/dist/web-experimental-http.d.ts +0 -2
- package/dist/web-experimental-http.js +0 -12
- package/dist/web-experimental-http.js.map +0 -1
- package/dist/web-mod-agent.d.ts +0 -50
- package/dist/web-mod-agent.js +0 -207
- package/dist/web-mod-agent.js.map +0 -1
package/dist/web-api.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isCanvasWorkerHttpPath } from "./web-experimental-http.js";
|
|
1
|
+
import { CANVAS_WORKER_EXPERIMENTAL_MESSAGE } from "./web-experimental.js";
|
|
3
2
|
import { webHumanOrigin } from "./human-delivery-receipts.js";
|
|
4
3
|
import { classifyWebPeer, confirmAfterBody, noAuthority } from "./authority/web-authority.js";
|
|
5
|
-
import { guestChatRefusal, guestCommandText, guestDeliveries, guestFrameData, guestHistory, guestMessages, guestPollBody, guestPrefs, guestRequestOf, guestSeesMessage, guestStatus } from "./web-guest-access.js";
|
|
4
|
+
import { guestChatRefusal, guestCommandText, guestDeliveries, GUEST_PREF_MAX_BYTES, GUEST_SEARCHES, guestFrameData, guestHistory, guestMessages, guestOwnPrefs, guestPollBody, guestPrefs, guestRequestOf, guestSeesMessage, guestStatus } from "./web-guest-access.js";
|
|
6
5
|
import { withSpawnHoldReply } from "./spawn-hold-reply.js";
|
|
7
6
|
import { binaryProbe } from "./binary-probe.js";
|
|
8
7
|
import { settingsRequestExecution, settingsWrite, isSettingsReplay } from "./settings-request-capability.js";
|
|
@@ -14,7 +13,6 @@ import { buildModBundle } from "./web-mod-export.js";
|
|
|
14
13
|
import { AGENT_WIDGET_SLOTS, checkWidgetData, checkWidgetSpec } from "./web-widget.js";
|
|
15
14
|
import { checkOwnerCss, ownerCssAssetRefs } from "./web-owner-css.js";
|
|
16
15
|
import { canonicalText } from "./web-theme.js";
|
|
17
|
-
import { EFFECT_WIRE_LIMIT } from "./web-effect-upload.js";
|
|
18
16
|
import { SKIN_WIRE_BYTES } from "./web-office-skins.js";
|
|
19
17
|
import { gatewayRequestContext } from "./web-request-context.js";
|
|
20
18
|
import { crossInstanceVisibility } from "./cross-instance-notice.js";
|
|
@@ -51,7 +49,6 @@ import { parseMentionQuery } from "./web-mentions.js";
|
|
|
51
49
|
import { authorizeExplicitInstanceRemoval } from "./instance-removal.js";
|
|
52
50
|
import { measureSyncWork } from "./sync-work-attribution.js";
|
|
53
51
|
import { resolveTz } from "./tz-utils.js";
|
|
54
|
-
import { filesPlayInChat } from "./web-effect.js";
|
|
55
52
|
import { QUERY_MAX_CHARS, RESULT_MAX, SEARCH_SOURCES } from "./search/search-store.js";
|
|
56
53
|
import { xmsgShown } from "./search/search-docs.js";
|
|
57
54
|
import { WebSendDedupe } from "./web-send-dedupe.js";
|
|
@@ -233,6 +230,8 @@ function visibleNeeds(ctx, gateway, items) {
|
|
|
233
230
|
const { hidden } = boardHiding(ctx, true);
|
|
234
231
|
return items.filter((i) => {
|
|
235
232
|
const item = i;
|
|
233
|
+
if (item.type === "config")
|
|
234
|
+
return false; // private operator configuration diagnostics never cross a public link
|
|
236
235
|
if (item.type !== "task")
|
|
237
236
|
return true;
|
|
238
237
|
const code = item.card?.code;
|
|
@@ -317,6 +316,8 @@ export const SSE_HEARTBEAT_MS = 10_000;
|
|
|
317
316
|
async function parseBody(req) {
|
|
318
317
|
return JSON.parse((await readBoundedWebBody(req, 512 * 1024)).toString("utf8"));
|
|
319
318
|
}
|
|
319
|
+
/** #2855: the guest accounts with a command in progress (one at a time each). */
|
|
320
|
+
const guestCommandsRunning = new Set();
|
|
320
321
|
/** Send JSON response. */
|
|
321
322
|
function json(res, status, data) {
|
|
322
323
|
jsonText(res, status, JSON.stringify(data));
|
|
@@ -941,159 +942,6 @@ function handleOwnerCss(req, res, url, ctx, path, method) {
|
|
|
941
942
|
})().catch(fail);
|
|
942
943
|
return true;
|
|
943
944
|
}
|
|
944
|
-
/** #1629: static current-placement posters, separate from the local human-only runtime API. */
|
|
945
|
-
function handleEffectPosters(res, url, ctx, path, method) {
|
|
946
|
-
res.setHeader("Cache-Control", "private, no-store");
|
|
947
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
948
|
-
const image = /^\/ui\/effect-posters\/([0-9a-f]{32})\.png$/.exec(path);
|
|
949
|
-
const names = [...url.searchParams.keys()], instance = url.searchParams.get("instance");
|
|
950
|
-
if (method !== "GET" || path !== "/ui/effect-posters" && !image || names.length !== 1 || names[0] !== "instance" || !instance || instance.length > 128) {
|
|
951
|
-
json(res, 404, { error: "not_found" });
|
|
952
|
-
return true;
|
|
953
|
-
}
|
|
954
|
-
const service = ctx.effectService;
|
|
955
|
-
if (!service) {
|
|
956
|
-
json(res, 404, { error: "effects_unavailable" });
|
|
957
|
-
return true;
|
|
958
|
-
}
|
|
959
|
-
if (image) {
|
|
960
|
-
const r = service.placedPoster(image[1], instance);
|
|
961
|
-
if (!r.ok)
|
|
962
|
-
json(res, 404, { error: "not_found" });
|
|
963
|
-
else {
|
|
964
|
-
res.writeHead(200, { "Content-Type": "image/png", "Content-Security-Policy": "default-src 'none'; sandbox" });
|
|
965
|
-
res.end(r.bytes);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
else {
|
|
969
|
-
const result = service.posterPlacements(instance);
|
|
970
|
-
json(res, result.error === "themes_unreadable" ? 503 : result.error ? 404 : 200, result);
|
|
971
|
-
}
|
|
972
|
-
return true;
|
|
973
|
-
}
|
|
974
|
-
/** #1629 Try: separate local-admin routes. These never borrow ordinary read-tier/placement authority. */
|
|
975
|
-
function handleEffectTrials(req, res, url, ctx, path, method) {
|
|
976
|
-
res.setHeader("Cache-Control", "private, no-store");
|
|
977
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
978
|
-
const reading = method === "GET";
|
|
979
|
-
if (gatewayRequestContext(req)) {
|
|
980
|
-
json(res, reading ? 404 : 403, { error: "trials_not_over_public_link" });
|
|
981
|
-
return true;
|
|
982
|
-
}
|
|
983
|
-
const who = () => {
|
|
984
|
-
const sessions = ctx.webSessions;
|
|
985
|
-
const v = evaluateWebRequest(req, url, ctx.webToken ?? null, sessions, { touch: false });
|
|
986
|
-
if (v.kind !== "allow")
|
|
987
|
-
return { error: WEB_TOKEN_INVALID_MESSAGE, status: 401 };
|
|
988
|
-
if (v.via !== "session" || v.session.tier !== "admin" || v.session.surface !== "local")
|
|
989
|
-
return { error: "trials_need_a_local_admin", status: 403 };
|
|
990
|
-
const person = webPersonKey(v.session.owner);
|
|
991
|
-
let css = "unknown";
|
|
992
|
-
try {
|
|
993
|
-
const value = ctx.ownerCss?.effectState(person);
|
|
994
|
-
if (value === "off" || value === "on")
|
|
995
|
-
css = value;
|
|
996
|
-
}
|
|
997
|
-
catch { /* unavailable is never off */ }
|
|
998
|
-
// CSS is diagnostic for Try; its host-owned shadow shell isolates controls and local paint.
|
|
999
|
-
const actor = Object.freeze({ via: "session", surface: "local", tier: "admin", sessionHash: v.session.idHash, epoch: v.session.handle, person, ownerCss: css });
|
|
1000
|
-
return { actor, record: v.session, sessions };
|
|
1001
|
-
};
|
|
1002
|
-
const first = who();
|
|
1003
|
-
if ("error" in first) {
|
|
1004
|
-
json(res, first.status, { error: first.error });
|
|
1005
|
-
return true;
|
|
1006
|
-
}
|
|
1007
|
-
const service = ctx.effectService;
|
|
1008
|
-
if (!service) {
|
|
1009
|
-
json(res, 404, { error: "trials_unavailable" });
|
|
1010
|
-
return true;
|
|
1011
|
-
}
|
|
1012
|
-
const start = path === "/ui/effect-trials/start";
|
|
1013
|
-
const route = /^\/ui\/effect-trials\/([0-9a-f]{32})\/(resource|poster\.png|profile|stop)$/.exec(path);
|
|
1014
|
-
const resource = reading && route && ["resource", "poster.png", "profile"].includes(route[2]);
|
|
1015
|
-
const stop = method === "POST" && route?.[2] === "stop";
|
|
1016
|
-
if (!(resource || stop || method === "POST" && start)) {
|
|
1017
|
-
json(res, 404, { error: "not_found" });
|
|
1018
|
-
return true;
|
|
1019
|
-
}
|
|
1020
|
-
const allowed = resource ? ["ownerEpoch", "instance"] : ["ownerEpoch"];
|
|
1021
|
-
if ([...url.searchParams.keys()].some(k => !allowed.includes(k)) || allowed.some(k => url.searchParams.getAll(k).length !== 1)
|
|
1022
|
-
|| url.searchParams.get("ownerEpoch") !== first.actor.epoch) {
|
|
1023
|
-
json(res, reading ? 404 : 409, { error: reading ? "not_found" : "trials_person_changed" });
|
|
1024
|
-
return true;
|
|
1025
|
-
}
|
|
1026
|
-
const instance = url.searchParams.get("instance") ?? "";
|
|
1027
|
-
const answer = (result) => json(res, result.error ? result.error === "not_found" ? 404
|
|
1028
|
-
: ["profiles_unreadable", "profiles_busy", "profiles_write_failed"].includes(String(result.error)) ? 503 : 400 : 200, result);
|
|
1029
|
-
if (resource) {
|
|
1030
|
-
if (route[2] === "poster.png") {
|
|
1031
|
-
const result = service.trialPoster(route[1], instance, first.actor);
|
|
1032
|
-
if (!result.ok)
|
|
1033
|
-
json(res, 404, { error: "not_found" });
|
|
1034
|
-
else {
|
|
1035
|
-
res.writeHead(200, { "Content-Type": "image/png", "Content-Security-Policy": "default-src 'none'; sandbox" });
|
|
1036
|
-
res.end(result.bytes);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
else
|
|
1040
|
-
answer(route[2] === "profile" ? service.trialProfile(route[1], instance, first.actor) : service.trialResource(route[1], instance, first.actor));
|
|
1041
|
-
return true;
|
|
1042
|
-
}
|
|
1043
|
-
(async () => {
|
|
1044
|
-
let bytes;
|
|
1045
|
-
try {
|
|
1046
|
-
bytes = await readBoundedWebBody(req, 16 * 1024);
|
|
1047
|
-
}
|
|
1048
|
-
catch {
|
|
1049
|
-
json(res, 413, { error: "trial_body_bound" });
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
if (!permitWebContinuation(req, res, ctx))
|
|
1053
|
-
return;
|
|
1054
|
-
const now = who();
|
|
1055
|
-
if ("error" in now) {
|
|
1056
|
-
json(res, now.status, { error: now.error });
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
if (!canvasWorkerEnabled(ctx.fleetConfig?.web)) {
|
|
1060
|
-
json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
if (now.record !== first.record || now.sessions !== first.sessions || now.actor.sessionHash !== first.actor.sessionHash
|
|
1064
|
-
|| now.actor.person !== first.actor.person || now.actor.epoch !== first.actor.epoch) {
|
|
1065
|
-
json(res, 409, { error: "trials_person_changed" });
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
let body;
|
|
1069
|
-
try {
|
|
1070
|
-
body = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
1071
|
-
}
|
|
1072
|
-
catch {
|
|
1073
|
-
json(res, 400, { error: "trial_json" });
|
|
1074
|
-
return;
|
|
1075
|
-
}
|
|
1076
|
-
if (start) {
|
|
1077
|
-
const parsed = z.strictObject({ offer: z.string().regex(/^[0-9a-f]{32}$/), instance: z.string().min(1).max(128) }).safeParse(body);
|
|
1078
|
-
if (!parsed.success) {
|
|
1079
|
-
json(res, 400, { error: "trial_args" });
|
|
1080
|
-
return;
|
|
1081
|
-
}
|
|
1082
|
-
answer(service.trialStart(parsed.data.offer, parsed.data.instance, now.actor));
|
|
1083
|
-
}
|
|
1084
|
-
else {
|
|
1085
|
-
if (!z.strictObject({}).safeParse(body).success) {
|
|
1086
|
-
json(res, 400, { error: "trial_args" });
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
answer(service.trialStop(route[1], now.actor));
|
|
1090
|
-
}
|
|
1091
|
-
})().catch(() => { try {
|
|
1092
|
-
json(res, 500, { error: "trial_request_failed" });
|
|
1093
|
-
}
|
|
1094
|
-
catch { /* response ended */ } });
|
|
1095
|
-
return true;
|
|
1096
|
-
}
|
|
1097
945
|
/** #2599: the same closed browser controls as setup, for a current local human admin. */
|
|
1098
946
|
function handleBrowserBackendSignIns(req, res, url, ctx) {
|
|
1099
947
|
res.setHeader("Cache-Control", "private, no-store");
|
|
@@ -1289,148 +1137,6 @@ function handleBackendLogin(req, res, url, ctx, path, method) {
|
|
|
1289
1137
|
catch { /* response ended */ } });
|
|
1290
1138
|
return true;
|
|
1291
1139
|
}
|
|
1292
|
-
/** #1629 B2: effect routes require a human session; route bodies never supply an actor or reference holder. */
|
|
1293
|
-
function handleEffects(req, res, url, ctx, path, method) {
|
|
1294
|
-
res.setHeader("Cache-Control", "private, no-store");
|
|
1295
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
1296
|
-
const reading = method === "GET";
|
|
1297
|
-
// Reports are bounded untrusted diagnostics, not an appearance or admission write (r2 §6).
|
|
1298
|
-
const profilePost = method === "POST" && /^\/ui\/effects\/profile\/[0-9a-f]{32}$/.test(path);
|
|
1299
|
-
const gateway = gatewayRequestContext(req), guest = guestRequestOf(req);
|
|
1300
|
-
const instanceQuery = url.searchParams.get("instance"), keys = [...url.searchParams.keys()];
|
|
1301
|
-
const chatRead = reading && (path === "/ui/effects/placements" || /^\/ui\/effects\/resource\/[0-9a-f]{32}$/.test(path))
|
|
1302
|
-
&& keys.length === 2 && keys.includes("instance") && keys.includes("ownerEpoch") && !!instanceQuery && instanceQuery.length <= 128;
|
|
1303
|
-
// Public viewing grants current Chat reads, never ordinary library/owner/Trial access or writes.
|
|
1304
|
-
if (gateway && (!chatRead || !gateway.isCurrent())) {
|
|
1305
|
-
json(res, reading ? 404 : 403, { error: "effects_not_over_public_link" });
|
|
1306
|
-
return true;
|
|
1307
|
-
}
|
|
1308
|
-
const who = () => {
|
|
1309
|
-
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
1310
|
-
if (verdict.kind !== "allow")
|
|
1311
|
-
return { status: 401, error: WEB_TOKEN_INVALID_MESSAGE };
|
|
1312
|
-
if (verdict.via !== "session" || !verdict.session)
|
|
1313
|
-
return { status: 403, error: "effects_need_a_signed_in_person" };
|
|
1314
|
-
if (!reading && !profilePost && verdict.session.tier !== "admin")
|
|
1315
|
-
return { status: 403, error: "effects_read_only" };
|
|
1316
|
-
if (verdict.session.tier === "guest" && (!chatRead || !guest || !guest.current() || !guest.scope().has(instanceQuery)))
|
|
1317
|
-
return { status: 404, error: "not_found" };
|
|
1318
|
-
return { person: webPersonKey(verdict.session.owner), canWrite: verdict.session.tier === "admin", epoch: verdict.session.handle, guest: verdict.session.tier === "guest" };
|
|
1319
|
-
};
|
|
1320
|
-
const first = who();
|
|
1321
|
-
if ("status" in first) {
|
|
1322
|
-
json(res, first.status, { error: first.error });
|
|
1323
|
-
return true;
|
|
1324
|
-
}
|
|
1325
|
-
const service = ctx.effectService;
|
|
1326
|
-
if (!service) {
|
|
1327
|
-
json(res, 404, { error: "effects_unavailable" });
|
|
1328
|
-
return true;
|
|
1329
|
-
}
|
|
1330
|
-
const unavailable = new Set(["themes_unreadable", "profiles_unreadable", "library_unreadable", "references_unreadable", "library_busy", "profiles_busy", "profiles_write_failed", "library_write_failed", "prefs_unreadable"]);
|
|
1331
|
-
const answer = (result) => json(res, typeof result.error === "string" ? result.error === "not_found" ? 404 : result.error === "profile_rate_limited" ? 429 : unavailable.has(result.error) ? 503 : 400 : 200, result);
|
|
1332
|
-
const idPath = /^\/ui\/effects\/(resource|profile)\/([0-9a-f]{32})$/.exec(path);
|
|
1333
|
-
const instance = url.searchParams.get("instance") ?? undefined, slot = url.searchParams.get("slot");
|
|
1334
|
-
if (slot !== null && instance !== undefined) {
|
|
1335
|
-
json(res, 404, { error: "not_found" });
|
|
1336
|
-
return true;
|
|
1337
|
-
}
|
|
1338
|
-
const runtimeRead = reading && (slot !== null || path === "/ui/effects/owner" || path === "/ui/effects/placements" || idPath?.[1] === "resource" || instance !== undefined && /^\/ui\/effects\/image\//.test(path));
|
|
1339
|
-
if ((runtimeRead || profilePost) && (url.searchParams.getAll("ownerEpoch").length !== 1 || url.searchParams.get("ownerEpoch") !== first.epoch)) {
|
|
1340
|
-
const privateResource = reading && (slot !== null || idPath?.[1] === "resource" || /^\/ui\/effects\/image\//.test(path));
|
|
1341
|
-
json(res, privateResource ? 404 : 409, { error: privateResource ? "not_found" : "effects_person_changed" });
|
|
1342
|
-
return true;
|
|
1343
|
-
}
|
|
1344
|
-
if (reading) {
|
|
1345
|
-
const image = /^\/ui\/effects\/image\/([0-9a-f]{32})\/(poster|cursor)\.png$/.exec(path);
|
|
1346
|
-
if (image) {
|
|
1347
|
-
const r = slot === null ? service.image(first.person, image[1], image[2], instance) : service.ownerImage(first.person, image[1], image[2], slot);
|
|
1348
|
-
if (!r.ok)
|
|
1349
|
-
json(res, 404, { error: "not_found" });
|
|
1350
|
-
else {
|
|
1351
|
-
res.writeHead(200, { "Content-Type": "image/png", "Content-Security-Policy": "default-src 'none'; sandbox" });
|
|
1352
|
-
res.end(r.bytes);
|
|
1353
|
-
}
|
|
1354
|
-
return true;
|
|
1355
|
-
}
|
|
1356
|
-
if (path === "/ui/effects")
|
|
1357
|
-
answer({ ...service.list(first.person), canWrite: first.canWrite });
|
|
1358
|
-
else if (path === "/ui/effects/owner" && instance === undefined && slot === null)
|
|
1359
|
-
answer(service.ownerSelections(first.person));
|
|
1360
|
-
else if (path === "/ui/effects/placements" && instance !== undefined)
|
|
1361
|
-
answer(service.placements(first.person, instance, !!gateway || first.guest));
|
|
1362
|
-
else if (idPath?.[1] === "resource")
|
|
1363
|
-
answer(slot === null ? service.resource(first.person, idPath[2], instance, !!gateway || first.guest) : service.ownerResource(first.person, idPath[2], slot));
|
|
1364
|
-
else if (idPath?.[1] === "profile" && instance === undefined)
|
|
1365
|
-
answer(service.profile(first.person, idPath[2]));
|
|
1366
|
-
else
|
|
1367
|
-
json(res, 404, { error: "not_found" });
|
|
1368
|
-
return true;
|
|
1369
|
-
}
|
|
1370
|
-
const deletion = /^\/ui\/effects\/([0-9a-f]{32})$/.exec(path);
|
|
1371
|
-
if (method === "DELETE" && deletion) {
|
|
1372
|
-
answer(service.delete(first.person, deletion[1]));
|
|
1373
|
-
return true;
|
|
1374
|
-
}
|
|
1375
|
-
if (method !== "POST" || !["/ui/effects/upload", "/ui/effects/apply", "/ui/effects/remove", "/ui/effects/share"].includes(path) && idPath?.[1] !== "profile") {
|
|
1376
|
-
json(res, 404, { error: "not_found" });
|
|
1377
|
-
return true;
|
|
1378
|
-
}
|
|
1379
|
-
(async () => {
|
|
1380
|
-
let bytes;
|
|
1381
|
-
try {
|
|
1382
|
-
bytes = await readBoundedWebBody(req, path === "/ui/effects/upload" ? EFFECT_WIRE_LIMIT : 16 * 1024);
|
|
1383
|
-
}
|
|
1384
|
-
catch {
|
|
1385
|
-
json(res, 413, { error: "effect_body_bound" });
|
|
1386
|
-
return;
|
|
1387
|
-
}
|
|
1388
|
-
if (!permitWebContinuation(req, res, ctx))
|
|
1389
|
-
return;
|
|
1390
|
-
const now = who();
|
|
1391
|
-
if ("status" in now) {
|
|
1392
|
-
json(res, now.status, { error: now.error });
|
|
1393
|
-
return;
|
|
1394
|
-
}
|
|
1395
|
-
if (!canvasWorkerEnabled(ctx.fleetConfig?.web)) {
|
|
1396
|
-
json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
|
|
1397
|
-
return;
|
|
1398
|
-
}
|
|
1399
|
-
if (now.person !== first.person || now.epoch !== first.epoch) {
|
|
1400
|
-
json(res, 409, { error: "effects_person_changed" });
|
|
1401
|
-
return;
|
|
1402
|
-
}
|
|
1403
|
-
if (path === "/ui/effects/upload") {
|
|
1404
|
-
answer(service.upload(now.person, bytes));
|
|
1405
|
-
return;
|
|
1406
|
-
}
|
|
1407
|
-
let body;
|
|
1408
|
-
try {
|
|
1409
|
-
body = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
1410
|
-
}
|
|
1411
|
-
catch {
|
|
1412
|
-
json(res, 400, { error: "effect_json" });
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
if (path === "/ui/effects/apply")
|
|
1416
|
-
answer(service.apply(now.person, body));
|
|
1417
|
-
else if (path === "/ui/effects/remove")
|
|
1418
|
-
answer(service.remove(now.person, body));
|
|
1419
|
-
else if (path === "/ui/effects/share")
|
|
1420
|
-
answer(service.share(now.person, body));
|
|
1421
|
-
else {
|
|
1422
|
-
if (body && typeof body === "object" && body.kind === "playback" && (url.searchParams.getAll("instance").length !== 1 || slot !== null)) {
|
|
1423
|
-
json(res, 400, { error: "playback_report" });
|
|
1424
|
-
return;
|
|
1425
|
-
}
|
|
1426
|
-
answer(service.report(now.person, idPath[2], body, instance, now.epoch));
|
|
1427
|
-
}
|
|
1428
|
-
})().catch(() => { try {
|
|
1429
|
-
json(res, 500, { error: "effect_request_failed" });
|
|
1430
|
-
}
|
|
1431
|
-
catch { /* response ended */ } });
|
|
1432
|
-
return true;
|
|
1433
|
-
}
|
|
1434
1140
|
/**
|
|
1435
1141
|
* #1629 r4 §4 (r4-5): mods — a person's import (stage, then commit), uninstall and export. Every route is a signed-in
|
|
1436
1142
|
* session's, writes an admin's: never the CLI's header token (the way an agent could reach it) and never the public link
|
|
@@ -1513,18 +1219,13 @@ function handleMods(req, res, url, ctx, path, method) {
|
|
|
1513
1219
|
json(res, 404, { error: "no_such_mod" });
|
|
1514
1220
|
return true;
|
|
1515
1221
|
}
|
|
1516
|
-
// The file is the installed bundle, effects and all
|
|
1517
|
-
//
|
|
1518
|
-
// read every effect in it (their own claim on that content, or a shared one). A file without effects is any admin's.
|
|
1222
|
+
// The file is the installed bundle, effects and all. #2799: a file that carries Canvas Worker effects (or can't be
|
|
1223
|
+
// read to tell) is not handed out; one without is any admin's.
|
|
1519
1224
|
const fx = mods.bundleEffects(fileMatch[1]);
|
|
1520
|
-
if (
|
|
1225
|
+
if (fx === "unknown" || (fx?.length ?? 0) > 0) {
|
|
1521
1226
|
json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
|
|
1522
1227
|
return true;
|
|
1523
1228
|
}
|
|
1524
|
-
if (fx === "unknown" || (fx ?? []).some(id => !ctx.modPlacedEffect?.(first.person, id))) {
|
|
1525
|
-
json(res, 403, { error: "mod_effects_not_yours", detail: "this mod's file carries effects that are not yours to share: export your own look instead" });
|
|
1526
|
-
return true;
|
|
1527
|
-
}
|
|
1528
1229
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "X-Content-Type-Options": "nosniff", "Cache-Control": "private, no-store",
|
|
1529
1230
|
"Content-Disposition": `attachment; filename="${rec.name}-${rec.version}.agendmod.json"` });
|
|
1530
1231
|
res.end(text);
|
|
@@ -1648,14 +1349,9 @@ export function exportMod(ctx, person, body) {
|
|
|
1648
1349
|
const store = ctx.themeStore;
|
|
1649
1350
|
const rec = store.get(name);
|
|
1650
1351
|
if (inc.theme === true) {
|
|
1651
|
-
//
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
const effects = (canvasWorkerEnabled(ctx.fleetConfig?.web) ? rec.placements.effects : []).flatMap(p => { const e = ctx.modPlacedEffect?.(person, p.id); if (!e || !filesPlayInChat(e.files)) {
|
|
1655
|
-
omitted++;
|
|
1656
|
-
return [];
|
|
1657
|
-
} return [{ name: e.name, files: e.files, params: p.params ?? {} }]; });
|
|
1658
|
-
parts.theme = { theme: rec.theme, picture: id => { const a = rec.assets.find(x => x.id === id); const b = store.asset(name, id); return a && b ? { bytes: b.bytes, kind: a.kind } : null; }, effects };
|
|
1352
|
+
// #2799: Canvas Worker effects were removed; an older placement travels nowhere (it is counted as left out).
|
|
1353
|
+
omitted += rec.placements.effects.length;
|
|
1354
|
+
parts.theme = { theme: rec.theme, picture: id => { const a = rec.assets.find(x => x.id === id); const b = store.asset(name, id); return a && b ? { bytes: b.bytes, kind: a.kind } : null; }, effects: [] };
|
|
1659
1355
|
}
|
|
1660
1356
|
if (inc.identity === true)
|
|
1661
1357
|
parts.identity = ctx.modCardText?.(name) ?? {};
|
|
@@ -1728,6 +1424,109 @@ export function webPeerOf(req) {
|
|
|
1728
1424
|
const gateway = gatewayRequestContext(req);
|
|
1729
1425
|
return gateway ? (gateway.named === true ? "named_link" : "quick_link") : classifyWebPeer(req);
|
|
1730
1426
|
}
|
|
1427
|
+
/** #2855: the guest accounts with a search running, and how many guest searches run in all (GUEST_SEARCHES). */
|
|
1428
|
+
const guestSearchesRunning = new Set();
|
|
1429
|
+
let guestSearchesInFlight = 0;
|
|
1430
|
+
/**
|
|
1431
|
+
* #2855: a trusted guest's search — chat messages of its in-scope agents that it can see now (guestSeesMessage, on the
|
|
1432
|
+
* history's own row: one no longer kept is not shown, nothing else is), at most GUEST_SEARCHES.maxResults. Per account
|
|
1433
|
+
* GUEST_SEARCHES a minute and one at a time; at most GUEST_SEARCHES.global guest searches running in all, so the
|
|
1434
|
+
* index's queue always has room for the admins'. Its context: the same rows only.
|
|
1435
|
+
*/
|
|
1436
|
+
function guestSearch(req, res, url, ctx, guest, path) {
|
|
1437
|
+
const refusal = () => {
|
|
1438
|
+
if (!guest.current())
|
|
1439
|
+
return [401, WEB_TOKEN_INVALID_MESSAGE];
|
|
1440
|
+
if (!guest.trusted?.())
|
|
1441
|
+
return [403, "guest_not_trusted"];
|
|
1442
|
+
if (gatewayRequestContext(req) && ctx.fleetConfig?.web?.search?.public_link === false)
|
|
1443
|
+
return [404, "search_not_over_public_link"];
|
|
1444
|
+
if (ctx.fleetConfig?.web?.search?.enabled === false || !ctx.searchIndex || ctx.searchIndex.status() === "unavailable")
|
|
1445
|
+
return [404, "search_unavailable"];
|
|
1446
|
+
return null;
|
|
1447
|
+
};
|
|
1448
|
+
const first = refusal();
|
|
1449
|
+
if (first) {
|
|
1450
|
+
json(res, first[0], { error: first[1] });
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
const account = `${guest.owner.adapterId}:${guest.owner.userId}`;
|
|
1454
|
+
const limit = ctx.guestSearches?.check(account);
|
|
1455
|
+
if (limit && !limit.ok) {
|
|
1456
|
+
res.setHeader("Retry-After", String(limit.retry_after_s));
|
|
1457
|
+
json(res, 429, { error: "guest_search_rate_limited", retry_after_s: limit.retry_after_s });
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
if (guestSearchesRunning.has(account)) {
|
|
1461
|
+
json(res, 429, { error: "guest_search_running" });
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
if (guestSearchesInFlight >= GUEST_SEARCHES.global) {
|
|
1465
|
+
res.setHeader("Retry-After", "2");
|
|
1466
|
+
json(res, 503, { error: "search_busy" });
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
const index = ctx.searchIndex, history = ctx.webChatHistory;
|
|
1470
|
+
// What this guest may see of one indexed chat row: its history row, now, by the guest rule; else nothing.
|
|
1471
|
+
const visible = (hit) => {
|
|
1472
|
+
if (hit.source !== "chat" || !guest.scope().has(hit.instance) || !history)
|
|
1473
|
+
return false;
|
|
1474
|
+
const row = history.find(hit.instance, String(hit.ref.boot ?? ""), Number(hit.ref.id));
|
|
1475
|
+
return !!row && guestSeesMessage(row, guest, guest.scope());
|
|
1476
|
+
};
|
|
1477
|
+
const finish = (status, body) => { const now = refusal(); if (now)
|
|
1478
|
+
json(res, now[0], { error: now[1] });
|
|
1479
|
+
else
|
|
1480
|
+
json(res, status, body); };
|
|
1481
|
+
const failed = (reason) => finish(reason === "timeout" || reason === "busy" ? 503 : 404, { error: `search_${reason}` });
|
|
1482
|
+
let run;
|
|
1483
|
+
if (path === "/ui/search/context") {
|
|
1484
|
+
const key = url.searchParams.get("key") ?? "";
|
|
1485
|
+
if (!key || key.length > 300) {
|
|
1486
|
+
json(res, 400, { error: "key required" });
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
run = index.context(key).then(r => {
|
|
1490
|
+
if (!r.ok)
|
|
1491
|
+
return failed(r.reason);
|
|
1492
|
+
const value = r.value;
|
|
1493
|
+
if (!value?.doc || !visible(value.doc))
|
|
1494
|
+
return finish(404, { error: "not_found" });
|
|
1495
|
+
finish(200, { doc: value.doc, before: value.before.filter(visible), after: value.after.filter(visible), building: r.building });
|
|
1496
|
+
}, () => failed("unavailable"));
|
|
1497
|
+
}
|
|
1498
|
+
else {
|
|
1499
|
+
// Never a team (whose members, and whether it exists, are the admin's) or "only mine" (an admin's own messages).
|
|
1500
|
+
const own = new URL(url.href);
|
|
1501
|
+
own.searchParams.delete("team");
|
|
1502
|
+
own.searchParams.delete("mine");
|
|
1503
|
+
const parsed = parseSearchQuery(own, ctx);
|
|
1504
|
+
if ("error" in parsed) {
|
|
1505
|
+
json(res, 400, { error: parsed.error });
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
const scope = guest.scope();
|
|
1509
|
+
const asked = parsed.filters.instances?.filter(n => scope.has(n)) ?? [...scope];
|
|
1510
|
+
if (!asked.length) {
|
|
1511
|
+
json(res, 200, { hits: [], limited: false });
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
// Chat only, its scope only; more asked of the index than shown, since rows it can't see are left out after.
|
|
1515
|
+
run = index.query({ q: parsed.filters.q, instances: asked, sources: ["chat"], limit: SEARCH_LIMIT_DEFAULT,
|
|
1516
|
+
...(parsed.filters.since !== undefined ? { since: parsed.filters.since } : {}), ...(parsed.filters.until !== undefined ? { until: parsed.filters.until } : {}) }).then(r => {
|
|
1517
|
+
if (!r.ok)
|
|
1518
|
+
return failed(r.reason);
|
|
1519
|
+
const hits = r.value.hits.filter(visible);
|
|
1520
|
+
finish(200, { hits: hits.slice(0, GUEST_SEARCHES.maxResults).map(h => ({ ...h, live: true })), limited: r.value.limited || hits.length > GUEST_SEARCHES.maxResults, building: r.building });
|
|
1521
|
+
}, () => failed("unavailable"));
|
|
1522
|
+
}
|
|
1523
|
+
ctx.guestSearches?.record(account);
|
|
1524
|
+
guestSearchesRunning.add(account);
|
|
1525
|
+
guestSearchesInFlight++;
|
|
1526
|
+
void run.catch(() => { if (!res.headersSent)
|
|
1527
|
+
json(res, 500, { error: "search_failed" }); })
|
|
1528
|
+
.finally(() => { guestSearchesRunning.delete(account); guestSearchesInFlight--; });
|
|
1529
|
+
}
|
|
1731
1530
|
/**
|
|
1732
1531
|
* #2122: what let this request in, asked now — its session (by handle, under the token it was signed in with) or the
|
|
1733
1532
|
* header token; null when nothing does any more (the gate also refuses a public link that is no longer the current
|
|
@@ -1911,10 +1710,6 @@ function statusTick(ctx, t) {
|
|
|
1911
1710
|
export function handleWebRequest(req, res, url, ctx) {
|
|
1912
1711
|
const path = url.pathname;
|
|
1913
1712
|
const method = req.method ?? "GET";
|
|
1914
|
-
if (!canvasWorkerEnabled(ctx.fleetConfig?.web) && isCanvasWorkerHttpPath(path)) {
|
|
1915
|
-
json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
|
|
1916
|
-
return true;
|
|
1917
|
-
}
|
|
1918
1713
|
// Auth check for all /ui routes. Defence in depth behind the fleet-manager
|
|
1919
1714
|
// gate: the same session cookie or header token, and an unset token closes
|
|
1920
1715
|
// the panel instead of comparing null against a missing credential.
|
|
@@ -2190,7 +1985,10 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2190
1985
|
const guest = guestRequestOf(req);
|
|
2191
1986
|
const messages = !ctx.webChatHistory ? [] : guest ? guestHistory(ctx.webChatHistory, instance, limit, guest, guest.scope())
|
|
2192
1987
|
: visibleMessages(ctx, !!gatewayRequestContext(req), ctx.webChatHistory.list(instance, limit));
|
|
2193
|
-
|
|
1988
|
+
// #2848: the chat has messages, none of them a guest's to see (all between agents, or kept before they were marked):
|
|
1989
|
+
// said, so its empty chat is explained rather than blank. Only that there are some — never how many, or what.
|
|
1990
|
+
const allHidden = !!guest && messages.length === 0 && (ctx.webChatHistory?.list(instance, 1).length ?? 0) > 0;
|
|
1991
|
+
json(res, 200, { messages, boot: ctx.webChatHistory?.boot ?? null, lastId: ctx.webChatHistory?.lastId ?? 0, ...(allHidden ? { allHidden: true } : {}) });
|
|
2194
1992
|
return true;
|
|
2195
1993
|
}
|
|
2196
1994
|
// ── #1218: the Log tab ─────────────────────────────────
|
|
@@ -2854,6 +2652,13 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2854
2652
|
json(res, 404, { error: "Preferences are not kept on this fleet" });
|
|
2855
2653
|
return true;
|
|
2856
2654
|
}
|
|
2655
|
+
// #2855: a guest saves its own preferences (GUEST_OWN_PREF_KEYS) into its own account's bucket — on any listener,
|
|
2656
|
+
// the public link too, since nothing of the host's is written. At most GUEST_PREF_WRITES a minute per account.
|
|
2657
|
+
const guest = guestRequestOf(req);
|
|
2658
|
+
if (guest) {
|
|
2659
|
+
saveGuestPrefs(req, res, ctx, prefs, guest);
|
|
2660
|
+
return true;
|
|
2661
|
+
}
|
|
2857
2662
|
if (gatewayRequestContext(req)) {
|
|
2858
2663
|
json(res, 403, { error: "prefs_not_over_public_link" });
|
|
2859
2664
|
return true;
|
|
@@ -2894,30 +2699,16 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2894
2699
|
return;
|
|
2895
2700
|
}
|
|
2896
2701
|
const personal = [...parsed.patch.keys()].some(isOwnerEffectKey);
|
|
2897
|
-
|
|
2702
|
+
// #2799: a person's own effect selections were Canvas Worker effects' (removed): refused, as before.
|
|
2703
|
+
if (personal) {
|
|
2898
2704
|
json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
|
|
2899
2705
|
return;
|
|
2900
2706
|
}
|
|
2901
|
-
|
|
2902
|
-
json(res, 403, { error: "prefs_need_a_signed_in_person" });
|
|
2903
|
-
return;
|
|
2904
|
-
}
|
|
2905
|
-
// A scope marker rejects a queued old page after a different login, including the same person's new session.
|
|
2906
|
-
// It supplies neither identity nor authority; the current verified session still chooses the bucket.
|
|
2907
|
-
if (personal && body.ownerEpoch !== now.epoch) {
|
|
2908
|
-
json(res, 409, { error: "prefs_person_changed" });
|
|
2909
|
-
return;
|
|
2910
|
-
}
|
|
2911
|
-
const before = prefs.readForViewer(now.person);
|
|
2912
|
-
const written = prefs.writeForViewer(now.person, parsed.patch, { onlyIfUnset: body.onlyIfUnset === true,
|
|
2913
|
-
validateEffect: (person, key, id) => ctx.effectService ? ctx.effectService.validateOwnerSelection(person, key, id) : "effects_unavailable" });
|
|
2707
|
+
const written = prefs.writeForViewer(now.person, parsed.patch, { onlyIfUnset: body.onlyIfUnset === true, validateEffect: () => "effects_unavailable" });
|
|
2914
2708
|
if (!written.ok) {
|
|
2915
2709
|
json(res, written.status, { error: written.error });
|
|
2916
2710
|
return;
|
|
2917
2711
|
}
|
|
2918
|
-
const dropped = written.changed.filter(isOwnerEffectKey).flatMap(key => typeof before.values[key] === "string" && before.values[key] !== written.view.values[key] ? [before.values[key]] : []);
|
|
2919
|
-
if (dropped.length)
|
|
2920
|
-
ctx.effectService?.collectOwnerReplaced(dropped);
|
|
2921
2712
|
if (written.changed.length)
|
|
2922
2713
|
ctx.emitSseEvent("status", ctx.getUiStatus());
|
|
2923
2714
|
json(res, 200, { ...written.view, ...prefsScope(now), changed: written.changed });
|
|
@@ -2943,19 +2734,11 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2943
2734
|
// #1629 r4-5: mods — a person's import, uninstall and export (handleMods: sessions only, never the public link).
|
|
2944
2735
|
if (path === "/ui/mods" || path.startsWith("/ui/mods/"))
|
|
2945
2736
|
return handleMods(req, res, url, ctx, path, method);
|
|
2946
|
-
// Posters remain behind the common session gate, with a closed GET-only public manifest.
|
|
2947
|
-
if (path === "/ui/effect-posters" || path.startsWith("/ui/effect-posters/"))
|
|
2948
|
-
return handleEffectPosters(res, url, ctx, path, method);
|
|
2949
|
-
// Canvas effects are absent from the public gateway manifest and independently refused above.
|
|
2950
|
-
if (path === "/ui/effect-trials" || path.startsWith("/ui/effect-trials/"))
|
|
2951
|
-
return handleEffectTrials(req, res, url, ctx, path, method);
|
|
2952
2737
|
// #2233: backend sign-in — absent from the public manifest and the guest table, and refused here too.
|
|
2953
2738
|
if (path === "/ui/browser-backend-signins")
|
|
2954
2739
|
return handleBrowserBackendSignIns(req, res, url, ctx);
|
|
2955
2740
|
if (path === "/ui/backend-login" || path.startsWith("/ui/backend-login/"))
|
|
2956
2741
|
return handleBackendLogin(req, res, url, ctx, path, method);
|
|
2957
|
-
if (path === "/ui/effects" || path.startsWith("/ui/effects/"))
|
|
2958
|
-
return handleEffects(req, res, url, ctx, path, method);
|
|
2959
2742
|
// #1386: Acknowledge a "Needs you" delivery item — a session write (CSRF, checked by the gate) or the CLI's header
|
|
2960
2743
|
// token. Who did it is recorded by principal: the session's public handle, never the session id or a credential.
|
|
2961
2744
|
if (method === "POST" && path === "/ui/needs/ack") {
|
|
@@ -3050,6 +2833,21 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
3050
2833
|
return true;
|
|
3051
2834
|
}
|
|
3052
2835
|
const run = ctx.webCommand.bind(ctx);
|
|
2836
|
+
// #2855: a guest — an in-scope agent, while guests may chat, the table's "anyone" commands only (runWebCommand), at
|
|
2837
|
+
// most GUEST_COMMAND_RUNS a minute per account and one at a time.
|
|
2838
|
+
const guest = guestRequestOf(req), account = guest ? `${guest.owner.adapterId}:${guest.owner.userId}` : "";
|
|
2839
|
+
if (guest) {
|
|
2840
|
+
const limit = ctx.guestCommandRuns?.check(account);
|
|
2841
|
+
if (limit && !limit.ok) {
|
|
2842
|
+
res.setHeader("Retry-After", String(limit.retry_after_s));
|
|
2843
|
+
json(res, 429, { error: "guest_commands_rate_limited", retry_after_s: limit.retry_after_s });
|
|
2844
|
+
return true;
|
|
2845
|
+
}
|
|
2846
|
+
if (guestCommandsRunning.has(account)) {
|
|
2847
|
+
json(res, 429, { error: "guest_command_running" });
|
|
2848
|
+
return true;
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
3053
2851
|
const arrival = webAuthorityOf(req, url, ctx); // #1939 §3: on arrival, and again after the body
|
|
3054
2852
|
(async () => {
|
|
3055
2853
|
let body;
|
|
@@ -3079,9 +2877,39 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
3079
2877
|
const person = webPersonOr401(req, res, url, ctx); // #1827: asked again, now the body is in
|
|
3080
2878
|
if (!person)
|
|
3081
2879
|
return;
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
2880
|
+
if (guest) {
|
|
2881
|
+
if (!guest.current()) {
|
|
2882
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
const refused = guestChatRefusal(guest, instance);
|
|
2886
|
+
if (refused) {
|
|
2887
|
+
json(res, refused.status, { error: refused.error });
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
if (guestCommandsRunning.has(account)) {
|
|
2891
|
+
json(res, 429, { error: "guest_command_running" });
|
|
2892
|
+
return;
|
|
2893
|
+
}
|
|
2894
|
+
// The budget again, now (#2857 review): commands that ran while this body was on its way have counted.
|
|
2895
|
+
const budget = ctx.guestCommandRuns?.check(account);
|
|
2896
|
+
if (budget && !budget.ok) {
|
|
2897
|
+
res.setHeader("Retry-After", String(budget.retry_after_s));
|
|
2898
|
+
json(res, 429, { error: "guest_commands_rate_limited", retry_after_s: budget.retry_after_s });
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
guestCommandsRunning.add(account);
|
|
2902
|
+
ctx.guestCommandRuns?.record(account);
|
|
2903
|
+
}
|
|
2904
|
+
try {
|
|
2905
|
+
const { authority } = confirmAfterBody(arrival, webAuthorityOf(req, url, ctx, instance));
|
|
2906
|
+
const r = await run({ instance, command, args: str(body.args, MAX_COMMAND_ARGS), confirm: body.confirm }, { publicLink: !!gatewayRequestContext(req), person, authority, ...(guest ? { guest: true } : {}) });
|
|
2907
|
+
json(res, r.status, r.body);
|
|
2908
|
+
}
|
|
2909
|
+
finally {
|
|
2910
|
+
if (guest)
|
|
2911
|
+
guestCommandsRunning.delete(account);
|
|
2912
|
+
}
|
|
3085
2913
|
})().catch(err => {
|
|
3086
2914
|
ctx.logger.error({ err: err.message }, "Web command failed");
|
|
3087
2915
|
try {
|
|
@@ -3706,11 +3534,15 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
3706
3534
|
// #2478 (the user's request): over the public link too, unless web.search.public_link is false. Asked on arrival and
|
|
3707
3535
|
// again before the answer is sent.
|
|
3708
3536
|
if (method === "GET" && (path === "/ui/search" || path === "/ui/search/context")) {
|
|
3537
|
+
// #2855: a trusted guest searches only what it can see (guestSearch).
|
|
3538
|
+
const searchGuest = guestRequestOf(req);
|
|
3539
|
+
if (searchGuest) {
|
|
3540
|
+
guestSearch(req, res, url, ctx, searchGuest, path);
|
|
3541
|
+
return true;
|
|
3542
|
+
}
|
|
3709
3543
|
const refusal = () => {
|
|
3710
3544
|
if (gatewayRequestContext(req) && ctx.fleetConfig?.web?.search?.public_link === false)
|
|
3711
3545
|
return [404, "search_not_over_public_link"];
|
|
3712
|
-
if (guestRequestOf(req))
|
|
3713
|
-
return [403, "guest_forbidden"];
|
|
3714
3546
|
if (ctx.fleetConfig?.web?.search?.enabled === false || !ctx.searchIndex || ctx.searchIndex.status() === "unavailable")
|
|
3715
3547
|
return [404, "search_unavailable"];
|
|
3716
3548
|
return null;
|
|
@@ -4121,7 +3953,7 @@ function handleSendMessage(req, res, ctx) {
|
|
|
4121
3953
|
let quoted;
|
|
4122
3954
|
if (replyTo) {
|
|
4123
3955
|
quoted = ctx.webChatHistory?.find(instance, replyTo.boot, replyTo.id);
|
|
4124
|
-
if (!quoted ||
|
|
3956
|
+
if (!quoted || (guest && !guestSeesMessage(quoted, guest, guest.scope()))) {
|
|
4125
3957
|
json(res, 409, { error: t("web.reply_gone"), reason: "reply_gone" });
|
|
4126
3958
|
return;
|
|
4127
3959
|
}
|
|
@@ -4533,6 +4365,64 @@ function serveMediaRange(req, res, files, id, file) {
|
|
|
4533
4365
|
res.writeHead(ranged ? 206 : 200);
|
|
4534
4366
|
res.end(got.bytes);
|
|
4535
4367
|
}
|
|
4368
|
+
/** #2855: a guest's own preferences — its own keys, its own bucket, favourites only in its scope. */
|
|
4369
|
+
function saveGuestPrefs(req, res, ctx, prefs, guest) {
|
|
4370
|
+
const person = webPersonKey(guest.owner), account = `${guest.owner.adapterId}:${guest.owner.userId}`;
|
|
4371
|
+
const limit = ctx.guestPrefWrites?.check(account);
|
|
4372
|
+
if (limit && !limit.ok) {
|
|
4373
|
+
res.setHeader("Retry-After", String(limit.retry_after_s));
|
|
4374
|
+
json(res, 429, { error: "guest_prefs_rate_limited", retry_after_s: limit.retry_after_s });
|
|
4375
|
+
return;
|
|
4376
|
+
}
|
|
4377
|
+
// Too large by its own declared length: answered before a byte is read (the body is drained, not cut off).
|
|
4378
|
+
if (Number(req.headers["content-length"]) > GUEST_PREF_MAX_BYTES) {
|
|
4379
|
+
req.resume();
|
|
4380
|
+
json(res, 413, { error: "guest_prefs_too_large", detail: "at most 16 KB" });
|
|
4381
|
+
return;
|
|
4382
|
+
}
|
|
4383
|
+
(async () => {
|
|
4384
|
+
let body;
|
|
4385
|
+
try {
|
|
4386
|
+
body = JSON.parse((await readBoundedWebBody(req, GUEST_PREF_MAX_BYTES)).toString("utf8"));
|
|
4387
|
+
}
|
|
4388
|
+
catch {
|
|
4389
|
+
json(res, 400, { error: "Invalid JSON, or over 16 KB" });
|
|
4390
|
+
return;
|
|
4391
|
+
}
|
|
4392
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
4393
|
+
return;
|
|
4394
|
+
if (!guest.current()) {
|
|
4395
|
+
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
4396
|
+
return;
|
|
4397
|
+
}
|
|
4398
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !body.set || typeof body.set !== "object" || Array.isArray(body.set)) {
|
|
4399
|
+
json(res, 400, { error: "set required" });
|
|
4400
|
+
return;
|
|
4401
|
+
}
|
|
4402
|
+
const set = body.set, scope = guest.scope();
|
|
4403
|
+
const favourites = set["nav.favorites"];
|
|
4404
|
+
if (Array.isArray(favourites)) {
|
|
4405
|
+
const outside = favourites.find((n) => typeof n !== "string" || !scope.has(n));
|
|
4406
|
+
if (outside !== undefined) {
|
|
4407
|
+
json(res, 400, { error: `not_in_scope: ${String(outside).slice(0, 64)}` });
|
|
4408
|
+
return;
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
const written = prefs.writeForGuest(person, new Map(Object.entries(set)), { onlyIfUnset: body.onlyIfUnset === true });
|
|
4412
|
+
if (!written.ok) {
|
|
4413
|
+
json(res, written.status, { error: written.error });
|
|
4414
|
+
return;
|
|
4415
|
+
}
|
|
4416
|
+
ctx.guestPrefWrites?.record(account);
|
|
4417
|
+
json(res, 200, { ...guestOwnPrefs(prefs.readForViewer(person), prefs.readGuestOwn(person), scope), ownerScope: "person", ownerEpoch: null, changed: written.changed });
|
|
4418
|
+
})().catch(err => {
|
|
4419
|
+
ctx.logger.error({ err: err.message }, "Saving a guest's web preferences failed");
|
|
4420
|
+
try {
|
|
4421
|
+
json(res, 500, { error: "Saving preferences failed" });
|
|
4422
|
+
}
|
|
4423
|
+
catch { /* already answered */ }
|
|
4424
|
+
});
|
|
4425
|
+
}
|
|
4536
4426
|
function prefsViewer(req, url, ctx) {
|
|
4537
4427
|
const v = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
4538
4428
|
if (v.kind !== "allow")
|
|
@@ -4541,6 +4431,12 @@ function prefsViewer(req, url, ctx) {
|
|
|
4541
4431
|
// it (#2094) read the host's preferences unfiltered: the sidebar's order and favourites, naming instances out of its
|
|
4542
4432
|
// scope. Its account (phase 3) names it on its page.
|
|
4543
4433
|
const guest = v.via === "session" && v.session?.tier === "guest" ? { guest: true, ...(v.session.owner ? { owner: v.session.owner } : {}) } : {};
|
|
4434
|
+
// #2855: a guest reads its own preferences (its account's bucket) on every listener, the public link too.
|
|
4435
|
+
if (v.via === "session" && v.session?.tier === "guest" && v.session.owner) {
|
|
4436
|
+
const g = guestRequestOf(req);
|
|
4437
|
+
return { person: webPersonKey(v.session.owner), epoch: v.session.handle, scope: "person", canWrite: false, via: "session", ...guest,
|
|
4438
|
+
guestScope: () => g?.scope() ?? new Set() };
|
|
4439
|
+
}
|
|
4544
4440
|
if (!gatewayRequestContext(req) && v.via === "session" && v.session) {
|
|
4545
4441
|
const person = webPersonKey(v.session.owner);
|
|
4546
4442
|
return { person, epoch: v.session.handle, scope: person === OPERATOR ? "operator" : "person", canWrite: v.session.tier === "admin", via: "session", ...guest };
|
|
@@ -4551,7 +4447,10 @@ const prefsScope = (v) => ({ ownerScope: v?.scope ?? "none", ownerEpoch: v?.epoc
|
|
|
4551
4447
|
// #1920: a guest reads the host's look and its own, never a preference that names an instance (guestPrefs).
|
|
4552
4448
|
const prefsView = (prefs, v) => {
|
|
4553
4449
|
const view = { ...prefs.readForViewer(v?.person ?? null), ...prefsScope(v) };
|
|
4554
|
-
|
|
4450
|
+
if (!v?.guest)
|
|
4451
|
+
return view;
|
|
4452
|
+
// #2855: the host's look, then the guest's own preferences (only on a guest with an account: its bucket).
|
|
4453
|
+
return v.person ? guestOwnPrefs(view, prefs.readGuestOwn(v.person), v.guestScope?.() ?? new Set()) : guestPrefs(view);
|
|
4555
4454
|
};
|
|
4556
4455
|
function shellHtmlTag(ctx, mode, viewer) {
|
|
4557
4456
|
if (mode !== "full" || !ctx.webPrefs)
|
|
@@ -4570,20 +4469,12 @@ function shellOwnerCss(ctx, mode, viewer) {
|
|
|
4570
4469
|
}
|
|
4571
4470
|
function guestBodyAttrs(ctx, owner, attr) {
|
|
4572
4471
|
const badge = ctx.guestBadge?.(owner);
|
|
4573
|
-
|
|
4574
|
-
}
|
|
4575
|
-
/** A separate session fence for Chat playback: public/guest viewers acquire no owner-pref identity. */
|
|
4576
|
-
function shellEffect(req, ctx, mode) {
|
|
4577
|
-
if (mode !== "full" || typeof ctx.effectBoot !== "string" || !/^[0-9a-f]{32}$/.test(ctx.effectBoot))
|
|
4578
|
-
return null;
|
|
4579
|
-
const v = evaluateWebRequest(req, new URL(req.url ?? "/", "http://localhost"), ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
4580
|
-
if (v.kind !== "allow" || v.via !== "session" || !v.session || !/^[a-f0-9]{16}$/.test(v.session.handle))
|
|
4581
|
-
return null;
|
|
4582
|
-
return { boot: ctx.effectBoot, epoch: v.session.handle, scope: gatewayRequestContext(req) || v.session.tier === "guest" ? "chat" : "owner" };
|
|
4472
|
+
const trusted = ctx.guestTrusted?.(owner) ? ' data-guest-level="trusted"' : "";
|
|
4473
|
+
return ` data-tier="guest"${trusted}${badge ? ` data-guest-name="${attr(badge.name)}" data-guest-platform="${attr(badge.platform)}"` : ""}`;
|
|
4583
4474
|
}
|
|
4584
4475
|
function shellBodyTag(req, ctx, mode, viewer) {
|
|
4585
4476
|
const p = shellPreview(req, ctx, mode);
|
|
4586
|
-
const ownerCss = shellOwnerCss(ctx, mode, viewer)
|
|
4477
|
+
const ownerCss = shellOwnerCss(ctx, mode, viewer);
|
|
4587
4478
|
let ownerCssState = mode === "full" && (gatewayRequestContext(req) || viewer?.guest) ? "off" : "unknown";
|
|
4588
4479
|
if (mode === "full" && !viewer?.guest && viewer?.person != null && ctx.ownerCss) {
|
|
4589
4480
|
ownerCssState = ctx.ownerCss.effectState(viewer.person);
|
|
@@ -4592,17 +4483,15 @@ function shellBodyTag(req, ctx, mode, viewer) {
|
|
|
4592
4483
|
}
|
|
4593
4484
|
const attr = (v) => String(v ?? "").replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
|
4594
4485
|
return `<body data-mode="${mode}"${mode === "full" && gatewayRequestContext(req) ? ' data-web-transport="poll"' : ""} data-dashboard-origin="${attr(p?.dashboardOrigin)}" data-preview-origin="${attr(p?.previewOrigin)}"`
|
|
4595
|
-
+ `${effect ? ` data-effect-boot="${effect.boot}" data-effect-epoch="${effect.epoch}" data-effect-scope="${effect.scope}"` : ""}`
|
|
4596
4486
|
+ ` data-preview-boot="${attr(p?.previewOrigin ? p.boot : "")}" data-preview-reason="${attr(p ? p.reason : "Previews are not available on this fleet.")}"`
|
|
4597
4487
|
// #1554: no availability asked (the public link, or not the full app) still says why, in the page's language.
|
|
4598
4488
|
+ ` data-preview-reason-code="${attr(p ? p.code ?? "" : gatewayRequestContext(req) ? "publicLink" : "notOffered")}"`
|
|
4599
4489
|
// #1723: why the public link's preview tunnel failed (a fixed reason the page words), when it did.
|
|
4600
4490
|
+ `${p?.detail ? ` data-preview-detail="${attr(p.detail)}"` : ""}`
|
|
4601
4491
|
// #1523 N3: whether the fleet serves /api/ai-usage (web.usage_panel), for every page's ◔ — never to a guest (#1920).
|
|
4602
|
-
+ ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false
|
|
4492
|
+
+ ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false ? "0" : "1"}"` // #2855: guests' too
|
|
4603
4493
|
// #1218: whether the Log tab is on (web.log_tab; off by default): the tab says so and offers to turn it on.
|
|
4604
4494
|
+ ` data-log-tab="${ctx.fleetConfig?.web?.log_tab === true ? "1" : "0"}"`
|
|
4605
|
-
+ ` data-canvas-worker="${canvasWorkerEnabled(ctx.fleetConfig?.web) ? "1" : "0"}"`
|
|
4606
4495
|
// #1937: the upload limits in force for this page (the config's; the public link's own cap on it) — what the chat
|
|
4607
4496
|
// checks before sending a file, and the numbers its words say. The server checks again on every upload.
|
|
4608
4497
|
+ ` data-upload-limits="${attr(JSON.stringify(uploadLimits(ctx.fleetConfig?.web, { publicLink: gatewayRequestContext(req) !== undefined })))}"`
|
|
@@ -4632,10 +4521,7 @@ export function serveAppShell(req, res, ctx, mode) {
|
|
|
4632
4521
|
// A public-link page frames what its exposure's preview names (the quick-tunnel wildcard: its host is not known when
|
|
4633
4522
|
// the page may load); every other page frames exactly the origin this load chose (#1723).
|
|
4634
4523
|
const previewFrames = p && "frameSrc" in p ? p.frameSrc : p?.previewOrigin ? `${p.previewOrigin}/frame` : null;
|
|
4635
|
-
|
|
4636
|
-
const effectOrigin = gatewayRequestContext(req)?.expectedOrigin ?? dashboardOriginFor(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req));
|
|
4637
|
-
const effectFrame = shellEffect(req, ctx, mode) && effectOrigin ? `${effectOrigin}/effect-bootstrap` : null;
|
|
4638
|
-
const frameSrc = [previewFrames, effectFrame].filter(Boolean).join(" ");
|
|
4524
|
+
const frameSrc = previewFrames ?? "";
|
|
4639
4525
|
sendPanelHtml(res, withAppIconLinks(html).replace('<html lang="en">', shellHtmlTag(ctx, mode, viewer)).replace("<body>", shellBodyTag(req, ctx, mode, viewer)), 200, {}, { ...(frameSrc ? { frameSrc } : {}), mediaSrc });
|
|
4640
4526
|
}
|
|
4641
4527
|
catch {
|