@cmmd-center/forge 0.13.11 → 0.13.13
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/dist/{BunHttpServer-C7-1JJ7b.cjs → BunHttpServer-C0oPP_3A.cjs} +4 -4
- package/dist/{BunHttpServer-CVPGkizx.mjs → BunHttpServer-FbbQmsl_.mjs} +4 -4
- package/dist/{BunServices-CPrZSIfE.mjs → BunServices-DC7nhA3w.mjs} +2 -2
- package/dist/{BunServices-Dl-3897e.cjs → BunServices-DkenwD7S.cjs} +2 -2
- package/dist/{Cache-Cf3ARNaS.cjs → Cache-BmGc_2BZ.cjs} +2 -2
- package/dist/{Cache-C9cwdCDt.mjs → Cache-D57GWb9-.mjs} +2 -2
- package/dist/{HttpServer-CggZzScj.cjs → HttpServer-BBzRBmVt.cjs} +2 -2
- package/dist/{HttpServer-COaBeM7-.mjs → HttpServer-Bclwvu0I.mjs} +2 -2
- package/dist/{NodeHttpServer-UQnvZnmu.mjs → NodeHttpServer-BJ4l15f-.mjs} +5 -5
- package/dist/{NodeHttpServer-Bi339XFk.cjs → NodeHttpServer-Cjd3bFrD.cjs} +5 -5
- package/dist/{NodePTY-UOaRyQsj.cjs → NodePTY-BehazUMi.cjs} +2 -2
- package/dist/{NodePTY-B1u6RHXk.mjs → NodePTY-BhezYy9H.mjs} +2 -2
- package/dist/{NodeServices-R0BZsZ72.cjs → NodeServices-BvdIc9Vh.cjs} +1 -1
- package/dist/{NodeServices-CKFIZVu5.cjs → NodeServices-CtC4a-h5.cjs} +2 -2
- package/dist/{NodeServices-z1Ac4lEO.mjs → NodeServices-D3IxOzHl.mjs} +2 -2
- package/dist/{NodeSqliteClient-CyJvo9y9.cjs → NodeSqliteClient-CfsdC3AB.cjs} +4 -4
- package/dist/{NodeSqliteClient-BrzqdOsF.mjs → NodeSqliteClient-md8xd9hY.mjs} +4 -4
- package/dist/{NodeTerminal-pYVVRTiD.mjs → NodeTerminal-9iHZIdt1.mjs} +2 -2
- package/dist/{NodeTerminal-B89MobLp.cjs → NodeTerminal-CO-i-6Je.cjs} +2 -2
- package/dist/{Path-BunGrE8F.cjs → Path-BQOH_fWQ.cjs} +7 -1
- package/dist/{Path-evOm-H0Q.mjs → Path-CZJ1gPPo.mjs} +2 -2
- package/dist/{SqlError-CezTFGvr.mjs → SqlError-BMBkezUO.mjs} +2 -2
- package/dist/{SqlError-1wIMjXAf.cjs → SqlError-g4pLzNWR.cjs} +2 -2
- package/dist/{SqliteClient-D3Yd3ZHn.mjs → SqliteClient-BKB695RF.mjs} +3 -3
- package/dist/{SqliteClient-bo7iui1t.cjs → SqliteClient-O1ABIngw.cjs} +3 -3
- package/dist/bin.cjs +1451 -233
- package/dist/bin.mjs +1470 -260
- package/dist/client/assets/{CanvasPanel-z0F-jdf1.js → CanvasPanel-RpD9Eh_S.js} +2 -2
- package/dist/client/assets/{CompositeItem-P9UVXCKv.js → CompositeItem-DEIBUHJR.js} +2 -2
- package/dist/client/assets/{DiffPanel-BrlSm5kH.js → DiffPanel-cwm7LEtZ.js} +2 -2
- package/dist/client/assets/{DiffPanel.logic-OZhO8GfQ.js → DiffPanel.logic-B9AWYFxE.js} +2 -2
- package/dist/client/assets/{DiffPanelShell-DnugngHM.js → DiffPanelShell-BhokrrDZ.js} +112 -112
- package/dist/client/assets/{DiffWorkerPoolProvider-BOD7cQPe.js → DiffWorkerPoolProvider-BZjEmEzL.js} +2 -2
- package/dist/client/assets/{PullRequestCodePanel-DACYpcbJ.js → PullRequestCodePanel-BPJINzCp.js} +2 -2
- package/dist/client/assets/{Virtualizer-R_LG4CvG.js → Virtualizer-i6HiQ7Qm.js} +2 -2
- package/dist/client/assets/{canvasStateStore-CdQxd2aO.js → canvasStateStore-NrAxDqvM.js} +2 -2
- package/dist/client/assets/cmmdAuthRedirect-BahlX0gF.js +2 -0
- package/dist/client/assets/{index-BXzYKPmD.js → index-DMWfw0Ih.js} +61 -61
- package/dist/client/assets/index-dEPiAWVG.css +1 -0
- package/dist/client/assets/{popover-CUSlb3eK.js → popover-Z8Tvl-OD.js} +2 -2
- package/dist/client/assets/src-BU7yHzO9.js +3 -0
- package/dist/client/assets/{toggle-BREV66nT.js → toggle-CWYdWYBn.js} +2 -2
- package/dist/client/index.html +11 -11
- package/dist/forge-build.json +1 -1
- package/dist/{open-D8mGyWmt.mjs → open-FIgTDosf.mjs} +3 -3
- package/dist/runtimeBootstrap.cjs +1 -1
- package/dist/runtimeBootstrap.mjs +1 -1
- package/dist/{runtimeControlAuthority-B3kNmYRT.cjs → runtimeControlAuthority-BkVB0uAA.cjs} +17 -1
- package/dist/{runtimeControlAuthority-D_zKr_eI.mjs → runtimeControlAuthority-E_kdeD_P.mjs} +12 -2
- package/package.json +1 -1
- package/dist/client/assets/cmmdAuthRedirect-Bp1uQaQ9.js +0 -2
- package/dist/client/assets/index-Bn-ASQRG.css +0 -1
- package/dist/client/assets/src-BL-mOkT1.js +0 -3
package/dist/bin.cjs
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
4
|
const require_chunk = require("./chunk-BhaIgcZK.cjs");
|
|
5
5
|
const require_Schema$1 = require("./Schema-CDCJ3xZo.cjs");
|
|
6
|
-
const require_Path = require("./Path-
|
|
7
|
-
const require_NodeTerminal = require("./NodeTerminal-
|
|
8
|
-
const require_NodeServices = require("./NodeServices-
|
|
9
|
-
const require_HttpServer = require("./HttpServer-
|
|
6
|
+
const require_Path = require("./Path-BQOH_fWQ.cjs");
|
|
7
|
+
const require_NodeTerminal = require("./NodeTerminal-CO-i-6Je.cjs");
|
|
8
|
+
const require_NodeServices = require("./NodeServices-CtC4a-h5.cjs");
|
|
9
|
+
const require_HttpServer = require("./HttpServer-BBzRBmVt.cjs");
|
|
10
10
|
const require_Config = require("./Config-CwKCCk14.cjs");
|
|
11
|
-
const require_Cache = require("./Cache-
|
|
12
|
-
const require_SqlError = require("./SqlError-
|
|
11
|
+
const require_Cache = require("./Cache-BmGc_2BZ.cjs");
|
|
12
|
+
const require_SqlError = require("./SqlError-g4pLzNWR.cjs");
|
|
13
13
|
const require_Mime = require("./Mime-Bilnh8zE.cjs");
|
|
14
14
|
const require_esm = require("./esm-BZhXwX5p.cjs");
|
|
15
|
-
const require_runtimeControlAuthority = require("./runtimeControlAuthority-
|
|
15
|
+
const require_runtimeControlAuthority = require("./runtimeControlAuthority-BkVB0uAA.cjs");
|
|
16
16
|
const require_PTY = require("./PTY-B4ZctbwQ.cjs");
|
|
17
17
|
let node_child_process = require("node:child_process");
|
|
18
18
|
node_child_process = require_chunk.__toESM(node_child_process);
|
|
@@ -17402,7 +17402,8 @@ const ORCHESTRATION_WS_METHODS = {
|
|
|
17402
17402
|
getArchivedShellSnapshot: "orchestration.getArchivedShellSnapshot",
|
|
17403
17403
|
subscribeShell: "orchestration.subscribeShell",
|
|
17404
17404
|
subscribeThread: "orchestration.subscribeThread",
|
|
17405
|
-
searchThreads: "orchestration.searchThreads"
|
|
17405
|
+
searchThreads: "orchestration.searchThreads",
|
|
17406
|
+
getThreadDetailPage: "orchestration.getThreadDetailPage"
|
|
17406
17407
|
};
|
|
17407
17408
|
const ProviderKind = require_Schema$1.Literals([
|
|
17408
17409
|
"codex",
|
|
@@ -17920,10 +17921,47 @@ const OrchestrationShellStreamItem = require_Schema$1.Union([require_Schema$1.St
|
|
|
17920
17921
|
kind: require_Schema$1.Literal("snapshot"),
|
|
17921
17922
|
snapshot: OrchestrationShellSnapshot
|
|
17922
17923
|
}), OrchestrationShellStreamEvent]);
|
|
17923
|
-
const OrchestrationSubscribeThreadInput = require_Schema$1.Struct({
|
|
17924
|
+
const OrchestrationSubscribeThreadInput = require_Schema$1.Struct({
|
|
17925
|
+
threadId: ThreadId,
|
|
17926
|
+
afterSequence: require_Schema$1.optionalKey(NonNegativeInt),
|
|
17927
|
+
requestCompletionMarker: require_Schema$1.optionalKey(require_Schema$1.Boolean),
|
|
17928
|
+
turnLimit: require_Schema$1.optionalKey(PositiveInt)
|
|
17929
|
+
});
|
|
17930
|
+
require_Schema$1.Struct({
|
|
17931
|
+
turnLimit: require_Schema$1.optionalKey(PositiveInt),
|
|
17932
|
+
beforeCursor: require_Schema$1.optionalKey(TrimmedNonEmptyString)
|
|
17933
|
+
});
|
|
17934
|
+
/**
|
|
17935
|
+
* Reads one windowed page of a thread's detail directly over the RPC socket.
|
|
17936
|
+
*
|
|
17937
|
+
* The HTTP snapshot route and `subscribeThread`'s fallback snapshot both take
|
|
17938
|
+
* the same `turnLimit`/`beforeCursor` window, but neither can serve "load
|
|
17939
|
+
* earlier" on a connection with no HTTP endpoint: the fallback snapshot only
|
|
17940
|
+
* fires once, on subscribe. This method exists for exactly that case (a
|
|
17941
|
+
* relay-only connection), gated on the server's `threadDetailPageRpc`
|
|
17942
|
+
* capability so older servers are never sent a method they do not know.
|
|
17943
|
+
*/
|
|
17944
|
+
const OrchestrationGetThreadDetailPageInput = require_Schema$1.Struct({
|
|
17945
|
+
threadId: ThreadId,
|
|
17946
|
+
turnLimit: require_Schema$1.optionalKey(PositiveInt),
|
|
17947
|
+
beforeCursor: require_Schema$1.optionalKey(TrimmedNonEmptyString)
|
|
17948
|
+
});
|
|
17949
|
+
/**
|
|
17950
|
+
* Page metadata for a windowed thread detail read. `beforeCursor` is opaque and
|
|
17951
|
+
* exclusive: passing it back returns the adjacent slice of older turns. `null`
|
|
17952
|
+
* means nothing older is left. `snapshotSequence` mirrors the top-level
|
|
17953
|
+
* sequence so a history page can be sequence-checked against live state before
|
|
17954
|
+
* a client merges it.
|
|
17955
|
+
*/
|
|
17956
|
+
const OrchestrationThreadDetailPage = require_Schema$1.Struct({
|
|
17957
|
+
beforeCursor: require_Schema$1.NullOr(TrimmedNonEmptyString),
|
|
17958
|
+
hasMore: require_Schema$1.Boolean,
|
|
17959
|
+
snapshotSequence: NonNegativeInt
|
|
17960
|
+
});
|
|
17924
17961
|
const OrchestrationThreadDetailSnapshot = require_Schema$1.Struct({
|
|
17925
17962
|
snapshotSequence: NonNegativeInt,
|
|
17926
|
-
thread: OrchestrationThread
|
|
17963
|
+
thread: OrchestrationThread,
|
|
17964
|
+
page: require_Schema$1.optional(OrchestrationThreadDetailPage)
|
|
17927
17965
|
});
|
|
17928
17966
|
const ProjectCreateCommand = require_Schema$1.Struct({
|
|
17929
17967
|
type: require_Schema$1.Literal("project.create"),
|
|
@@ -18747,13 +18785,17 @@ const OrchestrationEvent = require_Schema$1.Union([
|
|
|
18747
18785
|
payload: ThreadRateLimitSetPayload$1
|
|
18748
18786
|
})
|
|
18749
18787
|
]);
|
|
18750
|
-
const OrchestrationThreadStreamItem = require_Schema$1.Union([
|
|
18751
|
-
kind: require_Schema$1.Literal("
|
|
18752
|
-
|
|
18753
|
-
|
|
18754
|
-
|
|
18755
|
-
|
|
18756
|
-
|
|
18788
|
+
const OrchestrationThreadStreamItem = require_Schema$1.Union([
|
|
18789
|
+
require_Schema$1.Struct({ kind: require_Schema$1.Literal("synchronized") }),
|
|
18790
|
+
require_Schema$1.Struct({
|
|
18791
|
+
kind: require_Schema$1.Literal("snapshot"),
|
|
18792
|
+
snapshot: OrchestrationThreadDetailSnapshot
|
|
18793
|
+
}),
|
|
18794
|
+
require_Schema$1.Struct({
|
|
18795
|
+
kind: require_Schema$1.Literal("event"),
|
|
18796
|
+
event: OrchestrationEvent
|
|
18797
|
+
})
|
|
18798
|
+
]);
|
|
18757
18799
|
const OrchestrationCommandReceiptStatus = require_Schema$1.Literals(["accepted", "rejected"]);
|
|
18758
18800
|
const TurnCountRange = require_Schema$1.Struct({
|
|
18759
18801
|
fromTurnCount: NonNegativeInt,
|
|
@@ -18902,6 +18944,10 @@ const OrchestrationRpcSchemas = {
|
|
|
18902
18944
|
searchThreads: {
|
|
18903
18945
|
input: OrchestrationSearchThreadsInput,
|
|
18904
18946
|
output: OrchestrationSearchThreadsResult
|
|
18947
|
+
},
|
|
18948
|
+
getThreadDetailPage: {
|
|
18949
|
+
input: OrchestrationGetThreadDetailPageInput,
|
|
18950
|
+
output: OrchestrationThreadDetailSnapshot
|
|
18905
18951
|
}
|
|
18906
18952
|
};
|
|
18907
18953
|
var OrchestrationGetSnapshotError = class extends require_Schema$1.TaggedErrorClass()("OrchestrationGetSnapshotError", {
|
|
@@ -22978,6 +23024,9 @@ const ServerConfig$1 = require_Schema$1.Struct({
|
|
|
22978
23024
|
availableEditors: require_Schema$1.ArraySchema(EditorId),
|
|
22979
23025
|
shellRevealInFileManager: require_Schema$1.optionalKey(require_Schema$1.Boolean),
|
|
22980
23026
|
shellRevealInFileManagerKind: require_Schema$1.optionalKey(FileManagerRevealKind),
|
|
23027
|
+
threadResumeCompletionMarker: require_Schema$1.optionalKey(require_Schema$1.Boolean),
|
|
23028
|
+
threadSnapshotPagination: require_Schema$1.optionalKey(require_Schema$1.Boolean),
|
|
23029
|
+
threadDetailPageRpc: require_Schema$1.optionalKey(require_Schema$1.Boolean),
|
|
22981
23030
|
observability: ServerObservability,
|
|
22982
23031
|
settings: ServerSettings
|
|
22983
23032
|
});
|
|
@@ -26937,6 +26986,17 @@ const WsOrchestrationSearchThreadsRpc = make$30(ORCHESTRATION_WS_METHODS.searchT
|
|
|
26937
26986
|
error: OrchestrationSearchThreadsError
|
|
26938
26987
|
});
|
|
26939
26988
|
/**
|
|
26989
|
+
* The socket-only path for "load earlier turns": a relay-only connection has
|
|
26990
|
+
* no HTTP endpoint, so this reads the same windowed page the HTTP snapshot
|
|
26991
|
+
* route and `subscribeThread`'s fallback snapshot serve, over the RPC socket
|
|
26992
|
+
* instead.
|
|
26993
|
+
*/
|
|
26994
|
+
const WsOrchestrationGetThreadDetailPageRpc = make$30(ORCHESTRATION_WS_METHODS.getThreadDetailPage, {
|
|
26995
|
+
payload: OrchestrationRpcSchemas.getThreadDetailPage.input,
|
|
26996
|
+
success: OrchestrationRpcSchemas.getThreadDetailPage.output,
|
|
26997
|
+
error: OrchestrationGetSnapshotError
|
|
26998
|
+
});
|
|
26999
|
+
/**
|
|
26940
27000
|
* Resolve `owner/repo` to the repository the caller is allowed to clone.
|
|
26941
27001
|
*
|
|
26942
27002
|
* Both of these run against the same CMMD-brokered GitHub credential the web
|
|
@@ -27049,7 +27109,7 @@ const WsRpcGroup = make$29(WsServerGetConfigRpc, WsServerRefreshProvidersRpc, Ws
|
|
|
27049
27109
|
payload: ProviderUploadFeedbackInput,
|
|
27050
27110
|
success: ProviderUploadFeedbackResult,
|
|
27051
27111
|
error: ProviderUploadFeedbackError
|
|
27052
|
-
}), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc);
|
|
27112
|
+
}), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsOrchestrationGetThreadDetailPageRpc);
|
|
27053
27113
|
const RelayWsRpcGroup = WsRpcGroup.middleware(RelayAuthorizationMiddleware).middleware(RelayDeliveryMiddleware);
|
|
27054
27114
|
/**
|
|
27055
27115
|
* The direct-WebSocket mount of the same group, with per-method scope
|
|
@@ -27122,6 +27182,32 @@ function encodeCmmdRuntimeToolActivityPayload(kind, payload) {
|
|
|
27122
27182
|
}
|
|
27123
27183
|
};
|
|
27124
27184
|
}
|
|
27185
|
+
const WORKTREE_LAUNCH_ACTIVITY_KINDS = new Set([
|
|
27186
|
+
"worktree.requested",
|
|
27187
|
+
"worktree.stage",
|
|
27188
|
+
"worktree.output",
|
|
27189
|
+
"worktree.created",
|
|
27190
|
+
"worktree.failed",
|
|
27191
|
+
"setup-script.requested",
|
|
27192
|
+
"setup-script.started",
|
|
27193
|
+
"setup-script.skipped",
|
|
27194
|
+
"setup-script.failed",
|
|
27195
|
+
"launch.agent-starting",
|
|
27196
|
+
"launch.cancelled"
|
|
27197
|
+
]);
|
|
27198
|
+
function encodeCmmdRuntimeWorktreeLaunchActivityPayload(kind, payload) {
|
|
27199
|
+
return {
|
|
27200
|
+
...isRecord$23(payload) ? payload : { sourcePayload: payload },
|
|
27201
|
+
forgeWorktreeLaunchActivity: {
|
|
27202
|
+
schemaVersion: 1,
|
|
27203
|
+
source: "native",
|
|
27204
|
+
kind
|
|
27205
|
+
}
|
|
27206
|
+
};
|
|
27207
|
+
}
|
|
27208
|
+
function isCmmdRuntimeWorktreeLaunchActivityKind(kind) {
|
|
27209
|
+
return WORKTREE_LAUNCH_ACTIVITY_KINDS.has(kind);
|
|
27210
|
+
}
|
|
27125
27211
|
//#endregion
|
|
27126
27212
|
//#region ../../packages/contracts/src/cmmdProjectPatch.ts
|
|
27127
27213
|
const CMMD_PROJECT_PATCH_FIELDS = {
|
|
@@ -32232,6 +32318,11 @@ const RELAY_METHOD_POLICY_BASES = [
|
|
|
32232
32318
|
requiredScope: "thread.read",
|
|
32233
32319
|
deliveryClass: "subscription"
|
|
32234
32320
|
},
|
|
32321
|
+
{
|
|
32322
|
+
method: ORCHESTRATION_WS_METHODS.getThreadDetailPage,
|
|
32323
|
+
requiredScope: "thread.read",
|
|
32324
|
+
deliveryClass: "read"
|
|
32325
|
+
},
|
|
32235
32326
|
{
|
|
32236
32327
|
method: WS_METHODS.assetsCreateUrl,
|
|
32237
32328
|
requiredScope: "thread.read",
|
|
@@ -41486,7 +41577,7 @@ function splitPath(filename) {
|
|
|
41486
41577
|
const parts = splitPathRe.exec(truncated);
|
|
41487
41578
|
return parts ? parts.slice(1) : [];
|
|
41488
41579
|
}
|
|
41489
|
-
function dirname$
|
|
41580
|
+
function dirname$8(path) {
|
|
41490
41581
|
const result = splitPath(path);
|
|
41491
41582
|
const root = result[0] || "";
|
|
41492
41583
|
let dir = result[1];
|
|
@@ -58801,7 +58892,7 @@ function getSpotlightConfig(optionsSpotlight) {
|
|
|
58801
58892
|
function normalizeWindowsPath(path) {
|
|
58802
58893
|
return path.replace(/^[A-Z]:/, "").replace(/\\/g, "/");
|
|
58803
58894
|
}
|
|
58804
|
-
function createGetModuleFromFilename(basePath = process.argv[1] ? dirname$
|
|
58895
|
+
function createGetModuleFromFilename(basePath = process.argv[1] ? dirname$8(process.argv[1]) : process.cwd(), isWindows = node_path.sep === "\\") {
|
|
58805
58896
|
const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath;
|
|
58806
58897
|
return (filename) => {
|
|
58807
58898
|
if (!filename) return;
|
|
@@ -66289,7 +66380,7 @@ function normalizeNumberish(value) {
|
|
|
66289
66380
|
}
|
|
66290
66381
|
//#endregion
|
|
66291
66382
|
//#region package.json
|
|
66292
|
-
var version$1 = "0.13.
|
|
66383
|
+
var version$1 = "0.13.13";
|
|
66293
66384
|
//#endregion
|
|
66294
66385
|
//#region src/sentry.ts
|
|
66295
66386
|
const SERVER_APP_NAME = "forge-server";
|
|
@@ -66691,6 +66782,129 @@ const respondToAuthError = (error) => require_Schema$1.gen(function* () {
|
|
|
66691
66782
|
}));
|
|
66692
66783
|
return response;
|
|
66693
66784
|
});
|
|
66785
|
+
//#endregion
|
|
66786
|
+
//#region src/http/rateLimit.ts
|
|
66787
|
+
/**
|
|
66788
|
+
* Per-client, per-route-class rate limiting for Forge's own HTTP surface.
|
|
66789
|
+
*
|
|
66790
|
+
* Scoped to the routes a broken or looping client can hammer before it ever
|
|
66791
|
+
* has an authenticated session — the CMMD device-code poll, the bootstrap and
|
|
66792
|
+
* pairing-token exchanges — plus the authenticated /api/cmmd proxy, which
|
|
66793
|
+
* every desktop UI interaction runs through. None of these had any limit
|
|
66794
|
+
* before FOR-36706: a stuck pairing loop or a poll that never saw `slow_down`
|
|
66795
|
+
* could retry as fast as the network allowed, with nothing to stop it and
|
|
66796
|
+
* nothing telling the client to back off.
|
|
66797
|
+
*
|
|
66798
|
+
* Defaults are generous on purpose. This exists to bound a runaway client,
|
|
66799
|
+
* not to throttle a working one — see `defaultRateLimitPolicies` for the
|
|
66800
|
+
* reasoning behind each number and how to override it.
|
|
66801
|
+
*
|
|
66802
|
+
* Deliberately plain module state, not an Effect service: every route this
|
|
66803
|
+
* guards is a synchronous decision (count, compare, maybe reset a window),
|
|
66804
|
+
* and threading a new Context requirement through the whole server's layer
|
|
66805
|
+
* graph for that would touch every test file that stands up any of these
|
|
66806
|
+
* routes. `cmmdJwtResolver.ts`'s in-flight-refresh map is the same call.
|
|
66807
|
+
*/
|
|
66808
|
+
function envInt(name, fallback) {
|
|
66809
|
+
const raw = process.env[name];
|
|
66810
|
+
if (!raw) return fallback;
|
|
66811
|
+
const parsed = Number(raw);
|
|
66812
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
66813
|
+
}
|
|
66814
|
+
/**
|
|
66815
|
+
* One reasoning line per class, so a future change to a default has to
|
|
66816
|
+
* confront why the old one was picked:
|
|
66817
|
+
*
|
|
66818
|
+
* - `auth-device-poll` (`/api/auth/cmmd-oauth/device` and `/token`): RFC 8628
|
|
66819
|
+
* polls every `interval` seconds, 5 by default, so a well-behaved client
|
|
66820
|
+
* sits under 12/min. 120/min leaves room for several concurrent sign-in
|
|
66821
|
+
* attempts and CMMD's own `slow_down` handling without ever tripping on
|
|
66822
|
+
* normal use, while still bounding a client that ignores `slow_down`.
|
|
66823
|
+
* - `auth-bootstrap` (`/api/auth/bootstrap*`, the bearer bootstrap routes):
|
|
66824
|
+
* a working client bootstraps once per sign-in. 30/min is generous headroom
|
|
66825
|
+
* for retries after a network blip, not a budget anyone should approach.
|
|
66826
|
+
* - `auth-token-exchange` (`/oauth/token`, the vendored pairing exchange):
|
|
66827
|
+
* same shape as bootstrap — one pairing attempt, occasionally retried.
|
|
66828
|
+
* - `cmmd-proxy` (every `/api/cmmd/*` route): this is the one real desktop
|
|
66829
|
+
* traffic actually leans on — task lists, project browsing, comments, all
|
|
66830
|
+
* day. 1200/min (20/sec) is set high enough that no normal session should
|
|
66831
|
+
* ever see it; it exists for a retry loop or a runaway resync, not for
|
|
66832
|
+
* ordinary use.
|
|
66833
|
+
*/
|
|
66834
|
+
function buildPoliciesFromEnv() {
|
|
66835
|
+
return {
|
|
66836
|
+
"auth-device-poll": {
|
|
66837
|
+
maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_DEVICE_POLL_MAX", 120),
|
|
66838
|
+
windowMs: envInt("FORGE_RATE_LIMIT_AUTH_DEVICE_POLL_WINDOW_SECONDS", 60) * 1e3
|
|
66839
|
+
},
|
|
66840
|
+
"auth-bootstrap": {
|
|
66841
|
+
maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_BOOTSTRAP_MAX", 30),
|
|
66842
|
+
windowMs: envInt("FORGE_RATE_LIMIT_AUTH_BOOTSTRAP_WINDOW_SECONDS", 60) * 1e3
|
|
66843
|
+
},
|
|
66844
|
+
"auth-token-exchange": {
|
|
66845
|
+
maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_TOKEN_EXCHANGE_MAX", 30),
|
|
66846
|
+
windowMs: envInt("FORGE_RATE_LIMIT_AUTH_TOKEN_EXCHANGE_WINDOW_SECONDS", 60) * 1e3
|
|
66847
|
+
},
|
|
66848
|
+
"cmmd-proxy": {
|
|
66849
|
+
maxRequests: envInt("FORGE_RATE_LIMIT_CMMD_PROXY_MAX", 1200),
|
|
66850
|
+
windowMs: envInt("FORGE_RATE_LIMIT_CMMD_PROXY_WINDOW_SECONDS", 60) * 1e3
|
|
66851
|
+
}
|
|
66852
|
+
};
|
|
66853
|
+
}
|
|
66854
|
+
let policies = buildPoliciesFromEnv();
|
|
66855
|
+
const windows = /* @__PURE__ */ new Map();
|
|
66856
|
+
function windowKey(routeClass, clientKey) {
|
|
66857
|
+
return `${routeClass}\0${clientKey}`;
|
|
66858
|
+
}
|
|
66859
|
+
/**
|
|
66860
|
+
* Fixed-window counting: cheap, and precise enough for a loop-guard rather
|
|
66861
|
+
* than a security boundary. The one imprecision this trades away — up to 2x
|
|
66862
|
+
* the configured rate right at a window boundary — matters for an attacker
|
|
66863
|
+
* budgeting requests against the limiter's clock, not for the runaway
|
|
66864
|
+
* clients this exists to catch.
|
|
66865
|
+
*/
|
|
66866
|
+
function checkHttpRateLimit(input) {
|
|
66867
|
+
const policy = policies[input.routeClass];
|
|
66868
|
+
const nowMs = Date.now();
|
|
66869
|
+
const key = windowKey(input.routeClass, input.clientKey);
|
|
66870
|
+
const existing = windows.get(key);
|
|
66871
|
+
const windowIsLive = existing !== void 0 && nowMs - existing.windowStartMs < policy.windowMs;
|
|
66872
|
+
const windowStartMs = windowIsLive ? existing.windowStartMs : nowMs;
|
|
66873
|
+
const count = (windowIsLive ? existing.count : 0) + 1;
|
|
66874
|
+
windows.set(key, {
|
|
66875
|
+
windowStartMs,
|
|
66876
|
+
count
|
|
66877
|
+
});
|
|
66878
|
+
return {
|
|
66879
|
+
allowed: count <= policy.maxRequests,
|
|
66880
|
+
retryAfterSeconds: Math.max(0, Math.ceil((windowStartMs + policy.windowMs - nowMs) / 1e3))
|
|
66881
|
+
};
|
|
66882
|
+
}
|
|
66883
|
+
/**
|
|
66884
|
+
* Checks the limiter for one request and, when the caller is over budget,
|
|
66885
|
+
* returns the 429 to send instead of running the route — `null` means
|
|
66886
|
+
* proceed. Every route this guards is expected to call this first and return
|
|
66887
|
+
* early on a non-null result, before doing any other work.
|
|
66888
|
+
*/
|
|
66889
|
+
function rateLimitOrRespond(input) {
|
|
66890
|
+
const decision = checkHttpRateLimit(input);
|
|
66891
|
+
if (decision.allowed) return null;
|
|
66892
|
+
return require_HttpServer.jsonUnsafe({ error: "Too many requests. Try again later." }, {
|
|
66893
|
+
status: 429,
|
|
66894
|
+
headers: { "retry-after": String(decision.retryAfterSeconds) }
|
|
66895
|
+
});
|
|
66896
|
+
}
|
|
66897
|
+
/**
|
|
66898
|
+
* The client identity for a route with no session yet — the device-code
|
|
66899
|
+
* poll, bootstrap and pairing-token exchange all run before Forge has issued
|
|
66900
|
+
* one. IP address is the only stable-enough signal available at that point;
|
|
66901
|
+
* the authenticated `/api/cmmd` proxy keys on the session instead (see its
|
|
66902
|
+
* call site), so one desktop and one phone sharing a NAT never share a
|
|
66903
|
+
* budget there.
|
|
66904
|
+
*/
|
|
66905
|
+
function clientKeyFromRequest(request) {
|
|
66906
|
+
return normalizeIpAddress(require_Schema$1.getOrNull(request.remoteAddress)) ?? "unknown";
|
|
66907
|
+
}
|
|
66694
66908
|
const OAUTH_ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
|
|
66695
66909
|
/**
|
|
66696
66910
|
* The one exchange this endpoint performs, or the reason it will not.
|
|
@@ -66716,6 +66930,29 @@ function accessTokenExpiresInSeconds(remainingMillis) {
|
|
|
66716
66930
|
return Math.max(0, Math.floor(remainingMillis / 1e3));
|
|
66717
66931
|
}
|
|
66718
66932
|
//#endregion
|
|
66933
|
+
//#region src/auth/commandBootstrapClient.ts
|
|
66934
|
+
const BOOTSTRAP_CLIENT_LABELS = {
|
|
66935
|
+
desktop: "Forge Desktop Cloud",
|
|
66936
|
+
mobile: "Forge Mobile Cloud"
|
|
66937
|
+
};
|
|
66938
|
+
/**
|
|
66939
|
+
* Client metadata for a bearer bootstrap session.
|
|
66940
|
+
*
|
|
66941
|
+
* The declared device type wins over the one inferred from the user agent. A
|
|
66942
|
+
* React Native fetch sends whatever the platform's networking stack sends, and
|
|
66943
|
+
* on iOS that is a CFNetwork string no user-agent heuristic reads as a phone.
|
|
66944
|
+
*/
|
|
66945
|
+
function resolveCommandBootstrapClient(input) {
|
|
66946
|
+
const deviceType = input.clientDeviceType ?? "desktop";
|
|
66947
|
+
return {
|
|
66948
|
+
...deriveAuthClientMetadata({
|
|
66949
|
+
request: input.request,
|
|
66950
|
+
label: BOOTSTRAP_CLIENT_LABELS[deviceType]
|
|
66951
|
+
}),
|
|
66952
|
+
deviceType
|
|
66953
|
+
};
|
|
66954
|
+
}
|
|
66955
|
+
//#endregion
|
|
66719
66956
|
//#region ../../packages/shared/src/cmmdIdentity.ts
|
|
66720
66957
|
const MAX_CMMD_AVATAR_URL_LENGTH = 256 * 1024;
|
|
66721
66958
|
const DATA_IMAGE_AVATAR_PATTERN = /^data:image\/(?:avif|gif|jpeg|jpg|png|webp);base64,[a-z0-9+/=]+$/i;
|
|
@@ -67169,14 +67406,26 @@ function extractJwtExpiresAt(jwtToken) {
|
|
|
67169
67406
|
}
|
|
67170
67407
|
}
|
|
67171
67408
|
/**
|
|
67172
|
-
*
|
|
67173
|
-
*
|
|
67409
|
+
* How long before `exp` a stored JWT is already treated as spent.
|
|
67410
|
+
*
|
|
67411
|
+
* Comparing against `exp` exactly leaves a window where Forge sends a token it
|
|
67412
|
+
* believes is good and CMMD has already stopped accepting it: the clocks differ by a
|
|
67413
|
+
* little, the request spends time in flight, and the answer comes back 401 on a
|
|
67414
|
+
* session that had a perfectly good refresh token sitting next to it. Two
|
|
67415
|
+
* minutes is far inside the JWT's own lifetime, so this costs one early
|
|
67416
|
+
* rotation and removes the race.
|
|
67417
|
+
*/
|
|
67418
|
+
const JWT_EXPIRY_SKEW_MS = 2 * 6e4;
|
|
67419
|
+
/**
|
|
67420
|
+
* Returns true when a stored JWT is at or near its expiry, or has no decodable
|
|
67421
|
+
* expiry. CMMD REST JWTs are short-lived bearer tokens, so unknown expiry is
|
|
67422
|
+
* not safe.
|
|
67174
67423
|
*/
|
|
67175
67424
|
function isJwtExpiredForSession(sessionId) {
|
|
67176
67425
|
if (!jwtCache.has(sessionId)) return false;
|
|
67177
67426
|
const expiresAt = jwtExpiryCache.get(sessionId);
|
|
67178
67427
|
if (!expiresAt) return true;
|
|
67179
|
-
return new Date(expiresAt)
|
|
67428
|
+
return new Date(expiresAt).getTime() - JWT_EXPIRY_SKEW_MS <= Date.now();
|
|
67180
67429
|
}
|
|
67181
67430
|
/**
|
|
67182
67431
|
* Hydrate the in-memory cache from the configured persistence layer. Call once at server startup
|
|
@@ -67732,10 +67981,27 @@ const clearUnrecoverableCmmdAuthForSession = (sessionId) => require_Schema$1.gen
|
|
|
67732
67981
|
* Retire refresh credentials copied into historical desktop bearer sessions.
|
|
67733
67982
|
* Electron owns rotation for native desktop OAuth; Forge Local keeps only the
|
|
67734
67983
|
* derived access authority needed by the current bearer session.
|
|
67735
|
-
|
|
67736
|
-
|
|
67737
|
-
|
|
67738
|
-
|
|
67984
|
+
*
|
|
67985
|
+
* Scoped to the bootstrapping client, not to the whole subject. The phone and
|
|
67986
|
+
* Electron sign in as the same `user:<id>`, so a subject-wide sweep let one
|
|
67987
|
+
* desktop launch null out a phone's refresh token and end its session a day
|
|
67988
|
+
* later. Two clauses keep that from happening:
|
|
67989
|
+
*
|
|
67990
|
+
* - `client_device_type = 'desktop'`, which now excludes phones outright,
|
|
67991
|
+
* because the mobile app declares itself at bootstrap.
|
|
67992
|
+
* - the same recorded `client_user_agent` as the bootstrap doing the
|
|
67993
|
+
* retirement, which is the only thing that separates a phone row written
|
|
67994
|
+
* before that stamp existed from a real Electron row. Both are stamped
|
|
67995
|
+
* `desktop`; neither carries any other mark. An empty or unmatched user agent
|
|
67996
|
+
* retires nothing, which is the direction that keeps a live credential.
|
|
67997
|
+
*
|
|
67998
|
+
* Electron re-bootstraps under a stable user agent, so its own historical rows
|
|
67999
|
+
* still match and still get swept.
|
|
68000
|
+
*/
|
|
68001
|
+
const clearDesktopBearerRefreshTokensForSubject = (input) => require_Schema$1.gen(function* () {
|
|
68002
|
+
const normalizedSubject = input.subject.trim();
|
|
68003
|
+
const normalizedUserAgent = input.clientUserAgent?.trim() ?? "";
|
|
68004
|
+
if (!normalizedSubject || !normalizedUserAgent) return;
|
|
67739
68005
|
const rows = yield* (yield* require_SqlError.SqlClient)`
|
|
67740
68006
|
UPDATE command_access_tokens
|
|
67741
68007
|
SET
|
|
@@ -67748,6 +68014,7 @@ const clearDesktopBearerRefreshTokensForSubject = (subject) => require_Schema$1.
|
|
|
67748
68014
|
AND role = 'owner'
|
|
67749
68015
|
AND method = 'bearer-session-token'
|
|
67750
68016
|
AND client_device_type = 'desktop'
|
|
68017
|
+
AND client_user_agent = ${normalizedUserAgent}
|
|
67751
68018
|
)
|
|
67752
68019
|
RETURNING session_id AS "sessionId"
|
|
67753
68020
|
`.pipe(require_Schema$1.mapError(toPersistenceSqlError("CommandAccessTokenStore.clearDesktopBearerRefresh:query")));
|
|
@@ -67770,6 +68037,17 @@ const OAUTH_STATE_COOKIE = "forge_oauth_state";
|
|
|
67770
68037
|
const OAUTH_VERIFIER_COOKIE = "forge_oauth_verifier";
|
|
67771
68038
|
//#endregion
|
|
67772
68039
|
//#region src/auth/http/cloudOwnerOAuth.ts
|
|
68040
|
+
const FORGE_CLOUD_SESSION_GRANT_COOKIE = "forge_cloud_session_grant";
|
|
68041
|
+
function resolveCloudOwnerSessionGrantCookie(input) {
|
|
68042
|
+
const token = input.token?.trim();
|
|
68043
|
+
if (!token || !input.expiresAt) return null;
|
|
68044
|
+
const expiresAt = new Date(input.expiresAt);
|
|
68045
|
+
if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() <= Date.now()) return null;
|
|
68046
|
+
return {
|
|
68047
|
+
token,
|
|
68048
|
+
expiresAt
|
|
68049
|
+
};
|
|
68050
|
+
}
|
|
67773
68051
|
function validateCloudOwnerCallbackAssertion(input) {
|
|
67774
68052
|
if (input.serverAuth.isForgeCloudShell?.() !== true) return require_Schema$1.succeed(null);
|
|
67775
68053
|
if (!input.assertionToken || !input.serverAuth.verifyForgeCloudOwnerAssertion) return require_Schema$1.fail(new AuthError({
|
|
@@ -67833,8 +68111,19 @@ function cloudOwnerAssertionCookieOptions(assertion) {
|
|
|
67833
68111
|
secure: true
|
|
67834
68112
|
};
|
|
67835
68113
|
}
|
|
67836
|
-
function
|
|
67837
|
-
return
|
|
68114
|
+
function cloudOwnerSessionGrantCookieOptions(grant) {
|
|
68115
|
+
return {
|
|
68116
|
+
expires: grant.expiresAt,
|
|
68117
|
+
httpOnly: true,
|
|
68118
|
+
path: "/api/auth",
|
|
68119
|
+
sameSite: "lax",
|
|
68120
|
+
secure: true
|
|
68121
|
+
};
|
|
68122
|
+
}
|
|
68123
|
+
function cloudOwnerOAuthResponse(homeUrl, assertion, sessionGrant) {
|
|
68124
|
+
let response = require_HttpServer.redirect(homeUrl, { status: 302 }).pipe(require_HttpServer.setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
|
|
68125
|
+
if (sessionGrant) response = response.pipe(require_Schema$1.flatMap(require_HttpServer.setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, sessionGrant.token, cloudOwnerSessionGrantCookieOptions(sessionGrant))));
|
|
68126
|
+
return response.pipe(require_Schema$1.flatMap(require_HttpServer.setCookie(OAUTH_STATE_COOKIE, "", expiredCallbackCookie)), require_Schema$1.flatMap(require_HttpServer.setCookie(OAUTH_VERIFIER_COOKIE, "", expiredCallbackCookie)));
|
|
67838
68127
|
}
|
|
67839
68128
|
//#endregion
|
|
67840
68129
|
//#region src/auth/http.ts
|
|
@@ -67884,6 +68173,56 @@ const authSessionRouteLayer = add$1("GET", "/api/auth/session", require_Schema$1
|
|
|
67884
68173
|
}));
|
|
67885
68174
|
return response;
|
|
67886
68175
|
}));
|
|
68176
|
+
const authSessionRenewRouteLayer = add$1("POST", "/api/auth/session/renew", require_Schema$1.gen(function* () {
|
|
68177
|
+
const request = yield* require_HttpServer.HttpServerRequest;
|
|
68178
|
+
const serverAuth = yield* ServerAuth;
|
|
68179
|
+
if (serverAuth.isForgeCloudShell?.() !== true) return yield* new AuthError({
|
|
68180
|
+
message: "Session renewal is unavailable.",
|
|
68181
|
+
status: 401
|
|
68182
|
+
});
|
|
68183
|
+
const sessionGrant = request.cookies[FORGE_CLOUD_SESSION_GRANT_COOKIE]?.trim();
|
|
68184
|
+
if (!sessionGrant) return yield* new AuthError({
|
|
68185
|
+
message: "Session renewal is unavailable.",
|
|
68186
|
+
status: 401
|
|
68187
|
+
});
|
|
68188
|
+
const credentials = yield* resolveCmmdTokenBundleCredentials(yield* readCmmdJsonResponse(yield* require_Schema$1.tryPromise({
|
|
68189
|
+
try: () => fetch(resolveCommandAppEndpoint("/api/mcp-server/oauth/token"), {
|
|
68190
|
+
method: "POST",
|
|
68191
|
+
headers: { "content-type": "application/json" },
|
|
68192
|
+
body: JSON.stringify({
|
|
68193
|
+
grant_type: "forge_session_grant",
|
|
68194
|
+
grant_token: sessionGrant,
|
|
68195
|
+
client_id: "cmmd-forge-web"
|
|
68196
|
+
})
|
|
68197
|
+
}),
|
|
68198
|
+
catch: (cause) => new AuthError({
|
|
68199
|
+
message: "Failed to renew the Forge Cloud session with CMMD.",
|
|
68200
|
+
status: 500,
|
|
68201
|
+
cause
|
|
68202
|
+
})
|
|
68203
|
+
}), "Failed to renew the Forge Cloud session with CMMD."));
|
|
68204
|
+
const { cmmdIdentity, userId, patToStore } = credentials;
|
|
68205
|
+
if (!cmmdIdentity || !userId || !patToStore) return yield* new AuthError({
|
|
68206
|
+
message: "CMMD did not return a complete renewed Forge session.",
|
|
68207
|
+
status: 500
|
|
68208
|
+
});
|
|
68209
|
+
const assertion = yield* validateCloudOwnerCallbackAssertion({
|
|
68210
|
+
serverAuth,
|
|
68211
|
+
assertionToken: credentials.forgeOwnerAssertion,
|
|
68212
|
+
authenticatedUserId: userId
|
|
68213
|
+
});
|
|
68214
|
+
if (!assertion) return yield* require_Schema$1.die(/* @__PURE__ */ new Error("Expected a renewed Forge Cloud owner assertion."));
|
|
68215
|
+
yield* persistCloudOwnerArtifacts({
|
|
68216
|
+
assertion,
|
|
68217
|
+
accessToken: patToStore,
|
|
68218
|
+
identity: cmmdIdentity,
|
|
68219
|
+
jwtToken: credentials.jwtToStore,
|
|
68220
|
+
refreshToken: credentials.refreshToken,
|
|
68221
|
+
sessionGrant,
|
|
68222
|
+
sessionGrantExpiresAt: void 0
|
|
68223
|
+
});
|
|
68224
|
+
return yield* require_HttpServer.jsonUnsafe({ renewed: true }, { status: 200 }).pipe(require_HttpServer.setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
|
|
68225
|
+
}).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
67887
68226
|
const authLogoutRouteLayer = add$1("POST", "/api/auth/logout", require_Schema$1.gen(function* () {
|
|
67888
68227
|
const request = yield* require_HttpServer.HttpServerRequest;
|
|
67889
68228
|
const serverAuth = yield* ServerAuth;
|
|
@@ -67906,14 +68245,24 @@ const authLogoutRouteLayer = add$1("POST", "/api/auth/logout", require_Schema$1.
|
|
|
67906
68245
|
sameSite: "lax",
|
|
67907
68246
|
secure
|
|
67908
68247
|
}));
|
|
67909
|
-
if (cloudShell)
|
|
67910
|
-
|
|
67911
|
-
|
|
67912
|
-
|
|
67913
|
-
|
|
67914
|
-
|
|
67915
|
-
|
|
67916
|
-
|
|
68248
|
+
if (cloudShell) {
|
|
68249
|
+
response = yield* response.pipe(require_HttpServer.setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, "", {
|
|
68250
|
+
expires: /* @__PURE__ */ new Date(0),
|
|
68251
|
+
httpOnly: true,
|
|
68252
|
+
maxAge: 0,
|
|
68253
|
+
path: "/",
|
|
68254
|
+
sameSite: "lax",
|
|
68255
|
+
secure: true
|
|
68256
|
+
}));
|
|
68257
|
+
response = yield* response.pipe(require_HttpServer.setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, "", {
|
|
68258
|
+
expires: /* @__PURE__ */ new Date(0),
|
|
68259
|
+
httpOnly: true,
|
|
68260
|
+
maxAge: 0,
|
|
68261
|
+
path: "/api/auth",
|
|
68262
|
+
sameSite: "lax",
|
|
68263
|
+
secure: true
|
|
68264
|
+
}));
|
|
68265
|
+
}
|
|
67917
68266
|
response = yield* pipeExpireLegacySessionCookie(sessions.cookieName, require_Schema$1.succeed(response), { secure });
|
|
67918
68267
|
if (sessions.cookieName !== "forge_session") response = yield* response.pipe(require_HttpServer.setCookie(BASE_SESSION_COOKIE_NAME, "", {
|
|
67919
68268
|
expires: /* @__PURE__ */ new Date(0),
|
|
@@ -67936,7 +68285,8 @@ const CommandBootstrapInput = require_Schema$1.Struct({
|
|
|
67936
68285
|
refreshToken: require_Schema$1.optionalKey(require_Schema$1.String),
|
|
67937
68286
|
userId: require_Schema$1.optionalKey(require_Schema$1.Number),
|
|
67938
68287
|
displayName: require_Schema$1.optionalKey(require_Schema$1.String),
|
|
67939
|
-
user: require_Schema$1.optionalKey(require_Schema$1.Unknown)
|
|
68288
|
+
user: require_Schema$1.optionalKey(require_Schema$1.Unknown),
|
|
68289
|
+
clientDeviceType: require_Schema$1.optionalKey(require_Schema$1.Literals(["desktop", "mobile"]))
|
|
67940
68290
|
});
|
|
67941
68291
|
const CommandOAuthDeviceInput = require_Schema$1.Struct({
|
|
67942
68292
|
client_id: require_Schema$1.String,
|
|
@@ -68402,6 +68752,11 @@ function commandOAuthProxyStatus(path, status, body) {
|
|
|
68402
68752
|
}
|
|
68403
68753
|
const authBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap", require_Schema$1.gen(function* () {
|
|
68404
68754
|
const request = yield* require_HttpServer.HttpServerRequest;
|
|
68755
|
+
const limited = rateLimitOrRespond({
|
|
68756
|
+
routeClass: "auth-bootstrap",
|
|
68757
|
+
clientKey: clientKeyFromRequest(request)
|
|
68758
|
+
});
|
|
68759
|
+
if (limited) return limited;
|
|
68405
68760
|
const serverAuth = yield* ServerAuth;
|
|
68406
68761
|
const sessions = yield* SessionCredentialService;
|
|
68407
68762
|
const payload = yield* require_HttpServer.schemaBodyJson(AuthBootstrapInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
@@ -68419,6 +68774,11 @@ const authBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap", require_Sch
|
|
|
68419
68774
|
}).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
68420
68775
|
const authCommandBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/command", require_Schema$1.gen(function* () {
|
|
68421
68776
|
const request = yield* require_HttpServer.HttpServerRequest;
|
|
68777
|
+
const limited = rateLimitOrRespond({
|
|
68778
|
+
routeClass: "auth-bootstrap",
|
|
68779
|
+
clientKey: clientKeyFromRequest(request)
|
|
68780
|
+
});
|
|
68781
|
+
if (limited) return limited;
|
|
68422
68782
|
const serverAuth = yield* ServerAuth;
|
|
68423
68783
|
const sessions = yield* SessionCredentialService;
|
|
68424
68784
|
if (serverAuth.isForgeCloudShell?.() === true) return yield* new AuthError({
|
|
@@ -68467,6 +68827,11 @@ const authCommandBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/comman
|
|
|
68467
68827
|
}).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
68468
68828
|
const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/command/bearer", require_Schema$1.gen(function* () {
|
|
68469
68829
|
const request = yield* require_HttpServer.HttpServerRequest;
|
|
68830
|
+
const limited = rateLimitOrRespond({
|
|
68831
|
+
routeClass: "auth-bootstrap",
|
|
68832
|
+
clientKey: clientKeyFromRequest(request)
|
|
68833
|
+
});
|
|
68834
|
+
if (limited) return limited;
|
|
68470
68835
|
const serverAuth = yield* ServerAuth;
|
|
68471
68836
|
const sessions = yield* SessionCredentialService;
|
|
68472
68837
|
if (serverAuth.isForgeCloudShell?.() === true) return yield* new AuthError({
|
|
@@ -68484,13 +68849,10 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
|
|
|
68484
68849
|
method: "bearer-session-token",
|
|
68485
68850
|
role: "owner",
|
|
68486
68851
|
subject: sessionSubject,
|
|
68487
|
-
client: {
|
|
68488
|
-
|
|
68489
|
-
|
|
68490
|
-
|
|
68491
|
-
}),
|
|
68492
|
-
deviceType: "desktop"
|
|
68493
|
-
}
|
|
68852
|
+
client: resolveCommandBootstrapClient({
|
|
68853
|
+
request,
|
|
68854
|
+
clientDeviceType: payload.clientDeviceType
|
|
68855
|
+
})
|
|
68494
68856
|
}).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
68495
68857
|
message: "Failed to issue authenticated bearer session.",
|
|
68496
68858
|
cause
|
|
@@ -68500,7 +68862,10 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
|
|
|
68500
68862
|
payload,
|
|
68501
68863
|
cmmdIdentity: validated.cmmdIdentity
|
|
68502
68864
|
});
|
|
68503
|
-
if (!payload.refreshToken?.trim()) yield* clearDesktopBearerRefreshTokensForSubject(
|
|
68865
|
+
if ((payload.clientDeviceType ?? "desktop") === "desktop" && !payload.refreshToken?.trim()) yield* clearDesktopBearerRefreshTokensForSubject({
|
|
68866
|
+
subject: sessionSubject,
|
|
68867
|
+
clientUserAgent: request.headers["user-agent"]
|
|
68868
|
+
}).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
68504
68869
|
message: "Failed to retire legacy desktop refresh credentials.",
|
|
68505
68870
|
status: 500,
|
|
68506
68871
|
cause
|
|
@@ -68515,6 +68880,11 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
|
|
|
68515
68880
|
}, { status: 200 });
|
|
68516
68881
|
}).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
68517
68882
|
const authCmmdOAuthDeviceRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/device", require_Schema$1.gen(function* () {
|
|
68883
|
+
const limited = rateLimitOrRespond({
|
|
68884
|
+
routeClass: "auth-device-poll",
|
|
68885
|
+
clientKey: clientKeyFromRequest(yield* require_HttpServer.HttpServerRequest)
|
|
68886
|
+
});
|
|
68887
|
+
if (limited) return limited;
|
|
68518
68888
|
const payload = yield* require_HttpServer.schemaBodyJson(CommandOAuthDeviceInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
68519
68889
|
message: "Invalid CMMD OAuth device payload.",
|
|
68520
68890
|
status: 400,
|
|
@@ -68526,7 +68896,17 @@ const authCmmdOAuthDeviceRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/device
|
|
|
68526
68896
|
...payload.scope ? { scope: payload.scope } : {}
|
|
68527
68897
|
});
|
|
68528
68898
|
}).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
68899
|
+
/**
|
|
68900
|
+
* The device-code poll. Called every `interval` seconds by design (RFC
|
|
68901
|
+
* 8628), which is exactly why FOR-36706 puts a limiter on it: a client that
|
|
68902
|
+
* ignores `slow_down` — a bug, not a person — has no other backstop here.
|
|
68903
|
+
*/
|
|
68529
68904
|
const authCmmdOAuthTokenRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/token", require_Schema$1.gen(function* () {
|
|
68905
|
+
const limited = rateLimitOrRespond({
|
|
68906
|
+
routeClass: "auth-device-poll",
|
|
68907
|
+
clientKey: clientKeyFromRequest(yield* require_HttpServer.HttpServerRequest)
|
|
68908
|
+
});
|
|
68909
|
+
if (limited) return limited;
|
|
68530
68910
|
const payload = yield* require_HttpServer.schemaBodyJson(CommandOAuthTokenInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
68531
68911
|
message: "Invalid CMMD OAuth token payload.",
|
|
68532
68912
|
status: 400,
|
|
@@ -68568,6 +68948,14 @@ function completeCloudOwnerPasswordLogin(input) {
|
|
|
68568
68948
|
authenticatedUserId: userId
|
|
68569
68949
|
});
|
|
68570
68950
|
if (!assertion) return yield* require_Schema$1.die(/* @__PURE__ */ new Error("Expected a Forge Cloud owner assertion in cloud shell mode."));
|
|
68951
|
+
const sessionGrant = resolveCloudOwnerSessionGrantCookie({
|
|
68952
|
+
token: credentials.sessionGrant,
|
|
68953
|
+
expiresAt: credentials.sessionGrantExpiresAt
|
|
68954
|
+
});
|
|
68955
|
+
if (!sessionGrant) return yield* new AuthError({
|
|
68956
|
+
message: "CMMD did not return a durable Forge Cloud session grant.",
|
|
68957
|
+
status: 500
|
|
68958
|
+
});
|
|
68571
68959
|
yield* persistCloudOwnerArtifacts({
|
|
68572
68960
|
assertion,
|
|
68573
68961
|
accessToken: patToStore,
|
|
@@ -68577,10 +68965,12 @@ function completeCloudOwnerPasswordLogin(input) {
|
|
|
68577
68965
|
sessionGrant: credentials.sessionGrant,
|
|
68578
68966
|
sessionGrantExpiresAt: credentials.sessionGrantExpiresAt
|
|
68579
68967
|
});
|
|
68580
|
-
|
|
68968
|
+
let response = require_HttpServer.jsonUnsafe({
|
|
68581
68969
|
authenticated: true,
|
|
68582
68970
|
cmmdIdentity
|
|
68583
68971
|
}, { status: 200 }).pipe(require_HttpServer.setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
|
|
68972
|
+
if (sessionGrant) response = response.pipe(require_Schema$1.flatMap(require_HttpServer.setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, sessionGrant.token, cloudOwnerSessionGrantCookieOptions(sessionGrant))));
|
|
68973
|
+
return yield* response;
|
|
68584
68974
|
});
|
|
68585
68975
|
}
|
|
68586
68976
|
const authCmmdPasswordTokenRouteLayer = add$1("POST", "/api/auth/cmmd-password/token", require_Schema$1.gen(function* () {
|
|
@@ -68754,6 +69144,14 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", require_Sch
|
|
|
68754
69144
|
assertionToken: forgeOwnerAssertion,
|
|
68755
69145
|
authenticatedUserId: userId
|
|
68756
69146
|
});
|
|
69147
|
+
const cloudSessionGrant = cloudOwnerAssertion ? resolveCloudOwnerSessionGrantCookie({
|
|
69148
|
+
token: sessionGrant,
|
|
69149
|
+
expiresAt: sessionGrantExpiresAt
|
|
69150
|
+
}) : null;
|
|
69151
|
+
if (cloudOwnerAssertion && !cloudSessionGrant) return yield* new AuthError({
|
|
69152
|
+
message: "CMMD did not return a durable Forge Cloud session grant.",
|
|
69153
|
+
status: 500
|
|
69154
|
+
});
|
|
68757
69155
|
const sessionSubject = resolveCommandOAuthSubject(userId);
|
|
68758
69156
|
const issued = cloudOwnerAssertion ? null : yield* sessions.issue({
|
|
68759
69157
|
method: "browser-session-cookie",
|
|
@@ -68785,7 +69183,7 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", require_Sch
|
|
|
68785
69183
|
else yield* persistLocalOAuthArtifacts(persistenceInput);
|
|
68786
69184
|
const homeUrl = `${publicUrl}/`;
|
|
68787
69185
|
const secureCookie = shouldUseSecureCookie(publicUrl);
|
|
68788
|
-
if (cloudOwnerAssertion) return yield* cloudOwnerOAuthResponse(homeUrl, cloudOwnerAssertion);
|
|
69186
|
+
if (cloudOwnerAssertion) return yield* cloudOwnerOAuthResponse(homeUrl, cloudOwnerAssertion, cloudSessionGrant);
|
|
68789
69187
|
if (!issued) return yield* require_Schema$1.die(/* @__PURE__ */ new Error("Expected a local session outside Forge Cloud."));
|
|
68790
69188
|
return yield* pipeExpireLegacySessionCookie(sessions.cookieName, require_HttpServer.redirect(homeUrl, { status: 302 }).pipe(require_HttpServer.setCookie(sessions.cookieName, issued.token, {
|
|
68791
69189
|
expires: require_Schema$1.toDate(issued.expiresAt),
|
|
@@ -68803,6 +69201,11 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", require_Sch
|
|
|
68803
69201
|
}).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
68804
69202
|
const authBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/bearer", require_Schema$1.gen(function* () {
|
|
68805
69203
|
const request = yield* require_HttpServer.HttpServerRequest;
|
|
69204
|
+
const limited = rateLimitOrRespond({
|
|
69205
|
+
routeClass: "auth-bootstrap",
|
|
69206
|
+
clientKey: clientKeyFromRequest(request)
|
|
69207
|
+
});
|
|
69208
|
+
if (limited) return limited;
|
|
68806
69209
|
const serverAuth = yield* ServerAuth;
|
|
68807
69210
|
const payload = yield* require_HttpServer.schemaBodyJson(AuthBootstrapInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
68808
69211
|
message: "Invalid bootstrap payload.",
|
|
@@ -68835,6 +69238,11 @@ const authBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/bearer"
|
|
|
68835
69238
|
*/
|
|
68836
69239
|
const authOAuthTokenExchangeRouteLayer = add$1("POST", "/oauth/token", require_Schema$1.gen(function* () {
|
|
68837
69240
|
const request = yield* require_HttpServer.HttpServerRequest;
|
|
69241
|
+
const limited = rateLimitOrRespond({
|
|
69242
|
+
routeClass: "auth-token-exchange",
|
|
69243
|
+
clientKey: clientKeyFromRequest(request)
|
|
69244
|
+
});
|
|
69245
|
+
if (limited) return limited;
|
|
68838
69246
|
const serverAuth = yield* ServerAuth;
|
|
68839
69247
|
const payload = yield* require_HttpServer.schemaBodyUrlParams(OAuthTokenExchangeInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
68840
69248
|
message: "Invalid token exchange payload.",
|
|
@@ -70829,6 +71237,11 @@ function redeemCmmdSessionGrant(grantToken) {
|
|
|
70829
71237
|
};
|
|
70830
71238
|
}).catch(() => null);
|
|
70831
71239
|
}
|
|
71240
|
+
const REJECTED_OAUTH_ERROR_CODES = new Set([
|
|
71241
|
+
"invalid_grant",
|
|
71242
|
+
"invalid_client",
|
|
71243
|
+
"invalid_request"
|
|
71244
|
+
]);
|
|
70832
71245
|
/**
|
|
70833
71246
|
* Rotate a CMMD refresh token into a fresh access JWT.
|
|
70834
71247
|
*
|
|
@@ -70841,7 +71254,7 @@ function redeemCmmdSessionGrant(grantToken) {
|
|
|
70841
71254
|
* code are what identify the failure, and `invalid_grant` in particular means
|
|
70842
71255
|
* the token was already spent, which is a different fix from a 500.
|
|
70843
71256
|
*/
|
|
70844
|
-
function refreshCmmdJwt(refreshToken) {
|
|
71257
|
+
function refreshCmmdJwt(refreshToken, observer) {
|
|
70845
71258
|
return fetch(new URL("/api/mcp-server/oauth/token", resolveCommandAppUrl()).toString(), {
|
|
70846
71259
|
method: "POST",
|
|
70847
71260
|
headers: { "Content-Type": "application/json" },
|
|
@@ -70852,7 +71265,9 @@ function refreshCmmdJwt(refreshToken) {
|
|
|
70852
71265
|
})
|
|
70853
71266
|
}).then(async (response) => {
|
|
70854
71267
|
if (!response.ok) {
|
|
70855
|
-
|
|
71268
|
+
const errorCode = await readOAuthErrorCode(response);
|
|
71269
|
+
if (errorCode !== null && REJECTED_OAUTH_ERROR_CODES.has(errorCode)) observer?.onCredentialRejected?.();
|
|
71270
|
+
console.warn(`[cmmd-auth] CMMD refused a JWT rotation with ${response.status}${errorCode === null ? "" : ` (${errorCode})`}`);
|
|
70856
71271
|
return null;
|
|
70857
71272
|
}
|
|
70858
71273
|
const parsed = parseCmmdOAuthRefreshResponse(await response.json());
|
|
@@ -70864,20 +71279,73 @@ function refreshCmmdJwt(refreshToken) {
|
|
|
70864
71279
|
});
|
|
70865
71280
|
}
|
|
70866
71281
|
/**
|
|
70867
|
-
* The OAuth `error` code from a refusal,
|
|
71282
|
+
* The OAuth `error` code from a refusal, lowercased, or null when there is none.
|
|
70868
71283
|
*
|
|
70869
71284
|
* Best effort by design: a body that is not JSON, or is already consumed, must
|
|
70870
|
-
* not turn a logged failure into an unlogged one.
|
|
71285
|
+
* not turn a logged failure into an unlogged one. Null also reads correctly as
|
|
71286
|
+
* "no evidence the credential is dead", which is the safe answer for a caller
|
|
71287
|
+
* deciding whether to keep a refresh token.
|
|
70871
71288
|
*/
|
|
70872
|
-
async function
|
|
71289
|
+
async function readOAuthErrorCode(response) {
|
|
70873
71290
|
try {
|
|
70874
71291
|
const code = (await response.json())["error"];
|
|
70875
|
-
return typeof code === "string" && code.trim().length > 0 ?
|
|
71292
|
+
return typeof code === "string" && code.trim().length > 0 ? code.trim().toLowerCase() : null;
|
|
70876
71293
|
} catch {
|
|
70877
|
-
return
|
|
71294
|
+
return null;
|
|
70878
71295
|
}
|
|
70879
71296
|
}
|
|
70880
71297
|
//#endregion
|
|
71298
|
+
//#region src/command/cmmdRotationFence.ts
|
|
71299
|
+
/**
|
|
71300
|
+
* True when Forge must not rotate this session's CMMD credentials.
|
|
71301
|
+
*
|
|
71302
|
+
* Fails closed: if the session row cannot be read, assume the fence applies.
|
|
71303
|
+
* Spending a token Forge had no right to spend is worse than one extra
|
|
71304
|
+
* reconnect.
|
|
71305
|
+
*/
|
|
71306
|
+
function isRotationFencedSession(sessionId) {
|
|
71307
|
+
if (readCommandRefreshTokenForSession(sessionId)) return require_Schema$1.succeed(false);
|
|
71308
|
+
return isDesktopBearerAuthSession(sessionId).pipe(require_Schema$1.orElseSucceed(() => true));
|
|
71309
|
+
}
|
|
71310
|
+
//#endregion
|
|
71311
|
+
//#region src/cmmdConnection/cmmdCredentialRejection.ts
|
|
71312
|
+
/**
|
|
71313
|
+
* How long a recorded rejection stays usable.
|
|
71314
|
+
*
|
|
71315
|
+
* Long enough to span the resolver ladder and the response that follows it,
|
|
71316
|
+
* short enough that it cannot outlive the credential it describes.
|
|
71317
|
+
*/
|
|
71318
|
+
const REJECTION_TTL_MS = 6e4;
|
|
71319
|
+
const rejectedAt = /* @__PURE__ */ new Map();
|
|
71320
|
+
/** Record that CMMD refused this session's credential outright. */
|
|
71321
|
+
function noteCmmdCredentialRejected(sessionId) {
|
|
71322
|
+
rejectedAt.set(sessionId, Date.now());
|
|
71323
|
+
}
|
|
71324
|
+
/** Whether CMMD refused this session's credential recently enough to act on. */
|
|
71325
|
+
function hasCmmdCredentialRejection(sessionId) {
|
|
71326
|
+
const observedAt = rejectedAt.get(sessionId);
|
|
71327
|
+
if (observedAt === void 0) return false;
|
|
71328
|
+
if (Date.now() - observedAt <= REJECTION_TTL_MS) return true;
|
|
71329
|
+
rejectedAt.delete(sessionId);
|
|
71330
|
+
return false;
|
|
71331
|
+
}
|
|
71332
|
+
/** Drop a recorded rejection, once it has been acted on or superseded. */
|
|
71333
|
+
function forgetCmmdCredentialRejection(sessionId) {
|
|
71334
|
+
rejectedAt.delete(sessionId);
|
|
71335
|
+
}
|
|
71336
|
+
/**
|
|
71337
|
+
* Clear a session's CMMD credentials, but only if CMMD rejected them.
|
|
71338
|
+
*
|
|
71339
|
+
* This is the only door the 401 path may use. A resolver that returned null
|
|
71340
|
+
* because it was not allowed to try is a policy refusal and leaves the stored
|
|
71341
|
+
* credential exactly where it is, so the next request can rotate it.
|
|
71342
|
+
*/
|
|
71343
|
+
const clearCmmdAuthAfterCmmdRejection = (sessionId) => require_Schema$1.gen(function* () {
|
|
71344
|
+
if (!hasCmmdCredentialRejection(sessionId)) return;
|
|
71345
|
+
yield* clearUnrecoverableCmmdAuthForSession(sessionId);
|
|
71346
|
+
forgetCmmdCredentialRejection(sessionId);
|
|
71347
|
+
});
|
|
71348
|
+
//#endregion
|
|
70881
71349
|
//#region src/command/cmmdJwtResolver.ts
|
|
70882
71350
|
function usableCmmdUserJwt(token) {
|
|
70883
71351
|
return isCmmdUserAccessJwt(token) ? token : null;
|
|
@@ -70890,7 +71358,7 @@ function refreshOnceForSession(sessionId, refreshToken) {
|
|
|
70890
71358
|
return require_Schema$1.promise(() => {
|
|
70891
71359
|
const existing = inFlightRefreshes.get(sessionId);
|
|
70892
71360
|
if (existing) return existing;
|
|
70893
|
-
const pending = refreshCmmdJwt(refreshToken).catch(() => null).finally(() => {
|
|
71361
|
+
const pending = refreshCmmdJwt(refreshToken, { onCredentialRejected: () => noteCmmdCredentialRejected(sessionId) }).catch(() => null).finally(() => {
|
|
70894
71362
|
inFlightRefreshes.delete(sessionId);
|
|
70895
71363
|
});
|
|
70896
71364
|
inFlightRefreshes.set(sessionId, pending);
|
|
@@ -71000,13 +71468,16 @@ function trySubjectFallback(sessionId, subject, logPrefix) {
|
|
|
71000
71468
|
* Fail-closed: never returns a non-user / malformed Bearer. Callers must map
|
|
71001
71469
|
* null to CMMD_AUTH_REQUIRED and must not call CMMD upstream with a bad token.
|
|
71002
71470
|
*/
|
|
71003
|
-
function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false }) {
|
|
71471
|
+
function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false, staleAccessToken = null }) {
|
|
71004
71472
|
const cached = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
|
|
71005
|
-
if (
|
|
71473
|
+
if (cached && !isJwtExpiredForSession(sessionId)) {
|
|
71474
|
+
if (!forceRefresh) return require_Schema$1.succeed(cached);
|
|
71475
|
+
if (staleAccessToken && cached !== staleAccessToken) return require_Schema$1.succeed(cached);
|
|
71476
|
+
}
|
|
71006
71477
|
const normalizedSubject = subject?.trim() || null;
|
|
71007
71478
|
if (!normalizedSubject && !forceRefresh) return require_Schema$1.succeed(null);
|
|
71008
71479
|
return require_Schema$1.gen(function* () {
|
|
71009
|
-
if (yield*
|
|
71480
|
+
if (yield* isRotationFencedSession(sessionId)) return null;
|
|
71010
71481
|
if (forceRefresh) {
|
|
71011
71482
|
const forced = yield* tryOwnCredentialRecovery(sessionId, logPrefix);
|
|
71012
71483
|
if (forced) return forced;
|
|
@@ -71765,15 +72236,25 @@ function clearCmmdAuthAfterFailure(sessionId) {
|
|
|
71765
72236
|
})));
|
|
71766
72237
|
}
|
|
71767
72238
|
/**
|
|
71768
|
-
* The single "no usable CMMD credential" response:
|
|
71769
|
-
*
|
|
71770
|
-
*
|
|
71771
|
-
*
|
|
71772
|
-
*
|
|
71773
|
-
*
|
|
72239
|
+
* The single "no usable CMMD credential" response: returns the canonical
|
|
72240
|
+
* `CMMD_AUTH_REQUIRED` body. Every prologue path that decides the session has
|
|
72241
|
+
* no way to reach CMMD — a missing PAT, a JWT that fails to resolve, a bespoke
|
|
72242
|
+
* gate elsewhere in http.ts — emits this same response so the Shell only ever
|
|
72243
|
+
* has one `CMMD_AUTH_REQUIRED` body to branch on.
|
|
72244
|
+
*
|
|
72245
|
+
* It used to delete the session's stored credentials on the way out, and that
|
|
72246
|
+
* was the wrong reading of its own argument. Most of the paths that reach here
|
|
72247
|
+
* are refusals, not rejections: the resolver was fenced, the subject was
|
|
72248
|
+
* missing, a gate said no. A phone would arrive holding a refresh token good
|
|
72249
|
+
* for the next three months, be refused by a rule, and have that token deleted
|
|
72250
|
+
* as "unrecoverable". The clear now happens only where CMMD has actually
|
|
72251
|
+
* refused the credential, which `clearCmmdAuthAfterCmmdRejection` checks.
|
|
71774
72252
|
*/
|
|
71775
72253
|
function respondCmmdConnectionRequired(sessionId) {
|
|
71776
|
-
return
|
|
72254
|
+
return clearCmmdAuthAfterCmmdRejection(sessionId).pipe(require_Schema$1.catch_((error) => require_Schema$1.logWarning("Failed to clear CMMD auth that CMMD itself rejected.", {
|
|
72255
|
+
cause: error,
|
|
72256
|
+
sessionId
|
|
72257
|
+
})), require_Schema$1.as(buildProxyErrorResponse(new CmmdProxyAuthenticationRequiredError("CMMD connection required. Reconnect CMMD to continue."))));
|
|
71777
72258
|
}
|
|
71778
72259
|
async function dispatchToProxy(method, jwt, cmmdPath, rawBody, orgId, searchParams) {
|
|
71779
72260
|
return cmmdGateway.request({
|
|
@@ -71829,6 +72310,11 @@ function buildProxyRouteLayer(route) {
|
|
|
71829
72310
|
const handler = require_Schema$1.gen(function* () {
|
|
71830
72311
|
const req = yield* require_HttpServer.HttpServerRequest;
|
|
71831
72312
|
const session = yield* (yield* ServerAuth).authenticateHttpRequest(req);
|
|
72313
|
+
const limited = rateLimitOrRespond({
|
|
72314
|
+
routeClass: "cmmd-proxy",
|
|
72315
|
+
clientKey: session.sessionId
|
|
72316
|
+
});
|
|
72317
|
+
if (limited) return limited;
|
|
71832
72318
|
const pathParams = yield* params;
|
|
71833
72319
|
const cmmdPath = substitutePath(route.cmmdPath, pathParams);
|
|
71834
72320
|
if (!cmmdPath) return require_HttpServer.jsonUnsafe({ error: "Missing required path parameter." }, { status: 400 });
|
|
@@ -71889,6 +72375,11 @@ function buildProxyHandlerEffect(route) {
|
|
|
71889
72375
|
return require_Schema$1.gen(function* () {
|
|
71890
72376
|
const req = yield* require_HttpServer.HttpServerRequest;
|
|
71891
72377
|
const session = yield* (yield* ServerAuth).authenticateHttpRequest(req);
|
|
72378
|
+
const limited = rateLimitOrRespond({
|
|
72379
|
+
routeClass: "cmmd-proxy",
|
|
72380
|
+
clientKey: session.sessionId
|
|
72381
|
+
});
|
|
72382
|
+
if (limited) return limited;
|
|
71892
72383
|
if (!readCommandAccessTokenForSession(session.sessionId)) return yield* respondCmmdConnectionRequired(session.sessionId);
|
|
71893
72384
|
const parsed = yield* route.parse(req);
|
|
71894
72385
|
if (!parsed.ok) return require_HttpServer.jsonUnsafe(parsed.body, { status: parsed.status });
|
|
@@ -83674,12 +84165,12 @@ const makePullRequestService = require_Schema$1.gen(function* () {
|
|
|
83674
84165
|
}));
|
|
83675
84166
|
const submitReview = (input) => requireProject(input).pipe(require_Schema$1.flatMap((project) => {
|
|
83676
84167
|
const review = project.api.capabilities.review;
|
|
83677
|
-
if (!review.verdicts.includes(input.verdict)) return refuse(`This host cannot ${VERDICT_LABELS[input.verdict]} a change request.`);
|
|
83678
|
-
if (input.comments.length > 0 && !review.inlineComment) return refuse("This host cannot comment on a line of a change request.");
|
|
83679
|
-
if (input.verdict !== "approve" && input.body.trim().length === 0 && input.comments.length === 0) return refuse("A review needs a summary or at least one comment.");
|
|
84168
|
+
if (!review.verdicts.includes(input.verdict)) return refuse$1(`This host cannot ${VERDICT_LABELS[input.verdict]} a change request.`);
|
|
84169
|
+
if (input.comments.length > 0 && !review.inlineComment) return refuse$1("This host cannot comment on a line of a change request.");
|
|
84170
|
+
if (input.verdict !== "approve" && input.body.trim().length === 0 && input.comments.length === 0) return refuse$1("A review needs a summary or at least one comment.");
|
|
83680
84171
|
return viewerPermissionsOf(project, input, "submitReview").pipe(require_Schema$1.flatMap((viewer) => {
|
|
83681
|
-
if (!viewer.verdicts.includes(input.verdict)) return refuse(`You need write access on this repository to ${VERDICT_LABELS[input.verdict]} a change request.`);
|
|
83682
|
-
if (input.comments.length > 0 && !viewer.comment) return refuse("You need write access on this repository to comment on a line of a change request.");
|
|
84172
|
+
if (!viewer.verdicts.includes(input.verdict)) return refuse$1(`You need write access on this repository to ${VERDICT_LABELS[input.verdict]} a change request.`);
|
|
84173
|
+
if (input.comments.length > 0 && !viewer.comment) return refuse$1("You need write access on this repository to comment on a line of a change request.");
|
|
83683
84174
|
return project.api.submitReview({
|
|
83684
84175
|
projectId: project.project.id,
|
|
83685
84176
|
cwd: project.project.workspaceRoot,
|
|
@@ -84077,7 +84568,7 @@ const toEntry = (input) => {
|
|
|
84077
84568
|
...input.item.reviewDecision === void 0 || input.item.reviewDecision === null ? {} : { reviewDecision: input.item.reviewDecision }
|
|
84078
84569
|
};
|
|
84079
84570
|
};
|
|
84080
|
-
const refuse = (detail) => require_Schema$1.fail(new PullRequestOperationError({
|
|
84571
|
+
const refuse$1 = (detail) => require_Schema$1.fail(new PullRequestOperationError({
|
|
84081
84572
|
operation: "submitReview",
|
|
84082
84573
|
detail
|
|
84083
84574
|
}));
|
|
@@ -93944,6 +94435,166 @@ const admitNormalizedDispatchCommand = (command, admit) => require_Schema$1.unin
|
|
|
93944
94435
|
};
|
|
93945
94436
|
}));
|
|
93946
94437
|
//#endregion
|
|
94438
|
+
//#region src/orchestration/threadDetailWindow.ts
|
|
94439
|
+
function earliest(current, candidate) {
|
|
94440
|
+
return current === void 0 || candidate < current ? candidate : current;
|
|
94441
|
+
}
|
|
94442
|
+
/**
|
|
94443
|
+
* Every turn in the thread, oldest first.
|
|
94444
|
+
*
|
|
94445
|
+
* Ordering is by the earliest timestamp across all four windowed collections
|
|
94446
|
+
* rather than by message order alone: a turn can open with an activity (a tool
|
|
94447
|
+
* call before any text) and a checkpoint-only turn has no message at all.
|
|
94448
|
+
* Ties break on turn id so the order is total and a cursor means the same thing
|
|
94449
|
+
* on every read.
|
|
94450
|
+
*/
|
|
94451
|
+
function orderThreadTurns(thread) {
|
|
94452
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
94453
|
+
const touch = (turnId, at, isUserMessage) => {
|
|
94454
|
+
if (turnId === null) return;
|
|
94455
|
+
const existing = anchors.get(turnId);
|
|
94456
|
+
anchors.set(turnId, {
|
|
94457
|
+
at: earliest(existing?.at, at),
|
|
94458
|
+
hasUserMessage: (existing?.hasUserMessage ?? false) || isUserMessage
|
|
94459
|
+
});
|
|
94460
|
+
};
|
|
94461
|
+
for (const message of thread.messages) touch(message.turnId, message.createdAt, message.role === "user");
|
|
94462
|
+
for (const activity of thread.activities) touch(activity.turnId, activity.createdAt, false);
|
|
94463
|
+
for (const plan of thread.proposedPlans) touch(plan.turnId, plan.createdAt, false);
|
|
94464
|
+
for (const checkpoint of thread.checkpoints) touch(checkpoint.turnId, checkpoint.completedAt, false);
|
|
94465
|
+
return Array.from(anchors, ([turnId, anchor]) => ({
|
|
94466
|
+
turnId,
|
|
94467
|
+
...anchor
|
|
94468
|
+
})).toSorted((left, right) => left.at === right.at ? left.turnId.localeCompare(right.turnId) : left.at < right.at ? -1 : 1);
|
|
94469
|
+
}
|
|
94470
|
+
/**
|
|
94471
|
+
* Index of the first turn to include, counting back `turnLimit` user-anchored
|
|
94472
|
+
* turns from the end of `ordered`. Returns 0 when the thread holds fewer user
|
|
94473
|
+
* turns than the limit, which is how a short thread comes back whole.
|
|
94474
|
+
*/
|
|
94475
|
+
function windowStartIndex(ordered, turnLimit) {
|
|
94476
|
+
let userTurns = 0;
|
|
94477
|
+
for (let index = ordered.length - 1; index >= 0; index -= 1) {
|
|
94478
|
+
const anchor = ordered[index];
|
|
94479
|
+
if (anchor !== void 0 && anchor.hasUserMessage) {
|
|
94480
|
+
userTurns += 1;
|
|
94481
|
+
if (userTurns === turnLimit) return index;
|
|
94482
|
+
}
|
|
94483
|
+
}
|
|
94484
|
+
return 0;
|
|
94485
|
+
}
|
|
94486
|
+
/**
|
|
94487
|
+
* The turns a windowed read returns.
|
|
94488
|
+
*
|
|
94489
|
+
* `beforeCursor` bounds the read to turns strictly older than a cursor an
|
|
94490
|
+
* earlier read handed out, which is what "load earlier" sends back. A cursor
|
|
94491
|
+
* naming a turn this thread no longer holds, after a revert, say, yields an
|
|
94492
|
+
* empty page rather than the whole thread: returning everything would be a
|
|
94493
|
+
* silent full download at the exact moment the client asked for less.
|
|
94494
|
+
*/
|
|
94495
|
+
function selectWindowTurnIds(ordered, window) {
|
|
94496
|
+
const cursorIndex = window.beforeCursor === void 0 ? ordered.length : ordered.findIndex((anchor) => anchor.turnId === window.beforeCursor);
|
|
94497
|
+
if (cursorIndex < 0) return {
|
|
94498
|
+
turnIds: /* @__PURE__ */ new Set(),
|
|
94499
|
+
beforeCursor: null,
|
|
94500
|
+
hasMore: false,
|
|
94501
|
+
includeUnanchored: false
|
|
94502
|
+
};
|
|
94503
|
+
const candidates = ordered.slice(0, cursorIndex);
|
|
94504
|
+
const startIndex = window.turnLimit === void 0 ? 0 : windowStartIndex(candidates, window.turnLimit);
|
|
94505
|
+
const included = candidates.slice(startIndex);
|
|
94506
|
+
const oldest = included[0];
|
|
94507
|
+
return {
|
|
94508
|
+
turnIds: new Set(included.map((anchor) => anchor.turnId)),
|
|
94509
|
+
beforeCursor: oldest?.turnId ?? null,
|
|
94510
|
+
hasMore: startIndex > 0,
|
|
94511
|
+
includeUnanchored: startIndex === 0
|
|
94512
|
+
};
|
|
94513
|
+
}
|
|
94514
|
+
function keepsTurn(selection, turnId) {
|
|
94515
|
+
return turnId === null ? selection.includeUnanchored : selection.turnIds.has(turnId);
|
|
94516
|
+
}
|
|
94517
|
+
/**
|
|
94518
|
+
* Applies a turn window to a full thread detail read.
|
|
94519
|
+
*
|
|
94520
|
+
* A request with neither field is a full read and comes back untouched with no
|
|
94521
|
+
* page, which is exactly what a pre-pagination client already receives. Only
|
|
94522
|
+
* the four turn-scoped collections are filtered; thread metadata, the session,
|
|
94523
|
+
* and `latestTurn` describe the thread as a whole and stay as they are, so a
|
|
94524
|
+
* windowed snapshot still renders a correct header and a correct running state.
|
|
94525
|
+
*/
|
|
94526
|
+
function windowThreadDetail(input) {
|
|
94527
|
+
const { thread, window, snapshotSequence } = input;
|
|
94528
|
+
if (window.turnLimit === void 0 && window.beforeCursor === void 0) return {
|
|
94529
|
+
thread,
|
|
94530
|
+
page: void 0
|
|
94531
|
+
};
|
|
94532
|
+
const selection = selectWindowTurnIds(orderThreadTurns(thread), window);
|
|
94533
|
+
return {
|
|
94534
|
+
thread: {
|
|
94535
|
+
...thread,
|
|
94536
|
+
messages: thread.messages.filter((row) => keepsTurn(selection, row.turnId)),
|
|
94537
|
+
activities: thread.activities.filter((row) => keepsTurn(selection, row.turnId)),
|
|
94538
|
+
proposedPlans: thread.proposedPlans.filter((row) => keepsTurn(selection, row.turnId)),
|
|
94539
|
+
checkpoints: thread.checkpoints.filter((row) => keepsTurn(selection, row.turnId)),
|
|
94540
|
+
...thread.turnModelSelections === void 0 ? {} : { turnModelSelections: thread.turnModelSelections.filter((row) => keepsTurn(selection, row.turnId)) }
|
|
94541
|
+
},
|
|
94542
|
+
page: {
|
|
94543
|
+
beforeCursor: selection.hasMore ? selection.beforeCursor : null,
|
|
94544
|
+
hasMore: selection.hasMore,
|
|
94545
|
+
snapshotSequence
|
|
94546
|
+
}
|
|
94547
|
+
};
|
|
94548
|
+
}
|
|
94549
|
+
/** Whether a subscriber's cursor can be served by replay rather than a snapshot. */
|
|
94550
|
+
function canResumeFromSequence(input) {
|
|
94551
|
+
if (input.afterSequence > input.currentSequence) return false;
|
|
94552
|
+
return input.currentSequence - input.afterSequence <= (input.maxGap ?? 2e3);
|
|
94553
|
+
}
|
|
94554
|
+
function snapshotCatchUp(snapshot) {
|
|
94555
|
+
return {
|
|
94556
|
+
items: [{
|
|
94557
|
+
kind: "snapshot",
|
|
94558
|
+
snapshot
|
|
94559
|
+
}],
|
|
94560
|
+
watermark: snapshot.snapshotSequence
|
|
94561
|
+
};
|
|
94562
|
+
}
|
|
94563
|
+
function replayCatchUp(events, afterSequence) {
|
|
94564
|
+
return {
|
|
94565
|
+
items: events.map((event) => ({
|
|
94566
|
+
kind: "event",
|
|
94567
|
+
event
|
|
94568
|
+
})),
|
|
94569
|
+
watermark: events.at(-1)?.sequence ?? afterSequence
|
|
94570
|
+
};
|
|
94571
|
+
}
|
|
94572
|
+
/**
|
|
94573
|
+
* Builds the frames that precede live events: either the replayed catch-up or
|
|
94574
|
+
* the snapshot, plus the completion marker when one was asked for.
|
|
94575
|
+
*/
|
|
94576
|
+
const buildCatchUp = require_Schema$1.fn("threadSubscription.buildCatchUp")(function* (input, sources) {
|
|
94577
|
+
const afterSequence = input.afterSequence;
|
|
94578
|
+
if (afterSequence === void 0) return snapshotCatchUp(yield* sources.loadSnapshot);
|
|
94579
|
+
if (!canResumeFromSequence({
|
|
94580
|
+
afterSequence,
|
|
94581
|
+
currentSequence: yield* sources.currentSequence
|
|
94582
|
+
})) return snapshotCatchUp(yield* sources.loadSnapshot);
|
|
94583
|
+
const replayed = yield* require_Path.runCollect(sources.replayEvents(afterSequence));
|
|
94584
|
+
return replayCatchUp(Array.from(replayed), afterSequence);
|
|
94585
|
+
});
|
|
94586
|
+
function makeThreadSubscriptionStream(input, sources) {
|
|
94587
|
+
return require_Path.unwrap(require_Schema$1.gen(function* () {
|
|
94588
|
+
const liveEvents = yield* sources.subscribeLiveEvents;
|
|
94589
|
+
const catchUp = yield* buildCatchUp(input, sources);
|
|
94590
|
+
const marker = input.requestCompletionMarker === true ? [{ kind: "synchronized" }] : [];
|
|
94591
|
+
return require_Path.concat(require_Path.fromArray([...catchUp.items, ...marker]), liveEvents.pipe(require_Path.filter((event) => event.sequence > catchUp.watermark), require_Path.map((event) => ({
|
|
94592
|
+
kind: "event",
|
|
94593
|
+
event
|
|
94594
|
+
}))));
|
|
94595
|
+
}));
|
|
94596
|
+
}
|
|
94597
|
+
//#endregion
|
|
93947
94598
|
//#region src/orchestration/bootstrapCommandCoordinator.ts
|
|
93948
94599
|
const MAX_COMPLETED_COMMANDS_PER_ENGINE = 256;
|
|
93949
94600
|
const commandsByEngine = /* @__PURE__ */ new WeakMap();
|
|
@@ -97628,6 +98279,26 @@ function serializeNativeToolActivity(activity) {
|
|
|
97628
98279
|
}
|
|
97629
98280
|
};
|
|
97630
98281
|
}
|
|
98282
|
+
function serializeWorktreeLaunchActivity(activity) {
|
|
98283
|
+
if (decodeWorktreeLaunchActivityKind(activity.kind) === null) return null;
|
|
98284
|
+
const summary = text(activity.summary, 4e3);
|
|
98285
|
+
if (summary === null) return null;
|
|
98286
|
+
return {
|
|
98287
|
+
sourceSequence: null,
|
|
98288
|
+
event: {
|
|
98289
|
+
eventId: `activity:${activity.id}`,
|
|
98290
|
+
occurredAt: canonicalRuntimeEventTimestamp(activity.createdAt),
|
|
98291
|
+
...eventTurn(activity.turnId),
|
|
98292
|
+
kind: "tool_call",
|
|
98293
|
+
summary,
|
|
98294
|
+
status: "completed",
|
|
98295
|
+
payload: encodeCmmdRuntimeWorktreeLaunchActivityPayload(activity.kind, activity.payload)
|
|
98296
|
+
}
|
|
98297
|
+
};
|
|
98298
|
+
}
|
|
98299
|
+
function decodeWorktreeLaunchActivityKind(kind) {
|
|
98300
|
+
return isCmmdRuntimeWorktreeLaunchActivityKind(kind) ? kind : null;
|
|
98301
|
+
}
|
|
97631
98302
|
function isReplicatedFailureActivity(activity) {
|
|
97632
98303
|
return activity.kind === "runtime.error" || activity.kind.startsWith("provider.") && activity.kind.endsWith(".failed");
|
|
97633
98304
|
}
|
|
@@ -98008,12 +98679,17 @@ function serializeRuntimeThreadEvents(thread) {
|
|
|
98008
98679
|
const event = serializeNativeToolActivity(activity);
|
|
98009
98680
|
return event === null ? [] : [event];
|
|
98010
98681
|
});
|
|
98682
|
+
const worktreeLaunchActivities = thread.activities.flatMap((activity) => {
|
|
98683
|
+
const event = serializeWorktreeLaunchActivity(activity);
|
|
98684
|
+
return event === null ? [] : [event];
|
|
98685
|
+
});
|
|
98011
98686
|
const recordedTurns = serializeRecordedTurns(thread);
|
|
98012
98687
|
const textGenerations = serializeTextGenerations(thread);
|
|
98013
98688
|
return [
|
|
98014
98689
|
...messages,
|
|
98015
98690
|
...replicaActivities,
|
|
98016
98691
|
...toolActivities,
|
|
98692
|
+
...worktreeLaunchActivities,
|
|
98017
98693
|
...failureActivities,
|
|
98018
98694
|
...recordedTurns,
|
|
98019
98695
|
...textGenerations
|
|
@@ -102295,7 +102971,7 @@ function claudeReadinessReasonCode(reason) {
|
|
|
102295
102971
|
//#endregion
|
|
102296
102972
|
//#region src/egress/providerDeliveryReceipt.ts
|
|
102297
102973
|
const PROVIDER_ACCOUNT_IDENTITY$2 = /^pai_v1_[A-Za-z0-9_-]{43}$/;
|
|
102298
|
-
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
102974
|
+
const UUID$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
102299
102975
|
const RFC3339$1 = /^(\d{4})-(\d{2})-(\d{2})T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
|
|
102300
102976
|
const RECEIPT_KEYS = new Set([
|
|
102301
102977
|
"receiptId",
|
|
@@ -102378,7 +103054,7 @@ function claudeAccountFields(value) {
|
|
|
102378
103054
|
}
|
|
102379
103055
|
function parseClaudeDeliveryIdentity(value) {
|
|
102380
103056
|
const account = claudeAccountFields(value.providerAccountIdentity);
|
|
102381
|
-
if (!account || !hasAllowedKeys(value, CLAUDE_IDENTITY_KEYS, CLAUDE_IDENTITY_OPTIONAL_KEYS) || value.kind !== "credential_generation" || value.provider !== "claude" || typeof value.credentialGenerationId !== "string" || !UUID.test(value.credentialGenerationId)) return null;
|
|
103057
|
+
if (!account || !hasAllowedKeys(value, CLAUDE_IDENTITY_KEYS, CLAUDE_IDENTITY_OPTIONAL_KEYS) || value.kind !== "credential_generation" || value.provider !== "claude" || typeof value.credentialGenerationId !== "string" || !UUID$1.test(value.credentialGenerationId)) return null;
|
|
102382
103058
|
return {
|
|
102383
103059
|
kind: "credential_generation",
|
|
102384
103060
|
provider: "claude",
|
|
@@ -103145,7 +103821,10 @@ function extractClaudeProviderAccountId(response) {
|
|
|
103145
103821
|
if (typeof response !== "object" || response === null || !("data" in response)) return null;
|
|
103146
103822
|
const data = response.data;
|
|
103147
103823
|
if (typeof data !== "object" || data === null) return null;
|
|
103148
|
-
if ("connected" in data && data.connected === true)
|
|
103824
|
+
if ("connected" in data && data.connected === true) {
|
|
103825
|
+
if ("providerAccountId" in data) return asNonEmptyString$1(data.providerAccountId) ?? null;
|
|
103826
|
+
return extractClaudeCredentialGenerationId(response);
|
|
103827
|
+
}
|
|
103149
103828
|
if (!("account" in data)) return null;
|
|
103150
103829
|
const account = data.account;
|
|
103151
103830
|
if (typeof account !== "object" || account === null || !("providerAccountId" in account) || typeof account.providerAccountId !== "string") return null;
|
|
@@ -106121,6 +106800,14 @@ function makeRelayCommandInboxRepository(sql) {
|
|
|
106121
106800
|
function isThreadDetailEvent(event) {
|
|
106122
106801
|
return event.type === "thread.message-sent" || event.type === "thread.proposed-plan-upserted" || event.type === "thread.activity-appended" || event.type === "thread.turn-diff-completed" || event.type === "thread.reverted" || event.type === "thread.session-set" || event.type === "thread.rate-limit-set";
|
|
106123
106802
|
}
|
|
106803
|
+
/**
|
|
106804
|
+
* Narrows the domain-event feed to one thread's stream-visible events. The same
|
|
106805
|
+
* predicate guards the live feed and the resume replay, so a subscriber cannot
|
|
106806
|
+
* be handed a different set of events depending on which path served it.
|
|
106807
|
+
*/
|
|
106808
|
+
function isThreadDetailEventFor(threadId) {
|
|
106809
|
+
return (event) => event.aggregateKind === "thread" && event.aggregateId === threadId && isThreadDetailEvent(event);
|
|
106810
|
+
}
|
|
106124
106811
|
const PROVIDER_STATUS_DEBOUNCE_MS = 200;
|
|
106125
106812
|
function streamBoundedText(contents) {
|
|
106126
106813
|
if (Buffer.byteLength(contents, "utf8") > 16777216) return require_Schema$1.fail(new BulkTransferError({
|
|
@@ -106166,6 +106853,39 @@ function loadInitialShellSnapshot(input) {
|
|
|
106166
106853
|
return yield* input.projectionSnapshotQuery.getShellSnapshot();
|
|
106167
106854
|
});
|
|
106168
106855
|
}
|
|
106856
|
+
/**
|
|
106857
|
+
* The thread snapshot a subscription falls back to, windowed when the
|
|
106858
|
+
* subscriber asked for a window. A missing thread is a failure rather than an
|
|
106859
|
+
* empty snapshot: the subscriber asked about a thread this server does not
|
|
106860
|
+
* have, and saying so is the only honest answer.
|
|
106861
|
+
*
|
|
106862
|
+
* Also the read behind `getThreadDetailPage`, the socket RPC a relay-only
|
|
106863
|
+
* connection uses for "load earlier turns" in place of the HTTP snapshot
|
|
106864
|
+
* route: same window, same pure `windowThreadDetail`, so the two reads cannot
|
|
106865
|
+
* disagree about what a page is.
|
|
106866
|
+
*/
|
|
106867
|
+
function loadThreadDetailSnapshot(input) {
|
|
106868
|
+
return require_Schema$1.gen(function* () {
|
|
106869
|
+
const [threadDetail, snapshotSequence] = yield* require_Schema$1.all([input.projectionSnapshotQuery.getThreadDetailById(input.threadId).pipe(require_Schema$1.mapError((cause) => new OrchestrationGetSnapshotError({
|
|
106870
|
+
message: `Failed to load thread ${input.threadId}`,
|
|
106871
|
+
cause
|
|
106872
|
+
}))), input.orchestrationEngine.getReadModel().pipe(require_Schema$1.map((readModel) => readModel.snapshotSequence))]);
|
|
106873
|
+
if (require_Schema$1.isNone(threadDetail)) return yield* new OrchestrationGetSnapshotError({
|
|
106874
|
+
message: `Thread ${input.threadId} was not found`,
|
|
106875
|
+
cause: input.threadId
|
|
106876
|
+
});
|
|
106877
|
+
const windowed = windowThreadDetail({
|
|
106878
|
+
thread: threadDetail.value,
|
|
106879
|
+
window: input.window,
|
|
106880
|
+
snapshotSequence
|
|
106881
|
+
});
|
|
106882
|
+
return {
|
|
106883
|
+
snapshotSequence,
|
|
106884
|
+
thread: windowed.thread,
|
|
106885
|
+
...windowed.page === void 0 ? {} : { page: windowed.page }
|
|
106886
|
+
};
|
|
106887
|
+
});
|
|
106888
|
+
}
|
|
106169
106889
|
function makeSnapshotThenLiveStream(input) {
|
|
106170
106890
|
return require_Path.unwrap(require_Schema$1.gen(function* () {
|
|
106171
106891
|
const liveEvents = yield* input.subscribeLiveEvents;
|
|
@@ -106413,6 +107133,9 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(r
|
|
|
106413
107133
|
shellRevealInFileManager: true,
|
|
106414
107134
|
shellRevealInFileManagerKind: fileManagerRevealKind
|
|
106415
107135
|
},
|
|
107136
|
+
threadResumeCompletionMarker: true,
|
|
107137
|
+
threadSnapshotPagination: true,
|
|
107138
|
+
threadDetailPageRpc: true,
|
|
106416
107139
|
observability: {
|
|
106417
107140
|
logsDirectoryPath: config.logsDir,
|
|
106418
107141
|
localTracingEnabled: true,
|
|
@@ -106510,26 +107233,28 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(r
|
|
|
106510
107233
|
projectionSnapshotQuery
|
|
106511
107234
|
})), require_Path.flatMap((event) => require_Schema$1.isSome(event) ? require_Path.succeed(event.value) : require_Path.empty))))
|
|
106512
107235
|
})), { "rpc.aggregate": "orchestration" }),
|
|
106513
|
-
[ORCHESTRATION_WS_METHODS.subscribeThread]: (input) => observeRpcStreamEffect(ORCHESTRATION_WS_METHODS.subscribeThread, require_Schema$1.
|
|
106514
|
-
|
|
106515
|
-
|
|
107236
|
+
[ORCHESTRATION_WS_METHODS.subscribeThread]: (input) => observeRpcStreamEffect(ORCHESTRATION_WS_METHODS.subscribeThread, require_Schema$1.succeed(makeThreadSubscriptionStream(input, {
|
|
107237
|
+
subscribeLiveEvents: orchestrationEngine.subscribeDomainEvents().pipe(require_Schema$1.map((events) => events.pipe(require_Path.filter(isThreadDetailEventFor(input.threadId))))),
|
|
107238
|
+
currentSequence: orchestrationEngine.getReadModel().pipe(require_Schema$1.map((readModel) => readModel.snapshotSequence)),
|
|
107239
|
+
loadSnapshot: loadThreadDetailSnapshot({
|
|
107240
|
+
threadId: input.threadId,
|
|
107241
|
+
window: input.turnLimit === void 0 ? {} : { turnLimit: input.turnLimit },
|
|
107242
|
+
projectionSnapshotQuery,
|
|
107243
|
+
orchestrationEngine
|
|
107244
|
+
}),
|
|
107245
|
+
replayEvents: (afterSequence) => orchestrationEngine.readEvents(afterSequence).pipe(require_Path.filter(isThreadDetailEventFor(input.threadId)), require_Path.mapError((cause) => new OrchestrationGetSnapshotError({
|
|
107246
|
+
message: `Failed to replay thread ${input.threadId}`,
|
|
106516
107247
|
cause
|
|
106517
|
-
})))
|
|
106518
|
-
|
|
106519
|
-
|
|
106520
|
-
|
|
106521
|
-
|
|
106522
|
-
|
|
106523
|
-
|
|
106524
|
-
|
|
106525
|
-
|
|
106526
|
-
|
|
106527
|
-
kind: "snapshot",
|
|
106528
|
-
snapshot: {
|
|
106529
|
-
snapshotSequence,
|
|
106530
|
-
thread: threadDetail.value
|
|
106531
|
-
}
|
|
106532
|
-
}), liveStream);
|
|
107248
|
+
})))
|
|
107249
|
+
})), { "rpc.aggregate": "orchestration" }),
|
|
107250
|
+
[ORCHESTRATION_WS_METHODS.getThreadDetailPage]: (input) => observeRpcEffect(ORCHESTRATION_WS_METHODS.getThreadDetailPage, loadThreadDetailSnapshot({
|
|
107251
|
+
threadId: input.threadId,
|
|
107252
|
+
window: {
|
|
107253
|
+
...input.turnLimit === void 0 ? {} : { turnLimit: input.turnLimit },
|
|
107254
|
+
...input.beforeCursor === void 0 ? {} : { beforeCursor: input.beforeCursor }
|
|
107255
|
+
},
|
|
107256
|
+
projectionSnapshotQuery,
|
|
107257
|
+
orchestrationEngine
|
|
106533
107258
|
}), { "rpc.aggregate": "orchestration" }),
|
|
106534
107259
|
[WS_METHODS.serverGetConfig]: (_input) => observeRpcEffect(WS_METHODS.serverGetConfig, loadServerConfig, { "rpc.aggregate": "server" }),
|
|
106535
107260
|
[WS_METHODS.serverRefreshProviders]: (_input) => observeRpcEffect(WS_METHODS.serverRefreshProviders, providerRegistry.refresh().pipe(require_Schema$1.map((providers) => ({ providers }))), { "rpc.aggregate": "server" }),
|
|
@@ -118006,8 +118731,8 @@ require_Schema$1.effectDiscard(runMigrations());
|
|
|
118006
118731
|
//#endregion
|
|
118007
118732
|
//#region src/persistence/Layers/Sqlite.ts
|
|
118008
118733
|
const defaultSqliteClientLoaders = {
|
|
118009
|
-
bun: () => Promise.resolve().then(() => require("./SqliteClient-
|
|
118010
|
-
node: () => Promise.resolve().then(() => require("./NodeSqliteClient-
|
|
118734
|
+
bun: () => Promise.resolve().then(() => require("./SqliteClient-O1ABIngw.cjs")),
|
|
118735
|
+
node: () => Promise.resolve().then(() => require("./NodeSqliteClient-CfsdC3AB.cjs"))
|
|
118011
118736
|
};
|
|
118012
118737
|
const makeRuntimeSqliteLayer = require_Schema$1.fn("makeRuntimeSqliteLayer")(function* (config) {
|
|
118013
118738
|
const loader = defaultSqliteClientLoaders[process.versions.bun !== void 0 ? "bun" : "node"];
|
|
@@ -178324,7 +179049,7 @@ function em(e) {
|
|
|
178324
179049
|
BO ??= new Int32Array(new SharedArrayBuffer(4)), Atomics.wait(BO, 0, 0, SY);
|
|
178325
179050
|
}
|
|
178326
179051
|
}
|
|
178327
|
-
function rm$
|
|
179052
|
+
function rm$7(e, t) {
|
|
178328
179053
|
let n = (0, path.resolve)(t).toLowerCase(), r = (0, path.dirname)((0, path.resolve)(e)).toLowerCase();
|
|
178329
179054
|
if (r === n || KO(r, n) || qO(r, n, "lexical")) return !0;
|
|
178330
179055
|
let o = em(t)?.toLowerCase();
|
|
@@ -191174,7 +191899,7 @@ function Wo() {
|
|
|
191174
191899
|
}
|
|
191175
191900
|
function sle(e) {
|
|
191176
191901
|
let t = process.cwd();
|
|
191177
|
-
return e.filter((n) => !rm$
|
|
191902
|
+
return e.filter((n) => !rm$7(n, t));
|
|
191178
191903
|
}
|
|
191179
191904
|
function ZU(e) {
|
|
191180
191905
|
let t = Bun.which(e);
|
|
@@ -212600,6 +213325,8 @@ const PUBLIC_PROVIDER_LEAK_PATTERNS = [
|
|
|
212600
213325
|
/\bCURSOR_API_KEY\b/,
|
|
212601
213326
|
/\bOPENCODE_API_KEY\b/,
|
|
212602
213327
|
/\bforge_proxy_[A-Za-z0-9._-]+\b/,
|
|
213328
|
+
/\bProviderValidationError\b/,
|
|
213329
|
+
/\/forge-server\/node_modules\//,
|
|
212603
213330
|
/https?:\/\/[^\s'"<>)]*\/api\/provider-egress\/[^\s'"<>)]*/i,
|
|
212604
213331
|
/https?:\/\/[^\s'"<>)]*(?:apex|deepseek|moonshot|kimi)[^\s'"<>)]*/i
|
|
212605
213332
|
];
|
|
@@ -212609,6 +213336,12 @@ function isProviderRateOrQuotaError(message) {
|
|
|
212609
213336
|
const normalized = message.toLowerCase();
|
|
212610
213337
|
return /\b(rate[\s-]?limit|too\s+many\s+requests|usage[ _-]limit|usage_limit_reached|quota|insufficient_quota)\b/.test(normalized) || /\b429\b/.test(normalized);
|
|
212611
213338
|
}
|
|
213339
|
+
function providerLifecycleOrAccountCopy(message) {
|
|
213340
|
+
if (/\bbound to provider account\b/.test(message) && /\bcannot use provider account\b/.test(message)) return "Forge could not switch this conversation to the selected AI account. Retry the account switch.";
|
|
213341
|
+
if (/\bforge_runtime_repairing\b/.test(message) || /\bthis turn was interrupted because the environment is being repaired\b/.test(message)) return "This turn was interrupted because the environment is being repaired. Retry after the repair finishes.";
|
|
213342
|
+
if (/\bforge_runtime_stopped\b/.test(message) || /\bthis turn was interrupted because the environment was stopped\b/.test(message)) return "This turn was interrupted because the environment was stopped. Start the environment and retry.";
|
|
213343
|
+
return null;
|
|
213344
|
+
}
|
|
212612
213345
|
/**
|
|
212613
213346
|
* Infer a SAFE, clearer failure category from a (possibly leaky) provider error.
|
|
212614
213347
|
*
|
|
@@ -212621,7 +213354,8 @@ function isProviderRateOrQuotaError(message) {
|
|
|
212621
213354
|
function categorizePublicProviderError(message) {
|
|
212622
213355
|
if (!message) return null;
|
|
212623
213356
|
const m = message.toLowerCase();
|
|
212624
|
-
|
|
213357
|
+
const lifecycleOrAccountCopy = providerLifecycleOrAccountCopy(m);
|
|
213358
|
+
if (lifecycleOrAccountCopy !== null) return lifecycleOrAccountCopy;
|
|
212625
213359
|
if (/\bforge_provider_model_not_allowed\b/.test(m) || /\bnot\s+scoped\s+to\s+the\s+requested\s+model\b/.test(m)) return "The workspace AI credential does not cover the model this turn asked for. Retry, or pick that model for the thread.";
|
|
212626
213360
|
if (/\bmodel\b[\s\S]*\b(not\s+(found|available|supported)|does\s+not\s+exist|unsupported|invalid|unknown)\b/.test(m) || /\b(unsupported|unknown|invalid)\b[\s\S]*\bmodel\b/.test(m) || /\b(model_not_found|invalid_model|model_not_supported)\b/.test(m)) return "The selected model isn't available right now. Pick a different model and retry.";
|
|
212627
213361
|
if (isProviderRateOrQuotaError(m)) return "The AI service is rate-limited or out of quota. Wait a moment and retry.";
|
|
@@ -212687,60 +213421,14 @@ function hasConfiguredApexCredential(profiles) {
|
|
|
212687
213421
|
}
|
|
212688
213422
|
//#endregion
|
|
212689
213423
|
//#region src/provider/claudeCredentialDirectory.ts
|
|
212690
|
-
/**
|
|
212691
|
-
* Per-account credential directories for Claude turns.
|
|
212692
|
-
*
|
|
212693
|
-
* Claude Code keeps conversation transcripts under
|
|
212694
|
-
* `${CLAUDE_CONFIG_DIR}/projects/<cwd-slug>/<sessionId>.jsonl` and its login
|
|
212695
|
-
* under `.credentials.json`. `CLAUDE_SECURESTORAGE_CONFIG_DIR` moves only the
|
|
212696
|
-
* credential file, so each Provider Account can hold its own login while every
|
|
212697
|
-
* session file stays in one place and `--resume` keeps finding its target. See
|
|
212698
|
-
* `docs/adr/claude-accounts-split-credentials-not-sessions.md`.
|
|
212699
|
-
*
|
|
212700
|
-
* That variable is not documented. Under
|
|
212701
|
-
* `docs/adr/unpublished-provider-surface-needs-a-fallback.md` the dependency is
|
|
212702
|
-
* only allowed because absence degrades to a named behaviour that already
|
|
212703
|
-
* works: a thread with no account runs on the Environment's own login. This
|
|
212704
|
-
* module sets the variable only when the resolved directory holds a credential
|
|
212705
|
-
* the CLI can use, because a missing file, or one whose access token a refusal
|
|
212706
|
-
* has blanked, makes the CLI report logged out and a live turn would fail.
|
|
212707
|
-
*
|
|
212708
|
-
* Nothing writes these directories yet. #3049 adds the writer and keys each
|
|
212709
|
-
* directory on the account identity CMMD writes on the delivery receipt it
|
|
212710
|
-
* signs (`docs/adr/provider-account-identity-is-issued-by-cmmd.md`). Until then
|
|
212711
|
-
* every runtime takes the `no-account` skip.
|
|
212712
|
-
*
|
|
212713
|
-
* Free of Effect services on purpose, for the same reason as
|
|
212714
|
-
* `claudeCredentialContainment.ts`: ClaudeAdapter drags in Effect platform
|
|
212715
|
-
* services, the browser manager and the server config, so a unit test that
|
|
212716
|
-
* imports it cannot run in a worktree. This rule decides which account pays for
|
|
212717
|
-
* a turn, so it has to stay directly testable.
|
|
212718
|
-
*/
|
|
213424
|
+
/** Per-account credentials share the existing Claude transcript directory. */
|
|
212719
213425
|
/** Repoints `.credentials.json` without moving the transcripts. */
|
|
212720
213426
|
const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
|
|
212721
213427
|
/** The one file Claude Code writes into the credential directory. */
|
|
212722
213428
|
const CLAUDE_CREDENTIAL_FILE_NAME = ".credentials.json";
|
|
212723
|
-
/**
|
|
212724
|
-
* Runtime env var naming the Provider Account this runtime's Claude turns
|
|
212725
|
-
* belong to. Nothing in Forge or CMMD sets it, so no runtime names an account
|
|
212726
|
-
* and each keeps its one login.
|
|
212727
|
-
*
|
|
212728
|
-
* It is a bridge that will not get a writer. #3073 settled the key in
|
|
212729
|
-
* `docs/adr/provider-account-identity-is-issued-by-cmmd.md`: the directory name
|
|
212730
|
-
* comes from the identity CMMD issues and writes on the delivery receipt it
|
|
212731
|
-
* signs, and this variable goes away. #3049 makes that change. The credential
|
|
212732
|
-
* generation was ruled out as a key: it rotates on every reconnect, and CMMD
|
|
212733
|
-
* uses it as the primary account's id, so keying on it would strand every
|
|
212734
|
-
* thread pinned to that account on a directory that stops receiving deliveries.
|
|
212735
|
-
*
|
|
212736
|
-
* Tests reach the `credentialDirectory` parameter on
|
|
212737
|
-
* `ClaudeAdapter.buildClaudeQueryEnv` directly.
|
|
212738
|
-
*/
|
|
212739
|
-
const CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV = "FORGE_CLAUDE_PROVIDER_ACCOUNT_IDENTITY";
|
|
212740
213429
|
/** Directory under the server's base directory that holds one directory per account. */
|
|
212741
213430
|
const CREDENTIAL_ROOT_DIRECTORY_NAME = "claude-credentials";
|
|
212742
213431
|
const DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude";
|
|
212743
|
-
const MAX_ACCOUNT_IDENTITY_LENGTH = 256;
|
|
212744
213432
|
const DIRECTORY_NAME_HASH_LENGTH = 32;
|
|
212745
213433
|
/**
|
|
212746
213434
|
* Directory name for an account. A hash, so no email or display name reaches a
|
|
@@ -212749,11 +213437,12 @@ const DIRECTORY_NAME_HASH_LENGTH = 32;
|
|
|
212749
213437
|
function claudeCredentialDirectoryName(accountIdentity) {
|
|
212750
213438
|
return `account-${(0, node_crypto.createHash)("sha256").update(accountIdentity, "utf8").digest("hex").slice(0, DIRECTORY_NAME_HASH_LENGTH)}`;
|
|
212751
213439
|
}
|
|
212752
|
-
/**
|
|
212753
|
-
function
|
|
212754
|
-
|
|
212755
|
-
|
|
212756
|
-
|
|
213440
|
+
/** Only a matching CMMD delivery can select a credential directory. */
|
|
213441
|
+
function resolveSelectedClaudeAccountIdentity(selectedAccountId, binding) {
|
|
213442
|
+
if (selectedAccountId === void 0) return null;
|
|
213443
|
+
const identity = binding?.identity;
|
|
213444
|
+
if (identity?.kind !== "credential_generation" || identity.provider !== "claude" || identity.providerAccountIdentity !== selectedAccountId) throw new Error("The selected Claude account has no matching credential delivery. Reconnect that account and try again.");
|
|
213445
|
+
return identity.providerAccountIdentity;
|
|
212757
213446
|
}
|
|
212758
213447
|
/**
|
|
212759
213448
|
* Where per-account credential directories live: `claude-credentials` under the
|
|
@@ -212780,29 +213469,19 @@ function resolveClaudeCredentialDirectory(input) {
|
|
|
212780
213469
|
if (!root) return null;
|
|
212781
213470
|
return (0, node_path.join)(root, claudeCredentialDirectoryName(input.accountIdentity));
|
|
212782
213471
|
}
|
|
212783
|
-
/**
|
|
212784
|
-
* Decide whether this turn may point at a per-account credential directory.
|
|
212785
|
-
*
|
|
212786
|
-
* The cheap checks run first and the capability probe runs last, so a runtime
|
|
212787
|
-
* with no usable credential never spawns the CLI. The probe's error channel is
|
|
212788
|
-
* empty, so nothing here catches.
|
|
212789
|
-
*
|
|
212790
|
-
* A skip is not a fallback onto the default account. With no account the turn
|
|
212791
|
-
* runs on the Environment's own login, unchanged. A thread pinned to an account
|
|
212792
|
-
* whose credential is missing is refused by name under
|
|
212793
|
-
* `docs/adr/provider-account-identity-is-issued-by-cmmd.md`, and #3049 turns
|
|
212794
|
-
* `no-usable-credential`, and `capability-not-supported` for a pinned account,
|
|
212795
|
-
* into those refusals. Until #3049 nothing names an account, so `no-account` is
|
|
212796
|
-
* the only skip a runtime sees.
|
|
212797
|
-
*/
|
|
213472
|
+
/** Refuse selected accounts that cannot use their own credential directory. */
|
|
212798
213473
|
function decideClaudeCredentialDirectory(input) {
|
|
212799
213474
|
return require_Schema$1.gen(function* () {
|
|
212800
213475
|
if (!input.accountIdentity) return skip("no-account");
|
|
212801
|
-
if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return
|
|
213476
|
+
if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return refuse("operator-override");
|
|
212802
213477
|
const directory = resolveClaudeCredentialDirectory(input);
|
|
212803
|
-
if (!directory) return
|
|
212804
|
-
if (!holdsUsableCredential(directory)) return
|
|
212805
|
-
|
|
213478
|
+
if (!directory) return refuse("no-root");
|
|
213479
|
+
if (!holdsUsableCredential(directory)) return refuse("no-usable-credential");
|
|
213480
|
+
if ((yield* input.probeSupport) === "supported") return set(directory);
|
|
213481
|
+
return input.primaryCredentialMatchesSelected === true ? {
|
|
213482
|
+
outcome: "skipped",
|
|
213483
|
+
reason: "same-primary-account"
|
|
213484
|
+
} : refuse("capability-not-supported");
|
|
212806
213485
|
});
|
|
212807
213486
|
}
|
|
212808
213487
|
/**
|
|
@@ -212841,6 +213520,12 @@ function skip(reason) {
|
|
|
212841
213520
|
reason
|
|
212842
213521
|
};
|
|
212843
213522
|
}
|
|
213523
|
+
function refuse(reason) {
|
|
213524
|
+
return {
|
|
213525
|
+
outcome: "refused",
|
|
213526
|
+
reason
|
|
213527
|
+
};
|
|
213528
|
+
}
|
|
212844
213529
|
function claudeConfigDirectory(env) {
|
|
212845
213530
|
const configured = env.CLAUDE_CONFIG_DIR?.trim();
|
|
212846
213531
|
if (configured) return configured;
|
|
@@ -212850,6 +213535,24 @@ function claudeConfigDirectory(env) {
|
|
|
212850
213535
|
return null;
|
|
212851
213536
|
}
|
|
212852
213537
|
}
|
|
213538
|
+
/**
|
|
213539
|
+
* The fallback is allowed only when the primary on-disk credential is
|
|
213540
|
+
* byte-identical to what CMMD prepared for the selected account. A ticket's
|
|
213541
|
+
* `credentialRevisionHash` cannot express that: it hashes the encrypted row
|
|
213542
|
+
* CMMD stores, not the decrypted bytes this process ever sees. Comparing
|
|
213543
|
+
* files directly is the only check that means what the fallback needs.
|
|
213544
|
+
*/
|
|
213545
|
+
function primaryClaudeCredentialMatchesPrepared(env, preparedDirectory) {
|
|
213546
|
+
const directory = claudeConfigDirectory(env);
|
|
213547
|
+
if (!directory) return false;
|
|
213548
|
+
try {
|
|
213549
|
+
const primary = (0, node_fs.readFileSync)((0, node_path.join)(directory, CLAUDE_CREDENTIAL_FILE_NAME), "utf8");
|
|
213550
|
+
const prepared = (0, node_fs.readFileSync)((0, node_path.join)(preparedDirectory, CLAUDE_CREDENTIAL_FILE_NAME), "utf8");
|
|
213551
|
+
return hasAccessToken(JSON.parse(primary)) && (0, node_crypto.createHash)("sha256").update(primary).digest("hex") === (0, node_crypto.createHash)("sha256").update(prepared).digest("hex");
|
|
213552
|
+
} catch {
|
|
213553
|
+
return false;
|
|
213554
|
+
}
|
|
213555
|
+
}
|
|
212853
213556
|
//#endregion
|
|
212854
213557
|
//#region src/provider/personalClaudeInVm.ts
|
|
212855
213558
|
/**
|
|
@@ -212982,13 +213685,106 @@ function withoutInheritedAnthropicCredentials(env, options) {
|
|
|
212982
213685
|
* marker CMMD writes on disk (#3081), so a Sprite switched on in place, which
|
|
212983
213686
|
* has the marker and no env var until it restarts, drops the override too.
|
|
212984
213687
|
*/
|
|
212985
|
-
function claudeSubscriptionChildEnv(processEnv, credentialDirectory) {
|
|
213688
|
+
function claudeSubscriptionChildEnv(processEnv, credentialDirectory, selectedSubscription = false) {
|
|
212986
213689
|
return {
|
|
212987
|
-
...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: personalClaudeInVmEnabled(processEnv) }),
|
|
213690
|
+
...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: selectedSubscription || credentialDirectory !== null || personalClaudeInVmEnabled(processEnv) }),
|
|
212988
213691
|
...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
|
|
212989
213692
|
};
|
|
212990
213693
|
}
|
|
212991
213694
|
//#endregion
|
|
213695
|
+
//#region src/provider/claudeSelectedCredentialStore.ts
|
|
213696
|
+
async function ensurePrivateDirectory(path) {
|
|
213697
|
+
await (0, node_fs_promises.mkdir)(path, {
|
|
213698
|
+
recursive: true,
|
|
213699
|
+
mode: 448
|
|
213700
|
+
});
|
|
213701
|
+
const info = await (0, node_fs_promises.lstat)(path);
|
|
213702
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Unsafe credential directory.");
|
|
213703
|
+
await (0, node_fs_promises.chmod)(path, 448);
|
|
213704
|
+
}
|
|
213705
|
+
async function makeCapacity(root, target, active, maximum) {
|
|
213706
|
+
const entries = (await (0, node_fs_promises.readdir)(root, { withFileTypes: true })).filter((entry) => entry.isDirectory() && /^account-[a-f0-9]{32}$/.test(entry.name));
|
|
213707
|
+
if (entries.some((entry) => (0, node_path.join)(root, entry.name) === target)) return;
|
|
213708
|
+
const candidates = await Promise.all(entries.filter((entry) => !active.has((0, node_path.join)(root, entry.name))).map(async (entry) => ({
|
|
213709
|
+
path: (0, node_path.join)(root, entry.name),
|
|
213710
|
+
modified: (await (0, node_fs_promises.lstat)((0, node_path.join)(root, entry.name))).mtimeMs
|
|
213711
|
+
})));
|
|
213712
|
+
candidates.sort((a, b) => a.modified - b.modified);
|
|
213713
|
+
const required = entries.length - maximum + 1;
|
|
213714
|
+
if (required > candidates.length) throw new Error("All Claude credential slots are active. Stop a session before choosing another account.");
|
|
213715
|
+
for (const candidate of candidates.slice(0, Math.max(0, required))) await (0, node_fs_promises.rm)(candidate.path, { recursive: true });
|
|
213716
|
+
}
|
|
213717
|
+
function createClaudeSelectedCredentialStore(baseDir, options = {}) {
|
|
213718
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
213719
|
+
const maximum = options.maxAccounts ?? 8;
|
|
213720
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1) throw new Error("Invalid credential capacity.");
|
|
213721
|
+
let queue = Promise.resolve();
|
|
213722
|
+
async function install(threadId, acquired) {
|
|
213723
|
+
if (acquired.claims.threadId !== threadId) throw new Error("Credential thread mismatch.");
|
|
213724
|
+
const directory = resolveClaudeCredentialDirectory({
|
|
213725
|
+
env: process.env,
|
|
213726
|
+
baseDir,
|
|
213727
|
+
accountIdentity: acquired.claims.providerAccountId
|
|
213728
|
+
});
|
|
213729
|
+
if (!directory) throw new Error("Unsafe credential directory.");
|
|
213730
|
+
const binding = {
|
|
213731
|
+
runtimeId: acquired.claims.runtimeId,
|
|
213732
|
+
machineId: acquired.claims.machineId,
|
|
213733
|
+
credentialEpoch: acquired.claims.credentialEpoch,
|
|
213734
|
+
identity: {
|
|
213735
|
+
kind: "credential_generation",
|
|
213736
|
+
provider: "claude",
|
|
213737
|
+
credentialGenerationId: acquired.claims.generationId,
|
|
213738
|
+
providerAccountIdentity: acquired.claims.providerAccountId
|
|
213739
|
+
}
|
|
213740
|
+
};
|
|
213741
|
+
for (const prepared of sessions.values()) if (prepared.directory === directory && (!providerDeliveryBindingsEqual(prepared.binding, binding) || prepared.credentialRevisionHash !== acquired.claims.credentialRevisionHash)) throw new Error("The Claude account has an active session. Stop it before replacing its credentials.");
|
|
213742
|
+
const root = (0, node_path.dirname)(directory);
|
|
213743
|
+
await ensurePrivateDirectory(root);
|
|
213744
|
+
await makeCapacity(root, directory, new Set([...sessions.values()].map((entry) => entry.directory)), maximum);
|
|
213745
|
+
await ensurePrivateDirectory(directory);
|
|
213746
|
+
const temporary = (0, node_path.join)(directory, `.credential-${(0, node_crypto.randomUUID)()}.tmp`);
|
|
213747
|
+
try {
|
|
213748
|
+
await (0, node_fs_promises.writeFile)(temporary, acquired.credentialJson, {
|
|
213749
|
+
mode: 384,
|
|
213750
|
+
flag: "wx"
|
|
213751
|
+
});
|
|
213752
|
+
await (0, node_fs_promises.rename)(temporary, (0, node_path.join)(directory, ".credentials.json"));
|
|
213753
|
+
} finally {
|
|
213754
|
+
await (0, node_fs_promises.rm)(temporary, { force: true });
|
|
213755
|
+
}
|
|
213756
|
+
sessions.set(threadId, {
|
|
213757
|
+
directory,
|
|
213758
|
+
binding,
|
|
213759
|
+
accountKind: acquired.claims.accountKind,
|
|
213760
|
+
credentialRevisionHash: acquired.claims.credentialRevisionHash
|
|
213761
|
+
});
|
|
213762
|
+
return binding;
|
|
213763
|
+
}
|
|
213764
|
+
return {
|
|
213765
|
+
prepare(threadId, acquired) {
|
|
213766
|
+
const operation = queue.then(() => install(threadId, acquired));
|
|
213767
|
+
queue = operation.then(() => void 0, () => void 0);
|
|
213768
|
+
return operation;
|
|
213769
|
+
},
|
|
213770
|
+
read(threadId, binding) {
|
|
213771
|
+
const prepared = sessions.get(threadId);
|
|
213772
|
+
return prepared && providerDeliveryBindingsEqual(prepared.binding, binding) ? prepared : null;
|
|
213773
|
+
},
|
|
213774
|
+
release(threadId) {
|
|
213775
|
+
sessions.delete(threadId);
|
|
213776
|
+
}
|
|
213777
|
+
};
|
|
213778
|
+
}
|
|
213779
|
+
const stores = /* @__PURE__ */ new Map();
|
|
213780
|
+
function selectedClaudeCredentialStore(baseDir) {
|
|
213781
|
+
const existing = stores.get(baseDir);
|
|
213782
|
+
if (existing) return existing;
|
|
213783
|
+
const store = createClaudeSelectedCredentialStore(baseDir);
|
|
213784
|
+
stores.set(baseDir, store);
|
|
213785
|
+
return store;
|
|
213786
|
+
}
|
|
213787
|
+
//#endregion
|
|
212992
213788
|
//#region src/provider/claudeCredentialDirectoryProbe.ts
|
|
212993
213789
|
/**
|
|
212994
213790
|
* Capability probe for `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
|
|
@@ -215033,7 +215829,8 @@ function claudeForgeAttributionPrompt() {
|
|
|
215033
215829
|
* turn, an API key is, so pointing the variable there would add a second source
|
|
215034
215830
|
* of truth that a later reader has to rule out.
|
|
215035
215831
|
*/
|
|
215036
|
-
function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory) {
|
|
215832
|
+
function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory, selectedSubscription = false) {
|
|
215833
|
+
if (selectedSubscription) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory, true);
|
|
215037
215834
|
const baseEnv = scrubForgeProviderChildEnvSecrets(baseProcessEnv);
|
|
215038
215835
|
const effectiveBaseUrl = resolveClaudeQueryBaseUrl(endpointProfile, claudeSettings, providerProxyCredential);
|
|
215039
215836
|
if (!effectiveBaseUrl) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory);
|
|
@@ -216947,6 +217744,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
|
|
|
216947
217744
|
if (streamFiber && streamFiber.pollUnsafe() === void 0) yield* require_Path.interrupt(streamFiber);
|
|
216948
217745
|
try {
|
|
216949
217746
|
context.query.close();
|
|
217747
|
+
selectedClaudeCredentialStore(serverConfig.baseDir).release(context.session.threadId);
|
|
216950
217748
|
} catch (cause) {
|
|
216951
217749
|
yield* emitRuntimeError(context, "Failed to close Claude runtime query.", cause);
|
|
216952
217750
|
}
|
|
@@ -217258,6 +218056,43 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
|
|
|
217258
218056
|
sessionId: existingResumeSessionId ?? newSessionId
|
|
217259
218057
|
};
|
|
217260
218058
|
});
|
|
218059
|
+
const resolveSelectedCredentialDirectory = require_Schema$1.fn("resolveSelectedCredentialDirectory")(function* (input, resumeState, claudeBinaryPath) {
|
|
218060
|
+
const requestedAccountId = input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.providerAccountId : void 0;
|
|
218061
|
+
const selectedAccountIdentity = yield* require_Schema$1.try_({
|
|
218062
|
+
try: () => resolveSelectedClaudeAccountIdentity(requestedAccountId, resumeState?.providerDeliveryBinding),
|
|
218063
|
+
catch: () => new ProviderAdapterValidationError({
|
|
218064
|
+
provider: PROVIDER$11,
|
|
218065
|
+
operation: "startSession",
|
|
218066
|
+
issue: `The selected Claude account ${requestedAccountId} has no matching credential delivery. Reconnect that account and try again.`
|
|
218067
|
+
})
|
|
218068
|
+
});
|
|
218069
|
+
const preparedCredential = resumeState?.providerDeliveryBinding ? selectedClaudeCredentialStore(serverConfig.baseDir).read(input.threadId, resumeState.providerDeliveryBinding) : null;
|
|
218070
|
+
if (selectedAccountIdentity !== null && preparedCredential === null) return yield* new ProviderAdapterValidationError({
|
|
218071
|
+
provider: PROVIDER$11,
|
|
218072
|
+
operation: "startSession",
|
|
218073
|
+
issue: `The selected Claude account ${selectedAccountIdentity} has no prepared credential. Try again to reconnect it.`
|
|
218074
|
+
});
|
|
218075
|
+
const credentialDirectoryDecision = yield* decideClaudeCredentialDirectory({
|
|
218076
|
+
env: process.env,
|
|
218077
|
+
baseDir: serverConfig.baseDir,
|
|
218078
|
+
accountIdentity: selectedAccountIdentity,
|
|
218079
|
+
probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support,
|
|
218080
|
+
primaryCredentialMatchesSelected: preparedCredential?.accountKind === "primary" && primaryClaudeCredentialMatchesPrepared(process.env, preparedCredential.directory)
|
|
218081
|
+
});
|
|
218082
|
+
if (credentialDirectoryDecision.outcome === "refused") return yield* new ProviderAdapterValidationError({
|
|
218083
|
+
provider: PROVIDER$11,
|
|
218084
|
+
operation: "startSession",
|
|
218085
|
+
issue: `The selected Claude account ${selectedAccountIdentity} cannot run on this environment: ${credentialDirectoryDecision.reason}. Reconnect the account or choose another environment.`
|
|
218086
|
+
});
|
|
218087
|
+
if (credentialDirectoryDecision.outcome === "skipped") {
|
|
218088
|
+
const skipLine = `Claude credential directory skipped: ${credentialDirectoryDecision.reason}`;
|
|
218089
|
+
yield* credentialDirectoryDecision.reason === "no-account" ? require_Schema$1.logDebug(skipLine) : require_Schema$1.logWarning(skipLine);
|
|
218090
|
+
}
|
|
218091
|
+
return {
|
|
218092
|
+
selectedAccountIdentity,
|
|
218093
|
+
claudeCredentialDirectory: credentialDirectoryDecision.outcome === "set" ? credentialDirectoryDecision.directory : null
|
|
218094
|
+
};
|
|
218095
|
+
});
|
|
217261
218096
|
const startSession = require_Schema$1.fn("startSession")(function* (input) {
|
|
217262
218097
|
const sessionProvider = yield* validateClaudeStartSessionProvider(input.provider);
|
|
217263
218098
|
const startedAt = yield* nowIso;
|
|
@@ -217318,19 +218153,14 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
|
|
|
217318
218153
|
})));
|
|
217319
218154
|
const claudeSettings = serverSettings.providers.claudeAgent;
|
|
217320
218155
|
const claudeBinaryPath = claudeSettings.binaryPath;
|
|
217321
|
-
const
|
|
217322
|
-
env: process.env,
|
|
217323
|
-
baseDir: serverConfig.baseDir,
|
|
217324
|
-
accountIdentity: resolveClaudeProviderAccountIdentity(process.env),
|
|
217325
|
-
probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support
|
|
217326
|
-
});
|
|
217327
|
-
if (credentialDirectoryDecision.outcome === "skipped") {
|
|
217328
|
-
const skipLine = `Claude credential directory skipped: ${credentialDirectoryDecision.reason}`;
|
|
217329
|
-
yield* credentialDirectoryDecision.reason === "no-account" ? require_Schema$1.logDebug(skipLine) : require_Schema$1.logWarning(skipLine);
|
|
217330
|
-
}
|
|
217331
|
-
const claudeCredentialDirectory = credentialDirectoryDecision.outcome === "set" ? credentialDirectoryDecision.directory : null;
|
|
218156
|
+
const { selectedAccountIdentity, claudeCredentialDirectory } = yield* resolveSelectedCredentialDirectory(input, resumeState, claudeBinaryPath);
|
|
217332
218157
|
const modelConfig = resolveClaudeSessionModelConfig(input.modelSelection, claudeSettings.endpointProfiles);
|
|
217333
|
-
|
|
218158
|
+
if (selectedAccountIdentity !== null && modelConfig.endpointProfile !== void 0) return yield* new ProviderAdapterValidationError({
|
|
218159
|
+
provider: PROVIDER$11,
|
|
218160
|
+
operation: "startSession",
|
|
218161
|
+
issue: "The selected Claude account cannot be used with a custom API endpoint. Choose a subscription model."
|
|
218162
|
+
});
|
|
218163
|
+
const providerProxyCredential = selectedAccountIdentity !== null ? null : yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
|
|
217334
218164
|
const permissionMode = permissionModeForRuntimeMode(input.runtimeMode);
|
|
217335
218165
|
const settings = claudeQuerySettings(modelConfig.thinking, modelConfig.fastMode);
|
|
217336
218166
|
const workspacePreflight = input.cwd ? formatWorkspaceEnvironmentPreflightForAgent(runWorkspaceEnvironmentPreflight({ cwd: input.cwd })) : void 0;
|
|
@@ -217353,7 +218183,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
|
|
|
217353
218183
|
existingResumeSessionId,
|
|
217354
218184
|
newSessionId,
|
|
217355
218185
|
canUseTool,
|
|
217356
|
-
env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory),
|
|
218186
|
+
env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory, selectedAccountIdentity !== null),
|
|
217357
218187
|
browserManager,
|
|
217358
218188
|
threadId,
|
|
217359
218189
|
browserToolEnvironmentId,
|
|
@@ -217405,7 +218235,9 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
|
|
|
217405
218235
|
});
|
|
217406
218236
|
attachClaudeStreamFiber(context$4, runFork);
|
|
217407
218237
|
return { ...session };
|
|
217408
|
-
})
|
|
218238
|
+
}, (effect, input) => effect.pipe(require_Schema$1.onExit((exit) => require_Schema$1.sync(() => {
|
|
218239
|
+
if (exit._tag === "Failure") selectedClaudeCredentialStore(serverConfig.baseDir).release(input.threadId);
|
|
218240
|
+
}))));
|
|
217409
218241
|
const sendTurn = require_Schema$1.fn("sendTurn")(function* (input) {
|
|
217410
218242
|
const context = yield* requireSession(input.threadId);
|
|
217411
218243
|
const modelSelection = input.modelSelection?.provider === "claudeAgent" || input.modelSelection?.provider === "apex" || input.modelSelection?.provider === "cmmd" ? input.modelSelection : void 0;
|
|
@@ -231714,6 +232546,9 @@ const ProviderRollbackConversationInput = require_Schema$1.Struct({
|
|
|
231714
232546
|
threadId: ThreadId,
|
|
231715
232547
|
numTurns: NonNegativeInt
|
|
231716
232548
|
});
|
|
232549
|
+
function selectedAccountGuardFields(providerAccountId) {
|
|
232550
|
+
return providerAccountId === void 0 ? {} : { providerAccountId };
|
|
232551
|
+
}
|
|
231717
232552
|
function cursorRecord(value) {
|
|
231718
232553
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? { ...value } : {};
|
|
231719
232554
|
}
|
|
@@ -231806,9 +232641,13 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
|
|
|
231806
232641
|
const runtimeEventPubSub = yield* require_Path.unbounded$1();
|
|
231807
232642
|
const assertProviderExecutable = options?.assertProviderExecutable ?? (() => require_Schema$1.succeed(null));
|
|
231808
232643
|
const assertCurrentProviderDelivery = require_Schema$1.fn("assertCurrentProviderDelivery")(function* (input) {
|
|
231809
|
-
const currentBinding = yield* assertProviderExecutable(input);
|
|
231810
|
-
if (!currentBinding) return;
|
|
231811
232644
|
const persistedBinding = require_Schema$1.getOrUndefined$1(yield* directory.getBinding(input.threadId));
|
|
232645
|
+
const providerAccountId = readPersistedProviderAccountId(persistedBinding?.runtimePayload);
|
|
232646
|
+
const currentBinding = yield* assertProviderExecutable({
|
|
232647
|
+
...input,
|
|
232648
|
+
...selectedAccountGuardFields(providerAccountId)
|
|
232649
|
+
});
|
|
232650
|
+
if (!currentBinding) return;
|
|
231812
232651
|
if (!persistedBinding || persistedBinding.provider !== input.provider) return yield* toValidationError(input.operation, `Cannot verify the delivered provider identity for thread '${input.threadId}'.`);
|
|
231813
232652
|
yield* bindProviderDeliveryToResumeCursor({
|
|
231814
232653
|
operation: input.operation,
|
|
@@ -231864,10 +232703,12 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
|
|
|
231864
232703
|
if (!hasResumeCursor) return yield* toValidationError(input.operation, `Cannot recover thread '${input.binding.threadId}' because no provider resume state is persisted.`);
|
|
231865
232704
|
const persistedCwd = readPersistedCwd(input.binding.runtimePayload);
|
|
231866
232705
|
const persistedModelSelection = readPersistedModelSelection(input.binding.runtimePayload);
|
|
232706
|
+
const persistedProviderAccountId = readProviderAccountIdFromModelSelection(persistedModelSelection);
|
|
231867
232707
|
const currentDeliveryBinding = yield* assertProviderExecutable({
|
|
231868
232708
|
threadId: input.binding.threadId,
|
|
231869
232709
|
provider: input.binding.provider,
|
|
231870
|
-
operation: input.operation
|
|
232710
|
+
operation: input.operation,
|
|
232711
|
+
...selectedAccountGuardFields(persistedProviderAccountId)
|
|
231871
232712
|
});
|
|
231872
232713
|
const boundResumeCursor = yield* bindProviderDeliveryToResumeCursor({
|
|
231873
232714
|
operation: input.operation,
|
|
@@ -231993,7 +232834,8 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
|
|
|
231993
232834
|
currentBinding: yield* assertProviderExecutable({
|
|
231994
232835
|
threadId,
|
|
231995
232836
|
provider: effectiveProvider,
|
|
231996
|
-
operation: "ProviderService.startSession"
|
|
232837
|
+
operation: "ProviderService.startSession",
|
|
232838
|
+
...selectedAccountGuardFields(requestedAccountId)
|
|
231997
232839
|
})
|
|
231998
232840
|
});
|
|
231999
232841
|
const session = yield* adapter.startSession({
|
|
@@ -232059,6 +232901,8 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
|
|
|
232059
232901
|
allowRecovery: true
|
|
232060
232902
|
});
|
|
232061
232903
|
const persistedBinding = require_Schema$1.getOrUndefined$1(yield* directory.getBinding(input.threadId));
|
|
232904
|
+
const persistedModelSelection = readPersistedModelSelection(persistedBinding?.runtimePayload);
|
|
232905
|
+
const nextModelSelection = input.modelSelection ?? persistedModelSelection;
|
|
232062
232906
|
yield* assertProviderAccountBoundary({
|
|
232063
232907
|
operation: "ProviderService.sendTurn",
|
|
232064
232908
|
threadId: input.threadId,
|
|
@@ -232083,7 +232927,7 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
|
|
|
232083
232927
|
status: "running",
|
|
232084
232928
|
...turn.resumeCursor !== void 0 ? { resumeCursor: turn.resumeCursor } : {},
|
|
232085
232929
|
runtimePayload: {
|
|
232086
|
-
...
|
|
232930
|
+
...nextModelSelection !== void 0 ? { modelSelection: nextModelSelection } : {},
|
|
232087
232931
|
activeTurnId: turn.turnId,
|
|
232088
232932
|
lastRuntimeEvent: "provider.sendTurn",
|
|
232089
232933
|
lastRuntimeEventAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -232340,6 +233184,201 @@ function makeProviderServiceLive(options) {
|
|
|
232340
233184
|
return require_Schema$1.effect(ProviderService, makeProviderService(options));
|
|
232341
233185
|
}
|
|
232342
233186
|
//#endregion
|
|
233187
|
+
//#region src/runtimeAuth/cmmdClaudeSelectedTicket.ts
|
|
233188
|
+
const ACCOUNT = /^pai_v1_[A-Za-z0-9_-]{43}$/;
|
|
233189
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
233190
|
+
const REVISION_HASH = /^[a-f0-9]{64}$/;
|
|
233191
|
+
function hasValidTicketLifetime(claims, payload) {
|
|
233192
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
233193
|
+
const issuedAt = payload.iat;
|
|
233194
|
+
const notBefore = payload.nbf;
|
|
233195
|
+
return typeof issuedAt === "number" && Number.isSafeInteger(issuedAt) && issuedAt <= now + 5 && typeof notBefore === "number" && Number.isSafeInteger(notBefore) && notBefore <= now && Number.isSafeInteger(claims.expiresAtSeconds) && claims.expiresAtSeconds - issuedAt <= 900;
|
|
233196
|
+
}
|
|
233197
|
+
function hasSelectedCredentialScope(claims) {
|
|
233198
|
+
return claims.scope.length === 1 && claims.scope[0] === "claude-credential-read";
|
|
233199
|
+
}
|
|
233200
|
+
function hasExpectedSelectedBinding(payload, expected) {
|
|
233201
|
+
return payload.provider_account_id === expected.providerAccountId && ACCOUNT.test(expected.providerAccountId) && payload.thread_id === expected.threadId && payload.operation_id === expected.operationId && UUID.test(expected.operationId);
|
|
233202
|
+
}
|
|
233203
|
+
function selectedCredentialClaims(payload) {
|
|
233204
|
+
const jti = payload.jti;
|
|
233205
|
+
const generationId = payload.credential_generation_id;
|
|
233206
|
+
const credentialRevisionHash = payload.credential_revision_hash;
|
|
233207
|
+
const accountKind = payload.account_kind;
|
|
233208
|
+
if (typeof jti !== "string" || !jti.trim() || typeof generationId !== "string" || !UUID.test(generationId) || typeof credentialRevisionHash !== "string" || !REVISION_HASH.test(credentialRevisionHash) || accountKind !== "primary" && accountKind !== "linked") return null;
|
|
233209
|
+
return {
|
|
233210
|
+
jti,
|
|
233211
|
+
generationId,
|
|
233212
|
+
credentialRevisionHash,
|
|
233213
|
+
accountKind
|
|
233214
|
+
};
|
|
233215
|
+
}
|
|
233216
|
+
function verifyCmmdClaudeSelectedTicket(token, expected) {
|
|
233217
|
+
const verified = require_runtimeControlAuthority.verifyCmmdScopedRuntimePayload(token, expected, "forge-claude-credential-read");
|
|
233218
|
+
if (!verified) return null;
|
|
233219
|
+
const { claims, payload } = verified;
|
|
233220
|
+
if (!hasValidTicketLifetime(claims, payload)) return null;
|
|
233221
|
+
if (!hasSelectedCredentialScope(claims)) return null;
|
|
233222
|
+
if (!hasExpectedSelectedBinding(payload, expected)) return null;
|
|
233223
|
+
const selected = selectedCredentialClaims(payload);
|
|
233224
|
+
if (!selected) return null;
|
|
233225
|
+
return {
|
|
233226
|
+
...claims,
|
|
233227
|
+
jti: selected.jti,
|
|
233228
|
+
providerAccountId: expected.providerAccountId,
|
|
233229
|
+
generationId: selected.generationId,
|
|
233230
|
+
credentialRevisionHash: selected.credentialRevisionHash,
|
|
233231
|
+
accountKind: selected.accountKind,
|
|
233232
|
+
threadId: expected.threadId,
|
|
233233
|
+
operationId: expected.operationId
|
|
233234
|
+
};
|
|
233235
|
+
}
|
|
233236
|
+
//#endregion
|
|
233237
|
+
//#region src/provider/claudeSelectedCredentialAcquisition.ts
|
|
233238
|
+
function readToken(response) {
|
|
233239
|
+
if (typeof response !== "object" || response === null || !("data" in response)) return null;
|
|
233240
|
+
const data = response.data;
|
|
233241
|
+
return typeof data === "object" && data !== null && "token" in data && typeof data.token === "string" ? data.token : null;
|
|
233242
|
+
}
|
|
233243
|
+
async function acquireSelectedClaudeCredential(expected, transport) {
|
|
233244
|
+
try {
|
|
233245
|
+
const token = readToken(await transport.requestTicket());
|
|
233246
|
+
const claims = token === null ? null : verifyCmmdClaudeSelectedTicket(token, expected);
|
|
233247
|
+
if (!token || !claims) throw new Error("invalid_ticket");
|
|
233248
|
+
const credentialJson = await transport.readCredential(token);
|
|
233249
|
+
if (Buffer.byteLength(credentialJson, "utf8") > 512e3 || !hasUsableClaudeCredentialBlob(credentialJson)) throw new Error("invalid_credential");
|
|
233250
|
+
return {
|
|
233251
|
+
claims,
|
|
233252
|
+
credentialJson
|
|
233253
|
+
};
|
|
233254
|
+
} catch {
|
|
233255
|
+
throw new Error("The selected Claude account could not be prepared. Reconnect the account and try again.");
|
|
233256
|
+
}
|
|
233257
|
+
}
|
|
233258
|
+
//#endregion
|
|
233259
|
+
//#region src/provider/claudeSelectedCredentialPreparer.ts
|
|
233260
|
+
const REQUEST_TIMEOUT_MS = 1e4;
|
|
233261
|
+
const OWNER_TICKET_PATH = "/api/forge/context/providers/claude/runtime-read-ticket";
|
|
233262
|
+
const CREDENTIAL_READ_PATH = "/api/forge/context/auth-runtime-claude-credential";
|
|
233263
|
+
function isRuntimeTicketSession$1(sessionId) {
|
|
233264
|
+
return String(sessionId).startsWith("cmmd-runtime:");
|
|
233265
|
+
}
|
|
233266
|
+
function nonEmptyConfigString(value) {
|
|
233267
|
+
return value?.trim() || null;
|
|
233268
|
+
}
|
|
233269
|
+
function selectedRuntimeStrings(config) {
|
|
233270
|
+
const values = [
|
|
233271
|
+
nonEmptyConfigString(config.forgeCmmdRuntimeId),
|
|
233272
|
+
nonEmptyConfigString(config.forgeCmmdRuntimeMachineId),
|
|
233273
|
+
nonEmptyConfigString(config.forgeCmmdRuntimeOrgId),
|
|
233274
|
+
nonEmptyConfigString(config.forgeCmmdRuntimeUserId),
|
|
233275
|
+
nonEmptyConfigString(config.forgeCmmdRuntimePublicKey)
|
|
233276
|
+
];
|
|
233277
|
+
if (values.includes(null)) return null;
|
|
233278
|
+
return {
|
|
233279
|
+
runtimeId: values[0],
|
|
233280
|
+
machineId: values[1],
|
|
233281
|
+
orgId: values[2],
|
|
233282
|
+
userId: values[3],
|
|
233283
|
+
publicKeyPem: values[4]
|
|
233284
|
+
};
|
|
233285
|
+
}
|
|
233286
|
+
function selectedCredentialEpoch(config) {
|
|
233287
|
+
const credentialEpoch = config.forgeCmmdRuntimeCredentialEpoch;
|
|
233288
|
+
return typeof credentialEpoch === "number" && Number.isSafeInteger(credentialEpoch) && credentialEpoch >= 0 ? credentialEpoch : null;
|
|
233289
|
+
}
|
|
233290
|
+
function selectedConfig(config) {
|
|
233291
|
+
if (config.forgeCmmdRuntimeMode !== true) return null;
|
|
233292
|
+
const strings = selectedRuntimeStrings(config);
|
|
233293
|
+
const credentialEpoch = selectedCredentialEpoch(config);
|
|
233294
|
+
if (strings === null || credentialEpoch === null) return null;
|
|
233295
|
+
return {
|
|
233296
|
+
...strings,
|
|
233297
|
+
credentialEpoch
|
|
233298
|
+
};
|
|
233299
|
+
}
|
|
233300
|
+
async function boundedRuntimeTicketRequest(grant, runtimeId, request) {
|
|
233301
|
+
const response = await fetch(resolveCommandAppEndpoint(`/api/forge/runtimes/${encodeURIComponent(runtimeId)}/provider/claude/read-ticket`), {
|
|
233302
|
+
method: "POST",
|
|
233303
|
+
headers: {
|
|
233304
|
+
authorization: `Bearer ${grant}`,
|
|
233305
|
+
"content-type": "application/json"
|
|
233306
|
+
},
|
|
233307
|
+
body: JSON.stringify(request),
|
|
233308
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
233309
|
+
});
|
|
233310
|
+
if (!response.ok) throw new Error("selected_ticket_unavailable");
|
|
233311
|
+
return await response.json();
|
|
233312
|
+
}
|
|
233313
|
+
async function boundedCredentialRead(token) {
|
|
233314
|
+
const response = await fetch(resolveCommandAppEndpoint(CREDENTIAL_READ_PATH), {
|
|
233315
|
+
headers: {
|
|
233316
|
+
authorization: `Bearer ${token}`,
|
|
233317
|
+
accept: "application/json"
|
|
233318
|
+
},
|
|
233319
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
233320
|
+
});
|
|
233321
|
+
if (!response.ok) throw new Error("selected_credential_unavailable");
|
|
233322
|
+
return await response.text();
|
|
233323
|
+
}
|
|
233324
|
+
function missingAuthority() {
|
|
233325
|
+
throw new Error("selected_authority_unavailable");
|
|
233326
|
+
}
|
|
233327
|
+
/**
|
|
233328
|
+
* Acquires one signed, account-scoped Claude credential for the thread that
|
|
233329
|
+
* selected it. Owner and runtime-ticket sessions use different CMMD lanes.
|
|
233330
|
+
*/
|
|
233331
|
+
function makeClaudeSelectedCredentialPreparer(config, dependencies) {
|
|
233332
|
+
const readRuntimeAuthority = dependencies.readRuntimeAuthority ?? readRuntimeBrokerAuthority;
|
|
233333
|
+
const requestOwnerTicket = dependencies.requestOwnerTicket ?? ((jwt, request) => proxyPost(jwt, OWNER_TICKET_PATH, request, void 0, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }));
|
|
233334
|
+
const requestRuntimeTicket = dependencies.requestRuntimeTicket ?? boundedRuntimeTicketRequest;
|
|
233335
|
+
const readCredential = dependencies.readCredential ?? boundedCredentialRead;
|
|
233336
|
+
const prepare = dependencies.prepare ?? ((threadId, acquired) => {
|
|
233337
|
+
if (!config.baseDir) return Promise.reject(/* @__PURE__ */ new Error("selected_store_unavailable"));
|
|
233338
|
+
return selectedClaudeCredentialStore(config.baseDir).prepare(threadId, acquired);
|
|
233339
|
+
});
|
|
233340
|
+
const createOperationId = dependencies.createOperationId ?? node_crypto.randomUUID;
|
|
233341
|
+
return async (input) => {
|
|
233342
|
+
const sealed = selectedConfig(config);
|
|
233343
|
+
const sessionId = input.executionSessionId ?? dependencies.readSessionId(input.threadId);
|
|
233344
|
+
if (sealed === null || sessionId === null) return missingAuthority();
|
|
233345
|
+
const operationId = createOperationId();
|
|
233346
|
+
const request = {
|
|
233347
|
+
providerAccountId: input.providerAccountId,
|
|
233348
|
+
threadId: String(input.threadId),
|
|
233349
|
+
operationId
|
|
233350
|
+
};
|
|
233351
|
+
const acquired = await acquireSelectedClaudeCredential({
|
|
233352
|
+
...sealed,
|
|
233353
|
+
providerAccountId: input.providerAccountId,
|
|
233354
|
+
threadId: String(input.threadId),
|
|
233355
|
+
operationId
|
|
233356
|
+
}, {
|
|
233357
|
+
requestTicket: async () => {
|
|
233358
|
+
if (isRuntimeTicketSession$1(sessionId)) {
|
|
233359
|
+
const grant = readRuntimeAuthority({
|
|
233360
|
+
sessionId,
|
|
233361
|
+
runtimeId: sealed.runtimeId,
|
|
233362
|
+
machineId: sealed.machineId,
|
|
233363
|
+
credentialEpoch: sealed.credentialEpoch,
|
|
233364
|
+
requiredScope: "provider"
|
|
233365
|
+
});
|
|
233366
|
+
if (grant === null) return missingAuthority();
|
|
233367
|
+
return { data: await requestRuntimeTicket(grant, sealed.runtimeId, request) };
|
|
233368
|
+
}
|
|
233369
|
+
const jwt = await dependencies.resolveJwt(sessionId, { forceRefresh: false });
|
|
233370
|
+
if (jwt === null) return missingAuthority();
|
|
233371
|
+
return await requestOwnerTicket(jwt, {
|
|
233372
|
+
...request,
|
|
233373
|
+
runtimeId: sealed.runtimeId
|
|
233374
|
+
});
|
|
233375
|
+
},
|
|
233376
|
+
readCredential
|
|
233377
|
+
});
|
|
233378
|
+
return await prepare(String(input.threadId), acquired);
|
|
233379
|
+
};
|
|
233380
|
+
}
|
|
233381
|
+
//#endregion
|
|
232343
233382
|
//#region src/provider/claudeRuntimeReadinessGuard.ts
|
|
232344
233383
|
const GENERATION_HASH_PATTERN = /^[a-f0-9]{64}$/;
|
|
232345
233384
|
const RUNTIME_READINESS_TIMEOUT_MS$1 = 1e4;
|
|
@@ -232608,7 +233647,22 @@ function makeClaudeRuntimeReadinessGuard(config, dependencies) {
|
|
|
232608
233647
|
return (input) => claudeReadinessGuardEffect(config, dependencies, resolved, input);
|
|
232609
233648
|
}
|
|
232610
233649
|
function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
|
|
232611
|
-
if (!requiresNativeClaudeReadiness(config, input.provider)) return require_Schema$1.succeed(null);
|
|
233650
|
+
if (!requiresNativeClaudeReadiness(config, input.provider, input.providerAccountId)) return require_Schema$1.succeed(null);
|
|
233651
|
+
if (input.providerAccountId !== void 0) {
|
|
233652
|
+
const prepareSelectedCredential = dependencies.prepareSelectedCredential ?? makeClaudeSelectedCredentialPreparer(config, {
|
|
233653
|
+
resolveJwt: dependencies.resolveJwt,
|
|
233654
|
+
readSessionId: resolved.readSessionId,
|
|
233655
|
+
readRuntimeAuthority: resolved.readRuntimeAuthority
|
|
233656
|
+
});
|
|
233657
|
+
const selectedInput = {
|
|
233658
|
+
...input,
|
|
233659
|
+
providerAccountId: input.providerAccountId
|
|
233660
|
+
};
|
|
233661
|
+
return require_Schema$1.tryPromise({
|
|
233662
|
+
try: () => prepareSelectedCredential(selectedInput),
|
|
233663
|
+
catch: () => selectedCredentialValidationError(input.operation, selectedInput.providerAccountId)
|
|
233664
|
+
});
|
|
233665
|
+
}
|
|
232612
233666
|
const authority = resolveGuardAuthority(config, resolved.readSessionId, input);
|
|
232613
233667
|
if (authority === null) return invalidOwnerSessionEffect(input.operation);
|
|
232614
233668
|
return require_Schema$1.tryPromise({
|
|
@@ -232616,8 +233670,9 @@ function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
|
|
|
232616
233670
|
catch: (cause) => readinessValidationError(input.operation, cause)
|
|
232617
233671
|
});
|
|
232618
233672
|
}
|
|
232619
|
-
function requiresNativeClaudeReadiness(config, provider) {
|
|
232620
|
-
|
|
233673
|
+
function requiresNativeClaudeReadiness(config, provider, providerAccountId) {
|
|
233674
|
+
if (provider !== "claudeAgent" || config.forgeCmmdRuntimeMode !== true) return false;
|
|
233675
|
+
return providerAccountId !== void 0 || personalClaudeInVmEnabled();
|
|
232621
233676
|
}
|
|
232622
233677
|
function resolveGuardAuthority(config, readSessionId, input) {
|
|
232623
233678
|
const runtimeId = config.forgeCmmdRuntimeId?.trim();
|
|
@@ -232654,6 +233709,12 @@ async function verifyGuardAuthority(config, dependencies, resolved, authority) {
|
|
|
232654
233709
|
fetchReadiness: resolved.fetchReadiness
|
|
232655
233710
|
});
|
|
232656
233711
|
}
|
|
233712
|
+
function selectedCredentialValidationError(operation, accountId) {
|
|
233713
|
+
return new ProviderValidationError({
|
|
233714
|
+
operation,
|
|
233715
|
+
issue: `The selected Claude account ${accountId} could not be prepared. Reconnect the account and try again.`
|
|
233716
|
+
});
|
|
233717
|
+
}
|
|
232657
233718
|
function readinessValidationError(operation, cause) {
|
|
232658
233719
|
return new ProviderValidationError({
|
|
232659
233720
|
operation,
|
|
@@ -291409,6 +292470,10 @@ const make$8 = require_Schema$1.gen(function* () {
|
|
|
291409
292470
|
shouldRestartForAccountChange,
|
|
291410
292471
|
hasResumeCursor: resumeCursor !== void 0
|
|
291411
292472
|
});
|
|
292473
|
+
if (shouldRestartForAccountChange) yield* providerService.stopSession({
|
|
292474
|
+
threadId,
|
|
292475
|
+
preserveResumeState: true
|
|
292476
|
+
});
|
|
291412
292477
|
const restartedSession = yield* startProviderSession(resumeCursor !== void 0 ? { resumeCursor } : void 0);
|
|
291413
292478
|
yield* require_Schema$1.logInfo("provider command reactor restarted provider session", {
|
|
291414
292479
|
threadId,
|
|
@@ -297393,7 +298458,7 @@ function resolveBuildCommitFromEnv(env) {
|
|
|
297393
298458
|
* environment descriptor down instead of reporting an honest "unknown".
|
|
297394
298459
|
*/
|
|
297395
298460
|
function readBakedBuildCommit() {
|
|
297396
|
-
return "
|
|
298461
|
+
return "81ce9360f406bcb5ae9fbdc9f90dd55d33f25f0c";
|
|
297397
298462
|
}
|
|
297398
298463
|
async function resolveServerBuildCommit(input) {
|
|
297399
298464
|
if (isFullCommitSha(input.baked)) return input.baked;
|
|
@@ -298818,20 +299883,20 @@ const generatePairingToken = () => {
|
|
|
298818
299883
|
const randomBytes = crypto.getRandomValues(new Uint8Array(PAIRING_TOKEN_LENGTH));
|
|
298819
299884
|
return Array.from(randomBytes, (value) => PAIRING_TOKEN_ALPHABET[value & 31]).join("");
|
|
298820
299885
|
};
|
|
299886
|
+
const invalidBootstrapCredentialError = (message) => new BootstrapCredentialError({
|
|
299887
|
+
message,
|
|
299888
|
+
status: 401
|
|
299889
|
+
});
|
|
299890
|
+
const internalBootstrapCredentialError = (message, cause) => new BootstrapCredentialError({
|
|
299891
|
+
message,
|
|
299892
|
+
status: 500,
|
|
299893
|
+
cause
|
|
299894
|
+
});
|
|
298821
299895
|
const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
298822
299896
|
const config = yield* ServerConfig;
|
|
298823
299897
|
const pairingLinks = yield* AuthPairingLinkRepository;
|
|
298824
299898
|
const seededGrantsRef = yield* require_HttpServer.make$3(/* @__PURE__ */ new Map());
|
|
298825
299899
|
const changesPubSub = yield* require_Path.unbounded$1();
|
|
298826
|
-
const invalidBootstrapCredentialError = (message) => new BootstrapCredentialError({
|
|
298827
|
-
message,
|
|
298828
|
-
status: 401
|
|
298829
|
-
});
|
|
298830
|
-
const internalBootstrapCredentialError = (message, cause) => new BootstrapCredentialError({
|
|
298831
|
-
message,
|
|
298832
|
-
status: 500,
|
|
298833
|
-
cause
|
|
298834
|
-
});
|
|
298835
299900
|
const seedGrant = (credential, grant) => require_HttpServer.update(seededGrantsRef, (current) => {
|
|
298836
299901
|
const next = new Map(current);
|
|
298837
299902
|
next.set(credential, grant);
|
|
@@ -298846,19 +299911,19 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
298846
299911
|
id
|
|
298847
299912
|
}).pipe(require_Schema$1.asVoid);
|
|
298848
299913
|
if (config.desktopBootstrapToken) {
|
|
298849
|
-
const now$
|
|
299914
|
+
const now$3 = yield* require_Schema$1.now;
|
|
298850
299915
|
yield* seedGrant(config.desktopBootstrapToken, {
|
|
298851
299916
|
method: "desktop-bootstrap",
|
|
298852
299917
|
role: "owner",
|
|
298853
299918
|
subject: "desktop-bootstrap",
|
|
298854
|
-
expiresAt: require_Schema$1.add(now$
|
|
299919
|
+
expiresAt: require_Schema$1.add(now$3, { milliseconds: require_Schema$1.toMillis(DESKTOP_BOOTSTRAP_TOKEN_TTL) }),
|
|
298855
299920
|
remainingUses: "unbounded"
|
|
298856
299921
|
});
|
|
298857
299922
|
}
|
|
298858
299923
|
const toBootstrapCredentialError = (message) => (cause) => internalBootstrapCredentialError(message, cause);
|
|
298859
299924
|
const listActive = () => require_Schema$1.gen(function* () {
|
|
298860
|
-
const now$
|
|
298861
|
-
return (yield* pairingLinks.listActive({ now: now$
|
|
299925
|
+
const now$2 = yield* require_Schema$1.now;
|
|
299926
|
+
return (yield* pairingLinks.listActive({ now: now$2 })).map((row) => row.label ? {
|
|
298862
299927
|
id: row.id,
|
|
298863
299928
|
credential: row.credential,
|
|
298864
299929
|
role: row.role,
|
|
@@ -298888,8 +299953,8 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
298888
299953
|
const id = crypto.randomUUID();
|
|
298889
299954
|
const credential = generatePairingToken();
|
|
298890
299955
|
const ttl = input?.ttl ?? DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES;
|
|
298891
|
-
const now$
|
|
298892
|
-
const expiresAt = require_Schema$1.add(now$
|
|
299956
|
+
const now$5 = yield* require_Schema$1.now;
|
|
299957
|
+
const expiresAt = require_Schema$1.add(now$5, { milliseconds: require_Schema$1.toMillis(ttl) });
|
|
298893
299958
|
const issued = {
|
|
298894
299959
|
id,
|
|
298895
299960
|
credential,
|
|
@@ -298903,7 +299968,7 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
298903
299968
|
role: input?.role ?? "client",
|
|
298904
299969
|
subject: input?.subject ?? "one-time-token",
|
|
298905
299970
|
label: input?.label ?? null,
|
|
298906
|
-
createdAt: now$
|
|
299971
|
+
createdAt: now$5,
|
|
298907
299972
|
expiresAt
|
|
298908
299973
|
});
|
|
298909
299974
|
yield* emitUpsert({
|
|
@@ -298912,13 +299977,13 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
298912
299977
|
role: input?.role ?? "client",
|
|
298913
299978
|
subject: input?.subject ?? "one-time-token",
|
|
298914
299979
|
...input?.label ? { label: input.label } : {},
|
|
298915
|
-
createdAt: now$
|
|
299980
|
+
createdAt: now$5,
|
|
298916
299981
|
expiresAt
|
|
298917
299982
|
});
|
|
298918
299983
|
return issued;
|
|
298919
299984
|
}).pipe(require_Schema$1.mapError(toBootstrapCredentialError("Failed to issue pairing credential.")));
|
|
298920
299985
|
const consume = (credential) => require_Schema$1.gen(function* () {
|
|
298921
|
-
const now$
|
|
299986
|
+
const now$4 = yield* require_Schema$1.now;
|
|
298922
299987
|
const seededResult = yield* require_HttpServer.modify(seededGrantsRef, (current) => {
|
|
298923
299988
|
const grant = current.get(credential);
|
|
298924
299989
|
if (!grant) return [{
|
|
@@ -298927,7 +299992,7 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
298927
299992
|
error: invalidBootstrapCredentialError("Unknown bootstrap credential.")
|
|
298928
299993
|
}, current];
|
|
298929
299994
|
const next = new Map(current);
|
|
298930
|
-
if (require_Schema$1.isGreaterThanOrEqualTo$1(now$
|
|
299995
|
+
if (require_Schema$1.isGreaterThanOrEqualTo$1(now$4, grant.expiresAt)) {
|
|
298931
299996
|
next.delete(credential);
|
|
298932
299997
|
return [{
|
|
298933
299998
|
_tag: "error",
|
|
@@ -298956,8 +300021,8 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
298956
300021
|
if (seededResult.reason !== "not-found") return yield* seededResult.error;
|
|
298957
300022
|
const consumed = yield* pairingLinks.consumeAvailable({
|
|
298958
300023
|
credential,
|
|
298959
|
-
consumedAt: now$
|
|
298960
|
-
now: now$
|
|
300024
|
+
consumedAt: now$4,
|
|
300025
|
+
now: now$4
|
|
298961
300026
|
});
|
|
298962
300027
|
if (require_Schema$1.isSome(consumed)) {
|
|
298963
300028
|
yield* emitRemoved(consumed.value.id);
|
|
@@ -298973,9 +300038,9 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
298973
300038
|
if (require_Schema$1.isNone(matching)) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
|
|
298974
300039
|
if (matching.value.revokedAt !== null) return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
|
|
298975
300040
|
if (matching.value.consumedAt !== null) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
|
|
298976
|
-
if (require_Schema$1.isGreaterThanOrEqualTo$1(now$
|
|
300041
|
+
if (require_Schema$1.isGreaterThanOrEqualTo$1(now$4, matching.value.expiresAt)) return yield* invalidBootstrapCredentialError("Bootstrap credential expired.");
|
|
298977
300042
|
return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
|
|
298978
|
-
}).pipe(require_Schema$1.mapError((cause) => cause instanceof BootstrapCredentialError ? cause : internalBootstrapCredentialError("Failed to consume bootstrap credential.", cause)));
|
|
300043
|
+
}).pipe(require_Schema$1.mapError((cause) => cause instanceof BootstrapCredentialError ? cause : internalBootstrapCredentialError("Failed to consume bootstrap credential.", cause)), require_Schema$1.tapError((error) => error.status === 401 ? require_Schema$1.logWarning("Rejected bootstrap credential exchange.", { reason: error.message }) : require_Schema$1.void_));
|
|
298979
300044
|
return {
|
|
298980
300045
|
issueOneTimeToken,
|
|
298981
300046
|
listActive,
|
|
@@ -299031,6 +300096,10 @@ const SetAuthSessionLastConnectedAtInput = require_Schema$1.Struct({
|
|
|
299031
300096
|
sessionId: AuthSessionId,
|
|
299032
300097
|
lastConnectedAt: require_Schema$1.DateTimeUtcFromString
|
|
299033
300098
|
});
|
|
300099
|
+
const ExtendAuthSessionExpiryInput = require_Schema$1.Struct({
|
|
300100
|
+
sessionId: AuthSessionId,
|
|
300101
|
+
expiresAt: require_Schema$1.DateTimeUtcFromString
|
|
300102
|
+
});
|
|
299034
300103
|
var AuthSessionRepository = class extends require_Schema$1.Service()("forge/persistence/Services/AuthSessions/AuthSessionRepository") {};
|
|
299035
300104
|
//#endregion
|
|
299036
300105
|
//#region src/persistence/Layers/AuthSessions.ts
|
|
@@ -299182,6 +300251,17 @@ const makeAuthSessionRepository = require_Schema$1.gen(function* () {
|
|
|
299182
300251
|
WHERE session_id = ${sessionId}
|
|
299183
300252
|
AND revoked_at IS NULL
|
|
299184
300253
|
RETURNING session_id AS "sessionId"
|
|
300254
|
+
`
|
|
300255
|
+
});
|
|
300256
|
+
const extendSessionExpiryRows = findAll({
|
|
300257
|
+
Request: ExtendAuthSessionExpiryInput,
|
|
300258
|
+
Result: require_Schema$1.Struct({ sessionId: AuthSessionId }),
|
|
300259
|
+
execute: ({ sessionId, expiresAt }) => sql`
|
|
300260
|
+
UPDATE auth_sessions
|
|
300261
|
+
SET expires_at = ${expiresAt}
|
|
300262
|
+
WHERE session_id = ${sessionId}
|
|
300263
|
+
AND revoked_at IS NULL
|
|
300264
|
+
RETURNING session_id AS "sessionId"
|
|
299185
300265
|
`
|
|
299186
300266
|
});
|
|
299187
300267
|
const revokeOtherSessionRows = findAll({
|
|
@@ -299204,13 +300284,15 @@ const makeAuthSessionRepository = require_Schema$1.gen(function* () {
|
|
|
299204
300284
|
const revoke = (input) => revokeSessionRows(input).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.revoke:query", "AuthSessionRepository.revoke:decodeRows")), require_Schema$1.map((rows) => rows.length > 0));
|
|
299205
300285
|
const revokeAllExcept = (input) => revokeOtherSessionRows(input).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.revokeAllExcept:query", "AuthSessionRepository.revokeAllExcept:decodeRows")), require_Schema$1.map((rows) => rows.map((row) => row.sessionId)));
|
|
299206
300286
|
const setLastConnectedAt = (input) => setLastConnectedAtRow(input).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.setLastConnectedAt:query", "AuthSessionRepository.setLastConnectedAt:encodeRequest")));
|
|
300287
|
+
const extendExpiry = (input) => extendSessionExpiryRows(input).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.extendExpiry:query", "AuthSessionRepository.extendExpiry:decodeRows")), require_Schema$1.map((rows) => rows.length > 0));
|
|
299207
300288
|
return {
|
|
299208
300289
|
create,
|
|
299209
300290
|
getById,
|
|
299210
300291
|
listActive,
|
|
299211
300292
|
revoke,
|
|
299212
300293
|
revokeAllExcept,
|
|
299213
|
-
setLastConnectedAt
|
|
300294
|
+
setLastConnectedAt,
|
|
300295
|
+
extendExpiry
|
|
299214
300296
|
};
|
|
299215
300297
|
});
|
|
299216
300298
|
const AuthSessionRepositoryLive = require_Schema$1.effect(AuthSessionRepository, makeAuthSessionRepository);
|
|
@@ -299219,6 +300301,20 @@ const AuthSessionRepositoryLive = require_Schema$1.effect(AuthSessionRepository,
|
|
|
299219
300301
|
const SIGNING_SECRET_NAME = "server-signing-key";
|
|
299220
300302
|
const DEFAULT_SESSION_TTL = require_Schema$1.days(30);
|
|
299221
300303
|
const DEFAULT_WEBSOCKET_TOKEN_TTL = require_Schema$1.minutes(5);
|
|
300304
|
+
/**
|
|
300305
|
+
* Whether a session presented at `nowMs` has used up half its own lifetime.
|
|
300306
|
+
*
|
|
300307
|
+
* A phone that opens the app daily should never hit the 30-day wall, but a
|
|
300308
|
+
* lost phone's session should still die on schedule if it is never presented
|
|
300309
|
+
* again. Sliding the expiry forward only when the session is already more
|
|
300310
|
+
* than half spent gives an active phone many chances to renew before expiry
|
|
300311
|
+
* while keeping a dormant session's remaining lifetime bounded by the last
|
|
300312
|
+
* time it was actually used.
|
|
300313
|
+
*/
|
|
300314
|
+
function isPastHalfLife(input) {
|
|
300315
|
+
const halfLifeMarkMs = input.issuedAtMs + (input.expiresAtMs - input.issuedAtMs) / 2;
|
|
300316
|
+
return input.nowMs >= halfLifeMarkMs;
|
|
300317
|
+
}
|
|
299222
300318
|
const toSessionCredentialError = (message) => (cause) => new SessionCredentialError({
|
|
299223
300319
|
message,
|
|
299224
300320
|
cause
|
|
@@ -299379,6 +300475,42 @@ const makeSessionCredentialService = require_Schema$1.gen(function* () {
|
|
|
299379
300475
|
role: claims.role
|
|
299380
300476
|
};
|
|
299381
300477
|
}).pipe(require_Schema$1.mapError(toSessionCredentialError("Failed to issue session credential.")));
|
|
300478
|
+
/**
|
|
300479
|
+
* Slide a session's expiry forward when the token it just verified is past
|
|
300480
|
+
* half its own lifetime, so a phone that keeps using the app never runs
|
|
300481
|
+
* into the 30-day wall.
|
|
300482
|
+
*
|
|
300483
|
+
* The session id, subject, role and method carry over unchanged — only the
|
|
300484
|
+
* signature-bearing `iat`/`exp` differ — which is what lets the DB row be
|
|
300485
|
+
* extended in place (`extendExpiry`) rather than replaced: everything keyed
|
|
300486
|
+
* by session id, most importantly the `command_access_tokens` row holding
|
|
300487
|
+
* this session's CMMD credential, needs no migration.
|
|
300488
|
+
*/
|
|
300489
|
+
const renewIfPastHalfLife = (input) => require_Schema$1.gen(function* () {
|
|
300490
|
+
const nowMs = yield* require_Schema$1.currentTimeMillis;
|
|
300491
|
+
if (!isPastHalfLife({
|
|
300492
|
+
issuedAtMs: input.row.issuedAt.epochMilliseconds,
|
|
300493
|
+
expiresAtMs: input.row.expiresAt.epochMilliseconds,
|
|
300494
|
+
nowMs
|
|
300495
|
+
})) return void 0;
|
|
300496
|
+
const newIssuedAt = yield* require_Schema$1.now;
|
|
300497
|
+
const newExpiresAt = require_Schema$1.add(newIssuedAt, { milliseconds: require_Schema$1.toMillis(DEFAULT_SESSION_TTL) });
|
|
300498
|
+
const newClaims = {
|
|
300499
|
+
...input.claims,
|
|
300500
|
+
iat: newIssuedAt.epochMilliseconds,
|
|
300501
|
+
exp: newExpiresAt.epochMilliseconds
|
|
300502
|
+
};
|
|
300503
|
+
const newEncodedPayload = base64UrlEncode(JSON.stringify(newClaims));
|
|
300504
|
+
const newSignature = signPayload(newEncodedPayload, signingSecret);
|
|
300505
|
+
if (!(yield* authSessions.extendExpiry({
|
|
300506
|
+
sessionId: input.claims.sid,
|
|
300507
|
+
expiresAt: newExpiresAt
|
|
300508
|
+
}).pipe(require_Schema$1.orElseSucceed(() => false)))) return void 0;
|
|
300509
|
+
return {
|
|
300510
|
+
token: `${newEncodedPayload}.${newSignature}`,
|
|
300511
|
+
expiresAt: newExpiresAt
|
|
300512
|
+
};
|
|
300513
|
+
});
|
|
299382
300514
|
const verify = (token) => require_Schema$1.gen(function* () {
|
|
299383
300515
|
const [encodedPayload, signature] = token.split(".");
|
|
299384
300516
|
if (!encodedPayload || !signature) return yield* new SessionCredentialError({ message: "Malformed session token." });
|
|
@@ -299393,14 +300525,19 @@ const makeSessionCredentialService = require_Schema$1.gen(function* () {
|
|
|
299393
300525
|
if (require_Schema$1.isSome(row)) {
|
|
299394
300526
|
if (row.value.revokedAt !== null) return yield* new SessionCredentialError({ message: "Session token revoked." });
|
|
299395
300527
|
} else if (!(statelessCookieSessions && claims.method === "browser-session-cookie")) return yield* new SessionCredentialError({ message: "Unknown session token." });
|
|
300528
|
+
const renewed = require_Schema$1.isSome(row) ? yield* renewIfPastHalfLife({
|
|
300529
|
+
claims,
|
|
300530
|
+
row: row.value
|
|
300531
|
+
}) : void 0;
|
|
299396
300532
|
return {
|
|
299397
300533
|
sessionId: claims.sid,
|
|
299398
300534
|
token,
|
|
299399
300535
|
method: claims.method,
|
|
299400
300536
|
client: require_Schema$1.isSome(row) ? toClientMetadata(row.value.client) : createDefaultClientMetadata(),
|
|
299401
|
-
expiresAt: require_Schema$1.makeUnsafe(claims.exp),
|
|
300537
|
+
expiresAt: renewed?.expiresAt ?? require_Schema$1.makeUnsafe(claims.exp),
|
|
299402
300538
|
subject: claims.sub,
|
|
299403
|
-
role: claims.role
|
|
300539
|
+
role: claims.role,
|
|
300540
|
+
...renewed ? { renewed } : {}
|
|
299404
300541
|
};
|
|
299405
300542
|
}).pipe(require_Schema$1.mapError((cause) => cause instanceof SessionCredentialError ? cause : new SessionCredentialError({
|
|
299406
300543
|
message: "Failed to verify session credential.",
|
|
@@ -299893,7 +301030,10 @@ const makeServerAuth = require_Schema$1.gen(function* () {
|
|
|
299893
301030
|
subject: session.subject,
|
|
299894
301031
|
method: session.method,
|
|
299895
301032
|
role: session.role,
|
|
299896
|
-
...session.
|
|
301033
|
+
...session.renewed ? {
|
|
301034
|
+
expiresAt: session.renewed.expiresAt,
|
|
301035
|
+
renewedSessionToken: session.renewed.token
|
|
301036
|
+
} : session.expiresAt ? { expiresAt: session.expiresAt } : {}
|
|
299897
301037
|
})), require_Schema$1.mapError((cause) => new AuthError({
|
|
299898
301038
|
message: "Unauthorized request.",
|
|
299899
301039
|
status: 401,
|
|
@@ -299928,6 +301068,7 @@ const makeServerAuth = require_Schema$1.gen(function* () {
|
|
|
299928
301068
|
role: session.role,
|
|
299929
301069
|
sessionMethod: session.method,
|
|
299930
301070
|
...session.expiresAt ? { expiresAt: require_Schema$1.toUtc(session.expiresAt) } : {},
|
|
301071
|
+
...session.renewedSessionToken ? { renewedSessionToken: session.renewedSessionToken } : {},
|
|
299931
301072
|
cmmdIdentity
|
|
299932
301073
|
})))), require_Schema$1.tapError((e) => {
|
|
299933
301074
|
if (e._tag === "AuthError" && (e.status ?? 500) < 500) return require_Schema$1.void_;
|
|
@@ -306307,6 +307448,82 @@ const orchestrationAutomationTerminalReconcileRouteLayer = add$1("POST", "/api/o
|
|
|
306307
307448
|
}, { status: 200 });
|
|
306308
307449
|
}).pipe(require_Schema$1.catchTag("OrchestrationDispatchCommandError", respondToWorkRunEventError)));
|
|
306309
307450
|
//#endregion
|
|
307451
|
+
//#region src/orchestration/threadSnapshotRoute.ts
|
|
307452
|
+
/**
|
|
307453
|
+
* `GET /api/orchestration/threads/:threadId`, one thread's detail snapshot,
|
|
307454
|
+
* optionally windowed to a page of recent turns.
|
|
307455
|
+
*
|
|
307456
|
+
* Forge never served this route. The mobile client has asked for it on every
|
|
307457
|
+
* thread open since it shipped, fell through to the socket-embedded snapshot on
|
|
307458
|
+
* the 404, and its "load earlier" control had nowhere to fetch from, so a long
|
|
307459
|
+
* thread was only ever loadable in full. Serving it here gives the initial read
|
|
307460
|
+
* a body the transport can gzip, off the RPC socket, and gives older turns a
|
|
307461
|
+
* cursor to arrive on.
|
|
307462
|
+
*
|
|
307463
|
+
* The window is applied by the same pure function the WebSocket fallback
|
|
307464
|
+
* snapshot uses, so the two reads cannot disagree about what a page is.
|
|
307465
|
+
*/
|
|
307466
|
+
/**
|
|
307467
|
+
* Reads the window out of the query string.
|
|
307468
|
+
*
|
|
307469
|
+
* Returns null for a malformed value rather than ignoring it. A client that
|
|
307470
|
+
* asked for 10 turns and silently received 4,000 has no way to tell, and the
|
|
307471
|
+
* failure would show up as a slow app rather than as an error.
|
|
307472
|
+
*/
|
|
307473
|
+
function parseThreadSnapshotWindow(searchParams) {
|
|
307474
|
+
const rawTurnLimit = searchParams.get("turnLimit");
|
|
307475
|
+
const rawBeforeCursor = searchParams.get("beforeCursor");
|
|
307476
|
+
let turnLimit;
|
|
307477
|
+
if (rawTurnLimit !== null) {
|
|
307478
|
+
const parsed = Number(rawTurnLimit);
|
|
307479
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) return null;
|
|
307480
|
+
turnLimit = parsed;
|
|
307481
|
+
}
|
|
307482
|
+
let beforeCursor;
|
|
307483
|
+
if (rawBeforeCursor !== null) {
|
|
307484
|
+
const trimmed = rawBeforeCursor.trim();
|
|
307485
|
+
if (trimmed.length === 0) return null;
|
|
307486
|
+
beforeCursor = trimmed;
|
|
307487
|
+
}
|
|
307488
|
+
return {
|
|
307489
|
+
...turnLimit === void 0 ? {} : { turnLimit },
|
|
307490
|
+
...beforeCursor === void 0 ? {} : { beforeCursor }
|
|
307491
|
+
};
|
|
307492
|
+
}
|
|
307493
|
+
const orchestrationThreadSnapshotRouteLayer = add$1("GET", "/api/orchestration/threads/:threadId", require_Schema$1.gen(function* () {
|
|
307494
|
+
const request = yield* require_HttpServer.HttpServerRequest;
|
|
307495
|
+
yield* (yield* ServerAuth).authenticateHttpRequest(request);
|
|
307496
|
+
const rawThreadId = (yield* params)["threadId"];
|
|
307497
|
+
if (rawThreadId === void 0 || rawThreadId.length === 0) return require_HttpServer.jsonUnsafe({ error: "Missing thread id." }, { status: 400 });
|
|
307498
|
+
const url = require_HttpServer.toURL(request);
|
|
307499
|
+
const window = require_Schema$1.isNone(url) ? {} : parseThreadSnapshotWindow(url.value.searchParams);
|
|
307500
|
+
if (window === null) return require_HttpServer.jsonUnsafe({ error: "Invalid turnLimit or beforeCursor." }, { status: 400 });
|
|
307501
|
+
const threadId = ThreadId.make(decodeURIComponent(rawThreadId));
|
|
307502
|
+
const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
|
|
307503
|
+
const orchestrationEngine = yield* OrchestrationEngineService;
|
|
307504
|
+
const [threadDetail, snapshotSequence] = yield* require_Schema$1.all([projectionSnapshotQuery.getThreadDetailById(threadId).pipe(require_Schema$1.mapError((cause) => new OrchestrationGetSnapshotError({
|
|
307505
|
+
message: `Failed to load thread ${threadId}`,
|
|
307506
|
+
cause
|
|
307507
|
+
}))), orchestrationEngine.getReadModel().pipe(require_Schema$1.map((readModel) => readModel.snapshotSequence))]);
|
|
307508
|
+
if (require_Schema$1.isNone(threadDetail)) return require_HttpServer.jsonUnsafe({ error: "Thread not found." }, { status: 404 });
|
|
307509
|
+
const windowed = windowThreadDetail({
|
|
307510
|
+
thread: threadDetail.value,
|
|
307511
|
+
window,
|
|
307512
|
+
snapshotSequence
|
|
307513
|
+
});
|
|
307514
|
+
return require_HttpServer.jsonUnsafe({
|
|
307515
|
+
snapshotSequence,
|
|
307516
|
+
thread: windowed.thread,
|
|
307517
|
+
...windowed.page === void 0 ? {} : { page: windowed.page }
|
|
307518
|
+
}, { status: 200 });
|
|
307519
|
+
}).pipe(require_Schema$1.catchTag("AuthError", respondToAuthError), require_Schema$1.catchTag("OrchestrationGetSnapshotError", (error) => require_Schema$1.gen(function* () {
|
|
307520
|
+
yield* require_Schema$1.logError("thread snapshot route failed", {
|
|
307521
|
+
message: error.message,
|
|
307522
|
+
cause: error.cause
|
|
307523
|
+
});
|
|
307524
|
+
return require_HttpServer.jsonUnsafe({ error: error.message }, { status: 500 });
|
|
307525
|
+
}))));
|
|
307526
|
+
//#endregion
|
|
306310
307527
|
//#region src/localAutomations/scheduling.ts
|
|
306311
307528
|
const MINUTE_MS = 6e4;
|
|
306312
307529
|
const HOUR_MS = 60 * MINUTE_MS;
|
|
@@ -318217,7 +319434,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
|
|
|
318217
319434
|
const sql = yield* require_SqlError.SqlClient;
|
|
318218
319435
|
const fetchFn = options.fetchFn ?? fetch;
|
|
318219
319436
|
const baseUrl = options.baseUrl ?? resolveCommandAppUrl();
|
|
318220
|
-
const resolveStoredJwt = (forceRefresh) => require_Schema$1.gen(function* () {
|
|
319437
|
+
const resolveStoredJwt = (forceRefresh, staleJwt) => require_Schema$1.gen(function* () {
|
|
318221
319438
|
const owner = yield* readMostRecentOwnerCommandAuthSession.pipe(require_Schema$1.provideService(require_SqlError.SqlClient, sql), require_Schema$1.mapError((cause) => new ProjectLeadControllerClientError({
|
|
318222
319439
|
kind: "unavailable",
|
|
318223
319440
|
operation: "resolve-owner-session",
|
|
@@ -318231,7 +319448,8 @@ function makeProjectLeadControllerClientLive(options = {}) {
|
|
|
318231
319448
|
sessionId: owner.sessionId,
|
|
318232
319449
|
subject: owner.subject,
|
|
318233
319450
|
logPrefix: "project-lead-scheduler",
|
|
318234
|
-
forceRefresh
|
|
319451
|
+
forceRefresh,
|
|
319452
|
+
staleAccessToken: staleJwt
|
|
318235
319453
|
}).pipe(require_Schema$1.provideService(require_SqlError.SqlClient, sql));
|
|
318236
319454
|
if (!jwt) return yield* new ProjectLeadControllerClientError({
|
|
318237
319455
|
kind: "unauthenticated",
|
|
@@ -318274,7 +319492,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
|
|
|
318274
319492
|
}));
|
|
318275
319493
|
if (first._tag === "Success") return first.success;
|
|
318276
319494
|
if (first.failure.kind !== "unauthenticated") return yield* first.failure;
|
|
318277
|
-
const refreshed = yield* resolveJwt(true);
|
|
319495
|
+
const refreshed = yield* resolveJwt(true, jwt);
|
|
318278
319496
|
return yield* requestAttempt({
|
|
318279
319497
|
...input,
|
|
318280
319498
|
jwt: refreshed
|
|
@@ -320681,12 +321899,12 @@ const gitUrlCloneRouteLayer = add$1("POST", "/api/cloud/git/clone-url", require_
|
|
|
320681
321899
|
//#region src/server.ts
|
|
320682
321900
|
const PtyAdapterLive = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
|
|
320683
321901
|
if (typeof Bun !== "undefined") return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunPTY-uyreNRGp.cjs")))).layer;
|
|
320684
|
-
else return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodePTY-
|
|
321902
|
+
else return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodePTY-BehazUMi.cjs")))).layer;
|
|
320685
321903
|
}));
|
|
320686
321904
|
const HttpServerLive = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
|
|
320687
321905
|
const config = yield* ServerConfig;
|
|
320688
|
-
if (typeof Bun !== "undefined") return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunHttpServer-
|
|
320689
|
-
const [NodeHttpServer, NodeHttp] = yield* require_Schema$1.all([require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeHttpServer-
|
|
321906
|
+
if (typeof Bun !== "undefined") return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunHttpServer-C0oPP_3A.cjs")))).layer(bunHttpServerOptions(config));
|
|
321907
|
+
const [NodeHttpServer, NodeHttp] = yield* require_Schema$1.all([require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeHttpServer-Cjd3bFrD.cjs"))), require_Schema$1.promise(() => import("node:http"))]);
|
|
320690
321908
|
return NodeHttpServer.layer(NodeHttp.createServer, {
|
|
320691
321909
|
host: config.host,
|
|
320692
321910
|
port: config.port
|
|
@@ -320694,10 +321912,10 @@ const HttpServerLive = require_Schema$1.unwrap(require_Schema$1.gen(function* ()
|
|
|
320694
321912
|
}));
|
|
320695
321913
|
const PlatformServicesLive = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
|
|
320696
321914
|
if (typeof Bun !== "undefined") {
|
|
320697
|
-
const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunServices-
|
|
321915
|
+
const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunServices-DkenwD7S.cjs")));
|
|
320698
321916
|
return layer;
|
|
320699
321917
|
} else {
|
|
320700
|
-
const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeServices-
|
|
321918
|
+
const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeServices-BvdIc9Vh.cjs")));
|
|
320701
321919
|
return layer;
|
|
320702
321920
|
}
|
|
320703
321921
|
}));
|
|
@@ -320764,7 +321982,7 @@ const MiscRuntimeLayerLive = require_Schema$1.mergeAll(AnalyticsServiceLayerLive
|
|
|
320764
321982
|
const RuntimeDependenciesWithoutPullRequestsLive = ReactorLayerLive.pipe(require_Schema$1.provideMerge(CheckpointingLayerLive), require_Schema$1.provideMerge(GitLayerLive), require_Schema$1.provideMerge(OrchestrationLayerLive), require_Schema$1.provideMerge(ProviderLayerLive), require_Schema$1.provideMerge(TerminalLayerLive), require_Schema$1.provideMerge(AgentBrowserManagerLive), require_Schema$1.provideMerge(require_Schema$1.mergeAll(ThreadDevRuntimeLive, DevTunnelLive.pipe(require_Schema$1.provide$1(ThreadDevRuntimeLive)))), require_Schema$1.provideMerge(PersistenceLayerLive), require_Schema$1.provideMerge(KeybindingsLive), require_Schema$1.provideMerge(ProviderRegistryLive), require_Schema$1.provideMerge(require_Schema$1.mergeAll(ServerSettingsLive, UsageServiceLive.pipe(require_Schema$1.provide$1(ServerSettingsLive), require_Schema$1.provide$1(UsageSessionLookupLive), require_Schema$1.provide$1(UsageRecordedSourceLive.pipe(require_Schema$1.provide$1(OrchestrationLayerLive), require_Schema$1.provide$1(RuntimeReplicationOutboxLive), require_Schema$1.provide$1(ProviderSessionRuntimeRepositoryLive), require_Schema$1.provide$1(TextGenerationSessionRepositoryLive)))))), require_Schema$1.provideMerge(WorkspaceLayerLive), require_Schema$1.provideMerge(WorkspaceReadinessStateLive), require_Schema$1.provideMerge(ProjectFaviconResolverLive), require_Schema$1.provideMerge(RepositoryIdentityResolverLive), require_Schema$1.provideMerge(ServerEnvironmentLive), require_Schema$1.provideMerge(AuthLayerLive), require_Schema$1.provideMerge(ServerSecretStoreLayerLive), require_Schema$1.provideMerge(require_Schema$1.mergeAll(CloudRedisCoordinationLive, CloudStateSyncLive.pipe(require_Schema$1.provide$1(PersistenceLayerLive)))), require_Schema$1.provideMerge(MiscRuntimeLayerLive));
|
|
320765
321983
|
const RuntimeDependenciesLive = pullRequestServiceFactoryLayer.pipe(require_Schema$1.provideMerge(RuntimeDependenciesWithoutPullRequestsLive), require_Schema$1.provideMerge(cmmdProjectIdentityResolverLive.pipe(require_Schema$1.provide$1(RuntimeDependenciesWithoutPullRequestsLive))), require_Schema$1.provideMerge(pullRequestProviderRuntimeLive));
|
|
320766
321984
|
const RuntimeServicesLive = ServerRuntimeStartupLive.pipe(require_Schema$1.provideMerge(RuntimeDependenciesLive));
|
|
320767
|
-
const makeRoutesLayer = require_Schema$1.mergeAll(attachmentUploadRouteLayer, authBearerBootstrapRouteLayer, authBootstrapRouteLayer, authCommandBearerBootstrapRouteLayer, authCommandBootstrapRouteLayer, authOAuthTokenExchangeRouteLayer, authWebSocketTicketRouteLayer, authCmmdOAuthDeviceRouteLayer, authCmmdOAuthTokenRouteLayer, authCmmdPasswordTokenRouteLayer, authClientsRevokeOthersRouteLayer, authClientsRevokeRouteLayer, authClientsRouteLayer, authLogoutRouteLayer, authPairingLinksRevokeRouteLayer, authPairingLinksRouteLayer, authPairingCredentialRouteLayer, authSessionRouteLayer, authWebSocketTokenRouteLayer, authWebLoginRouteLayer, authWebCallbackRouteLayer, desktopLocalWebBridgePublishRouteLayer, desktopLocalWebBridgeBootstrapRouteLayer, desktopRelayControlPlaneRouteLayer, apiDocsHtmlRouteLayer, apiDocsOpenApiRouteLayer, pulseSignalStateListRouteLayer, pulseSignalStateUpsertRouteLayer, cmmdProfileRouteLayer, ...cmmdProxyRouteLayers, cmmdWorkIntakeRouteLayer, cmmdWsTokenRouteLayer, cmmdGetProjectCommentsRouteLayer, cmmdAddProjectCommentRouteLayer, cmmdUpdateProjectRouteLayer, cmmdUploadTaskAttachmentRouteLayer, cmmdUpdateTaskCommentRouteLayer, cmmdListWorkspaceMembersRouteLayer, cmmdAddProjectMemberRouteLayer, cmmdUpdateProjectAgentMemberAvatarRouteLayer, runtimeReplicationStatusRouteLayer, cmmdRefreshProviderUsageRouteLayer, cmmdForgeCloudProjectsListRouteLayer, cmmdForgeCloudProjectsEventsRouteLayer, cmmdForgeRuntimesEnsureRouteLayer, cmmdForgeRuntimesConnectRouteLayer, ownerRuntimeCodexReadinessRouteLayer, cmmdForgeRuntimesStatusRouteLayer, cmmdForgeRuntimesStartRouteLayer, cmmdForgeRuntimesStopRouteLayer, cmmdForgeRuntimesRecycleRouteLayer, cmmdForgeRuntimesUpdateCommitRouteLayer, cmmdForgeRuntimesDestroyRouteLayer, mobileProviderCatalogRouteLayer, attachmentsRouteLayer, orchestrationDispatchRouteLayer, orchestrationCmmdWorkRunEventRouteLayer, orchestrationAutomationStartThreadRouteLayer, orchestrationAutomationRebindRouteLayer, orchestrationAutomationTerminalReconcileRouteLayer, mobileGitRunStackedActionRouteLayer, pullRequestDiffRouteLayer, orchestrationSnapshotRouteLayer, localAutomationListRouteLayer, localAutomationCreateRouteLayer, localAutomationUpdateRouteLayer, localAutomationDeleteRouteLayer, localAutomationRunRouteLayer, otlpTracesProxyRouteLayer, devTunnelProxyRouteLayer, devTunnelLocalPortProxyRouteLayer, projectFaviconRouteLayer, serverEnvironmentRouteLayer, browserWebRtcReadinessRouteLayer, staticAndDevRouteLayer, githubStatusRouteLayer, githubAuthorizeUrlRouteLayer, githubDisconnectRouteLayer, githubReposRouteLayer, githubCloneRouteLayer, githubCloneJobStartRouteLayer, githubCloneJobStatusRouteLayer, githubRefreshCredentialsRouteLayer, githubToolConfirmationApproveRouteLayer, projectLinkGetRouteLayer, projectLinkListRouteLayer, projectLinkSetRouteLayer, projectLinkDeleteRouteLayer, cmmdWorkspacesRouteLayer, cmmdCampaignsRouteLayer, cmmdProjectLinksRouteLayer, cmmdEnsureGitHubProjectContextRouteLayer, cmmdProjectRepositoryEnsureRouteLayer, cmmdProjectsGetRouteLayer, cmmdProjectsCreateRouteLayer, workspaceReadinessRouteLayer, runtimeLivenessRouteLayer, runtimeFilesystemHealthRouteLayer, claudeNativeGenerationProofRouteLayer, claudeUsageRouteLayer, codexRuntimeProviderReadinessRouteLayer, runtimeDiagnosticsRouteLayer, cloudFoldersRouteLayer, cloudProjectCreateRouteLayer, gitUrlCloneRouteLayer, websocketRpcRouteLayer).pipe(require_Schema$1.provide$1(browserApiCorsLayer));
|
|
321985
|
+
const makeRoutesLayer = require_Schema$1.mergeAll(attachmentUploadRouteLayer, authBearerBootstrapRouteLayer, authBootstrapRouteLayer, authCommandBearerBootstrapRouteLayer, authCommandBootstrapRouteLayer, authOAuthTokenExchangeRouteLayer, authWebSocketTicketRouteLayer, authCmmdOAuthDeviceRouteLayer, authCmmdOAuthTokenRouteLayer, authCmmdPasswordTokenRouteLayer, authClientsRevokeOthersRouteLayer, authClientsRevokeRouteLayer, authClientsRouteLayer, authLogoutRouteLayer, authPairingLinksRevokeRouteLayer, authPairingLinksRouteLayer, authPairingCredentialRouteLayer, authSessionRenewRouteLayer, authSessionRouteLayer, authWebSocketTokenRouteLayer, authWebLoginRouteLayer, authWebCallbackRouteLayer, desktopLocalWebBridgePublishRouteLayer, desktopLocalWebBridgeBootstrapRouteLayer, desktopRelayControlPlaneRouteLayer, apiDocsHtmlRouteLayer, apiDocsOpenApiRouteLayer, pulseSignalStateListRouteLayer, pulseSignalStateUpsertRouteLayer, cmmdProfileRouteLayer, ...cmmdProxyRouteLayers, cmmdWorkIntakeRouteLayer, cmmdWsTokenRouteLayer, cmmdGetProjectCommentsRouteLayer, cmmdAddProjectCommentRouteLayer, cmmdUpdateProjectRouteLayer, cmmdUploadTaskAttachmentRouteLayer, cmmdUpdateTaskCommentRouteLayer, cmmdListWorkspaceMembersRouteLayer, cmmdAddProjectMemberRouteLayer, cmmdUpdateProjectAgentMemberAvatarRouteLayer, runtimeReplicationStatusRouteLayer, cmmdRefreshProviderUsageRouteLayer, cmmdForgeCloudProjectsListRouteLayer, cmmdForgeCloudProjectsEventsRouteLayer, cmmdForgeRuntimesEnsureRouteLayer, cmmdForgeRuntimesConnectRouteLayer, ownerRuntimeCodexReadinessRouteLayer, cmmdForgeRuntimesStatusRouteLayer, cmmdForgeRuntimesStartRouteLayer, cmmdForgeRuntimesStopRouteLayer, cmmdForgeRuntimesRecycleRouteLayer, cmmdForgeRuntimesUpdateCommitRouteLayer, cmmdForgeRuntimesDestroyRouteLayer, mobileProviderCatalogRouteLayer, attachmentsRouteLayer, orchestrationDispatchRouteLayer, orchestrationCmmdWorkRunEventRouteLayer, orchestrationAutomationStartThreadRouteLayer, orchestrationAutomationRebindRouteLayer, orchestrationAutomationTerminalReconcileRouteLayer, mobileGitRunStackedActionRouteLayer, pullRequestDiffRouteLayer, orchestrationSnapshotRouteLayer, orchestrationThreadSnapshotRouteLayer, localAutomationListRouteLayer, localAutomationCreateRouteLayer, localAutomationUpdateRouteLayer, localAutomationDeleteRouteLayer, localAutomationRunRouteLayer, otlpTracesProxyRouteLayer, devTunnelProxyRouteLayer, devTunnelLocalPortProxyRouteLayer, projectFaviconRouteLayer, serverEnvironmentRouteLayer, browserWebRtcReadinessRouteLayer, staticAndDevRouteLayer, githubStatusRouteLayer, githubAuthorizeUrlRouteLayer, githubDisconnectRouteLayer, githubReposRouteLayer, githubCloneRouteLayer, githubCloneJobStartRouteLayer, githubCloneJobStatusRouteLayer, githubRefreshCredentialsRouteLayer, githubToolConfirmationApproveRouteLayer, projectLinkGetRouteLayer, projectLinkListRouteLayer, projectLinkSetRouteLayer, projectLinkDeleteRouteLayer, cmmdWorkspacesRouteLayer, cmmdCampaignsRouteLayer, cmmdProjectLinksRouteLayer, cmmdEnsureGitHubProjectContextRouteLayer, cmmdProjectRepositoryEnsureRouteLayer, cmmdProjectsGetRouteLayer, cmmdProjectsCreateRouteLayer, workspaceReadinessRouteLayer, runtimeLivenessRouteLayer, runtimeFilesystemHealthRouteLayer, claudeNativeGenerationProofRouteLayer, claudeUsageRouteLayer, codexRuntimeProviderReadinessRouteLayer, runtimeDiagnosticsRouteLayer, cloudFoldersRouteLayer, cloudProjectCreateRouteLayer, gitUrlCloneRouteLayer, websocketRpcRouteLayer).pipe(require_Schema$1.provide$1(browserApiCorsLayer));
|
|
320768
321986
|
const makeServerLayer = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
|
|
320769
321987
|
const config = yield* ServerConfig;
|
|
320770
321988
|
fixPath({ preferProcessPath: config.forgeCmmdRuntimeMode === true });
|