toolroll 0.9.48 → 0.9.50
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/dist/cli-http.js +3 -0
- package/dist/desktop-update.d.ts +2 -2
- package/dist/flow-triggers.js +7 -0
- package/dist/mcp-http.d.ts +1 -0
- package/dist/mcp-http.js +12 -15
- package/dist/mcp-oauth.d.ts +3 -0
- package/dist/mcp-oauth.js +13 -21
- package/dist/mcp-person.d.ts +0 -9
- package/dist/mcp-person.js +2 -1
- package/dist/operate.js +4 -1
- package/dist/public-access.d.ts +3 -1
- package/dist/public-access.js +5 -1
- package/dist/request-budget.d.ts +30 -3
- package/dist/request-budget.js +44 -1
- package/dist/serve.d.ts +2 -605
- package/dist/serve.js +462 -23733
- package/dist/server/chat.d.ts +41 -0
- package/dist/server/chat.js +1458 -0
- package/dist/server/edge-refusal.d.ts +5 -0
- package/dist/server/edge-refusal.js +56 -0
- package/dist/server/flows.d.ts +12 -0
- package/dist/server/flows.js +832 -0
- package/dist/server/guards.d.ts +72 -0
- package/dist/server/guards.js +357 -0
- package/dist/server/handler-context.d.ts +22 -0
- package/dist/server/handler-context.js +1 -0
- package/dist/server/handler-registry.d.ts +21 -0
- package/dist/server/handler-registry.js +34 -0
- package/dist/server/pages-assets.d.ts +10 -0
- package/dist/server/pages-assets.js +103 -0
- package/dist/server/pages.d.ts +11 -0
- package/dist/server/pages.js +910 -0
- package/dist/server/people-tokens.d.ts +20 -0
- package/dist/server/people-tokens.js +694 -0
- package/dist/server/remote-hooks.d.ts +29 -0
- package/dist/server/remote-hooks.js +130 -0
- package/dist/server/remote.d.ts +59 -0
- package/dist/server/remote.js +185 -0
- package/dist/server/route-policy.d.ts +18 -0
- package/dist/server/route-policy.js +30 -0
- package/dist/server/route-table.d.ts +74 -0
- package/dist/server/route-table.js +350 -0
- package/dist/server/runtime.d.ts +280 -0
- package/dist/server/runtime.js +25 -0
- package/dist/server/settings.d.ts +22 -0
- package/dist/server/settings.js +2388 -0
- package/dist/server/shared.d.ts +3334 -0
- package/dist/server/shared.js +12708 -0
- package/dist/server/task-views.d.ts +82 -0
- package/dist/server/task-views.js +1099 -0
- package/dist/server/tasks.d.ts +87 -0
- package/dist/server/tasks.js +3579 -0
- package/dist/session-http.d.ts +5 -0
- package/dist/session-http.js +16 -0
- package/dist/session-server.d.ts +5 -0
- package/dist/session-server.js +2 -0
- package/dist/team-cli.js +28 -6
- package/dist/team-contract.d.ts +7 -0
- package/dist/team-contract.js +7 -0
- package/dist/team-http.d.ts +10 -1
- package/dist/team-http.js +23 -3
- package/dist/team-leads.d.ts +5 -1
- package/dist/team-leads.js +42 -4
- package/dist/team-runtime.d.ts +1 -1
- package/dist/team-runtime.js +3 -0
- package/dist/teams.d.ts +3 -0
- package/dist/teams.js +18 -0
- package/package.json +1 -1
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ROUTE TABLE: every address this server answers, in one ordered list.
|
|
3
|
+
*
|
|
4
|
+
* Each row declares its method, its exact pattern, which callers may reach it,
|
|
5
|
+
* the scope it needs, how its project is found, and what a project-limited
|
|
6
|
+
* account may do there. The server matches a request against this table before
|
|
7
|
+
* any handler runs; a request no row declares is refused (fail closed). The
|
|
8
|
+
* scattered allowlists that used to repeat these facts by hand (the limited
|
|
9
|
+
* account's read/write sets, the viewer's POST set, the selected-project
|
|
10
|
+
* exceptions) are projections of this table.
|
|
11
|
+
*
|
|
12
|
+
* Handlers still re-prove what can change after admission: passwords, nonces,
|
|
13
|
+
* webhook signatures, object membership, stale sessions and revoked tokens.
|
|
14
|
+
*/
|
|
15
|
+
const CONSOLE = ["cookie", "bearer"];
|
|
16
|
+
const exact = (path) => `^${path.replace(/[.*+?^${}()|[\]\\/]/g, m => (m === "/" ? "\\/" : `\\${m}`))}$`;
|
|
17
|
+
const sampleOf = (pattern) => {
|
|
18
|
+
// Literal patterns sample themselves; others must name a sample.
|
|
19
|
+
if (!/^\^[^()[\]?*+{}|]*\$$/.test(pattern.replace(/\\[./-]/g, "")))
|
|
20
|
+
throw new Error(`route ${pattern} needs a sample`);
|
|
21
|
+
return pattern.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
22
|
+
};
|
|
23
|
+
const rows = [];
|
|
24
|
+
function row(stage, domain, id, method, pattern, options = {}) {
|
|
25
|
+
const console_ = stage === "console";
|
|
26
|
+
rows.push({
|
|
27
|
+
id, domain, stage, method, pattern,
|
|
28
|
+
callers: options.callers ?? (options.scope === "step-up" ? ["cookie"] : console_ ? CONSOLE : ["anonymous"]),
|
|
29
|
+
scope: options.scope ?? (console_ ? (method === "POST" ? "act" : "read") : "none"),
|
|
30
|
+
project: options.project ?? (console_ ? "session" : "none"),
|
|
31
|
+
limited: options.limited ?? "deny",
|
|
32
|
+
needsProject: options.needsProject ?? (console_ && method === "GET"),
|
|
33
|
+
viewer: options.viewer ?? false,
|
|
34
|
+
host: options.host ?? "after",
|
|
35
|
+
proof: options.proof ?? (console_ ? "console" : "public"),
|
|
36
|
+
sample: options.sample ?? sampleOf(pattern),
|
|
37
|
+
...(options.scopeRefusal === undefined ? {} : { scopeRefusal: options.scopeRefusal }),
|
|
38
|
+
...(options.callerRefusal === undefined ? {} : { callerRefusal: options.callerRefusal }),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const edge = (domain, id, method, path, options = {}) => row("edge", domain, id, method, path.startsWith("^") ? path : exact(path), options);
|
|
42
|
+
const get = (domain, id, path, options = {}) => row("console", domain, id, "GET", path.startsWith("^") ? path : exact(path), options);
|
|
43
|
+
const post = (domain, id, path, options = {}) => row("console", domain, id, "POST", path.startsWith("^") ? path : exact(path), options);
|
|
44
|
+
const ID = "[0-9]{1,15}";
|
|
45
|
+
const FLOW = "[1-9][0-9]{0,9}";
|
|
46
|
+
const TASK = "[^/]+";
|
|
47
|
+
/** Free of a project: no opener redirect for a session with nothing open. */
|
|
48
|
+
const free = { needsProject: false };
|
|
49
|
+
// ---- edge: answered before the console's sign-in ---------------------------------------------------------------
|
|
50
|
+
// Ordered exactly as the server checks them: health and webhooks before the Host check, the rest after it.
|
|
51
|
+
edge("pages", "edge.healthz", "GET,HEAD", "/healthz", { host: "before" });
|
|
52
|
+
edge("remote", "edge.telegram-hook", "POST", "/hooks/telegram", { callers: ["service"], host: "before", proof: "adapter" });
|
|
53
|
+
edge("remote", "edge.flow-form", "GET,HEAD", "^\\/hooks\\/form\\/[^/]+$", { project: "flow", sample: "/hooks/form/abc", host: "before", proof: "adapter" });
|
|
54
|
+
edge("remote", "edge.flow-form-send", "POST", "^\\/hooks\\/form\\/[^/]+$", { project: "flow", sample: "/hooks/form/abc", host: "before", proof: "adapter" });
|
|
55
|
+
edge("remote", "edge.flow-hook", "POST", "^\\/hooks\\/flow\\/[^/]+$", { callers: ["service"], project: "flow", sample: "/hooks/flow/abc", host: "before", proof: "adapter" });
|
|
56
|
+
// MCP and CLI multiplex read/act operations in POST bodies. Their minimum is read;
|
|
57
|
+
// the same evaluator receives the operation capability after protocol decoding.
|
|
58
|
+
edge("remote", "edge.mcp", "POST", "/mcp", { callers: ["bearer", "oauth", "coordinator"], scope: "read", project: "adapter", proof: "adapter" });
|
|
59
|
+
edge("remote", "edge.oauth-discovery", "GET,HEAD", "^\\/\\.well-known\\/(oauth-protected-resource(\\/mcp)?|oauth-authorization-server)$", { sample: "/.well-known/oauth-authorization-server" });
|
|
60
|
+
edge("remote", "edge.oauth-register", "POST", "/oauth/register");
|
|
61
|
+
edge("remote", "edge.oauth-token", "POST", "/oauth/token", { proof: "adapter", callers: ["oauth"] });
|
|
62
|
+
edge("remote", "edge.oauth-authorize", "GET,HEAD", "/oauth/authorize");
|
|
63
|
+
edge("remote", "edge.oauth-consent", "POST", "/oauth/authorize", { callers: ["cookie"], scope: "step-up", viewer: true, proof: "adapter" });
|
|
64
|
+
edge("settings", "edge.google-callback", "GET", "/settings/google/callback", { callers: ["service"], proof: "adapter" });
|
|
65
|
+
edge("settings", "edge.connect-callback", "GET", "/settings/tools/connected", { callers: ["service"], proof: "adapter" });
|
|
66
|
+
for (const operation of ["list", "show", "changes", "start", "send", "stop", "resume", "recover"]) {
|
|
67
|
+
edge("remote", `edge.sessions-${operation}`, "POST", `/api/sessions/${operation}`, { callers: ["bearer"], scope: ["list", "show", "changes"].includes(operation) ? "read" : "act", project: "adapter", proof: "adapter" });
|
|
68
|
+
}
|
|
69
|
+
edge("remote", "edge.team-read", "GET", "/api/team", { callers: ["cookie", "bearer"], scope: "read", project: "adapter", proof: "adapter" });
|
|
70
|
+
edge("remote", "edge.team-send", "POST", "/api/team", { callers: ["cookie", "bearer"], scope: "read", project: "adapter", proof: "adapter" });
|
|
71
|
+
edge("remote", "edge.team-events", "GET", "/api/team/events", { callers: ["cookie", "bearer"], scope: "read", project: "adapter", proof: "adapter" });
|
|
72
|
+
edge("remote", "edge.cli", "POST", "/api/cli", { callers: ["bearer"], scope: "read", project: "adapter", proof: "adapter" });
|
|
73
|
+
edge("remote", "edge.teams", "POST", "/teams/messages", { callers: ["service"], proof: "adapter" });
|
|
74
|
+
edge("pages", "edge.browser-asset", "GET,HEAD", "^\\/assets\\/(workspace\\.js|workspace\\.css|THIRD_PARTY_NOTICES\\.txt)$", { sample: "/assets/workspace.js" });
|
|
75
|
+
edge("pages", "edge.workspace-style", "GET,HEAD", "^\\/assets\\/workspace-[0-9a-f]{64}\\.css$", { sample: `/assets/workspace-${"0".repeat(64)}.css` });
|
|
76
|
+
edge("pages", "edge.install-asset", "GET", "^\\/(manifest\\.webmanifest|favicon\\.ico|icon\\.svg|icon-192\\.png|icon-512\\.png|apple-touch-icon\\.png|sw\\.js)$", { sample: "/sw.js" });
|
|
77
|
+
edge("pages", "edge.font", "GET", "^\\/fonts\\/geist-(sans|mono)-(400|500|600)\\.woff2$", { sample: "/fonts/geist-sans-400.woff2" });
|
|
78
|
+
edge("pages", "edge.desktop-health", "GET", "/desktop/health");
|
|
79
|
+
edge("people", "edge.login-page", "GET", "/login");
|
|
80
|
+
edge("people", "edge.login-link", "GET", "^\\/login\\/once\\/[^/]+$", { sample: "/login/once/abc" });
|
|
81
|
+
edge("people", "edge.sso-start", "GET", "/login/sso");
|
|
82
|
+
edge("people", "edge.sso-callback", "GET", "/login/sso/callback", { callers: ["service"], proof: "adapter" });
|
|
83
|
+
edge("people", "edge.sso-finish", "GET", "/login/sso/finish");
|
|
84
|
+
edge("people", "edge.signup", "POST", "/signup");
|
|
85
|
+
edge("people", "edge.login", "POST", "/login");
|
|
86
|
+
edge("people", "edge.logout", "POST", "/logout", { callers: ["anonymous", "cookie"] });
|
|
87
|
+
edge("people", "edge.join", "GET", "^\\/join\\/[A-Za-z0-9_-]{16,64}$", { sample: "/join/abcdefghijklmnop" });
|
|
88
|
+
edge("people", "edge.join-send", "POST", "^\\/join\\/[A-Za-z0-9_-]{16,64}$", { sample: "/join/abcdefghijklmnop" });
|
|
89
|
+
// ---- console reads ---------------------------------------------------------------------------------------------
|
|
90
|
+
// Coding: the handler answers any other caller in its own format (JSON for the workspace's fetches).
|
|
91
|
+
get("tasks", "code.page", "^\\/code(\\/.*)?$", { project: "coding", limited: "self", ...free, sample: "/code" });
|
|
92
|
+
get("pages", "projects.browse", "/projects/browse", free);
|
|
93
|
+
get("pages", "projects.github", "/projects/github", free);
|
|
94
|
+
get("pages", "projects.page", "/projects", { limited: "unscoped", ...free });
|
|
95
|
+
get("pages", "home", "/", { limited: "collection", ...free });
|
|
96
|
+
get("tasks", "inbox", "/inbox", { limited: "collection", ...free });
|
|
97
|
+
get("tasks", "work", "/work", { limited: "collection", ...free });
|
|
98
|
+
get("tasks", "next", "/next", { limited: "collection" });
|
|
99
|
+
get("pages", "morning", "/morning");
|
|
100
|
+
get("pages", "workbench", "/workbench", free);
|
|
101
|
+
get("tasks", "board", "/board", { limited: "collection" });
|
|
102
|
+
get("tasks", "review", "/review", { limited: "collection" });
|
|
103
|
+
get("settings", "spend", "/spend", free);
|
|
104
|
+
get("pages", "ledger", "/ledger", { limited: "unscoped", ...free });
|
|
105
|
+
get("pages", "activity", "/activity");
|
|
106
|
+
get("tasks", "done", "/done", { limited: "collection" });
|
|
107
|
+
get("settings", "mode", "/mode");
|
|
108
|
+
get("people", "people.page", "/people", { limited: "unscoped", ...free });
|
|
109
|
+
get("pages", "system", "/system");
|
|
110
|
+
get("tasks", "tasks", "/tasks", { limited: "collection" });
|
|
111
|
+
get("tasks", "queue", "/queue");
|
|
112
|
+
get("pages", "peek", "/peek");
|
|
113
|
+
get("pages", "fleet", "/fleet", free);
|
|
114
|
+
get("tasks", "tasks.new", "/tasks/new", { limited: "collection", ...free });
|
|
115
|
+
get("tasks", "task.live", `^\\/t\\/${TASK}\\/live$`, { project: "task", limited: "resource", ...free, sample: "/t/one/live" });
|
|
116
|
+
get("tasks", "task.page", `^\\/t\\/${TASK}$`, { project: "task", limited: "resource", ...free, sample: "/t/one" });
|
|
117
|
+
get("tasks", "task.evidence", `^\\/t\\/${TASK}\\/evidence$`, { project: "task", limited: "resource", ...free, sample: "/t/one/evidence" });
|
|
118
|
+
get("pages", "ledger.export", "/ledger/export", { limited: "unscoped", ...free });
|
|
119
|
+
get("tasks", "runs", "/runs", { limited: "collection" });
|
|
120
|
+
get("pages", "menu", "/menu", { limited: "collection", ...free });
|
|
121
|
+
get("tasks", "run.page", `^\\/r\\/${ID}$`, { project: "run", limited: "resource", ...free, sample: "/r/1" });
|
|
122
|
+
get("tasks", "run.evidence", `^\\/r\\/${ID}\\/evidence\\/${ID}$`, { project: "run", limited: "resource", ...free, sample: "/r/1/evidence/2" });
|
|
123
|
+
get("tasks", "contest.page", `^\\/contest\\/${ID}$`, { ...free, sample: "/contest/1" });
|
|
124
|
+
get("pages", "caps", "/caps");
|
|
125
|
+
get("flows", "flows.new", "/flows/new", { limited: "resource", ...free });
|
|
126
|
+
get("flows", "flows.gallery", "^\\/flows\\/new\\/[a-z-]{1,40}$", { limited: "resource", ...free, sample: "/flows/new/triage" });
|
|
127
|
+
get("flows", "flows.page", "/flows", { limited: "resource", ...free });
|
|
128
|
+
get("flows", "kits", "/kits");
|
|
129
|
+
get("flows", "kit.page", "^\\/kits\\/[a-z-]{1,40}$", { sample: "/kits/support" });
|
|
130
|
+
get("flows", "teammates", "/teammates", free);
|
|
131
|
+
get("flows", "teammate.page", `^\\/teammates\\/[1-9][0-9]{0,9}(\\/soul\\.md)?$`, { ...free, sample: "/teammates/1/soul.md" });
|
|
132
|
+
get("flows", "flow.read", `^\\/flows\\/${FLOW}\\/(insights|runs\\/${FLOW}\\/${FLOW})$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1/runs/2/3" });
|
|
133
|
+
get("flows", "flow.export", `^\\/flows\\/${FLOW}\\/export$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1/export" });
|
|
134
|
+
get("flows", "flow.live", `^\\/flows\\/${FLOW}\\/live$`, { callers: ["cookie"], project: "flow", limited: "resource", ...free, sample: "/flows/1/live",
|
|
135
|
+
callerRefusal: { status: 403, type: "application/json", body: JSON.stringify({ error: "session" }) } });
|
|
136
|
+
get("flows", "flow.page", `^\\/flows\\/${FLOW}$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1" });
|
|
137
|
+
get("flows", "recipes", "/recipes", { limited: "collection", ...free });
|
|
138
|
+
for (const one of ["run", "start", "new", "edit", "from-task", "preview", "export"])
|
|
139
|
+
get("flows", `recipes.${one}`, `/recipes/${one}`, { limited: "collection" });
|
|
140
|
+
// Any other recipe screen answers the recipe area's own refusal.
|
|
141
|
+
get("flows", "recipes.other", "^\\/recipes\\/.*$", { sample: "/recipes/unknown" });
|
|
142
|
+
get("tasks", "routines", "/routines", { limited: "collection" });
|
|
143
|
+
get("chat", "chat.stream", "/chat/stream", free);
|
|
144
|
+
get("chat", "chat.mate-status", "/chat/mate/status", free);
|
|
145
|
+
get("chat", "chat.demo-live", "/chat/demo/live", free);
|
|
146
|
+
get("chat", "chat.page", "/chat", { project: "conversation", limited: "conversation", ...free });
|
|
147
|
+
get("chat", "chat.ack", `^\\/chat\\/ack\\/${ID}$`, { sample: "/chat/ack/1" });
|
|
148
|
+
get("tasks", "routine.page", `^\\/routines\\/${ID}$`, { project: "routine", limited: "resource", sample: "/routines/1" });
|
|
149
|
+
get("settings", "control", "/control");
|
|
150
|
+
get("settings", "control.connection", "/control/connection");
|
|
151
|
+
get("chat", "chat.action", `^\\/chat\\/action\\/${ID}$`, { project: "proposal", limited: "proposal", ...free, sample: "/chat/action/1" });
|
|
152
|
+
for (const one of ["models", "tools", "project", "approval", "policy", "sessions", "integrations", "monitoring", "updates", "retention", "storage", "pull-requests", "checks", "backups", "data", "sign-in", "lead", "teams", "discord", "slack", "slack/manifest"]) {
|
|
153
|
+
get(one === "sessions" ? "people" : "settings", `settings.${one}`, `/settings/${one}`, free);
|
|
154
|
+
}
|
|
155
|
+
get("settings", "settings.skills", "/settings/skills", { limited: "unscoped", ...free });
|
|
156
|
+
get("settings", "settings.flows", "/settings/flows", { limited: "collection", ...free });
|
|
157
|
+
get("settings", "settings.knowledge", "/settings/knowledge", { limited: "unscoped", ...free });
|
|
158
|
+
get("settings", "settings.learning", "/settings/learning", { limited: "unscoped", ...free });
|
|
159
|
+
get("settings", "settings.telegram", "/settings/telegram", { limited: "self", ...free });
|
|
160
|
+
get("settings", "settings.page", "/settings", { limited: "unscoped", ...free });
|
|
161
|
+
get("pages", "health", "/health", free);
|
|
162
|
+
get("pages", "metrics", "/metrics", free);
|
|
163
|
+
get("chat", "push.key", "/push/key");
|
|
164
|
+
get("tasks", "decision.page", `^\\/d\\/${ID}$`, { project: "decision", limited: "resource", ...free, sample: "/d/1" });
|
|
165
|
+
get("chat", "lead.status", "/lead/status", free);
|
|
166
|
+
get("chat", "chat.task-status", "/chat/task-status", free);
|
|
167
|
+
get("tasks", "decision.evidence", `^\\/d\\/${ID}\\/evidence\\/${ID}$`, { project: "decision", limited: "resource", ...free, sample: "/d/1/evidence/2" });
|
|
168
|
+
// ---- console actions -------------------------------------------------------------------------------------------
|
|
169
|
+
// The attended beat guards itself (cookie, form type, Sec-Fetch-Site) before the shared mutation guard.
|
|
170
|
+
post("tasks", "session.attended-beats", "/session/attended-beats", { callers: ["cookie"], scopeRefusal: "your login can watch, not keep sessions alive", callerRefusal: { status: 403, type: "text/plain; charset=utf-8", body: "watching is a browser session's act" } });
|
|
171
|
+
// The scripted demo: the handler answers a non-browser caller "no page here".
|
|
172
|
+
post("chat", "chat.demo", "^\\/chat\\/demo\\/(ask|[0-9]{1,9}\\/(approve|change|revise|complete))$", { sample: "/chat/demo/ask" });
|
|
173
|
+
post("settings", "provider.resume", "^\\/providers\\/[a-z]+\\/resume$", { sample: "/providers/codex/resume" });
|
|
174
|
+
post("settings", "settings.updates-dismiss", "/settings/updates/dismiss");
|
|
175
|
+
post("settings", "settings.updates-checks", "/settings/updates/checks");
|
|
176
|
+
post("tasks", "code.act", "^\\/code(\\/.*)?$", { scope: "step-up", project: "coding", limited: "self", sample: "/code/start" });
|
|
177
|
+
post("settings", "settings.skills-revise", "/settings/skills/revise", { project: "form", limited: "unscoped" });
|
|
178
|
+
post("settings", "settings.skills-import", "/settings/skills/import", { project: "form", limited: "unscoped" });
|
|
179
|
+
post("settings", "settings.skills-change", "/settings/skills/change", { project: "form", limited: "unscoped" });
|
|
180
|
+
post("flows", "kit.act", "^\\/kits\\/[a-z-]{1,40}\\/(setup|sample|github)$", { project: "form", sample: "/kits/support/setup" });
|
|
181
|
+
post("flows", "teammate.new", "/teammates/new", { project: "form" });
|
|
182
|
+
post("flows", "teammate.act", "^\\/teammates\\/[1-9][0-9]{0,9}\\/(soul|state|note|settings|summary|tools|memory|routines|week)$", { sample: "/teammates/1/soul" });
|
|
183
|
+
post("flows", "teammate.answer", "^\\/teammates\\/questions\\/[1-9][0-9]{0,9}\\/answer$", { sample: "/teammates/questions/1/answer" });
|
|
184
|
+
post("flows", "flows.gallery-create", "^\\/flows\\/new\\/[a-z-]{1,40}$", { project: "form", limited: "resource", sample: "/flows/new/triage" });
|
|
185
|
+
post("flows", "flows.create", "/flows/new", { project: "form", limited: "resource" });
|
|
186
|
+
post("flows", "flows.example", "/flows/example", { project: "form" });
|
|
187
|
+
post("flows", "flows.import", "/flows/import", { project: "form", limited: "resource" });
|
|
188
|
+
post("flows", "flow.act", `^\\/flows\\/${FLOW}\\/(save|cards|archive|scripts)$`, { project: "flow", limited: "resource", sample: "/flows/1/save" });
|
|
189
|
+
post("flows", "flow.instance-act", `^\\/flows\\/${FLOW}\\/(linear-key|hooks-address|secrets)$`, { scope: "step-up", project: "flow", sample: "/flows/1/secrets" });
|
|
190
|
+
post("flows", "flow.triggers", `^\\/flows\\/${FLOW}\\/triggers$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers" });
|
|
191
|
+
post("flows", "flow.card", `^\\/flows\\/${FLOW}\\/cards\\/${FLOW}\\/(move|decide|cancel|comment|assign|watch)$`, { project: "flow", limited: "resource", sample: "/flows/1/cards/2/move" });
|
|
192
|
+
post("flows", "flow.card-choose", `^\\/flows\\/${FLOW}\\/cards\\/${FLOW}\\/choose$`, { project: "flow", sample: "/flows/1/cards/2/choose" });
|
|
193
|
+
post("flows", "flow.trigger.pause", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/pause$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/pause" });
|
|
194
|
+
post("flows", "flow.trigger.resume", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/resume$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/resume" });
|
|
195
|
+
post("flows", "flow.trigger.remove", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/remove$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/remove" });
|
|
196
|
+
post("flows", "flow.trigger.check", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/check$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/check" });
|
|
197
|
+
post("flows", "flow.trigger.press", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/press$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/press" });
|
|
198
|
+
post("flows", "flow.trigger.renew", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/renew$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/renew" });
|
|
199
|
+
post("flows", "flow.trigger.secret", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/secret$`, { scope: "step-up", project: "flow", limited: "resource", sample: "/flows/1/triggers/2/secret" });
|
|
200
|
+
post("flows", "flow.trigger.share", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/share$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/share" });
|
|
201
|
+
post("flows", "flow.trigger.unshare", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/unshare$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/unshare" });
|
|
202
|
+
post("settings", "spend.budget", "/spend/budget", { scope: "step-up" });
|
|
203
|
+
post("settings", "settings.project-concurrency", "/settings/project/concurrency");
|
|
204
|
+
post("settings", "settings.project-delete", "/settings/project/delete", { scope: "step-up" });
|
|
205
|
+
post("pages", "ledger.checkpoint", "/ledger/checkpoint");
|
|
206
|
+
for (const one of ["policy", "approval", "request-limits", "sessions", "sign-in", "updates", "updates/seen", "updates/cancel", "retention", "storage", "storage/clean", "storage/discard", "pull-requests", "checks", "backups/now", "backups", "data", "integrations/test", "monitoring", "tools/connect", "tools/change", "appearance"]) {
|
|
207
|
+
post(one === "sessions" ? "people" : "settings", `settings.${one.replaceAll("/", "-")}-send`, `/settings/${one}`, { ...(["policy", "approval", "request-limits", "sessions", "sign-in", "updates", "updates/seen", "updates/cancel", "retention", "storage", "storage/clean", "storage/discard", "pull-requests", "checks", "backups", "data", "monitoring", "tools/connect", "tools/change"].includes(one) ? { scope: "step-up" } : {}) });
|
|
208
|
+
}
|
|
209
|
+
post("settings", "settings.flows-on", "/settings/flows/on", { limited: "collection" });
|
|
210
|
+
// Any other models action answers the models area's own refusal.
|
|
211
|
+
post("settings", "settings.models-act", "^\\/settings\\/models\\/.*$", { sample: "/settings/models/check" });
|
|
212
|
+
post("settings", "settings.knowledge-refresh", "/settings/knowledge/refresh", { project: "form", limited: "unscoped" });
|
|
213
|
+
post("settings", "settings.knowledge-proposal", "/settings/knowledge/proposal", { project: "form" });
|
|
214
|
+
post("settings", "settings.knowledge-decision", "/settings/knowledge/decision", { project: "form" });
|
|
215
|
+
post("settings", "settings.knowledge-change", "/settings/knowledge/change", { project: "form", limited: "unscoped" });
|
|
216
|
+
post("settings", "settings.learning-change", "/settings/learning/change", { project: "form", limited: "unscoped" });
|
|
217
|
+
for (const one of ["setup-preview", "setup-approve", "instructions-preview", "instructions-approve"])
|
|
218
|
+
post("settings", `control.${one}`, `/control/${one}`, { ...(one.endsWith("approve") ? { scope: "step-up" } : {}) });
|
|
219
|
+
for (const service of ["slack", "teams", "discord"])
|
|
220
|
+
post("settings", `settings.${service}-send`, `^\\/settings\\/${service}\\/(connect|pair|unpair|disconnect|alerts)$`, { sample: `/settings/${service}/pair`, scope: "step-up" });
|
|
221
|
+
post("settings", "settings.telegram-retry", "/settings/telegram/retry", { limited: "self", scope: "step-up" });
|
|
222
|
+
for (const one of ["confirm", "save", "off"])
|
|
223
|
+
post("settings", `settings.chat-approval-${one}`, `/settings/chat-approval/${one}`, { limited: "self", callers: ["cookie"], ...(one === "save" ? { scope: "step-up" } : {}) });
|
|
224
|
+
post("settings", "settings.telegram-pair", "/settings/telegram/pair", { limited: "self", scope: "step-up" });
|
|
225
|
+
post("settings", "settings.telegram-unpair", "/settings/telegram/unpair", { limited: "self", scope: "step-up" });
|
|
226
|
+
for (const one of ["messaging", "permission-default", "quality-default", "notifications", "notifications/mute", "telegram-digest", "provider-key", "provider-key-clear", "telegram-token", "email", "email-test", "email-read-test", "google", "google/disconnect"]) {
|
|
227
|
+
post("settings", `settings.${one.replaceAll("/", "-")}-send`, `/settings/${one}`);
|
|
228
|
+
}
|
|
229
|
+
post("pages", "projects.select", "/projects/select", { project: "form-path", limited: "unscoped", viewer: true });
|
|
230
|
+
post("pages", "projects.remove", "/projects/remove", { project: "form" });
|
|
231
|
+
post("pages", "projects.open", "/projects/open", { project: "form-path" });
|
|
232
|
+
post("tasks", "tasks.add", "/tasks/add", { project: "form", limited: "collection" });
|
|
233
|
+
post("tasks", "queue.move", "/queue/move");
|
|
234
|
+
post("tasks", "queue.note", "/queue/note");
|
|
235
|
+
post("pages", "fleet.register", "/fleet/runner/register", { scope: "step-up" });
|
|
236
|
+
post("settings", "mode.confirm", "/mode/confirm", { scope: "step-up" });
|
|
237
|
+
post("settings", "mode.sign", "/mode/sign", { scope: "step-up" });
|
|
238
|
+
post("settings", "mode.revoke", "/mode/revoke");
|
|
239
|
+
post("people", "people.projects", "/people/projects", { scope: "step-up" });
|
|
240
|
+
post("people", "people.invite", "/people/invite", { scope: "step-up" });
|
|
241
|
+
post("people", "people.invite-revoke", "/people/invite-revoke", { scope: "step-up" });
|
|
242
|
+
post("people", "people.revoke", "/people/revoke", { scope: "step-up" });
|
|
243
|
+
post("pages", "fleet.retire", "/fleet/runner/retire", { scope: "step-up" });
|
|
244
|
+
// Answering a parked decision needs act access and irreversible-choice confirmation, not password step-up.
|
|
245
|
+
post("tasks", "decision.answer", `^\\/d\\/${ID}\\/answer$`, { project: "decision", limited: "resource", scope: "act", sample: "/d/1/answer" });
|
|
246
|
+
post("tasks", "contest.act.arm", `^\\/contest\\/${ID}\\/arm$`, { sample: "/contest/1/arm" });
|
|
247
|
+
post("tasks", "contest.act.pick", `^\\/contest\\/${ID}\\/pick$`, { scope: "step-up", sample: "/contest/1/pick" });
|
|
248
|
+
post("tasks", "contest.act.abandon", `^\\/contest\\/${ID}\\/abandon$`, { scope: "step-up", sample: "/contest/1/abandon" });
|
|
249
|
+
post("tasks", "task.attend.attend-preview", `^\\/t\\/${TASK}\\/attend-preview$`, { callers: ["cookie"], project: "task", sample: "/t/one/attend-preview" });
|
|
250
|
+
post("tasks", "task.attend.attend", `^\\/t\\/${TASK}\\/attend$`, { scope: "step-up", project: "task", sample: "/t/one/attend" });
|
|
251
|
+
post("tasks", "task.attend.attend-revoke", `^\\/t\\/${TASK}\\/attend-revoke$`, { callers: ["cookie"], project: "task", sample: "/t/one/attend-revoke" });
|
|
252
|
+
post("tasks", "task.act.hold", `^\\/t\\/${TASK}\\/hold$`, { project: "task", limited: "resource", sample: "/t/one/hold" });
|
|
253
|
+
post("tasks", "task.act.unhold", `^\\/t\\/${TASK}\\/unhold$`, { project: "task", limited: "resource", sample: "/t/one/unhold" });
|
|
254
|
+
post("tasks", "task.act.requeue", `^\\/t\\/${TASK}\\/requeue$`, { project: "task", limited: "resource", sample: "/t/one/requeue" });
|
|
255
|
+
post("tasks", "task.act.cancel", `^\\/t\\/${TASK}\\/cancel$`, { project: "task", limited: "resource", sample: "/t/one/cancel" });
|
|
256
|
+
post("tasks", "task.act.scope", `^\\/t\\/${TASK}\\/scope$`, { project: "task", limited: "resource", sample: "/t/one/scope" });
|
|
257
|
+
post("tasks", "task.act.approve", `^\\/t\\/${TASK}\\/approve$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/approve" });
|
|
258
|
+
post("tasks", "task.act.plan", `^\\/t\\/${TASK}\\/plan$`, { project: "task", limited: "resource", sample: "/t/one/plan" });
|
|
259
|
+
post("tasks", "task.act.plan-edit", `^\\/t\\/${TASK}\\/plan-edit$`, { project: "task", limited: "resource", sample: "/t/one/plan-edit" });
|
|
260
|
+
post("tasks", "task.act.next", `^\\/t\\/${TASK}\\/next$`, { project: "task", limited: "resource", sample: "/t/one/next" });
|
|
261
|
+
post("tasks", "task.act.reopen", `^\\/t\\/${TASK}\\/reopen$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/reopen" });
|
|
262
|
+
post("tasks", "task.act.steer", `^\\/t\\/${TASK}\\/steer$`, { project: "task", limited: "resource", sample: "/t/one/steer" });
|
|
263
|
+
post("tasks", "task.act.accept-proof", `^\\/t\\/${TASK}\\/accept-proof$`, { project: "task", limited: "resource", sample: "/t/one/accept-proof" });
|
|
264
|
+
post("tasks", "task.act.accept-revision", `^\\/t\\/${TASK}\\/accept-revision$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/accept-revision" });
|
|
265
|
+
post("tasks", "task.act.reject-revision", `^\\/t\\/${TASK}\\/reject-revision$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/reject-revision" });
|
|
266
|
+
post("tasks", "task.act.route", `^\\/t\\/${TASK}\\/route$`, { project: "task", limited: "resource", sample: "/t/one/route" });
|
|
267
|
+
post("tasks", "task.act.retry-review", `^\\/t\\/${TASK}\\/retry-review$`, { project: "task", limited: "resource", sample: "/t/one/retry-review" });
|
|
268
|
+
post("tasks", "task.act.complete", `^\\/t\\/${TASK}\\/complete$`, { project: "task", limited: "resource", sample: "/t/one/complete" });
|
|
269
|
+
post("tasks", "task.act.merge", `^\\/t\\/${TASK}\\/merge$`, { project: "task", limited: "resource", sample: "/t/one/merge" });
|
|
270
|
+
post("tasks", "task.act.confirm-stopped", `^\\/t\\/${TASK}\\/confirm-stopped$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/confirm-stopped" });
|
|
271
|
+
post("tasks", "task.act.stop", `^\\/t\\/${TASK}\\/stop$`, { project: "task", limited: "resource", sample: "/t/one/stop" });
|
|
272
|
+
post("tasks", "task.act.resume-arm", `^\\/t\\/${TASK}\\/resume-arm$`, { project: "task", limited: "resource", sample: "/t/one/resume-arm" });
|
|
273
|
+
post("tasks", "task.act.resume", `^\\/t\\/${TASK}\\/resume$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/resume" });
|
|
274
|
+
post("tasks", "task.instance-act", `^\\/t\\/${TASK}\\/(block|unblock|repair-dependency|follow-up)$`, { project: "task", sample: "/t/one/block" });
|
|
275
|
+
// Onboarding names a GitHub repository, not a local project. Its root-index/nonce ceremony proves the future destination.
|
|
276
|
+
post("pages", "projects.onboard-preview", "/projects/onboard-preview", { project: "none" });
|
|
277
|
+
post("pages", "projects.onboard-confirm", "/projects/onboard-confirm", { scope: "step-up", project: "none" });
|
|
278
|
+
post("chat", "push.subscribe", "/push/subscribe", { scope: "step-up" });
|
|
279
|
+
post("chat", "push.remove", "/push/remove");
|
|
280
|
+
post("chat", "onboarding.phone-dismiss", "/onboarding/phone/dismiss");
|
|
281
|
+
for (const one of ["identity", "about", "promise/cancel", "on"])
|
|
282
|
+
post("settings", `settings.lead-${one.replaceAll("/", "-")}`, `/settings/lead/${one}`);
|
|
283
|
+
post("chat", "chat.config", "/chat/config", { scope: "step-up" });
|
|
284
|
+
for (const one of ["mint", "follow", "end", "stop"])
|
|
285
|
+
post("chat", `chat.mate-${one}`, `/chat/mate/${one}`, { callers: ["cookie"], ...(one === "mint" ? { scope: "step-up" } : {}) });
|
|
286
|
+
post("chat", "chat.proposal", `^\\/chat\\/proposal\\/${ID}\\/(confirm|dismiss)$`, { project: "proposal", limited: "proposal", sample: "/chat/proposal/1/confirm" });
|
|
287
|
+
post("chat", "coordinator.proposal", `^\\/proposals\\/${ID}\\/(confirm|dismiss)$`, { sample: "/proposals/1/confirm" });
|
|
288
|
+
post("chat", "chat.send", "/chat", { scope: "step-up", project: "conversation" });
|
|
289
|
+
post("chat", "chat.file", "^\\/chat\\/file\\/[0-9a-f]{32}$", { scope: "step-up", sample: `/chat/file/${"a".repeat(32)}` });
|
|
290
|
+
post("chat", "chat.ack-send", `^\\/chat\\/ack\\/${ID}$`, { scope: "step-up", sample: "/chat/ack/1" });
|
|
291
|
+
for (const one of ["prepare", "preview", "import", "save"])
|
|
292
|
+
post("flows", `recipes.${one}-send`, `/recipes/${one}`, { limited: "collection" });
|
|
293
|
+
post("flows", "recipes.launch-send", "/recipes/launch", { limited: "collection" });
|
|
294
|
+
post("tasks", "routines.add", "/routines/add", { project: "form", limited: "collection" });
|
|
295
|
+
post("tasks", "routine.act.approve", `^\\/routines\\/${ID}\\/approve$`, { scope: "step-up", project: "routine", limited: "resource", sample: "/routines/1/approve" });
|
|
296
|
+
post("tasks", "routine.act.refresh", `^\\/routines\\/${ID}\\/refresh$`, { project: "routine", limited: "resource", sample: "/routines/1/refresh" });
|
|
297
|
+
post("tasks", "routine.act.pause", `^\\/routines\\/${ID}\\/pause$`, { project: "routine", limited: "resource", sample: "/routines/1/pause" });
|
|
298
|
+
post("tasks", "routine.act.resume", `^\\/routines\\/${ID}\\/resume$`, { project: "routine", limited: "resource", sample: "/routines/1/resume" });
|
|
299
|
+
post("tasks", "routine.act.run-now", `^\\/routines\\/${ID}\\/run-now$`, { scope: "step-up", project: "routine", limited: "resource", sample: "/routines/1/run-now" });
|
|
300
|
+
post("tasks", "run.act", `^\\/r\\/${ID}\\/(note|comment|revise|draft-repair|checks|add-tests)$`, { project: "run", limited: "resource", sample: "/r/1/note" });
|
|
301
|
+
post("tasks", "session.editor-links", "/session/editor-links", { viewer: true });
|
|
302
|
+
post("tasks", "run.turn", `^\\/r\\/${ID}\\/turn$`, { project: "run", sample: "/r/1/turn" });
|
|
303
|
+
post("tasks", "incident.resolve", `^\\/i\\/${ID}\\/resolve$`, { project: "incident", sample: "/i/1/resolve" });
|
|
304
|
+
/** The sole route table, in match order. */
|
|
305
|
+
export const ROUTES = Object.freeze(rows.map(one => Object.freeze(one)));
|
|
306
|
+
const compiled = ROUTES.map(route => ({ route, test: new RegExp(route.pattern) }));
|
|
307
|
+
const methodMatches = (declared, method) => declared === method || (declared === "GET,HEAD" && (method === "GET" || method === "HEAD"));
|
|
308
|
+
/** The first row that declares this method and path, or null: an undeclared request never reaches a handler. */
|
|
309
|
+
export function matchRoute(method, pathname, stage) {
|
|
310
|
+
for (const { route, test } of compiled) {
|
|
311
|
+
if (stage !== undefined && route.stage !== stage)
|
|
312
|
+
continue;
|
|
313
|
+
if (methodMatches(route.method, method) && test.test(pathname))
|
|
314
|
+
return route;
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
/** True when some row declares this path for another method (a wrong-method request). */
|
|
319
|
+
export function declaredForAnotherMethod(method, pathname, stage) {
|
|
320
|
+
return compiled.some(({ route, test }) => (stage === undefined || route.stage === stage) && !methodMatches(route.method, method) && test.test(pathname));
|
|
321
|
+
}
|
|
322
|
+
/** Refuses a malformed table at startup: duplicate ids, duplicate method+pattern rows, or a row shadowed by an earlier one for its own sample. */
|
|
323
|
+
export function assertRouteTable(routes = ROUTES) {
|
|
324
|
+
const ids = new Set(), keys = new Set();
|
|
325
|
+
for (const route of routes) {
|
|
326
|
+
if (ids.has(route.id))
|
|
327
|
+
throw new Error(`route table: duplicate id ${route.id}`);
|
|
328
|
+
ids.add(route.id);
|
|
329
|
+
const key = `${route.stage} ${route.method} ${route.pattern}`;
|
|
330
|
+
if (keys.has(key))
|
|
331
|
+
throw new Error(`route table: duplicate route ${key}`);
|
|
332
|
+
keys.add(key);
|
|
333
|
+
if (route.scope === "step-up" && (route.callers.length !== 1 || route.callers[0] !== "cookie"))
|
|
334
|
+
throw new Error(`route table: ${route.id} step-up must be cookie-only`);
|
|
335
|
+
if (route.stage === "console" && (route.proof !== "console" || route.host !== "after"))
|
|
336
|
+
throw new Error(`route table: ${route.id} cannot bypass console admission`);
|
|
337
|
+
if (route.callers.length === 0)
|
|
338
|
+
throw new Error(`route table: ${route.id} admits no caller`);
|
|
339
|
+
const test = new RegExp(route.pattern);
|
|
340
|
+
if (!test.test(route.sample))
|
|
341
|
+
throw new Error(`route table: ${route.id} does not match its sample ${route.sample}`);
|
|
342
|
+
const method = route.method === "GET,HEAD" ? "GET" : route.method;
|
|
343
|
+
const winner = routes.find(one => one.stage === route.stage && methodMatches(one.method, method) && new RegExp(one.pattern).test(route.sample));
|
|
344
|
+
if (winner !== route)
|
|
345
|
+
throw new Error(`route table: ${route.id} is shadowed by ${winner?.id} for ${route.sample}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
assertRouteTable();
|
|
349
|
+
/** The effective policy of every row, as plain data — what the snapshot test compares. */
|
|
350
|
+
export const routePolicySnapshot = () => ROUTES.map(({ sample: _sample, ...rest }) => ({ ...rest }));
|