toolroll 0.9.47 → 0.9.49
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 +21 -0
- package/dist/browser/workspace.js +16 -16
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -0
- package/dist/contracts/cli.d.ts +87 -0
- package/dist/contracts/cli.js +2 -0
- package/dist/contracts/health.d.ts +84 -0
- package/dist/contracts/health.js +11 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/flow-live.d.ts +5 -13
- package/dist/flow-live.js +11 -110
- package/dist/health-cli.d.ts +3 -0
- package/dist/health-cli.js +79 -0
- package/dist/live-bus.d.ts +89 -0
- package/dist/live-bus.js +321 -0
- package/dist/mcp-http.d.ts +1 -0
- package/dist/mcp-http.js +6 -4
- package/dist/mcp-oauth.d.ts +3 -0
- package/dist/mcp-oauth.js +8 -0
- package/dist/metrics.d.ts +3 -0
- package/dist/metrics.js +53 -1
- package/dist/operate.js +37 -24
- package/dist/public-access.d.ts +3 -1
- package/dist/public-access.js +5 -1
- package/dist/read-executor-worker.d.ts +1 -0
- package/dist/read-executor-worker.js +26 -0
- package/dist/read-executor.d.ts +49 -0
- package/dist/read-executor.js +119 -0
- package/dist/request-budget.d.ts +31 -3
- package/dist/request-budget.js +50 -1
- package/dist/serve.d.ts +2 -0
- package/dist/serve.js +213 -100
- package/dist/server-telemetry.d.ts +86 -0
- package/dist/server-telemetry.js +263 -0
- package/dist/session-http.d.ts +5 -0
- package/dist/session-http.js +13 -0
- package/dist/session-server.d.ts +5 -0
- package/dist/session-server.js +2 -0
- package/dist/sqlite-telemetry.d.ts +9 -0
- package/dist/sqlite-telemetry.js +98 -0
- package/dist/store.d.ts +6 -0
- package/dist/store.js +25 -3
- package/dist/surface.js +2 -1
- package/dist/task-live.d.ts +5 -14
- package/dist/task-live.js +13 -110
- 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 +5 -0
- package/dist/team-http.js +19 -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 +15 -0
- package/dist/work-index.js +12 -1
- package/dist/workspace-revision.d.ts +2 -0
- package/dist/workspace-revision.js +3 -1
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -44,6 +44,7 @@ import { addRoutine, removeRoutine, runRoutine } from "./teammate-desk.js";
|
|
|
44
44
|
import { sendTeammateWeeklies, undoCall } from "./teammate-week.js";
|
|
45
45
|
import { answerTeammateQuestion } from "./teammate-work.js";
|
|
46
46
|
import { createFlowRooms, flowFingerprint } from "./flow-live.js";
|
|
47
|
+
import { createLiveBus, followWorkspace } from "./live-bus.js";
|
|
47
48
|
import { createTaskRooms, taskFingerprint } from "./task-live.js";
|
|
48
49
|
import { runActivityOf } from "./task-activity.js";
|
|
49
50
|
import { disconnectGoogle, finishGoogleConsent, GOOGLE_CALLBACK, googleConnected, googleConsent, readGoogleMail, saveGoogleClient } from "./google-mail.js";
|
|
@@ -126,8 +127,8 @@ import { projectAuthority } from "./project-access.js";
|
|
|
126
127
|
* centralized gate — `authorizeMutation` — that proves content type, an
|
|
127
128
|
* allowed Origin, and the per-session CSRF nonce, and refuses duplicated
|
|
128
129
|
* security fields; a mutation route cannot forget a check it never wrote.
|
|
129
|
-
* Bearer mutations carry no cookie for a hostile page to ride, so they skip
|
|
130
|
-
*
|
|
130
|
+
* Bearer mutations carry no cookie for a hostile page to ride, so they skip CSRF.
|
|
131
|
+
* Step-up ceremonies still require a cookie session; repeating a bearer password cannot approve.
|
|
131
132
|
*
|
|
132
133
|
* **Approval is step-up.** A session alone never approves a scope: the
|
|
133
134
|
* approval form restates the goal, the exclusions, and the touches — the
|
|
@@ -242,6 +243,7 @@ import { createIntegrationMonitor } from "./integrations.js";
|
|
|
242
243
|
import { BACKUP_CSS, backupHtml } from "./backup-ui.js";
|
|
243
244
|
import { BACKUP_EVERY_HOURS, MAX_KEEP, backupFolderOf, backupNow, checkBackupFolder, defaultBackupFolder } from "./backup.js";
|
|
244
245
|
import { prometheusMetrics } from "./metrics.js";
|
|
246
|
+
import { healthWords, instrumentRequest } from "./server-telemetry.js";
|
|
245
247
|
import { SPEND_CSS, spendCsv, spendHtml } from "./spend-ui.js";
|
|
246
248
|
import { RETENTION_CSS, retentionHtml } from "./retention-ui.js";
|
|
247
249
|
import { STORAGE_CSS, storageHtml } from "./storage-ui.js";
|
|
@@ -266,9 +268,10 @@ import { targetOf } from "./monitoring.js";
|
|
|
266
268
|
import { SSO_CSS, ssoSettingsHtml } from "./sso-ui.js";
|
|
267
269
|
import { mintApiToken, parseApiToken, secretMatches, TOKEN_DAYS, tokenLive, tokenProjects } from "./api-tokens.js";
|
|
268
270
|
import { createMcpHttp } from "./mcp-http.js";
|
|
269
|
-
import { FLUSH_MS as REQUEST_BUDGET_FLUSH_MS, parseLimit, PER_DAY_MAX as REQUEST_BUDGET_PER_DAY_MAX, PER_MINUTE_MAX as REQUEST_BUDGET_PER_MINUTE_MAX, RequestBudget, setLimitOverride } from "./request-budget.js";
|
|
271
|
+
import { FLUSH_MS as REQUEST_BUDGET_FLUSH_MS, limitWords, parseLimit, PER_DAY_MAX as REQUEST_BUDGET_PER_DAY_MAX, PER_MINUTE_MAX as REQUEST_BUDGET_PER_MINUTE_MAX, RequestBudget, setLimitOverride, SOURCE_BUDGET_DEFAULTS, SourceAdmission } from "./request-budget.js";
|
|
272
|
+
import { REMOTE_MESSAGES, reproveRemote } from "./operate-remote.js";
|
|
270
273
|
import { hstsFor, LOOPBACK_PEERS, plainHttpRefusal, transportOf } from "./public-access.js";
|
|
271
|
-
import { createOAuthHttp, oauthTokenAllowed, resourceMetadataUrl } from "./mcp-oauth.js";
|
|
274
|
+
import { createOAuthHttp, oauthProjects, oauthTokenAllowed, resourceMetadataUrl } from "./mcp-oauth.js";
|
|
272
275
|
import { CREDENTIALS_CSS, credentialsHtml, tokenShownHtml } from "./credentials-ui.js";
|
|
273
276
|
import { REQUEST_LIMITS_CSS, requestLimitsHtml, tokenLimitWords } from "./request-budget-ui.js";
|
|
274
277
|
import { PEOPLE_AUDIT_CSS, personAuditHtml, personHref } from "./people-audit-ui.js";
|
|
@@ -291,6 +294,7 @@ import { installMethod } from "./install-method.js";
|
|
|
291
294
|
import { PACKAGE_VERSION } from "./version.js";
|
|
292
295
|
import { updateNoticeWords } from "./update-notice.js";
|
|
293
296
|
import { whenHtml } from "./when-html.js";
|
|
297
|
+
import { ReadExecutor, attachReadExecutor, defaultReadWorkers } from "./read-executor.js";
|
|
294
298
|
const SESSION_COOKIE = "standing-orders_session";
|
|
295
299
|
/** Stands in for "no project" in a ceiling: no folder resolves to it, so it admits nothing. */
|
|
296
300
|
const NO_PROJECT = "/\0no-project";
|
|
@@ -460,6 +464,11 @@ export function createDecisionServer(options) {
|
|
|
460
464
|
}
|
|
461
465
|
}
|
|
462
466
|
const clock = options.clock ?? (() => new Date());
|
|
467
|
+
// Heavy reads (the work index behind `task list`) run on read-only worker connections, off the request loop.
|
|
468
|
+
const readFile = store.isDemo() ? null : store.databaseFile();
|
|
469
|
+
const readWorkers = options.readWorkers ?? (process.env["VITEST"] !== undefined ? 0 : defaultReadWorkers());
|
|
470
|
+
const reads = readFile === null || readWorkers < 1 ? null : new ReadExecutor(readFile, readWorkers);
|
|
471
|
+
const detachReads = reads === null ? () => { } : attachReadExecutor(store, reads);
|
|
463
472
|
/** Task checkouts, for Settings → Storage: the pool's own root, or the folder beside the database. */
|
|
464
473
|
const storagePool = (databaseFile) => new WorktreePool(store, { root: options.poolRoot ?? join(dirname(databaseFile), "worktrees") });
|
|
465
474
|
const install = installMethod(options.installBin);
|
|
@@ -560,7 +569,12 @@ export function createDecisionServer(options) {
|
|
|
560
569
|
* access to the project) is checked either way.
|
|
561
570
|
*/
|
|
562
571
|
// v100: the shared check honours a fresh identity-provider sign-in itself (freshIdentitySignIn, set per request below).
|
|
563
|
-
|
|
572
|
+
function authenticateApprover(who, token, repo) {
|
|
573
|
+
// Re-entering a password never upgrades a machine credential into a browser ceremony.
|
|
574
|
+
if (who.via !== "cookie")
|
|
575
|
+
return { ok: false, reason: "not-an-approver" };
|
|
576
|
+
return checkApproverPassword(store, who.name, token, repo);
|
|
577
|
+
}
|
|
564
578
|
const signInBudget = new SourceBudget();
|
|
565
579
|
const ssoVisits = new Map();
|
|
566
580
|
const ssoHandoffs = new Map();
|
|
@@ -750,8 +764,11 @@ export function createDecisionServer(options) {
|
|
|
750
764
|
/** One-click connections on their way: the service's sign-in page and back, 15 minutes at most. */
|
|
751
765
|
const connectVisits = new Map();
|
|
752
766
|
/** Flows open in a browser (v88): who's here, and a nudge when one changes. */
|
|
753
|
-
|
|
754
|
-
const
|
|
767
|
+
// Live views push on write (live-bus.ts): the revision triggers move, the rooms hear it.
|
|
768
|
+
const liveBus = createLiveBus();
|
|
769
|
+
const liveFollower = followWorkspace(store, () => workspaceRevision.current(), liveBus, { file: store.databaseFile() });
|
|
770
|
+
const flowRooms = createFlowRooms(flow => flowFingerprint(store, flow), { bus: liveBus });
|
|
771
|
+
const taskRooms = createTaskRooms(root => taskFingerprint(store, root, clock()), { bus: liveBus });
|
|
755
772
|
const teamChatProvider = () => { const enabled = chatEnablement(); return enabled.ok ? { config: enabled.config, key: enabled.key } : null; };
|
|
756
773
|
const team = createTeamRuntime({ store, repos: codingProjects, evidenceRoot, clock, workspaceRevision,
|
|
757
774
|
...(options.chatFetcher ? { fetcher: options.chatFetcher } : {}),
|
|
@@ -771,17 +788,59 @@ export function createDecisionServer(options) {
|
|
|
771
788
|
return { ...summary, card: teamProposalCardParts(store, actor, proposal, snapshot, csrf, decision, clock(), teamChatProvider).card };
|
|
772
789
|
}) } };
|
|
773
790
|
};
|
|
791
|
+
// v112: one request budget per server for person API tokens, shared by every route a token signs in at (request-budget.ts).
|
|
792
|
+
const requestBudget = new RequestBudget({ store, ...(options.requestBudgetClock === undefined ? {} : { clock: options.requestBudgetClock }) });
|
|
793
|
+
// Unproved password attempts share a source budget. Only successful proof spends an account budget.
|
|
794
|
+
const sourceClock = options.requestBudgetClock === undefined ? {} : { clock: options.requestBudgetClock };
|
|
795
|
+
const passwordSourceBudget = new SourceAdmission({ perMinute: SOURCE_BUDGET_DEFAULTS.password, ...sourceClock });
|
|
796
|
+
const passwordAccountBudget = new SourceAdmission({ perMinute: SOURCE_BUDGET_DEFAULTS.password, ...sourceClock });
|
|
797
|
+
const teamsSourceBudget = new SourceAdmission({ perMinute: SOURCE_BUDGET_DEFAULTS.teamsSource, ...sourceClock });
|
|
798
|
+
const teamsTenantBudget = new SourceAdmission({ perMinute: SOURCE_BUDGET_DEFAULTS.teamsTenant, ...sourceClock });
|
|
799
|
+
const oauthTokenBudget = new SourceAdmission({ perMinute: SOURCE_BUDGET_DEFAULTS.oauthToken, ...sourceClock });
|
|
800
|
+
const presentsPassword = (request) => /^Bearer (.+):(.+)$/.test(request.headers.authorization ?? "");
|
|
801
|
+
const admitPasswordSource = (request) => presentsPassword(request) ? passwordSourceBudget.admit(joinSourceOf(request)) : { ok: true };
|
|
802
|
+
/** Called only with a proved identity; cookies spend neither bearer budget. */
|
|
803
|
+
const admitBearer = (request, actor) => {
|
|
804
|
+
if (actor.principal !== undefined)
|
|
805
|
+
return requestBudget.admit(actor.principal.tokenId, "api");
|
|
806
|
+
return presentsPassword(request) ? passwordAccountBudget.admit(actor.name) : { ok: true };
|
|
807
|
+
};
|
|
808
|
+
/**
|
|
809
|
+
* The full principal a live `so_` API token stands for: its person and their generation, the token's own read/act
|
|
810
|
+
* scope, its project limit narrowed by the person's current access, and its id. Every route that accepts an API token
|
|
811
|
+
* builds its identity here and carries it whole, so no route can drop the token's narrower scope or projects.
|
|
812
|
+
*/
|
|
813
|
+
const tokenPrincipalOf = (request) => {
|
|
814
|
+
const who = identify(request, false, true);
|
|
815
|
+
return who?.via === "bearer" ? who.principal ?? null : null;
|
|
816
|
+
};
|
|
817
|
+
const presentsToken = (request) => /^Bearer so_\S+$/.test(request.headers.authorization ?? "");
|
|
774
818
|
const teamEndpoint = (request, response) => handleTeamHttp(request, response, {
|
|
775
819
|
authenticate: request => {
|
|
820
|
+
if (presentsToken(request)) {
|
|
821
|
+
const principal = tokenPrincipalOf(request);
|
|
822
|
+
return principal === null ? null : { name: principal.account, generation: principal.generation, principal };
|
|
823
|
+
}
|
|
776
824
|
const who = identify(request, request.method === 'POST');
|
|
777
825
|
const account = who === null ? null : store.accountOf(who.name);
|
|
778
826
|
return who && account && account.revokedAt === null ? { name: who.name, generation: account.generation } : null;
|
|
779
827
|
},
|
|
828
|
+
admit: admitPasswordSource,
|
|
829
|
+
admitAuthenticated: admitBearer,
|
|
780
830
|
revalidate: (request, actor) => {
|
|
781
831
|
const account = store.accountOf(actor.name);
|
|
782
832
|
if (!account || account.revokedAt !== null || account.generation !== actor.generation)
|
|
783
833
|
return false;
|
|
784
|
-
//
|
|
834
|
+
// An API token is re-proved against the store every time: unrevoked, unexpired, inside its project limit.
|
|
835
|
+
if (actor.principal !== undefined) {
|
|
836
|
+
const live = reproveRemote(store, actor.principal, new Date());
|
|
837
|
+
if (!live.ok)
|
|
838
|
+
return false;
|
|
839
|
+
// The post-body mutation check must see a scope lowered since authentication, too.
|
|
840
|
+
actor.principal = { ...actor.principal, scope: live.scope };
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
// A password bearer was proved when this connection opened. Cookie expiry/revocation
|
|
785
844
|
// is rechecked without allowing a passive stream to extend its lifetime.
|
|
786
845
|
if (request.headers.authorization)
|
|
787
846
|
return true;
|
|
@@ -789,6 +848,8 @@ export function createDecisionServer(options) {
|
|
|
789
848
|
return who?.name === actor.name && who.via === 'cookie' && who.session.generation === actor.generation;
|
|
790
849
|
},
|
|
791
850
|
authorizeMutation: (request, actor) => {
|
|
851
|
+
if (actor.principal !== undefined)
|
|
852
|
+
return request.headers.origin === undefined;
|
|
792
853
|
const who = identify(request, false);
|
|
793
854
|
if (!who || who.name !== actor.name)
|
|
794
855
|
return false;
|
|
@@ -802,29 +863,22 @@ export function createDecisionServer(options) {
|
|
|
802
863
|
},
|
|
803
864
|
execute: async (actor, input) => {
|
|
804
865
|
const reply = await team.execute(actor, input);
|
|
866
|
+
if (actor.principal !== undefined)
|
|
867
|
+
return reply;
|
|
805
868
|
const who = identify(request, false);
|
|
806
869
|
return who?.via === 'cookie' ? teamBrowserReply(reply, actor, who.session.csrf) : reply;
|
|
807
870
|
}, cursor: team.cursor, streams: teamStreams,
|
|
808
871
|
});
|
|
809
|
-
// v112: one request budget per server for person API tokens, shared by /api/cli and /mcp (request-budget.ts).
|
|
810
|
-
const requestBudget = new RequestBudget({ store, ...(options.requestBudgetClock === undefined ? {} : { clock: options.requestBudgetClock }) });
|
|
811
872
|
// Remote CLI: one live API token names the person; the shared command boundary decides everything else.
|
|
812
873
|
const cliEndpoint = (request, response) => handleCliHttp(request, response, {
|
|
813
874
|
admit: principal => requestBudget.admit(principal.tokenId, "api"),
|
|
814
|
-
authenticate:
|
|
815
|
-
const who = identify(request, false, true);
|
|
816
|
-
const id = parseApiToken(/^Bearer (so_\S+)$/.exec(request.headers.authorization ?? "")?.[1] ?? "")?.id;
|
|
817
|
-
const row = id === undefined ? undefined : store.apiTokenSecret(id)?.row;
|
|
818
|
-
const account = who === null ? null : store.accountOf(who.name);
|
|
819
|
-
if (who?.via !== "bearer" || who.token === undefined || row === undefined || row.account !== who.name || account === null || account.revokedAt !== null)
|
|
820
|
-
return null;
|
|
821
|
-
return { kind: "person", account: who.name, generation: account.generation, scope: row.access, tokenId: row.id, projects: tokenProjects(account.projects, row.projects) };
|
|
822
|
-
},
|
|
875
|
+
authenticate: tokenPrincipalOf,
|
|
823
876
|
run: async () => options.cliRunner ?? (await import("./operate.js")).runOperateAs ?? null,
|
|
824
877
|
modeOf: options.cliModeOf,
|
|
825
878
|
store,
|
|
826
879
|
});
|
|
827
|
-
const sessionEndpoint = createSessionEndpoint({ store, workspace: coding, projects: codingProjects, projectAllowed: repo => rowVisible(liveCeiling(), repo)
|
|
880
|
+
const sessionEndpoint = createSessionEndpoint({ store, workspace: coding, projects: codingProjects, projectAllowed: repo => rowVisible(liveCeiling(), repo),
|
|
881
|
+
admit: admitPasswordSource, admitAuthenticated: admitBearer });
|
|
828
882
|
/** The enumerable admission list for roll-up SQL: repos-only ceilings
|
|
829
883
|
* enumerate themselves; root ceilings enumerate the STORED repos that
|
|
830
884
|
* pass the ceiling (Codex roll-up review, finding 11); unscoped = null. */
|
|
@@ -841,6 +895,7 @@ export function createDecisionServer(options) {
|
|
|
841
895
|
/** The task behind a resource, for the ceiling check; null = no ref (visible). */
|
|
842
896
|
const taskRepoOf = (taskRef) => store.refForId(taskRef)?.repo ?? null;
|
|
843
897
|
const server = createServer((request, response) => {
|
|
898
|
+
instrumentRequest(store.telemetry, request, response);
|
|
844
899
|
void handle(request, response).catch(error => {
|
|
845
900
|
// Every unhandled error is logged (v99): the path and the message, never the request's body or query.
|
|
846
901
|
logEvent("error", "serve.error", { method: request.method, path: redactedPath(new URL(request.url ?? "/", "http://placeholder").pathname), error: error instanceof Error ? error.message : String(error) });
|
|
@@ -857,6 +912,8 @@ export function createDecisionServer(options) {
|
|
|
857
912
|
}
|
|
858
913
|
});
|
|
859
914
|
});
|
|
915
|
+
server.once('listening', () => store.telemetry.start());
|
|
916
|
+
server.once('close', () => store.telemetry.stop());
|
|
860
917
|
// --public-url (arc 3): validated to EXACTLY an https origin. Its host
|
|
861
918
|
// joins the allowed set, its origin authorizes POSTs, and cookies turn
|
|
862
919
|
// Secure. Anything malformed refuses at startup, loudly.
|
|
@@ -931,8 +988,18 @@ export function createDecisionServer(options) {
|
|
|
931
988
|
* it is a smuggling attempt.
|
|
932
989
|
*/
|
|
933
990
|
function authorizeMutation(request, who, body) {
|
|
991
|
+
// A read token never mutates, whatever its person's role (operate-remote.ts says the same to the remote CLI).
|
|
992
|
+
if (who.via === "bearer") {
|
|
993
|
+
if (who.principal !== undefined) {
|
|
994
|
+
const live = reproveRemote(store, who.principal, new Date());
|
|
995
|
+
if (!live.ok)
|
|
996
|
+
return { status: 403, message: REMOTE_MESSAGES.stale };
|
|
997
|
+
if (live.scope !== "act")
|
|
998
|
+
return { status: 403, message: REMOTE_MESSAGES.read };
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
934
1001
|
const current = store.accountOf(who.name);
|
|
935
|
-
if (current === null || current.revokedAt !== null || current.role !== who.role || (who.via === "cookie"
|
|
1002
|
+
if (current === null || current.revokedAt !== null || current.role !== who.role || current.generation !== (who.via === "cookie" ? who.session.generation : who.generation)) {
|
|
936
1003
|
return { status: 403, message: "Your access changed. Sign in again." };
|
|
937
1004
|
}
|
|
938
1005
|
const type = request.headers["content-type"] ?? "";
|
|
@@ -1294,7 +1361,8 @@ export function createDecisionServer(options) {
|
|
|
1294
1361
|
return;
|
|
1295
1362
|
if (await cliEndpoint(request, response))
|
|
1296
1363
|
return;
|
|
1297
|
-
if (options.configDir !== undefined && await handleTeamsHttp(request, response, { store, dir: options.configDir, ...(options.teamsFetcher ? { fetcher: options.teamsFetcher } : {}), clock
|
|
1364
|
+
if (options.configDir !== undefined && await handleTeamsHttp(request, response, { store, dir: options.configDir, ...(options.teamsFetcher ? { fetcher: options.teamsFetcher } : {}), clock,
|
|
1365
|
+
admitSource: request => teamsSourceBudget.admit(joinSourceOf(request)), admitTenant: tenant => teamsTenantBudget.admit(tenant) }))
|
|
1298
1366
|
return;
|
|
1299
1367
|
if (serveBrowserAsset(request, response, url.pathname))
|
|
1300
1368
|
return;
|
|
@@ -1350,7 +1418,30 @@ export function createDecisionServer(options) {
|
|
|
1350
1418
|
const fragmentName = url.searchParams.get("fragment");
|
|
1351
1419
|
const workspaceRead = method === "GET" && url.searchParams.get('format') === 'workspace';
|
|
1352
1420
|
const touch = !(method === "GET" && (workspaceRead || (fragmentName !== null && NO_TOUCH_FRAGMENTS.has(fragmentName)) || /^\/code\/[a-f0-9]{32}\/state$/.test(url.pathname)));
|
|
1353
|
-
const
|
|
1421
|
+
const refuseBudget = (admitted) => {
|
|
1422
|
+
if (admitted.ok)
|
|
1423
|
+
return false;
|
|
1424
|
+
request.resume();
|
|
1425
|
+
if (admitted.status === 429)
|
|
1426
|
+
response.setHeader("Retry-After", String(admitted.retryAfter));
|
|
1427
|
+
respond(response, admitted.status, "text/plain; charset=utf-8", admitted.status === 429 ? limitWords(admitted.limit, admitted.retryAfter) : "Request limits could not be checked; nothing ran. Try again shortly.");
|
|
1428
|
+
return true;
|
|
1429
|
+
};
|
|
1430
|
+
// One source charge before password proof and one account/token charge after it, including GET /login.
|
|
1431
|
+
if (refuseBudget(admitPasswordSource(request)))
|
|
1432
|
+
return;
|
|
1433
|
+
const refusedToken = {};
|
|
1434
|
+
const who = identify(request, touch, false, refusedToken);
|
|
1435
|
+
if (refusedToken.principal !== undefined) {
|
|
1436
|
+
if (refuseBudget(requestBudget.admit(refusedToken.principal.tokenId, "api")))
|
|
1437
|
+
return;
|
|
1438
|
+
if (method === "POST") {
|
|
1439
|
+
request.resume();
|
|
1440
|
+
return respond(response, 403, "text/plain; charset=utf-8", REMOTE_MESSAGES["step-up"]);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (who?.via === "bearer" && refuseBudget(admitBearer(request, who)))
|
|
1444
|
+
return;
|
|
1354
1445
|
if (url.pathname === "/login" && method === "GET") {
|
|
1355
1446
|
if (options.setupCode !== undefined && store.listApprovers().length === 0) {
|
|
1356
1447
|
return page(response, 200, signupPage(null, setupAttemptsLeft));
|
|
@@ -2000,7 +2091,7 @@ export function createDecisionServer(options) {
|
|
|
2000
2091
|
url.pathname !== "/lead/status" && url.pathname !== "/onboarding/phone/dismiss" &&
|
|
2001
2092
|
!url.pathname.startsWith("/chat/demo/") &&
|
|
2002
2093
|
!/^\/chat\/action\/[0-9]{1,15}$/.test(url.pathname) &&
|
|
2003
|
-
!url.pathname.startsWith("/settings") && url.pathname !== "/logout" && url.pathname !== "/people" && url.pathname !== "/ledger" && url.pathname !== "/ledger/export" && url.pathname !== "/metrics" && url.pathname !== "/spend" && url.pathname !== "/spend/budget" &&
|
|
2094
|
+
!url.pathname.startsWith("/settings") && url.pathname !== "/logout" && url.pathname !== "/people" && url.pathname !== "/ledger" && url.pathname !== "/ledger/export" && url.pathname !== "/metrics" && url.pathname !== "/health" && url.pathname !== "/spend" && url.pathname !== "/spend/budget" &&
|
|
2004
2095
|
!(url.pathname === "/board" && url.searchParams.get("scope") === "all");
|
|
2005
2096
|
if (needsProject)
|
|
2006
2097
|
return redirect(response, `/projects?return=${encodeURIComponent(safeReturn(url.pathname + url.search))}`);
|
|
@@ -4226,6 +4317,14 @@ export function createDecisionServer(options) {
|
|
|
4226
4317
|
return sendScreen(response, 200, screen("Data", `<p><a href="/settings">Settings</a></p><h1>Data</h1>${dataExportHtml(who.session.csrf, { problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "settings") }));
|
|
4227
4318
|
}
|
|
4228
4319
|
// v104: Prometheus metrics, for an instance operator (a scraper sends one's API token).
|
|
4320
|
+
if (url.pathname === "/health") {
|
|
4321
|
+
if (!store.isInstanceOperator(who.name))
|
|
4322
|
+
return refuse(response, who, 403, "An instance operator reads server health.", "/");
|
|
4323
|
+
const health = store.telemetry.snapshot();
|
|
4324
|
+
return request.headers.accept?.includes('application/json')
|
|
4325
|
+
? respond(response, 200, 'application/json; charset=utf-8', JSON.stringify(health))
|
|
4326
|
+
: respond(response, 200, 'text/plain; charset=utf-8', `${healthWords(health)}\n`);
|
|
4327
|
+
}
|
|
4229
4328
|
if (url.pathname === "/metrics") {
|
|
4230
4329
|
if (!store.isInstanceOperator(who.name))
|
|
4231
4330
|
return refuse(response, who, 403, "An instance operator reads metrics.", "/");
|
|
@@ -6613,15 +6712,15 @@ export function createDecisionServer(options) {
|
|
|
6613
6712
|
}
|
|
6614
6713
|
switch (verb) {
|
|
6615
6714
|
case "approve": {
|
|
6715
|
+
if (who.via !== "cookie")
|
|
6716
|
+
return refuse(response, who, 403, REMOTE_MESSAGES["step-up"]);
|
|
6616
6717
|
// Step-up, identical to a scope's: the session got you here; only
|
|
6617
6718
|
// the password agrees. The digest names what was seen.
|
|
6618
6719
|
const digest = body.get("digest") ?? "";
|
|
6619
6720
|
const token = body.get("token") ?? "";
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
return routinePage(response, who, routineId, "that approval form is stale — read it again", 409);
|
|
6624
|
-
}
|
|
6721
|
+
const nonce = body.get("nonce") ?? "";
|
|
6722
|
+
if (!consumeApprovalNonce(nonce, who.name, `routine:${routine.id}`, digest)) {
|
|
6723
|
+
return routinePage(response, who, routineId, "that approval form is stale — read it again", 409);
|
|
6625
6724
|
}
|
|
6626
6725
|
if (token === "" && !hasFreshIdentitySignIn(who.name)) {
|
|
6627
6726
|
return routinePage(response, who, routineId, "approval requires your password, typed again", 400);
|
|
@@ -6655,19 +6754,18 @@ export function createDecisionServer(options) {
|
|
|
6655
6754
|
return redirect(response, `/routines/${routineId}`);
|
|
6656
6755
|
}
|
|
6657
6756
|
case "run-now": {
|
|
6757
|
+
if (who.via !== "cookie")
|
|
6758
|
+
return refuse(response, who, 403, REMOTE_MESSAGES["step-up"]);
|
|
6658
6759
|
// Step-up (Codex Phase C review, M3): run-now is spend outside the
|
|
6659
6760
|
// approved schedule, so a session alone cannot ask for it — the
|
|
6660
|
-
// password is typed again, like an approval.
|
|
6661
|
-
|
|
6662
|
-
if (
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
if (!authenticated.ok) {
|
|
6669
|
-
return routinePage(response, who, routineId, "that is not your password", 403);
|
|
6670
|
-
}
|
|
6761
|
+
// password is typed again, like an approval.
|
|
6762
|
+
const token = body.get("token") ?? "";
|
|
6763
|
+
if (token === "" && !hasFreshIdentitySignIn(who.name)) {
|
|
6764
|
+
return routinePage(response, who, routineId, "run now requires your password, typed again", 400);
|
|
6765
|
+
}
|
|
6766
|
+
const authenticated = authenticateApprover(who, token);
|
|
6767
|
+
if (!authenticated.ok) {
|
|
6768
|
+
return routinePage(response, who, routineId, "that is not your password", 403);
|
|
6671
6769
|
}
|
|
6672
6770
|
const outcome = fireRoutine(store, routineId, now, { manual: true });
|
|
6673
6771
|
if (!outcome.ok) {
|
|
@@ -6854,7 +6952,7 @@ export function createDecisionServer(options) {
|
|
|
6854
6952
|
const repo = body.get('repo') ?? '';
|
|
6855
6953
|
if (!permitted(repo))
|
|
6856
6954
|
return fail(403, 'Choose a project available to your account.');
|
|
6857
|
-
if (!authenticateApprover(
|
|
6955
|
+
if (!authenticateApprover(who, body.get('password') ?? '').ok)
|
|
6858
6956
|
return fail(403, 'Enter your Toolroll password to authorize this coding session.');
|
|
6859
6957
|
const session = await coding.start(actor, { repo, title: body.get('title') ?? '', model: body.get('model')?.trim() || null, prompt: body.get('prompt') ?? '', requestId: body.get('requestId') ?? '' });
|
|
6860
6958
|
id = session.id;
|
|
@@ -6889,7 +6987,7 @@ export function createDecisionServer(options) {
|
|
|
6889
6987
|
const decision = body.get('decision');
|
|
6890
6988
|
if (decision !== 'accept' && decision !== 'decline' && decision !== 'cancel')
|
|
6891
6989
|
return fail(400, 'Choose an explicit decision for this request.');
|
|
6892
|
-
if (pending.kind !== 'questions' && decision === 'accept' && !authenticateApprover(
|
|
6990
|
+
if (pending.kind !== 'questions' && decision === 'accept' && !authenticateApprover(who, body.get('password') ?? '').ok)
|
|
6893
6991
|
return fail(403, 'Enter your password to approve this additional access.');
|
|
6894
6992
|
const answers = body.has('answers') ? JSON.parse(body.get('answers')) : Object.fromEntries(pending.questions.map(q => [q.id, { answers: [body.get(`question:${q.id}`) ?? ''] }]));
|
|
6895
6993
|
coding.answer(id, actor, token, decision, answers);
|
|
@@ -7306,7 +7404,7 @@ export function createDecisionServer(options) {
|
|
|
7306
7404
|
return renewed.ok ? settle(renewed.said, { reveal: renewed.reveal }) : answer(409, { ok: false, said: renewed.message });
|
|
7307
7405
|
}
|
|
7308
7406
|
// Linear's signing secret, pasted on this secure panel behind the password — never in chat.
|
|
7309
|
-
if (!authenticateApprover(
|
|
7407
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
7310
7408
|
return answer(403, { ok: false, said: "Enter your Toolroll password to save a secret." });
|
|
7311
7409
|
const saved = saveLinearSigningSecret(trigger, body.get("secret") ?? "", dir);
|
|
7312
7410
|
return saved.ok ? settle("Signing secret saved. Linear's deliveries can be proved now.") : answer(400, { ok: false, said: saved.message });
|
|
@@ -7344,7 +7442,7 @@ export function createDecisionServer(options) {
|
|
|
7344
7442
|
removeLinearKey(dir);
|
|
7345
7443
|
return settle("Linear key removed.");
|
|
7346
7444
|
}
|
|
7347
|
-
if (!authenticateApprover(
|
|
7445
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
7348
7446
|
return answer(403, { ok: false, said: "Enter your Toolroll password to save a key." });
|
|
7349
7447
|
const saved = saveLinearKey(dir, body.get("key") ?? "");
|
|
7350
7448
|
return saved.ok ? settle("Linear key saved on this computer.") : answer(400, { ok: false, said: saved.message });
|
|
@@ -7444,7 +7542,7 @@ export function createDecisionServer(options) {
|
|
|
7444
7542
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7445
7543
|
return refuse(response, who, 403, "An instance operator sets budgets.", "/spend");
|
|
7446
7544
|
const back = (key, words) => redirect(response, `/spend?${key}=${encodeURIComponent(words)}`);
|
|
7447
|
-
if (!authenticateApprover(
|
|
7545
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
7448
7546
|
return back("problem", "Enter your Toolroll password to change a budget.");
|
|
7449
7547
|
const target = /^(installation|project|person|teammate):(.+)$/.exec(body.get("target") ?? "");
|
|
7450
7548
|
if (target === null)
|
|
@@ -7508,7 +7606,7 @@ export function createDecisionServer(options) {
|
|
|
7508
7606
|
return back("problem", `Nothing was deleted: ${view.running.join(", ")}. Stop it, then try again.`);
|
|
7509
7607
|
if (body.get("step") !== "delete")
|
|
7510
7608
|
return confirm(null);
|
|
7511
|
-
if (!authenticateApprover(
|
|
7609
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
7512
7610
|
return confirm("That password didn't match. Nothing was deleted.");
|
|
7513
7611
|
const done = await deleteProject(store, repo, { actor: who.name, via: "console", now, evidenceRoot, poolRoot: options.poolRoot ?? null });
|
|
7514
7612
|
if (!done.ok)
|
|
@@ -7537,7 +7635,7 @@ export function createDecisionServer(options) {
|
|
|
7537
7635
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7538
7636
|
return refuse(response, who, 403, "An instance operator sets the policy.", "/settings/policy");
|
|
7539
7637
|
const back = (key, words) => redirect(response, `/settings/policy?${key}=${encodeURIComponent(words)}`);
|
|
7540
|
-
if (!authenticateApprover(
|
|
7638
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
7541
7639
|
return back("problem", "That password didn't match. Nothing changed.");
|
|
7542
7640
|
const chosen = body.getAll("provider");
|
|
7543
7641
|
const checked = checkPolicy({ providers: chosen.length === 4 ? null : chosen, models: parseList(body.get("models")), tools: parseList(body.get("tools")), ceiling: body.get("ceiling") ?? "" });
|
|
@@ -7558,7 +7656,7 @@ export function createDecisionServer(options) {
|
|
|
7558
7656
|
if (!known.includes(repo))
|
|
7559
7657
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
|
|
7560
7658
|
const back = (key, words) => redirect(response, `/settings/approval?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
7561
|
-
if (!authenticateApprover(
|
|
7659
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
7562
7660
|
return back("problem", "That password didn't match. Nothing changed.");
|
|
7563
7661
|
const protect = body.get("protect") ?? "none";
|
|
7564
7662
|
const paths = parseProtectedPaths(body.get("paths") ?? "");
|
|
@@ -7581,7 +7679,7 @@ export function createDecisionServer(options) {
|
|
|
7581
7679
|
const token = target === "*" ? null : store.apiTokens(null).find(one => one.id === target && one.revokedAt === null) ?? null;
|
|
7582
7680
|
if (target !== "*" && token === null)
|
|
7583
7681
|
return back("problem", "That token is no longer live. Nothing changed.");
|
|
7584
|
-
if (!authenticateApprover(
|
|
7682
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
7585
7683
|
return back("problem", "Enter your Toolroll password to change request limits.");
|
|
7586
7684
|
const read = (name) => parseLimit(body.get(name) ?? "", name === "per-day" ? REQUEST_BUDGET_PER_DAY_MAX : REQUEST_BUDGET_PER_MINUTE_MAX);
|
|
7587
7685
|
const clear = body.get("action") === "clear";
|
|
@@ -7642,7 +7740,7 @@ export function createDecisionServer(options) {
|
|
|
7642
7740
|
return back("problem", "A viewer's token can only read.");
|
|
7643
7741
|
// A token is a credential: made with the person's password (or their identity provider's check from moments ago), counted once.
|
|
7644
7742
|
const typed = body.get("password") ?? "";
|
|
7645
|
-
const confirmed = who.role === "approver" ? authenticateApprover(
|
|
7743
|
+
const confirmed = who.role === "approver" ? authenticateApprover(who, typed).ok
|
|
7646
7744
|
: typed === "" ? requestContext.getStore()?.sso?.fresh === true : authenticateAccount(store, who.name, typed).ok;
|
|
7647
7745
|
if (!confirmed)
|
|
7648
7746
|
return back("problem", "Enter your Toolroll password to make a token.");
|
|
@@ -7668,7 +7766,7 @@ export function createDecisionServer(options) {
|
|
|
7668
7766
|
const found = await providerFor(before.issuer);
|
|
7669
7767
|
return found.ok ? back("said", `${before.label} answers: it's ready for people to sign in.`) : back("problem", found.said);
|
|
7670
7768
|
}
|
|
7671
|
-
if (!authenticateApprover(
|
|
7769
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
7672
7770
|
return back("problem", "Enter your Toolroll password to change sign-in.");
|
|
7673
7771
|
if (action === "remove") {
|
|
7674
7772
|
removeSsoSettings(options.configDir);
|
|
@@ -7705,7 +7803,7 @@ export function createDecisionServer(options) {
|
|
|
7705
7803
|
}
|
|
7706
7804
|
if (url.pathname === "/settings/updates/cancel")
|
|
7707
7805
|
return back("said", requestRuntimeUpdateCancel(stateDir));
|
|
7708
|
-
if (!authenticateApprover(
|
|
7806
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
7709
7807
|
return back("problem", "Enter your Toolroll password to update.");
|
|
7710
7808
|
const version = body.get("version") ?? "";
|
|
7711
7809
|
const when = { now: "now", "when-idle": "when-idle", tonight: "at" }[body.get("when") ?? ""] ?? null;
|
|
@@ -7744,7 +7842,7 @@ export function createDecisionServer(options) {
|
|
|
7744
7842
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7745
7843
|
return refuse(response, who, 403, "An instance operator sets retention.", "/settings");
|
|
7746
7844
|
const back = (key, words) => redirect(response, `/settings/retention?${key}=${encodeURIComponent(words)}`);
|
|
7747
|
-
if (!authenticateApprover(
|
|
7845
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
7748
7846
|
return back("problem", "Enter your Toolroll password to change retention.");
|
|
7749
7847
|
const before = store.retentionPeriods();
|
|
7750
7848
|
// Only what the page offers (or the period already set): a hand-made value doesn't save.
|
|
@@ -7766,7 +7864,7 @@ export function createDecisionServer(options) {
|
|
|
7766
7864
|
return refuse(response, who, 403, "An instance operator looks after storage.", "/settings");
|
|
7767
7865
|
const back = (key, words) => redirect(response, `/settings/storage?${key}=${encodeURIComponent(words)}`);
|
|
7768
7866
|
const what = url.pathname.endsWith("/clean") ? "clean up" : url.pathname.endsWith("/discard") ? "discard a checkout" : "change checkout cleanup";
|
|
7769
|
-
if (!authenticateApprover(
|
|
7867
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
7770
7868
|
return back("problem", `Enter your Toolroll password to ${what}.`);
|
|
7771
7869
|
if (url.pathname === "/settings/storage") {
|
|
7772
7870
|
const cleanup = parseCleanup(body.get("cleanup") ?? "");
|
|
@@ -7797,13 +7895,15 @@ export function createDecisionServer(options) {
|
|
|
7797
7895
|
const body = readForm(posted, CONSOLE_FORMS.pullRequests);
|
|
7798
7896
|
const repo = body.get("repo") ?? "";
|
|
7799
7897
|
const known = [...new Set([...managedRepos(), ...store.knownRepos(), ...store.listProjects().map(one => one.path)])];
|
|
7800
|
-
if (who.via !== "cookie"
|
|
7898
|
+
if (who.via !== "cookie")
|
|
7899
|
+
return refuse(response, who, 403, REMOTE_MESSAGES["step-up"]);
|
|
7900
|
+
if (!known.includes(repo) || !visible(repo))
|
|
7801
7901
|
return refuse(response, who, 404, "No such project.", "/projects");
|
|
7802
7902
|
if (who.role !== "approver" || store.isDemo())
|
|
7803
7903
|
return refuse(response, who, 403, "An approver sets up pull requests.", "/projects");
|
|
7804
7904
|
const back = (key, words) => redirect(response, `/settings/pull-requests?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
7805
7905
|
const act = body.get("act");
|
|
7806
|
-
if (!authenticateApprover(
|
|
7906
|
+
if (!authenticateApprover(who, body.get("password") ?? "", repo).ok)
|
|
7807
7907
|
return back("problem", "Enter your Toolroll password to change pull requests.");
|
|
7808
7908
|
if (act === "on") {
|
|
7809
7909
|
const checked = await checkPublishing(repo, options.publishExec === undefined ? {} : { exec: options.publishExec });
|
|
@@ -7835,12 +7935,14 @@ export function createDecisionServer(options) {
|
|
|
7835
7935
|
const body = readForm(posted, CONSOLE_FORMS.checks);
|
|
7836
7936
|
const repo = body.get("repo") ?? "";
|
|
7837
7937
|
const known = [...new Set([...managedRepos(), ...store.knownRepos(), ...store.listProjects().map(one => one.path)])];
|
|
7838
|
-
if (who.via !== "cookie"
|
|
7938
|
+
if (who.via !== "cookie")
|
|
7939
|
+
return refuse(response, who, 403, REMOTE_MESSAGES["step-up"]);
|
|
7940
|
+
if (!known.includes(repo) || !visible(repo))
|
|
7839
7941
|
return refuse(response, who, 404, "No such project.", "/projects");
|
|
7840
7942
|
if (who.role !== "approver" || store.isDemo())
|
|
7841
7943
|
return refuse(response, who, 403, "An approver sets a project's checks.", "/projects");
|
|
7842
7944
|
const back = (key, words) => redirect(response, `/settings/checks?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
7843
|
-
if (!authenticateApprover(
|
|
7945
|
+
if (!authenticateApprover(who, body.get("password") ?? "", repo).ok)
|
|
7844
7946
|
return back("problem", "Enter your Toolroll password to change checks.");
|
|
7845
7947
|
const act = body.get("act");
|
|
7846
7948
|
// The automatic review switch: the policy log keeps before → after.
|
|
@@ -7899,7 +8001,7 @@ export function createDecisionServer(options) {
|
|
|
7899
8001
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
|
|
7900
8002
|
return refuse(response, who, 403, "An instance operator looks after backups.", "/settings");
|
|
7901
8003
|
const back = (key, words) => redirect(response, `/settings/backups?${key}=${encodeURIComponent(words)}`);
|
|
7902
|
-
if (!authenticateApprover(
|
|
8004
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
7903
8005
|
return back("problem", "Enter your Toolroll password to change backups.");
|
|
7904
8006
|
const every = body.get("every") ?? "";
|
|
7905
8007
|
const hours = Number(every);
|
|
@@ -7924,7 +8026,7 @@ export function createDecisionServer(options) {
|
|
|
7924
8026
|
const body = readForm(posted, CONSOLE_FORMS.data);
|
|
7925
8027
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7926
8028
|
return refuse(response, who, 403, "An instance operator exports data.", "/settings");
|
|
7927
|
-
if (!authenticateApprover(
|
|
8029
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok) {
|
|
7928
8030
|
return redirect(response, `/settings/data?problem=${encodeURIComponent("Enter your Toolroll password to download the export.")}`);
|
|
7929
8031
|
}
|
|
7930
8032
|
store.recordAction({ at: now.toISOString(), actor: who.name, repo: null, taskId: null, runId: null, action: "everything exported", outcome: "exported", source: "access", detail: "downloaded as a .zip" });
|
|
@@ -7969,7 +8071,7 @@ export function createDecisionServer(options) {
|
|
|
7969
8071
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || !options.configDir)
|
|
7970
8072
|
return refuse(response, who, 403, "An instance operator sets up monitoring.", "/settings");
|
|
7971
8073
|
const back = (key, words) => redirect(response, `/settings/monitoring?${key}=${encodeURIComponent(words)}`);
|
|
7972
|
-
if (!authenticateApprover(
|
|
8074
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
7973
8075
|
return back("problem", "Enter your Toolroll password to change monitoring.");
|
|
7974
8076
|
const before = readMonitoring(options.configDir);
|
|
7975
8077
|
// Never into Toolroll's own folder, a project or the build checkouts: an agent could read the stream there.
|
|
@@ -8026,7 +8128,7 @@ export function createDecisionServer(options) {
|
|
|
8026
8128
|
if (connectionsOf(store, target).find(one => one.id === service.id)?.state !== "open")
|
|
8027
8129
|
return back(`${projectName(target)} already has ${service.label}.`);
|
|
8028
8130
|
}
|
|
8029
|
-
if (!authenticateApprover(
|
|
8131
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
8030
8132
|
return back("Enter your Toolroll password to connect a tool.");
|
|
8031
8133
|
// An app on this computer: added like a common tool and tested; one that can't be reached isn't kept.
|
|
8032
8134
|
if (local !== null) {
|
|
@@ -8081,7 +8183,7 @@ export function createDecisionServer(options) {
|
|
|
8081
8183
|
const action = body.get("action") ?? "";
|
|
8082
8184
|
const name = body.get("name") ?? "";
|
|
8083
8185
|
// Anything that adds what a build can run or reach, or a secret, needs the password.
|
|
8084
|
-
if (["add-catalog", "add-custom", "import", "secret"].includes(action) && !authenticateApprover(
|
|
8186
|
+
if (["add-catalog", "add-custom", "import", "secret"].includes(action) && !authenticateApprover(who, body.get("password") ?? "").ok) {
|
|
8085
8187
|
return back("problem", "Enter your Toolroll password to change this project's tools.");
|
|
8086
8188
|
}
|
|
8087
8189
|
const now = clock();
|
|
@@ -8336,6 +8438,8 @@ export function createDecisionServer(options) {
|
|
|
8336
8438
|
}
|
|
8337
8439
|
if (["/control/setup-preview", "/control/setup-approve", "/control/instructions-preview", "/control/instructions-approve"].includes(url.pathname)) {
|
|
8338
8440
|
const body = readForm(posted, CONSOLE_FORMS.projectSetup);
|
|
8441
|
+
if (who.via !== "cookie")
|
|
8442
|
+
return refuse(response, who, 403, REMOTE_MESSAGES["step-up"]);
|
|
8339
8443
|
const project = projectOf(who, request);
|
|
8340
8444
|
if (project == null || body.get("repo") !== project || !visible(project))
|
|
8341
8445
|
return refuse(response, who, 409, "The selected project changed. Open project setup again.", "/control");
|
|
@@ -8351,7 +8455,7 @@ export function createDecisionServer(options) {
|
|
|
8351
8455
|
}
|
|
8352
8456
|
if (who.via === "cookie" && !consumeApprovalNonce(body.get("nonce") ?? "", who.name, nonceKey, body.get("fingerprint") ?? ""))
|
|
8353
8457
|
return refuse(response, who, 409, "This instruction preview expired. Review it again.", "/control");
|
|
8354
|
-
if (!authenticateApprover(
|
|
8458
|
+
if (!authenticateApprover(who, body.get("token") ?? "").ok)
|
|
8355
8459
|
return refuse(response, who, 403, "Your operator credential is required.", "/control");
|
|
8356
8460
|
const added = addProjectInstructions(project, body.get("fingerprint") ?? "");
|
|
8357
8461
|
if (!added.ok)
|
|
@@ -8389,7 +8493,7 @@ export function createDecisionServer(options) {
|
|
|
8389
8493
|
savePrimary(dir, "slack");
|
|
8390
8494
|
return redirect(response, "/settings/slack");
|
|
8391
8495
|
}
|
|
8392
|
-
if (!authenticateApprover(
|
|
8496
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
8393
8497
|
return show("Enter your Toolroll password to change Slack access.", 403);
|
|
8394
8498
|
const generation = store.accountOf(who.name).generation;
|
|
8395
8499
|
if (action === "connect") {
|
|
@@ -8522,7 +8626,7 @@ export function createDecisionServer(options) {
|
|
|
8522
8626
|
savePrimary(dir, "teams");
|
|
8523
8627
|
return redirect(response, "/settings/teams");
|
|
8524
8628
|
}
|
|
8525
|
-
if (!authenticateApprover(
|
|
8629
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
8526
8630
|
return show("Enter your Toolroll password to change Teams access.", 403);
|
|
8527
8631
|
const generation = store.accountOf(who.name).generation;
|
|
8528
8632
|
if (action === "connect") {
|
|
@@ -8577,7 +8681,7 @@ export function createDecisionServer(options) {
|
|
|
8577
8681
|
savePrimary(dir, "discord");
|
|
8578
8682
|
return redirect(response, "/settings/discord");
|
|
8579
8683
|
}
|
|
8580
|
-
if (!authenticateApprover(
|
|
8684
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
8581
8685
|
return show("Enter your Toolroll password to change Discord access.", 403);
|
|
8582
8686
|
const generation = store.accountOf(who.name).generation;
|
|
8583
8687
|
if (action === "connect") {
|
|
@@ -9117,7 +9221,7 @@ export function createDecisionServer(options) {
|
|
|
9117
9221
|
if (who.via !== "cookie")
|
|
9118
9222
|
return refuse(response, who, 403, "runner registration is a browser surface");
|
|
9119
9223
|
const token = body.get("token") ?? "";
|
|
9120
|
-
if (!authenticateApprover(
|
|
9224
|
+
if (!authenticateApprover(who, token).ok) {
|
|
9121
9225
|
return refuse(response, who, 403, "registering a worker takes your password, typed again", "/fleet");
|
|
9122
9226
|
}
|
|
9123
9227
|
const name = (body.get("name") ?? "").trim();
|
|
@@ -9218,7 +9322,7 @@ export function createDecisionServer(options) {
|
|
|
9218
9322
|
return refuse(response, who, 409, "the terms moved while you were reading — read them again", "/mode");
|
|
9219
9323
|
}
|
|
9220
9324
|
const token = body.get("token") ?? "";
|
|
9221
|
-
if (!authenticateApprover(
|
|
9325
|
+
if (!authenticateApprover(who, token).ok) {
|
|
9222
9326
|
return refuse(response, who, 403, "signing a mode takes your password, typed again", "/mode");
|
|
9223
9327
|
}
|
|
9224
9328
|
store.signMode({
|
|
@@ -9247,7 +9351,7 @@ export function createDecisionServer(options) {
|
|
|
9247
9351
|
const body = readForm(posted, CONSOLE_FORMS.peopleProjects);
|
|
9248
9352
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
9249
9353
|
return refuse(response, who, 403, "An instance operator manages project access.", "/people");
|
|
9250
|
-
if (!authenticateApprover(
|
|
9354
|
+
if (!authenticateApprover(who, body.get("token") ?? "", null).ok)
|
|
9251
9355
|
return refuse(response, who, 403, "Saving access requires your password.", "/people");
|
|
9252
9356
|
const access = readAccessForm(body);
|
|
9253
9357
|
if (!access.ok)
|
|
@@ -9265,7 +9369,7 @@ export function createDecisionServer(options) {
|
|
|
9265
9369
|
const token = body.get("token") ?? "";
|
|
9266
9370
|
// RAISING authority is a password ceremony (the doctrine): adding a
|
|
9267
9371
|
// person to the instance is exactly that.
|
|
9268
|
-
if (!authenticateApprover(
|
|
9372
|
+
if (!authenticateApprover(who, token).ok) {
|
|
9269
9373
|
return refuse(response, who, 403, "making an invite takes your password, typed again", "/people");
|
|
9270
9374
|
}
|
|
9271
9375
|
const role = body.get("role") === "approver" ? "approver" : "viewer";
|
|
@@ -9294,7 +9398,7 @@ export function createDecisionServer(options) {
|
|
|
9294
9398
|
if (who.via !== "cookie")
|
|
9295
9399
|
return refuse(response, who, 403, "inviting is a browser surface");
|
|
9296
9400
|
const token = body.get("token") ?? "";
|
|
9297
|
-
if (!authenticateApprover(
|
|
9401
|
+
if (!authenticateApprover(who, token).ok) {
|
|
9298
9402
|
return refuse(response, who, 403, "cancelling an invite takes your password, typed again", "/people");
|
|
9299
9403
|
}
|
|
9300
9404
|
const id = Number(body.get("id") ?? "");
|
|
@@ -9306,7 +9410,7 @@ export function createDecisionServer(options) {
|
|
|
9306
9410
|
if (who.via !== "cookie")
|
|
9307
9411
|
return refuse(response, who, 403, "removing a person is a browser surface");
|
|
9308
9412
|
const token = body.get("token") ?? "";
|
|
9309
|
-
if (!authenticateApprover(
|
|
9413
|
+
if (!authenticateApprover(who, token).ok) {
|
|
9310
9414
|
return refuse(response, who, 403, "removing a person takes your password, typed again", "/people");
|
|
9311
9415
|
}
|
|
9312
9416
|
const name = (body.get("name") ?? "").trim();
|
|
@@ -9326,7 +9430,7 @@ export function createDecisionServer(options) {
|
|
|
9326
9430
|
if (who.via !== "cookie")
|
|
9327
9431
|
return refuse(response, who, 403, "runner retirement is a browser surface");
|
|
9328
9432
|
const token = body.get("token") ?? "";
|
|
9329
|
-
if (!authenticateApprover(
|
|
9433
|
+
if (!authenticateApprover(who, token).ok) {
|
|
9330
9434
|
return refuse(response, who, 403, "retiring a worker takes your password, typed again", "/fleet");
|
|
9331
9435
|
}
|
|
9332
9436
|
const name = (body.get("name") ?? "").trim();
|
|
@@ -9377,6 +9481,8 @@ export function createDecisionServer(options) {
|
|
|
9377
9481
|
}
|
|
9378
9482
|
const contestAct = /^\/contest\/([0-9]{1,15})\/(arm|pick|abandon)$/.exec(url.pathname);
|
|
9379
9483
|
if (contestAct !== null) {
|
|
9484
|
+
if (who.via !== "cookie")
|
|
9485
|
+
return refuse(response, who, 403, REMOTE_MESSAGES["step-up"]);
|
|
9380
9486
|
const body = readForm(posted, CONSOLE_FORMS.contest);
|
|
9381
9487
|
const contestId = Number(contestAct[1]);
|
|
9382
9488
|
const verb = contestAct[2];
|
|
@@ -9429,7 +9535,7 @@ export function createDecisionServer(options) {
|
|
|
9429
9535
|
// POST minted. The store consumes the nonce conditionally inside the
|
|
9430
9536
|
// same transaction that moves the tournament — replay finds it gone.
|
|
9431
9537
|
const token = body.get("token") ?? "";
|
|
9432
|
-
if (!authenticateApprover(
|
|
9538
|
+
if (!authenticateApprover(who, token).ok) {
|
|
9433
9539
|
return contestScreen(response, who, contestId, "that decision takes your password, typed again", 403);
|
|
9434
9540
|
}
|
|
9435
9541
|
const nonceValue = body.get("nonce") ?? "";
|
|
@@ -9471,7 +9577,7 @@ export function createDecisionServer(options) {
|
|
|
9471
9577
|
if (run === null || runTask === null || familyOf(runTask)?.root.id !== (familyOf(act.taskId)?.root.id ?? act.taskId)) {
|
|
9472
9578
|
return taskScreen(response, who, act.taskId, "That build isn't part of this task.", 409);
|
|
9473
9579
|
}
|
|
9474
|
-
if (!authenticateApprover(
|
|
9580
|
+
if (!authenticateApprover(who, body.get("token") ?? "", ref.repo).ok || !store.accountCanAccess(who.name, ref.repo)) {
|
|
9475
9581
|
return taskScreen(response, who, act.taskId, "That password didn't match. Nothing changed.", 403);
|
|
9476
9582
|
}
|
|
9477
9583
|
// When Toolroll can't check at all, the approver also says they checked: never a one-click confirmation.
|
|
@@ -9579,7 +9685,7 @@ export function createDecisionServer(options) {
|
|
|
9579
9685
|
// (finding 22/33) — the record is consumed from the RETURNED live
|
|
9580
9686
|
// session, synchronously, before the first await.
|
|
9581
9687
|
const password = body.get("token") ?? "";
|
|
9582
|
-
if (password === "" || !authenticateApprover(
|
|
9688
|
+
if (password === "" || !authenticateApprover(who, password).ok) {
|
|
9583
9689
|
return projectsScreen(response, who, "cloning takes your password, typed again", 403);
|
|
9584
9690
|
}
|
|
9585
9691
|
const cookieId = /(?:^|;\s*)standing-orders_session=([0-9a-f]{64})/.exec(request.headers.cookie ?? "")?.[1];
|
|
@@ -9647,7 +9753,7 @@ export function createDecisionServer(options) {
|
|
|
9647
9753
|
if (store.isDemo())
|
|
9648
9754
|
return refuse(response, who, 403, "the sandbox never pushes");
|
|
9649
9755
|
const password = body.get("token") ?? "";
|
|
9650
|
-
if (password === "" || !authenticateApprover(
|
|
9756
|
+
if (password === "" || !authenticateApprover(who, password).ok) {
|
|
9651
9757
|
return redirect(response, `/settings?said=${encodeURIComponent("enrolling this device takes your password, typed again")}`);
|
|
9652
9758
|
}
|
|
9653
9759
|
const endpoint = body.get("endpoint") ?? "";
|
|
@@ -9750,7 +9856,7 @@ export function createDecisionServer(options) {
|
|
|
9750
9856
|
return refuse(response, who, 403, "chat setup is a browser surface");
|
|
9751
9857
|
const back = body.get("return") === "/settings/lead" ? "/settings/lead" : safeChatReturn(body.get("return"));
|
|
9752
9858
|
const password = body.get("token") ?? "";
|
|
9753
|
-
if (password === "" || !authenticateApprover(
|
|
9859
|
+
if (password === "" || !authenticateApprover(who, password).ok) {
|
|
9754
9860
|
return redirect(response, chatReturnWithSaid(back, "configuring chat spend takes your password, typed again"));
|
|
9755
9861
|
}
|
|
9756
9862
|
if (body.get("off") === "1") {
|
|
@@ -10076,7 +10182,7 @@ export function createDecisionServer(options) {
|
|
|
10076
10182
|
// The password, typed again, on EVERY message (v2 ruling 2): chat is
|
|
10077
10183
|
// spend, and a seven-day cookie is not a spend credential.
|
|
10078
10184
|
const password = body.get("token") ?? "";
|
|
10079
|
-
if (password === "" || !authenticateApprover(
|
|
10185
|
+
if (password === "" || !authenticateApprover(who, password).ok) {
|
|
10080
10186
|
return redirect(response, `/chat?said=${encodeURIComponent("chat spends — your password, typed again, with every message")}`);
|
|
10081
10187
|
}
|
|
10082
10188
|
const message = (body.get("message") ?? "").trim();
|
|
@@ -10145,7 +10251,7 @@ export function createDecisionServer(options) {
|
|
|
10145
10251
|
const candidate = chat?.candidates.get(key);
|
|
10146
10252
|
// The filing act creates durable rows from model text: password again.
|
|
10147
10253
|
const password = body.get("token") ?? "";
|
|
10148
|
-
if (password === "" || !authenticateApprover(
|
|
10254
|
+
if (password === "" || !authenticateApprover(who, password).ok) {
|
|
10149
10255
|
return redirect(response, `/chat?said=${encodeURIComponent("filing a draft takes your password, typed again")}`);
|
|
10150
10256
|
}
|
|
10151
10257
|
if (chat === undefined || candidate === undefined) {
|
|
@@ -10218,7 +10324,7 @@ export function createDecisionServer(options) {
|
|
|
10218
10324
|
if (turn === null)
|
|
10219
10325
|
return refuse(response, who, 404, "no such turn", "/chat");
|
|
10220
10326
|
const password = body.get("token") ?? "";
|
|
10221
|
-
if (password === "" || !authenticateApprover(
|
|
10327
|
+
if (password === "" || !authenticateApprover(who, password).ok) {
|
|
10222
10328
|
return refuse(response, who, 403, "acknowledging unknown spend takes your password", "/chat");
|
|
10223
10329
|
}
|
|
10224
10330
|
const nonce = body.get("nonce") ?? "";
|
|
@@ -10926,7 +11032,7 @@ export function createDecisionServer(options) {
|
|
|
10926
11032
|
}
|
|
10927
11033
|
basis = { kind: "mode", digest: mode.digest };
|
|
10928
11034
|
}
|
|
10929
|
-
else if (!authenticateApprover(
|
|
11035
|
+
else if (!authenticateApprover(who, token).ok) {
|
|
10930
11036
|
return refuse(response, who, 403, "authorizing takes your password, typed again", taskHref(taskId));
|
|
10931
11037
|
}
|
|
10932
11038
|
const minted = store.mintAttendedAuthorization({
|
|
@@ -11103,7 +11209,7 @@ export function createDecisionServer(options) {
|
|
|
11103
11209
|
// Authenticated like every approving act: the session alone may
|
|
11104
11210
|
// read; resuming external work takes the password, typed again.
|
|
11105
11211
|
const token = (body.get("token") ?? "").trim();
|
|
11106
|
-
const authenticated = token !== "" ? authenticateApprover(
|
|
11212
|
+
const authenticated = token !== "" ? authenticateApprover(who, token) : null;
|
|
11107
11213
|
if (authenticated === null || !authenticated.ok) {
|
|
11108
11214
|
return taskScreen(response, who, taskId, "reopening takes your password, typed again", 403);
|
|
11109
11215
|
}
|
|
@@ -11284,7 +11390,7 @@ export function createDecisionServer(options) {
|
|
|
11284
11390
|
// authority takes the password ceremony again — the same act
|
|
11285
11391
|
// that would be required to approve that authority from scratch.
|
|
11286
11392
|
const token = (body.get("token") ?? "").trim();
|
|
11287
|
-
if (!authenticateApprover(
|
|
11393
|
+
if (!authenticateApprover(who, token).ok) {
|
|
11288
11394
|
return taskScreen(response, who, taskId, "accepting this revision takes your password, typed again", 403);
|
|
11289
11395
|
}
|
|
11290
11396
|
}
|
|
@@ -11600,6 +11706,8 @@ export function createDecisionServer(options) {
|
|
|
11600
11706
|
return redirect(response, taskHref(taskId));
|
|
11601
11707
|
}
|
|
11602
11708
|
case "approve": {
|
|
11709
|
+
if (who.via !== "cookie")
|
|
11710
|
+
return refuse(response, who, 403, REMOTE_MESSAGES["step-up"]);
|
|
11603
11711
|
const body = readForm(posted, CONSOLE_FORMS.taskApprove);
|
|
11604
11712
|
const requestedReturn = body.get("return");
|
|
11605
11713
|
const approvalBack = requestedReturn === "next"
|
|
@@ -11622,11 +11730,9 @@ export function createDecisionServer(options) {
|
|
|
11622
11730
|
// rendered to this approver and is spent either way.
|
|
11623
11731
|
const digest = body.get("digest") ?? "";
|
|
11624
11732
|
const token = body.get("token") ?? "";
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
return approvalProblem("that approval form is stale — read it again", 409);
|
|
11629
|
-
}
|
|
11733
|
+
const nonce = body.get("nonce") ?? "";
|
|
11734
|
+
if (!consumeApprovalNonce(nonce, who.name, taskId, digest)) {
|
|
11735
|
+
return approvalProblem("that approval form is stale — read it again", 409);
|
|
11630
11736
|
}
|
|
11631
11737
|
if (token === "" && !hasFreshIdentitySignIn(who.name)) {
|
|
11632
11738
|
return approvalProblem("approval requires your password, typed again", 400);
|
|
@@ -11823,7 +11929,7 @@ export function createDecisionServer(options) {
|
|
|
11823
11929
|
return taskScreen(response, who, taskId, "which attempt? the resume names the exact stopped run", 400);
|
|
11824
11930
|
const runId = Number(named);
|
|
11825
11931
|
const token = body.get("token") ?? "";
|
|
11826
|
-
if (!authenticateApprover(
|
|
11932
|
+
if (!authenticateApprover(who, token).ok) {
|
|
11827
11933
|
return taskScreen(response, who, taskId, "resuming takes your password, typed again", 403);
|
|
11828
11934
|
}
|
|
11829
11935
|
const nonceValue = body.get("nonce") ?? "";
|
|
@@ -11903,7 +12009,7 @@ export function createDecisionServer(options) {
|
|
|
11903
12009
|
approved: (() => { const scope = store.getScope(taskId); return scope !== null && approvalOf(scope).approved; })(),
|
|
11904
12010
|
}));
|
|
11905
12011
|
}
|
|
11906
|
-
const mcpHttp = createMcpHttp({ store, clock, evidenceRoot, signedIn: request => identify(request, true, true) !== null, admit: person => requestBudget.admit(person.principal.tokenId, "mcp"),
|
|
12012
|
+
const mcpHttp = createMcpHttp({ store, clock, evidenceRoot, ...(options.requestBudgetClock === undefined ? {} : { requestBudgetClock: options.requestBudgetClock }), signedIn: request => identify(request, true, true) !== null, admit: person => requestBudget.admit(person.principal.tokenId, "mcp"),
|
|
11907
12013
|
resourceMetadata: request => { const origin = consoleOrigin(request.headers.host); return origin === null ? null : resourceMetadataUrl(origin); },
|
|
11908
12014
|
enrolled: () => [...new Set([...managedRepos(), ...store.listProjects().map(project => project.path)])], ...(options.runOperateAs === undefined ? {} : { runAs: options.runOperateAs }) });
|
|
11909
12015
|
// MCP sign-in: the console's own sign-in and step-up, a same-site consent form, and the person's current projects.
|
|
@@ -11926,9 +12032,10 @@ export function createDecisionServer(options) {
|
|
|
11926
12032
|
// grant (their role, their projects) is checked beside it.
|
|
11927
12033
|
confirm: (session, typed) => typed === "" ? session.sso?.fresh === true : authenticateAccount(store, session.name, typed).ok,
|
|
11928
12034
|
projectsFor: account => store.knownRepos().filter(repo => store.accountCanAccess(account, repo)).sort(),
|
|
12035
|
+
admitToken: request => oauthTokenBudget.admit(joinSourceOf(request)),
|
|
11929
12036
|
});
|
|
11930
12037
|
// ---- identity ------------------------------------------------------------
|
|
11931
|
-
function identify(request, touch = true, limited = false) {
|
|
12038
|
+
function identify(request, touch = true, limited = false, refusedToken) {
|
|
11932
12039
|
// v101: an API token. Wrong ones spend the address's tries like a wrong password; expired or revoked ones, and a removed account's, name no one.
|
|
11933
12040
|
const presented = /^Bearer (so_\S+)$/.exec(request.headers.authorization ?? "")?.[1];
|
|
11934
12041
|
if (presented !== undefined) {
|
|
@@ -11945,16 +12052,20 @@ export function createDecisionServer(options) {
|
|
|
11945
12052
|
return null;
|
|
11946
12053
|
// v111: a token limited to some projects signs in only where its limit travels with it (the remote CLI and MCP,
|
|
11947
12054
|
// through Principal.projects); the console's own pages and APIs check the account's access alone, so it is refused there.
|
|
11948
|
-
if (kept.row.projects !== null && !limited)
|
|
11949
|
-
return null;
|
|
11950
12055
|
const account = store.accountOf(kept.row.account);
|
|
11951
12056
|
if (account === null || account.revokedAt !== null)
|
|
11952
12057
|
return null;
|
|
11953
|
-
|
|
11954
|
-
|
|
12058
|
+
const principal = { kind: "person", account: kept.row.account, generation: account.generation, scope: kept.row.access, tokenId: kept.row.id,
|
|
12059
|
+
projects: tokenProjects(tokenProjects(account.projects, kept.row.projects), oauthProjects(store, kept.row.id)) };
|
|
12060
|
+
// Preserve proof only for a refusal and its admission charge. This never makes an OAuth token a console login.
|
|
12061
|
+
if (!oauthTokenAllowed(store, kept.row.id, new URL(request.url ?? "/", "http://placeholder").pathname, new Date(at)) ||
|
|
12062
|
+
(kept.row.projects !== null && !limited)) {
|
|
12063
|
+
if (refusedToken !== undefined)
|
|
12064
|
+
refusedToken.principal = principal;
|
|
11955
12065
|
return null;
|
|
12066
|
+
}
|
|
11956
12067
|
store.touchApiToken(kept.row.id, new Date(at));
|
|
11957
|
-
return { name: kept.row.account, via: "bearer", role: kept.row.access === "read" ? "viewer" : account.role, token: kept.row.name };
|
|
12068
|
+
return { name: kept.row.account, via: "bearer", role: kept.row.access === "read" ? "viewer" : account.role, token: kept.row.name, generation: account.generation, principal };
|
|
11958
12069
|
}
|
|
11959
12070
|
const bearer = /^Bearer (.+):(.+)$/.exec(request.headers.authorization ?? "");
|
|
11960
12071
|
if (bearer !== null) {
|
|
@@ -11965,7 +12076,7 @@ export function createDecisionServer(options) {
|
|
|
11965
12076
|
const authenticated = authenticateAccount(store, bearer[1], bearer[2]);
|
|
11966
12077
|
if (!authenticated.ok && authenticated.reason === "unknown")
|
|
11967
12078
|
signInBudget.failed(source, at);
|
|
11968
|
-
return authenticated.ok && passwordAllowed(bearer[1]) ? { name: bearer[1], via: "bearer", role: authenticated.role } : null;
|
|
12079
|
+
return authenticated.ok && passwordAllowed(bearer[1]) ? { name: bearer[1], via: "bearer", role: authenticated.role, generation: authenticated.generation } : null;
|
|
11969
12080
|
}
|
|
11970
12081
|
const cookies = request.headers.cookie ?? "";
|
|
11971
12082
|
const match = new RegExp(`(?:^|;\\s*)${SESSION_COOKIE}=([0-9a-f]{64})`).exec(cookies);
|
|
@@ -12559,7 +12670,9 @@ export function createDecisionServer(options) {
|
|
|
12559
12670
|
chatStreams.clear();
|
|
12560
12671
|
flowRooms.close();
|
|
12561
12672
|
taskRooms.close();
|
|
12562
|
-
|
|
12673
|
+
detachReads();
|
|
12674
|
+
liveFollower.close();
|
|
12675
|
+
void Promise.all([closeCoding(), bounded(team.close()), bounded(leadMaintenance?.stop()), bounded(reads?.close())]).then(() => closeServer(callback)).catch(error => {
|
|
12563
12676
|
if (callback)
|
|
12564
12677
|
callback(error instanceof Error ? error : Error('Coding session shutdown failed.'));
|
|
12565
12678
|
else
|