@cmmd-center/forge 0.13.12 → 0.13.14
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/bin.cjs +543 -75
- package/dist/bin.mjs +543 -75
- package/dist/client/assets/{CanvasPanel-BjHxCGlb.js → CanvasPanel-RpD9Eh_S.js} +2 -2
- package/dist/client/assets/{CompositeItem-D4LER3__.js → CompositeItem-DEIBUHJR.js} +2 -2
- package/dist/client/assets/{DiffPanel-D3btZGjW.js → DiffPanel-P3t5Fq1q.js} +2 -2
- package/dist/client/assets/{DiffPanel.logic-DWcwE9Wj.js → DiffPanel.logic-BXQ1C0-e.js} +2 -2
- package/dist/client/assets/{DiffPanelShell-oXHh--ae.js → DiffPanelShell-3EpWEr7p.js} +103 -103
- package/dist/client/assets/{DiffWorkerPoolProvider-R9YPaaNy.js → DiffWorkerPoolProvider-3_hkcEDq.js} +2 -2
- package/dist/client/assets/{PullRequestCodePanel-DTCc2TBl.js → PullRequestCodePanel-B2T6MPcE.js} +2 -2
- package/dist/client/assets/{Virtualizer-arKGxq_z.js → Virtualizer-DCt36RGD.js} +2 -2
- package/dist/client/assets/{canvasStateStore-B5zd1u9r.js → canvasStateStore-NrAxDqvM.js} +2 -2
- package/dist/client/assets/{cmmdAuthRedirect-DeFAIQpj.js → cmmdAuthRedirect-BahlX0gF.js} +2 -2
- package/dist/client/assets/index-C3pC5t1K.css +1 -0
- package/dist/client/assets/index-DvwXD11B.js +154 -0
- package/dist/client/assets/{popover-DM7IA0zJ.js → popover-Z8Tvl-OD.js} +2 -2
- package/dist/client/assets/src-BU7yHzO9.js +3 -0
- package/dist/client/assets/{toggle-DVtS0BJJ.js → toggle-CWYdWYBn.js} +2 -2
- package/dist/client/index.html +11 -11
- package/dist/forge-build.json +1 -1
- package/package.json +1 -1
- package/dist/client/assets/index-Bn-ASQRG.css +0 -1
- package/dist/client/assets/index-C7YKDHIe.js +0 -154
- package/dist/client/assets/src-LDQtPKQt.js +0 -3
package/dist/bin.cjs
CHANGED
|
@@ -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",
|
|
@@ -17931,6 +17932,21 @@ require_Schema$1.Struct({
|
|
|
17931
17932
|
beforeCursor: require_Schema$1.optionalKey(TrimmedNonEmptyString)
|
|
17932
17933
|
});
|
|
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
|
+
/**
|
|
17934
17950
|
* Page metadata for a windowed thread detail read. `beforeCursor` is opaque and
|
|
17935
17951
|
* exclusive: passing it back returns the adjacent slice of older turns. `null`
|
|
17936
17952
|
* means nothing older is left. `snapshotSequence` mirrors the top-level
|
|
@@ -18928,6 +18944,10 @@ const OrchestrationRpcSchemas = {
|
|
|
18928
18944
|
searchThreads: {
|
|
18929
18945
|
input: OrchestrationSearchThreadsInput,
|
|
18930
18946
|
output: OrchestrationSearchThreadsResult
|
|
18947
|
+
},
|
|
18948
|
+
getThreadDetailPage: {
|
|
18949
|
+
input: OrchestrationGetThreadDetailPageInput,
|
|
18950
|
+
output: OrchestrationThreadDetailSnapshot
|
|
18931
18951
|
}
|
|
18932
18952
|
};
|
|
18933
18953
|
var OrchestrationGetSnapshotError = class extends require_Schema$1.TaggedErrorClass()("OrchestrationGetSnapshotError", {
|
|
@@ -23006,6 +23026,7 @@ const ServerConfig$1 = require_Schema$1.Struct({
|
|
|
23006
23026
|
shellRevealInFileManagerKind: require_Schema$1.optionalKey(FileManagerRevealKind),
|
|
23007
23027
|
threadResumeCompletionMarker: require_Schema$1.optionalKey(require_Schema$1.Boolean),
|
|
23008
23028
|
threadSnapshotPagination: require_Schema$1.optionalKey(require_Schema$1.Boolean),
|
|
23029
|
+
threadDetailPageRpc: require_Schema$1.optionalKey(require_Schema$1.Boolean),
|
|
23009
23030
|
observability: ServerObservability,
|
|
23010
23031
|
settings: ServerSettings
|
|
23011
23032
|
});
|
|
@@ -26965,6 +26986,17 @@ const WsOrchestrationSearchThreadsRpc = make$30(ORCHESTRATION_WS_METHODS.searchT
|
|
|
26965
26986
|
error: OrchestrationSearchThreadsError
|
|
26966
26987
|
});
|
|
26967
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
|
+
/**
|
|
26968
27000
|
* Resolve `owner/repo` to the repository the caller is allowed to clone.
|
|
26969
27001
|
*
|
|
26970
27002
|
* Both of these run against the same CMMD-brokered GitHub credential the web
|
|
@@ -27077,7 +27109,7 @@ const WsRpcGroup = make$29(WsServerGetConfigRpc, WsServerRefreshProvidersRpc, Ws
|
|
|
27077
27109
|
payload: ProviderUploadFeedbackInput,
|
|
27078
27110
|
success: ProviderUploadFeedbackResult,
|
|
27079
27111
|
error: ProviderUploadFeedbackError
|
|
27080
|
-
}), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc);
|
|
27112
|
+
}), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsOrchestrationGetThreadDetailPageRpc);
|
|
27081
27113
|
const RelayWsRpcGroup = WsRpcGroup.middleware(RelayAuthorizationMiddleware).middleware(RelayDeliveryMiddleware);
|
|
27082
27114
|
/**
|
|
27083
27115
|
* The direct-WebSocket mount of the same group, with per-method scope
|
|
@@ -27150,6 +27182,32 @@ function encodeCmmdRuntimeToolActivityPayload(kind, payload) {
|
|
|
27150
27182
|
}
|
|
27151
27183
|
};
|
|
27152
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
|
+
}
|
|
27153
27211
|
//#endregion
|
|
27154
27212
|
//#region ../../packages/contracts/src/cmmdProjectPatch.ts
|
|
27155
27213
|
const CMMD_PROJECT_PATCH_FIELDS = {
|
|
@@ -32260,6 +32318,11 @@ const RELAY_METHOD_POLICY_BASES = [
|
|
|
32260
32318
|
requiredScope: "thread.read",
|
|
32261
32319
|
deliveryClass: "subscription"
|
|
32262
32320
|
},
|
|
32321
|
+
{
|
|
32322
|
+
method: ORCHESTRATION_WS_METHODS.getThreadDetailPage,
|
|
32323
|
+
requiredScope: "thread.read",
|
|
32324
|
+
deliveryClass: "read"
|
|
32325
|
+
},
|
|
32263
32326
|
{
|
|
32264
32327
|
method: WS_METHODS.assetsCreateUrl,
|
|
32265
32328
|
requiredScope: "thread.read",
|
|
@@ -66317,7 +66380,7 @@ function normalizeNumberish(value) {
|
|
|
66317
66380
|
}
|
|
66318
66381
|
//#endregion
|
|
66319
66382
|
//#region package.json
|
|
66320
|
-
var version$1 = "0.13.
|
|
66383
|
+
var version$1 = "0.13.14";
|
|
66321
66384
|
//#endregion
|
|
66322
66385
|
//#region src/sentry.ts
|
|
66323
66386
|
const SERVER_APP_NAME = "forge-server";
|
|
@@ -66719,6 +66782,129 @@ const respondToAuthError = (error) => require_Schema$1.gen(function* () {
|
|
|
66719
66782
|
}));
|
|
66720
66783
|
return response;
|
|
66721
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
|
+
}
|
|
66722
66908
|
const OAUTH_ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
|
|
66723
66909
|
/**
|
|
66724
66910
|
* The one exchange this endpoint performs, or the reason it will not.
|
|
@@ -68566,6 +68752,11 @@ function commandOAuthProxyStatus(path, status, body) {
|
|
|
68566
68752
|
}
|
|
68567
68753
|
const authBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap", require_Schema$1.gen(function* () {
|
|
68568
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;
|
|
68569
68760
|
const serverAuth = yield* ServerAuth;
|
|
68570
68761
|
const sessions = yield* SessionCredentialService;
|
|
68571
68762
|
const payload = yield* require_HttpServer.schemaBodyJson(AuthBootstrapInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
@@ -68583,6 +68774,11 @@ const authBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap", require_Sch
|
|
|
68583
68774
|
}).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
68584
68775
|
const authCommandBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/command", require_Schema$1.gen(function* () {
|
|
68585
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;
|
|
68586
68782
|
const serverAuth = yield* ServerAuth;
|
|
68587
68783
|
const sessions = yield* SessionCredentialService;
|
|
68588
68784
|
if (serverAuth.isForgeCloudShell?.() === true) return yield* new AuthError({
|
|
@@ -68631,6 +68827,11 @@ const authCommandBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/comman
|
|
|
68631
68827
|
}).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
68632
68828
|
const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/command/bearer", require_Schema$1.gen(function* () {
|
|
68633
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;
|
|
68634
68835
|
const serverAuth = yield* ServerAuth;
|
|
68635
68836
|
const sessions = yield* SessionCredentialService;
|
|
68636
68837
|
if (serverAuth.isForgeCloudShell?.() === true) return yield* new AuthError({
|
|
@@ -68679,6 +68880,11 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
|
|
|
68679
68880
|
}, { status: 200 });
|
|
68680
68881
|
}).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
68681
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;
|
|
68682
68888
|
const payload = yield* require_HttpServer.schemaBodyJson(CommandOAuthDeviceInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
68683
68889
|
message: "Invalid CMMD OAuth device payload.",
|
|
68684
68890
|
status: 400,
|
|
@@ -68690,7 +68896,17 @@ const authCmmdOAuthDeviceRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/device
|
|
|
68690
68896
|
...payload.scope ? { scope: payload.scope } : {}
|
|
68691
68897
|
});
|
|
68692
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
|
+
*/
|
|
68693
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;
|
|
68694
68910
|
const payload = yield* require_HttpServer.schemaBodyJson(CommandOAuthTokenInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
68695
68911
|
message: "Invalid CMMD OAuth token payload.",
|
|
68696
68912
|
status: 400,
|
|
@@ -68985,6 +69201,11 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", require_Sch
|
|
|
68985
69201
|
}).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
|
|
68986
69202
|
const authBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/bearer", require_Schema$1.gen(function* () {
|
|
68987
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;
|
|
68988
69209
|
const serverAuth = yield* ServerAuth;
|
|
68989
69210
|
const payload = yield* require_HttpServer.schemaBodyJson(AuthBootstrapInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
68990
69211
|
message: "Invalid bootstrap payload.",
|
|
@@ -69017,6 +69238,11 @@ const authBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/bearer"
|
|
|
69017
69238
|
*/
|
|
69018
69239
|
const authOAuthTokenExchangeRouteLayer = add$1("POST", "/oauth/token", require_Schema$1.gen(function* () {
|
|
69019
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;
|
|
69020
69246
|
const serverAuth = yield* ServerAuth;
|
|
69021
69247
|
const payload = yield* require_HttpServer.schemaBodyUrlParams(OAuthTokenExchangeInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
69022
69248
|
message: "Invalid token exchange payload.",
|
|
@@ -71242,9 +71468,12 @@ function trySubjectFallback(sessionId, subject, logPrefix) {
|
|
|
71242
71468
|
* Fail-closed: never returns a non-user / malformed Bearer. Callers must map
|
|
71243
71469
|
* null to CMMD_AUTH_REQUIRED and must not call CMMD upstream with a bad token.
|
|
71244
71470
|
*/
|
|
71245
|
-
function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false }) {
|
|
71471
|
+
function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false, staleAccessToken = null }) {
|
|
71246
71472
|
const cached = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
|
|
71247
|
-
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
|
+
}
|
|
71248
71477
|
const normalizedSubject = subject?.trim() || null;
|
|
71249
71478
|
if (!normalizedSubject && !forceRefresh) return require_Schema$1.succeed(null);
|
|
71250
71479
|
return require_Schema$1.gen(function* () {
|
|
@@ -72081,6 +72310,11 @@ function buildProxyRouteLayer(route) {
|
|
|
72081
72310
|
const handler = require_Schema$1.gen(function* () {
|
|
72082
72311
|
const req = yield* require_HttpServer.HttpServerRequest;
|
|
72083
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;
|
|
72084
72318
|
const pathParams = yield* params;
|
|
72085
72319
|
const cmmdPath = substitutePath(route.cmmdPath, pathParams);
|
|
72086
72320
|
if (!cmmdPath) return require_HttpServer.jsonUnsafe({ error: "Missing required path parameter." }, { status: 400 });
|
|
@@ -72141,6 +72375,11 @@ function buildProxyHandlerEffect(route) {
|
|
|
72141
72375
|
return require_Schema$1.gen(function* () {
|
|
72142
72376
|
const req = yield* require_HttpServer.HttpServerRequest;
|
|
72143
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;
|
|
72144
72383
|
if (!readCommandAccessTokenForSession(session.sessionId)) return yield* respondCmmdConnectionRequired(session.sessionId);
|
|
72145
72384
|
const parsed = yield* route.parse(req);
|
|
72146
72385
|
if (!parsed.ok) return require_HttpServer.jsonUnsafe(parsed.body, { status: parsed.status });
|
|
@@ -98040,6 +98279,26 @@ function serializeNativeToolActivity(activity) {
|
|
|
98040
98279
|
}
|
|
98041
98280
|
};
|
|
98042
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
|
+
}
|
|
98043
98302
|
function isReplicatedFailureActivity(activity) {
|
|
98044
98303
|
return activity.kind === "runtime.error" || activity.kind.startsWith("provider.") && activity.kind.endsWith(".failed");
|
|
98045
98304
|
}
|
|
@@ -98420,12 +98679,17 @@ function serializeRuntimeThreadEvents(thread) {
|
|
|
98420
98679
|
const event = serializeNativeToolActivity(activity);
|
|
98421
98680
|
return event === null ? [] : [event];
|
|
98422
98681
|
});
|
|
98682
|
+
const worktreeLaunchActivities = thread.activities.flatMap((activity) => {
|
|
98683
|
+
const event = serializeWorktreeLaunchActivity(activity);
|
|
98684
|
+
return event === null ? [] : [event];
|
|
98685
|
+
});
|
|
98423
98686
|
const recordedTurns = serializeRecordedTurns(thread);
|
|
98424
98687
|
const textGenerations = serializeTextGenerations(thread);
|
|
98425
98688
|
return [
|
|
98426
98689
|
...messages,
|
|
98427
98690
|
...replicaActivities,
|
|
98428
98691
|
...toolActivities,
|
|
98692
|
+
...worktreeLaunchActivities,
|
|
98429
98693
|
...failureActivities,
|
|
98430
98694
|
...recordedTurns,
|
|
98431
98695
|
...textGenerations
|
|
@@ -106589,6 +106853,39 @@ function loadInitialShellSnapshot(input) {
|
|
|
106589
106853
|
return yield* input.projectionSnapshotQuery.getShellSnapshot();
|
|
106590
106854
|
});
|
|
106591
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
|
+
}
|
|
106592
106889
|
function makeSnapshotThenLiveStream(input) {
|
|
106593
106890
|
return require_Path.unwrap(require_Schema$1.gen(function* () {
|
|
106594
106891
|
const liveEvents = yield* input.subscribeLiveEvents;
|
|
@@ -106815,32 +107112,6 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(r
|
|
|
106815
107112
|
}, normalizedCommand) : (normalizedCommand.type === "thread.turn.interrupt" ? interruptBootstrapTurnStart(orchestrationEngine, normalizedCommand) : orchestrationEngine.dispatch(normalizedCommand)).pipe(require_Schema$1.mapError((cause) => toDispatchCommandError(cause, "Failed to dispatch orchestration command")));
|
|
106816
107113
|
return startup.enqueueCommand(dispatchEffect).pipe(require_Schema$1.mapError((cause) => toDispatchCommandError(cause, "Failed to dispatch orchestration command")));
|
|
106817
107114
|
};
|
|
106818
|
-
/**
|
|
106819
|
-
* The thread snapshot a subscription falls back to, windowed when the
|
|
106820
|
-
* subscriber asked for a window. A missing thread is a failure rather
|
|
106821
|
-
* than an empty snapshot: the subscriber asked about a thread this server
|
|
106822
|
-
* does not have, and saying so is the only honest answer.
|
|
106823
|
-
*/
|
|
106824
|
-
const loadThreadDetailSnapshot = (threadId, turnLimit) => require_Schema$1.gen(function* () {
|
|
106825
|
-
const [threadDetail, snapshotSequence] = yield* require_Schema$1.all([projectionSnapshotQuery.getThreadDetailById(threadId).pipe(require_Schema$1.mapError((cause) => new OrchestrationGetSnapshotError({
|
|
106826
|
-
message: `Failed to load thread ${threadId}`,
|
|
106827
|
-
cause
|
|
106828
|
-
}))), orchestrationEngine.getReadModel().pipe(require_Schema$1.map((readModel) => readModel.snapshotSequence))]);
|
|
106829
|
-
if (require_Schema$1.isNone(threadDetail)) return yield* new OrchestrationGetSnapshotError({
|
|
106830
|
-
message: `Thread ${threadId} was not found`,
|
|
106831
|
-
cause: threadId
|
|
106832
|
-
});
|
|
106833
|
-
const windowed = windowThreadDetail({
|
|
106834
|
-
thread: threadDetail.value,
|
|
106835
|
-
window: turnLimit === void 0 ? {} : { turnLimit },
|
|
106836
|
-
snapshotSequence
|
|
106837
|
-
});
|
|
106838
|
-
return {
|
|
106839
|
-
snapshotSequence,
|
|
106840
|
-
thread: windowed.thread,
|
|
106841
|
-
...windowed.page === void 0 ? {} : { page: windowed.page }
|
|
106842
|
-
};
|
|
106843
|
-
});
|
|
106844
107115
|
const loadServerConfig = require_Schema$1.gen(function* () {
|
|
106845
107116
|
const keybindingsConfig = yield* keybindings.loadConfigState;
|
|
106846
107117
|
const providers = yield* providerRegistry.getProviders;
|
|
@@ -106864,6 +107135,7 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(r
|
|
|
106864
107135
|
},
|
|
106865
107136
|
threadResumeCompletionMarker: true,
|
|
106866
107137
|
threadSnapshotPagination: true,
|
|
107138
|
+
threadDetailPageRpc: true,
|
|
106867
107139
|
observability: {
|
|
106868
107140
|
logsDirectoryPath: config.logsDir,
|
|
106869
107141
|
localTracingEnabled: true,
|
|
@@ -106964,12 +107236,26 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(r
|
|
|
106964
107236
|
[ORCHESTRATION_WS_METHODS.subscribeThread]: (input) => observeRpcStreamEffect(ORCHESTRATION_WS_METHODS.subscribeThread, require_Schema$1.succeed(makeThreadSubscriptionStream(input, {
|
|
106965
107237
|
subscribeLiveEvents: orchestrationEngine.subscribeDomainEvents().pipe(require_Schema$1.map((events) => events.pipe(require_Path.filter(isThreadDetailEventFor(input.threadId))))),
|
|
106966
107238
|
currentSequence: orchestrationEngine.getReadModel().pipe(require_Schema$1.map((readModel) => readModel.snapshotSequence)),
|
|
106967
|
-
loadSnapshot: loadThreadDetailSnapshot(
|
|
107239
|
+
loadSnapshot: loadThreadDetailSnapshot({
|
|
107240
|
+
threadId: input.threadId,
|
|
107241
|
+
window: input.turnLimit === void 0 ? {} : { turnLimit: input.turnLimit },
|
|
107242
|
+
projectionSnapshotQuery,
|
|
107243
|
+
orchestrationEngine
|
|
107244
|
+
}),
|
|
106968
107245
|
replayEvents: (afterSequence) => orchestrationEngine.readEvents(afterSequence).pipe(require_Path.filter(isThreadDetailEventFor(input.threadId)), require_Path.mapError((cause) => new OrchestrationGetSnapshotError({
|
|
106969
107246
|
message: `Failed to replay thread ${input.threadId}`,
|
|
106970
107247
|
cause
|
|
106971
107248
|
})))
|
|
106972
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
|
|
107258
|
+
}), { "rpc.aggregate": "orchestration" }),
|
|
106973
107259
|
[WS_METHODS.serverGetConfig]: (_input) => observeRpcEffect(WS_METHODS.serverGetConfig, loadServerConfig, { "rpc.aggregate": "server" }),
|
|
106974
107260
|
[WS_METHODS.serverRefreshProviders]: (_input) => observeRpcEffect(WS_METHODS.serverRefreshProviders, providerRegistry.refresh().pipe(require_Schema$1.map((providers) => ({ providers }))), { "rpc.aggregate": "server" }),
|
|
106975
107261
|
[WS_METHODS.serverStartProviderAuth]: (input) => observeRpcEffect(WS_METHODS.serverStartProviderAuth, require_Schema$1.gen(function* () {
|
|
@@ -213039,6 +213325,8 @@ const PUBLIC_PROVIDER_LEAK_PATTERNS = [
|
|
|
213039
213325
|
/\bCURSOR_API_KEY\b/,
|
|
213040
213326
|
/\bOPENCODE_API_KEY\b/,
|
|
213041
213327
|
/\bforge_proxy_[A-Za-z0-9._-]+\b/,
|
|
213328
|
+
/\bProviderValidationError\b/,
|
|
213329
|
+
/\/forge-server\/node_modules\//,
|
|
213042
213330
|
/https?:\/\/[^\s'"<>)]*\/api\/provider-egress\/[^\s'"<>)]*/i,
|
|
213043
213331
|
/https?:\/\/[^\s'"<>)]*(?:apex|deepseek|moonshot|kimi)[^\s'"<>)]*/i
|
|
213044
213332
|
];
|
|
@@ -213048,6 +213336,12 @@ function isProviderRateOrQuotaError(message) {
|
|
|
213048
213336
|
const normalized = message.toLowerCase();
|
|
213049
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);
|
|
213050
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
|
+
}
|
|
213051
213345
|
/**
|
|
213052
213346
|
* Infer a SAFE, clearer failure category from a (possibly leaky) provider error.
|
|
213053
213347
|
*
|
|
@@ -213060,7 +213354,8 @@ function isProviderRateOrQuotaError(message) {
|
|
|
213060
213354
|
function categorizePublicProviderError(message) {
|
|
213061
213355
|
if (!message) return null;
|
|
213062
213356
|
const m = message.toLowerCase();
|
|
213063
|
-
|
|
213357
|
+
const lifecycleOrAccountCopy = providerLifecycleOrAccountCopy(m);
|
|
213358
|
+
if (lifecycleOrAccountCopy !== null) return lifecycleOrAccountCopy;
|
|
213064
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.";
|
|
213065
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.";
|
|
213066
213361
|
if (isProviderRateOrQuotaError(m)) return "The AI service is rate-limited or out of quota. Wait a moment and retry.";
|
|
@@ -213240,13 +213535,20 @@ function claudeConfigDirectory(env) {
|
|
|
213240
213535
|
return null;
|
|
213241
213536
|
}
|
|
213242
213537
|
}
|
|
213243
|
-
/**
|
|
213244
|
-
|
|
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) {
|
|
213245
213546
|
const directory = claudeConfigDirectory(env);
|
|
213246
|
-
if (!directory
|
|
213547
|
+
if (!directory) return false;
|
|
213247
213548
|
try {
|
|
213248
|
-
const
|
|
213249
|
-
|
|
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");
|
|
213250
213552
|
} catch {
|
|
213251
213553
|
return false;
|
|
213252
213554
|
}
|
|
@@ -215636,6 +215938,7 @@ function buildClaudeSessionContext(input) {
|
|
|
215636
215938
|
pendingUserInputs: input.pendingUserInputs,
|
|
215637
215939
|
turns: [],
|
|
215638
215940
|
inFlightTools: input.inFlightTools,
|
|
215941
|
+
taskPlan: makeClaudeTaskPlanState(),
|
|
215639
215942
|
turnState: void 0,
|
|
215640
215943
|
lastKnownContextWindow: void 0,
|
|
215641
215944
|
lastKnownTokenUsage: void 0,
|
|
@@ -216145,6 +216448,7 @@ function classifyToolItemType(toolName) {
|
|
|
216145
216448
|
if (normalized.includes("agent")) return "collab_agent_tool_call";
|
|
216146
216449
|
if (normalized === "task" || normalized === "agent" || normalized.includes("subagent") || normalized.includes("sub-agent")) return "collab_agent_tool_call";
|
|
216147
216450
|
if (normalized.includes("bash") || normalized.includes("command") || normalized.includes("shell") || normalized.includes("terminal")) return "command_execution";
|
|
216451
|
+
if (isTaskBookkeepingTool(normalized)) return "dynamic_tool_call";
|
|
216148
216452
|
if (normalized.includes("edit") || normalized.includes("write") || normalized.includes("file") || normalized.includes("patch") || normalized.includes("replace") || normalized.includes("create") || normalized.includes("delete")) return "file_change";
|
|
216149
216453
|
if (normalized.includes("browser")) return "browser_automation";
|
|
216150
216454
|
if (normalized.includes("mcp")) return "mcp_tool_call";
|
|
@@ -216164,6 +216468,88 @@ function classifyRequestType(toolName) {
|
|
|
216164
216468
|
function isTodoTool(toolName) {
|
|
216165
216469
|
return toolName.toLowerCase().includes("todowrite");
|
|
216166
216470
|
}
|
|
216471
|
+
function makeClaudeTaskPlanState() {
|
|
216472
|
+
return {
|
|
216473
|
+
entries: [],
|
|
216474
|
+
createsByToolUseId: /* @__PURE__ */ new Map()
|
|
216475
|
+
};
|
|
216476
|
+
}
|
|
216477
|
+
function taskPlanToolFor(toolName) {
|
|
216478
|
+
const normalized = toolName.toLowerCase();
|
|
216479
|
+
if (normalized === "taskcreate") return "TaskCreate";
|
|
216480
|
+
if (normalized === "taskupdate") return "TaskUpdate";
|
|
216481
|
+
}
|
|
216482
|
+
/** Task bookkeeping tools (create, update, list, get) are not file changes. */
|
|
216483
|
+
function isTaskBookkeepingTool(normalizedToolName) {
|
|
216484
|
+
return /^task(create|update|list|get)$/.test(normalizedToolName);
|
|
216485
|
+
}
|
|
216486
|
+
function taskPlanStatus(value) {
|
|
216487
|
+
return value === "completed" ? "completed" : value === "in_progress" ? "inProgress" : "pending";
|
|
216488
|
+
}
|
|
216489
|
+
function taskPlanSnapshot(state) {
|
|
216490
|
+
return state.entries.map((entry) => ({
|
|
216491
|
+
step: entry.step,
|
|
216492
|
+
status: entry.status
|
|
216493
|
+
}));
|
|
216494
|
+
}
|
|
216495
|
+
/**
|
|
216496
|
+
* Applies one TaskCreate or TaskUpdate input to the session's task list and
|
|
216497
|
+
* returns the plan to publish, or null when the input carries nothing yet.
|
|
216498
|
+
* Input JSON streams in pieces, so the same tool use can arrive more than once;
|
|
216499
|
+
* creates are keyed by tool use id so a repeat updates the entry in place.
|
|
216500
|
+
*/
|
|
216501
|
+
function applyTaskPlanToolInput(state, tool, toolUseId, input) {
|
|
216502
|
+
if (tool === "TaskCreate") {
|
|
216503
|
+
const subject = typeof input.subject === "string" ? input.subject.trim() : "";
|
|
216504
|
+
const existing = state.createsByToolUseId.get(toolUseId);
|
|
216505
|
+
if (existing) {
|
|
216506
|
+
if (subject.length > 0) existing.step = subject;
|
|
216507
|
+
return taskPlanSnapshot(state);
|
|
216508
|
+
}
|
|
216509
|
+
const entry = {
|
|
216510
|
+
id: void 0,
|
|
216511
|
+
step: subject.length > 0 ? subject : "Task",
|
|
216512
|
+
status: taskPlanStatus(input.status)
|
|
216513
|
+
};
|
|
216514
|
+
state.entries.push(entry);
|
|
216515
|
+
state.createsByToolUseId.set(toolUseId, entry);
|
|
216516
|
+
return taskPlanSnapshot(state);
|
|
216517
|
+
}
|
|
216518
|
+
const taskId = typeof input.taskId === "string" ? input.taskId.trim() : typeof input.taskId === "number" ? String(input.taskId) : "";
|
|
216519
|
+
if (taskId.length === 0) return null;
|
|
216520
|
+
let entry = state.entries.find((candidate) => candidate.id === taskId);
|
|
216521
|
+
if (!entry) {
|
|
216522
|
+
const position = Number.parseInt(taskId, 10);
|
|
216523
|
+
const candidate = Number.isInteger(position) ? state.entries[position - 1] : void 0;
|
|
216524
|
+
if (candidate && candidate.id === void 0) {
|
|
216525
|
+
candidate.id = taskId;
|
|
216526
|
+
entry = candidate;
|
|
216527
|
+
}
|
|
216528
|
+
}
|
|
216529
|
+
if (input.status === "deleted") {
|
|
216530
|
+
if (entry) state.entries.splice(state.entries.indexOf(entry), 1);
|
|
216531
|
+
return taskPlanSnapshot(state);
|
|
216532
|
+
}
|
|
216533
|
+
if (!entry) {
|
|
216534
|
+
entry = {
|
|
216535
|
+
id: taskId,
|
|
216536
|
+
step: `Task #${taskId}`,
|
|
216537
|
+
status: "pending"
|
|
216538
|
+
};
|
|
216539
|
+
state.entries.push(entry);
|
|
216540
|
+
}
|
|
216541
|
+
if (typeof input.subject === "string" && input.subject.trim().length > 0) entry.step = input.subject.trim();
|
|
216542
|
+
if (input.status !== void 0) entry.status = taskPlanStatus(input.status);
|
|
216543
|
+
return taskPlanSnapshot(state);
|
|
216544
|
+
}
|
|
216545
|
+
const TASK_CREATE_RESULT_ID = /Task #(\S+) created/;
|
|
216546
|
+
/** Binds the id Claude Code assigned in the TaskCreate result to the entry. */
|
|
216547
|
+
function bindTaskPlanCreateResult(state, toolUseId, resultText) {
|
|
216548
|
+
const entry = state.createsByToolUseId.get(toolUseId);
|
|
216549
|
+
if (!entry || entry.id !== void 0) return;
|
|
216550
|
+
const match = TASK_CREATE_RESULT_ID.exec(resultText);
|
|
216551
|
+
if (match?.[1]) entry.id = match[1];
|
|
216552
|
+
}
|
|
216167
216553
|
function extractPlanStepsFromTodoInput(input) {
|
|
216168
216554
|
const todos = input.todos;
|
|
216169
216555
|
if (!Array.isArray(todos) || todos.length === 0) return null;
|
|
@@ -216881,9 +217267,9 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
|
|
|
216881
217267
|
}
|
|
216882
217268
|
});
|
|
216883
217269
|
});
|
|
216884
|
-
const emitPlanUpdateFromToolInput = require_Schema$1.fn("emitPlanUpdateFromToolInput")(function* (context, toolName, parsedInput) {
|
|
216885
|
-
|
|
216886
|
-
const planSteps = extractPlanStepsFromTodoInput(parsedInput);
|
|
217270
|
+
const emitPlanUpdateFromToolInput = require_Schema$1.fn("emitPlanUpdateFromToolInput")(function* (context, toolName, toolUseId, parsedInput) {
|
|
217271
|
+
const taskPlanTool = taskPlanToolFor(toolName);
|
|
217272
|
+
const planSteps = taskPlanTool ? applyTaskPlanToolInput(context.taskPlan, taskPlanTool, toolUseId, parsedInput) : isTodoTool(toolName) ? extractPlanStepsFromTodoInput(parsedInput) : null;
|
|
216887
217273
|
if (!planSteps || planSteps.length === 0) return;
|
|
216888
217274
|
const planStamp = yield* makeEventStamp();
|
|
216889
217275
|
yield* offerRuntimeEvent({
|
|
@@ -216941,7 +217327,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
|
|
|
216941
217327
|
};
|
|
216942
217328
|
context.inFlightTools.set(event.index, nextTool);
|
|
216943
217329
|
yield* emitToolInputUpdated(context, message, nextTool);
|
|
216944
|
-
if (state.parsedInput) yield* emitPlanUpdateFromToolInput(context, nextTool.toolName, state.parsedInput);
|
|
217330
|
+
if (state.parsedInput) yield* emitPlanUpdateFromToolInput(context, nextTool.toolName, nextTool.itemId, state.parsedInput);
|
|
216945
217331
|
});
|
|
216946
217332
|
const handleContentBlockDeltaEvent = require_Schema$1.fn("handleContentBlockDeltaEvent")(function* (context, message, event) {
|
|
216947
217333
|
yield* handleTextOrThinkingDelta(context, message, event);
|
|
@@ -216994,6 +217380,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
|
|
|
216994
217380
|
if (!toolEntry) continue;
|
|
216995
217381
|
const [index, tool] = toolEntry;
|
|
216996
217382
|
const itemStatus = toolResult.isError ? "failed" : "completed";
|
|
217383
|
+
if (taskPlanToolFor(tool.toolName) === "TaskCreate" && !toolResult.isError) bindTaskPlanCreateResult(context.taskPlan, tool.itemId, toolResult.text);
|
|
216997
217384
|
const toolData = {
|
|
216998
217385
|
toolName: tool.toolName,
|
|
216999
217386
|
input: tool.input,
|
|
@@ -217775,7 +218162,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
|
|
|
217775
218162
|
baseDir: serverConfig.baseDir,
|
|
217776
218163
|
accountIdentity: selectedAccountIdentity,
|
|
217777
218164
|
probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support,
|
|
217778
|
-
primaryCredentialMatchesSelected: preparedCredential?.accountKind === "primary" &&
|
|
218165
|
+
primaryCredentialMatchesSelected: preparedCredential?.accountKind === "primary" && primaryClaudeCredentialMatchesPrepared(process.env, preparedCredential.directory)
|
|
217779
218166
|
});
|
|
217780
218167
|
if (credentialDirectoryDecision.outcome === "refused") return yield* new ProviderAdapterValidationError({
|
|
217781
218168
|
provider: PROVIDER$11,
|
|
@@ -232944,7 +233331,7 @@ async function acquireSelectedClaudeCredential(expected, transport) {
|
|
|
232944
233331
|
const claims = token === null ? null : verifyCmmdClaudeSelectedTicket(token, expected);
|
|
232945
233332
|
if (!token || !claims) throw new Error("invalid_ticket");
|
|
232946
233333
|
const credentialJson = await transport.readCredential(token);
|
|
232947
|
-
if (Buffer.byteLength(credentialJson, "utf8") > 512e3 ||
|
|
233334
|
+
if (Buffer.byteLength(credentialJson, "utf8") > 512e3 || !hasUsableClaudeCredentialBlob(credentialJson)) throw new Error("invalid_credential");
|
|
232948
233335
|
return {
|
|
232949
233336
|
claims,
|
|
232950
233337
|
credentialJson
|
|
@@ -292168,6 +292555,10 @@ const make$8 = require_Schema$1.gen(function* () {
|
|
|
292168
292555
|
shouldRestartForAccountChange,
|
|
292169
292556
|
hasResumeCursor: resumeCursor !== void 0
|
|
292170
292557
|
});
|
|
292558
|
+
if (shouldRestartForAccountChange) yield* providerService.stopSession({
|
|
292559
|
+
threadId,
|
|
292560
|
+
preserveResumeState: true
|
|
292561
|
+
});
|
|
292171
292562
|
const restartedSession = yield* startProviderSession(resumeCursor !== void 0 ? { resumeCursor } : void 0);
|
|
292172
292563
|
yield* require_Schema$1.logInfo("provider command reactor restarted provider session", {
|
|
292173
292564
|
threadId,
|
|
@@ -298152,7 +298543,7 @@ function resolveBuildCommitFromEnv(env) {
|
|
|
298152
298543
|
* environment descriptor down instead of reporting an honest "unknown".
|
|
298153
298544
|
*/
|
|
298154
298545
|
function readBakedBuildCommit() {
|
|
298155
|
-
return "
|
|
298546
|
+
return "dea91e954d3d2f23154802ae304412fabe90dbbd";
|
|
298156
298547
|
}
|
|
298157
298548
|
async function resolveServerBuildCommit(input) {
|
|
298158
298549
|
if (isFullCommitSha(input.baked)) return input.baked;
|
|
@@ -299577,20 +299968,20 @@ const generatePairingToken = () => {
|
|
|
299577
299968
|
const randomBytes = crypto.getRandomValues(new Uint8Array(PAIRING_TOKEN_LENGTH));
|
|
299578
299969
|
return Array.from(randomBytes, (value) => PAIRING_TOKEN_ALPHABET[value & 31]).join("");
|
|
299579
299970
|
};
|
|
299971
|
+
const invalidBootstrapCredentialError = (message) => new BootstrapCredentialError({
|
|
299972
|
+
message,
|
|
299973
|
+
status: 401
|
|
299974
|
+
});
|
|
299975
|
+
const internalBootstrapCredentialError = (message, cause) => new BootstrapCredentialError({
|
|
299976
|
+
message,
|
|
299977
|
+
status: 500,
|
|
299978
|
+
cause
|
|
299979
|
+
});
|
|
299580
299980
|
const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
299581
299981
|
const config = yield* ServerConfig;
|
|
299582
299982
|
const pairingLinks = yield* AuthPairingLinkRepository;
|
|
299583
299983
|
const seededGrantsRef = yield* require_HttpServer.make$3(/* @__PURE__ */ new Map());
|
|
299584
299984
|
const changesPubSub = yield* require_Path.unbounded$1();
|
|
299585
|
-
const invalidBootstrapCredentialError = (message) => new BootstrapCredentialError({
|
|
299586
|
-
message,
|
|
299587
|
-
status: 401
|
|
299588
|
-
});
|
|
299589
|
-
const internalBootstrapCredentialError = (message, cause) => new BootstrapCredentialError({
|
|
299590
|
-
message,
|
|
299591
|
-
status: 500,
|
|
299592
|
-
cause
|
|
299593
|
-
});
|
|
299594
299985
|
const seedGrant = (credential, grant) => require_HttpServer.update(seededGrantsRef, (current) => {
|
|
299595
299986
|
const next = new Map(current);
|
|
299596
299987
|
next.set(credential, grant);
|
|
@@ -299605,19 +299996,19 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
299605
299996
|
id
|
|
299606
299997
|
}).pipe(require_Schema$1.asVoid);
|
|
299607
299998
|
if (config.desktopBootstrapToken) {
|
|
299608
|
-
const now$
|
|
299999
|
+
const now$3 = yield* require_Schema$1.now;
|
|
299609
300000
|
yield* seedGrant(config.desktopBootstrapToken, {
|
|
299610
300001
|
method: "desktop-bootstrap",
|
|
299611
300002
|
role: "owner",
|
|
299612
300003
|
subject: "desktop-bootstrap",
|
|
299613
|
-
expiresAt: require_Schema$1.add(now$
|
|
300004
|
+
expiresAt: require_Schema$1.add(now$3, { milliseconds: require_Schema$1.toMillis(DESKTOP_BOOTSTRAP_TOKEN_TTL) }),
|
|
299614
300005
|
remainingUses: "unbounded"
|
|
299615
300006
|
});
|
|
299616
300007
|
}
|
|
299617
300008
|
const toBootstrapCredentialError = (message) => (cause) => internalBootstrapCredentialError(message, cause);
|
|
299618
300009
|
const listActive = () => require_Schema$1.gen(function* () {
|
|
299619
|
-
const now$
|
|
299620
|
-
return (yield* pairingLinks.listActive({ now: now$
|
|
300010
|
+
const now$2 = yield* require_Schema$1.now;
|
|
300011
|
+
return (yield* pairingLinks.listActive({ now: now$2 })).map((row) => row.label ? {
|
|
299621
300012
|
id: row.id,
|
|
299622
300013
|
credential: row.credential,
|
|
299623
300014
|
role: row.role,
|
|
@@ -299647,8 +300038,8 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
299647
300038
|
const id = crypto.randomUUID();
|
|
299648
300039
|
const credential = generatePairingToken();
|
|
299649
300040
|
const ttl = input?.ttl ?? DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES;
|
|
299650
|
-
const now$
|
|
299651
|
-
const expiresAt = require_Schema$1.add(now$
|
|
300041
|
+
const now$5 = yield* require_Schema$1.now;
|
|
300042
|
+
const expiresAt = require_Schema$1.add(now$5, { milliseconds: require_Schema$1.toMillis(ttl) });
|
|
299652
300043
|
const issued = {
|
|
299653
300044
|
id,
|
|
299654
300045
|
credential,
|
|
@@ -299662,7 +300053,7 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
299662
300053
|
role: input?.role ?? "client",
|
|
299663
300054
|
subject: input?.subject ?? "one-time-token",
|
|
299664
300055
|
label: input?.label ?? null,
|
|
299665
|
-
createdAt: now$
|
|
300056
|
+
createdAt: now$5,
|
|
299666
300057
|
expiresAt
|
|
299667
300058
|
});
|
|
299668
300059
|
yield* emitUpsert({
|
|
@@ -299671,13 +300062,13 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
299671
300062
|
role: input?.role ?? "client",
|
|
299672
300063
|
subject: input?.subject ?? "one-time-token",
|
|
299673
300064
|
...input?.label ? { label: input.label } : {},
|
|
299674
|
-
createdAt: now$
|
|
300065
|
+
createdAt: now$5,
|
|
299675
300066
|
expiresAt
|
|
299676
300067
|
});
|
|
299677
300068
|
return issued;
|
|
299678
300069
|
}).pipe(require_Schema$1.mapError(toBootstrapCredentialError("Failed to issue pairing credential.")));
|
|
299679
300070
|
const consume = (credential) => require_Schema$1.gen(function* () {
|
|
299680
|
-
const now$
|
|
300071
|
+
const now$4 = yield* require_Schema$1.now;
|
|
299681
300072
|
const seededResult = yield* require_HttpServer.modify(seededGrantsRef, (current) => {
|
|
299682
300073
|
const grant = current.get(credential);
|
|
299683
300074
|
if (!grant) return [{
|
|
@@ -299686,7 +300077,7 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
299686
300077
|
error: invalidBootstrapCredentialError("Unknown bootstrap credential.")
|
|
299687
300078
|
}, current];
|
|
299688
300079
|
const next = new Map(current);
|
|
299689
|
-
if (require_Schema$1.isGreaterThanOrEqualTo$1(now$
|
|
300080
|
+
if (require_Schema$1.isGreaterThanOrEqualTo$1(now$4, grant.expiresAt)) {
|
|
299690
300081
|
next.delete(credential);
|
|
299691
300082
|
return [{
|
|
299692
300083
|
_tag: "error",
|
|
@@ -299715,8 +300106,8 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
299715
300106
|
if (seededResult.reason !== "not-found") return yield* seededResult.error;
|
|
299716
300107
|
const consumed = yield* pairingLinks.consumeAvailable({
|
|
299717
300108
|
credential,
|
|
299718
|
-
consumedAt: now$
|
|
299719
|
-
now: now$
|
|
300109
|
+
consumedAt: now$4,
|
|
300110
|
+
now: now$4
|
|
299720
300111
|
});
|
|
299721
300112
|
if (require_Schema$1.isSome(consumed)) {
|
|
299722
300113
|
yield* emitRemoved(consumed.value.id);
|
|
@@ -299732,9 +300123,9 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
299732
300123
|
if (require_Schema$1.isNone(matching)) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
|
|
299733
300124
|
if (matching.value.revokedAt !== null) return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
|
|
299734
300125
|
if (matching.value.consumedAt !== null) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
|
|
299735
|
-
if (require_Schema$1.isGreaterThanOrEqualTo$1(now$
|
|
300126
|
+
if (require_Schema$1.isGreaterThanOrEqualTo$1(now$4, matching.value.expiresAt)) return yield* invalidBootstrapCredentialError("Bootstrap credential expired.");
|
|
299736
300127
|
return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
|
|
299737
|
-
}).pipe(require_Schema$1.mapError((cause) => cause instanceof BootstrapCredentialError ? cause : internalBootstrapCredentialError("Failed to consume bootstrap credential.", cause)));
|
|
300128
|
+
}).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_));
|
|
299738
300129
|
return {
|
|
299739
300130
|
issueOneTimeToken,
|
|
299740
300131
|
listActive,
|
|
@@ -299790,6 +300181,10 @@ const SetAuthSessionLastConnectedAtInput = require_Schema$1.Struct({
|
|
|
299790
300181
|
sessionId: AuthSessionId,
|
|
299791
300182
|
lastConnectedAt: require_Schema$1.DateTimeUtcFromString
|
|
299792
300183
|
});
|
|
300184
|
+
const ExtendAuthSessionExpiryInput = require_Schema$1.Struct({
|
|
300185
|
+
sessionId: AuthSessionId,
|
|
300186
|
+
expiresAt: require_Schema$1.DateTimeUtcFromString
|
|
300187
|
+
});
|
|
299793
300188
|
var AuthSessionRepository = class extends require_Schema$1.Service()("forge/persistence/Services/AuthSessions/AuthSessionRepository") {};
|
|
299794
300189
|
//#endregion
|
|
299795
300190
|
//#region src/persistence/Layers/AuthSessions.ts
|
|
@@ -299941,6 +300336,17 @@ const makeAuthSessionRepository = require_Schema$1.gen(function* () {
|
|
|
299941
300336
|
WHERE session_id = ${sessionId}
|
|
299942
300337
|
AND revoked_at IS NULL
|
|
299943
300338
|
RETURNING session_id AS "sessionId"
|
|
300339
|
+
`
|
|
300340
|
+
});
|
|
300341
|
+
const extendSessionExpiryRows = findAll({
|
|
300342
|
+
Request: ExtendAuthSessionExpiryInput,
|
|
300343
|
+
Result: require_Schema$1.Struct({ sessionId: AuthSessionId }),
|
|
300344
|
+
execute: ({ sessionId, expiresAt }) => sql`
|
|
300345
|
+
UPDATE auth_sessions
|
|
300346
|
+
SET expires_at = ${expiresAt}
|
|
300347
|
+
WHERE session_id = ${sessionId}
|
|
300348
|
+
AND revoked_at IS NULL
|
|
300349
|
+
RETURNING session_id AS "sessionId"
|
|
299944
300350
|
`
|
|
299945
300351
|
});
|
|
299946
300352
|
const revokeOtherSessionRows = findAll({
|
|
@@ -299963,13 +300369,15 @@ const makeAuthSessionRepository = require_Schema$1.gen(function* () {
|
|
|
299963
300369
|
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));
|
|
299964
300370
|
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)));
|
|
299965
300371
|
const setLastConnectedAt = (input) => setLastConnectedAtRow(input).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.setLastConnectedAt:query", "AuthSessionRepository.setLastConnectedAt:encodeRequest")));
|
|
300372
|
+
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));
|
|
299966
300373
|
return {
|
|
299967
300374
|
create,
|
|
299968
300375
|
getById,
|
|
299969
300376
|
listActive,
|
|
299970
300377
|
revoke,
|
|
299971
300378
|
revokeAllExcept,
|
|
299972
|
-
setLastConnectedAt
|
|
300379
|
+
setLastConnectedAt,
|
|
300380
|
+
extendExpiry
|
|
299973
300381
|
};
|
|
299974
300382
|
});
|
|
299975
300383
|
const AuthSessionRepositoryLive = require_Schema$1.effect(AuthSessionRepository, makeAuthSessionRepository);
|
|
@@ -299978,6 +300386,20 @@ const AuthSessionRepositoryLive = require_Schema$1.effect(AuthSessionRepository,
|
|
|
299978
300386
|
const SIGNING_SECRET_NAME = "server-signing-key";
|
|
299979
300387
|
const DEFAULT_SESSION_TTL = require_Schema$1.days(30);
|
|
299980
300388
|
const DEFAULT_WEBSOCKET_TOKEN_TTL = require_Schema$1.minutes(5);
|
|
300389
|
+
/**
|
|
300390
|
+
* Whether a session presented at `nowMs` has used up half its own lifetime.
|
|
300391
|
+
*
|
|
300392
|
+
* A phone that opens the app daily should never hit the 30-day wall, but a
|
|
300393
|
+
* lost phone's session should still die on schedule if it is never presented
|
|
300394
|
+
* again. Sliding the expiry forward only when the session is already more
|
|
300395
|
+
* than half spent gives an active phone many chances to renew before expiry
|
|
300396
|
+
* while keeping a dormant session's remaining lifetime bounded by the last
|
|
300397
|
+
* time it was actually used.
|
|
300398
|
+
*/
|
|
300399
|
+
function isPastHalfLife(input) {
|
|
300400
|
+
const halfLifeMarkMs = input.issuedAtMs + (input.expiresAtMs - input.issuedAtMs) / 2;
|
|
300401
|
+
return input.nowMs >= halfLifeMarkMs;
|
|
300402
|
+
}
|
|
299981
300403
|
const toSessionCredentialError = (message) => (cause) => new SessionCredentialError({
|
|
299982
300404
|
message,
|
|
299983
300405
|
cause
|
|
@@ -300138,6 +300560,42 @@ const makeSessionCredentialService = require_Schema$1.gen(function* () {
|
|
|
300138
300560
|
role: claims.role
|
|
300139
300561
|
};
|
|
300140
300562
|
}).pipe(require_Schema$1.mapError(toSessionCredentialError("Failed to issue session credential.")));
|
|
300563
|
+
/**
|
|
300564
|
+
* Slide a session's expiry forward when the token it just verified is past
|
|
300565
|
+
* half its own lifetime, so a phone that keeps using the app never runs
|
|
300566
|
+
* into the 30-day wall.
|
|
300567
|
+
*
|
|
300568
|
+
* The session id, subject, role and method carry over unchanged — only the
|
|
300569
|
+
* signature-bearing `iat`/`exp` differ — which is what lets the DB row be
|
|
300570
|
+
* extended in place (`extendExpiry`) rather than replaced: everything keyed
|
|
300571
|
+
* by session id, most importantly the `command_access_tokens` row holding
|
|
300572
|
+
* this session's CMMD credential, needs no migration.
|
|
300573
|
+
*/
|
|
300574
|
+
const renewIfPastHalfLife = (input) => require_Schema$1.gen(function* () {
|
|
300575
|
+
const nowMs = yield* require_Schema$1.currentTimeMillis;
|
|
300576
|
+
if (!isPastHalfLife({
|
|
300577
|
+
issuedAtMs: input.row.issuedAt.epochMilliseconds,
|
|
300578
|
+
expiresAtMs: input.row.expiresAt.epochMilliseconds,
|
|
300579
|
+
nowMs
|
|
300580
|
+
})) return void 0;
|
|
300581
|
+
const newIssuedAt = yield* require_Schema$1.now;
|
|
300582
|
+
const newExpiresAt = require_Schema$1.add(newIssuedAt, { milliseconds: require_Schema$1.toMillis(DEFAULT_SESSION_TTL) });
|
|
300583
|
+
const newClaims = {
|
|
300584
|
+
...input.claims,
|
|
300585
|
+
iat: newIssuedAt.epochMilliseconds,
|
|
300586
|
+
exp: newExpiresAt.epochMilliseconds
|
|
300587
|
+
};
|
|
300588
|
+
const newEncodedPayload = base64UrlEncode(JSON.stringify(newClaims));
|
|
300589
|
+
const newSignature = signPayload(newEncodedPayload, signingSecret);
|
|
300590
|
+
if (!(yield* authSessions.extendExpiry({
|
|
300591
|
+
sessionId: input.claims.sid,
|
|
300592
|
+
expiresAt: newExpiresAt
|
|
300593
|
+
}).pipe(require_Schema$1.orElseSucceed(() => false)))) return void 0;
|
|
300594
|
+
return {
|
|
300595
|
+
token: `${newEncodedPayload}.${newSignature}`,
|
|
300596
|
+
expiresAt: newExpiresAt
|
|
300597
|
+
};
|
|
300598
|
+
});
|
|
300141
300599
|
const verify = (token) => require_Schema$1.gen(function* () {
|
|
300142
300600
|
const [encodedPayload, signature] = token.split(".");
|
|
300143
300601
|
if (!encodedPayload || !signature) return yield* new SessionCredentialError({ message: "Malformed session token." });
|
|
@@ -300152,14 +300610,19 @@ const makeSessionCredentialService = require_Schema$1.gen(function* () {
|
|
|
300152
300610
|
if (require_Schema$1.isSome(row)) {
|
|
300153
300611
|
if (row.value.revokedAt !== null) return yield* new SessionCredentialError({ message: "Session token revoked." });
|
|
300154
300612
|
} else if (!(statelessCookieSessions && claims.method === "browser-session-cookie")) return yield* new SessionCredentialError({ message: "Unknown session token." });
|
|
300613
|
+
const renewed = require_Schema$1.isSome(row) ? yield* renewIfPastHalfLife({
|
|
300614
|
+
claims,
|
|
300615
|
+
row: row.value
|
|
300616
|
+
}) : void 0;
|
|
300155
300617
|
return {
|
|
300156
300618
|
sessionId: claims.sid,
|
|
300157
300619
|
token,
|
|
300158
300620
|
method: claims.method,
|
|
300159
300621
|
client: require_Schema$1.isSome(row) ? toClientMetadata(row.value.client) : createDefaultClientMetadata(),
|
|
300160
|
-
expiresAt: require_Schema$1.makeUnsafe(claims.exp),
|
|
300622
|
+
expiresAt: renewed?.expiresAt ?? require_Schema$1.makeUnsafe(claims.exp),
|
|
300161
300623
|
subject: claims.sub,
|
|
300162
|
-
role: claims.role
|
|
300624
|
+
role: claims.role,
|
|
300625
|
+
...renewed ? { renewed } : {}
|
|
300163
300626
|
};
|
|
300164
300627
|
}).pipe(require_Schema$1.mapError((cause) => cause instanceof SessionCredentialError ? cause : new SessionCredentialError({
|
|
300165
300628
|
message: "Failed to verify session credential.",
|
|
@@ -300652,7 +301115,10 @@ const makeServerAuth = require_Schema$1.gen(function* () {
|
|
|
300652
301115
|
subject: session.subject,
|
|
300653
301116
|
method: session.method,
|
|
300654
301117
|
role: session.role,
|
|
300655
|
-
...session.
|
|
301118
|
+
...session.renewed ? {
|
|
301119
|
+
expiresAt: session.renewed.expiresAt,
|
|
301120
|
+
renewedSessionToken: session.renewed.token
|
|
301121
|
+
} : session.expiresAt ? { expiresAt: session.expiresAt } : {}
|
|
300656
301122
|
})), require_Schema$1.mapError((cause) => new AuthError({
|
|
300657
301123
|
message: "Unauthorized request.",
|
|
300658
301124
|
status: 401,
|
|
@@ -300687,6 +301153,7 @@ const makeServerAuth = require_Schema$1.gen(function* () {
|
|
|
300687
301153
|
role: session.role,
|
|
300688
301154
|
sessionMethod: session.method,
|
|
300689
301155
|
...session.expiresAt ? { expiresAt: require_Schema$1.toUtc(session.expiresAt) } : {},
|
|
301156
|
+
...session.renewedSessionToken ? { renewedSessionToken: session.renewedSessionToken } : {},
|
|
300690
301157
|
cmmdIdentity
|
|
300691
301158
|
})))), require_Schema$1.tapError((e) => {
|
|
300692
301159
|
if (e._tag === "AuthError" && (e.status ?? 500) < 500) return require_Schema$1.void_;
|
|
@@ -319052,7 +319519,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
|
|
|
319052
319519
|
const sql = yield* require_SqlError.SqlClient;
|
|
319053
319520
|
const fetchFn = options.fetchFn ?? fetch;
|
|
319054
319521
|
const baseUrl = options.baseUrl ?? resolveCommandAppUrl();
|
|
319055
|
-
const resolveStoredJwt = (forceRefresh) => require_Schema$1.gen(function* () {
|
|
319522
|
+
const resolveStoredJwt = (forceRefresh, staleJwt) => require_Schema$1.gen(function* () {
|
|
319056
319523
|
const owner = yield* readMostRecentOwnerCommandAuthSession.pipe(require_Schema$1.provideService(require_SqlError.SqlClient, sql), require_Schema$1.mapError((cause) => new ProjectLeadControllerClientError({
|
|
319057
319524
|
kind: "unavailable",
|
|
319058
319525
|
operation: "resolve-owner-session",
|
|
@@ -319066,7 +319533,8 @@ function makeProjectLeadControllerClientLive(options = {}) {
|
|
|
319066
319533
|
sessionId: owner.sessionId,
|
|
319067
319534
|
subject: owner.subject,
|
|
319068
319535
|
logPrefix: "project-lead-scheduler",
|
|
319069
|
-
forceRefresh
|
|
319536
|
+
forceRefresh,
|
|
319537
|
+
staleAccessToken: staleJwt
|
|
319070
319538
|
}).pipe(require_Schema$1.provideService(require_SqlError.SqlClient, sql));
|
|
319071
319539
|
if (!jwt) return yield* new ProjectLeadControllerClientError({
|
|
319072
319540
|
kind: "unauthenticated",
|
|
@@ -319109,7 +319577,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
|
|
|
319109
319577
|
}));
|
|
319110
319578
|
if (first._tag === "Success") return first.success;
|
|
319111
319579
|
if (first.failure.kind !== "unauthenticated") return yield* first.failure;
|
|
319112
|
-
const refreshed = yield* resolveJwt(true);
|
|
319580
|
+
const refreshed = yield* resolveJwt(true, jwt);
|
|
319113
319581
|
return yield* requestAttempt({
|
|
319114
319582
|
...input,
|
|
319115
319583
|
jwt: refreshed
|