toolroll 0.9.63 → 0.9.67
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/CHANGELOG.md +108 -0
- package/README.md +33 -5
- package/dist/acp-agents.d.ts +43 -0
- package/dist/acp-agents.js +62 -0
- package/dist/acp-attest.d.ts +44 -0
- package/dist/acp-attest.js +269 -0
- package/dist/acp-check.d.ts +28 -0
- package/dist/acp-check.js +410 -0
- package/dist/acp-client.d.ts +163 -0
- package/dist/acp-client.js +738 -0
- package/dist/acp-inspect.d.ts +23 -0
- package/dist/acp-inspect.js +109 -0
- package/dist/agent-board.d.ts +19 -6
- package/dist/agent-board.js +30 -9
- package/dist/agent-fence.d.ts +117 -2
- package/dist/agent-fence.js +439 -12
- package/dist/agent-pulse.js +9 -2
- package/dist/agent-state-words.d.ts +19 -0
- package/dist/agent-state-words.js +20 -0
- package/dist/agent-state.d.ts +65 -0
- package/dist/agent-state.js +229 -0
- package/dist/agentconfig.js +39 -13
- package/dist/agents-board.d.ts +34 -2
- package/dist/agents-board.js +110 -1
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-delivery.d.ts +14 -0
- package/dist/assignment-delivery.js +13 -0
- package/dist/assignment.js +13 -0
- package/dist/attest.js +7 -0
- package/dist/backup-ui.js +2 -2
- package/dist/batch-checks.js +3 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +94 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +228 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +42 -38
- package/dist/browser-shell.js +6 -3
- package/dist/browser-workspace.d.ts +235 -1
- package/dist/browser-workspace.js +60 -0
- package/dist/builder.d.ts +10 -1
- package/dist/builder.js +87 -24
- package/dist/chat-actions.d.ts +13 -1
- package/dist/chat-actions.js +197 -15
- package/dist/chat-ask.d.ts +4 -0
- package/dist/chat-ask.js +14 -0
- package/dist/chat-channel.js +20 -0
- package/dist/chat-continuity.js +5 -4
- package/dist/chat-decide.d.ts +0 -14
- package/dist/chat-decide.js +4 -1
- package/dist/chat-delivery.d.ts +9 -0
- package/dist/chat-delivery.js +45 -6
- package/dist/chat-messages.d.ts +8 -0
- package/dist/chat-messages.js +36 -4
- package/dist/check-levels-ui.js +2 -2
- package/dist/checkout-proof.d.ts +28 -0
- package/dist/checkout-proof.js +62 -0
- package/dist/claim.d.ts +12 -0
- package/dist/claim.js +19 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +4 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +21 -2
- package/dist/coding-ui.js +6 -4
- package/dist/console-instance.d.ts +124 -0
- package/dist/console-instance.js +337 -0
- package/dist/contracts/chat-actions.d.ts +16 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/chat-content.d.ts +19 -0
- package/dist/contracts/chat-content.js +6 -0
- package/dist/contracts/console-api.d.ts +13 -4
- package/dist/contracts/console-api.js +13 -4
- package/dist/contracts/flow-card.d.ts +4 -0
- package/dist/contracts/flow-card.js +3 -0
- package/dist/contracts/flow.d.ts +46 -0
- package/dist/contracts/flow.js +11 -2
- package/dist/contracts/lead-tools.d.ts +92 -0
- package/dist/contracts/lead-tools.js +13 -0
- package/dist/contracts/provider.d.ts +11 -1
- package/dist/contracts/provider.js +1 -1
- package/dist/contracts/route.d.ts +76 -1
- package/dist/contracts/route.js +1 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/control-ui.js +2 -2
- package/dist/converse.d.ts +16 -0
- package/dist/converse.js +32 -4
- package/dist/demo-chat.js +4 -4
- package/dist/demo.js +2 -2
- package/dist/desktop-update-gate.js +6 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +4 -1
- package/dist/doctor.js +1 -1
- package/dist/egress-bridge.d.ts +17 -0
- package/dist/egress-bridge.js +111 -0
- package/dist/egress.d.ts +119 -0
- package/dist/egress.js +423 -0
- package/dist/exec.d.ts +78 -0
- package/dist/exec.js +367 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-engine.js +24 -5
- package/dist/flow-gallery-ui.js +2 -2
- package/dist/flow-gallery.js +27 -8
- package/dist/flow-share.js +16 -5
- package/dist/flow-triggers.d.ts +77 -8
- package/dist/flow-triggers.js +517 -70
- package/dist/flows-cli.js +8 -2
- package/dist/flows-ui.d.ts +0 -2
- package/dist/flows-ui.js +52 -4
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/invoke.d.ts +35 -0
- package/dist/invoke.js +347 -28
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/lead-capabilities.js +3 -2
- package/dist/lead-contract.js +2 -0
- package/dist/lead-doors.d.ts +2 -0
- package/dist/lead-doors.js +7 -0
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +58 -2
- package/dist/lead-progress.js +1 -0
- package/dist/lead-queue-runtime.d.ts +46 -0
- package/dist/lead-queue-runtime.js +191 -0
- package/dist/lead-queue.d.ts +104 -0
- package/dist/lead-queue.js +187 -0
- package/dist/lead-sweep.d.ts +67 -0
- package/dist/lead-sweep.js +99 -0
- package/dist/lead-tools.d.ts +22 -0
- package/dist/lead-tools.js +152 -1
- package/dist/lead.d.ts +13 -5
- package/dist/lead.js +89 -29
- package/dist/limits-ui.d.ts +8 -1
- package/dist/limits-ui.js +26 -0
- package/dist/live-bus.d.ts +8 -0
- package/dist/live-bus.js +40 -4
- package/dist/live-detail.d.ts +27 -0
- package/dist/live-detail.js +57 -0
- package/dist/live-notes.d.ts +20 -0
- package/dist/live-notes.js +47 -0
- package/dist/live-records.d.ts +140 -0
- package/dist/live-records.js +935 -0
- package/dist/live-terminal.d.ts +269 -0
- package/dist/live-terminal.js +686 -0
- package/dist/live.d.ts +38 -27
- package/dist/live.js +142 -124
- package/dist/mcp-oauth.d.ts +6 -3
- package/dist/mcp-oauth.js +8 -4
- package/dist/memory-pass.js +2 -2
- package/dist/mobile-viewport.d.ts +5 -1
- package/dist/mobile-viewport.js +18 -2
- package/dist/model-catalog.js +4 -0
- package/dist/monitoring-ui.js +2 -2
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +11 -4
- package/dist/operate.d.ts +4 -0
- package/dist/operate.js +335 -19
- package/dist/passwordless.d.ts +23 -0
- package/dist/passwordless.js +26 -0
- package/dist/peek-cli.d.ts +12 -4
- package/dist/peek-cli.js +19 -11
- package/dist/phase-routing.js +10 -3
- package/dist/planner-source.d.ts +3 -0
- package/dist/planner-source.js +14 -0
- package/dist/planner.d.ts +2 -1
- package/dist/planner.js +7 -1
- package/dist/policy-ui.js +2 -2
- package/dist/policy.d.ts +1 -20
- package/dist/policy.js +29 -2
- package/dist/project-delete-ui.d.ts +18 -0
- package/dist/project-delete-ui.js +60 -9
- package/dist/project-delete.js +2 -2
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +5 -3
- package/dist/project-tools.js +12 -9
- package/dist/provider-auth.d.ts +4 -3
- package/dist/provider-auth.js +5 -4
- package/dist/provider-connection.d.ts +2 -1
- package/dist/provider-connection.js +5 -0
- package/dist/provider.d.ts +25 -2
- package/dist/provider.js +63 -9
- package/dist/pull-request-ui.js +2 -2
- package/dist/push.js +1 -1
- package/dist/quick-start.d.ts +24 -0
- package/dist/quick-start.js +127 -0
- package/dist/release-adapters.d.ts +6 -0
- package/dist/release-adapters.js +70 -3
- package/dist/release.d.ts +10 -1
- package/dist/release.js +34 -15
- package/dist/request-budget-ui.js +2 -2
- package/dist/result-follow-ups.js +3 -2
- package/dist/result-review.d.ts +1 -1
- package/dist/result-review.js +9 -3
- package/dist/retention-ui.js +2 -2
- package/dist/retention.js +10 -1
- package/dist/run-efficiency.d.ts +73 -0
- package/dist/run-efficiency.js +228 -0
- package/dist/runner.d.ts +5 -1
- package/dist/runner.js +22 -1
- package/dist/scope.d.ts +183 -5
- package/dist/scope.js +256 -31
- package/dist/scout-net.d.ts +4 -1
- package/dist/scout-net.js +37 -12
- package/dist/scout.d.ts +20 -6
- package/dist/scout.js +27 -3
- package/dist/serve.js +248 -40
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +16 -2
- package/dist/server/chrome.js +63 -11
- package/dist/server/console.css +107 -42
- package/dist/server/flows.js +38 -8
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +23 -9
- package/dist/server/http.d.ts +7 -0
- package/dist/server/http.js +3 -2
- package/dist/server/live.d.ts +34 -1
- package/dist/server/live.js +39 -17
- package/dist/server/pages-assets.js +5 -0
- package/dist/server/pages.js +90 -13
- package/dist/server/people-tokens.d.ts +3 -0
- package/dist/server/people-tokens.js +55 -17
- package/dist/server/remote-hooks.js +12 -6
- package/dist/server/remote.d.ts +2 -1
- package/dist/server/remote.js +14 -5
- package/dist/server/render-chat.d.ts +12 -3
- package/dist/server/render-chat.js +47 -16
- package/dist/server/render-pages.d.ts +32 -4
- package/dist/server/render-pages.js +83 -13
- package/dist/server/render-people.d.ts +3 -2
- package/dist/server/render-people.js +3 -2
- package/dist/server/render-results.d.ts +18 -2
- package/dist/server/render-results.js +37 -28
- package/dist/server/render-settings.js +6 -5
- package/dist/server/render-tasks.d.ts +35 -10
- package/dist/server/render-tasks.js +111 -27
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.js +2 -1
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +47 -5
- package/dist/server/runtime.d.ts +33 -2
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +142 -10
- package/dist/server/sweeps.d.ts +4 -0
- package/dist/server/sweeps.js +58 -0
- package/dist/server/task-views.d.ts +2 -1
- package/dist/server/task-views.js +96 -6
- package/dist/server/tasks.js +327 -78
- package/dist/server/terminals.d.ts +31 -0
- package/dist/server/terminals.js +365 -0
- package/dist/setup-guide.js +3 -2
- package/dist/slack-chat.js +4 -1
- package/dist/spend-ui.js +3 -3
- package/dist/spend.js +2 -1
- package/dist/steer-capabilities.d.ts +32 -0
- package/dist/steer-capabilities.js +60 -0
- package/dist/steer-controls.d.ts +14 -0
- package/dist/steer-controls.js +36 -0
- package/dist/steer-status.js +2 -2
- package/dist/storage-ui.js +4 -4
- package/dist/store-baseline.d.ts +1 -1
- package/dist/store-baseline.js +1 -1
- package/dist/store.d.ts +440 -15
- package/dist/store.js +1072 -52
- package/dist/subagent-desk.js +1 -1
- package/dist/summary.js +3 -0
- package/dist/task-source.d.ts +58 -0
- package/dist/task-source.js +93 -0
- package/dist/team-leads.js +1 -1
- package/dist/teams-chat.js +4 -1
- package/dist/teardown-git.d.ts +22 -0
- package/dist/teardown-git.js +136 -2
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +44 -7
- package/dist/telegram.d.ts +11 -2
- package/dist/telegram.js +51 -17
- package/dist/terminal-catalog.d.ts +11 -0
- package/dist/terminal-catalog.js +129 -0
- package/dist/text-limits.d.ts +2 -0
- package/dist/text-limits.js +2 -0
- package/dist/tool-broker.d.ts +14 -0
- package/dist/tool-broker.js +67 -2
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +13 -3
- package/dist/tools-ui.js +2 -2
- package/dist/unfenced-test-support.d.ts +2 -0
- package/dist/unfenced-test-support.js +11 -0
- package/dist/webhook-limits.d.ts +23 -0
- package/dist/webhook-limits.js +30 -0
- package/dist/webhook-readers.d.ts +56 -0
- package/dist/webhook-readers.js +138 -0
- package/dist/webhook-signing.d.ts +27 -0
- package/dist/webhook-signing.js +119 -0
- package/dist/work-index.d.ts +29 -0
- package/dist/work-index.js +16 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/writes-mode.d.ts +9 -0
- package/dist/writes-mode.js +8 -0
- package/package.json +15 -2
package/dist/server/live.js
CHANGED
|
@@ -10,8 +10,8 @@ import { respond } from "./http.js";
|
|
|
10
10
|
import { adapterPolicy } from "./route-policy.js";
|
|
11
11
|
import { requestContext } from "./request-context.js";
|
|
12
12
|
/** A page asks for a few rooms at once: its workspace, what it shows, its conversation. */
|
|
13
|
-
export const LIVE_ROOMS_PER_PAGE =
|
|
14
|
-
const ROOM_NAME = /^(workspace|task|flow|chat|team|agent)(?::([^?]{1,400}))?(?:\?([^#]{0,600}))?$/;
|
|
13
|
+
export const LIVE_ROOMS_PER_PAGE = 6;
|
|
14
|
+
const ROOM_NAME = /^(workspace|task|flow|chat|team|agent|summary)(?::([^?]{1,400}))?(?:\?([^#]{0,600}))?$/;
|
|
15
15
|
/** At most one workspace nudge a second: a burst of writes is one re-read, the first heard at once. */
|
|
16
16
|
const WORKSPACE_SETTLE_MS = 1_000;
|
|
17
17
|
/** A running reply's preview: at most one snapshot every 80 ms; the last one always goes out. */
|
|
@@ -33,6 +33,7 @@ export function parseLiveRoom(name) {
|
|
|
33
33
|
const only = (...allowed) => [...params.keys()].every(one => allowed.includes(one)) && allowed.every(one => params.getAll(one).length <= 1);
|
|
34
34
|
switch (kind) {
|
|
35
35
|
case "workspace": return key === null && query === undefined ? { kind } : null;
|
|
36
|
+
case "summary": return key === null && query === undefined ? { kind } : null;
|
|
36
37
|
case "task": return key !== null && key !== "" && query === undefined ? { kind, task: key } : null;
|
|
37
38
|
case "flow": {
|
|
38
39
|
const flow = Number(key), card = params.get("card");
|
|
@@ -53,11 +54,34 @@ export function parseLiveRoom(name) {
|
|
|
53
54
|
}
|
|
54
55
|
return null;
|
|
55
56
|
}
|
|
57
|
+
/** The same session, still signed in, from the cookie and the account (never this request's context): a live room's and
|
|
58
|
+
* a live terminal's re-proof. Null when it was signed out, ended elsewhere, revoked, rotated or re-roled. */
|
|
59
|
+
export function rereadBrowser(runtime, request, who) {
|
|
60
|
+
const again = runtime.identify(request, false);
|
|
61
|
+
if (again?.via !== "cookie" || again.name !== who.name || again.session.generation !== who.session.generation)
|
|
62
|
+
return null;
|
|
63
|
+
// The in-memory session may still exist after another process ends the saved session.
|
|
64
|
+
const hash = runtime.sessions.hashOf(again.session);
|
|
65
|
+
const saved = hash === null ? null : runtime.store.webSession(hash), account = runtime.store.accountOf(again.name);
|
|
66
|
+
if (saved === null || account === null || account.revokedAt !== null || account.generation !== again.session.generation
|
|
67
|
+
|| saved.generation !== account.generation || saved.role !== account.role || again.role !== account.role)
|
|
68
|
+
return null;
|
|
69
|
+
return again;
|
|
70
|
+
}
|
|
56
71
|
const json = (response, status, value) => respond(response, status, "application/json", JSON.stringify(value));
|
|
57
72
|
export function createLiveHandlers(runtime, live) {
|
|
58
73
|
const { store, identify, liveCeiling, familyOf, taskRooms, flowRooms, clock, leadPrincipal, taskChatFocus, chatScopeOf, liveTurns, team, visible } = runtime;
|
|
59
74
|
// The reader's workspace: one room for everyone, nudged by any meaningful write or a clock-bound lapse.
|
|
60
75
|
const workspaceRooms = createLiveRooms(() => `${live.workspaceRevision.current()}:${live.workspaceRevision.expiresAt(clock())}`, null, { bus: live.bus, minIntervalMs: WORKSPACE_SETTLE_MS });
|
|
76
|
+
// The reader's project states, per account AND scope: the fingerprint IS the snapshot, so each `change` carries it
|
|
77
|
+
// (`at`). Every delivery re-proves the viewer's scope, so a snapshot read for a wider scope (a cached one, a held
|
|
78
|
+
// one, one a newcomer would be handed on joining) never reaches a narrowed reader: it hears `gone` instead. Reads
|
|
79
|
+
// settle: a burst of writes is one read, while every signal still re-proves at once.
|
|
80
|
+
const summaryKey = (account, scope) => JSON.stringify([account, scope]);
|
|
81
|
+
const summaryRooms = createLiveRooms(key => {
|
|
82
|
+
const [account, scope] = JSON.parse(key);
|
|
83
|
+
return live.projectSummaryFor(account, scope);
|
|
84
|
+
}, null, { bus: live.bus, minIntervalMs: WORKSPACE_SETTLE_MS, readSettleMs: WORKSPACE_SETTLE_MS });
|
|
61
85
|
// A team audience is per actor (and per token): its key carries who reads, the room keeps how to read as them.
|
|
62
86
|
const teamActors = new Map();
|
|
63
87
|
const teamRooms = createLiveRooms(key => {
|
|
@@ -68,19 +92,7 @@ export function createLiveHandlers(runtime, live) {
|
|
|
68
92
|
const chatViewers = new Set();
|
|
69
93
|
// One agent's open view (Agents board): what it shows, keyed by the exact run.
|
|
70
94
|
const agentRooms = createLiveRooms(run => agentRunFingerprint(store, run), null, { bus: live.bus });
|
|
71
|
-
|
|
72
|
-
const readBrowser = (request, who) => {
|
|
73
|
-
const again = identify(request, false);
|
|
74
|
-
if (again?.via !== "cookie" || again.name !== who.name || again.session.generation !== who.session.generation)
|
|
75
|
-
return null;
|
|
76
|
-
// The in-memory session may still exist after another process ends the saved session.
|
|
77
|
-
const hash = runtime.sessions.hashOf(again.session);
|
|
78
|
-
const saved = hash === null ? null : store.webSession(hash), account = store.accountOf(again.name);
|
|
79
|
-
if (saved === null || account === null || account.revokedAt !== null || account.generation !== again.session.generation
|
|
80
|
-
|| saved.generation !== account.generation || saved.role !== account.role || again.role !== account.role)
|
|
81
|
-
return null;
|
|
82
|
-
return again;
|
|
83
|
-
};
|
|
95
|
+
const readBrowser = (request, who) => rereadBrowser(runtime, request, who);
|
|
84
96
|
const sameBrowser = (request, who) => () => readBrowser(request, who) !== null;
|
|
85
97
|
function admitted(viewer, thread) {
|
|
86
98
|
let valid = false;
|
|
@@ -90,7 +102,7 @@ export function createLiveHandlers(runtime, live) {
|
|
|
90
102
|
catch { /* Failed proof stops this room. */ }
|
|
91
103
|
if (!valid) {
|
|
92
104
|
viewer.connection.send(viewer.room, "gone");
|
|
93
|
-
viewer.leave();
|
|
105
|
+
viewer.connection.refuse(() => viewer.leave());
|
|
94
106
|
}
|
|
95
107
|
return valid;
|
|
96
108
|
}
|
|
@@ -184,7 +196,7 @@ export function createLiveHandlers(runtime, live) {
|
|
|
184
196
|
if ([...url.searchParams.keys()].some(key => key !== "room") || names.length === 0 || names.length > LIVE_ROOMS_PER_PAGE || new Set(names).size !== names.length)
|
|
185
197
|
return json(response, 400, { error: "room" });
|
|
186
198
|
const rooms = names.map(name => ({ name, room: parseLiveRoom(name) }));
|
|
187
|
-
if (rooms.some(one => one.room === null) || rooms.filter(one => one.room?.kind === "agent").length > 1 || rooms.filter(one => one.room?.kind === "workspace").length > 1 || rooms.filter(one => one.room?.kind === "chat").length > 1 || rooms.filter(one => one.room?.kind === "team").length > 1)
|
|
199
|
+
if (rooms.some(one => one.room === null) || rooms.filter(one => one.room?.kind === "agent").length > 1 || rooms.filter(one => one.room?.kind === "workspace").length > 1 || rooms.filter(one => one.room?.kind === "chat").length > 1 || rooms.filter(one => one.room?.kind === "team").length > 1 || rooms.filter(one => one.room?.kind === "summary").length > 1)
|
|
188
200
|
return json(response, 400, { error: "room" });
|
|
189
201
|
// Admit every room before the stream opens, with the answers each page's own stream gave.
|
|
190
202
|
const joins = [];
|
|
@@ -195,6 +207,15 @@ export function createLiveHandlers(runtime, live) {
|
|
|
195
207
|
const browser = who;
|
|
196
208
|
joins.push(connection => workspaceRooms.join("workspace", { name: who.name, room: name, connection, valid: sameBrowser(request, browser) }));
|
|
197
209
|
}
|
|
210
|
+
else if (room.kind === "summary") {
|
|
211
|
+
const browser = who;
|
|
212
|
+
const scope = live.projectScopeFor(who.name);
|
|
213
|
+
if (scope === null)
|
|
214
|
+
return json(response, 404, { error: "summary" });
|
|
215
|
+
const signedIn = sameBrowser(request, browser);
|
|
216
|
+
joins.push(connection => summaryRooms.join(summaryKey(who.name, scope), { name: who.name, room: name, connection,
|
|
217
|
+
valid: () => signedIn() && live.projectScopeFor(who.name) === scope }));
|
|
218
|
+
}
|
|
198
219
|
else if (room.kind === "task") {
|
|
199
220
|
const browser = who;
|
|
200
221
|
const family = familyOf(room.task);
|
|
@@ -333,6 +354,7 @@ export function createLiveHandlers(runtime, live) {
|
|
|
333
354
|
close() {
|
|
334
355
|
detachChat();
|
|
335
356
|
workspaceRooms.close();
|
|
357
|
+
summaryRooms.close();
|
|
336
358
|
teamRooms.close();
|
|
337
359
|
agentRooms.close();
|
|
338
360
|
for (const viewer of [...chatViewers])
|
|
@@ -6,6 +6,9 @@ const PWA_ICON_192 = "iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAIAAADdvvtQAAAB1klEQVR42u
|
|
|
6
6
|
const PWA_ICON_512 = "iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAIAAAB7GkOtAAAJF0lEQVR42u3VwQ0AEBBFQXU4KUD/TW0R3JzcRLJhfqYCwivDzMy+XHEEZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmZgJgZmYCYGZmAmBmJgBmZiYAZmYmAGZmJgBmZiYAZmYmAGZmJgDb1dYBOCEAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACAIAAACAAAAgAAAIAgAAAIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACAIAAACAAAAgAAAIAgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIAAACAIAAACAAAAgAAAIAgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIAAACAIAAACAAAAgAAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACcF+Y5Z5HKgACIAAmAAgAAmACgAAgACYACAACYAKAACAAJgAIAAJgAoAAIAAmAAgAAmACIAACgP/FBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQABMABAAARAAEwAEAAEwAUAAEAATAAQAATABQAAQABMABAABMAFAABAAEwABEAAEwARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEQADMBEAABEAAzARAAARAAMwEQAAEAEAABABAAAQAQAAAEAAABAAAAQBAAAAQAAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAANwcgAAAIAAACAAAAgCAAAAgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIAgAAAIAAACAAAAgCAAAAgAAACIAC/C7Pc80gFQAAEwAQAAUAATAAQAATABAABQABMABAABMAEAAFAAEwAEAAEwAQAAUAATAAEQADwv5gACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgAkAAiAAAmACgAAgACYACAACYAKAACAAJgAIAAJgAoAAIAAmAAgAAmACIAACgACYAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAIgJkACIAACICZAAiAAAiAmQAIgAAACIAAAAiAAAAIAAACAIAAACAAAAgAAAIAgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAmwMQAAAEAAABAEAAABAAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAQAQAAEAEAABABAAAAQAAAEAAABAEAAABAAAASAJcxyzyMVAAEQABMABAABMAFAABAAEwAEAAEwAUAAEAATAAQAATABQAAQABMABAABMAEQAAHA/2ICIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgACYACIAACIAJAAKAAJgAIAAIgAkAAoAAmAAgAAiACQACgACYACAACIAJgAAIAAJgAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIgAGYCIAACIABmAiAAAiAAZgIgAAIAIAACACAAAgAgAAAIAAACAIAAACAAAAgAAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAAgAgAAIAIAACACAAbg5AAAAQAAAEAAABAEAAABAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAAQBAAAAQAAAEAAABAEAAABAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABEAAAARAAAAEQAAABcHkAAgCAAAAgAAAIAAACAIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgJmZPT4BMDMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMxMAMzMTADMzEwAzMwEwMzMBMDMzATAzMwEwMzMBMDOzJzYBVJhD+Nnu218AAAAASUVORK5CYII=";
|
|
7
7
|
const PWA_ICON_APPLE = "iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAIAAACyr5FlAAABqklEQVR42u3aMQ0AIAxFwepgQgD+TSECFHQiYWjv5ylobmwcs2ThBAaHwWFwGBwGh8FhcBgcBofBkW7MparBITgEh+AQHIJDcAgOwQEHHHDAAYfgEByCQ3AIDsEhOASHI8IBBxxwCA7BITgEh+AQHIJDcAgOOOCAQ3AIDsEhOASH4BAcguNPu9PggAMOOOCAAw444IADDjjggAMOOOCAAw444IADDjjggAMOOOCAAw44PPsIDsEhOASH4BAcgkNwCA444IADDjgEh+AQHIJDcAgOwSE44IDDg7EHYzjggAMOOOCAAw444IADDjjggAMOOOCAAw444IADDjjggAMOOOCAAw559hEcgkNwwAEHHHDAITgEh+AQHIJDcAgOwQEHHHDAAYfgEByCQ3B4MIYDDjjggAMOOOCAAw444IADDjjggAMOOOCAAw444IADDjjggAMOOODw7OPZBw7BITgEh+AQHIJDcAgOOOCAAw44BIfgEByCQ3AIDsEhOAQHHHDAITgEh+AQHIJDcAgOwSE44IADDjgecVjbwWFwGBwGh8FhcBgcBofBYeV3AaohX51oqNRKAAAAAElFTkSuQmCC";
|
|
8
8
|
const PWA_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="6" y="6" width="88" height="88" rx="14" fill="#1a202c"/><rect x="28" y="28" width="44" height="8" rx="4" fill="#ebebeb"/><rect x="28" y="47" width="44" height="8" rx="4" fill="#ebebeb"/><rect x="28" y="66" width="44" height="8" rx="4" fill="#ebebeb"/></svg>`;
|
|
9
|
+
/** The icon with the attention dot (cockpit §3): a tab whose reader has something waiting shows it. Same origin, so
|
|
10
|
+
* the page's img-src 'self' allows it; nothing about the reader rides the asset. */
|
|
11
|
+
const PWA_ICON_ATTENTION_SVG = PWA_ICON_SVG.replace("</svg>", `<circle cx="78" cy="22" r="20" fill="#bb2649" stroke="#ffffff" stroke-width="6"/></svg>`);
|
|
9
12
|
/** The typefaces by route: exact names only, served pre-auth like the icons. */
|
|
10
13
|
const FONT_FILES = {
|
|
11
14
|
"/fonts/geist-sans-400.woff2": GEIST_SANS_400,
|
|
@@ -87,6 +90,8 @@ export function serveInstallAsset(response, pathname, respond) {
|
|
|
87
90
|
return asset("image/png", Buffer.from(PWA_ICON_APPLE, "base64"));
|
|
88
91
|
if (pathname === "/icon.svg")
|
|
89
92
|
return asset("image/svg+xml", PWA_ICON_SVG);
|
|
93
|
+
if (pathname === "/icon-attention.svg")
|
|
94
|
+
return asset("image/svg+xml", PWA_ICON_ATTENTION_SVG);
|
|
90
95
|
if (pathname === "/icon-192.png")
|
|
91
96
|
return asset("image/png", Buffer.from(PWA_ICON_192, "base64"));
|
|
92
97
|
if (pathname === "/icon-512.png")
|
package/dist/server/pages.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { steerControlsFor } from "../steer-controls.js";
|
|
2
|
+
import { canReveal, cdCommandOf, localCheckoutOf } from "../checkout-proof.js";
|
|
3
|
+
import { instanceHttpAnswer } from "../console-instance.js";
|
|
1
4
|
import { handlersOf } from './handler-registry.js';
|
|
2
|
-
import { html, joinHtml, postForm } from "../html.js";
|
|
5
|
+
import { html, joinHtml, postForm, reauth } from "../html.js";
|
|
3
6
|
/** pages handlers, moved without changing their route bodies. */
|
|
4
7
|
import { createHmac, randomBytes } from "node:crypto";
|
|
5
8
|
import { existsSync, opendirSync, readdirSync, realpathSync } from "node:fs";
|
|
@@ -21,7 +24,7 @@ import { run as execRun } from "../exec.js";
|
|
|
21
24
|
import { computeGaps } from "../gaps.js";
|
|
22
25
|
import { LEDGER_CSV_HEADER, ledgerCsvRows } from "../ledger-csv.js";
|
|
23
26
|
import { ledgerBody } from "../ledger-view.js";
|
|
24
|
-
import { agentRowOf, agentsBoardOf, agentFactsFor, sealedDiffOf } from "../agents-board.js";
|
|
27
|
+
import { agentLanesOf, agentRowOf, agentsBoardOf, agentFactsFor, laneWindowOf, sealedDiffOf } from "../agents-board.js";
|
|
25
28
|
import { markAgentSeen } from "../agent-board.js";
|
|
26
29
|
import { prometheusMetrics } from "../metrics.js";
|
|
27
30
|
import { readMonitoring } from "../monitoring-settings.js";
|
|
@@ -37,12 +40,14 @@ import { serveInstallAsset } from "./pages-assets.js";
|
|
|
37
40
|
import { healthz } from "./remote-hooks.js";
|
|
38
41
|
import { projectChip, refuse, regionScript, relativeAge, screen, settingsRows, transcriptScript, WORKSPACE_STYLE, workToolRows } from "./chrome.js";
|
|
39
42
|
import { redirect, respond, safeReturn, SAFETY, taskHref } from "./http.js";
|
|
43
|
+
import { assignmentOf } from "../assignment.js";
|
|
44
|
+
import { assignmentTaskStatusOf } from "../assignment-presentation.js";
|
|
40
45
|
import { browsePage, fleetBody, fleetScript, githubIdentityOf, githubReposPage, homePage, originUrlOf, portfolioOverview, projectsPage, systemPage, workbenchRail } from "./render-pages.js";
|
|
41
46
|
import { capsPage } from "./render-settings.js";
|
|
42
47
|
import { decisionAnswerScript, taskBody } from "./render-tasks.js";
|
|
43
48
|
import {} from "./session.js";
|
|
44
49
|
export function createPagesHandlers(runtime) {
|
|
45
|
-
const { clock, store, visible, consoleProjects, restricted, ceiling, options, projectFamilyPeek, sendScreen, chromeFor, unscopedMode, admissionList, taskRepoOf, taskViewData, runVisible, evidenceRoot, workAccess, identify, liveCeiling, sessions, deletedRepos, authenticateApprover, projectOf, lookupSession } = runtime;
|
|
50
|
+
const { clock, store, visible, consoleProjects, restricted, ceiling, options, projectFamilyPeek, sendScreen, chromeFor, unscopedMode, admissionList, taskRepoOf, taskViewData, runVisible, evidenceRoot, workAccess, identify, liveCeiling, sessions, deletedRepos, authenticateApprover, authenticateCredential, projectOf, lookupSession } = runtime;
|
|
46
51
|
// The filesystem browser (operator request): pick a project folder by
|
|
47
52
|
// looking, not typing. CONFINED to exactly what opening allows —
|
|
48
53
|
// explicit --repo mode has an enumerable list and gets no browser at
|
|
@@ -382,11 +387,57 @@ export function createPagesHandlers(runtime) {
|
|
|
382
387
|
/** What the board says after a Reply or a steer, in fixed words (never text from the address). */
|
|
383
388
|
const AGENT_SAID = {
|
|
384
389
|
replied: { tone: "info", text: "Reply sent. The agent reads it at its next check." },
|
|
385
|
-
steered: { tone: "info", text: "Note sent.
|
|
390
|
+
steered: { tone: "info", text: "Note sent. It shows in the conversation with whether the agent has read it." },
|
|
391
|
+
"steer-ended": { tone: "error", text: "That attempt ended before your note, so nothing was sent. Your note is kept below." },
|
|
392
|
+
"steered-next": { tone: "info", text: "Note sent. The task's next attempt reads it before starting." },
|
|
393
|
+
"steer-next-finished": { tone: "error", text: "This task is finished, so there's no next attempt to send it to. Your note is kept below." },
|
|
394
|
+
"steer-invalid": { tone: "error", text: "That note will not store. Your note is still in the box." },
|
|
395
|
+
"steer-stale": { tone: "error", text: "That form was already used. Your note is still in the box — send it again." },
|
|
386
396
|
"reply-closed": { tone: "error", text: "That question was already answered." },
|
|
387
397
|
"reply-stale": { tone: "error", text: "This attempt ended before your reply, so nothing was sent." },
|
|
388
398
|
"reply-invalid": { tone: "error", text: "Write a reply of up to 1,500 characters." },
|
|
399
|
+
"steer-no-notes": { tone: "error", text: "This agent takes no notes right now, so nothing was sent. Stop it to change its course." },
|
|
400
|
+
stopping: { tone: "info", text: "Stopping. The work so far is kept." },
|
|
401
|
+
pausing: { tone: "info", text: "Pausing. It stops at the end of its current step." },
|
|
402
|
+
"pausing-with-note": { tone: "info", text: "Pausing. Your note goes to it as soon as it stops." },
|
|
403
|
+
"pause-note-stale": { tone: "error", text: "That form was already used, so your note was not sent. It is still in the box — send it again." },
|
|
404
|
+
resuming: { tone: "info", text: "Resuming. It carries on where it stopped." },
|
|
405
|
+
revealed: { tone: "info", text: "Shown in Finder." },
|
|
406
|
+
"reveal-refused": { tone: "error", text: "That checkout couldn't be shown: it isn't this attempt's running checkout on this computer any more." },
|
|
407
|
+
"reveal-mac-only": { tone: "error", text: "Show in Finder works on a Mac. Copy the command instead." },
|
|
408
|
+
"reveal-failed": { tone: "error", text: "Finder didn't show it. The path is above; copy the command instead." },
|
|
389
409
|
};
|
|
410
|
+
/**
|
|
411
|
+
* The opened agent's head (docs/plans/one-page-task-and-agent.md): its task's status in the task page's own words and
|
|
412
|
+
* that status's one action as a link to the task page tab holding its form, and where this run opens there — a
|
|
413
|
+
* result (the shared predicate: a finished builder or scout run) on the Result tab at this run, a plan on the Plan
|
|
414
|
+
* tab, anything else on its record.
|
|
415
|
+
*/
|
|
416
|
+
function agentTaskHeadOf(taskId, runId, live, access, now) {
|
|
417
|
+
const family = store.taskFamilyOf(taskId, access.repos, access.includeUnplaced);
|
|
418
|
+
const root = family?.root.id ?? taskId;
|
|
419
|
+
const versionOf = (id) => family !== null && id !== family.current.id ? `version=${encodeURIComponent(id)}&` : "";
|
|
420
|
+
const run = store.getRun(runId);
|
|
421
|
+
const open = live || run === null ? null
|
|
422
|
+
: run.finishedAt !== null && (run.role === "builder" || run.role === "scout") ? { label: run.role === "scout" ? "Report" : "Result", href: `${taskHref(root)}?${versionOf(taskId)}tab=result&run=${runId}` }
|
|
423
|
+
: run.role === "planner" ? { label: "Plan", href: `${taskHref(root)}?${versionOf(taskId)}tab=plan` }
|
|
424
|
+
: { label: "Record", href: `/r/${runId}?record=1` };
|
|
425
|
+
let task = null;
|
|
426
|
+
try {
|
|
427
|
+
const assignment = family === null ? null : assignmentOf(store, family.current.id, now, access, evidenceRoot);
|
|
428
|
+
if (assignment !== null) {
|
|
429
|
+
const words = assignmentTaskStatusOf(assignment);
|
|
430
|
+
const wants = words.primaryAction?.href ?? "";
|
|
431
|
+
// A result to review is on the Result tab; everything else the task page heads with (the approval, a retry…).
|
|
432
|
+
const href = /^\/review\?result=|[?&]result=/.test(wants) ? `${taskHref(root)}?tab=result` : taskHref(root);
|
|
433
|
+
task = { headline: words.headline, sentence: words.sentence, tone: words.tone, action: { label: words.primaryAction?.label ?? "Open task", href } };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
task = null;
|
|
438
|
+
}
|
|
439
|
+
return { task, open };
|
|
440
|
+
}
|
|
390
441
|
// The Agents board (one place to check on running agents): rows by state, and the opened agent's live view.
|
|
391
442
|
async function agentsPage(ctx) {
|
|
392
443
|
const { url, who, response, project } = ctx;
|
|
@@ -402,29 +453,43 @@ export function createPagesHandlers(runtime) {
|
|
|
402
453
|
if (facts.outcome !== null && markAgentSeen(store, who.name, facts.runId, now))
|
|
403
454
|
facts.seen = true;
|
|
404
455
|
const live = facts.outcome === null;
|
|
456
|
+
const composer = live ? steerControlsFor(store, facts.taskId, facts.runId, now, who.role === "approver") : null;
|
|
405
457
|
const sealed = live ? null : sealedDiffOf(store, evidenceRoot, facts.runId);
|
|
406
458
|
// A reply's own word first; otherwise the note that the agent asked past the questions cap.
|
|
407
459
|
const said = AGENT_SAID[url.searchParams.get("said") ?? ""] ?? (facts.questionNote === null ? null : { tone: "info", text: facts.questionNote });
|
|
408
460
|
selected = {
|
|
409
461
|
...agentRowOf(store, facts, now, evidenceRoot), live,
|
|
410
462
|
question: live && facts.question !== null ? { ...facts.question, action: `${taskHref(facts.taskId)}/reply` } : null,
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
463
|
+
// The conversation's window: its notes answer anywhere; its transcript only from the machine that runs it (the window says so).
|
|
464
|
+
transcript: `/r/${facts.runId}?fragment=transcript&shape=records`,
|
|
465
|
+
// The live look at its checkout needs this machine's runner — the one running it — and pool, as the run page's does.
|
|
466
|
+
peek: live && options.localRunner !== undefined && run.runner === options.localRunner && options.poolRoot !== undefined ? `/r/${facts.runId}?fragment=peek` : null,
|
|
414
467
|
files: (sealed?.files ?? []).slice(0, 30).map(one => ({ path: one.path, added: one.additions, removed: one.deletions })),
|
|
415
468
|
filesMore: Math.max(0, (sealed?.fileCount ?? 0) - 30),
|
|
416
|
-
|
|
469
|
+
// A note goes only to an attempt that reads notes (steer-controls.ts); the composer says what it takes.
|
|
470
|
+
steer: composer?.steer ?? null,
|
|
471
|
+
controls: composer?.controls ?? null,
|
|
472
|
+
checkout: (() => {
|
|
473
|
+
if (!live || who.role !== "approver" || store.isDemo())
|
|
474
|
+
return null;
|
|
475
|
+
const proof = localCheckoutOf(store, facts.runId, { runner: options.localRunner, poolRoot: options.poolRoot }, now);
|
|
476
|
+
// Show in Finder only where it can work (a Mac); elsewhere the path and the command stand alone.
|
|
477
|
+
return proof.ok ? { path: proof.path, cd: cdCommandOf(proof.path), reveal: canReveal(options.revealPath !== undefined) ? `/r/${facts.runId}/reveal` : null } : null;
|
|
478
|
+
})(),
|
|
417
479
|
canAct: who.role === "approver" && !store.isDemo(),
|
|
418
480
|
taskHref: taskHref(facts.taskId),
|
|
419
481
|
// The run's own address: a finished result opens on its result page, anything else on its record.
|
|
420
482
|
resultHref: live ? null : `/r/${facts.runId}`,
|
|
483
|
+
...agentTaskHeadOf(facts.taskId, facts.runId, live, access, now),
|
|
421
484
|
room: `agent:${facts.runId}`, said,
|
|
422
485
|
};
|
|
423
486
|
}
|
|
424
487
|
}
|
|
425
488
|
const groups = agentsBoardOf(store, now, access, who.name, evidenceRoot);
|
|
426
|
-
const
|
|
427
|
-
|
|
489
|
+
const lanes = selected === null ? agentLanesOf(store, now, access, who.name, laneWindowOf(url.searchParams.get("window"))) : null;
|
|
490
|
+
const view = { kind: "agents", groups, selected, lanes,
|
|
491
|
+
// Empty only when nothing ran at all: a planner or review on the timeline is activity too.
|
|
492
|
+
empty: groups.length === 0 && (lanes === null || lanes === undefined) ? "No agents in the last day. They show here the moment one starts." : null };
|
|
428
493
|
const fallback = joinHtml([
|
|
429
494
|
html `<h1>Agents</h1>`,
|
|
430
495
|
groups.length === 0 ? html `<p class="meta">${view.empty ?? ""}</p>` : joinHtml(groups.map(group => joinHtml([
|
|
@@ -456,7 +521,7 @@ export function createPagesHandlers(runtime) {
|
|
|
456
521
|
html `<p class="meta" id="fleet-region-stamp"></p>`,
|
|
457
522
|
html `<h2>Register a worker</h2>`,
|
|
458
523
|
postForm("/fleet/runner/register", html `<label>Name<input type="text" name="name" placeholder="builder-2" maxlength="60" required></label><label>Capacity<input type="text" name="capacity" inputmode="numeric" value="1" aria-label="capacity"></label><label>Your password, typed again<input type="password" name="token" autocomplete="current-password" required></label><button type="submit">Register — its token is shown once</button>`, { attrs: { class: "card" } }),
|
|
459
|
-
html `<details class="arm-danger"><summary>Retire a worker</summary>${postForm("/fleet/runner/retire", html `<label>Which worker<input type="text" name="name" placeholder="builder-1" required></label
|
|
524
|
+
html `<details class="arm-danger"><summary>Retire a worker</summary>${postForm("/fleet/runner/retire", html `<label>Which worker<input type="text" name="name" placeholder="builder-1" required></label>${reauth(html `<label>Your password, typed again<input type="password" name="token" autocomplete="current-password" required></label>`)}<button type="submit" class="danger">Retire it</button>`, { attrs: { class: "card" } })}</details>`,
|
|
460
525
|
], "\n"),
|
|
461
526
|
// The password fields make this screen sensitive: sendScreen strips
|
|
462
527
|
// the chrome additions and keeps the reorder poller — the named
|
|
@@ -649,7 +714,8 @@ export function createPagesHandlers(runtime) {
|
|
|
649
714
|
if (who.via !== "cookie")
|
|
650
715
|
return refuse(response, who, 403, "runner registration is a browser surface");
|
|
651
716
|
const token = body.get("token") ?? "";
|
|
652
|
-
|
|
717
|
+
// A worker credential is minted here: a credential act, a password typed now even with Passwordless on.
|
|
718
|
+
if (!authenticateCredential(who, token).ok) {
|
|
653
719
|
return refuse(response, who, 403, "registering a worker takes your password, typed again", "/fleet");
|
|
654
720
|
}
|
|
655
721
|
const name = (body.get("name") ?? "").trim();
|
|
@@ -769,7 +835,7 @@ export function createPagesHandlers(runtime) {
|
|
|
769
835
|
// (finding 22/33) — the record is consumed from the RETURNED live
|
|
770
836
|
// session, synchronously, before the first await.
|
|
771
837
|
const password = body.get("token") ?? "";
|
|
772
|
-
if (
|
|
838
|
+
if (!authenticateApprover(who, password).ok) {
|
|
773
839
|
return projectsScreen(response, who, "cloning takes your password, typed again", 403);
|
|
774
840
|
}
|
|
775
841
|
const cookieId = /(?:^|;\s*)standing-orders_session=([0-9a-f]{64})/.exec(request.headers.cookie ?? "")?.[1];
|
|
@@ -879,6 +945,16 @@ export function createPagesHandlers(runtime) {
|
|
|
879
945
|
peeks[path] = peekOf(path);
|
|
880
946
|
return sendScreen(response, status, projectsPage(chromeFor(open, "projects"), recent, [...candidates], open, problem, !restricted() && unscopedMode, !restricted() && (ceiling.roots.length > 0 || unscopedMode), onboardState, peeks, returnTo, who.via === "cookie" ? (path) => publishingOf(store, path).on : undefined, who.via === "cookie" ? (path) => projectCheckLevel(store, path).level : undefined));
|
|
881
947
|
}
|
|
948
|
+
async function instanceEdge(ctx) {
|
|
949
|
+
const { request, response, url } = ctx;
|
|
950
|
+
const secret = options.instanceSecret?.() ?? null;
|
|
951
|
+
if (secret === null)
|
|
952
|
+
return respond(response, 404, "application/json; charset=utf-8", JSON.stringify({ error: "not-up" }));
|
|
953
|
+
// Bound to what THIS console saw as the receiving end of this very connection — never the Host header.
|
|
954
|
+
const answer = instanceHttpAnswer(secret, url.searchParams.getAll("c").length === 1 ? url.searchParams.get("c") : null, { address: request.socket.localAddress, port: request.socket.localPort });
|
|
955
|
+
response.setHeader("Cache-Control", "no-store");
|
|
956
|
+
return respond(response, answer.status, "application/json; charset=utf-8", answer.body);
|
|
957
|
+
}
|
|
882
958
|
async function healthzEdge(ctx) {
|
|
883
959
|
const { response, method } = ctx;
|
|
884
960
|
return healthz(store, response, method === 'HEAD');
|
|
@@ -941,6 +1017,7 @@ export function createPagesHandlers(runtime) {
|
|
|
941
1017
|
"projects.onboard-confirm": onboardConfirm,
|
|
942
1018
|
}, {
|
|
943
1019
|
"edge.healthz": healthzEdge,
|
|
1020
|
+
"edge.instance": instanceEdge,
|
|
944
1021
|
"edge.browser-asset": browserAsset,
|
|
945
1022
|
"edge.workspace-style": workspaceStyle,
|
|
946
1023
|
"edge.install-asset": installAsset,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ServerRuntime } from './runtime.js';
|
|
2
2
|
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
3
3
|
import { type FormView } from "../contracts/console-api.js";
|
|
4
|
+
/** When the provider checked the person, in ms, if its auth_time is a check made for this visit: present, no earlier
|
|
5
|
+
* than the second the visit began, and not in the future beyond the skew. Null otherwise. */
|
|
6
|
+
export declare function sameVisitCheck(authTime: number | null, startedAt: number, now: number): number | null;
|
|
4
7
|
export declare function createPeopleHandlers(runtime: ServerRuntime): {
|
|
5
8
|
registrations: import("./handler-registry.js").Registration[];
|
|
6
9
|
takeJoinAttempt: (source: string) => boolean;
|
|
@@ -3,7 +3,7 @@ import { adapterPolicy } from "./route-policy.js";
|
|
|
3
3
|
/** settings handlers, moved without changing their route bodies. */
|
|
4
4
|
import { mintApiToken, TOKEN_DAYS } from "../api-tokens.js";
|
|
5
5
|
import { CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
|
|
6
|
-
import { html, htmlString, joinHtml, postForm } from "../html.js";
|
|
6
|
+
import { html, htmlString, joinHtml, postForm, reauth } from "../html.js";
|
|
7
7
|
import { credentialsHtml, tokenShownHtml } from "../credentials-ui.js";
|
|
8
8
|
import { projectName } from "../project.js";
|
|
9
9
|
import { requestLimitsHtml, tokenLimitWords } from "../request-budget-ui.js";
|
|
@@ -21,12 +21,26 @@ import { logEvent } from "../log.js";
|
|
|
21
21
|
import { accessFromGroups, accountNameFor, exchangeOidcCode, newOidcVisit, oidcAuthorizeUrl, verifyIdToken } from "../oidc.js";
|
|
22
22
|
import { personAuditHtml, personHref } from "../people-audit-ui.js";
|
|
23
23
|
import { cursorOf as auditCursorOf, remoteAudit } from "../remote-audit.js";
|
|
24
|
-
import { hashPassword } from "../scope.js";
|
|
24
|
+
import { credentialProof, hashPassword, proveAccountNow } from "../scope.js";
|
|
25
25
|
import { form, HANDOFF_STYLE, loginHref, loginReturn, page, SIGN_IN_COOKIE, signInSpent, startedHere } from "./http.js";
|
|
26
26
|
import { joinDeadPage, joinFormPage, loginPage, signupPage } from "./render-people.js";
|
|
27
27
|
import { SESSION_COOKIE, SIGN_IN_LINK_PATH } from "./session.js";
|
|
28
|
+
/** Clock skew allowed between the provider and this server when reading auth_time — forward only (a provider clock
|
|
29
|
+
* running ahead). auth_time is in whole seconds, so a check made in the visit's own second is floored below startedAt:
|
|
30
|
+
* the one allowance backward is that rounding, never a check made before the visit began. */
|
|
31
|
+
const AUTH_TIME_SKEW_MS = 60_000;
|
|
32
|
+
/** When the provider checked the person, in ms, if its auth_time is a check made for this visit: present, no earlier
|
|
33
|
+
* than the second the visit began, and not in the future beyond the skew. Null otherwise. */
|
|
34
|
+
export function sameVisitCheck(authTime, startedAt, now) {
|
|
35
|
+
if (authTime === null || !Number.isFinite(authTime))
|
|
36
|
+
return null;
|
|
37
|
+
const at = authTime * 1000;
|
|
38
|
+
if (at < Math.floor(startedAt / 1000) * 1000 || at > now + AUTH_TIME_SKEW_MS)
|
|
39
|
+
return null;
|
|
40
|
+
return Math.min(Math.max(at, startedAt), now);
|
|
41
|
+
}
|
|
28
42
|
export function createPeopleHandlers(runtime) {
|
|
29
|
-
const { settingsUpdates, joinBySource, ssoOffer, ssoVisits, ssoSettings, options, clock, ssoHandoffs, managedRepos, store, visible, admissionList, sendScreen, chromeFor, restricted, sessions, consoleProjects, authenticateApprover, bustBadge, projectOf, signInLinks, linkKey, fromThisComputer, recordSignIn, passwordAllowed, arrival, defaultProject, cookieSecure, consoleOrigin, providerFor, joinSourceOf, signInBudget, signInActor, minutesWords } = runtime;
|
|
43
|
+
const { settingsUpdates, joinBySource, ssoOffer, ssoVisits, ssoSettings, options, clock, ssoHandoffs, managedRepos, store, visible, admissionList, sendScreen, chromeFor, restricted, sessions, consoleProjects, authenticateApprover, authenticateCredential, bustBadge, projectOf, signInLinks, linkKey, fromThisComputer, recordSignIn, passwordAllowed, arrival, defaultProject, cookieSecure, consoleOrigin, providerFor, joinSourceOf, signInBudget, signInActor, minutesWords } = runtime;
|
|
30
44
|
// v101: Settings → Sessions & tokens: where you're signed in, and your API tokens. An instance operator can see everyone's.
|
|
31
45
|
async function sessionsPage(ctx) {
|
|
32
46
|
const { url, who, response, project } = ctx;
|
|
@@ -95,7 +109,7 @@ export function createPeopleHandlers(runtime) {
|
|
|
95
109
|
: joinHtml(acts.map(act => html `<p class="row meta">${act.kind} ${act.subject} \u00b7 ${act.at.slice(0, 16).replace("T", " ")}</p>`), "\n"),
|
|
96
110
|
approverView && one.revokedAt === null && one.name !== who.name
|
|
97
111
|
? postForm("/people/revoke", html `\
|
|
98
|
-
|
|
112
|
+
${reauth(html `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`)}\
|
|
99
113
|
<button type="submit">Remove ${one.name}'s sign-in</button>\
|
|
100
114
|
<span class="meta">ends their access and everything it signed \u2014 history stays</span>`, { attrs: { class: "row" }, hidden: { name: one.name } })
|
|
101
115
|
: "",
|
|
@@ -105,7 +119,7 @@ export function createPeopleHandlers(runtime) {
|
|
|
105
119
|
const invites = approverView ? store.openInvites(now) : [];
|
|
106
120
|
const inviteRows = invites.map(one => html `<p class="row">${one.role} invite · ${one.projects === null ? "all projects" : one.projects.map(repo => projectName(repo)).join(", ")} \u00b7 from ${personChip(one.mintedBy)} \u00b7 expires ${one.expiresAt.slice(0, 16).replace("T", " ")}${one.attempts > 0 ? ` \u00b7 ${one.attempts} failed attempt${one.attempts === 1 ? "" : "s"}` : ""}\
|
|
107
121
|
${postForm("/people/invite-revoke", html `\
|
|
108
|
-
|
|
122
|
+
${reauth(html `<label>Password <input type="password" name="token" autocomplete="current-password" style="width:8rem"></label>`)}\
|
|
109
123
|
<button type="submit">Cancel it</button>`, { attrs: { style: "display:inline" }, hidden: { id: one.id } })}</p>`);
|
|
110
124
|
const inviteCard = !approverView
|
|
111
125
|
? ""
|
|
@@ -193,10 +207,13 @@ export function createPeopleHandlers(runtime) {
|
|
|
193
207
|
return back("problem", "Choose when it expires.");
|
|
194
208
|
if (access === "act" && who.role !== "approver")
|
|
195
209
|
return back("problem", "A viewer's token can only read.");
|
|
196
|
-
// A token is a credential: made with the person's password (or
|
|
210
|
+
// A token is a credential: made with the person's password typed now (or an explicit identity-provider re-check,
|
|
211
|
+
// spent by it), counted once. Never a remembered password: this act is outside every freshness allowlist.
|
|
197
212
|
const typed = body.get("password") ?? "";
|
|
198
|
-
|
|
199
|
-
|
|
213
|
+
// Passwordless never covers it (docs/plans/passwordless.md: a credential act).
|
|
214
|
+
const viewerProof = who.role === "approver" || who.via !== "cookie" ? null : proveAccountNow(store, who.name, typed, { credential: true });
|
|
215
|
+
const confirmed = who.role === "approver" ? authenticateCredential(who, typed).ok
|
|
216
|
+
: viewerProof !== null && viewerProof.ok && credentialProof(viewerProof.method);
|
|
200
217
|
if (!confirmed)
|
|
201
218
|
return back("problem", "Enter your Toolroll password to make a token.");
|
|
202
219
|
const minted = mintApiToken();
|
|
@@ -210,7 +227,8 @@ export function createPeopleHandlers(runtime) {
|
|
|
210
227
|
async function peopleProjects(ctx) {
|
|
211
228
|
const { who, response, now, posted } = ctx;
|
|
212
229
|
const body = readForm(posted, CONSOLE_FORMS.peopleProjects);
|
|
213
|
-
|
|
230
|
+
// Widening access (up to instance operator) is a credential act: a password typed now, Passwordless or not.
|
|
231
|
+
if (!authenticateCredential(who, body.get("token") ?? "", null).ok)
|
|
214
232
|
return refuse(response, who, 403, "Saving access requires your password.", "/people");
|
|
215
233
|
const access = readAccessForm(body);
|
|
216
234
|
if (!access.ok)
|
|
@@ -229,7 +247,7 @@ export function createPeopleHandlers(runtime) {
|
|
|
229
247
|
const token = body.get("token") ?? "";
|
|
230
248
|
// RAISING authority is a password ceremony (the doctrine): adding a
|
|
231
249
|
// person to the instance is exactly that.
|
|
232
|
-
if (!
|
|
250
|
+
if (!authenticateCredential(who, token).ok) {
|
|
233
251
|
return refuse(response, who, 403, "making an invite takes your password, typed again", "/people");
|
|
234
252
|
}
|
|
235
253
|
const role = body.get("role") === "approver" ? "approver" : "viewer";
|
|
@@ -346,7 +364,9 @@ export function createPeopleHandlers(runtime) {
|
|
|
346
364
|
// Bounded: anyone may start a sign-in, so the oldest waiting ones give way.
|
|
347
365
|
while (ssoVisits.size >= 1000)
|
|
348
366
|
ssoVisits.delete(ssoVisits.keys().next().value);
|
|
349
|
-
|
|
367
|
+
const asker = intent === "sign-in" || who?.via !== "cookie" ? null : sessions.hashOf(who.session);
|
|
368
|
+
ssoVisits.set(visit.state, { visit, provider: found.provider, redirect: redirectUri, intent, returnTo, expires: Date.now() + 15 * 60_000, startedAt: Date.now(),
|
|
369
|
+
session: asker === null || who?.via !== "cookie" ? null : { hash: asker, generation: who.session.generation } });
|
|
350
370
|
response.writeHead(302, { location: oidcAuthorizeUrl(found.provider, settings, visit, redirectUri, intent === "reauth"), "cache-control": "no-store", "referrer-policy": "no-referrer",
|
|
351
371
|
"set-cookie": `${SIGN_IN_COOKIE}=${visit.state}; Path=${SSO_CALLBACK}; Max-Age=900; HttpOnly; SameSite=Lax${origin.startsWith("https:") ? "; Secure" : ""}` });
|
|
352
372
|
response.end();
|
|
@@ -371,18 +391,28 @@ export function createPeopleHandlers(runtime) {
|
|
|
371
391
|
if (handoff.intent === "link" || handoff.intent === "reauth") {
|
|
372
392
|
if (who?.via !== "cookie")
|
|
373
393
|
return redirect(response, loginHref(handoff.returnTo));
|
|
394
|
+
// Only the session that asked: same kept session, same credential generation.
|
|
395
|
+
if (handoff.session === null || sessions.hashOf(who.session) !== handoff.session.hash || who.session.generation !== handoff.session.generation) {
|
|
396
|
+
return page(response, 403, loginPage(`That ${settings.label} check was started by another sign-in. Start it again from this page.`, handoff.returnTo, ssoOffer()));
|
|
397
|
+
}
|
|
374
398
|
if (handoff.intent === "link") {
|
|
375
399
|
const linked = store.linkSsoIdentity(who.name, identity, clock());
|
|
376
400
|
if (!linked.ok)
|
|
377
401
|
return redirect(response, `/settings/sign-in?problem=${encodeURIComponent(`That ${settings.label} account signs in as someone else here.`)}`);
|
|
378
|
-
who.session.sso = { at:
|
|
402
|
+
who.session.sso = { at: handoff.checkedAt ?? 0 };
|
|
379
403
|
sessions.persist(who.session);
|
|
380
404
|
return redirect(response, `/settings/sign-in?said=${encodeURIComponent(`This account signs in with ${settings.label} now.`)}`);
|
|
381
405
|
}
|
|
382
406
|
// A step-up: the provider checked the same person again.
|
|
383
407
|
if (store.ssoAccount(identity.issuer, identity.subject) !== who.name)
|
|
384
408
|
return page(response, 403, loginPage(`That ${settings.label} account isn't the one signed in here.`, handoff.returnTo, ssoOffer()));
|
|
385
|
-
who.session.sso = { at:
|
|
409
|
+
who.session.sso = { at: handoff.checkedAt ?? 0 };
|
|
410
|
+
// An explicit re-check on request: the one identity-provider event that stands in for a password typed now (for ONE
|
|
411
|
+
// step-up, spent by it) and may start a freshness window (B2). Kept in memory only: a restart forgets it.
|
|
412
|
+
if (handoff.checkedAt !== null) {
|
|
413
|
+
who.session.ssoReauthAt = handoff.checkedAt;
|
|
414
|
+
who.session.ssoNowAt = handoff.checkedAt;
|
|
415
|
+
}
|
|
386
416
|
sessions.persist(who.session);
|
|
387
417
|
recordSignIn(who.name, "confirmed", settings.label);
|
|
388
418
|
return redirect(response, handoff.returnTo);
|
|
@@ -397,7 +427,7 @@ export function createPeopleHandlers(runtime) {
|
|
|
397
427
|
const account = store.accountOf(signedIn.account);
|
|
398
428
|
const id = randomBytes(32).toString("hex");
|
|
399
429
|
sessions.set(id, { ...arrival(request), name: signedIn.account, csrf: randomBytes(32).toString("hex"), role: account.role, generation: account.generation, createdAt: Date.now(), sawBoardAt: null, lastSeen: Date.now(),
|
|
400
|
-
project: defaultProject, projectRevision: 1, sso: { at:
|
|
430
|
+
project: defaultProject, projectRevision: 1, sso: { at: handoff.checkedAt ?? 0 } });
|
|
401
431
|
recordSignIn(signedIn.account, "signed in", settings.label);
|
|
402
432
|
response.setHeader("Set-Cookie", [signInSpent("/login/sso/finish"), `${SESSION_COOKIE}=${id}; HttpOnly; SameSite=Strict; Path=/${cookieSecure}`]);
|
|
403
433
|
return redirect(response, handoff.returnTo);
|
|
@@ -447,6 +477,8 @@ export function createPeopleHandlers(runtime) {
|
|
|
447
477
|
lastSeen: Date.now(),
|
|
448
478
|
project: defaultProject,
|
|
449
479
|
projectRevision: 1,
|
|
480
|
+
// A password typed on this computer starts this session's freshness window (cockpit B2); from anywhere else, never.
|
|
481
|
+
...(fromThisComputer(request) ? { passwordAt: Date.now() } : {}),
|
|
450
482
|
});
|
|
451
483
|
response.setHeader("Set-Cookie", `${SESSION_COOKIE}=${id}; HttpOnly; SameSite=Strict; Path=/${cookieSecure}`);
|
|
452
484
|
return redirect(response, "/");
|
|
@@ -496,6 +528,9 @@ export function createPeopleHandlers(runtime) {
|
|
|
496
528
|
lastSeen: Date.now(),
|
|
497
529
|
project: defaultProject,
|
|
498
530
|
projectRevision: 1,
|
|
531
|
+
// A password typed to sign in on this computer starts this session's freshness window (cockpit B2); from anywhere
|
|
532
|
+
// else (a phone, a proxy) it signs in and proves nothing more.
|
|
533
|
+
...(fromThisComputer(request) ? { passwordAt: Date.now() } : {}),
|
|
499
534
|
});
|
|
500
535
|
response.setHeader("Set-Cookie", `${SESSION_COOKIE}=${id}; HttpOnly; SameSite=Strict; Path=/${cookieSecure}`);
|
|
501
536
|
return redirect(response, returnTo);
|
|
@@ -641,8 +676,11 @@ export function createPeopleHandlers(runtime) {
|
|
|
641
676
|
logEvent("warn", "sso.refused", { reason: verified.said });
|
|
642
677
|
return back(verified.said);
|
|
643
678
|
}
|
|
644
|
-
//
|
|
645
|
-
|
|
679
|
+
// When the provider checked the person (its auth_time), proved against this visit (cockpit B2, N18): no earlier than
|
|
680
|
+
// this visit began, and not in the future beyond a minute of clock skew. A step-up must carry one that holds — a
|
|
681
|
+
// check made for this request, never a remembered one; any other sign-in without one signs in, but is never fresh.
|
|
682
|
+
const checkedAt = sameVisitCheck(verified.claims.authTime, held.startedAt, Date.now());
|
|
683
|
+
if (held.intent === "reauth" && checkedAt === null)
|
|
646
684
|
return back(`${settings.label} didn't check you again. Try once more.`);
|
|
647
685
|
const key = randomBytes(24).toString("base64url");
|
|
648
686
|
for (const [one, value] of ssoHandoffs)
|
|
@@ -650,7 +688,7 @@ export function createPeopleHandlers(runtime) {
|
|
|
650
688
|
ssoHandoffs.delete(one);
|
|
651
689
|
while (ssoHandoffs.size >= 1000)
|
|
652
690
|
ssoHandoffs.delete(ssoHandoffs.keys().next().value);
|
|
653
|
-
ssoHandoffs.set(key, { claims: verified.claims, issuer: held.provider.issuer, intent: held.intent, returnTo: held.returnTo, expires: Date.now() + 60_000 });
|
|
691
|
+
ssoHandoffs.set(key, { claims: verified.claims, issuer: held.provider.issuer, intent: held.intent, returnTo: held.returnTo, expires: Date.now() + 60_000, checkedAt, session: held.session });
|
|
654
692
|
const to = `/login/sso/finish?h=${key}`;
|
|
655
693
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "referrer-policy": "no-referrer", "x-content-type-options": "nosniff", "x-frame-options": "DENY",
|
|
656
694
|
// The hand-off key is this browser's alone, like the visit's state: its cookie rides to /login/sso/finish.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { adapterPolicy } from "./route-policy.js";
|
|
2
|
-
import { flowFormPage, FORM_PATH, HOOK_PATH, receiveFlowForm, receiveFlowHook } from "../flow-triggers.js";
|
|
2
|
+
import { flowFormPage, FORM_PATH, HOOK_PATH, hookRefusedUnread, receiveFlowForm, receiveFlowHook } from "../flow-triggers.js";
|
|
3
3
|
import { keepPushedUpdate, loadBotToken, pushedByTelegram, telegramHookSecret } from "../telegram.js";
|
|
4
4
|
/** Flow webhooks (v82): the one road a public relay may expose. The secret
|
|
5
5
|
* address proves nothing about the sender on its own for GitHub and Linear:
|
|
@@ -48,15 +48,21 @@ export async function telegramHook(ctx, request, response) {
|
|
|
48
48
|
}
|
|
49
49
|
export async function flowHook(ctx, request, response, url) {
|
|
50
50
|
const { store, options, clock } = ctx;
|
|
51
|
-
const reply = (status, said) => {
|
|
51
|
+
const reply = (status, said, retryAfter) => {
|
|
52
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...(retryAfter === undefined ? {} : { "retry-after": String(retryAfter) }) });
|
|
53
|
+
response.end(JSON.stringify({ said }));
|
|
54
|
+
request.resume();
|
|
55
|
+
};
|
|
52
56
|
if (url.pathname.startsWith(FORM_PATH))
|
|
53
57
|
return flowForm(ctx, request, response, url);
|
|
54
58
|
if (options.configDir === undefined || !url.pathname.startsWith(HOOK_PATH))
|
|
55
59
|
return reply(404, "No such address.");
|
|
56
60
|
if (request.method !== "POST")
|
|
57
61
|
return reply(405, "Send a POST.");
|
|
62
|
+
// Refused before it is read (v126): it still counts toward its address's minute, as anything unproven does.
|
|
63
|
+
const refused = (status, said) => { const over = hookRefusedUnread(store, url.pathname.slice(HOOK_PATH.length), clock()); return over === null ? reply(status, said) : reply(over.status, over.said, over.retryAfter); };
|
|
58
64
|
if (Number(request.headers["content-length"] ?? 0) > 1_000_000)
|
|
59
|
-
return
|
|
65
|
+
return refused(413, "Too large.");
|
|
60
66
|
const chunks = [];
|
|
61
67
|
try {
|
|
62
68
|
await new Promise((resolve, reject) => {
|
|
@@ -70,11 +76,11 @@ export async function flowHook(ctx, request, response, url) {
|
|
|
70
76
|
});
|
|
71
77
|
}
|
|
72
78
|
catch (error) {
|
|
73
|
-
return
|
|
79
|
+
return refused(error instanceof Error && error.message === "too-large" ? 413 : 400, "Couldn't read that.");
|
|
74
80
|
}
|
|
75
81
|
try {
|
|
76
|
-
const answer = receiveFlowHook(store, url.pathname.slice(HOOK_PATH.length), { headers: request.
|
|
77
|
-
return reply(answer.status, answer.said);
|
|
82
|
+
const answer = receiveFlowHook(store, url.pathname.slice(HOOK_PATH.length), { headers: request.headersDistinct, body: Buffer.concat(chunks) }, options.configDir, clock());
|
|
83
|
+
return reply(answer.status, answer.said, answer.retryAfter);
|
|
78
84
|
}
|
|
79
85
|
catch {
|
|
80
86
|
return reply(500, "Not saved.");
|
package/dist/server/remote.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FreshFacts } from "../scope.js";
|
|
1
2
|
/** tasks handlers, moved without changing their route bodies. */
|
|
2
3
|
import { type IncomingMessage } from "node:http";
|
|
3
4
|
import { CodingWorkspace } from '../coding-workspace.js';
|
|
@@ -45,11 +46,11 @@ export interface RemoteRuntime {
|
|
|
45
46
|
managedRepos: () => string[];
|
|
46
47
|
joinSourceOf: (request: IncomingMessage) => string;
|
|
47
48
|
ssoSettings: () => import("../oidc.js").OidcSettings | null;
|
|
48
|
-
SSO_FRESH_MS: number;
|
|
49
49
|
oauthTokenBudget: SourceAdmission;
|
|
50
50
|
hookContext: RemoteHookContext;
|
|
51
51
|
teamsSourceBudget: SourceAdmission;
|
|
52
52
|
teamsTenantBudget: SourceAdmission;
|
|
53
|
+
freshFactsOf: (request: IncomingMessage, who: Who | null) => FreshFacts | null;
|
|
53
54
|
}
|
|
54
55
|
export declare function createRemoteHandlers(runtime: RemoteRuntime): {
|
|
55
56
|
registrations: import("./handler-registry.js").Registration[];
|