@songsid/agend 2.3.0 → 2.3.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/authority/authority-state.d.ts +4 -0
- package/dist/authority/authority-state.js +16 -10
- package/dist/authority/authority-state.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +7 -1
- package/dist/authority/session-authorizations.js +6 -4
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/authority/web-authority.d.ts +19 -6
- package/dist/authority/web-authority.js +22 -8
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/muse.d.ts +20 -2
- package/dist/backend/muse.js +154 -16
- package/dist/backend/muse.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +33 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +1 -1
- package/dist/channel/adapters/telegram.js +3 -35
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +5 -5
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +11 -0
- package/dist/cli.js +6 -3
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +42 -5
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +47 -3
- package/dist/daemon.js +380 -60
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +23 -1
- package/dist/delivery-outbox.js +114 -17
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +9 -0
- package/dist/doctor.js.map +1 -1
- package/dist/event-log.d.ts +5 -0
- package/dist/event-log.js +13 -0
- package/dist/event-log.js.map +1 -1
- package/dist/fleet-context.d.ts +1 -1
- package/dist/fleet-lock.d.ts +6 -0
- package/dist/fleet-lock.js +19 -0
- package/dist/fleet-lock.js.map +1 -1
- package/dist/fleet-manager.d.ts +174 -2
- package/dist/fleet-manager.js +678 -68
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.d.ts +1 -0
- package/dist/fleet-process-handlers.js +9 -1
- package/dist/fleet-process-handlers.js.map +1 -1
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +11 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +4 -4
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +7 -7
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +109 -57
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +110 -58
- package/dist/guest-seen-people.d.ts +38 -0
- package/dist/guest-seen-people.js +78 -0
- package/dist/guest-seen-people.js.map +1 -0
- package/dist/instance-config-impact.js +4 -1
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +2 -0
- package/dist/instance-lifecycle.js +27 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +62 -12
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -0
- package/dist/log-service.js.map +1 -1
- package/dist/mac-auto-restore.d.ts +117 -0
- package/dist/mac-auto-restore.js +294 -0
- package/dist/mac-auto-restore.js.map +1 -0
- package/dist/needs-you.js +1 -0
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +2 -2
- package/dist/outbound-handlers.js +5 -5
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +2 -0
- package/dist/outbound-schemas.js +2 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +9 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/search/search-index.d.ts +10 -2
- package/dist/search/search-index.js +17 -4
- package/dist/search/search-index.js.map +1 -1
- package/dist/service-installer.d.ts +23 -1
- package/dist/service-installer.js +40 -11
- package/dist/service-installer.js.map +1 -1
- package/dist/session-oom-stop.d.ts +45 -0
- package/dist/session-oom-stop.js +259 -0
- package/dist/session-oom-stop.js.map +1 -0
- package/dist/session-oom.d.ts +13 -0
- package/dist/session-oom.js +119 -7
- package/dist/session-oom.js.map +1 -1
- package/dist/settings-api.d.ts +27 -1
- package/dist/settings-api.js +108 -2
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +37 -3
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +5 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/spawn-gate.d.ts +2 -0
- package/dist/spawn-gate.js +17 -4
- package/dist/spawn-gate.js.map +1 -1
- package/dist/start-attempt.d.ts +15 -1
- package/dist/start-attempt.js +35 -5
- package/dist/start-attempt.js.map +1 -1
- package/dist/start-failure-chat.d.ts +41 -0
- package/dist/start-failure-chat.js +159 -0
- package/dist/start-failure-chat.js.map +1 -0
- package/dist/start-failure-notice.d.ts +4 -0
- package/dist/start-failure-notice.js +8 -0
- package/dist/start-failure-notice.js.map +1 -1
- package/dist/start-failure.d.ts +49 -0
- package/dist/start-failure.js +141 -1
- package/dist/start-failure.js.map +1 -1
- package/dist/telegram-api-root.d.ts +1 -0
- package/dist/telegram-api-root.js +40 -0
- package/dist/telegram-api-root.js.map +1 -0
- package/dist/turn-reply-guard.d.ts +25 -0
- package/dist/turn-reply-guard.js +111 -12
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +21 -3
- package/dist/ui/backend-signin.js +5 -2
- package/dist/ui/chat-ambient.js +1 -1
- package/dist/ui/chat-effects.js +4 -4
- package/dist/ui/chat-render.js +5 -2
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/chat-thread.js +2 -2
- package/dist/ui/first-run.js +2 -2
- package/dist/ui/fleet-canvas-view.js +114 -0
- package/dist/ui/fleet-canvas.js +15 -77
- package/dist/ui/fleet-office-editor.js +80 -5
- package/dist/ui/fleet-office-layout.js +197 -0
- package/dist/ui/fleet-office-model.js +6 -3
- package/dist/ui/fleet-office-pack.d.ts +39 -0
- package/dist/ui/fleet-office-pack.js +240 -29
- package/dist/ui/fleet-office-skin.js +130 -21
- package/dist/ui/fleet-office-store.js +75 -4
- package/dist/ui/fleet-office-view.js +154 -0
- package/dist/ui/fleet-office-zones.js +95 -33
- package/dist/ui/fleet-office.js +530 -94
- package/dist/ui/fleet-usage.js +7 -6
- package/dist/ui/panel-chat.js +20 -7
- package/dist/ui/panel-details.js +2 -2
- package/dist/ui/panel-fleet.js +11 -77
- package/dist/ui/panel-log.js +5 -5
- package/dist/ui/panel-settings.js +586 -164
- package/dist/ui/search-dialog.js +26 -7
- package/dist/ui/settings-appearance.js +82 -29
- package/dist/ui/settings-apply.js +42 -7
- package/dist/ui/settings-dialogs.js +68 -22
- package/dist/ui/settings-draft.js +51 -0
- package/dist/ui/settings-guests.js +244 -78
- package/dist/ui/settings-learn.js +2 -2
- package/dist/ui/settings-model.js +2 -0
- package/dist/ui/settings-registry.js +142 -0
- package/dist/ui/settings-strings.js +108 -44
- package/dist/ui/settings-web-authority.js +34 -23
- package/dist/ui/shared/app-i18n.js +14 -0
- package/dist/ui/shared/app-nav.js +10 -5
- package/dist/ui/shared/app-prefs.js +80 -2
- package/dist/ui/shared/app-route.js +39 -5
- package/dist/ui/shared/app-shell.js +80 -8
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +251 -20
- package/dist/ui/shared/app.js +3 -0
- package/dist/ui/shared/effect-stock-trust.js +1 -1
- package/dist/ui/shared/identity-color.js +4 -4
- package/dist/ui/shared/signin.js +2 -2
- package/dist/ui/shared/ui-dialog.js +6 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/usage-dialog.js +21 -11
- package/dist/ui/shared/view-strings.js +2 -0
- package/dist/ui/shared/web-authorize.js +13 -12
- package/dist/web-api.d.ts +9 -3
- package/dist/web-api.js +161 -6
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +7 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-experimental.d.ts +1 -1
- package/dist/web-experimental.js +1 -1
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-mod-agent.js +2 -2
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-office-skins.d.ts +108 -0
- package/dist/web-office-skins.js +298 -0
- package/dist/web-office-skins.js.map +1 -0
- package/dist/web-office-store.d.ts +8 -2
- package/dist/web-office-store.js +37 -7
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +6 -0
- package/dist/web-settings-patch.js +61 -4
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +4 -2
- package/dist/web-shell-routes.js +17 -7
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/working-recovery.d.ts +104 -0
- package/dist/working-recovery.js +263 -0
- package/dist/working-recovery.js.map +1 -0
- package/dist/working-state-journal.d.ts +70 -0
- package/dist/working-state-journal.js +106 -0
- package/dist/working-state-journal.js.map +1 -0
- package/package.json +1 -1
|
@@ -52,6 +52,7 @@ register("view", {
|
|
|
52
52
|
"usageFull": "Full",
|
|
53
53
|
"usageFullTitle": "The full card for each login",
|
|
54
54
|
"usageExpand": "Show the full card",
|
|
55
|
+
"usageCachedAt": "cached at {at}",
|
|
55
56
|
"usageCollapse": "Back to one line",
|
|
56
57
|
"usageWindowHours": "{n}h",
|
|
57
58
|
"usageWindowDays": "{n}d",
|
|
@@ -294,6 +295,7 @@ register("view", {
|
|
|
294
295
|
"usageFull": "完整",
|
|
295
296
|
"usageFullTitle": "每個登入顯示完整卡片",
|
|
296
297
|
"usageExpand": "顯示完整卡片",
|
|
298
|
+
"usageCachedAt": "快取於 {at}",
|
|
297
299
|
"usageCollapse": "收回成一行",
|
|
298
300
|
"usageWindowHours": "{n}小時",
|
|
299
301
|
"usageWindowDays": "{n}天",
|
|
@@ -12,7 +12,6 @@ register("webauth", {
|
|
|
12
12
|
authorize: "Authorize this browser",
|
|
13
13
|
waiting: "Waiting for your Confirm in Discord or Telegram ({0})",
|
|
14
14
|
sent: "Sent — confirm it in your DM within 5 minutes.",
|
|
15
|
-
publicLink: "Not on the temporary public link: open the dashboard directly to authorize.",
|
|
16
15
|
reason_disabled: "Web authority is turned off.",
|
|
17
16
|
reason_loopback_only: "Only this computer can authorize, and this browser is not on it.",
|
|
18
17
|
reason_no_account: "This browser was not signed in as an account (agend web): sign in with /web to authorize.",
|
|
@@ -20,7 +19,8 @@ register("webauth", {
|
|
|
20
19
|
reason_not_admin_session: "Guests can't authorize.",
|
|
21
20
|
reason_token_rotated: "The dashboard's token changed: sign in again.",
|
|
22
21
|
reason_public_link: "Not on the temporary public link.",
|
|
23
|
-
reason_named_link_off: "Over your named link: turn on \"Also over my named link\" in Settings →
|
|
22
|
+
reason_named_link_off: "Over your named link: turn on \"Also over my named link\" in Settings → Access & security on this computer.",
|
|
23
|
+
reason_quick_link_off: "Over the temporary public link: turn on \"Also over the temporary public link\" in Settings → Access & security on this computer.",
|
|
24
24
|
reason_not_session: "Sign in to authorize.",
|
|
25
25
|
reason_other: "This browser can't be authorized here.",
|
|
26
26
|
refusal_pending: "A request is already waiting in your DM.",
|
|
@@ -39,7 +39,6 @@ register("webauth", {
|
|
|
39
39
|
authorize: "授權這個瀏覽器",
|
|
40
40
|
waiting: "等你在 Discord 或 Telegram 按下確認({0})",
|
|
41
41
|
sent: "已送出——請在 5 分鐘內到私訊確認。",
|
|
42
|
-
publicLink: "臨時公開連結上無法授權:請直接開啟儀表板再授權。",
|
|
43
42
|
reason_disabled: "網頁授權已關閉。",
|
|
44
43
|
reason_loopback_only: "只有這台電腦可以授權,而這個瀏覽器不在這台電腦上。",
|
|
45
44
|
reason_no_account: "這個瀏覽器不是用帳號登入的(agend web):請用 /web 登入後再授權。",
|
|
@@ -47,7 +46,8 @@ register("webauth", {
|
|
|
47
46
|
reason_not_admin_session: "訪客無法授權。",
|
|
48
47
|
reason_token_rotated: "儀表板的 token 已變更:請重新登入。",
|
|
49
48
|
reason_public_link: "臨時公開連結上無法授權。",
|
|
50
|
-
reason_named_link_off: "透過固定網域:請在這台電腦的 設定 →
|
|
49
|
+
reason_named_link_off: "透過固定網域:請在這台電腦的 設定 → 存取與安全 開啟「也允許透過我的固定網域」。",
|
|
50
|
+
reason_quick_link_off: "透過臨時公開連結:請在這台電腦的 設定 → 存取與安全 開啟「也允許透過臨時公開連結」。",
|
|
51
51
|
reason_not_session: "登入後才能授權。",
|
|
52
52
|
reason_other: "這個瀏覽器在這裡無法授權。",
|
|
53
53
|
refusal_pending: "已經有一個請求在你的私訊中等待。",
|
|
@@ -62,7 +62,9 @@ register("webauth", {
|
|
|
62
62
|
},
|
|
63
63
|
});
|
|
64
64
|
const tn = (k, ...v) => t(`webauth.${k}`, ...v);
|
|
65
|
-
|
|
65
|
+
// The switch and "Only this computer" are changed in Settings on this computer — never over the public link (#2525 review).
|
|
66
|
+
const fixable = (reason) => (reason === "disabled" || reason === "loopback_only") && globalThis.document?.body?.dataset?.publicLink !== "1";
|
|
67
|
+
const REASONS = ["disabled", "loopback_only", "no_account", "not_admin", "not_admin_session", "token_rotated", "public_link", "named_link_off", "quick_link_off", "not_session"];
|
|
66
68
|
// Until when: the time, with the day when it isn't today (an authorization lasts up to 48 h).
|
|
67
69
|
const clock = (ms) => {
|
|
68
70
|
const d = new Date(ms), today = new Date().toDateString() === d.toDateString();
|
|
@@ -70,7 +72,6 @@ const clock = (ms) => {
|
|
|
70
72
|
: d.toLocaleString([], { weekday: "short", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
71
73
|
};
|
|
72
74
|
const mmss = (ms) => { const s = Math.max(0, Math.ceil(ms / 1000)); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; };
|
|
73
|
-
const publicLink = () => globalThis.document?.body?.dataset?.publicLink === "1";
|
|
74
75
|
|
|
75
76
|
// One read per page load, shared by every line that shows it (the composer's, the session menu's): coming back to a chat
|
|
76
77
|
// reads nothing (#1374). It is read again only after an ask, and while a card is pending.
|
|
@@ -98,7 +99,9 @@ export function useWebAuthorization() {
|
|
|
98
99
|
const [deadline, setDeadline] = useState(0), [now, setNow] = useState(() => Date.now());
|
|
99
100
|
const alive = useRef(true);
|
|
100
101
|
const load = async () => {
|
|
101
|
-
|
|
102
|
+
// §0.2 / §0.3: over the public link too — the server says whether this browser may be authorized there (its link's
|
|
103
|
+
// option on) or why not; a guest never asks.
|
|
104
|
+
if (guestPage()) return;
|
|
102
105
|
const body = await readStatus();
|
|
103
106
|
if (!alive.current) return;
|
|
104
107
|
if (signedOut) { setSt(null); setNotice(null); return; }
|
|
@@ -144,7 +147,6 @@ export function resetWebAuthorization() { shared = null; answered = false; infli
|
|
|
144
147
|
export function AuthorizeChip() {
|
|
145
148
|
const a = useWebAuthorization();
|
|
146
149
|
if (guestPage()) return null;
|
|
147
|
-
if (publicLink()) return html`<p class="webauth-chip note" data-state="public">${tn("publicLink")}</p>`;
|
|
148
150
|
if (!a.st) return null;
|
|
149
151
|
return html`<div class="webauth-chip" data-state=${a.st.state}>${body(a)}${a.notice ? html` <span class="note" role="status">${a.notice}</span>` : null}</div>`;
|
|
150
152
|
}
|
|
@@ -163,7 +165,6 @@ export function ComposerAuth() {
|
|
|
163
165
|
const tip = useRef("wa-tip-" + Math.random().toString(36).slice(2, 8));
|
|
164
166
|
useEffect(() => { setOpen(false); }, [a.st && a.st.state]); // a new state closes the bubble
|
|
165
167
|
if (guestPage()) return null;
|
|
166
|
-
if (publicLink()) return html`<span class="webauth-row note" data-state="public" title=${tn("publicLink")}>${tn("publicLink")}</span>`;
|
|
167
168
|
if (!a.st) return null;
|
|
168
169
|
const st = a.st, id = tip.current;
|
|
169
170
|
const said = a.notice ? html`<span class="webauth-notice" role="status">${a.notice}</span>` : null;
|
|
@@ -184,8 +185,8 @@ export function ComposerAuth() {
|
|
|
184
185
|
title=${tn("authorizeWhy")} aria-label=${tn("authorizeWhy")} onClick=${a.ask}>${tn("authorizeShort")}</button>${said}</span>`;
|
|
185
186
|
}
|
|
186
187
|
const why = REASONS.includes(st.reason) ? tn(`reason_${st.reason}`) : tn("reason_other");
|
|
187
|
-
return html`<span class="webauth-row" data-state="none"><span class="note" title=${why}>${tn("notVerified")}<span class="sr-only">: ${why}</span></span>${st.reason
|
|
188
|
-
? html` · <a href="/settings/
|
|
188
|
+
return html`<span class="webauth-row" data-state="none"><span class="note" title=${why}>${tn("notVerified")}<span class="sr-only">: ${why}</span></span>${fixable(st.reason)
|
|
189
|
+
? html` · <a href="/settings/access" title=${why}>${tn("settings")}</a>` : null}</span>`;
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
function body(a) {
|
|
@@ -196,5 +197,5 @@ function body(a) {
|
|
|
196
197
|
return html`<span class="note">${tn("notVerified")} ·</span> <button type="button" class="btn btn-sm" onClick=${a.ask}>${tn("authorize")}</button>`;
|
|
197
198
|
const why = REASONS.includes(st.reason) ? tn(`reason_${st.reason}`) : tn("reason_other");
|
|
198
199
|
// The fix, where there is one the person can take here: the switch lives in Settings → Web.
|
|
199
|
-
return html`<span class="note">${tn("notVerified")}: ${why}</span>${st.reason
|
|
200
|
+
return html`<span class="note">${tn("notVerified")}: ${why}</span>${fixable(st.reason) ? html` <a href="/settings/access">${tn("settings")}</a>` : null}`;
|
|
200
201
|
}
|
package/dist/web-api.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { ModStore } from "./web-mod-store.js";
|
|
|
9
9
|
import type { WebEffectService } from "./web-effect-service.js";
|
|
10
10
|
import type { ThemeStore } from "./web-theme-store.js";
|
|
11
11
|
import type { OfficeStore, OfficeWriteLimiter } from "./web-office-store.js";
|
|
12
|
+
import { type OfficeSkinStore } from "./web-office-skins.js";
|
|
12
13
|
import type { WebAppearance } from "./web-appearance.js";
|
|
13
14
|
import type { SendOpts } from "./channel/types.js";
|
|
14
15
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
@@ -195,6 +196,10 @@ export interface WebApiContext {
|
|
|
195
196
|
readonly officeStore?: OfficeStore;
|
|
196
197
|
/** How often a person may save the offices (absent: no limit, as in a test without it). */
|
|
197
198
|
readonly officeWrites?: OfficeWriteLimiter;
|
|
199
|
+
/** #2513: the Office view's uploaded skin packs (web-office-skins/); absent: /ui/office-skins is not offered. */
|
|
200
|
+
readonly officeSkins?: OfficeSkinStore;
|
|
201
|
+
/** How often a person may upload or delete a skin pack (absent: no limit). */
|
|
202
|
+
readonly officeSkinWrites?: OfficeWriteLimiter;
|
|
198
203
|
readonly webAppearance?: WebAppearance;
|
|
199
204
|
/** #1629 r4-5: installed mods and a person's staged import. */
|
|
200
205
|
readonly mods?: ModStore;
|
|
@@ -212,7 +217,7 @@ export interface WebApiContext {
|
|
|
212
217
|
} | null;
|
|
213
218
|
/** A fleet.yaml instance (own key) or a ClassicBot room, now. */
|
|
214
219
|
isKnownInstance?(name: string): boolean;
|
|
215
|
-
startInstance(name: string, config: unknown, topicMode: boolean): Promise<void>;
|
|
220
|
+
startInstance(name: string, config: unknown, topicMode: boolean, kind?: "fleet-topic" | "classic", resumePaused?: boolean): Promise<void>;
|
|
216
221
|
stopInstance(name: string): Promise<{
|
|
217
222
|
backgroundWorkEnded: boolean;
|
|
218
223
|
}>;
|
|
@@ -402,8 +407,9 @@ export interface WebPerson {
|
|
|
402
407
|
readonly platformName?: string;
|
|
403
408
|
}
|
|
404
409
|
/**
|
|
405
|
-
* #1939 §3.4 / #2453 §0.2: where this request came from — the
|
|
406
|
-
*
|
|
410
|
+
* #1939 §3.4 / #2453 §0.2 / §0.3: where this request came from — over the public gateway, the link itself by the
|
|
411
|
+
* gateway's own provenance (never a header): the admin's named tunnel on its exact hostname, else the temporary
|
|
412
|
+
* quick-tunnel link. Off the gateway, what the TCP peer and Host say.
|
|
407
413
|
*/
|
|
408
414
|
export declare function webPeerOf(req: IncomingMessage): WebPeer;
|
|
409
415
|
export declare function handleWebRequest(req: IncomingMessage, res: ServerResponse, url: URL, ctx: WebApiContext): boolean;
|
package/dist/web-api.js
CHANGED
|
@@ -15,6 +15,7 @@ import { AGENT_WIDGET_SLOTS, checkWidgetData, checkWidgetSpec } from "./web-widg
|
|
|
15
15
|
import { checkOwnerCss, ownerCssAssetRefs } from "./web-owner-css.js";
|
|
16
16
|
import { canonicalText } from "./web-theme.js";
|
|
17
17
|
import { EFFECT_WIRE_LIMIT } from "./web-effect-upload.js";
|
|
18
|
+
import { SKIN_WIRE_BYTES } from "./web-office-skins.js";
|
|
18
19
|
import { gatewayRequestContext } from "./web-request-context.js";
|
|
19
20
|
import { crossInstanceVisibility } from "./cross-instance-notice.js";
|
|
20
21
|
import { permitWebContinuation } from "./web-continuation.js";
|
|
@@ -447,6 +448,141 @@ function handleAppIcon(req, res, url, ctx, method) {
|
|
|
447
448
|
});
|
|
448
449
|
return true;
|
|
449
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* #2513: the Office view's skin packs (web-office-skins.ts). The reads are the offices' (GET /ui/offices): whoever may
|
|
453
|
+
* open the org chart, the public link too (its manifest lists them; a guest's routes do not): the list, a pack's
|
|
454
|
+
* manifest (?v=<sha>: immutable, else no-store) and its pictures (by the sha of their bytes: immutable, image/png,
|
|
455
|
+
* nosniff, sandboxed). The writes are PUT /ui/offices's: an admin session only — never the CLI token, never the public
|
|
456
|
+
* link (absent from its manifest, and refused here too) — asked again once the body is in; rate limited per person.
|
|
457
|
+
* Every page hears `office_skins` on the stream and reads the list again.
|
|
458
|
+
*/
|
|
459
|
+
function handleOfficeSkins(req, res, url, ctx, path, method) {
|
|
460
|
+
const store = ctx.officeSkins;
|
|
461
|
+
if (!store) {
|
|
462
|
+
json(res, 404, { error: "Skin packs are not kept on this fleet" });
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
const one = /^\/ui\/office-skins\/([A-Za-z0-9_-]{1,40})$/.exec(path);
|
|
466
|
+
const picture = /^\/ui\/office-skins\/([A-Za-z0-9_-]{1,40})\/img\/([0-9a-f]{32})\.png$/.exec(path);
|
|
467
|
+
if (method === "GET" || method === "HEAD") {
|
|
468
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
469
|
+
if (path === "/ui/office-skins") {
|
|
470
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
471
|
+
json(res, 200, store.list());
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
if (one) {
|
|
475
|
+
const pack = store.get(one[1]);
|
|
476
|
+
if (!pack) {
|
|
477
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
478
|
+
json(res, 404, { error: "skin_unknown" });
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
const v = url.searchParams.get("v");
|
|
482
|
+
res.setHeader("Cache-Control", v === pack.sha ? "private, max-age=31536000, immutable" : "private, no-store");
|
|
483
|
+
json(res, 200, pack);
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
if (picture) {
|
|
487
|
+
const bytes = store.picture(picture[1], picture[2]);
|
|
488
|
+
if (!bytes) {
|
|
489
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
490
|
+
json(res, 404, { error: "No such image" });
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
res.setHeader("Content-Type", "image/png");
|
|
494
|
+
// Keyed by the sha of its bytes: a picture at this URL never changes.
|
|
495
|
+
res.setHeader("Cache-Control", "private, max-age=31536000, immutable");
|
|
496
|
+
res.setHeader("Content-Security-Policy", "default-src 'none'; sandbox");
|
|
497
|
+
res.writeHead(200);
|
|
498
|
+
res.end(method === "HEAD" ? undefined : bytes);
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
json(res, 404, { error: "not found" });
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
const upload = method === "POST" && path === "/ui/office-skins", removal = method === "DELETE" && !!one;
|
|
505
|
+
if (!upload && !removal) {
|
|
506
|
+
json(res, 404, { error: "not found" });
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
if (gatewayRequestContext(req)) {
|
|
510
|
+
json(res, 403, { error: "skins_not_over_public_link" });
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
const who = () => {
|
|
514
|
+
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
515
|
+
if (verdict.kind !== "allow")
|
|
516
|
+
return { status: 401 };
|
|
517
|
+
return verdict.via === "session" && verdict.session?.tier === "admin" ? { person: verdict.session.owner ? webPersonKey(verdict.session.owner) : "admin-session" } : { status: 403 };
|
|
518
|
+
};
|
|
519
|
+
const refuse = (status) => json(res, status, { error: status === 401 ? WEB_TOKEN_INVALID_MESSAGE : "skins_need_an_admin_session" });
|
|
520
|
+
const first = who();
|
|
521
|
+
if ("status" in first) {
|
|
522
|
+
refuse(first.status);
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
const limited = (person) => {
|
|
526
|
+
const allowed = ctx.officeSkinWrites ? ctx.officeSkinWrites.check(person) : { ok: true };
|
|
527
|
+
if (allowed.ok)
|
|
528
|
+
return false;
|
|
529
|
+
res.setHeader("Retry-After", String(allowed.retry_after_s));
|
|
530
|
+
json(res, 429, { error: "skins_rate_limited", retry_after_s: allowed.retry_after_s });
|
|
531
|
+
return true;
|
|
532
|
+
};
|
|
533
|
+
const answer = (r, body, person) => {
|
|
534
|
+
if (!r.ok) {
|
|
535
|
+
json(res, r.status, { error: r.error, ...(r.detail ? { detail: r.detail } : {}) });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
ctx.officeSkinWrites?.record(person);
|
|
539
|
+
ctx.emitSseEvent("office_skins", { rev: r.rev });
|
|
540
|
+
json(res, 200, body());
|
|
541
|
+
};
|
|
542
|
+
if (removal) {
|
|
543
|
+
if (limited(first.person))
|
|
544
|
+
return true;
|
|
545
|
+
const r = store.remove(one[1]);
|
|
546
|
+
answer(r, () => ({ rev: r.ok ? r.rev : "" }), first.person);
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
(async () => {
|
|
550
|
+
let bytes;
|
|
551
|
+
try {
|
|
552
|
+
bytes = await readBoundedWebBody(req, SKIN_WIRE_BYTES);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
json(res, 413, { error: "skin_too_large", detail: `at most ${Math.floor(SKIN_WIRE_BYTES / 1024 / 1024)} MiB a request` });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
559
|
+
return;
|
|
560
|
+
const now = who();
|
|
561
|
+
if ("status" in now) {
|
|
562
|
+
refuse(now.status);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
let body;
|
|
566
|
+
try {
|
|
567
|
+
body = JSON.parse(bytes.toString("utf8"));
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
json(res, 400, { error: "Invalid JSON" });
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (limited(now.person))
|
|
574
|
+
return;
|
|
575
|
+
const r = store.add(body, now.person);
|
|
576
|
+
answer(r, () => (r.ok ? { rev: r.rev, pack: r.pack } : null), now.person);
|
|
577
|
+
})().catch(err => {
|
|
578
|
+
ctx.logger.error({ err: err.message }, "Uploading a skin pack failed");
|
|
579
|
+
try {
|
|
580
|
+
json(res, 500, { error: "Uploading a skin pack failed" });
|
|
581
|
+
}
|
|
582
|
+
catch { /* already answered */ }
|
|
583
|
+
});
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
450
586
|
/**
|
|
451
587
|
* #1629 r4 §3: the owner-CSS routes (see their call site). `who()`, asked first and again after any body: the person a
|
|
452
588
|
* signed-in human session is, or the status to answer.
|
|
@@ -1311,11 +1447,13 @@ function webAuthorityOf(req, url, ctx, instance) {
|
|
|
1311
1447
|
peer: webPeerOf(req), ...(instance !== undefined ? { instance } : {}) });
|
|
1312
1448
|
}
|
|
1313
1449
|
/**
|
|
1314
|
-
* #1939 §3.4 / #2453 §0.2: where this request came from — the
|
|
1315
|
-
*
|
|
1450
|
+
* #1939 §3.4 / #2453 §0.2 / §0.3: where this request came from — over the public gateway, the link itself by the
|
|
1451
|
+
* gateway's own provenance (never a header): the admin's named tunnel on its exact hostname, else the temporary
|
|
1452
|
+
* quick-tunnel link. Off the gateway, what the TCP peer and Host say.
|
|
1316
1453
|
*/
|
|
1317
1454
|
export function webPeerOf(req) {
|
|
1318
|
-
|
|
1455
|
+
const gateway = gatewayRequestContext(req);
|
|
1456
|
+
return gateway ? (gateway.named === true ? "named_link" : "quick_link") : classifyWebPeer(req);
|
|
1319
1457
|
}
|
|
1320
1458
|
/**
|
|
1321
1459
|
* #2122: what let this request in, asked now — its session (by handle, under the token it was signed in with) or the
|
|
@@ -2074,6 +2212,8 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2074
2212
|
});
|
|
2075
2213
|
return true;
|
|
2076
2214
|
}
|
|
2215
|
+
if (path === "/ui/office-skins" || path.startsWith("/ui/office-skins/"))
|
|
2216
|
+
return handleOfficeSkins(req, res, url, ctx, path, method);
|
|
2077
2217
|
// ── #1630: avatars and Discord server emoji, same-origin (the page never loads a platform's CDN; img-src stays 'self') ──
|
|
2078
2218
|
const imageMatch = path.match(/^\/ui\/img\/(bot|avatar|emoji)\/([^/]+)$/);
|
|
2079
2219
|
if (method === "GET" && imageMatch) {
|
|
@@ -2625,7 +2765,7 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2625
2765
|
(async () => {
|
|
2626
2766
|
let held = false;
|
|
2627
2767
|
try {
|
|
2628
|
-
await withSpawnHoldReply(async (message) => { held = true; json(res, 202, { held: true, message, instance: name }); }, () => ctx.startInstance(name, config, topicMode ?? false));
|
|
2768
|
+
await withSpawnHoldReply(async (message) => { held = true; json(res, 202, { held: true, message, instance: name }); }, () => ctx.startInstance(name, config, topicMode ?? false, "fleet-topic", true));
|
|
2629
2769
|
ctx.emitSseEvent("status", ctx.getUiStatus());
|
|
2630
2770
|
if (!held)
|
|
2631
2771
|
json(res, 200, { started: name });
|
|
@@ -3268,10 +3408,10 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
3268
3408
|
return [403, "usage_report_not_over_public_link"];
|
|
3269
3409
|
return null;
|
|
3270
3410
|
};
|
|
3271
|
-
// Over the public link, where to turn it on: Settings → Web on the dashboard reached directly (this computer's own
|
|
3411
|
+
// Over the public link, where to turn it on: Settings → Web dashboard on the dashboard reached directly (this computer's own
|
|
3272
3412
|
// address — the public link's Settings only reads). Never the public origin's own Settings, which cannot change it.
|
|
3273
3413
|
const answer = ([status, error]) => json(res, status, {
|
|
3274
|
-
error, ...(status === 403 ? { settings: `http://localhost:${ctx.fleetConfig?.health_port ?? 19280}/settings/web` } : {})
|
|
3414
|
+
error, ...(status === 403 ? { settings: `http://localhost:${ctx.fleetConfig?.health_port ?? 19280}/settings/web-dashboard` } : {})
|
|
3275
3415
|
});
|
|
3276
3416
|
const first = refusal();
|
|
3277
3417
|
if (first) {
|
|
@@ -3866,6 +4006,11 @@ function handleUpload(req, res, url, ctx) {
|
|
|
3866
4006
|
// staging a file never wakes it. A stopped or deleted recipient must not regain an upload id.
|
|
3867
4007
|
const deliverable = () => uploadRecipient(ctx, instance);
|
|
3868
4008
|
const gone = () => drop(() => json(res, 404, { error: `Instance not found: ${instance}` }));
|
|
4009
|
+
// #2366: a guest's scope (and whether guests may chat) is asked again at each commit point — narrowed, revoked or made
|
|
4010
|
+
// read-only while the body was arriving, the upload is refused and its part file removed, as it would have been at
|
|
4011
|
+
// the start. Read now: the scope is never stored (guestRequestOf's scope() is the fleet's guestScope as it is now).
|
|
4012
|
+
const guestNow = () => guestChatRefusal(guestRequestOf(req), instance);
|
|
4013
|
+
const refuseGuest = (r) => drop(() => json(res, r.status, { error: r.error }));
|
|
3869
4014
|
req.on("end", () => {
|
|
3870
4015
|
if (over)
|
|
3871
4016
|
return;
|
|
@@ -3880,6 +4025,11 @@ function handleUpload(req, res, url, ctx) {
|
|
|
3880
4025
|
drop(() => json(res, 415, { error: "the file is empty" }));
|
|
3881
4026
|
return;
|
|
3882
4027
|
}
|
|
4028
|
+
const refusedAtEnd = guestNow();
|
|
4029
|
+
if (refusedAtEnd) {
|
|
4030
|
+
refuseGuest(refusedAtEnd);
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
3883
4033
|
if (!deliverable()) {
|
|
3884
4034
|
gone();
|
|
3885
4035
|
return;
|
|
@@ -3892,6 +4042,11 @@ function handleUpload(req, res, url, ctx) {
|
|
|
3892
4042
|
drop();
|
|
3893
4043
|
return;
|
|
3894
4044
|
}
|
|
4045
|
+
const refusedAtFinish = guestNow(); // #2366: and the guest's scope may have changed in it
|
|
4046
|
+
if (refusedAtFinish) {
|
|
4047
|
+
refuseGuest(refusedAtFinish);
|
|
4048
|
+
return;
|
|
4049
|
+
}
|
|
3895
4050
|
if (!deliverable()) {
|
|
3896
4051
|
gone();
|
|
3897
4052
|
return;
|