@songsid/agend 2.3.3-beta.3 → 2.3.3-beta.5
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/backend/codex.d.ts +1 -0
- package/dist/backend/codex.js +106 -15
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/types.d.ts +17 -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/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/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +6 -3
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +11 -3
- package/dist/daemon.js +59 -107
- package/dist/daemon.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/fleet-manager.d.ts +26 -44
- package/dist/fleet-manager.js +70 -226
- package/dist/fleet-manager.js.map +1 -1
- 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 +26 -63
- package/dist/instance-lifecycle.js +1 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +2 -0
- package/dist/locale.js.map +1 -1
- 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 +2 -41
- package/dist/outbound-schemas.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 +2 -0
- package/dist/public-web-link.js +5 -0
- package/dist/public-web-link.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 +6 -0
- package/dist/settings-api.js +14 -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/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 +6 -2
- package/dist/topic-commands.js +2 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +2 -1
- 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 +6 -42
- package/dist/ui/chat-thread.js +6 -44
- package/dist/ui/panel-chat.js +21 -50
- package/dist/ui/preview-panel.js +3 -15
- package/dist/ui/search-dialog.js +30 -14
- package/dist/ui/settings-appearance.js +1 -3
- 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-registry.js +0 -1
- package/dist/ui/settings-strings.js +6 -6
- package/dist/ui/shared/app-prefs.js +17 -17
- package/dist/ui/shared/app-shell.js +12 -8
- package/dist/ui/shared/app-store.js +6 -2
- package/dist/ui/shared/app.css +4 -36
- 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/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 +255 -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 +1 -11
- package/dist/web-preview.js +6 -20
- 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/fleet-service-owner.cjs +4 -1
- 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 -672
- 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";
|
|
@@ -319,6 +316,8 @@ export const SSE_HEARTBEAT_MS = 10_000;
|
|
|
319
316
|
async function parseBody(req) {
|
|
320
317
|
return JSON.parse((await readBoundedWebBody(req, 512 * 1024)).toString("utf8"));
|
|
321
318
|
}
|
|
319
|
+
/** #2855: the guest accounts with a command in progress (one at a time each). */
|
|
320
|
+
const guestCommandsRunning = new Set();
|
|
322
321
|
/** Send JSON response. */
|
|
323
322
|
function json(res, status, data) {
|
|
324
323
|
jsonText(res, status, JSON.stringify(data));
|
|
@@ -943,159 +942,6 @@ function handleOwnerCss(req, res, url, ctx, path, method) {
|
|
|
943
942
|
})().catch(fail);
|
|
944
943
|
return true;
|
|
945
944
|
}
|
|
946
|
-
/** #1629: static current-placement posters, separate from the local human-only runtime API. */
|
|
947
|
-
function handleEffectPosters(res, url, ctx, path, method) {
|
|
948
|
-
res.setHeader("Cache-Control", "private, no-store");
|
|
949
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
950
|
-
const image = /^\/ui\/effect-posters\/([0-9a-f]{32})\.png$/.exec(path);
|
|
951
|
-
const names = [...url.searchParams.keys()], instance = url.searchParams.get("instance");
|
|
952
|
-
if (method !== "GET" || path !== "/ui/effect-posters" && !image || names.length !== 1 || names[0] !== "instance" || !instance || instance.length > 128) {
|
|
953
|
-
json(res, 404, { error: "not_found" });
|
|
954
|
-
return true;
|
|
955
|
-
}
|
|
956
|
-
const service = ctx.effectService;
|
|
957
|
-
if (!service) {
|
|
958
|
-
json(res, 404, { error: "effects_unavailable" });
|
|
959
|
-
return true;
|
|
960
|
-
}
|
|
961
|
-
if (image) {
|
|
962
|
-
const r = service.placedPoster(image[1], instance);
|
|
963
|
-
if (!r.ok)
|
|
964
|
-
json(res, 404, { error: "not_found" });
|
|
965
|
-
else {
|
|
966
|
-
res.writeHead(200, { "Content-Type": "image/png", "Content-Security-Policy": "default-src 'none'; sandbox" });
|
|
967
|
-
res.end(r.bytes);
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
else {
|
|
971
|
-
const result = service.posterPlacements(instance);
|
|
972
|
-
json(res, result.error === "themes_unreadable" ? 503 : result.error ? 404 : 200, result);
|
|
973
|
-
}
|
|
974
|
-
return true;
|
|
975
|
-
}
|
|
976
|
-
/** #1629 Try: separate local-admin routes. These never borrow ordinary read-tier/placement authority. */
|
|
977
|
-
function handleEffectTrials(req, res, url, ctx, path, method) {
|
|
978
|
-
res.setHeader("Cache-Control", "private, no-store");
|
|
979
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
980
|
-
const reading = method === "GET";
|
|
981
|
-
if (gatewayRequestContext(req)) {
|
|
982
|
-
json(res, reading ? 404 : 403, { error: "trials_not_over_public_link" });
|
|
983
|
-
return true;
|
|
984
|
-
}
|
|
985
|
-
const who = () => {
|
|
986
|
-
const sessions = ctx.webSessions;
|
|
987
|
-
const v = evaluateWebRequest(req, url, ctx.webToken ?? null, sessions, { touch: false });
|
|
988
|
-
if (v.kind !== "allow")
|
|
989
|
-
return { error: WEB_TOKEN_INVALID_MESSAGE, status: 401 };
|
|
990
|
-
if (v.via !== "session" || v.session.tier !== "admin" || v.session.surface !== "local")
|
|
991
|
-
return { error: "trials_need_a_local_admin", status: 403 };
|
|
992
|
-
const person = webPersonKey(v.session.owner);
|
|
993
|
-
let css = "unknown";
|
|
994
|
-
try {
|
|
995
|
-
const value = ctx.ownerCss?.effectState(person);
|
|
996
|
-
if (value === "off" || value === "on")
|
|
997
|
-
css = value;
|
|
998
|
-
}
|
|
999
|
-
catch { /* unavailable is never off */ }
|
|
1000
|
-
// CSS is diagnostic for Try; its host-owned shadow shell isolates controls and local paint.
|
|
1001
|
-
const actor = Object.freeze({ via: "session", surface: "local", tier: "admin", sessionHash: v.session.idHash, epoch: v.session.handle, person, ownerCss: css });
|
|
1002
|
-
return { actor, record: v.session, sessions };
|
|
1003
|
-
};
|
|
1004
|
-
const first = who();
|
|
1005
|
-
if ("error" in first) {
|
|
1006
|
-
json(res, first.status, { error: first.error });
|
|
1007
|
-
return true;
|
|
1008
|
-
}
|
|
1009
|
-
const service = ctx.effectService;
|
|
1010
|
-
if (!service) {
|
|
1011
|
-
json(res, 404, { error: "trials_unavailable" });
|
|
1012
|
-
return true;
|
|
1013
|
-
}
|
|
1014
|
-
const start = path === "/ui/effect-trials/start";
|
|
1015
|
-
const route = /^\/ui\/effect-trials\/([0-9a-f]{32})\/(resource|poster\.png|profile|stop)$/.exec(path);
|
|
1016
|
-
const resource = reading && route && ["resource", "poster.png", "profile"].includes(route[2]);
|
|
1017
|
-
const stop = method === "POST" && route?.[2] === "stop";
|
|
1018
|
-
if (!(resource || stop || method === "POST" && start)) {
|
|
1019
|
-
json(res, 404, { error: "not_found" });
|
|
1020
|
-
return true;
|
|
1021
|
-
}
|
|
1022
|
-
const allowed = resource ? ["ownerEpoch", "instance"] : ["ownerEpoch"];
|
|
1023
|
-
if ([...url.searchParams.keys()].some(k => !allowed.includes(k)) || allowed.some(k => url.searchParams.getAll(k).length !== 1)
|
|
1024
|
-
|| url.searchParams.get("ownerEpoch") !== first.actor.epoch) {
|
|
1025
|
-
json(res, reading ? 404 : 409, { error: reading ? "not_found" : "trials_person_changed" });
|
|
1026
|
-
return true;
|
|
1027
|
-
}
|
|
1028
|
-
const instance = url.searchParams.get("instance") ?? "";
|
|
1029
|
-
const answer = (result) => json(res, result.error ? result.error === "not_found" ? 404
|
|
1030
|
-
: ["profiles_unreadable", "profiles_busy", "profiles_write_failed"].includes(String(result.error)) ? 503 : 400 : 200, result);
|
|
1031
|
-
if (resource) {
|
|
1032
|
-
if (route[2] === "poster.png") {
|
|
1033
|
-
const result = service.trialPoster(route[1], instance, first.actor);
|
|
1034
|
-
if (!result.ok)
|
|
1035
|
-
json(res, 404, { error: "not_found" });
|
|
1036
|
-
else {
|
|
1037
|
-
res.writeHead(200, { "Content-Type": "image/png", "Content-Security-Policy": "default-src 'none'; sandbox" });
|
|
1038
|
-
res.end(result.bytes);
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
else
|
|
1042
|
-
answer(route[2] === "profile" ? service.trialProfile(route[1], instance, first.actor) : service.trialResource(route[1], instance, first.actor));
|
|
1043
|
-
return true;
|
|
1044
|
-
}
|
|
1045
|
-
(async () => {
|
|
1046
|
-
let bytes;
|
|
1047
|
-
try {
|
|
1048
|
-
bytes = await readBoundedWebBody(req, 16 * 1024);
|
|
1049
|
-
}
|
|
1050
|
-
catch {
|
|
1051
|
-
json(res, 413, { error: "trial_body_bound" });
|
|
1052
|
-
return;
|
|
1053
|
-
}
|
|
1054
|
-
if (!permitWebContinuation(req, res, ctx))
|
|
1055
|
-
return;
|
|
1056
|
-
const now = who();
|
|
1057
|
-
if ("error" in now) {
|
|
1058
|
-
json(res, now.status, { error: now.error });
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
if (!canvasWorkerEnabled(ctx.fleetConfig?.web)) {
|
|
1062
|
-
json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
|
|
1063
|
-
return;
|
|
1064
|
-
}
|
|
1065
|
-
if (now.record !== first.record || now.sessions !== first.sessions || now.actor.sessionHash !== first.actor.sessionHash
|
|
1066
|
-
|| now.actor.person !== first.actor.person || now.actor.epoch !== first.actor.epoch) {
|
|
1067
|
-
json(res, 409, { error: "trials_person_changed" });
|
|
1068
|
-
return;
|
|
1069
|
-
}
|
|
1070
|
-
let body;
|
|
1071
|
-
try {
|
|
1072
|
-
body = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
1073
|
-
}
|
|
1074
|
-
catch {
|
|
1075
|
-
json(res, 400, { error: "trial_json" });
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
if (start) {
|
|
1079
|
-
const parsed = z.strictObject({ offer: z.string().regex(/^[0-9a-f]{32}$/), instance: z.string().min(1).max(128) }).safeParse(body);
|
|
1080
|
-
if (!parsed.success) {
|
|
1081
|
-
json(res, 400, { error: "trial_args" });
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
answer(service.trialStart(parsed.data.offer, parsed.data.instance, now.actor));
|
|
1085
|
-
}
|
|
1086
|
-
else {
|
|
1087
|
-
if (!z.strictObject({}).safeParse(body).success) {
|
|
1088
|
-
json(res, 400, { error: "trial_args" });
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
answer(service.trialStop(route[1], now.actor));
|
|
1092
|
-
}
|
|
1093
|
-
})().catch(() => { try {
|
|
1094
|
-
json(res, 500, { error: "trial_request_failed" });
|
|
1095
|
-
}
|
|
1096
|
-
catch { /* response ended */ } });
|
|
1097
|
-
return true;
|
|
1098
|
-
}
|
|
1099
945
|
/** #2599: the same closed browser controls as setup, for a current local human admin. */
|
|
1100
946
|
function handleBrowserBackendSignIns(req, res, url, ctx) {
|
|
1101
947
|
res.setHeader("Cache-Control", "private, no-store");
|
|
@@ -1291,148 +1137,6 @@ function handleBackendLogin(req, res, url, ctx, path, method) {
|
|
|
1291
1137
|
catch { /* response ended */ } });
|
|
1292
1138
|
return true;
|
|
1293
1139
|
}
|
|
1294
|
-
/** #1629 B2: effect routes require a human session; route bodies never supply an actor or reference holder. */
|
|
1295
|
-
function handleEffects(req, res, url, ctx, path, method) {
|
|
1296
|
-
res.setHeader("Cache-Control", "private, no-store");
|
|
1297
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
1298
|
-
const reading = method === "GET";
|
|
1299
|
-
// Reports are bounded untrusted diagnostics, not an appearance or admission write (r2 §6).
|
|
1300
|
-
const profilePost = method === "POST" && /^\/ui\/effects\/profile\/[0-9a-f]{32}$/.test(path);
|
|
1301
|
-
const gateway = gatewayRequestContext(req), guest = guestRequestOf(req);
|
|
1302
|
-
const instanceQuery = url.searchParams.get("instance"), keys = [...url.searchParams.keys()];
|
|
1303
|
-
const chatRead = reading && (path === "/ui/effects/placements" || /^\/ui\/effects\/resource\/[0-9a-f]{32}$/.test(path))
|
|
1304
|
-
&& keys.length === 2 && keys.includes("instance") && keys.includes("ownerEpoch") && !!instanceQuery && instanceQuery.length <= 128;
|
|
1305
|
-
// Public viewing grants current Chat reads, never ordinary library/owner/Trial access or writes.
|
|
1306
|
-
if (gateway && (!chatRead || !gateway.isCurrent())) {
|
|
1307
|
-
json(res, reading ? 404 : 403, { error: "effects_not_over_public_link" });
|
|
1308
|
-
return true;
|
|
1309
|
-
}
|
|
1310
|
-
const who = () => {
|
|
1311
|
-
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
1312
|
-
if (verdict.kind !== "allow")
|
|
1313
|
-
return { status: 401, error: WEB_TOKEN_INVALID_MESSAGE };
|
|
1314
|
-
if (verdict.via !== "session" || !verdict.session)
|
|
1315
|
-
return { status: 403, error: "effects_need_a_signed_in_person" };
|
|
1316
|
-
if (!reading && !profilePost && verdict.session.tier !== "admin")
|
|
1317
|
-
return { status: 403, error: "effects_read_only" };
|
|
1318
|
-
if (verdict.session.tier === "guest" && (!chatRead || !guest || !guest.current() || !guest.scope().has(instanceQuery)))
|
|
1319
|
-
return { status: 404, error: "not_found" };
|
|
1320
|
-
return { person: webPersonKey(verdict.session.owner), canWrite: verdict.session.tier === "admin", epoch: verdict.session.handle, guest: verdict.session.tier === "guest" };
|
|
1321
|
-
};
|
|
1322
|
-
const first = who();
|
|
1323
|
-
if ("status" in first) {
|
|
1324
|
-
json(res, first.status, { error: first.error });
|
|
1325
|
-
return true;
|
|
1326
|
-
}
|
|
1327
|
-
const service = ctx.effectService;
|
|
1328
|
-
if (!service) {
|
|
1329
|
-
json(res, 404, { error: "effects_unavailable" });
|
|
1330
|
-
return true;
|
|
1331
|
-
}
|
|
1332
|
-
const unavailable = new Set(["themes_unreadable", "profiles_unreadable", "library_unreadable", "references_unreadable", "library_busy", "profiles_busy", "profiles_write_failed", "library_write_failed", "prefs_unreadable"]);
|
|
1333
|
-
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);
|
|
1334
|
-
const idPath = /^\/ui\/effects\/(resource|profile)\/([0-9a-f]{32})$/.exec(path);
|
|
1335
|
-
const instance = url.searchParams.get("instance") ?? undefined, slot = url.searchParams.get("slot");
|
|
1336
|
-
if (slot !== null && instance !== undefined) {
|
|
1337
|
-
json(res, 404, { error: "not_found" });
|
|
1338
|
-
return true;
|
|
1339
|
-
}
|
|
1340
|
-
const runtimeRead = reading && (slot !== null || path === "/ui/effects/owner" || path === "/ui/effects/placements" || idPath?.[1] === "resource" || instance !== undefined && /^\/ui\/effects\/image\//.test(path));
|
|
1341
|
-
if ((runtimeRead || profilePost) && (url.searchParams.getAll("ownerEpoch").length !== 1 || url.searchParams.get("ownerEpoch") !== first.epoch)) {
|
|
1342
|
-
const privateResource = reading && (slot !== null || idPath?.[1] === "resource" || /^\/ui\/effects\/image\//.test(path));
|
|
1343
|
-
json(res, privateResource ? 404 : 409, { error: privateResource ? "not_found" : "effects_person_changed" });
|
|
1344
|
-
return true;
|
|
1345
|
-
}
|
|
1346
|
-
if (reading) {
|
|
1347
|
-
const image = /^\/ui\/effects\/image\/([0-9a-f]{32})\/(poster|cursor)\.png$/.exec(path);
|
|
1348
|
-
if (image) {
|
|
1349
|
-
const r = slot === null ? service.image(first.person, image[1], image[2], instance) : service.ownerImage(first.person, image[1], image[2], slot);
|
|
1350
|
-
if (!r.ok)
|
|
1351
|
-
json(res, 404, { error: "not_found" });
|
|
1352
|
-
else {
|
|
1353
|
-
res.writeHead(200, { "Content-Type": "image/png", "Content-Security-Policy": "default-src 'none'; sandbox" });
|
|
1354
|
-
res.end(r.bytes);
|
|
1355
|
-
}
|
|
1356
|
-
return true;
|
|
1357
|
-
}
|
|
1358
|
-
if (path === "/ui/effects")
|
|
1359
|
-
answer({ ...service.list(first.person), canWrite: first.canWrite });
|
|
1360
|
-
else if (path === "/ui/effects/owner" && instance === undefined && slot === null)
|
|
1361
|
-
answer(service.ownerSelections(first.person));
|
|
1362
|
-
else if (path === "/ui/effects/placements" && instance !== undefined)
|
|
1363
|
-
answer(service.placements(first.person, instance, !!gateway || first.guest));
|
|
1364
|
-
else if (idPath?.[1] === "resource")
|
|
1365
|
-
answer(slot === null ? service.resource(first.person, idPath[2], instance, !!gateway || first.guest) : service.ownerResource(first.person, idPath[2], slot));
|
|
1366
|
-
else if (idPath?.[1] === "profile" && instance === undefined)
|
|
1367
|
-
answer(service.profile(first.person, idPath[2]));
|
|
1368
|
-
else
|
|
1369
|
-
json(res, 404, { error: "not_found" });
|
|
1370
|
-
return true;
|
|
1371
|
-
}
|
|
1372
|
-
const deletion = /^\/ui\/effects\/([0-9a-f]{32})$/.exec(path);
|
|
1373
|
-
if (method === "DELETE" && deletion) {
|
|
1374
|
-
answer(service.delete(first.person, deletion[1]));
|
|
1375
|
-
return true;
|
|
1376
|
-
}
|
|
1377
|
-
if (method !== "POST" || !["/ui/effects/upload", "/ui/effects/apply", "/ui/effects/remove", "/ui/effects/share"].includes(path) && idPath?.[1] !== "profile") {
|
|
1378
|
-
json(res, 404, { error: "not_found" });
|
|
1379
|
-
return true;
|
|
1380
|
-
}
|
|
1381
|
-
(async () => {
|
|
1382
|
-
let bytes;
|
|
1383
|
-
try {
|
|
1384
|
-
bytes = await readBoundedWebBody(req, path === "/ui/effects/upload" ? EFFECT_WIRE_LIMIT : 16 * 1024);
|
|
1385
|
-
}
|
|
1386
|
-
catch {
|
|
1387
|
-
json(res, 413, { error: "effect_body_bound" });
|
|
1388
|
-
return;
|
|
1389
|
-
}
|
|
1390
|
-
if (!permitWebContinuation(req, res, ctx))
|
|
1391
|
-
return;
|
|
1392
|
-
const now = who();
|
|
1393
|
-
if ("status" in now) {
|
|
1394
|
-
json(res, now.status, { error: now.error });
|
|
1395
|
-
return;
|
|
1396
|
-
}
|
|
1397
|
-
if (!canvasWorkerEnabled(ctx.fleetConfig?.web)) {
|
|
1398
|
-
json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
|
|
1399
|
-
return;
|
|
1400
|
-
}
|
|
1401
|
-
if (now.person !== first.person || now.epoch !== first.epoch) {
|
|
1402
|
-
json(res, 409, { error: "effects_person_changed" });
|
|
1403
|
-
return;
|
|
1404
|
-
}
|
|
1405
|
-
if (path === "/ui/effects/upload") {
|
|
1406
|
-
answer(service.upload(now.person, bytes));
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
let body;
|
|
1410
|
-
try {
|
|
1411
|
-
body = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
1412
|
-
}
|
|
1413
|
-
catch {
|
|
1414
|
-
json(res, 400, { error: "effect_json" });
|
|
1415
|
-
return;
|
|
1416
|
-
}
|
|
1417
|
-
if (path === "/ui/effects/apply")
|
|
1418
|
-
answer(service.apply(now.person, body));
|
|
1419
|
-
else if (path === "/ui/effects/remove")
|
|
1420
|
-
answer(service.remove(now.person, body));
|
|
1421
|
-
else if (path === "/ui/effects/share")
|
|
1422
|
-
answer(service.share(now.person, body));
|
|
1423
|
-
else {
|
|
1424
|
-
if (body && typeof body === "object" && body.kind === "playback" && (url.searchParams.getAll("instance").length !== 1 || slot !== null)) {
|
|
1425
|
-
json(res, 400, { error: "playback_report" });
|
|
1426
|
-
return;
|
|
1427
|
-
}
|
|
1428
|
-
answer(service.report(now.person, idPath[2], body, instance, now.epoch));
|
|
1429
|
-
}
|
|
1430
|
-
})().catch(() => { try {
|
|
1431
|
-
json(res, 500, { error: "effect_request_failed" });
|
|
1432
|
-
}
|
|
1433
|
-
catch { /* response ended */ } });
|
|
1434
|
-
return true;
|
|
1435
|
-
}
|
|
1436
1140
|
/**
|
|
1437
1141
|
* #1629 r4 §4 (r4-5): mods — a person's import (stage, then commit), uninstall and export. Every route is a signed-in
|
|
1438
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
|
|
@@ -1515,18 +1219,13 @@ function handleMods(req, res, url, ctx, path, method) {
|
|
|
1515
1219
|
json(res, 404, { error: "no_such_mod" });
|
|
1516
1220
|
return true;
|
|
1517
1221
|
}
|
|
1518
|
-
// The file is the installed bundle, effects and all
|
|
1519
|
-
//
|
|
1520
|
-
// 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.
|
|
1521
1224
|
const fx = mods.bundleEffects(fileMatch[1]);
|
|
1522
|
-
if (
|
|
1225
|
+
if (fx === "unknown" || (fx?.length ?? 0) > 0) {
|
|
1523
1226
|
json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
|
|
1524
1227
|
return true;
|
|
1525
1228
|
}
|
|
1526
|
-
if (fx === "unknown" || (fx ?? []).some(id => !ctx.modPlacedEffect?.(first.person, id))) {
|
|
1527
|
-
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" });
|
|
1528
|
-
return true;
|
|
1529
|
-
}
|
|
1530
1229
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "X-Content-Type-Options": "nosniff", "Cache-Control": "private, no-store",
|
|
1531
1230
|
"Content-Disposition": `attachment; filename="${rec.name}-${rec.version}.agendmod.json"` });
|
|
1532
1231
|
res.end(text);
|
|
@@ -1650,14 +1349,9 @@ export function exportMod(ctx, person, body) {
|
|
|
1650
1349
|
const store = ctx.themeStore;
|
|
1651
1350
|
const rec = store.get(name);
|
|
1652
1351
|
if (inc.theme === true) {
|
|
1653
|
-
//
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
const effects = (canvasWorkerEnabled(ctx.fleetConfig?.web) ? rec.placements.effects : []).flatMap(p => { const e = ctx.modPlacedEffect?.(person, p.id); if (!e || !filesPlayInChat(e.files)) {
|
|
1657
|
-
omitted++;
|
|
1658
|
-
return [];
|
|
1659
|
-
} return [{ name: e.name, files: e.files, params: p.params ?? {} }]; });
|
|
1660
|
-
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: [] };
|
|
1661
1355
|
}
|
|
1662
1356
|
if (inc.identity === true)
|
|
1663
1357
|
parts.identity = ctx.modCardText?.(name) ?? {};
|
|
@@ -1730,6 +1424,109 @@ export function webPeerOf(req) {
|
|
|
1730
1424
|
const gateway = gatewayRequestContext(req);
|
|
1731
1425
|
return gateway ? (gateway.named === true ? "named_link" : "quick_link") : classifyWebPeer(req);
|
|
1732
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
|
+
}
|
|
1733
1530
|
/**
|
|
1734
1531
|
* #2122: what let this request in, asked now — its session (by handle, under the token it was signed in with) or the
|
|
1735
1532
|
* header token; null when nothing does any more (the gate also refuses a public link that is no longer the current
|
|
@@ -1913,10 +1710,6 @@ function statusTick(ctx, t) {
|
|
|
1913
1710
|
export function handleWebRequest(req, res, url, ctx) {
|
|
1914
1711
|
const path = url.pathname;
|
|
1915
1712
|
const method = req.method ?? "GET";
|
|
1916
|
-
if (!canvasWorkerEnabled(ctx.fleetConfig?.web) && isCanvasWorkerHttpPath(path)) {
|
|
1917
|
-
json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
|
|
1918
|
-
return true;
|
|
1919
|
-
}
|
|
1920
1713
|
// Auth check for all /ui routes. Defence in depth behind the fleet-manager
|
|
1921
1714
|
// gate: the same session cookie or header token, and an unset token closes
|
|
1922
1715
|
// the panel instead of comparing null against a missing credential.
|
|
@@ -2192,7 +1985,10 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2192
1985
|
const guest = guestRequestOf(req);
|
|
2193
1986
|
const messages = !ctx.webChatHistory ? [] : guest ? guestHistory(ctx.webChatHistory, instance, limit, guest, guest.scope())
|
|
2194
1987
|
: visibleMessages(ctx, !!gatewayRequestContext(req), ctx.webChatHistory.list(instance, limit));
|
|
2195
|
-
|
|
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 } : {}) });
|
|
2196
1992
|
return true;
|
|
2197
1993
|
}
|
|
2198
1994
|
// ── #1218: the Log tab ─────────────────────────────────
|
|
@@ -2856,6 +2652,13 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2856
2652
|
json(res, 404, { error: "Preferences are not kept on this fleet" });
|
|
2857
2653
|
return true;
|
|
2858
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
|
+
}
|
|
2859
2662
|
if (gatewayRequestContext(req)) {
|
|
2860
2663
|
json(res, 403, { error: "prefs_not_over_public_link" });
|
|
2861
2664
|
return true;
|
|
@@ -2896,30 +2699,16 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2896
2699
|
return;
|
|
2897
2700
|
}
|
|
2898
2701
|
const personal = [...parsed.patch.keys()].some(isOwnerEffectKey);
|
|
2899
|
-
|
|
2702
|
+
// #2799: a person's own effect selections were Canvas Worker effects' (removed): refused, as before.
|
|
2703
|
+
if (personal) {
|
|
2900
2704
|
json(res, 403, { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE });
|
|
2901
2705
|
return;
|
|
2902
2706
|
}
|
|
2903
|
-
|
|
2904
|
-
json(res, 403, { error: "prefs_need_a_signed_in_person" });
|
|
2905
|
-
return;
|
|
2906
|
-
}
|
|
2907
|
-
// A scope marker rejects a queued old page after a different login, including the same person's new session.
|
|
2908
|
-
// It supplies neither identity nor authority; the current verified session still chooses the bucket.
|
|
2909
|
-
if (personal && body.ownerEpoch !== now.epoch) {
|
|
2910
|
-
json(res, 409, { error: "prefs_person_changed" });
|
|
2911
|
-
return;
|
|
2912
|
-
}
|
|
2913
|
-
const before = prefs.readForViewer(now.person);
|
|
2914
|
-
const written = prefs.writeForViewer(now.person, parsed.patch, { onlyIfUnset: body.onlyIfUnset === true,
|
|
2915
|
-
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" });
|
|
2916
2708
|
if (!written.ok) {
|
|
2917
2709
|
json(res, written.status, { error: written.error });
|
|
2918
2710
|
return;
|
|
2919
2711
|
}
|
|
2920
|
-
const dropped = written.changed.filter(isOwnerEffectKey).flatMap(key => typeof before.values[key] === "string" && before.values[key] !== written.view.values[key] ? [before.values[key]] : []);
|
|
2921
|
-
if (dropped.length)
|
|
2922
|
-
ctx.effectService?.collectOwnerReplaced(dropped);
|
|
2923
2712
|
if (written.changed.length)
|
|
2924
2713
|
ctx.emitSseEvent("status", ctx.getUiStatus());
|
|
2925
2714
|
json(res, 200, { ...written.view, ...prefsScope(now), changed: written.changed });
|
|
@@ -2945,19 +2734,11 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2945
2734
|
// #1629 r4-5: mods — a person's import, uninstall and export (handleMods: sessions only, never the public link).
|
|
2946
2735
|
if (path === "/ui/mods" || path.startsWith("/ui/mods/"))
|
|
2947
2736
|
return handleMods(req, res, url, ctx, path, method);
|
|
2948
|
-
// Posters remain behind the common session gate, with a closed GET-only public manifest.
|
|
2949
|
-
if (path === "/ui/effect-posters" || path.startsWith("/ui/effect-posters/"))
|
|
2950
|
-
return handleEffectPosters(res, url, ctx, path, method);
|
|
2951
|
-
// Canvas effects are absent from the public gateway manifest and independently refused above.
|
|
2952
|
-
if (path === "/ui/effect-trials" || path.startsWith("/ui/effect-trials/"))
|
|
2953
|
-
return handleEffectTrials(req, res, url, ctx, path, method);
|
|
2954
2737
|
// #2233: backend sign-in — absent from the public manifest and the guest table, and refused here too.
|
|
2955
2738
|
if (path === "/ui/browser-backend-signins")
|
|
2956
2739
|
return handleBrowserBackendSignIns(req, res, url, ctx);
|
|
2957
2740
|
if (path === "/ui/backend-login" || path.startsWith("/ui/backend-login/"))
|
|
2958
2741
|
return handleBackendLogin(req, res, url, ctx, path, method);
|
|
2959
|
-
if (path === "/ui/effects" || path.startsWith("/ui/effects/"))
|
|
2960
|
-
return handleEffects(req, res, url, ctx, path, method);
|
|
2961
2742
|
// #1386: Acknowledge a "Needs you" delivery item — a session write (CSRF, checked by the gate) or the CLI's header
|
|
2962
2743
|
// token. Who did it is recorded by principal: the session's public handle, never the session id or a credential.
|
|
2963
2744
|
if (method === "POST" && path === "/ui/needs/ack") {
|
|
@@ -3052,6 +2833,21 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
3052
2833
|
return true;
|
|
3053
2834
|
}
|
|
3054
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
|
+
}
|
|
3055
2851
|
const arrival = webAuthorityOf(req, url, ctx); // #1939 §3: on arrival, and again after the body
|
|
3056
2852
|
(async () => {
|
|
3057
2853
|
let body;
|
|
@@ -3081,9 +2877,39 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
3081
2877
|
const person = webPersonOr401(req, res, url, ctx); // #1827: asked again, now the body is in
|
|
3082
2878
|
if (!person)
|
|
3083
2879
|
return;
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
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
|
+
}
|
|
3087
2913
|
})().catch(err => {
|
|
3088
2914
|
ctx.logger.error({ err: err.message }, "Web command failed");
|
|
3089
2915
|
try {
|
|
@@ -3708,11 +3534,15 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
3708
3534
|
// #2478 (the user's request): over the public link too, unless web.search.public_link is false. Asked on arrival and
|
|
3709
3535
|
// again before the answer is sent.
|
|
3710
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
|
+
}
|
|
3711
3543
|
const refusal = () => {
|
|
3712
3544
|
if (gatewayRequestContext(req) && ctx.fleetConfig?.web?.search?.public_link === false)
|
|
3713
3545
|
return [404, "search_not_over_public_link"];
|
|
3714
|
-
if (guestRequestOf(req))
|
|
3715
|
-
return [403, "guest_forbidden"];
|
|
3716
3546
|
if (ctx.fleetConfig?.web?.search?.enabled === false || !ctx.searchIndex || ctx.searchIndex.status() === "unavailable")
|
|
3717
3547
|
return [404, "search_unavailable"];
|
|
3718
3548
|
return null;
|
|
@@ -4123,7 +3953,7 @@ function handleSendMessage(req, res, ctx) {
|
|
|
4123
3953
|
let quoted;
|
|
4124
3954
|
if (replyTo) {
|
|
4125
3955
|
quoted = ctx.webChatHistory?.find(instance, replyTo.boot, replyTo.id);
|
|
4126
|
-
if (!quoted ||
|
|
3956
|
+
if (!quoted || (guest && !guestSeesMessage(quoted, guest, guest.scope()))) {
|
|
4127
3957
|
json(res, 409, { error: t("web.reply_gone"), reason: "reply_gone" });
|
|
4128
3958
|
return;
|
|
4129
3959
|
}
|
|
@@ -4535,6 +4365,64 @@ function serveMediaRange(req, res, files, id, file) {
|
|
|
4535
4365
|
res.writeHead(ranged ? 206 : 200);
|
|
4536
4366
|
res.end(got.bytes);
|
|
4537
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
|
+
}
|
|
4538
4426
|
function prefsViewer(req, url, ctx) {
|
|
4539
4427
|
const v = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
4540
4428
|
if (v.kind !== "allow")
|
|
@@ -4543,6 +4431,12 @@ function prefsViewer(req, url, ctx) {
|
|
|
4543
4431
|
// it (#2094) read the host's preferences unfiltered: the sidebar's order and favourites, naming instances out of its
|
|
4544
4432
|
// scope. Its account (phase 3) names it on its page.
|
|
4545
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
|
+
}
|
|
4546
4440
|
if (!gatewayRequestContext(req) && v.via === "session" && v.session) {
|
|
4547
4441
|
const person = webPersonKey(v.session.owner);
|
|
4548
4442
|
return { person, epoch: v.session.handle, scope: person === OPERATOR ? "operator" : "person", canWrite: v.session.tier === "admin", via: "session", ...guest };
|
|
@@ -4553,7 +4447,10 @@ const prefsScope = (v) => ({ ownerScope: v?.scope ?? "none", ownerEpoch: v?.epoc
|
|
|
4553
4447
|
// #1920: a guest reads the host's look and its own, never a preference that names an instance (guestPrefs).
|
|
4554
4448
|
const prefsView = (prefs, v) => {
|
|
4555
4449
|
const view = { ...prefs.readForViewer(v?.person ?? null), ...prefsScope(v) };
|
|
4556
|
-
|
|
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);
|
|
4557
4454
|
};
|
|
4558
4455
|
function shellHtmlTag(ctx, mode, viewer) {
|
|
4559
4456
|
if (mode !== "full" || !ctx.webPrefs)
|
|
@@ -4572,20 +4469,12 @@ function shellOwnerCss(ctx, mode, viewer) {
|
|
|
4572
4469
|
}
|
|
4573
4470
|
function guestBodyAttrs(ctx, owner, attr) {
|
|
4574
4471
|
const badge = ctx.guestBadge?.(owner);
|
|
4575
|
-
|
|
4576
|
-
}
|
|
4577
|
-
/** A separate session fence for Chat playback: public/guest viewers acquire no owner-pref identity. */
|
|
4578
|
-
function shellEffect(req, ctx, mode) {
|
|
4579
|
-
if (mode !== "full" || typeof ctx.effectBoot !== "string" || !/^[0-9a-f]{32}$/.test(ctx.effectBoot))
|
|
4580
|
-
return null;
|
|
4581
|
-
const v = evaluateWebRequest(req, new URL(req.url ?? "/", "http://localhost"), ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
4582
|
-
if (v.kind !== "allow" || v.via !== "session" || !v.session || !/^[a-f0-9]{16}$/.test(v.session.handle))
|
|
4583
|
-
return null;
|
|
4584
|
-
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)}"` : ""}`;
|
|
4585
4474
|
}
|
|
4586
4475
|
function shellBodyTag(req, ctx, mode, viewer) {
|
|
4587
4476
|
const p = shellPreview(req, ctx, mode);
|
|
4588
|
-
const ownerCss = shellOwnerCss(ctx, mode, viewer)
|
|
4477
|
+
const ownerCss = shellOwnerCss(ctx, mode, viewer);
|
|
4589
4478
|
let ownerCssState = mode === "full" && (gatewayRequestContext(req) || viewer?.guest) ? "off" : "unknown";
|
|
4590
4479
|
if (mode === "full" && !viewer?.guest && viewer?.person != null && ctx.ownerCss) {
|
|
4591
4480
|
ownerCssState = ctx.ownerCss.effectState(viewer.person);
|
|
@@ -4594,17 +4483,15 @@ function shellBodyTag(req, ctx, mode, viewer) {
|
|
|
4594
4483
|
}
|
|
4595
4484
|
const attr = (v) => String(v ?? "").replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
|
4596
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)}"`
|
|
4597
|
-
+ `${effect ? ` data-effect-boot="${effect.boot}" data-effect-epoch="${effect.epoch}" data-effect-scope="${effect.scope}"` : ""}`
|
|
4598
4486
|
+ ` data-preview-boot="${attr(p?.previewOrigin ? p.boot : "")}" data-preview-reason="${attr(p ? p.reason : "Previews are not available on this fleet.")}"`
|
|
4599
4487
|
// #1554: no availability asked (the public link, or not the full app) still says why, in the page's language.
|
|
4600
4488
|
+ ` data-preview-reason-code="${attr(p ? p.code ?? "" : gatewayRequestContext(req) ? "publicLink" : "notOffered")}"`
|
|
4601
4489
|
// #1723: why the public link's preview tunnel failed (a fixed reason the page words), when it did.
|
|
4602
4490
|
+ `${p?.detail ? ` data-preview-detail="${attr(p.detail)}"` : ""}`
|
|
4603
4491
|
// #1523 N3: whether the fleet serves /api/ai-usage (web.usage_panel), for every page's ◔ — never to a guest (#1920).
|
|
4604
|
-
+ ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false
|
|
4492
|
+
+ ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false ? "0" : "1"}"` // #2855: guests' too
|
|
4605
4493
|
// #1218: whether the Log tab is on (web.log_tab; off by default): the tab says so and offers to turn it on.
|
|
4606
4494
|
+ ` data-log-tab="${ctx.fleetConfig?.web?.log_tab === true ? "1" : "0"}"`
|
|
4607
|
-
+ ` data-canvas-worker="${canvasWorkerEnabled(ctx.fleetConfig?.web) ? "1" : "0"}"`
|
|
4608
4495
|
// #1937: the upload limits in force for this page (the config's; the public link's own cap on it) — what the chat
|
|
4609
4496
|
// checks before sending a file, and the numbers its words say. The server checks again on every upload.
|
|
4610
4497
|
+ ` data-upload-limits="${attr(JSON.stringify(uploadLimits(ctx.fleetConfig?.web, { publicLink: gatewayRequestContext(req) !== undefined })))}"`
|
|
@@ -4634,10 +4521,7 @@ export function serveAppShell(req, res, ctx, mode) {
|
|
|
4634
4521
|
// A public-link page frames what its exposure's preview names (the quick-tunnel wildcard: its host is not known when
|
|
4635
4522
|
// the page may load); every other page frames exactly the origin this load chose (#1723).
|
|
4636
4523
|
const previewFrames = p && "frameSrc" in p ? p.frameSrc : p?.previewOrigin ? `${p.previewOrigin}/frame` : null;
|
|
4637
|
-
|
|
4638
|
-
const effectOrigin = gatewayRequestContext(req)?.expectedOrigin ?? dashboardOriginFor(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req));
|
|
4639
|
-
const effectFrame = shellEffect(req, ctx, mode) && effectOrigin ? `${effectOrigin}/effect-bootstrap` : null;
|
|
4640
|
-
const frameSrc = [previewFrames, effectFrame].filter(Boolean).join(" ");
|
|
4524
|
+
const frameSrc = previewFrames ?? "";
|
|
4641
4525
|
sendPanelHtml(res, withAppIconLinks(html).replace('<html lang="en">', shellHtmlTag(ctx, mode, viewer)).replace("<body>", shellBodyTag(req, ctx, mode, viewer)), 200, {}, { ...(frameSrc ? { frameSrc } : {}), mediaSrc });
|
|
4642
4526
|
}
|
|
4643
4527
|
catch {
|