@cmmd-center/forge 0.13.12 → 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/bin.cjs +454 -71
- package/dist/bin.mjs +478 -95
- 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-cwm7LEtZ.js} +2 -2
- package/dist/client/assets/{DiffPanel.logic-DWcwE9Wj.js → DiffPanel.logic-B9AWYFxE.js} +2 -2
- package/dist/client/assets/{DiffPanelShell-oXHh--ae.js → DiffPanelShell-BhokrrDZ.js} +103 -103
- package/dist/client/assets/{DiffWorkerPoolProvider-R9YPaaNy.js → DiffWorkerPoolProvider-BZjEmEzL.js} +2 -2
- package/dist/client/assets/{PullRequestCodePanel-DTCc2TBl.js → PullRequestCodePanel-BPJINzCp.js} +2 -2
- package/dist/client/assets/{Virtualizer-arKGxq_z.js → Virtualizer-i6HiQ7Qm.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-DMWfw0Ih.js +154 -0
- package/dist/client/assets/index-dEPiAWVG.css +1 -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/dist/{open-D8mGyWmt.mjs → open-FIgTDosf.mjs} +3 -3
- 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.13";
|
|
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
|
}
|
|
@@ -217775,7 +218077,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
|
|
|
217775
218077
|
baseDir: serverConfig.baseDir,
|
|
217776
218078
|
accountIdentity: selectedAccountIdentity,
|
|
217777
218079
|
probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support,
|
|
217778
|
-
primaryCredentialMatchesSelected: preparedCredential?.accountKind === "primary" &&
|
|
218080
|
+
primaryCredentialMatchesSelected: preparedCredential?.accountKind === "primary" && primaryClaudeCredentialMatchesPrepared(process.env, preparedCredential.directory)
|
|
217779
218081
|
});
|
|
217780
218082
|
if (credentialDirectoryDecision.outcome === "refused") return yield* new ProviderAdapterValidationError({
|
|
217781
218083
|
provider: PROVIDER$11,
|
|
@@ -232944,7 +233246,7 @@ async function acquireSelectedClaudeCredential(expected, transport) {
|
|
|
232944
233246
|
const claims = token === null ? null : verifyCmmdClaudeSelectedTicket(token, expected);
|
|
232945
233247
|
if (!token || !claims) throw new Error("invalid_ticket");
|
|
232946
233248
|
const credentialJson = await transport.readCredential(token);
|
|
232947
|
-
if (Buffer.byteLength(credentialJson, "utf8") > 512e3 ||
|
|
233249
|
+
if (Buffer.byteLength(credentialJson, "utf8") > 512e3 || !hasUsableClaudeCredentialBlob(credentialJson)) throw new Error("invalid_credential");
|
|
232948
233250
|
return {
|
|
232949
233251
|
claims,
|
|
232950
233252
|
credentialJson
|
|
@@ -292168,6 +292470,10 @@ const make$8 = require_Schema$1.gen(function* () {
|
|
|
292168
292470
|
shouldRestartForAccountChange,
|
|
292169
292471
|
hasResumeCursor: resumeCursor !== void 0
|
|
292170
292472
|
});
|
|
292473
|
+
if (shouldRestartForAccountChange) yield* providerService.stopSession({
|
|
292474
|
+
threadId,
|
|
292475
|
+
preserveResumeState: true
|
|
292476
|
+
});
|
|
292171
292477
|
const restartedSession = yield* startProviderSession(resumeCursor !== void 0 ? { resumeCursor } : void 0);
|
|
292172
292478
|
yield* require_Schema$1.logInfo("provider command reactor restarted provider session", {
|
|
292173
292479
|
threadId,
|
|
@@ -298152,7 +298458,7 @@ function resolveBuildCommitFromEnv(env) {
|
|
|
298152
298458
|
* environment descriptor down instead of reporting an honest "unknown".
|
|
298153
298459
|
*/
|
|
298154
298460
|
function readBakedBuildCommit() {
|
|
298155
|
-
return "
|
|
298461
|
+
return "81ce9360f406bcb5ae9fbdc9f90dd55d33f25f0c";
|
|
298156
298462
|
}
|
|
298157
298463
|
async function resolveServerBuildCommit(input) {
|
|
298158
298464
|
if (isFullCommitSha(input.baked)) return input.baked;
|
|
@@ -299577,20 +299883,20 @@ const generatePairingToken = () => {
|
|
|
299577
299883
|
const randomBytes = crypto.getRandomValues(new Uint8Array(PAIRING_TOKEN_LENGTH));
|
|
299578
299884
|
return Array.from(randomBytes, (value) => PAIRING_TOKEN_ALPHABET[value & 31]).join("");
|
|
299579
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
|
+
});
|
|
299580
299895
|
const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
299581
299896
|
const config = yield* ServerConfig;
|
|
299582
299897
|
const pairingLinks = yield* AuthPairingLinkRepository;
|
|
299583
299898
|
const seededGrantsRef = yield* require_HttpServer.make$3(/* @__PURE__ */ new Map());
|
|
299584
299899
|
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
299900
|
const seedGrant = (credential, grant) => require_HttpServer.update(seededGrantsRef, (current) => {
|
|
299595
299901
|
const next = new Map(current);
|
|
299596
299902
|
next.set(credential, grant);
|
|
@@ -299605,19 +299911,19 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
299605
299911
|
id
|
|
299606
299912
|
}).pipe(require_Schema$1.asVoid);
|
|
299607
299913
|
if (config.desktopBootstrapToken) {
|
|
299608
|
-
const now$
|
|
299914
|
+
const now$3 = yield* require_Schema$1.now;
|
|
299609
299915
|
yield* seedGrant(config.desktopBootstrapToken, {
|
|
299610
299916
|
method: "desktop-bootstrap",
|
|
299611
299917
|
role: "owner",
|
|
299612
299918
|
subject: "desktop-bootstrap",
|
|
299613
|
-
expiresAt: require_Schema$1.add(now$
|
|
299919
|
+
expiresAt: require_Schema$1.add(now$3, { milliseconds: require_Schema$1.toMillis(DESKTOP_BOOTSTRAP_TOKEN_TTL) }),
|
|
299614
299920
|
remainingUses: "unbounded"
|
|
299615
299921
|
});
|
|
299616
299922
|
}
|
|
299617
299923
|
const toBootstrapCredentialError = (message) => (cause) => internalBootstrapCredentialError(message, cause);
|
|
299618
299924
|
const listActive = () => require_Schema$1.gen(function* () {
|
|
299619
|
-
const now$
|
|
299620
|
-
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 ? {
|
|
299621
299927
|
id: row.id,
|
|
299622
299928
|
credential: row.credential,
|
|
299623
299929
|
role: row.role,
|
|
@@ -299647,8 +299953,8 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
299647
299953
|
const id = crypto.randomUUID();
|
|
299648
299954
|
const credential = generatePairingToken();
|
|
299649
299955
|
const ttl = input?.ttl ?? DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES;
|
|
299650
|
-
const now$
|
|
299651
|
-
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) });
|
|
299652
299958
|
const issued = {
|
|
299653
299959
|
id,
|
|
299654
299960
|
credential,
|
|
@@ -299662,7 +299968,7 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
299662
299968
|
role: input?.role ?? "client",
|
|
299663
299969
|
subject: input?.subject ?? "one-time-token",
|
|
299664
299970
|
label: input?.label ?? null,
|
|
299665
|
-
createdAt: now$
|
|
299971
|
+
createdAt: now$5,
|
|
299666
299972
|
expiresAt
|
|
299667
299973
|
});
|
|
299668
299974
|
yield* emitUpsert({
|
|
@@ -299671,13 +299977,13 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
299671
299977
|
role: input?.role ?? "client",
|
|
299672
299978
|
subject: input?.subject ?? "one-time-token",
|
|
299673
299979
|
...input?.label ? { label: input.label } : {},
|
|
299674
|
-
createdAt: now$
|
|
299980
|
+
createdAt: now$5,
|
|
299675
299981
|
expiresAt
|
|
299676
299982
|
});
|
|
299677
299983
|
return issued;
|
|
299678
299984
|
}).pipe(require_Schema$1.mapError(toBootstrapCredentialError("Failed to issue pairing credential.")));
|
|
299679
299985
|
const consume = (credential) => require_Schema$1.gen(function* () {
|
|
299680
|
-
const now$
|
|
299986
|
+
const now$4 = yield* require_Schema$1.now;
|
|
299681
299987
|
const seededResult = yield* require_HttpServer.modify(seededGrantsRef, (current) => {
|
|
299682
299988
|
const grant = current.get(credential);
|
|
299683
299989
|
if (!grant) return [{
|
|
@@ -299686,7 +299992,7 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
299686
299992
|
error: invalidBootstrapCredentialError("Unknown bootstrap credential.")
|
|
299687
299993
|
}, current];
|
|
299688
299994
|
const next = new Map(current);
|
|
299689
|
-
if (require_Schema$1.isGreaterThanOrEqualTo$1(now$
|
|
299995
|
+
if (require_Schema$1.isGreaterThanOrEqualTo$1(now$4, grant.expiresAt)) {
|
|
299690
299996
|
next.delete(credential);
|
|
299691
299997
|
return [{
|
|
299692
299998
|
_tag: "error",
|
|
@@ -299715,8 +300021,8 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
299715
300021
|
if (seededResult.reason !== "not-found") return yield* seededResult.error;
|
|
299716
300022
|
const consumed = yield* pairingLinks.consumeAvailable({
|
|
299717
300023
|
credential,
|
|
299718
|
-
consumedAt: now$
|
|
299719
|
-
now: now$
|
|
300024
|
+
consumedAt: now$4,
|
|
300025
|
+
now: now$4
|
|
299720
300026
|
});
|
|
299721
300027
|
if (require_Schema$1.isSome(consumed)) {
|
|
299722
300028
|
yield* emitRemoved(consumed.value.id);
|
|
@@ -299732,9 +300038,9 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
|
|
|
299732
300038
|
if (require_Schema$1.isNone(matching)) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
|
|
299733
300039
|
if (matching.value.revokedAt !== null) return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
|
|
299734
300040
|
if (matching.value.consumedAt !== null) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
|
|
299735
|
-
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.");
|
|
299736
300042
|
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)));
|
|
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_));
|
|
299738
300044
|
return {
|
|
299739
300045
|
issueOneTimeToken,
|
|
299740
300046
|
listActive,
|
|
@@ -299790,6 +300096,10 @@ const SetAuthSessionLastConnectedAtInput = require_Schema$1.Struct({
|
|
|
299790
300096
|
sessionId: AuthSessionId,
|
|
299791
300097
|
lastConnectedAt: require_Schema$1.DateTimeUtcFromString
|
|
299792
300098
|
});
|
|
300099
|
+
const ExtendAuthSessionExpiryInput = require_Schema$1.Struct({
|
|
300100
|
+
sessionId: AuthSessionId,
|
|
300101
|
+
expiresAt: require_Schema$1.DateTimeUtcFromString
|
|
300102
|
+
});
|
|
299793
300103
|
var AuthSessionRepository = class extends require_Schema$1.Service()("forge/persistence/Services/AuthSessions/AuthSessionRepository") {};
|
|
299794
300104
|
//#endregion
|
|
299795
300105
|
//#region src/persistence/Layers/AuthSessions.ts
|
|
@@ -299941,6 +300251,17 @@ const makeAuthSessionRepository = require_Schema$1.gen(function* () {
|
|
|
299941
300251
|
WHERE session_id = ${sessionId}
|
|
299942
300252
|
AND revoked_at IS NULL
|
|
299943
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"
|
|
299944
300265
|
`
|
|
299945
300266
|
});
|
|
299946
300267
|
const revokeOtherSessionRows = findAll({
|
|
@@ -299963,13 +300284,15 @@ const makeAuthSessionRepository = require_Schema$1.gen(function* () {
|
|
|
299963
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));
|
|
299964
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)));
|
|
299965
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));
|
|
299966
300288
|
return {
|
|
299967
300289
|
create,
|
|
299968
300290
|
getById,
|
|
299969
300291
|
listActive,
|
|
299970
300292
|
revoke,
|
|
299971
300293
|
revokeAllExcept,
|
|
299972
|
-
setLastConnectedAt
|
|
300294
|
+
setLastConnectedAt,
|
|
300295
|
+
extendExpiry
|
|
299973
300296
|
};
|
|
299974
300297
|
});
|
|
299975
300298
|
const AuthSessionRepositoryLive = require_Schema$1.effect(AuthSessionRepository, makeAuthSessionRepository);
|
|
@@ -299978,6 +300301,20 @@ const AuthSessionRepositoryLive = require_Schema$1.effect(AuthSessionRepository,
|
|
|
299978
300301
|
const SIGNING_SECRET_NAME = "server-signing-key";
|
|
299979
300302
|
const DEFAULT_SESSION_TTL = require_Schema$1.days(30);
|
|
299980
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
|
+
}
|
|
299981
300318
|
const toSessionCredentialError = (message) => (cause) => new SessionCredentialError({
|
|
299982
300319
|
message,
|
|
299983
300320
|
cause
|
|
@@ -300138,6 +300475,42 @@ const makeSessionCredentialService = require_Schema$1.gen(function* () {
|
|
|
300138
300475
|
role: claims.role
|
|
300139
300476
|
};
|
|
300140
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
|
+
});
|
|
300141
300514
|
const verify = (token) => require_Schema$1.gen(function* () {
|
|
300142
300515
|
const [encodedPayload, signature] = token.split(".");
|
|
300143
300516
|
if (!encodedPayload || !signature) return yield* new SessionCredentialError({ message: "Malformed session token." });
|
|
@@ -300152,14 +300525,19 @@ const makeSessionCredentialService = require_Schema$1.gen(function* () {
|
|
|
300152
300525
|
if (require_Schema$1.isSome(row)) {
|
|
300153
300526
|
if (row.value.revokedAt !== null) return yield* new SessionCredentialError({ message: "Session token revoked." });
|
|
300154
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;
|
|
300155
300532
|
return {
|
|
300156
300533
|
sessionId: claims.sid,
|
|
300157
300534
|
token,
|
|
300158
300535
|
method: claims.method,
|
|
300159
300536
|
client: require_Schema$1.isSome(row) ? toClientMetadata(row.value.client) : createDefaultClientMetadata(),
|
|
300160
|
-
expiresAt: require_Schema$1.makeUnsafe(claims.exp),
|
|
300537
|
+
expiresAt: renewed?.expiresAt ?? require_Schema$1.makeUnsafe(claims.exp),
|
|
300161
300538
|
subject: claims.sub,
|
|
300162
|
-
role: claims.role
|
|
300539
|
+
role: claims.role,
|
|
300540
|
+
...renewed ? { renewed } : {}
|
|
300163
300541
|
};
|
|
300164
300542
|
}).pipe(require_Schema$1.mapError((cause) => cause instanceof SessionCredentialError ? cause : new SessionCredentialError({
|
|
300165
300543
|
message: "Failed to verify session credential.",
|
|
@@ -300652,7 +301030,10 @@ const makeServerAuth = require_Schema$1.gen(function* () {
|
|
|
300652
301030
|
subject: session.subject,
|
|
300653
301031
|
method: session.method,
|
|
300654
301032
|
role: session.role,
|
|
300655
|
-
...session.
|
|
301033
|
+
...session.renewed ? {
|
|
301034
|
+
expiresAt: session.renewed.expiresAt,
|
|
301035
|
+
renewedSessionToken: session.renewed.token
|
|
301036
|
+
} : session.expiresAt ? { expiresAt: session.expiresAt } : {}
|
|
300656
301037
|
})), require_Schema$1.mapError((cause) => new AuthError({
|
|
300657
301038
|
message: "Unauthorized request.",
|
|
300658
301039
|
status: 401,
|
|
@@ -300687,6 +301068,7 @@ const makeServerAuth = require_Schema$1.gen(function* () {
|
|
|
300687
301068
|
role: session.role,
|
|
300688
301069
|
sessionMethod: session.method,
|
|
300689
301070
|
...session.expiresAt ? { expiresAt: require_Schema$1.toUtc(session.expiresAt) } : {},
|
|
301071
|
+
...session.renewedSessionToken ? { renewedSessionToken: session.renewedSessionToken } : {},
|
|
300690
301072
|
cmmdIdentity
|
|
300691
301073
|
})))), require_Schema$1.tapError((e) => {
|
|
300692
301074
|
if (e._tag === "AuthError" && (e.status ?? 500) < 500) return require_Schema$1.void_;
|
|
@@ -319052,7 +319434,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
|
|
|
319052
319434
|
const sql = yield* require_SqlError.SqlClient;
|
|
319053
319435
|
const fetchFn = options.fetchFn ?? fetch;
|
|
319054
319436
|
const baseUrl = options.baseUrl ?? resolveCommandAppUrl();
|
|
319055
|
-
const resolveStoredJwt = (forceRefresh) => require_Schema$1.gen(function* () {
|
|
319437
|
+
const resolveStoredJwt = (forceRefresh, staleJwt) => require_Schema$1.gen(function* () {
|
|
319056
319438
|
const owner = yield* readMostRecentOwnerCommandAuthSession.pipe(require_Schema$1.provideService(require_SqlError.SqlClient, sql), require_Schema$1.mapError((cause) => new ProjectLeadControllerClientError({
|
|
319057
319439
|
kind: "unavailable",
|
|
319058
319440
|
operation: "resolve-owner-session",
|
|
@@ -319066,7 +319448,8 @@ function makeProjectLeadControllerClientLive(options = {}) {
|
|
|
319066
319448
|
sessionId: owner.sessionId,
|
|
319067
319449
|
subject: owner.subject,
|
|
319068
319450
|
logPrefix: "project-lead-scheduler",
|
|
319069
|
-
forceRefresh
|
|
319451
|
+
forceRefresh,
|
|
319452
|
+
staleAccessToken: staleJwt
|
|
319070
319453
|
}).pipe(require_Schema$1.provideService(require_SqlError.SqlClient, sql));
|
|
319071
319454
|
if (!jwt) return yield* new ProjectLeadControllerClientError({
|
|
319072
319455
|
kind: "unauthenticated",
|
|
@@ -319109,7 +319492,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
|
|
|
319109
319492
|
}));
|
|
319110
319493
|
if (first._tag === "Success") return first.success;
|
|
319111
319494
|
if (first.failure.kind !== "unauthenticated") return yield* first.failure;
|
|
319112
|
-
const refreshed = yield* resolveJwt(true);
|
|
319495
|
+
const refreshed = yield* resolveJwt(true, jwt);
|
|
319113
319496
|
return yield* requestAttempt({
|
|
319114
319497
|
...input,
|
|
319115
319498
|
jwt: refreshed
|