toolroll 0.9.53 → 0.9.55
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 +17 -0
- package/README.md +5 -4
- package/dist/agent-onboard.js +4 -1
- package/dist/approval-rules-ui.d.ts +3 -2
- package/dist/approval-rules-ui.js +15 -20
- package/dist/assignment-ui.d.ts +3 -2
- package/dist/assignment-ui.js +4 -11
- package/dist/backup-ui.d.ts +2 -1
- package/dist/backup-ui.js +16 -27
- package/dist/brand-mark.d.ts +3 -2
- package/dist/brand-mark.js +4 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-shell.d.ts +2 -1
- package/dist/browser-shell.js +9 -9
- package/dist/browser-workspace.d.ts +2 -3
- package/dist/browser-workspace.js +0 -3
- package/dist/chat-ask.js +8 -8
- package/dist/chat-core.d.ts +171 -0
- package/dist/chat-core.js +288 -0
- package/dist/chat-decide.d.ts +10 -8
- package/dist/chat-decide.js +26 -21
- package/dist/chat-delivery-state.d.ts +51 -75
- package/dist/chat-delivery-state.js +140 -170
- package/dist/chat-delivery.d.ts +21 -2
- package/dist/chat-delivery.js +313 -194
- package/dist/chat-flow.d.ts +29 -1
- package/dist/chat-flow.js +24 -50
- package/dist/chat-messages.d.ts +319 -0
- package/dist/chat-messages.js +841 -0
- package/dist/chat-migration.d.ts +30 -0
- package/dist/chat-migration.js +802 -0
- package/dist/chat-polish.d.ts +5 -4
- package/dist/chat-polish.js +9 -10
- package/dist/chat-quiet.d.ts +3 -0
- package/dist/chat-quiet.js +5 -0
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +26 -53
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +9 -3
- package/dist/coding-shipping-ui.d.ts +2 -1
- package/dist/coding-shipping-ui.js +6 -6
- package/dist/coding-ui.d.ts +3 -1
- package/dist/coding-ui.js +30 -29
- package/dist/contracts/chat-content.d.ts +71 -2
- package/dist/contracts/chat-content.js +22 -3
- package/dist/contracts/chat-tables.d.ts +11 -4
- package/dist/contracts/chat-tables.js +165 -87
- package/dist/contracts/cli.d.ts +1 -3
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +6 -5
- package/dist/contracts/discord-callback.js +2 -1
- package/dist/contracts/health.d.ts +1 -3
- package/dist/contracts/slack-callback.js +3 -2
- package/dist/contracts/store-json.d.ts +1 -5
- package/dist/contracts/store-json.js +1 -4
- package/dist/contracts/teams-callback.js +2 -1
- package/dist/control-ui.d.ts +5 -4
- package/dist/control-ui.js +9 -34
- package/dist/credentials-ui.d.ts +5 -4
- package/dist/credentials-ui.js +23 -24
- package/dist/demo-chat.d.ts +3 -3
- package/dist/demo-chat.js +29 -76
- package/dist/discord-chat.js +19 -38
- package/dist/discord-settings.d.ts +3 -2
- package/dist/discord-settings.js +21 -55
- package/dist/discord.js +4 -14
- package/dist/envelope.js +10 -0
- package/dist/evidence-pack.d.ts +2 -1
- package/dist/evidence-pack.js +45 -49
- package/dist/export-ui.d.ts +7 -2
- package/dist/export-ui.js +4 -10
- package/dist/export.js +4 -4
- package/dist/flow-gallery-ui.d.ts +4 -3
- package/dist/flow-gallery-ui.js +28 -43
- package/dist/flow-starters-ui.d.ts +2 -1
- package/dist/flow-starters-ui.js +12 -15
- package/dist/flow-triggers.d.ts +1 -0
- package/dist/flow-triggers.js +16 -14
- package/dist/flows-ui.d.ts +4 -3
- package/dist/flows-ui.js +19 -30
- package/dist/flows.js +2 -1
- package/dist/guides.js +108 -3
- package/dist/html.d.ts +76 -0
- package/dist/html.js +173 -0
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +19 -23
- package/dist/kits-ui.d.ts +5 -4
- package/dist/kits-ui.js +18 -20
- package/dist/knowledge-ui.d.ts +6 -5
- package/dist/knowledge-ui.js +26 -25
- package/dist/lead-context.d.ts +11 -1
- package/dist/lead-context.js +6 -8
- package/dist/ledger-view.d.ts +2 -1
- package/dist/ledger-view.js +27 -29
- package/dist/limits-ui.d.ts +2 -1
- package/dist/limits-ui.js +7 -7
- package/dist/live-bus.d.ts +50 -8
- package/dist/live-bus.js +133 -50
- package/dist/mate-tools.js +3 -2
- package/dist/mcp-oauth.js +35 -36
- package/dist/models-ui.d.ts +2 -1
- package/dist/models-ui.js +25 -35
- package/dist/modes.d.ts +13 -1
- package/dist/modes.js +17 -1
- package/dist/monitoring-ui.d.ts +4 -3
- package/dist/monitoring-ui.js +13 -26
- package/dist/notification-rows.d.ts +30 -0
- package/dist/notification-rows.js +102 -0
- package/dist/openrouter-models.d.ts +2 -1
- package/dist/openrouter-models.js +5 -9
- package/dist/operate.js +16 -9
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +13 -13
- package/dist/plane-review.js +5 -11
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +17 -23
- package/dist/project-delete-ui.d.ts +4 -3
- package/dist/project-delete-ui.js +15 -27
- package/dist/project-delete.js +21 -26
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +17 -32
- package/dist/recipe-ui.d.ts +8 -7
- package/dist/recipe-ui.js +39 -32
- package/dist/release-adapters.d.ts +60 -0
- package/dist/release-adapters.js +435 -0
- package/dist/release-cli.d.ts +24 -0
- package/dist/release-cli.js +181 -0
- package/dist/release-gate.d.ts +80 -0
- package/dist/release-gate.js +150 -0
- package/dist/release-links.d.ts +29 -0
- package/dist/release-links.js +102 -0
- package/dist/release.d.ts +331 -0
- package/dist/release.js +550 -0
- package/dist/reply-shape.d.ts +2 -1
- package/dist/reply-shape.js +5 -5
- package/dist/repository-context-ui.d.ts +2 -1
- package/dist/repository-context-ui.js +2 -7
- package/dist/request-budget-ui.d.ts +2 -1
- package/dist/request-budget-ui.js +16 -16
- package/dist/result-review.d.ts +2 -3
- package/dist/result-review.js +3 -9
- package/dist/result-shots.js +2 -2
- package/dist/retention-ui.d.ts +7 -1
- package/dist/retention-ui.js +7 -11
- package/dist/retention.js +1 -1
- package/dist/serve.d.ts +11 -2
- package/dist/serve.js +69 -58
- package/dist/server/chat.d.ts +3 -6
- package/dist/server/chat.js +969 -1006
- package/dist/server/chrome.d.ts +291 -0
- package/dist/server/chrome.js +747 -0
- package/dist/server/console.css +2819 -0
- package/dist/server/flows.d.ts +2 -6
- package/dist/server/flows.js +840 -766
- package/dist/server/guards.d.ts +1 -1
- package/dist/server/guards.js +5 -2
- package/dist/server/handler-context.d.ts +1 -1
- package/dist/server/handler-context.js +1 -1
- package/dist/server/handler-registry.d.ts +8 -6
- package/dist/server/handler-registry.js +14 -2
- package/dist/server/http.d.ts +274 -0
- package/dist/server/http.js +239 -0
- package/dist/server/live.d.ts +34 -0
- package/dist/server/live.js +309 -0
- package/dist/server/pages.d.ts +2 -6
- package/dist/server/pages.js +808 -789
- package/dist/server/people-tokens.d.ts +1 -6
- package/dist/server/people-tokens.js +580 -572
- package/dist/server/remote.d.ts +4 -7
- package/dist/server/remote.js +37 -70
- package/dist/server/render-chat.d.ts +402 -0
- package/dist/server/render-chat.js +927 -0
- package/dist/server/render-pages.d.ts +535 -0
- package/dist/server/render-pages.js +1277 -0
- package/dist/server/render-people.d.ts +32 -0
- package/dist/server/render-people.js +121 -0
- package/dist/server/render-results.d.ts +744 -0
- package/dist/server/render-results.js +2237 -0
- package/dist/server/render-settings.d.ts +147 -0
- package/dist/server/render-settings.js +577 -0
- package/dist/server/render-tasks.d.ts +632 -0
- package/dist/server/render-tasks.js +2345 -0
- package/dist/server/request-context.d.ts +46 -0
- package/dist/server/request-context.js +17 -0
- package/dist/server/route-table.d.ts +25 -3
- package/dist/server/route-table.js +163 -75
- package/dist/server/runtime.d.ts +15 -7
- package/dist/server/runtime.js +7 -1
- package/dist/server/session.d.ts +138 -0
- package/dist/server/session.js +94 -0
- package/dist/server/settings.d.ts +1 -5
- package/dist/server/settings.js +2182 -2117
- package/dist/server/task-views.d.ts +11 -4
- package/dist/server/task-views.js +26 -22
- package/dist/server/tasks.d.ts +9 -9
- package/dist/server/tasks.js +1486 -1418
- package/dist/server-telemetry.d.ts +4 -3
- package/dist/server-telemetry.js +4 -8
- package/dist/skills-ui.d.ts +4 -3
- package/dist/skills-ui.js +14 -17
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +2 -1
- package/dist/slack-api.d.ts +5 -1
- package/dist/slack-api.js +2 -2
- package/dist/slack-chat.d.ts +9 -4
- package/dist/slack-chat.js +29 -52
- package/dist/slack-settings.d.ts +3 -2
- package/dist/slack-settings.js +23 -57
- package/dist/slack.js +6 -16
- package/dist/spend-ui.d.ts +2 -1
- package/dist/spend-ui.js +9 -32
- package/dist/sso-ui.d.ts +3 -2
- package/dist/sso-ui.js +26 -27
- package/dist/storage-ui.d.ts +2 -1
- package/dist/storage-ui.js +14 -33
- package/dist/store.d.ts +97 -364
- package/dist/store.js +207 -1531
- package/dist/surface.js +14 -3
- package/dist/task-composer.d.ts +2 -1
- package/dist/task-composer.js +7 -7
- package/dist/task-status.d.ts +4 -3
- package/dist/task-status.js +15 -16
- package/dist/team-http.d.ts +5 -9
- package/dist/team-http.js +5 -63
- package/dist/team-ui.d.ts +2 -1
- package/dist/team-ui.js +2 -5
- package/dist/teammate-question.js +12 -12
- package/dist/teammates-ui.d.ts +5 -4
- package/dist/teammates-ui.js +61 -90
- package/dist/teams-chat.js +12 -9
- package/dist/teams-settings.d.ts +3 -2
- package/dist/teams-settings.js +22 -33
- package/dist/teams.js +4 -5
- package/dist/telegram-flow.js +5 -5
- package/dist/telegram-settings.d.ts +3 -2
- package/dist/telegram-settings.js +14 -25
- package/dist/telegram.js +4 -4
- package/dist/toolroll-update-ui.d.ts +3 -2
- package/dist/toolroll-update-ui.js +35 -39
- package/dist/toolroll-update.d.ts +10 -0
- package/dist/toolroll-update.js +50 -3
- package/dist/tools-ui.d.ts +5 -2
- package/dist/tools-ui.js +31 -31
- package/dist/when-html.d.ts +11 -3
- package/dist/when-html.js +5 -6
- package/dist/workspace-revision.js +3 -1
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +10 -10
- package/package.json +1 -1
- package/dist/server/shared.d.ts +0 -3151
- package/dist/server/shared.js +0 -12001
- package/dist/slack-state.d.ts +0 -13
- package/dist/slack-state.js +0 -10
package/dist/server/guards.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { authenticateApprover as checkApproverPassword } from "../scope.js";
|
|
|
6
6
|
import { SourceBudget } from "../sign-in-guard.js";
|
|
7
7
|
import { type Run, type Store } from "../store.js";
|
|
8
8
|
import type { RouteDeclaration } from './route-table.js';
|
|
9
|
-
import { PersistentSessions, type Session, type Who } from "./
|
|
9
|
+
import { PersistentSessions, type Session, type Who } from "./session.js";
|
|
10
10
|
/** Shared browser, credential and project guards. Runtime access is lazy to preserve server initialization order. */
|
|
11
11
|
export interface GuardsRuntime {
|
|
12
12
|
store: Store;
|
package/dist/server/guards.js
CHANGED
|
@@ -9,7 +9,10 @@ import { canonicalProject, rowVisible } from "../project.js";
|
|
|
9
9
|
import { authenticateAccount, authenticateApprover as checkApproverPassword } from "../scope.js";
|
|
10
10
|
import { SourceBudget } from "../sign-in-guard.js";
|
|
11
11
|
import {} from "../store.js";
|
|
12
|
-
import {
|
|
12
|
+
import { requestContext } from "./request-context.js";
|
|
13
|
+
import { refuse } from "./chrome.js";
|
|
14
|
+
import { matchTaskPath } from "./http.js";
|
|
15
|
+
import { PersistentSessions, SESSION_ABSOLUTE_MS, SESSION_COOKIE, SESSION_IDLE_MS } from "./session.js";
|
|
13
16
|
export function createSharedGuards(runtime) {
|
|
14
17
|
// v99: wrong passwords. One address guessing across names runs out of tries here; each name
|
|
15
18
|
// locks on its own (sign-in-guard.ts), and every lock, whatever road it came by, is kept.
|
|
@@ -163,7 +166,7 @@ export function createSharedGuards(runtime) {
|
|
|
163
166
|
const path = url.pathname;
|
|
164
167
|
const project = resolveRouteProject(source, url, who, request, body);
|
|
165
168
|
if (restricted() && body !== null) {
|
|
166
|
-
const repos = body.getAll("repo");
|
|
169
|
+
const repos = readForm(body, CONSOLE_FORMS.projectCeiling).getAll("repo");
|
|
167
170
|
if (repos.length > 1 || repos.some(repo => repo.trim() !== "" && !visible(repo.trim()))) {
|
|
168
171
|
refuse(response, who, 403, "That project is outside your access.", "/projects");
|
|
169
172
|
return false;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import type { RouteDeclaration } from './route-table.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type Who } from "./session.js";
|
|
4
4
|
export interface HandlerContext {
|
|
5
5
|
url: URL;
|
|
6
6
|
who: Who;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import {} from "./session.js";
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import type { EdgeContext, HandlerContext } from './handler-context.js';
|
|
2
|
-
import { type Domain, type RouteDeclaration
|
|
2
|
+
import { type Domain, type RouteDeclaration } from './route-table.js';
|
|
3
|
+
export type ConsoleHandler = (ctx: HandlerContext) => Promise<void>;
|
|
4
|
+
export type EdgeHandler = (ctx: EdgeContext) => Promise<void>;
|
|
3
5
|
export type Registration = {
|
|
4
6
|
id: string;
|
|
5
7
|
domain: Domain;
|
|
6
8
|
stage: 'console';
|
|
7
|
-
|
|
8
|
-
handle: (ctx: HandlerContext) => Promise<void>;
|
|
9
|
+
handle: ConsoleHandler;
|
|
9
10
|
} | {
|
|
10
11
|
id: string;
|
|
11
12
|
domain: Domain;
|
|
12
13
|
stage: 'edge';
|
|
13
|
-
|
|
14
|
-
handle: (ctx: EdgeContext) => Promise<void>;
|
|
14
|
+
handle: EdgeHandler;
|
|
15
15
|
};
|
|
16
|
-
/**
|
|
16
|
+
/** One domain's handlers by route id: one function per row. */
|
|
17
|
+
export declare function handlersOf(domain: Domain, console: Readonly<Record<string, ConsoleHandler>>, edge?: Readonly<Record<string, EdgeHandler>>): Registration[];
|
|
18
|
+
/** Registrations are independent of policy. Missing, extra, incompatible or shared handlers fail at startup. */
|
|
17
19
|
export declare function assertHandlerRegistry(registrations: readonly Registration[], routes?: readonly RouteDeclaration[]): void;
|
|
18
20
|
export declare function createHandlerRegistry(registrations: readonly Registration[]): {
|
|
19
21
|
console(route: RouteDeclaration, ctx: HandlerContext): Promise<void>;
|
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { ROUTES } from './route-table.js';
|
|
2
|
-
/**
|
|
2
|
+
/** One domain's handlers by route id: one function per row. */
|
|
3
|
+
export function handlersOf(domain, console, edge = {}) {
|
|
4
|
+
return [
|
|
5
|
+
...Object.entries(console).map(([id, handle]) => ({ id, domain, stage: 'console', handle })),
|
|
6
|
+
...Object.entries(edge).map(([id, handle]) => ({ id, domain, stage: 'edge', handle })),
|
|
7
|
+
];
|
|
8
|
+
}
|
|
9
|
+
/** Registrations are independent of policy. Missing, extra, incompatible or shared handlers fail at startup. */
|
|
3
10
|
export function assertHandlerRegistry(registrations, routes = ROUTES) {
|
|
4
11
|
const seen = new Set();
|
|
12
|
+
const callbacks = new Map();
|
|
5
13
|
for (const handler of registrations) {
|
|
6
14
|
if (seen.has(handler.id))
|
|
7
15
|
throw new Error(`Duplicate handler: ${handler.id}`);
|
|
8
16
|
seen.add(handler.id);
|
|
9
17
|
const route = routes.find(row => row.id === handler.id);
|
|
10
|
-
if (!route || route.domain !== handler.domain || route.
|
|
18
|
+
if (!route || route.domain !== handler.domain || route.stage !== handler.stage)
|
|
11
19
|
throw new Error(`Handler has no compatible route: ${handler.id}`);
|
|
20
|
+
const other = callbacks.get(handler.handle);
|
|
21
|
+
if (other !== undefined)
|
|
22
|
+
throw new Error(`Handler shared by ${other} and ${handler.id}`);
|
|
23
|
+
callbacks.set(handler.handle, handler.id);
|
|
12
24
|
}
|
|
13
25
|
for (const route of routes)
|
|
14
26
|
if (!seen.has(route.id))
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/** HTTP answers every console route shares: server options, body limits, safe responses, redirects and return paths. */
|
|
2
|
+
import { type Html } from "../html.js";
|
|
3
|
+
import { type CliHttpOptions, type RunOperateAs } from "../cli-http.js";
|
|
4
|
+
import { type CodingWorkspace } from "../coding-workspace.js";
|
|
5
|
+
import { type run as execRun } from "../exec.js";
|
|
6
|
+
import { type MailSender } from "../flow-actions.js";
|
|
7
|
+
import { type FetchLike } from "../flow-share.js";
|
|
8
|
+
import { type TriggerIo } from "../flow-triggers.js";
|
|
9
|
+
import { type InstallMethod } from "../install-method.js";
|
|
10
|
+
import { type IntegrationIo } from "../integrations.js";
|
|
11
|
+
import { type VersionRunner } from "../model-catalog.js";
|
|
12
|
+
import { type cloneGithubRepo, type listGithubRepos, type previewGithubRepo } from "../onboard.js";
|
|
13
|
+
import { type PublishExec } from "../publish.js";
|
|
14
|
+
import { type ResultTab } from "../result-review.js";
|
|
15
|
+
import { type Store } from "../store.js";
|
|
16
|
+
import { type SubscriptionMateRunner } from "../subscription-chat.js";
|
|
17
|
+
import { type launchRuntimeUpdate } from "../toolroll-update.js";
|
|
18
|
+
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
19
|
+
export type ServeOptions = {
|
|
20
|
+
/** Tests: runs a person's command for the HTTP MCP gateway instead of operate.ts's. */
|
|
21
|
+
runOperateAs?: import("../mcp-person.js").RunOperateAs;
|
|
22
|
+
/** `toolroll demo`: the scripted lead that answers Chat instead of a model. */
|
|
23
|
+
demoLead?: import("../demo.js").DemoLead;
|
|
24
|
+
/** Tests: the bin whose real path says how Toolroll was installed (Settings → Updates' command). */
|
|
25
|
+
installBin?: string;
|
|
26
|
+
/** Native coding workspace injection for isolated integration tests. */
|
|
27
|
+
codingWorkspace?: CodingWorkspace;
|
|
28
|
+
/** Read-only worker connections for heavy reads (read-executor.ts); 0 reads in-process. Default: off under tests. */
|
|
29
|
+
readWorkers?: number;
|
|
30
|
+
store: Store;
|
|
31
|
+
evidenceRoot: string;
|
|
32
|
+
clock?: () => Date;
|
|
33
|
+
/**
|
|
34
|
+
* The console's canonical https origin, when TLS terminates in front
|
|
35
|
+
* (arc 3 finding 2/16): EXACTLY an origin — no path, query, credentials.
|
|
36
|
+
* The one trust anchor for secure-context features: it joins the allowed
|
|
37
|
+
* hosts, its origin authorizes POSTs, cookies turn Secure, and the
|
|
38
|
+
* install/push cards light up. X-Forwarded-* is consulted only from a
|
|
39
|
+
* loopback peer, the same-host proxy (public-access.ts).
|
|
40
|
+
*/
|
|
41
|
+
publicUrl?: string;
|
|
42
|
+
/** Tests: the shared command boundary `POST /api/cli` runs (default: operate.ts's runOperateAs). */
|
|
43
|
+
cliRunner?: RunOperateAs;
|
|
44
|
+
/** Tests: remote command metadata until the shared contract declares it. */
|
|
45
|
+
cliModeOf?: CliHttpOptions['modeOf'];
|
|
46
|
+
/** Tests: the clock request budgets for API tokens count by (request-budget.ts). */
|
|
47
|
+
requestBudgetClock?: () => number;
|
|
48
|
+
/** Where repos.json lives — every enrollment locks exactly this file. */
|
|
49
|
+
registryPath?: string;
|
|
50
|
+
/** This console fronts an `up` process: onboarding copy says how to watch. */
|
|
51
|
+
upConsole?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The lead on by default (onboarding): with no lead set up, the agent CLI
|
|
54
|
+
* signed in on this computer runs it, once. `up` and `serve` turn this on;
|
|
55
|
+
* tests opt in.
|
|
56
|
+
*/
|
|
57
|
+
leadByDefault?: boolean;
|
|
58
|
+
/** Test seam: the operating system the sign-in command is written for. */
|
|
59
|
+
platform?: NodeJS.Platform;
|
|
60
|
+
/** Extra Host values this server answers as (a Tailscale name, a LAN ip:port). */
|
|
61
|
+
allowedHosts?: readonly string[];
|
|
62
|
+
/**
|
|
63
|
+
* This computer's names on its tailnet (onboarding): read at start and every
|
|
64
|
+
* few minutes; the console answers to each on its own port without an
|
|
65
|
+
* --allow-host, and the phone card names the address. Absent = none.
|
|
66
|
+
*/
|
|
67
|
+
tailnetNames?: () => Promise<readonly string[]>;
|
|
68
|
+
/**
|
|
69
|
+
* The first-account road (setup review): while NO approver exists, the
|
|
70
|
+
* login page offers "create the first account", gated by this code —
|
|
71
|
+
* printed once by the process that started the server, never stored.
|
|
72
|
+
* Five wrong codes close the road until the server restarts. The moment
|
|
73
|
+
* an approver exists, the page is the ordinary sign-in.
|
|
74
|
+
*/
|
|
75
|
+
setupCode?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Where the Telegram bot token lives when set from here. Present = the
|
|
78
|
+
* settings card renders; absent = no settings surface at all.
|
|
79
|
+
*/
|
|
80
|
+
telegramTokenFile?: string;
|
|
81
|
+
/** Where messaging config files live (beside the database) — enables the
|
|
82
|
+
* primary-messenger selector on the settings screen. */
|
|
83
|
+
configDir?: string;
|
|
84
|
+
/** Test seam for the Slack setup handshake. */
|
|
85
|
+
slackFetcher?: typeof fetch;
|
|
86
|
+
/** Settings → Updates seams: the latest release, how this Toolroll was
|
|
87
|
+
* installed, its version, and how the updater job starts. Tests and
|
|
88
|
+
* screenshots inject these; production reads the registry and launchd. */
|
|
89
|
+
updates?: {
|
|
90
|
+
latest?: () => Promise<{
|
|
91
|
+
version: string;
|
|
92
|
+
}>;
|
|
93
|
+
method?: InstallMethod;
|
|
94
|
+
current?: string;
|
|
95
|
+
dist?: string;
|
|
96
|
+
launch?: typeof launchRuntimeUpdate;
|
|
97
|
+
};
|
|
98
|
+
discordFetcher?: typeof fetch;
|
|
99
|
+
/** Injected by tests: Microsoft sign-in, key metadata and Teams conversation calls. */
|
|
100
|
+
teamsFetcher?: typeof fetch;
|
|
101
|
+
/** Injected by tests: how "Check now" reaches GitHub (gh) and Linear (fetch). */
|
|
102
|
+
flowTriggerIo?: Partial<TriggerIo>;
|
|
103
|
+
/**
|
|
104
|
+
* The repo this console serves. Scopes run evidence to that repo's tasks
|
|
105
|
+
* (and unplaced ones) and turns on the gaps and capabilities views —
|
|
106
|
+
* without it those pages say so instead of guessing.
|
|
107
|
+
*/
|
|
108
|
+
repo?: string;
|
|
109
|
+
/**
|
|
110
|
+
* The full authorization ceiling (v2 review, finding 1): `repos` this
|
|
111
|
+
* server may show and operate on, plus `projectRoots` under which any git
|
|
112
|
+
* repository qualifies. `repo` above is sugar for one entry in `repos`.
|
|
113
|
+
* No configuration at all is the legacy unscoped mode — everything
|
|
114
|
+
* visible, and stated as such where the code decides.
|
|
115
|
+
*/
|
|
116
|
+
repos?: readonly string[];
|
|
117
|
+
projectRoots?: readonly string[];
|
|
118
|
+
/** Repositories the co-located `up` process has proved and is watching.
|
|
119
|
+
* Kept as a callback so projects added after startup appear immediately
|
|
120
|
+
* without turning the durable registry itself into an authorization source. */
|
|
121
|
+
currentRepos?: () => readonly string[];
|
|
122
|
+
/** Injected by tests: the fetch direct-API chat turns use, and where chat
|
|
123
|
+
* keys are read from (defaults to process.env). */
|
|
124
|
+
chatFetcher?: typeof fetch;
|
|
125
|
+
/** Subscription-backed mate transport; injected in tests so no real
|
|
126
|
+
* Codex or Claude membership turn is consumed. */
|
|
127
|
+
subscriptionChatRunner?: SubscriptionMateRunner;
|
|
128
|
+
/** Tests: stands in for `codex mcp list --json` in the project (the Tools page's "Found on this computer"). */
|
|
129
|
+
codexToolList?: (cwd: string) => Promise<string | null>;
|
|
130
|
+
/** Tests: the home whose ~/.toolroll (or older ~/.standing-orders) tool-secrets and ~/.claude.json the Tools page uses. */
|
|
131
|
+
toolHome?: string;
|
|
132
|
+
/** v87: sends Send email steps' mail and the settings test (tests inject one). */
|
|
133
|
+
mailSender?: MailSender;
|
|
134
|
+
/** Injected by tests: how Flows → Import fetches a flow file's address. */
|
|
135
|
+
flowFetch?: FetchLike;
|
|
136
|
+
/** v89: Google's token endpoint (tests inject a scripted one). */
|
|
137
|
+
googleFetch?: typeof fetch;
|
|
138
|
+
/** Tests: every request a one-click connection makes (discovery, registration, tokens). */
|
|
139
|
+
connectFetch?: typeof fetch;
|
|
140
|
+
/** Tests: every request sign-in with the identity provider makes (discovery, keys, tokens). */
|
|
141
|
+
ssoFetch?: typeof fetch;
|
|
142
|
+
chatEnv?: Record<string, string | undefined>;
|
|
143
|
+
/**
|
|
144
|
+
* The live peek's locality ASSERTION (live-peek v3 §3): the administrator
|
|
145
|
+
* who starts serve names the runner this machine owns. This is documented
|
|
146
|
+
* as an assertion, not machine-bound credential enforcement — the product
|
|
147
|
+
* has none anywhere. Absent = the peek is off, and says so.
|
|
148
|
+
*/
|
|
149
|
+
desktopIdentity?: string;
|
|
150
|
+
/** Trusted native admission callback; enrollment rows alone never supply this authority. */
|
|
151
|
+
additionalProjectRepos?: () => readonly string[];
|
|
152
|
+
/** The co-located `up`'s exact admitted repositories, read on every request: its startup projects plus those
|
|
153
|
+
* added since (`repos add`, the lead, the console) and minus those removed. When given it replaces `repos` as
|
|
154
|
+
* the exact part of the ceiling, so an addition or removal shows without a restart. */
|
|
155
|
+
admittedRepos?: () => readonly string[];
|
|
156
|
+
connectionProbe?: typeof execRun;
|
|
157
|
+
connectionHome?: string;
|
|
158
|
+
/** Test seam for Chat's first tasks: the `gh` and `git grep` reads. */
|
|
159
|
+
firstTaskRunner?: typeof execRun;
|
|
160
|
+
/** Test seam for Settings → Integrations: how its checks reach services (fetch, gh, mail servers). */
|
|
161
|
+
integrationIo?: Partial<IntegrationIo>;
|
|
162
|
+
modelCatalogFetcher?: typeof fetch;
|
|
163
|
+
/** Test seams for Settings → Models: CLI version probes and the PATH they search. */
|
|
164
|
+
modelRunner?: VersionRunner;
|
|
165
|
+
modelPath?: string;
|
|
166
|
+
localRunner?: string;
|
|
167
|
+
/** The checkout pool root the peek confines itself to (realpath-proved). */
|
|
168
|
+
poolRoot?: string;
|
|
169
|
+
/**
|
|
170
|
+
* Editor deep links (arc 6): a DEPLOYMENT capability, not an activation.
|
|
171
|
+
* vscode:// links open on the BROWSER's machine, so links render only
|
|
172
|
+
* when three statements align: the operator started serve with
|
|
173
|
+
* --editor vscode AND --runner (this machine owns the worktrees), the
|
|
174
|
+
* run belongs to that runner, and THIS session turned links on for
|
|
175
|
+
* this device. "vscode" is the only value; the scheme is never data.
|
|
176
|
+
*/
|
|
177
|
+
editorLinks?: "vscode";
|
|
178
|
+
/** Injected by tests: the onboarding ceremony's gh-facing halves — the
|
|
179
|
+
* ceremony's gating, nonce, and enrollment logic is what the HTTP tests
|
|
180
|
+
* prove; gh itself is proved by onboard.test.ts. */
|
|
181
|
+
ghPreview?: typeof previewGithubRepo;
|
|
182
|
+
ghClone?: typeof cloneGithubRepo;
|
|
183
|
+
ghList?: typeof listGithubRepos;
|
|
184
|
+
/** Injected by tests: the git and gh calls pull-request setup and Merge make. */
|
|
185
|
+
publishExec?: PublishExec;
|
|
186
|
+
};
|
|
187
|
+
export declare const BODY_CAP: number;
|
|
188
|
+
/** How long a stop waits for teammates and the lead's follow pass before closing anyway. */
|
|
189
|
+
export declare const SHUTDOWN_WAIT_MS = 5000;
|
|
190
|
+
/**
|
|
191
|
+
* Match `/t/<id>` (suffix "") or `/t/<id>/<verb>` — the id percent-decoded
|
|
192
|
+
* exactly once, refused when it does not decode, is oversized, or carries
|
|
193
|
+
* control characters. Legacy CLI-created ids are free-form; the URL is not.
|
|
194
|
+
*/
|
|
195
|
+
export declare function matchTaskPath(pathname: string, suffixPattern: string): {
|
|
196
|
+
taskId: string;
|
|
197
|
+
verb: string;
|
|
198
|
+
} | null;
|
|
199
|
+
export declare function taskHref(taskId: string): string;
|
|
200
|
+
export declare const SAFETY: {
|
|
201
|
+
readonly "Content-Security-Policy": "default-src 'none'; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; worker-src 'self'; img-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'";
|
|
202
|
+
readonly "X-Content-Type-Options": "nosniff";
|
|
203
|
+
readonly "Referrer-Policy": "no-referrer";
|
|
204
|
+
readonly "Cache-Control": "no-store";
|
|
205
|
+
};
|
|
206
|
+
/** A text/html answer carries markup (Html); every other type, its own text. */
|
|
207
|
+
export type ResponseBody<T extends string> = T extends `text/html${string}` ? Html : string;
|
|
208
|
+
export declare function respond<T extends string>(response: ServerResponse, status: number, type: T, body: ResponseBody<T>): void;
|
|
209
|
+
/**
|
|
210
|
+
* A page response. With a nonce, this response's CSP admits exactly the one
|
|
211
|
+
* inline script the shell stamped with the same value — generated per
|
|
212
|
+
* response, never shared, never 'unsafe-inline' (Codex board review,
|
|
213
|
+
* finding 9). Everything else keeps the constant script-free policy.
|
|
214
|
+
*/
|
|
215
|
+
export declare function page(response: ServerResponse, status: number, document: Html, nonce?: string, fetches?: boolean): void;
|
|
216
|
+
/**
|
|
217
|
+
* An outside sign-in comes back only to the browser that started it: the
|
|
218
|
+
* start leaves the visit's state in this cookie (Lax, so it rides the
|
|
219
|
+
* service's redirect back; the session cookie is Strict and doesn't), and a
|
|
220
|
+
* return whose state isn't the one this browser holds is refused. So nobody
|
|
221
|
+
* can start a sign-in and have someone else finish it into their project.
|
|
222
|
+
*/
|
|
223
|
+
/** A moment's page on the way to or back from another site's sign-in: no stylesheet may load there, so the palette rides inline. */
|
|
224
|
+
export declare const HANDOFF_STYLE: Html;
|
|
225
|
+
export declare const SIGN_IN_COOKIE = "so-sign-in";
|
|
226
|
+
export declare function startedHere(request: IncomingMessage, state: string): boolean;
|
|
227
|
+
export declare const signInSpent: (path: string) => string;
|
|
228
|
+
/**
|
|
229
|
+
* Off to another site's sign-in (Google, a service's one-click connection):
|
|
230
|
+
* a page that moves on by itself. Not a redirect: every page's form-action
|
|
231
|
+
* 'self' also covers where a form's answer redirects, so a browser stops a
|
|
232
|
+
* form that is answered with another site's address.
|
|
233
|
+
*/
|
|
234
|
+
export declare function goOutside(response: ServerResponse, to: string, words: string, bind: {
|
|
235
|
+
state: string;
|
|
236
|
+
path: string;
|
|
237
|
+
secure: boolean;
|
|
238
|
+
}): void;
|
|
239
|
+
export declare function redirect(response: ServerResponse, to: string): void;
|
|
240
|
+
export declare const taskChatHref: (taskId: string) => string;
|
|
241
|
+
/** A project's own lead thread (v77). */
|
|
242
|
+
export declare const projectChatHref: (repo: string) => string;
|
|
243
|
+
export declare function safeReturn(raw: string | null | undefined): string;
|
|
244
|
+
/**
|
|
245
|
+
* The destination a sign-in returns to (a phone's deep link to an exact
|
|
246
|
+
* task or result): `safeReturn`, then narrower still — never the sign-in,
|
|
247
|
+
* sign-up, join or sign-out roads themselves (a loop, or a token in a
|
|
248
|
+
* path), never an encoded second scheme or host once the browser decodes
|
|
249
|
+
* it, never a query key that could carry a secret, and never a region
|
|
250
|
+
* fetch (`?fragment=`), which is a piece of a page and not a place to
|
|
251
|
+
* land. Only the app's own pages qualify: the task lens, the chat, the
|
|
252
|
+
* work and board views, and the fixed control destinations a phone
|
|
253
|
+
* button can name. Nothing here is a second redirect framework: one
|
|
254
|
+
* same-site path, or "/".
|
|
255
|
+
*/
|
|
256
|
+
export declare const LOGIN_RETURN_PAGES: RegExp;
|
|
257
|
+
export declare function loginReturn(raw: string | null | undefined): string;
|
|
258
|
+
/** A request path fit for a log: the one-time sign-in link's code is left out. */
|
|
259
|
+
export declare function redactedPath(path: string): string;
|
|
260
|
+
/** The sign-in page that comes back to `path` afterwards, or plain /login when the path is not one to come back to. */
|
|
261
|
+
export declare function loginHref(path: string): string;
|
|
262
|
+
/** Chat actions may return only to the unified chat or one task-focused
|
|
263
|
+
* lens. Other same-site paths are valid elsewhere, but not for chat forms. */
|
|
264
|
+
export declare function safeChatReturn(raw: string | null | undefined): string;
|
|
265
|
+
export declare function chatReturnWithSaid(back: string, said: string): string;
|
|
266
|
+
export declare const chatReturnWithLatest: (back: string) => string;
|
|
267
|
+
/** The one result page's address: the task and its run, never a project path
|
|
268
|
+
* (the page finds the result's project itself and keeps the person's own). */
|
|
269
|
+
export declare const reviewHref: (taskId: string, runId?: number | null) => string;
|
|
270
|
+
/** The chat's result detail (package 3): the same panel the run page
|
|
271
|
+
* and the cockpit render, opened beside the conversation when the screen
|
|
272
|
+
* has room and as a dedicated view with Back to chat when it does not. */
|
|
273
|
+
export declare const chatResultHref: (taskId: string, runId: number, tab?: ResultTab) => string;
|
|
274
|
+
export declare function form(request: IncomingMessage, cap?: number): Promise<URLSearchParams>;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/** HTTP answers every console route shares: server options, body limits, safe responses, redirects and return paths. */
|
|
2
|
+
import { html, htmlString, isHtml } from "../html.js";
|
|
3
|
+
import { chatResultHref as sharedResultHref } from "../chat-controls.js";
|
|
4
|
+
import {} from "../cli-http.js";
|
|
5
|
+
import {} from "../coding-workspace.js";
|
|
6
|
+
import { hasForbiddenControls } from "../decision.js";
|
|
7
|
+
import {} from "../exec.js";
|
|
8
|
+
import {} from "../flow-actions.js";
|
|
9
|
+
import {} from "../flow-share.js";
|
|
10
|
+
import {} from "../flow-triggers.js";
|
|
11
|
+
import {} from "../install-method.js";
|
|
12
|
+
import {} from "../integrations.js";
|
|
13
|
+
import {} from "../model-catalog.js";
|
|
14
|
+
import {} from "../onboard.js";
|
|
15
|
+
import {} from "../publish.js";
|
|
16
|
+
import {} from "../result-review.js";
|
|
17
|
+
import {} from "../store.js";
|
|
18
|
+
import {} from "../subscription-chat.js";
|
|
19
|
+
import {} from "../toolroll-update.js";
|
|
20
|
+
import { timingSafeEqual } from "node:crypto";
|
|
21
|
+
import {} from "node:http";
|
|
22
|
+
import { SIGN_IN_LINK_PATH } from "./session.js";
|
|
23
|
+
// A note is at most NOTE_BYTE_CAP (16,000) UTF-8 bytes, which URL encoding
|
|
24
|
+
// can triple: room for one whole, before canonical text validation.
|
|
25
|
+
export const BODY_CAP = 64 * 1024;
|
|
26
|
+
/** How long a stop waits for teammates and the lead's follow pass before closing anyway. */
|
|
27
|
+
export const SHUTDOWN_WAIT_MS = 5_000;
|
|
28
|
+
// ---- path plumbing ---------------------------------------------------------
|
|
29
|
+
/**
|
|
30
|
+
* Match `/t/<id>` (suffix "") or `/t/<id>/<verb>` — the id percent-decoded
|
|
31
|
+
* exactly once, refused when it does not decode, is oversized, or carries
|
|
32
|
+
* control characters. Legacy CLI-created ids are free-form; the URL is not.
|
|
33
|
+
*/
|
|
34
|
+
export function matchTaskPath(pathname, suffixPattern) {
|
|
35
|
+
const match = new RegExp(`^/t/([^/]+)${suffixPattern === "" ? "$" : suffixPattern}`).exec(pathname);
|
|
36
|
+
if (match === null)
|
|
37
|
+
return null;
|
|
38
|
+
let taskId;
|
|
39
|
+
try {
|
|
40
|
+
taskId = decodeURIComponent(match[1]);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (taskId.length === 0 || taskId.length > 64 || hasForbiddenControls(taskId))
|
|
46
|
+
return null;
|
|
47
|
+
return { taskId, verb: match[2] ?? "" };
|
|
48
|
+
}
|
|
49
|
+
export function taskHref(taskId) {
|
|
50
|
+
return `/t/${encodeURIComponent(taskId)}`;
|
|
51
|
+
}
|
|
52
|
+
// ---- rendering -------------------------------------------------------------
|
|
53
|
+
export const SAFETY = {
|
|
54
|
+
"Content-Security-Policy": "default-src 'none'; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; worker-src 'self'; img-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
|
|
55
|
+
"X-Content-Type-Options": "nosniff",
|
|
56
|
+
"Referrer-Policy": "no-referrer",
|
|
57
|
+
"Cache-Control": "no-store",
|
|
58
|
+
};
|
|
59
|
+
export function respond(response, status, type, body) {
|
|
60
|
+
// A route that set its OWN policy (the service worker's default-src
|
|
61
|
+
// 'none') or its own caching (the pre-auth assets) keeps it —
|
|
62
|
+
// writeHead's headers would otherwise win.
|
|
63
|
+
const own = response.getHeader("content-security-policy");
|
|
64
|
+
const cache = response.getHeader("cache-control");
|
|
65
|
+
response.writeHead(status, {
|
|
66
|
+
...SAFETY,
|
|
67
|
+
...(own === undefined ? {} : { "content-security-policy": own }),
|
|
68
|
+
...(cache === undefined ? {} : { "Cache-Control": cache }),
|
|
69
|
+
"Content-Type": type,
|
|
70
|
+
});
|
|
71
|
+
response.end(isHtml(body) ? htmlString(body) : body);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A page response. With a nonce, this response's CSP admits exactly the one
|
|
75
|
+
* inline script the shell stamped with the same value — generated per
|
|
76
|
+
* response, never shared, never 'unsafe-inline' (Codex board review,
|
|
77
|
+
* finding 9). Everything else keeps the constant script-free policy.
|
|
78
|
+
*/
|
|
79
|
+
export function page(response, status, document, nonce, fetches) {
|
|
80
|
+
if (nonce === undefined)
|
|
81
|
+
return respond(response, status, "text/html; charset=utf-8", document);
|
|
82
|
+
response.writeHead(status, {
|
|
83
|
+
...SAFETY,
|
|
84
|
+
"Content-Security-Policy": `default-src 'none'; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; worker-src 'self'; img-src 'self'; script-src 'nonce-${nonce}'; ` +
|
|
85
|
+
// connect-src only when the page's script actually fetches (a region
|
|
86
|
+
// poller, the full chrome beat, or the minimal sensitive-page beat).
|
|
87
|
+
`${fetches === true ? "connect-src 'self'; " : ""}form-action 'self'; base-uri 'none'; frame-ancestors 'none'`,
|
|
88
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
89
|
+
});
|
|
90
|
+
response.end(htmlString(document));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* An outside sign-in comes back only to the browser that started it: the
|
|
94
|
+
* start leaves the visit's state in this cookie (Lax, so it rides the
|
|
95
|
+
* service's redirect back; the session cookie is Strict and doesn't), and a
|
|
96
|
+
* return whose state isn't the one this browser holds is refused. So nobody
|
|
97
|
+
* can start a sign-in and have someone else finish it into their project.
|
|
98
|
+
*/
|
|
99
|
+
/** A moment's page on the way to or back from another site's sign-in: no stylesheet may load there, so the palette rides inline. */
|
|
100
|
+
export const HANDOFF_STYLE = html `<style>body{font:15px/1.5 "Geist",system-ui,sans-serif;margin:2rem;background:#efefef;color:#171717}a{color:#171717;text-decoration-color:#8f8f8f;text-underline-offset:3px}@media(prefers-color-scheme:dark){body{background:#0b0b0b;color:#ededed}a{color:#ededed;text-decoration-color:#ff6fb5}}</style>`;
|
|
101
|
+
export const SIGN_IN_COOKIE = "so-sign-in";
|
|
102
|
+
export function startedHere(request, state) {
|
|
103
|
+
const held = new RegExp(`(?:^|;\\s*)${SIGN_IN_COOKIE}=([A-Za-z0-9_-]{16,128})`).exec(request.headers.cookie ?? "")?.[1];
|
|
104
|
+
return held !== undefined && held.length === state.length && timingSafeEqual(Buffer.from(held), Buffer.from(state));
|
|
105
|
+
}
|
|
106
|
+
export const signInSpent = (path) => `${SIGN_IN_COOKIE}=; Path=${path}; Max-Age=0; HttpOnly; SameSite=Lax`;
|
|
107
|
+
/**
|
|
108
|
+
* Off to another site's sign-in (Google, a service's one-click connection):
|
|
109
|
+
* a page that moves on by itself. Not a redirect: every page's form-action
|
|
110
|
+
* 'self' also covers where a form's answer redirects, so a browser stops a
|
|
111
|
+
* form that is answered with another site's address.
|
|
112
|
+
*/
|
|
113
|
+
export function goOutside(response, to, words, bind) {
|
|
114
|
+
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",
|
|
115
|
+
"set-cookie": `${SIGN_IN_COOKIE}=${bind.state}; Path=${bind.path}; Max-Age=900; HttpOnly; SameSite=Lax${bind.secure ? "; Secure" : ""}`,
|
|
116
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'" });
|
|
117
|
+
response.end(htmlString(html `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="refresh" content="0;url=${to}"><title>Toolroll</title>${HANDOFF_STYLE}<p>${words} <a href="${to}">Continue</a></p>`));
|
|
118
|
+
}
|
|
119
|
+
export function redirect(response, to) {
|
|
120
|
+
response.writeHead(303, { ...SAFETY, Location: to });
|
|
121
|
+
response.end();
|
|
122
|
+
}
|
|
123
|
+
export const taskChatHref = (taskId) => `/chat?task=${encodeURIComponent(taskId)}`;
|
|
124
|
+
/** A project's own lead thread (v77). */
|
|
125
|
+
export const projectChatHref = (repo) => `/chat?project=${encodeURIComponent(repo)}`;
|
|
126
|
+
export function safeReturn(raw) {
|
|
127
|
+
if (raw === null || raw === undefined)
|
|
128
|
+
return "/";
|
|
129
|
+
// A backslash is a slash to a browser's URL parser (`/\evil` → `//evil`), so it is refused too (v3 review, finding 10).
|
|
130
|
+
if (!raw.startsWith("/") || raw.startsWith("//") || raw.includes("\\") || /[\r\n\t\u0000-\u001f]/.test(raw) || raw.length > 512)
|
|
131
|
+
return "/";
|
|
132
|
+
try {
|
|
133
|
+
const path = decodeURIComponent(new URL(raw, "http://standing-orders.local").pathname);
|
|
134
|
+
if (path.startsWith("//") || path.includes("\\") || /[\u0000-\u001f\u007f]/.test(path))
|
|
135
|
+
return "/";
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return "/";
|
|
139
|
+
}
|
|
140
|
+
return raw;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The destination a sign-in returns to (a phone's deep link to an exact
|
|
144
|
+
* task or result): `safeReturn`, then narrower still — never the sign-in,
|
|
145
|
+
* sign-up, join or sign-out roads themselves (a loop, or a token in a
|
|
146
|
+
* path), never an encoded second scheme or host once the browser decodes
|
|
147
|
+
* it, never a query key that could carry a secret, and never a region
|
|
148
|
+
* fetch (`?fragment=`), which is a piece of a page and not a place to
|
|
149
|
+
* land. Only the app's own pages qualify: the task lens, the chat, the
|
|
150
|
+
* work and board views, and the fixed control destinations a phone
|
|
151
|
+
* button can name. Nothing here is a second redirect framework: one
|
|
152
|
+
* same-site path, or "/".
|
|
153
|
+
*/
|
|
154
|
+
export const LOGIN_RETURN_PAGES = /^\/(oauth\/authorize|t\/[^/]+|r\/[1-9]\d*|code(?:\/[a-f0-9]{32}(?:\/ship)?)?|review|chat|work|board|projects|routines|recipes|fleet|settings(\/[a-z-]+)?|mode)$/;
|
|
155
|
+
export function loginReturn(raw) {
|
|
156
|
+
const safe = safeReturn(raw);
|
|
157
|
+
if (safe === "/")
|
|
158
|
+
return "/";
|
|
159
|
+
let parsed;
|
|
160
|
+
try {
|
|
161
|
+
parsed = new URL(safe, "http://standing-orders.local");
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return "/";
|
|
165
|
+
}
|
|
166
|
+
if (parsed.origin !== "http://standing-orders.local" || parsed.username !== "" || parsed.password !== "")
|
|
167
|
+
return "/";
|
|
168
|
+
let decodedPath;
|
|
169
|
+
try {
|
|
170
|
+
decodedPath = decodeURIComponent(parsed.pathname);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return "/";
|
|
174
|
+
}
|
|
175
|
+
if (/^\/\/|\\|[\r\n\t\u0000-\u001f]/.test(decodedPath) || /^\/(login|logout|signup|join)(\/|$)/.test(decodedPath))
|
|
176
|
+
return "/";
|
|
177
|
+
if (!LOGIN_RETURN_PAGES.test(decodedPath) || parsed.searchParams.has("fragment"))
|
|
178
|
+
return "/";
|
|
179
|
+
for (const key of [...parsed.searchParams.keys()]) {
|
|
180
|
+
if (/token|password|secret|csrf|code|key|auth/i.test(key))
|
|
181
|
+
parsed.searchParams.delete(key);
|
|
182
|
+
}
|
|
183
|
+
const query = parsed.searchParams.toString();
|
|
184
|
+
return `${parsed.pathname}${query === "" ? "" : `?${query}`}`;
|
|
185
|
+
}
|
|
186
|
+
/** A request path fit for a log: the one-time sign-in link's code is left out. */
|
|
187
|
+
export function redactedPath(path) {
|
|
188
|
+
return path.startsWith(SIGN_IN_LINK_PATH) ? `${SIGN_IN_LINK_PATH}…` : path;
|
|
189
|
+
}
|
|
190
|
+
/** The sign-in page that comes back to `path` afterwards, or plain /login when the path is not one to come back to. */
|
|
191
|
+
export function loginHref(path) {
|
|
192
|
+
const back = loginReturn(path);
|
|
193
|
+
return back === "/" ? "/login" : `/login?return=${encodeURIComponent(back)}`;
|
|
194
|
+
}
|
|
195
|
+
/** Chat actions may return only to the unified chat or one task-focused
|
|
196
|
+
* lens. Other same-site paths are valid elsewhere, but not for chat forms. */
|
|
197
|
+
export function safeChatReturn(raw) {
|
|
198
|
+
const safe = safeReturn(raw);
|
|
199
|
+
try {
|
|
200
|
+
const parsed = new URL(safe, "http://standing-orders.local");
|
|
201
|
+
if (parsed.pathname !== "/chat")
|
|
202
|
+
return "/chat";
|
|
203
|
+
const conversation = parsed.searchParams.get('conversation');
|
|
204
|
+
if (conversation && /^[a-f0-9-]{36}$/.test(conversation))
|
|
205
|
+
return `/chat?conversation=${encodeURIComponent(conversation)}`;
|
|
206
|
+
if (parsed.searchParams.get('private') === '1')
|
|
207
|
+
return '/chat?private=1';
|
|
208
|
+
const task = parsed.searchParams.get("task");
|
|
209
|
+
return task !== null && task.length > 0 && task.length <= 64 && !hasForbiddenControls(task)
|
|
210
|
+
? taskChatHref(task)
|
|
211
|
+
: "/chat";
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return "/chat";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export function chatReturnWithSaid(back, said) {
|
|
218
|
+
return `${back}${back.includes("?") ? "&" : "?"}said=${encodeURIComponent(said)}`;
|
|
219
|
+
}
|
|
220
|
+
export const chatReturnWithLatest = (back) => `${back}#latest`;
|
|
221
|
+
/** The one result page's address: the task and its run, never a project path
|
|
222
|
+
* (the page finds the result's project itself and keeps the person's own). */
|
|
223
|
+
export const reviewHref = (taskId, runId = null) => `/review?result=${encodeURIComponent(taskId)}${runId === null ? "" : `&run=${runId}`}`;
|
|
224
|
+
/** The chat's result detail (package 3): the same panel the run page
|
|
225
|
+
* and the cockpit render, opened beside the conversation when the screen
|
|
226
|
+
* has room and as a dedicated view with Back to chat when it does not. */
|
|
227
|
+
export const chatResultHref = (taskId, runId, tab = "summary") => sharedResultHref(taskId, runId, tab);
|
|
228
|
+
// ---- request plumbing ------------------------------------------------------
|
|
229
|
+
export async function form(request, cap = BODY_CAP) {
|
|
230
|
+
const chunks = [];
|
|
231
|
+
let size = 0;
|
|
232
|
+
for await (const chunk of request) {
|
|
233
|
+
size += chunk.length;
|
|
234
|
+
if (size > cap)
|
|
235
|
+
throw new Error("body too large");
|
|
236
|
+
chunks.push(chunk);
|
|
237
|
+
}
|
|
238
|
+
return new URLSearchParams(Buffer.concat(chunks).toString("utf8"));
|
|
239
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type LiveBus } from "../live-bus.js";
|
|
2
|
+
import type { WorkspaceRevision } from "../workspace-revision.js";
|
|
3
|
+
import type { ServerRuntime } from "./runtime.js";
|
|
4
|
+
/** A page asks for a few rooms at once: its workspace, what it shows, its conversation. */
|
|
5
|
+
export declare const LIVE_ROOMS_PER_PAGE = 5;
|
|
6
|
+
export type LiveRoom = {
|
|
7
|
+
kind: "workspace";
|
|
8
|
+
} | {
|
|
9
|
+
kind: "task";
|
|
10
|
+
task: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: "flow";
|
|
13
|
+
flow: number;
|
|
14
|
+
card: number | null;
|
|
15
|
+
editing: boolean;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "chat";
|
|
18
|
+
task: string | null;
|
|
19
|
+
project: string | null;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "team";
|
|
22
|
+
conversation: string | null;
|
|
23
|
+
};
|
|
24
|
+
/** A room's name, read strictly: anything else is not a room. */
|
|
25
|
+
export declare function parseLiveRoom(name: string): LiveRoom | null;
|
|
26
|
+
export declare function createLiveHandlers(runtime: ServerRuntime, live: {
|
|
27
|
+
bus: LiveBus;
|
|
28
|
+
workspaceRevision: WorkspaceRevision;
|
|
29
|
+
chatProjectOf: (projects: string[]) => string | null | undefined;
|
|
30
|
+
}): {
|
|
31
|
+
registrations: import("./handler-registry.js").Registration[];
|
|
32
|
+
turnStarted: (thread: number) => void;
|
|
33
|
+
close(): void;
|
|
34
|
+
};
|