@cmmd-center/forge 0.13.131 → 0.13.133
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 +453 -734
- package/dist/bin.mjs +450 -734
- package/dist/client/assets/{CanvasPanel-BBU-hosR.js → CanvasPanel-BZ55yFCm.js} +2 -2
- package/dist/client/assets/{DiffPanel-CsjpjcNO.js → DiffPanel-RwcR62h1.js} +2 -2
- package/dist/client/assets/{DiffPanel.logic-D0tJmmDH.js → DiffPanel.logic-p0v4whSE.js} +2 -2
- package/dist/client/assets/{DiffWorkerPoolProvider-BaYfOG54.js → DiffWorkerPoolProvider-DuQCCe4H.js} +2 -2
- package/dist/client/assets/{PullRequestCodePanel-FsC6jwvA.js → PullRequestCodePanel-dQ0P1BYv.js} +2 -2
- package/dist/client/assets/{ThemeEditorPanel-DHyAZtjN.js → ThemeEditorPanel-Bgrpa0cR.js} +2 -2
- package/dist/client/assets/{Virtualizer-Bt_R5UQD.js → Virtualizer-B7qA3afH.js} +2 -2
- package/dist/client/assets/{WorkerPoolContext-DSayjMnB.js → WorkerPoolContext-DJIHDFVZ.js} +44 -44
- package/dist/client/assets/cmmdAuthRedirect-B_Ad6N4c.js +3 -0
- package/dist/client/assets/{cmmdForgeRuntimeListResource-DFLs7Ho_.js → cmmdForgeRuntimeListResource-uAVdZfT3.js} +4 -4
- package/dist/client/assets/{currentForgeWorkspaceStore-B5FQv1gc.js → currentForgeWorkspaceStore-DgPGQMqz.js} +2 -2
- package/dist/client/assets/{dialog-mwxYKYq-.js → dialog-DMVibvkf.js} +2 -2
- package/dist/client/assets/index-BSqrLyPh.js +301 -0
- package/dist/client/assets/{popover-BMHhFoom.js → popover-DZpHoNco.js} +2 -2
- package/dist/client/assets/{scroll-area-9mmf9iy-.js → scroll-area-dh4WduWB.js} +2 -2
- package/dist/client/assets/{sidebarV2CustomSnoozeDialog-vLvqIwRU.js → sidebarV2CustomSnoozeDialog-DsUlSvAq.js} +2 -2
- package/dist/client/assets/src-DpAPOFgC.js +3 -0
- package/dist/client/assets/{switch-DpGoyzXB.js → switch-Bt0PG_99.js} +2 -2
- package/dist/client/assets/{themePalette-BII_gGLK.js → themePalette-rKk-Jh5K.js} +2 -2
- package/dist/client/assets/{toggle-CiTk7ztA.js → toggle-CM3FO84q.js} +2 -2
- package/dist/client/assets/{toggle-group-B0GQb1vL.js → toggle-group-sUSKwOOz.js} +2 -2
- package/dist/client/assets/{tooltip-DlvXFVEM.js → tooltip-DHkQTarv.js} +2 -2
- package/dist/client/assets/{utils-BWD4ATf-.js → utils-B9FLzOgF.js} +2 -2
- package/dist/client/index.html +18 -18
- package/dist/forge-build.json +1 -1
- package/package.json +1 -1
- package/dist/client/assets/cmmdAuthRedirect-B2FNK6pk.js +0 -3
- package/dist/client/assets/index-BFu1LcEj.js +0 -301
- package/dist/client/assets/src-DICOHE3E.js +0 -3
package/dist/bin.mjs
CHANGED
|
@@ -20558,6 +20558,42 @@ var FilesystemBrowseError = class extends TaggedErrorClass()("FilesystemBrowseEr
|
|
|
20558
20558
|
}
|
|
20559
20559
|
};
|
|
20560
20560
|
//#endregion
|
|
20561
|
+
//#region ../../packages/contracts/src/workspaceDirectory.ts
|
|
20562
|
+
const WORKSPACE_DIRECTORY_PATH_MAX_LENGTH = 512;
|
|
20563
|
+
/**
|
|
20564
|
+
* One immediate child of a listed workspace directory.
|
|
20565
|
+
*
|
|
20566
|
+
* `hasChildren` is what lets the Files tree draw an expander only on
|
|
20567
|
+
* directories that open onto something, without listing the whole workspace
|
|
20568
|
+
* up front: a directory entry is one stat-sized readdir, not a recursive walk.
|
|
20569
|
+
*/
|
|
20570
|
+
const WorkspaceDirectoryEntry = Struct({
|
|
20571
|
+
name: TrimmedNonEmptyString,
|
|
20572
|
+
kind: Literals(["file", "directory"]),
|
|
20573
|
+
hasChildren: Boolean$1
|
|
20574
|
+
});
|
|
20575
|
+
const WorkspaceListDirectoryInput = Struct({
|
|
20576
|
+
cwd: TrimmedNonEmptyString,
|
|
20577
|
+
relativePath: TrimmedString.check(isMaxLength(WORKSPACE_DIRECTORY_PATH_MAX_LENGTH))
|
|
20578
|
+
});
|
|
20579
|
+
const WorkspaceListDirectoryResult = Struct({ entries: ArraySchema(WorkspaceDirectoryEntry) });
|
|
20580
|
+
const WorkspaceListDirectoryFailure = Literals([
|
|
20581
|
+
"workspace_root_invalid",
|
|
20582
|
+
"path_outside_workspace",
|
|
20583
|
+
"read_directory_failed"
|
|
20584
|
+
]);
|
|
20585
|
+
var WorkspaceListDirectoryError = class extends TaggedErrorClass()("WorkspaceListDirectoryError", {
|
|
20586
|
+
cwd: String$1,
|
|
20587
|
+
relativePath: String$1,
|
|
20588
|
+
failure: WorkspaceListDirectoryFailure,
|
|
20589
|
+
detail: String$1,
|
|
20590
|
+
cause: optional$4(Defect)
|
|
20591
|
+
}) {
|
|
20592
|
+
get message() {
|
|
20593
|
+
return this.detail;
|
|
20594
|
+
}
|
|
20595
|
+
};
|
|
20596
|
+
//#endregion
|
|
20561
20597
|
//#region ../../packages/contracts/src/terminal.ts
|
|
20562
20598
|
const DEFAULT_TERMINAL_ID = "default";
|
|
20563
20599
|
const TrimmedNonEmptyStringSchema$3 = TrimmedNonEmptyString;
|
|
@@ -26009,6 +26045,7 @@ const WS_METHODS = {
|
|
|
26009
26045
|
projectsWriteFile: "projects.writeFile",
|
|
26010
26046
|
projectCheckoutDeletionPreview: "projectCheckoutDeletion.preview",
|
|
26011
26047
|
projectCheckoutDeletionExecute: "projectCheckoutDeletion.execute",
|
|
26048
|
+
workspaceListDirectory: "workspace.listDirectory",
|
|
26012
26049
|
workspaceReadFile: "workspace.readFile",
|
|
26013
26050
|
workspaceBeginWriteFile: "workspace.beginWriteFile",
|
|
26014
26051
|
workspaceWriteFileChunk: "workspace.writeFileChunk",
|
|
@@ -26320,6 +26357,17 @@ const WsWorkspaceCancelWriteFileRpc = make$29(WS_METHODS.workspaceCancelWriteFil
|
|
|
26320
26357
|
success: BulkTransferCancelResult,
|
|
26321
26358
|
error: BulkTransferError
|
|
26322
26359
|
});
|
|
26360
|
+
/**
|
|
26361
|
+
* One workspace directory's immediate children, for the right panel's Files
|
|
26362
|
+
* tree. Read-scoped and confined to the workspace root on the server; the tree
|
|
26363
|
+
* expands one directory at a time through this method rather than listing the
|
|
26364
|
+
* whole workspace up front.
|
|
26365
|
+
*/
|
|
26366
|
+
const WsWorkspaceListDirectoryRpc = make$29(WS_METHODS.workspaceListDirectory, {
|
|
26367
|
+
payload: WorkspaceListDirectoryInput,
|
|
26368
|
+
success: WorkspaceListDirectoryResult,
|
|
26369
|
+
error: WorkspaceListDirectoryError
|
|
26370
|
+
});
|
|
26323
26371
|
const WsShellOpenInEditorRpc = make$29(WS_METHODS.shellOpenInEditor, {
|
|
26324
26372
|
payload: OpenInEditorInput,
|
|
26325
26373
|
error: OpenError$1
|
|
@@ -26743,7 +26791,7 @@ const WsRpcGroup = make$28(WsServerGetConfigRpc, WsServerRefreshProvidersRpc, Ws
|
|
|
26743
26791
|
payload: UsageSummaryInput,
|
|
26744
26792
|
success: UsageSummary,
|
|
26745
26793
|
error: UsageReadError
|
|
26746
|
-
}), WsPullRequestsListRpc, WsPullRequestsListStatsRpc, WsPullRequestsDetailRpc, WsPullRequestsActivityRpc, WsPullRequestsThreadCommentsRpc, WsPullRequestsDiffFileContentsRpc, WsPullRequestsRunActionRpc, WsPullRequestsUpdateRpc, WsPullRequestsCommentRpc, WsPullRequestsUpdateCommentRpc, WsPullRequestsSubmitReviewRpc, WsPullRequestsReplyToThreadRpc, WsPullRequestsSetThreadResolutionRpc, WsPullRequestsSetReactionRpc, WsPullRequestsInvalidateRpc, WsPullRequestsReviewerCandidatesRpc, WsPullRequestsRequestReviewersRpc, WsProjectsSearchContentsRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsProjectCheckoutDeletionPreviewRpc, WsProjectCheckoutDeletionExecuteRpc, WsWorkspaceReadFileRpc, WsWorkspaceBeginWriteFileRpc, WsWorkspaceWriteFileChunkRpc, WsWorkspaceCommitWriteFileRpc, WsWorkspaceCancelWriteFileRpc, WsShellOpenInEditorRpc, WsSubscribeGitStatusRpc, WsGitPullRpc, WsGitRefreshStatusRpc, WsGitRunStackedActionRpc, WsGitResolvePullRequestRpc, WsGitPreparePullRequestThreadRpc, WsGitListBranchesRpc, WsVcsListRefsRpc, WsVcsRefreshStatusRpc, WsSubscribeVcsStatusRpc, WsVcsPullRpc, WsVcsCreateWorktreeRpc, WsVcsRemoveWorktreeRpc, WsVcsCreateRefRpc, WsVcsSwitchRefRpc, WsVcsInitRpc, WsGitCreateWorktreeRpc, WsGitRemoveWorktreeRpc, WsGitCreateBranchRpc, WsGitCheckoutRpc, WsGitInitRpc, WsFilesystemBrowseRpc, WsProjectsReadFileRpc, WsReviewGetDiffPreviewRpc, WsCloudListRuntimesRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, WsTerminalResizeRpc, WsTerminalSignalRpc, WsTerminalClearRpc, WsTerminalRestartRpc, WsTerminalCloseRpc, WsBrowserOpenSessionRpc, WsBrowserListSessionsRpc, WsBrowserCloseSessionRpc, WsBrowserGetSessionRpc, WsBrowserCommandRpc, WsBrowserClearBrowsingDataRpc, WsBrowserAttachDisplayRpc, WsBrowserAddDisplayIceCandidateRpc, WsComputerInputRpc, WsDevTunnelRegisterRpc, WsDevTunnelUnregisterRpc, WsDevTunnelListRpc, WsThreadDevRuntimeGetRpc, WsThreadDevRuntimeAllocatePortRpc, WsThreadDevRuntimeReleaseRpc, WsWorkspaceEnvironmentPreflightRpc, WsWorkspaceEnsureScratchRpc, WsSubscribeTerminalEventsRpc, WsSubscribeTerminalMetadataRpc, WsProjectsListEntriesRpc, WsServerDiscoverSourceControlRpc, WsServerReportClientActivityRpc, WsSubscribeBrowserEventsRpc, WsSubscribeServerConfigRpc, WsSubscribeServerLifecycleRpc, WsSubscribeAuthAccessRpc, WsSubscribeWorkspaceReadinessRpc, WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetFullThreadDiffRpc, WsOrchestrationStreamTurnDiffRpc, WsOrchestrationStreamFullThreadDiffRpc, WsOrchestrationReplayEventsRpc, WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc, make$29(WS_METHODS.assetsCreateUrl, {
|
|
26794
|
+
}), WsPullRequestsListRpc, WsPullRequestsListStatsRpc, WsPullRequestsDetailRpc, WsPullRequestsActivityRpc, WsPullRequestsThreadCommentsRpc, WsPullRequestsDiffFileContentsRpc, WsPullRequestsRunActionRpc, WsPullRequestsUpdateRpc, WsPullRequestsCommentRpc, WsPullRequestsUpdateCommentRpc, WsPullRequestsSubmitReviewRpc, WsPullRequestsReplyToThreadRpc, WsPullRequestsSetThreadResolutionRpc, WsPullRequestsSetReactionRpc, WsPullRequestsInvalidateRpc, WsPullRequestsReviewerCandidatesRpc, WsPullRequestsRequestReviewersRpc, WsProjectsSearchContentsRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsProjectCheckoutDeletionPreviewRpc, WsProjectCheckoutDeletionExecuteRpc, WsWorkspaceReadFileRpc, WsWorkspaceListDirectoryRpc, WsWorkspaceBeginWriteFileRpc, WsWorkspaceWriteFileChunkRpc, WsWorkspaceCommitWriteFileRpc, WsWorkspaceCancelWriteFileRpc, WsShellOpenInEditorRpc, WsSubscribeGitStatusRpc, WsGitPullRpc, WsGitRefreshStatusRpc, WsGitRunStackedActionRpc, WsGitResolvePullRequestRpc, WsGitPreparePullRequestThreadRpc, WsGitListBranchesRpc, WsVcsListRefsRpc, WsVcsRefreshStatusRpc, WsSubscribeVcsStatusRpc, WsVcsPullRpc, WsVcsCreateWorktreeRpc, WsVcsRemoveWorktreeRpc, WsVcsCreateRefRpc, WsVcsSwitchRefRpc, WsVcsInitRpc, WsGitCreateWorktreeRpc, WsGitRemoveWorktreeRpc, WsGitCreateBranchRpc, WsGitCheckoutRpc, WsGitInitRpc, WsFilesystemBrowseRpc, WsProjectsReadFileRpc, WsReviewGetDiffPreviewRpc, WsCloudListRuntimesRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, WsTerminalResizeRpc, WsTerminalSignalRpc, WsTerminalClearRpc, WsTerminalRestartRpc, WsTerminalCloseRpc, WsBrowserOpenSessionRpc, WsBrowserListSessionsRpc, WsBrowserCloseSessionRpc, WsBrowserGetSessionRpc, WsBrowserCommandRpc, WsBrowserClearBrowsingDataRpc, WsBrowserAttachDisplayRpc, WsBrowserAddDisplayIceCandidateRpc, WsComputerInputRpc, WsDevTunnelRegisterRpc, WsDevTunnelUnregisterRpc, WsDevTunnelListRpc, WsThreadDevRuntimeGetRpc, WsThreadDevRuntimeAllocatePortRpc, WsThreadDevRuntimeReleaseRpc, WsWorkspaceEnvironmentPreflightRpc, WsWorkspaceEnsureScratchRpc, WsSubscribeTerminalEventsRpc, WsSubscribeTerminalMetadataRpc, WsProjectsListEntriesRpc, WsServerDiscoverSourceControlRpc, WsServerReportClientActivityRpc, WsSubscribeBrowserEventsRpc, WsSubscribeServerConfigRpc, WsSubscribeServerLifecycleRpc, WsSubscribeAuthAccessRpc, WsSubscribeWorkspaceReadinessRpc, WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetFullThreadDiffRpc, WsOrchestrationStreamTurnDiffRpc, WsOrchestrationStreamFullThreadDiffRpc, WsOrchestrationReplayEventsRpc, WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc, make$29(WS_METHODS.assetsCreateUrl, {
|
|
26747
26795
|
payload: AssetsRpcSchemas.createUrl.input,
|
|
26748
26796
|
success: AssetsRpcSchemas.createUrl.output,
|
|
26749
26797
|
error: AssetAccessError
|
|
@@ -31485,6 +31533,11 @@ const RELAY_METHOD_POLICY_BASES = [
|
|
|
31485
31533
|
requiredScope: "workspace.read",
|
|
31486
31534
|
deliveryClass: "read"
|
|
31487
31535
|
},
|
|
31536
|
+
{
|
|
31537
|
+
method: WS_METHODS.workspaceListDirectory,
|
|
31538
|
+
requiredScope: "workspace.read",
|
|
31539
|
+
deliveryClass: "read"
|
|
31540
|
+
},
|
|
31488
31541
|
{
|
|
31489
31542
|
method: WS_METHODS.workspaceReadFile,
|
|
31490
31543
|
requiredScope: "workspace.read",
|
|
@@ -66062,7 +66115,7 @@ function normalizeNumberish(value) {
|
|
|
66062
66115
|
}
|
|
66063
66116
|
//#endregion
|
|
66064
66117
|
//#region package.json
|
|
66065
|
-
var version$1 = "0.13.
|
|
66118
|
+
var version$1 = "0.13.133";
|
|
66066
66119
|
//#endregion
|
|
66067
66120
|
//#region src/sentry.ts
|
|
66068
66121
|
const SERVER_APP_NAME = "forge-server";
|
|
@@ -67021,9 +67074,7 @@ const runtimeReplicationOutboxOldestEventAge = gauge("t3_runtime_replication_out
|
|
|
67021
67074
|
const runtimeReplicationPublicationAttemptsTotal = counter("t3_runtime_replication_publication_attempts_total", { description: "Total canonical runtime replication publication attempts." });
|
|
67022
67075
|
const runtimeReplicationPublicationRetriesTotal = counter("t3_runtime_replication_publication_retries_total", { description: "Total runtime replication retries scheduled after recoverable failures." });
|
|
67023
67076
|
const runtimeReplicationPublicationLatency = timer("t3_runtime_replication_publication_latency", { description: "Time from durable enqueue to canonical CMMD acknowledgement." });
|
|
67024
|
-
const
|
|
67025
|
-
const cmmdTokenMintTotal = counter("t3_cmmd_token_mint_total", { description: "CMMD short-lived forge-token mints through the mint lane (forge#3518), by outcome (attempted | succeeded). The legacy rotation ladder keeps reporting t3_cmmd_credential_refresh_total, so the two lanes stay separable in every dashboard." });
|
|
67026
|
-
const cmmdRotationFenceAppliedTotal = counter("t3_cmmd_rotation_fence_applied_total", { description: "Sessions Forge refused to rotate CMMD credentials for, because the rotation fence applied." });
|
|
67077
|
+
const cmmdTokenMintTotal = counter("t3_cmmd_token_mint_total", { description: "CMMD short-lived forge-token mints, by outcome (attempted | succeeded | failed). The mint is the only CMMD JWT refresh story since forge#3521 deleted the legacy rotation ladder." });
|
|
67027
67078
|
const cmmdCredentialRowDeletedTotal = counter("t3_cmmd_credential_row_deleted_total", { description: "command_access_tokens rows actually deleted by clearUnrecoverableCmmdAuthForSession, by reason." });
|
|
67028
67079
|
const metricAttributes = (attributes) => Object.entries(compactMetricAttributes(attributes));
|
|
67029
67080
|
const increment = (metric, attributes, amount = 1) => update$3(withAttributes(metric, metricAttributes(attributes)), amount);
|
|
@@ -67104,12 +67155,10 @@ const isPersistenceError = (u) => is$1(PersistenceSqlError)(u) || is$1(Persisten
|
|
|
67104
67155
|
const cache = /* @__PURE__ */ new Map();
|
|
67105
67156
|
const jwtCache = /* @__PURE__ */ new Map();
|
|
67106
67157
|
const jwtExpiryCache = /* @__PURE__ */ new Map();
|
|
67107
|
-
const refreshTokenCache = /* @__PURE__ */ new Map();
|
|
67108
67158
|
const identityCache = /* @__PURE__ */ new Map();
|
|
67109
67159
|
const sessionGrantCache = /* @__PURE__ */ new Map();
|
|
67110
67160
|
const ACCESS_TOKEN_SCOPE = "cmmd-access-token";
|
|
67111
67161
|
const JWT_TOKEN_SCOPE = "cmmd-jwt-token";
|
|
67112
|
-
const REFRESH_TOKEN_SCOPE = "cmmd-refresh-token";
|
|
67113
67162
|
const IDENTITY_SCOPE = "cmmd-user-identity";
|
|
67114
67163
|
const SESSION_GRANT_SCOPE = "cmmd-session-grant";
|
|
67115
67164
|
const CommandAccessTokenRow = Struct({
|
|
@@ -67117,27 +67166,15 @@ const CommandAccessTokenRow = Struct({
|
|
|
67117
67166
|
accessToken: String$1,
|
|
67118
67167
|
jwtToken: NullOr(String$1),
|
|
67119
67168
|
jwtExpiresAt: NullOr(String$1),
|
|
67120
|
-
refreshToken: NullOr(String$1),
|
|
67121
67169
|
cmmdUserIdentity: NullOr(String$1),
|
|
67122
67170
|
sessionGrant: NullOr(String$1)
|
|
67123
67171
|
});
|
|
67124
67172
|
const SubjectCommandAccessTokenRow = Struct({ accessToken: String$1 });
|
|
67125
|
-
const SubjectJwtRow = Struct({
|
|
67126
|
-
accessToken: String$1,
|
|
67127
|
-
jwtToken: NullOr(String$1),
|
|
67128
|
-
jwtExpiresAt: NullOr(String$1),
|
|
67129
|
-
refreshToken: NullOr(String$1),
|
|
67130
|
-
sessionId: String$1
|
|
67131
|
-
});
|
|
67132
|
-
const SubjectRefreshTokenRow = Struct({
|
|
67133
|
-
refreshToken: String$1,
|
|
67134
|
-
sessionId: String$1
|
|
67135
|
-
});
|
|
67136
67173
|
const OwnerCommandAuthBundleRow = Struct({
|
|
67137
67174
|
accessToken: String$1,
|
|
67138
67175
|
jwtToken: NullOr(String$1),
|
|
67139
67176
|
jwtExpiresAt: NullOr(String$1),
|
|
67140
|
-
|
|
67177
|
+
sessionGrant: NullOr(String$1),
|
|
67141
67178
|
sessionId: String$1,
|
|
67142
67179
|
cmmdUserIdentity: String$1
|
|
67143
67180
|
});
|
|
@@ -67182,8 +67219,6 @@ function decryptNullable(scope, value) {
|
|
|
67182
67219
|
}
|
|
67183
67220
|
const decodeTokenRows = decodeUnknownEffect(ArraySchema(CommandAccessTokenRow));
|
|
67184
67221
|
const decodeSubjectTokenRows = decodeUnknownEffect(ArraySchema(SubjectCommandAccessTokenRow));
|
|
67185
|
-
const decodeSubjectJwtRows = decodeUnknownEffect(ArraySchema(SubjectJwtRow));
|
|
67186
|
-
decodeUnknownEffect(ArraySchema(SubjectRefreshTokenRow));
|
|
67187
67222
|
const decodeOwnerCommandAuthBundleRows = decodeUnknownEffect(ArraySchema(OwnerCommandAuthBundleRow));
|
|
67188
67223
|
const decodeOwnerCommandAuthSessionRows = decodeUnknownEffect(ArraySchema(OwnerCommandAuthSessionRow));
|
|
67189
67224
|
const decodeCloudOwnerSessionRows = decodeUnknownEffect(ArraySchema(CloudOwnerSessionRow));
|
|
@@ -67209,9 +67244,9 @@ function extractJwtExpiresAt(jwtToken) {
|
|
|
67209
67244
|
* Comparing against `exp` exactly leaves a window where Forge sends a token it
|
|
67210
67245
|
* believes is good and CMMD has already stopped accepting it: the clocks differ by a
|
|
67211
67246
|
* little, the request spends time in flight, and the answer comes back 401 on a
|
|
67212
|
-
* session
|
|
67213
|
-
*
|
|
67214
|
-
*
|
|
67247
|
+
* session whose grant could have minted a good replacement. Two minutes is far
|
|
67248
|
+
* inside the JWT's own lifetime, so this costs one early mint and removes the
|
|
67249
|
+
* race.
|
|
67215
67250
|
*/
|
|
67216
67251
|
const JWT_EXPIRY_SKEW_MS = 2 * 6e4;
|
|
67217
67252
|
/**
|
|
@@ -67237,7 +67272,6 @@ const hydrateCommandAccessTokenStore = gen(function* () {
|
|
|
67237
67272
|
access_token AS "accessToken",
|
|
67238
67273
|
jwt_token AS "jwtToken",
|
|
67239
67274
|
jwt_expires_at AS "jwtExpiresAt",
|
|
67240
|
-
refresh_token AS "refreshToken",
|
|
67241
67275
|
cmmd_user_identity AS "cmmdUserIdentity",
|
|
67242
67276
|
session_grant AS "sessionGrant"
|
|
67243
67277
|
FROM command_access_tokens
|
|
@@ -67245,7 +67279,6 @@ const hydrateCommandAccessTokenStore = gen(function* () {
|
|
|
67245
67279
|
cache.clear();
|
|
67246
67280
|
jwtCache.clear();
|
|
67247
67281
|
jwtExpiryCache.clear();
|
|
67248
|
-
refreshTokenCache.clear();
|
|
67249
67282
|
identityCache.clear();
|
|
67250
67283
|
sessionGrantCache.clear();
|
|
67251
67284
|
yield* try_({
|
|
@@ -67256,8 +67289,6 @@ const hydrateCommandAccessTokenStore = gen(function* () {
|
|
|
67256
67289
|
const jwtToken = decryptNullable(JWT_TOKEN_SCOPE, row.jwtToken);
|
|
67257
67290
|
if (jwtToken) jwtCache.set(sessionId, jwtToken);
|
|
67258
67291
|
if (row.jwtExpiresAt) jwtExpiryCache.set(sessionId, row.jwtExpiresAt);
|
|
67259
|
-
const refreshToken = decryptNullable(REFRESH_TOKEN_SCOPE, row.refreshToken);
|
|
67260
|
-
if (refreshToken) refreshTokenCache.set(sessionId, refreshToken);
|
|
67261
67292
|
const identity = decodeCmmdUserIdentity(decryptNullable(IDENTITY_SCOPE, row.cmmdUserIdentity));
|
|
67262
67293
|
if (identity) identityCache.set(sessionId, identity);
|
|
67263
67294
|
const sessionGrant = decryptNullable(SESSION_GRANT_SCOPE, row.sessionGrant);
|
|
@@ -67364,19 +67395,11 @@ function readCommandJwtForSession(sessionId) {
|
|
|
67364
67395
|
return jwtCache.get(sessionId) ?? null;
|
|
67365
67396
|
}
|
|
67366
67397
|
/**
|
|
67367
|
-
* Read the CMMD OAuth refresh token for a session. Sync; hits the in-memory
|
|
67368
|
-
* cache only. Used to rotate a REST JWT after CMMD rejects a locally cached JWT
|
|
67369
|
-
* that still appears unexpired.
|
|
67370
|
-
*/
|
|
67371
|
-
function readCommandRefreshTokenForSession(sessionId) {
|
|
67372
|
-
return refreshTokenCache.get(sessionId) ?? null;
|
|
67373
|
-
}
|
|
67374
|
-
/**
|
|
67375
67398
|
* The durable CMMD session grant for a session, if one was issued.
|
|
67376
67399
|
*
|
|
67377
|
-
*
|
|
67378
|
-
*
|
|
67379
|
-
*
|
|
67400
|
+
* The grant is NON-rotating: minting from it does not invalidate it, so it
|
|
67401
|
+
* stays valid until CMMD expires or revokes it. That is what makes it a safe
|
|
67402
|
+
* long-lived credential: the short-lived JWT it mints can always be replaced.
|
|
67380
67403
|
*/
|
|
67381
67404
|
function readCommandSessionGrantForSession(sessionId) {
|
|
67382
67405
|
return sessionGrantCache.get(sessionId) ?? null;
|
|
@@ -67426,77 +67449,26 @@ const setCmmdUserIdentityForSession = (sessionId, identity) => gen(function* ()
|
|
|
67426
67449
|
* `jwt_expires_at` so callers can detect when the token has expired.
|
|
67427
67450
|
* Updates only the JWT columns; the PAT row must already exist
|
|
67428
67451
|
* (call `setCommandAccessTokenForSession` first in the OAuth callback).
|
|
67452
|
+
*
|
|
67453
|
+
* The JWT is short-lived by contract (CMMD clamps the mint TTL to one hour);
|
|
67454
|
+
* replacing one is the resolver's mint, so no sibling credential rides along.
|
|
67429
67455
|
*/
|
|
67430
|
-
const setCommandJwtForSession = (sessionId, jwtToken
|
|
67456
|
+
const setCommandJwtForSession = (sessionId, jwtToken) => gen(function* () {
|
|
67431
67457
|
const token = jwtToken.trim();
|
|
67432
67458
|
if (token.length === 0) return;
|
|
67433
67459
|
const expiresAt = extractJwtExpiresAt(token);
|
|
67434
|
-
const refreshToken = opts.refreshToken?.trim();
|
|
67435
67460
|
const storedToken = encodeSensitiveValue(JWT_TOKEN_SCOPE, token);
|
|
67436
|
-
|
|
67437
|
-
|
|
67438
|
-
|
|
67439
|
-
|
|
67440
|
-
|
|
67441
|
-
|
|
67442
|
-
|
|
67443
|
-
|
|
67444
|
-
refresh_token = ${storedRefreshToken},
|
|
67445
|
-
updated_at = ${now}
|
|
67446
|
-
WHERE session_id = ${sessionId}
|
|
67447
|
-
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.setJwt:query")));
|
|
67448
|
-
else yield* sql`
|
|
67449
|
-
UPDATE command_access_tokens
|
|
67450
|
-
SET
|
|
67451
|
-
jwt_token = ${storedToken},
|
|
67452
|
-
jwt_expires_at = ${expiresAt},
|
|
67453
|
-
updated_at = ${now}
|
|
67454
|
-
WHERE session_id = ${sessionId}
|
|
67455
|
-
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.setJwt:query")));
|
|
67461
|
+
yield* (yield* SqlClient)`
|
|
67462
|
+
UPDATE command_access_tokens
|
|
67463
|
+
SET
|
|
67464
|
+
jwt_token = ${storedToken},
|
|
67465
|
+
jwt_expires_at = ${expiresAt},
|
|
67466
|
+
updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
67467
|
+
WHERE session_id = ${sessionId}
|
|
67468
|
+
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.setJwt:query")));
|
|
67456
67469
|
jwtCache.set(sessionId, token);
|
|
67457
67470
|
if (expiresAt) jwtExpiryCache.set(sessionId, expiresAt);
|
|
67458
67471
|
else jwtExpiryCache.delete(sessionId);
|
|
67459
|
-
if (refreshToken) refreshTokenCache.set(sessionId, refreshToken);
|
|
67460
|
-
});
|
|
67461
|
-
/**
|
|
67462
|
-
* Look up the most recent non-expired JWT for an auth subject across active
|
|
67463
|
-
* owner sessions. Used as a continuity fallback when the current session ID
|
|
67464
|
-
* changed (e.g. after re-authentication) but the user's CMMD JWT should still
|
|
67465
|
-
* be usable.
|
|
67466
|
-
*
|
|
67467
|
-
* Also returns the sessionId that owns the JWT so callers can promote it to
|
|
67468
|
-
* the current session, avoiding repeated cross-session lookups.
|
|
67469
|
-
*/
|
|
67470
|
-
const readMostRecentJwtForSubject = (subject) => gen(function* () {
|
|
67471
|
-
const row = (yield* decodeSubjectJwtRows(yield* (yield* SqlClient)`
|
|
67472
|
-
SELECT
|
|
67473
|
-
cat.access_token AS "accessToken",
|
|
67474
|
-
cat.jwt_token AS "jwtToken",
|
|
67475
|
-
cat.jwt_expires_at AS "jwtExpiresAt",
|
|
67476
|
-
cat.refresh_token AS "refreshToken",
|
|
67477
|
-
cat.session_id AS "sessionId"
|
|
67478
|
-
FROM command_access_tokens AS cat
|
|
67479
|
-
JOIN auth_sessions AS s ON s.session_id = cat.session_id
|
|
67480
|
-
WHERE s.subject = ${subject}
|
|
67481
|
-
AND s.role = 'owner'
|
|
67482
|
-
AND s.revoked_at IS NULL
|
|
67483
|
-
AND cat.jwt_token IS NOT NULL
|
|
67484
|
-
AND cat.jwt_expires_at IS NOT NULL
|
|
67485
|
-
AND cat.jwt_expires_at > ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
67486
|
-
ORDER BY s.issued_at DESC
|
|
67487
|
-
LIMIT 1
|
|
67488
|
-
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.readMostRecentJwtBySubject:query")))).pipe(mapError(toPersistenceDecodeError("CommandAccessTokenStore.readMostRecentJwtBySubject:decodeRows"))))[0];
|
|
67489
|
-
if (!row?.jwtToken) return null;
|
|
67490
|
-
const jwtToken = decodeSensitiveValue(JWT_TOKEN_SCOPE, row.jwtToken);
|
|
67491
|
-
const refreshToken = decryptNullable(REFRESH_TOKEN_SCOPE, row.refreshToken);
|
|
67492
|
-
const accessToken = decodeSensitiveValue(ACCESS_TOKEN_SCOPE, row.accessToken);
|
|
67493
|
-
return {
|
|
67494
|
-
jwt: jwtToken,
|
|
67495
|
-
sessionId: row.sessionId,
|
|
67496
|
-
expiresAt: row.jwtExpiresAt ?? null,
|
|
67497
|
-
refreshToken,
|
|
67498
|
-
accessToken
|
|
67499
|
-
};
|
|
67500
67472
|
});
|
|
67501
67473
|
/**
|
|
67502
67474
|
* Look up the latest CMMD PAT for an auth subject across active owner sessions.
|
|
@@ -67535,7 +67507,7 @@ const readMostRecentCommandAccessTokenForSubject = (subject) => gen(function* ()
|
|
|
67535
67507
|
* `desktop-bootstrap` session that minted them, and nothing later rewrites that
|
|
67536
67508
|
* subject. So the clause matched no row on any machine, promotion always
|
|
67537
67509
|
* returned null, and a phone whose short-lived JWT had expired could not reach
|
|
67538
|
-
* the
|
|
67510
|
+
* the credentials sitting on its own previous session. It surfaced as "This
|
|
67539
67511
|
* session is no longer accepted. Sign in again." about a day after signing in,
|
|
67540
67512
|
* with nothing in the log, because the ladder never got as far as a call that
|
|
67541
67513
|
* could fail out loud.
|
|
@@ -67596,6 +67568,9 @@ const readMostRecentOwnerCommandAuthSession = gen(function* () {
|
|
|
67596
67568
|
* a CMMD user subject. The CMMD token is still account-owned, so the trusted
|
|
67597
67569
|
* local owner may inherit the most recent active owner token with an identity.
|
|
67598
67570
|
* Client pairings must not call this.
|
|
67571
|
+
*
|
|
67572
|
+
* The copy includes the session grant: the promoted session can then mint its
|
|
67573
|
+
* own short-lived JWT, which since forge#3521 is the only refresh story.
|
|
67599
67574
|
*/
|
|
67600
67575
|
const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => gen(function* () {
|
|
67601
67576
|
const sql = yield* SqlClient;
|
|
@@ -67604,7 +67579,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => gen(function*
|
|
|
67604
67579
|
cat.access_token AS "accessToken",
|
|
67605
67580
|
cat.jwt_token AS "jwtToken",
|
|
67606
67581
|
cat.jwt_expires_at AS "jwtExpiresAt",
|
|
67607
|
-
cat.
|
|
67582
|
+
cat.session_grant AS "sessionGrant",
|
|
67608
67583
|
cat.session_id AS "sessionId",
|
|
67609
67584
|
cat.cmmd_user_identity AS "cmmdUserIdentity"
|
|
67610
67585
|
FROM command_access_tokens AS cat
|
|
@@ -67616,7 +67591,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => gen(function*
|
|
|
67616
67591
|
if (!row) return null;
|
|
67617
67592
|
const accessToken = decodeSensitiveValue(ACCESS_TOKEN_SCOPE, row.accessToken);
|
|
67618
67593
|
const jwtToken = decryptNullable(JWT_TOKEN_SCOPE, row.jwtToken);
|
|
67619
|
-
const
|
|
67594
|
+
const sessionGrant = decryptNullable(SESSION_GRANT_SCOPE, row.sessionGrant);
|
|
67620
67595
|
const identity = decodeCmmdUserIdentity(decodeSensitiveValue(IDENTITY_SCOPE, row.cmmdUserIdentity));
|
|
67621
67596
|
if (!identity) return null;
|
|
67622
67597
|
const updateNow = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -67626,7 +67601,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => gen(function*
|
|
|
67626
67601
|
access_token,
|
|
67627
67602
|
jwt_token,
|
|
67628
67603
|
jwt_expires_at,
|
|
67629
|
-
|
|
67604
|
+
session_grant,
|
|
67630
67605
|
cmmd_user_identity,
|
|
67631
67606
|
created_at,
|
|
67632
67607
|
updated_at
|
|
@@ -67636,7 +67611,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => gen(function*
|
|
|
67636
67611
|
${row.accessToken},
|
|
67637
67612
|
${row.jwtToken},
|
|
67638
67613
|
${row.jwtExpiresAt},
|
|
67639
|
-
${row.
|
|
67614
|
+
${row.sessionGrant},
|
|
67640
67615
|
${row.cmmdUserIdentity},
|
|
67641
67616
|
${updateNow},
|
|
67642
67617
|
${updateNow}
|
|
@@ -67645,7 +67620,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => gen(function*
|
|
|
67645
67620
|
access_token = excluded.access_token,
|
|
67646
67621
|
jwt_token = excluded.jwt_token,
|
|
67647
67622
|
jwt_expires_at = excluded.jwt_expires_at,
|
|
67648
|
-
|
|
67623
|
+
session_grant = excluded.session_grant,
|
|
67649
67624
|
cmmd_user_identity = excluded.cmmd_user_identity,
|
|
67650
67625
|
updated_at = excluded.updated_at
|
|
67651
67626
|
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.promoteLatestOwnerAuth:upsert")));
|
|
@@ -67654,14 +67629,14 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => gen(function*
|
|
|
67654
67629
|
else jwtCache.delete(sessionId);
|
|
67655
67630
|
if (row.jwtExpiresAt) jwtExpiryCache.set(sessionId, row.jwtExpiresAt);
|
|
67656
67631
|
else jwtExpiryCache.delete(sessionId);
|
|
67657
|
-
if (
|
|
67658
|
-
else
|
|
67632
|
+
if (sessionGrant) sessionGrantCache.set(sessionId, sessionGrant);
|
|
67633
|
+
else sessionGrantCache.delete(sessionId);
|
|
67659
67634
|
identityCache.set(sessionId, identity);
|
|
67660
67635
|
return {
|
|
67661
67636
|
accessToken,
|
|
67662
67637
|
jwtToken,
|
|
67663
67638
|
jwtExpiresAt: row.jwtExpiresAt ?? null,
|
|
67664
|
-
|
|
67639
|
+
sessionGrant,
|
|
67665
67640
|
sourceSessionId: row.sessionId,
|
|
67666
67641
|
cmmdUserIdentity: identity
|
|
67667
67642
|
};
|
|
@@ -67706,11 +67681,11 @@ const clearCommandAccessTokenForSession = (sessionId) => gen(function* () {
|
|
|
67706
67681
|
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.clear:query")));
|
|
67707
67682
|
forgetCommandAccessTokenCachesForSession(sessionId);
|
|
67708
67683
|
});
|
|
67684
|
+
/** Evict a session's credentials, JWT expiry, identity, and grant from memory without changing storage. */
|
|
67709
67685
|
function forgetCommandAccessTokenCachesForSession(sessionId) {
|
|
67710
67686
|
cache.delete(sessionId);
|
|
67711
67687
|
jwtCache.delete(sessionId);
|
|
67712
67688
|
jwtExpiryCache.delete(sessionId);
|
|
67713
|
-
refreshTokenCache.delete(sessionId);
|
|
67714
67689
|
identityCache.delete(sessionId);
|
|
67715
67690
|
sessionGrantCache.delete(sessionId);
|
|
67716
67691
|
}
|
|
@@ -67742,14 +67717,14 @@ const scheduleExpiredForgeCloudOwnerCredentialMaintenance = sleep$4(minutes(5)).
|
|
|
67742
67717
|
* is dead too, so the promotion sources are cleared with it — otherwise the
|
|
67743
67718
|
* next promotion resurrects the same dead bundle in a loop.
|
|
67744
67719
|
* - **Every other session** owns its credentials outright: each OAuth exchange
|
|
67745
|
-
* mints
|
|
67746
|
-
*
|
|
67720
|
+
* mints its own PAT, JWT and session grant. One session's dead credential
|
|
67721
|
+
* says nothing about a
|
|
67747
67722
|
* sibling browser or device, so the clear stays local to that session.
|
|
67748
67723
|
*
|
|
67749
67724
|
* Clearing every active owner row unconditionally (the pre-CMMD-1871 behaviour)
|
|
67750
67725
|
* turned a single expired tab into a sign-out on every browser and device.
|
|
67751
67726
|
*
|
|
67752
|
-
* Destroys
|
|
67727
|
+
* Destroys durable credentials (the PAT, the grant), so every call is logged with a
|
|
67753
67728
|
* `reason` naming the caller's own decision (never the credential itself) and
|
|
67754
67729
|
* the actual row count deleted, at info level so it shows up without raising
|
|
67755
67730
|
* the server's log level. `reason` is optional only so a caller that has not
|
|
@@ -67790,65 +67765,10 @@ const clearUnrecoverableCmmdAuthForSession = (sessionId, reason) => gen(function
|
|
|
67790
67765
|
}
|
|
67791
67766
|
});
|
|
67792
67767
|
/**
|
|
67793
|
-
* Retire refresh credentials copied into historical desktop bearer sessions.
|
|
67794
|
-
* Electron owns rotation for native desktop OAuth; Forge Local keeps only the
|
|
67795
|
-
* derived access authority needed by the current bearer session.
|
|
67796
|
-
*
|
|
67797
|
-
* Scoped to the bootstrapping client, not to the whole subject. The phone and
|
|
67798
|
-
* Electron sign in as the same `user:<id>`, so a subject-wide sweep let one
|
|
67799
|
-
* desktop launch null out a phone's refresh token and end its session a day
|
|
67800
|
-
* later. Two clauses keep that from happening:
|
|
67801
|
-
*
|
|
67802
|
-
* - `client_device_type = 'desktop'`, which now excludes phones outright,
|
|
67803
|
-
* because the mobile app declares itself at bootstrap.
|
|
67804
|
-
* - the same recorded `client_user_agent` as the bootstrap doing the
|
|
67805
|
-
* retirement, which is the only thing that separates a phone row written
|
|
67806
|
-
* before that stamp existed from a real Electron row. Both are stamped
|
|
67807
|
-
* `desktop`; neither carries any other mark. An empty or unmatched user agent
|
|
67808
|
-
* retires nothing, which is the direction that keeps a live credential.
|
|
67809
|
-
*
|
|
67810
|
-
* Electron re-bootstraps under a stable user agent, so its own historical rows
|
|
67811
|
-
* still match and still get swept.
|
|
67812
|
-
*/
|
|
67813
|
-
const clearDesktopBearerRefreshTokensForSubject = (input) => gen(function* () {
|
|
67814
|
-
const normalizedSubject = input.subject.trim();
|
|
67815
|
-
const normalizedUserAgent = input.clientUserAgent?.trim() ?? "";
|
|
67816
|
-
if (!normalizedSubject || !normalizedUserAgent) return;
|
|
67817
|
-
const rows = yield* (yield* SqlClient)`
|
|
67818
|
-
UPDATE command_access_tokens
|
|
67819
|
-
SET
|
|
67820
|
-
refresh_token = NULL,
|
|
67821
|
-
updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
67822
|
-
WHERE session_id IN (
|
|
67823
|
-
SELECT session_id
|
|
67824
|
-
FROM auth_sessions
|
|
67825
|
-
WHERE subject = ${normalizedSubject}
|
|
67826
|
-
AND role = 'owner'
|
|
67827
|
-
AND method = 'bearer-session-token'
|
|
67828
|
-
AND client_device_type = 'desktop'
|
|
67829
|
-
AND client_user_agent = ${normalizedUserAgent}
|
|
67830
|
-
)
|
|
67831
|
-
RETURNING session_id AS "sessionId"
|
|
67832
|
-
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.clearDesktopBearerRefresh:query")));
|
|
67833
|
-
for (const row of rows) refreshTokenCache.delete(row.sessionId);
|
|
67834
|
-
});
|
|
67835
|
-
const isDesktopBearerAuthSession = (sessionId) => gen(function* () {
|
|
67836
|
-
const rows = yield* (yield* SqlClient)`
|
|
67837
|
-
SELECT
|
|
67838
|
-
method,
|
|
67839
|
-
client_device_type AS "clientDeviceType"
|
|
67840
|
-
FROM auth_sessions
|
|
67841
|
-
WHERE session_id = ${sessionId}
|
|
67842
|
-
LIMIT 1
|
|
67843
|
-
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.isDesktopBearerSession:query")));
|
|
67844
|
-
return rows[0]?.method === "bearer-session-token" && rows[0].clientDeviceType === "desktop";
|
|
67845
|
-
});
|
|
67846
|
-
/**
|
|
67847
67768
|
* The device type a session's bearer bootstrap declared, for log lines only.
|
|
67848
67769
|
*
|
|
67849
|
-
*
|
|
67850
|
-
*
|
|
67851
|
-
* what made the request, not whether Forge may rotate for it. Never fails:
|
|
67770
|
+
* Returns the raw column: a log line wants to say what made the request.
|
|
67771
|
+
* Never fails:
|
|
67852
67772
|
* an unreadable or missing row logs as `"unknown"` rather than losing the
|
|
67853
67773
|
* decision-point log line it is attached to.
|
|
67854
67774
|
*/
|
|
@@ -67901,6 +67821,7 @@ function artifactError(message) {
|
|
|
67901
67821
|
cause
|
|
67902
67822
|
});
|
|
67903
67823
|
}
|
|
67824
|
+
/** Store credentials under the asserted Cloud owner session and retain its durable mint grant. */
|
|
67904
67825
|
function persistCloudOwnerArtifacts(input) {
|
|
67905
67826
|
return gen(function* () {
|
|
67906
67827
|
const sql = yield* SqlClient;
|
|
@@ -67908,7 +67829,7 @@ function persistCloudOwnerArtifacts(input) {
|
|
|
67908
67829
|
yield* setCommandAccessTokenForSession(input.assertion.sessionId, input.accessToken).pipe(mapError(artifactError("Failed to persist CMMD access token.")));
|
|
67909
67830
|
yield* setCmmdUserIdentityForSession(input.assertion.sessionId, input.identity).pipe(mapError(artifactError("Failed to persist CMMD user identity.")));
|
|
67910
67831
|
if (input.sessionGrant) yield* setCommandSessionGrantForSession(input.assertion.sessionId, input.sessionGrant, { expiresAt: input.sessionGrantExpiresAt ?? null }).pipe(mapError(artifactError("Failed to persist CMMD session grant.")));
|
|
67911
|
-
if (input.jwtToken) yield* setCommandJwtForSession(input.assertion.sessionId, input.jwtToken
|
|
67832
|
+
if (input.jwtToken) yield* setCommandJwtForSession(input.assertion.sessionId, input.jwtToken).pipe(mapError(artifactError("Failed to persist CMMD JWT.")));
|
|
67912
67833
|
});
|
|
67913
67834
|
yield* sql.withTransaction(transaction).pipe(mapError((cause) => cause instanceof AuthError ? cause : new AuthError({
|
|
67914
67835
|
message: "Failed to persist Forge Cloud owner credentials.",
|
|
@@ -68046,7 +67967,6 @@ const authSessionRenewRouteLayer = add$1("POST", "/api/auth/session/renew", gen(
|
|
|
68046
67967
|
accessToken: patToStore,
|
|
68047
67968
|
identity: cmmdIdentity,
|
|
68048
67969
|
jwtToken: credentials.jwtToStore,
|
|
68049
|
-
refreshToken: credentials.refreshToken,
|
|
68050
67970
|
sessionGrant,
|
|
68051
67971
|
sessionGrantExpiresAt: void 0
|
|
68052
67972
|
});
|
|
@@ -68122,7 +68042,6 @@ const PairingCredentialRequestHeaders = Struct({
|
|
|
68122
68042
|
const CommandBootstrapInput = Struct({
|
|
68123
68043
|
accessToken: String$1,
|
|
68124
68044
|
mcpToken: optionalKey(String$1),
|
|
68125
|
-
refreshToken: optionalKey(String$1),
|
|
68126
68045
|
userId: optionalKey(Number$1),
|
|
68127
68046
|
displayName: optionalKey(String$1),
|
|
68128
68047
|
user: optionalKey(Unknown),
|
|
@@ -68356,6 +68275,7 @@ function validateCommandBootstrapPayload(payload) {
|
|
|
68356
68275
|
};
|
|
68357
68276
|
});
|
|
68358
68277
|
}
|
|
68278
|
+
/** Store the bootstrap PAT, optional JWT, and identity; report persistence failures as auth errors. */
|
|
68359
68279
|
function persistCommandBootstrapSessionArtifacts(input) {
|
|
68360
68280
|
return gen(function* () {
|
|
68361
68281
|
const token = input.payload.accessToken.trim();
|
|
@@ -68370,14 +68290,11 @@ function persistCommandBootstrapSessionArtifacts(input) {
|
|
|
68370
68290
|
status: 500,
|
|
68371
68291
|
cause
|
|
68372
68292
|
})));
|
|
68373
|
-
if (isCmmdJwtToken(token)) {
|
|
68374
|
-
|
|
68375
|
-
|
|
68376
|
-
|
|
68377
|
-
|
|
68378
|
-
cause
|
|
68379
|
-
})));
|
|
68380
|
-
}
|
|
68293
|
+
if (isCmmdJwtToken(token)) yield* setCommandJwtForSession(input.sessionId, token).pipe(mapError((cause) => new AuthError({
|
|
68294
|
+
message: "Failed to persist CMMD JWT.",
|
|
68295
|
+
status: 500,
|
|
68296
|
+
cause
|
|
68297
|
+
})));
|
|
68381
68298
|
const cmmdIdentity = input.cmmdIdentity ?? normalizeCmmdUserIdentity(input.payload.user);
|
|
68382
68299
|
if (cmmdIdentity) yield* setCmmdUserIdentityForSession(input.sessionId, cmmdIdentity).pipe(mapError((cause) => new AuthError({
|
|
68383
68300
|
message: "Failed to persist CMMD user identity.",
|
|
@@ -68479,7 +68396,6 @@ function resolveCmmdTokenBundleCredentials(tokenResult) {
|
|
|
68479
68396
|
userId: cmmdIdentity?.id,
|
|
68480
68397
|
accessToken,
|
|
68481
68398
|
mcpToken,
|
|
68482
|
-
refreshToken: readString("refresh_token"),
|
|
68483
68399
|
sessionGrant: readString("session_grant"),
|
|
68484
68400
|
sessionGrantExpiresAt: readString("session_grant_expires_at"),
|
|
68485
68401
|
forgeOwnerAssertion: readString("forge_owner_assertion"),
|
|
@@ -68501,7 +68417,6 @@ function describeCmmdTokenResponseForLog(input) {
|
|
|
68501
68417
|
accessTokenType: input.tokenObj ? typeof input.tokenObj["access_token"] : "n/a",
|
|
68502
68418
|
hasAccessToken: input.accessToken !== void 0,
|
|
68503
68419
|
hasMcpToken: input.mcpToken !== void 0,
|
|
68504
|
-
hasRefreshToken: input.refreshToken !== void 0,
|
|
68505
68420
|
expiresInType: input.tokenObj ? typeof input.tokenObj["expires_in"] : "n/a",
|
|
68506
68421
|
isJwt: input.isJwt,
|
|
68507
68422
|
userKeys: userObj ? Object.keys(userObj) : [],
|
|
@@ -68729,19 +68644,6 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
|
|
|
68729
68644
|
payload,
|
|
68730
68645
|
cmmdIdentity: validated.cmmdIdentity
|
|
68731
68646
|
});
|
|
68732
|
-
if (deviceType === "desktop" && !payload.refreshToken?.trim()) yield* clearDesktopBearerRefreshTokensForSubject({
|
|
68733
|
-
subject: sessionSubject,
|
|
68734
|
-
clientUserAgent: request.headers["user-agent"]
|
|
68735
|
-
}).pipe(mapError((cause) => new AuthError({
|
|
68736
|
-
message: "Failed to retire legacy desktop refresh credentials.",
|
|
68737
|
-
status: 500,
|
|
68738
|
-
cause
|
|
68739
|
-
})), tapError(() => logWarning$1("CMMD bearer bootstrap failed.", {
|
|
68740
|
-
sessionId: issued.sessionId,
|
|
68741
|
-
deviceType,
|
|
68742
|
-
outcome: "failed",
|
|
68743
|
-
reason: "legacy-desktop-refresh-retirement-failed"
|
|
68744
|
-
})));
|
|
68745
68647
|
yield* logInfo("CMMD bearer bootstrap succeeded.", {
|
|
68746
68648
|
sessionId: issued.sessionId,
|
|
68747
68649
|
deviceType,
|
|
@@ -68839,7 +68741,6 @@ function completeCloudOwnerPasswordLogin(input) {
|
|
|
68839
68741
|
accessToken: patToStore,
|
|
68840
68742
|
identity: cmmdIdentity,
|
|
68841
68743
|
jwtToken: credentials.jwtToStore,
|
|
68842
|
-
refreshToken: credentials.refreshToken,
|
|
68843
68744
|
sessionGrant: credentials.sessionGrant,
|
|
68844
68745
|
sessionGrantExpiresAt: credentials.sessionGrantExpiresAt
|
|
68845
68746
|
});
|
|
@@ -68922,6 +68823,10 @@ const authWebLoginRouteLayer = add$1("GET", "/api/auth/login", gen(function* ()
|
|
|
68922
68823
|
transientCookie
|
|
68923
68824
|
]]));
|
|
68924
68825
|
}));
|
|
68826
|
+
/**
|
|
68827
|
+
* Persist the local OAuth PAT and identity, failing if either write fails.
|
|
68828
|
+
* Capture and ignore failures while storing the optional mint grant and JWT.
|
|
68829
|
+
*/
|
|
68925
68830
|
function persistLocalOAuthArtifacts(input) {
|
|
68926
68831
|
return gen(function* () {
|
|
68927
68832
|
yield* setCommandAccessTokenForSession(input.sessionId, input.accessToken).pipe(mapError((cause) => new AuthError({
|
|
@@ -68938,7 +68843,7 @@ function persistLocalOAuthArtifacts(input) {
|
|
|
68938
68843
|
route: "oauth-callback",
|
|
68939
68844
|
step: "grant-store"
|
|
68940
68845
|
}))), ignore({ log: true }));
|
|
68941
|
-
if (input.jwtToken) yield* setCommandJwtForSession(input.sessionId, input.jwtToken
|
|
68846
|
+
if (input.jwtToken) yield* setCommandJwtForSession(input.sessionId, input.jwtToken).pipe(tapError((error) => sync$1(() => captureServerError(error, {
|
|
68942
68847
|
route: "oauth-callback",
|
|
68943
68848
|
step: "jwt-store"
|
|
68944
68849
|
}))), ignore({ log: true }));
|
|
@@ -68992,13 +68897,12 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", gen(functio
|
|
|
68992
68897
|
cause
|
|
68993
68898
|
})
|
|
68994
68899
|
}), "Failed to exchange authorization code with CMMD.");
|
|
68995
|
-
const { tokenObj, cmmdIdentity, userId, accessToken, mcpToken,
|
|
68900
|
+
const { tokenObj, cmmdIdentity, userId, accessToken, mcpToken, sessionGrant, sessionGrantExpiresAt, forgeOwnerAssertion, isJwt, patToStore, jwtToStore } = yield* resolveCmmdTokenBundleCredentials(tokenResult);
|
|
68996
68901
|
const tokenResponseLogDetails = describeCmmdTokenResponseForLog({
|
|
68997
68902
|
tokenObj,
|
|
68998
68903
|
tokenResult,
|
|
68999
68904
|
accessToken,
|
|
69000
68905
|
mcpToken,
|
|
69001
|
-
refreshToken,
|
|
69002
68906
|
isJwt,
|
|
69003
68907
|
userId,
|
|
69004
68908
|
patToStore,
|
|
@@ -69051,7 +68955,6 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", gen(functio
|
|
|
69051
68955
|
accessToken: patToStore,
|
|
69052
68956
|
identity: cmmdIdentity,
|
|
69053
68957
|
jwtToken: jwtToStore,
|
|
69054
|
-
refreshToken,
|
|
69055
68958
|
sessionGrant,
|
|
69056
68959
|
sessionGrantExpiresAt
|
|
69057
68960
|
};
|
|
@@ -71045,51 +70948,83 @@ function isCmmdUserAccessJwt(token) {
|
|
|
71045
70948
|
}
|
|
71046
70949
|
//#endregion
|
|
71047
70950
|
//#region src/command/cmmdJwt.ts
|
|
71048
|
-
function parseCmmdOAuthRefreshResponse(value) {
|
|
71049
|
-
if (!value || typeof value !== "object") return null;
|
|
71050
|
-
const record = value;
|
|
71051
|
-
const accessToken = record["access_token"];
|
|
71052
|
-
if (typeof accessToken !== "string" || accessToken.trim().length === 0) return null;
|
|
71053
|
-
const refreshToken = record["refresh_token"];
|
|
71054
|
-
return {
|
|
71055
|
-
accessToken: accessToken.trim(),
|
|
71056
|
-
refreshToken: typeof refreshToken === "string" && refreshToken.trim().length > 0 ? refreshToken.trim() : null
|
|
71057
|
-
};
|
|
71058
|
-
}
|
|
71059
70951
|
/**
|
|
71060
|
-
* Read
|
|
71061
|
-
*
|
|
71062
|
-
|
|
70952
|
+
* Read a minted JWT with finite issuance and expiry times and a lifetime of
|
|
70953
|
+
* at most one hour. Reject expired tokens and tokens issued in the future.
|
|
70954
|
+
* This checks the mint contract only; CMMD verifies signatures on use.
|
|
70955
|
+
*/
|
|
70956
|
+
/** Machine-client tokens live at most one hour; anything longer is a CMMD
|
|
70957
|
+
* contract violation that must not silently extend forge sessions. */
|
|
70958
|
+
const MAX_MACHINE_TOKEN_LIFETIME_SECONDS = 3600;
|
|
70959
|
+
/**
|
|
70960
|
+
* The minted access token's lifetime in seconds, read from its JWT `exp` and
|
|
70961
|
+
* `iat` claims without verifying the signature (the signature is checked on
|
|
70962
|
+
* every use at the guard; here the claims only measure the lifetime). `iat`
|
|
70963
|
+
* falls back to the current time when absent. Null when the payload cannot be
|
|
70964
|
+
* read, which is itself a contract violation.
|
|
70965
|
+
*/
|
|
70966
|
+
function readJwtLifetimeSeconds(accessToken) {
|
|
70967
|
+
const claims = decodeJwtPayload(accessToken);
|
|
70968
|
+
const expiresAt = claims?.["exp"];
|
|
70969
|
+
const issuedAtRaw = claims?.["iat"];
|
|
70970
|
+
if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return null;
|
|
70971
|
+
const issuedAt = typeof issuedAtRaw === "number" && Number.isFinite(issuedAtRaw) ? issuedAtRaw : Math.floor(Date.now() / 1e3);
|
|
70972
|
+
const now = Date.now() / 1e3;
|
|
70973
|
+
if (expiresAt <= now || issuedAt > now) return null;
|
|
70974
|
+
return expiresAt - issuedAt;
|
|
70975
|
+
}
|
|
70976
|
+
/** True when the minted token's lifetime honors the one-hour contract. A
|
|
70977
|
+
* refusal is logged here because the reason is mint-contract specific and the
|
|
70978
|
+
* caller only wants the token-or-null answer. */
|
|
70979
|
+
function mintLifetimeWithinContract(accessToken) {
|
|
70980
|
+
const lifetime = readJwtLifetimeSeconds(accessToken);
|
|
70981
|
+
if (lifetime !== null && lifetime <= MAX_MACHINE_TOKEN_LIFETIME_SECONDS) return true;
|
|
70982
|
+
console.warn(`[cmmd-auth] CMMD minted a forge token outside the one-hour contract (lifetime=${lifetime === null ? "unreadable" : `${lifetime}s`}); refusing to persist it`);
|
|
70983
|
+
return false;
|
|
70984
|
+
}
|
|
71063
70985
|
function parseCmmdTokenMintResponse(value) {
|
|
71064
70986
|
if (!value || typeof value !== "object") return null;
|
|
71065
70987
|
const accessToken = value["access_token"];
|
|
71066
70988
|
if (typeof accessToken !== "string" || accessToken.trim().length === 0) return null;
|
|
71067
|
-
|
|
70989
|
+
const trimmed = accessToken.trim();
|
|
70990
|
+
if (!mintLifetimeWithinContract(trimmed)) return null;
|
|
70991
|
+
return { accessToken: trimmed };
|
|
71068
70992
|
}
|
|
71069
70993
|
/**
|
|
71070
|
-
* How long the mint may take before the
|
|
70994
|
+
* How long the mint may take before the caller gives up on it. Minting is one
|
|
71071
70995
|
* grant lookup and one signature on CMMD's side; anything slower is treated
|
|
71072
|
-
* like a failure and the
|
|
70996
|
+
* like a failure and the resolution returns null.
|
|
71073
70997
|
*/
|
|
71074
70998
|
const MINT_TIMEOUT_MS = 1e4;
|
|
71075
70999
|
/**
|
|
71000
|
+
* The OAuth error codes that are evidence the grant itself is dead, and the
|
|
71001
|
+
* only mint refusals allowed to feed the credential-rejection ledger.
|
|
71002
|
+
*
|
|
71003
|
+
* This set is grant-specific on purpose. Per RFC 6749, `invalid_grant` names
|
|
71004
|
+
* the grant. `invalid_client` names the client (CMMD did not accept
|
|
71005
|
+
* `cmmd-forge-web`) and `invalid_request` names the request shape (a missing or
|
|
71006
|
+
* unsupported parameter, for example after a CMMD contract change); both would
|
|
71007
|
+
* fire for every session at once, so a single misconfiguration or
|
|
71008
|
+
* deploy-ordering mistake could delete every stored credential. Those codes
|
|
71009
|
+
* stay logged refusals that resolve null and leave the row in place. If CMMD's
|
|
71010
|
+
* contract ever makes one of them grant-specific, document that here before
|
|
71011
|
+
* widening this set.
|
|
71012
|
+
*/
|
|
71013
|
+
const REJECTED_OAUTH_ERROR_CODES = new Set(["invalid_grant"]);
|
|
71014
|
+
/**
|
|
71076
71015
|
* Mint a short-lived forge-scoped access token from a durable session grant
|
|
71077
|
-
* (forge#3518 expand phase).
|
|
71016
|
+
* (forge#3518 expand; the only lane since the forge#3521 contract phase).
|
|
71078
71017
|
*
|
|
71079
|
-
* The grant is non-rotating and redeemable on demand, so this is the
|
|
71080
|
-
*
|
|
71081
|
-
*
|
|
71082
|
-
*
|
|
71083
|
-
*
|
|
71018
|
+
* The grant is non-rotating and redeemable on demand, so this is the entire
|
|
71019
|
+
* refresh story: call it again when the short-lived token expires. CMMD answers
|
|
71020
|
+
* with `access_token` only (no refresh token, no MCP PAT, nothing durable), so
|
|
71021
|
+
* a session living on this lane never accumulates long-lived credential state
|
|
71022
|
+
* beyond the grant it already holds.
|
|
71084
71023
|
*
|
|
71085
71024
|
* Returns null for every failure because the resolver wants a value, not a
|
|
71086
|
-
* thrown error; the failure modes are logged by status and OAuth error code
|
|
71087
|
-
* the same way `refreshCmmdJwt` logs them. A rejection here is never fed to
|
|
71088
|
-
* the credential-rejection ledger: the ledger exists to prove a session's
|
|
71089
|
-
* *rotation* credential is dead, and the grant dying says nothing about the
|
|
71090
|
-
* refresh token the legacy fallback still owns.
|
|
71025
|
+
* thrown error; the failure modes are logged by status and OAuth error code.
|
|
71091
71026
|
*/
|
|
71092
|
-
function mintCmmdShortLivedToken(grantToken) {
|
|
71027
|
+
function mintCmmdShortLivedToken(grantToken, observer) {
|
|
71093
71028
|
return fetch(new URL("/api/mcp-server/oauth/token", resolveCommandAppUrl()).toString(), {
|
|
71094
71029
|
method: "POST",
|
|
71095
71030
|
headers: { "Content-Type": "application/json" },
|
|
@@ -71102,6 +71037,7 @@ function mintCmmdShortLivedToken(grantToken) {
|
|
|
71102
71037
|
}).then(async (response) => {
|
|
71103
71038
|
if (!response.ok) {
|
|
71104
71039
|
const errorCode = await readOAuthErrorCode(response);
|
|
71040
|
+
if (errorCode !== null && REJECTED_OAUTH_ERROR_CODES.has(errorCode)) observer?.onCredentialRejected?.();
|
|
71105
71041
|
console.warn(`[cmmd-auth] CMMD refused a forge token mint with ${response.status}${errorCode === null ? "" : ` (${errorCode})`}`);
|
|
71106
71042
|
return null;
|
|
71107
71043
|
}
|
|
@@ -71114,103 +71050,149 @@ function mintCmmdShortLivedToken(grantToken) {
|
|
|
71114
71050
|
});
|
|
71115
71051
|
}
|
|
71116
71052
|
/**
|
|
71117
|
-
* The
|
|
71118
|
-
*
|
|
71119
|
-
*
|
|
71053
|
+
* The OAuth `error` code from a refusal, lowercased, or null when there is none.
|
|
71054
|
+
*
|
|
71055
|
+
* Best effort by design: a body that is not JSON, or is already consumed, must
|
|
71056
|
+
* not turn a logged failure into an unlogged one. Null also reads correctly as
|
|
71057
|
+
* "no evidence the grant is dead", which is the safe answer for a caller
|
|
71058
|
+
* deciding whether to keep the session's stored credential.
|
|
71120
71059
|
*/
|
|
71121
|
-
function
|
|
71122
|
-
|
|
71123
|
-
|
|
71124
|
-
|
|
71125
|
-
|
|
71126
|
-
|
|
71127
|
-
}
|
|
71060
|
+
async function readOAuthErrorCode(response) {
|
|
71061
|
+
try {
|
|
71062
|
+
const code = (await response.json())["error"];
|
|
71063
|
+
return typeof code === "string" && code.trim().length > 0 ? code.trim().toLowerCase() : null;
|
|
71064
|
+
} catch {
|
|
71065
|
+
return null;
|
|
71066
|
+
}
|
|
71128
71067
|
}
|
|
71068
|
+
//#endregion
|
|
71069
|
+
//#region src/cmmdConnection/cmmdCredentialRejection.ts
|
|
71129
71070
|
/**
|
|
71130
|
-
*
|
|
71131
|
-
*
|
|
71132
|
-
* The grant is CMMD's designed recovery path for exactly the case a rotating
|
|
71133
|
-
* refresh token cannot cover: the refresh token is single-use, so one lost or
|
|
71134
|
-
* raced rotation strands the session with no way back. The grant is
|
|
71135
|
-
* NON-rotating — the same token redeems repeatedly until CMMD expires or
|
|
71136
|
-
* revokes it — so redemption is safe to retry and cannot race itself.
|
|
71071
|
+
* How long a recorded rejection stays usable.
|
|
71137
71072
|
*
|
|
71138
|
-
*
|
|
71139
|
-
*
|
|
71073
|
+
* Long enough to span the resolver ladder and the response that follows it,
|
|
71074
|
+
* short enough that it cannot outlive the credential it describes.
|
|
71140
71075
|
*/
|
|
71141
|
-
|
|
71142
|
-
|
|
71143
|
-
|
|
71144
|
-
|
|
71145
|
-
|
|
71146
|
-
|
|
71147
|
-
|
|
71148
|
-
|
|
71149
|
-
|
|
71150
|
-
|
|
71151
|
-
|
|
71152
|
-
|
|
71153
|
-
|
|
71154
|
-
|
|
71155
|
-
|
|
71156
|
-
|
|
71076
|
+
const REJECTION_TTL_MS = 6e4;
|
|
71077
|
+
const rejectedAt = /* @__PURE__ */ new Map();
|
|
71078
|
+
/** Record that CMMD refused this session's credential outright. */
|
|
71079
|
+
function noteCmmdCredentialRejected(sessionId) {
|
|
71080
|
+
rejectedAt.set(sessionId, Date.now());
|
|
71081
|
+
}
|
|
71082
|
+
/** Whether CMMD refused this session's credential recently enough to act on. */
|
|
71083
|
+
function hasCmmdCredentialRejection(sessionId) {
|
|
71084
|
+
const observedAt = rejectedAt.get(sessionId);
|
|
71085
|
+
if (observedAt === void 0) return false;
|
|
71086
|
+
if (Date.now() - observedAt <= REJECTION_TTL_MS) return true;
|
|
71087
|
+
rejectedAt.delete(sessionId);
|
|
71088
|
+
return false;
|
|
71089
|
+
}
|
|
71090
|
+
/** Drop a recorded rejection, once it has been acted on or superseded. */
|
|
71091
|
+
function forgetCmmdCredentialRejection(sessionId) {
|
|
71092
|
+
rejectedAt.delete(sessionId);
|
|
71157
71093
|
}
|
|
71158
|
-
const REJECTED_OAUTH_ERROR_CODES = new Set([
|
|
71159
|
-
"invalid_grant",
|
|
71160
|
-
"invalid_client",
|
|
71161
|
-
"invalid_request"
|
|
71162
|
-
]);
|
|
71163
71094
|
/**
|
|
71164
|
-
*
|
|
71095
|
+
* Clear a session's CMMD credentials, but only if CMMD rejected them.
|
|
71165
71096
|
*
|
|
71166
|
-
*
|
|
71167
|
-
*
|
|
71168
|
-
*
|
|
71169
|
-
* phone that had silently stopped accepting its session cost an afternoon
|
|
71170
|
-
* partly because a failed rotation here was indistinguishable from one that
|
|
71171
|
-
* never ran. The token itself is never logged; the status and the OAuth error
|
|
71172
|
-
* code are what identify the failure, and `invalid_grant` in particular means
|
|
71173
|
-
* the token was already spent, which is a different fix from a 500.
|
|
71097
|
+
* This is the only door the 401 path may use. A resolver that returned null
|
|
71098
|
+
* without a recorded rejection is a transient failure and leaves the stored
|
|
71099
|
+
* credential exactly where it is, so the next request can mint from it again.
|
|
71174
71100
|
*/
|
|
71175
|
-
function
|
|
71176
|
-
|
|
71177
|
-
|
|
71178
|
-
|
|
71179
|
-
|
|
71180
|
-
|
|
71181
|
-
|
|
71182
|
-
|
|
71183
|
-
|
|
71184
|
-
|
|
71185
|
-
|
|
71186
|
-
|
|
71187
|
-
|
|
71188
|
-
|
|
71189
|
-
|
|
71190
|
-
|
|
71191
|
-
|
|
71192
|
-
|
|
71193
|
-
|
|
71194
|
-
|
|
71195
|
-
console.warn(`[cmmd-auth] CMMD JWT rotation could not be completed: ${String(cause)}`);
|
|
71196
|
-
return null;
|
|
71101
|
+
const clearCmmdAuthAfterCmmdRejection = (sessionId) => gen(function* () {
|
|
71102
|
+
if (!hasCmmdCredentialRejection(sessionId)) return;
|
|
71103
|
+
yield* clearUnrecoverableCmmdAuthForSession(sessionId, "cmmd-rejected-credential");
|
|
71104
|
+
forgetCmmdCredentialRejection(sessionId);
|
|
71105
|
+
});
|
|
71106
|
+
//#endregion
|
|
71107
|
+
//#region src/command/cmmdJwtResolver.ts
|
|
71108
|
+
function usableCmmdUserJwt(token) {
|
|
71109
|
+
return isCmmdUserAccessJwt(token) ? token : null;
|
|
71110
|
+
}
|
|
71111
|
+
/** Log a resolver decision with session context and a fixed reason, excluding credential values. */
|
|
71112
|
+
function logCmmdJwtRung(context, rung, outcome, reason) {
|
|
71113
|
+
return (outcome === "failed" ? logWarning$1 : logInfo)("CMMD JWT resolver rung.", {
|
|
71114
|
+
sessionId: context.sessionId,
|
|
71115
|
+
subject: context.subject,
|
|
71116
|
+
deviceType: context.deviceType,
|
|
71117
|
+
caller: context.caller,
|
|
71118
|
+
rung,
|
|
71119
|
+
outcome,
|
|
71120
|
+
reason
|
|
71197
71121
|
});
|
|
71198
71122
|
}
|
|
71123
|
+
/** In-flight mints, keyed by session. */
|
|
71124
|
+
const inFlightMints = /* @__PURE__ */ new Map();
|
|
71199
71125
|
/**
|
|
71200
|
-
*
|
|
71126
|
+
* Collapse concurrent mints for one session into a single upstream call. A
|
|
71127
|
+
* mint cannot race itself into `invalid_grant` (the grant is non-rotating),
|
|
71128
|
+
* so this is call-rate hygiene, not correctness: two tabs observing the same
|
|
71129
|
+
* expiry must not both hit CMMD when one mint would do.
|
|
71130
|
+
*/
|
|
71131
|
+
function mintOnceForSession(sessionId, grantToken) {
|
|
71132
|
+
return promise(() => {
|
|
71133
|
+
const existing = inFlightMints.get(sessionId);
|
|
71134
|
+
if (existing) return existing;
|
|
71135
|
+
const pending = mintCmmdShortLivedToken(grantToken, { onCredentialRejected: () => noteCmmdCredentialRejected(sessionId) }).catch(() => null).finally(() => {
|
|
71136
|
+
inFlightMints.delete(sessionId);
|
|
71137
|
+
});
|
|
71138
|
+
inFlightMints.set(sessionId, pending);
|
|
71139
|
+
return pending;
|
|
71140
|
+
});
|
|
71141
|
+
}
|
|
71142
|
+
/**
|
|
71143
|
+
* Resolve the CMMD REST JWT for a Forge session.
|
|
71201
71144
|
*
|
|
71202
|
-
*
|
|
71203
|
-
*
|
|
71204
|
-
*
|
|
71205
|
-
*
|
|
71145
|
+
* Forge session auth is long-lived compared with the short-lived CMMD JWT. When
|
|
71146
|
+
* the session JWT expires, mint a fresh short-lived token from the session's
|
|
71147
|
+
* durable, non-rotating grant: CMMD answers with one forge-scoped access token
|
|
71148
|
+
* (TTL at most one hour) and nothing else, and minting is the entire refresh
|
|
71149
|
+
* story (forge#3521 contract phase). There is no rotation to fence, no
|
|
71150
|
+
* single-use token to single-flight, and no sibling credential to borrow.
|
|
71151
|
+
*
|
|
71152
|
+
* Fail-closed: never returns a non-user / malformed Bearer. Callers must map
|
|
71153
|
+
* null to CMMD_AUTH_REQUIRED and must not call CMMD upstream with a bad token.
|
|
71206
71154
|
*/
|
|
71207
|
-
|
|
71208
|
-
|
|
71209
|
-
|
|
71210
|
-
|
|
71211
|
-
|
|
71212
|
-
|
|
71155
|
+
function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false, staleAccessToken = null }) {
|
|
71156
|
+
const cached = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
|
|
71157
|
+
if (cached && !isJwtExpiredForSession(sessionId)) {
|
|
71158
|
+
if (!forceRefresh) return succeed(cached);
|
|
71159
|
+
if (staleAccessToken && cached !== staleAccessToken) return succeed(cached).pipe(tap(() => gen(function* () {
|
|
71160
|
+
const deviceType = yield* readClientDeviceTypeForSession(sessionId);
|
|
71161
|
+
yield* logCmmdJwtRung({
|
|
71162
|
+
sessionId,
|
|
71163
|
+
subject: subject?.trim() || null,
|
|
71164
|
+
deviceType,
|
|
71165
|
+
caller: logPrefix
|
|
71166
|
+
}, "cache", "skipped", "forced refresh skipped: cache already holds a token another caller minted");
|
|
71167
|
+
})));
|
|
71213
71168
|
}
|
|
71169
|
+
const normalizedSubject = subject?.trim() || null;
|
|
71170
|
+
if (!normalizedSubject && !forceRefresh && readCommandSessionGrantForSession(sessionId) === null) return succeed(null);
|
|
71171
|
+
return gen(function* () {
|
|
71172
|
+
const context = {
|
|
71173
|
+
sessionId,
|
|
71174
|
+
subject: normalizedSubject,
|
|
71175
|
+
deviceType: yield* readClientDeviceTypeForSession(sessionId),
|
|
71176
|
+
caller: logPrefix
|
|
71177
|
+
};
|
|
71178
|
+
const grantToken = readCommandSessionGrantForSession(sessionId);
|
|
71179
|
+
if (!grantToken) {
|
|
71180
|
+
yield* logCmmdJwtRung(context, "mint", "skipped", "no session grant stored for this session");
|
|
71181
|
+
return null;
|
|
71182
|
+
}
|
|
71183
|
+
yield* logCmmdJwtRung(context, "mint", "attempted", "minting a short-lived forge-scoped token from this session's grant");
|
|
71184
|
+
yield* update$3(withAttributes(cmmdTokenMintTotal, metricAttributes({ outcome: "attempted" })), 1);
|
|
71185
|
+
const accessToken = usableCmmdUserJwt((yield* mintOnceForSession(sessionId, grantToken))?.accessToken);
|
|
71186
|
+
if (!accessToken) {
|
|
71187
|
+
yield* update$3(withAttributes(cmmdTokenMintTotal, metricAttributes({ outcome: "failed" })), 1);
|
|
71188
|
+
yield* logCmmdJwtRung(context, "mint", "failed", "CMMD token mint did not return a usable user access JWT");
|
|
71189
|
+
return null;
|
|
71190
|
+
}
|
|
71191
|
+
yield* setCommandJwtForSession(sessionId, accessToken).pipe(ignore({ log: true }));
|
|
71192
|
+
yield* update$3(withAttributes(cmmdTokenMintTotal, metricAttributes({ outcome: "succeeded" })), 1);
|
|
71193
|
+
yield* logCmmdJwtRung(context, "mint", "succeeded", "CMMD minted a short-lived forge-scoped token");
|
|
71194
|
+
return accessToken;
|
|
71195
|
+
});
|
|
71214
71196
|
}
|
|
71215
71197
|
//#endregion
|
|
71216
71198
|
//#region ../../packages/shared/src/cmmdSessionPop.ts
|
|
@@ -71328,10 +71310,6 @@ function resolveCmmdSessionPopSigner() {
|
|
|
71328
71310
|
cachedSigner ??= createServerSigner();
|
|
71329
71311
|
return cachedSigner;
|
|
71330
71312
|
}
|
|
71331
|
-
/** The `pop_public_key` value to present at issuance, or null while disabled. */
|
|
71332
|
-
function cmmdSessionPopPublicKey() {
|
|
71333
|
-
return resolveCmmdSessionPopSigner()?.publicKeyPem ?? null;
|
|
71334
|
-
}
|
|
71335
71313
|
/**
|
|
71336
71314
|
* The three proof headers for one CMMD-origin request, or no headers while
|
|
71337
71315
|
* disabled. Each call carries a fresh nonce; never reuse the result across
|
|
@@ -71347,407 +71325,6 @@ async function cmmdSessionPopRequestHeaders(input) {
|
|
|
71347
71325
|
});
|
|
71348
71326
|
}
|
|
71349
71327
|
//#endregion
|
|
71350
|
-
//#region src/command/cmmdTokenMintLane.ts
|
|
71351
|
-
/**
|
|
71352
|
-
* The CMMD token-mint lane (forge#3518, expand phase).
|
|
71353
|
-
*
|
|
71354
|
-
* A new FIRST rung beside the legacy credential ladder: instead of rotating
|
|
71355
|
-
* CMMD's single-use refresh tokens, the resolver mints a short-lived
|
|
71356
|
-
* forge-scoped access token from the session's durable, non-rotating session
|
|
71357
|
-
* grant (`grant_type: forge_token_mint` on CMMD's token endpoint). The mint
|
|
71358
|
-
* spends nothing that cannot be spent again, so it cannot race itself into
|
|
71359
|
-
* `invalid_grant` the way a rotation can.
|
|
71360
|
-
*
|
|
71361
|
-
* Behind a config flag that is OFF unless explicitly enabled: production
|
|
71362
|
-
* keeps the legacy ladder as the only lane until this is switched on
|
|
71363
|
-
* deliberately. When enabled, the lane is tried before the legacy ladder;
|
|
71364
|
-
* every legacy rung (rotation fence, self-refresh, grant redemption,
|
|
71365
|
-
* bootstrap promotion, subject fallback) still exists below it unchanged and
|
|
71366
|
-
* still guards itself with the rejection ledger.
|
|
71367
|
-
*/
|
|
71368
|
-
/** Env var that enables the mint lane. Unset and anything but an affirmative value is off. */
|
|
71369
|
-
const CMMD_TOKEN_MINT_LANE_ENV = "FORGE_CMMD_TOKEN_MINT_LANE";
|
|
71370
|
-
const AFFIRMATIVE_VALUES = new Set([
|
|
71371
|
-
"1",
|
|
71372
|
-
"on",
|
|
71373
|
-
"true",
|
|
71374
|
-
"yes"
|
|
71375
|
-
]);
|
|
71376
|
-
/**
|
|
71377
|
-
* Whether the mint lane may run. Default off everywhere: a missing env var is
|
|
71378
|
-
* the production posture, and an opt-in flag must not half-default to on in
|
|
71379
|
-
* any environment a misconfigured deploy might land in.
|
|
71380
|
-
*/
|
|
71381
|
-
function isCmmdTokenMintLaneEnabled(env = process.env) {
|
|
71382
|
-
const raw = env[CMMD_TOKEN_MINT_LANE_ENV]?.trim().toLowerCase();
|
|
71383
|
-
return raw !== void 0 && AFFIRMATIVE_VALUES.has(raw);
|
|
71384
|
-
}
|
|
71385
|
-
//#endregion
|
|
71386
|
-
//#region src/command/cmmdRotationFence.ts
|
|
71387
|
-
/**
|
|
71388
|
-
* Log and count a fence application, without changing the verdict it reports.
|
|
71389
|
-
*
|
|
71390
|
-
* The two facts named in the log line are exactly the ones the predicate
|
|
71391
|
-
* above the caller already computed — whether this session ever deposited a
|
|
71392
|
-
* refresh token of its own, and (only when that was false) whether the
|
|
71393
|
-
* session row reads as a desktop bearer session. Neither fact nor the log
|
|
71394
|
-
* line changes what the caller returns.
|
|
71395
|
-
*/
|
|
71396
|
-
function logRotationFenceApplied(input) {
|
|
71397
|
-
return logInfo("CMMD rotation fence applied: session may not rotate its own credential.", {
|
|
71398
|
-
sessionId: input.sessionId,
|
|
71399
|
-
hasStoredRefreshToken: input.hasStoredRefreshToken,
|
|
71400
|
-
isDesktopBearerSession: input.isDesktopBearerSession
|
|
71401
|
-
}).pipe(andThen(() => update$3(withAttributes(cmmdRotationFenceAppliedTotal, metricAttributes({})), 1)));
|
|
71402
|
-
}
|
|
71403
|
-
/**
|
|
71404
|
-
* True when Forge must not rotate this session's CMMD credentials.
|
|
71405
|
-
*
|
|
71406
|
-
* Fails closed: if the session row cannot be read, assume the fence applies.
|
|
71407
|
-
* Spending a token Forge had no right to spend is worse than one extra
|
|
71408
|
-
* reconnect.
|
|
71409
|
-
*/
|
|
71410
|
-
function isRotationFencedSession(sessionId) {
|
|
71411
|
-
const hasStoredRefreshToken = readCommandRefreshTokenForSession(sessionId) !== null;
|
|
71412
|
-
if (hasStoredRefreshToken) return succeed(false);
|
|
71413
|
-
return isDesktopBearerAuthSession(sessionId).pipe(match$1({
|
|
71414
|
-
onFailure: () => ({
|
|
71415
|
-
fenced: true,
|
|
71416
|
-
isDesktopBearerSession: null
|
|
71417
|
-
}),
|
|
71418
|
-
onSuccess: (isDesktopBearerSession) => ({
|
|
71419
|
-
fenced: isDesktopBearerSession,
|
|
71420
|
-
isDesktopBearerSession
|
|
71421
|
-
})
|
|
71422
|
-
}), tap(({ fenced, isDesktopBearerSession }) => fenced ? logRotationFenceApplied({
|
|
71423
|
-
sessionId,
|
|
71424
|
-
hasStoredRefreshToken,
|
|
71425
|
-
isDesktopBearerSession
|
|
71426
|
-
}) : void_$1), map$3(({ fenced }) => fenced));
|
|
71427
|
-
}
|
|
71428
|
-
//#endregion
|
|
71429
|
-
//#region src/cmmdConnection/cmmdCredentialRejection.ts
|
|
71430
|
-
/**
|
|
71431
|
-
* How long a recorded rejection stays usable.
|
|
71432
|
-
*
|
|
71433
|
-
* Long enough to span the resolver ladder and the response that follows it,
|
|
71434
|
-
* short enough that it cannot outlive the credential it describes.
|
|
71435
|
-
*/
|
|
71436
|
-
const REJECTION_TTL_MS = 6e4;
|
|
71437
|
-
const rejectedAt = /* @__PURE__ */ new Map();
|
|
71438
|
-
/** Record that CMMD refused this session's credential outright. */
|
|
71439
|
-
function noteCmmdCredentialRejected(sessionId) {
|
|
71440
|
-
rejectedAt.set(sessionId, Date.now());
|
|
71441
|
-
}
|
|
71442
|
-
/** Whether CMMD refused this session's credential recently enough to act on. */
|
|
71443
|
-
function hasCmmdCredentialRejection(sessionId) {
|
|
71444
|
-
const observedAt = rejectedAt.get(sessionId);
|
|
71445
|
-
if (observedAt === void 0) return false;
|
|
71446
|
-
if (Date.now() - observedAt <= REJECTION_TTL_MS) return true;
|
|
71447
|
-
rejectedAt.delete(sessionId);
|
|
71448
|
-
return false;
|
|
71449
|
-
}
|
|
71450
|
-
/** Drop a recorded rejection, once it has been acted on or superseded. */
|
|
71451
|
-
function forgetCmmdCredentialRejection(sessionId) {
|
|
71452
|
-
rejectedAt.delete(sessionId);
|
|
71453
|
-
}
|
|
71454
|
-
/**
|
|
71455
|
-
* Clear a session's CMMD credentials, but only if CMMD rejected them.
|
|
71456
|
-
*
|
|
71457
|
-
* This is the only door the 401 path may use. A resolver that returned null
|
|
71458
|
-
* because it was not allowed to try is a policy refusal and leaves the stored
|
|
71459
|
-
* credential exactly where it is, so the next request can rotate it.
|
|
71460
|
-
*/
|
|
71461
|
-
const clearCmmdAuthAfterCmmdRejection = (sessionId) => gen(function* () {
|
|
71462
|
-
if (!hasCmmdCredentialRejection(sessionId)) return;
|
|
71463
|
-
yield* clearUnrecoverableCmmdAuthForSession(sessionId, "cmmd-rejected-credential");
|
|
71464
|
-
forgetCmmdCredentialRejection(sessionId);
|
|
71465
|
-
});
|
|
71466
|
-
//#endregion
|
|
71467
|
-
//#region src/command/cmmdJwtResolver.ts
|
|
71468
|
-
function usableCmmdUserJwt(token) {
|
|
71469
|
-
return isCmmdUserAccessJwt(token) ? token : null;
|
|
71470
|
-
}
|
|
71471
|
-
function storeResolvedJwt(sessionId, accessToken, refreshToken) {
|
|
71472
|
-
return setCommandJwtForSession(sessionId, accessToken, refreshToken ? { refreshToken } : {}).pipe(ignore({ log: true }));
|
|
71473
|
-
}
|
|
71474
|
-
function logCmmdJwtRung(context, rung, outcome, reason, lane = "legacy") {
|
|
71475
|
-
return (outcome === "failed" ? logWarning$1 : logInfo)("CMMD JWT resolver rung.", {
|
|
71476
|
-
sessionId: context.sessionId,
|
|
71477
|
-
subject: context.subject,
|
|
71478
|
-
deviceType: context.deviceType,
|
|
71479
|
-
caller: context.caller,
|
|
71480
|
-
lane,
|
|
71481
|
-
rung,
|
|
71482
|
-
outcome,
|
|
71483
|
-
reason
|
|
71484
|
-
});
|
|
71485
|
-
}
|
|
71486
|
-
function recordCmmdCredentialRefreshMetric(rung, outcome) {
|
|
71487
|
-
return update$3(withAttributes(cmmdCredentialRefreshTotal, metricAttributes({
|
|
71488
|
-
rung,
|
|
71489
|
-
outcome
|
|
71490
|
-
})), 1);
|
|
71491
|
-
}
|
|
71492
|
-
const inFlightRefreshes = /* @__PURE__ */ new Map();
|
|
71493
|
-
/** In-flight mints, keyed by session, same shape as `inFlightRefreshes`. */
|
|
71494
|
-
const inFlightMints = /* @__PURE__ */ new Map();
|
|
71495
|
-
/**
|
|
71496
|
-
* Collapse concurrent mints for one session into a single upstream call. A
|
|
71497
|
-
* mint cannot race itself into `invalid_grant` (the grant is non-rotating),
|
|
71498
|
-
* so this is call-rate hygiene, not correctness: two tabs observing the same
|
|
71499
|
-
* expiry must not both hit CMMD when one mint would do.
|
|
71500
|
-
*/
|
|
71501
|
-
function mintOnceForSession(sessionId, grantToken) {
|
|
71502
|
-
return promise(() => {
|
|
71503
|
-
const existing = inFlightMints.get(sessionId);
|
|
71504
|
-
if (existing) return existing;
|
|
71505
|
-
const pending = mintCmmdShortLivedToken(grantToken).catch(() => null).finally(() => {
|
|
71506
|
-
inFlightMints.delete(sessionId);
|
|
71507
|
-
});
|
|
71508
|
-
inFlightMints.set(sessionId, pending);
|
|
71509
|
-
return pending;
|
|
71510
|
-
});
|
|
71511
|
-
}
|
|
71512
|
-
function refreshOnceForSession(sessionId, refreshToken) {
|
|
71513
|
-
return promise(() => {
|
|
71514
|
-
const existing = inFlightRefreshes.get(sessionId);
|
|
71515
|
-
if (existing) return existing;
|
|
71516
|
-
const pending = refreshCmmdJwt(refreshToken, { onCredentialRejected: () => noteCmmdCredentialRejected(sessionId) }).catch(() => null).finally(() => {
|
|
71517
|
-
inFlightRefreshes.delete(sessionId);
|
|
71518
|
-
});
|
|
71519
|
-
inFlightRefreshes.set(sessionId, pending);
|
|
71520
|
-
return pending;
|
|
71521
|
-
});
|
|
71522
|
-
}
|
|
71523
|
-
/**
|
|
71524
|
-
* Rotate this session's **own** CMMD refresh token.
|
|
71525
|
-
*
|
|
71526
|
-
* CMMD rotates refresh tokens single-use — the presented hash is revoked inside
|
|
71527
|
-
* the rotation transaction (CMMD `server/jwt-auth.ts`). A session must
|
|
71528
|
-
* therefore only ever present the refresh token it was issued: rotating a
|
|
71529
|
-
* sibling session's token silently invalidates that sibling's stored copy and
|
|
71530
|
-
* signs it out.
|
|
71531
|
-
*/
|
|
71532
|
-
function trySelfRefresh(sessionId, context) {
|
|
71533
|
-
const currentRefreshToken = readCommandRefreshTokenForSession(sessionId);
|
|
71534
|
-
if (!currentRefreshToken) return logCmmdJwtRung(context, "self-refresh", "skipped", "no refresh token stored for this session").pipe(as$3(null));
|
|
71535
|
-
return gen(function* () {
|
|
71536
|
-
yield* logCmmdJwtRung(context, "self-refresh", "attempted", "rotating this session's own stored refresh token");
|
|
71537
|
-
yield* recordCmmdCredentialRefreshMetric("self-refresh", "attempted");
|
|
71538
|
-
const refreshed = yield* refreshOnceForSession(sessionId, currentRefreshToken);
|
|
71539
|
-
const accessToken = usableCmmdUserJwt(refreshed?.accessToken);
|
|
71540
|
-
if (!accessToken) {
|
|
71541
|
-
yield* logCmmdJwtRung(context, "self-refresh", "failed", "CMMD refresh did not return a usable user access JWT");
|
|
71542
|
-
return null;
|
|
71543
|
-
}
|
|
71544
|
-
yield* storeResolvedJwt(sessionId, accessToken, refreshed?.refreshToken);
|
|
71545
|
-
yield* recordCmmdCredentialRefreshMetric("self-refresh", "succeeded");
|
|
71546
|
-
yield* logCmmdJwtRung(context, "self-refresh", "succeeded", "CMMD rotated the stored refresh token");
|
|
71547
|
-
return accessToken;
|
|
71548
|
-
});
|
|
71549
|
-
}
|
|
71550
|
-
/**
|
|
71551
|
-
* The mint lane's rung (forge#3518, expand phase): mint a short-lived
|
|
71552
|
-
* forge-scoped access token from this session's durable session grant.
|
|
71553
|
-
*
|
|
71554
|
-
* Tried FIRST, before the rotation fence and the whole legacy ladder, when
|
|
71555
|
-
* `FORGE_CMMD_TOKEN_MINT_LANE` is on. Minting spends nothing that cannot be
|
|
71556
|
-
* spent again (the grant is non-rotating), so unlike the legacy rungs it is
|
|
71557
|
-
* safe for a session the rotation fence fences off, and it never feeds the
|
|
71558
|
-
* credential-rejection ledger: this rung failing says nothing about whether
|
|
71559
|
-
* the legacy rungs below it can still succeed.
|
|
71560
|
-
*
|
|
71561
|
-
* Only the JWT is persisted (`setCommandJwtForSession`, no refresh token):
|
|
71562
|
-
* the short lifetime is the expiry story, and re-minting is the refresh
|
|
71563
|
-
* story. A session that lives entirely on this lane accumulates no new
|
|
71564
|
-
* long-lived credential state beyond the grant it already had.
|
|
71565
|
-
*/
|
|
71566
|
-
function tryMintShortLivedToken(sessionId, context) {
|
|
71567
|
-
const grantToken = readCommandSessionGrantForSession(sessionId);
|
|
71568
|
-
if (!grantToken) return logCmmdJwtRung(context, "mint", "skipped", "no session grant stored for this session", "mint").pipe(as$3(null));
|
|
71569
|
-
return gen(function* () {
|
|
71570
|
-
yield* logCmmdJwtRung(context, "mint", "attempted", "minting a short-lived forge-scoped token from this session's grant", "mint");
|
|
71571
|
-
yield* update$3(withAttributes(cmmdTokenMintTotal, metricAttributes({ outcome: "attempted" })), 1);
|
|
71572
|
-
const accessToken = usableCmmdUserJwt((yield* mintOnceForSession(sessionId, grantToken))?.accessToken);
|
|
71573
|
-
if (!accessToken) {
|
|
71574
|
-
yield* logCmmdJwtRung(context, "mint", "failed", "CMMD token mint did not return a usable user access JWT", "mint");
|
|
71575
|
-
return null;
|
|
71576
|
-
}
|
|
71577
|
-
yield* setCommandJwtForSession(sessionId, accessToken, {}).pipe(ignore({ log: true }));
|
|
71578
|
-
yield* update$3(withAttributes(cmmdTokenMintTotal, metricAttributes({ outcome: "succeeded" })), 1);
|
|
71579
|
-
yield* logCmmdJwtRung(context, "mint", "succeeded", "CMMD minted a short-lived forge-scoped token", "mint");
|
|
71580
|
-
return accessToken;
|
|
71581
|
-
});
|
|
71582
|
-
}
|
|
71583
|
-
/**
|
|
71584
|
-
* Last-resort recovery: redeem this session's durable CMMD session grant.
|
|
71585
|
-
*
|
|
71586
|
-
* A refresh token is single-use, so a session that lost a rotation — raced, or
|
|
71587
|
-
* stranded by a redeploy mid-flight — has no way back and the user is bounced
|
|
71588
|
-
* to /login. CMMD mints a long-lived, non-rotating grant at login for exactly
|
|
71589
|
-
* this, and redemption returns a fresh JWT, refresh token AND MCP PAT.
|
|
71590
|
-
*
|
|
71591
|
-
* Only reached after the session's own refresh has already failed, so this
|
|
71592
|
-
* never competes with a live rotation. The grant is not cleared on failure:
|
|
71593
|
-
* CMMD owns its lifetime, and a transient network failure must not throw away
|
|
71594
|
-
* the one credential that can still recover the session.
|
|
71595
|
-
*/
|
|
71596
|
-
function tryGrantRedemption(sessionId, context) {
|
|
71597
|
-
const grantToken = readCommandSessionGrantForSession(sessionId);
|
|
71598
|
-
if (!grantToken) return logCmmdJwtRung(context, "grant-redemption", "skipped", "no session grant stored for this session").pipe(as$3(null));
|
|
71599
|
-
return gen(function* () {
|
|
71600
|
-
yield* logCmmdJwtRung(context, "grant-redemption", "attempted", "redeeming this session's durable CMMD session grant");
|
|
71601
|
-
yield* recordCmmdCredentialRefreshMetric("grant-redemption", "attempted");
|
|
71602
|
-
const redeemed = yield* promise(() => redeemCmmdSessionGrant(grantToken, cmmdSessionPopPublicKey())).pipe(orElseSucceed(() => null));
|
|
71603
|
-
const accessToken = usableCmmdUserJwt(redeemed?.accessToken);
|
|
71604
|
-
if (!accessToken) {
|
|
71605
|
-
yield* logCmmdJwtRung(context, "grant-redemption", "failed", "session grant redemption did not return a usable user access JWT");
|
|
71606
|
-
return null;
|
|
71607
|
-
}
|
|
71608
|
-
if (redeemed?.mcpToken) yield* setCommandAccessTokenForSession(sessionId, redeemed.mcpToken).pipe(ignore({ log: true }));
|
|
71609
|
-
yield* storeResolvedJwt(sessionId, accessToken, redeemed?.refreshToken);
|
|
71610
|
-
yield* recordCmmdCredentialRefreshMetric("grant-redemption", "succeeded");
|
|
71611
|
-
yield* logCmmdJwtRung(context, "grant-redemption", "succeeded", "session grant redeemed");
|
|
71612
|
-
return accessToken;
|
|
71613
|
-
});
|
|
71614
|
-
}
|
|
71615
|
-
/**
|
|
71616
|
-
* Recover using only credentials this session owns, strongest first.
|
|
71617
|
-
*
|
|
71618
|
-
* Rotating its own refresh token is preferred; the durable grant is the
|
|
71619
|
-
* fallback for when that rotation is already gone. Neither touches another
|
|
71620
|
-
* session's credentials, so this ladder is safe to run from any path.
|
|
71621
|
-
*/
|
|
71622
|
-
function tryOwnCredentialRecovery(sessionId, context) {
|
|
71623
|
-
return gen(function* () {
|
|
71624
|
-
const refreshed = yield* trySelfRefresh(sessionId, context);
|
|
71625
|
-
if (refreshed) return refreshed;
|
|
71626
|
-
return yield* tryGrantRedemption(sessionId, context);
|
|
71627
|
-
});
|
|
71628
|
-
}
|
|
71629
|
-
function tryPromoteBootstrap(sessionId, subject, forceRefresh, context) {
|
|
71630
|
-
if (subject !== "owner-bootstrap" && subject !== "desktop-bootstrap") return logCmmdJwtRung(context, "promote-bootstrap", "skipped", "subject is not a bootstrap subject").pipe(as$3(null));
|
|
71631
|
-
return gen(function* () {
|
|
71632
|
-
yield* logCmmdJwtRung(context, "promote-bootstrap", "attempted", "promoting the most recent owner CMMD auth to this bootstrap session");
|
|
71633
|
-
yield* promoteMostRecentOwnerCommandAuthToSession(sessionId).pipe(catch_((error) => logWarning$1("Failed to promote owner CMMD auth for REST JWT resolution.", {
|
|
71634
|
-
cause: error,
|
|
71635
|
-
sessionId,
|
|
71636
|
-
subject
|
|
71637
|
-
})));
|
|
71638
|
-
const promotedJwt = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
|
|
71639
|
-
if (!forceRefresh && promotedJwt && !isJwtExpiredForSession(sessionId)) {
|
|
71640
|
-
yield* logCmmdJwtRung(context, "promote-bootstrap", "succeeded", "promoted owner JWT is usable");
|
|
71641
|
-
return promotedJwt;
|
|
71642
|
-
}
|
|
71643
|
-
yield* logCmmdJwtRung(context, "promote-bootstrap", "failed", forceRefresh ? "forced refresh: a promoted JWT is never reused as-is" : "no promoted JWT available, or it is already expired");
|
|
71644
|
-
return null;
|
|
71645
|
-
});
|
|
71646
|
-
}
|
|
71647
|
-
/**
|
|
71648
|
-
* Borrow a sibling session's still-valid CMMD access JWT.
|
|
71649
|
-
*
|
|
71650
|
-
* `readMostRecentJwtForSubject` only returns a JWT that has not expired, so
|
|
71651
|
-
* this is a cheap read-only recovery that avoids an upstream rotation. The
|
|
71652
|
-
* sibling's *refresh* token is deliberately NOT copied: it is single-use and
|
|
71653
|
-
* must stay private to the session it was issued to (see `trySelfRefresh`).
|
|
71654
|
-
* Copying it is what let one expired tab sign the user out everywhere.
|
|
71655
|
-
*/
|
|
71656
|
-
function trySubjectFallback(sessionId, subject, context) {
|
|
71657
|
-
return gen(function* () {
|
|
71658
|
-
yield* logCmmdJwtRung(context, "subject-fallback", "attempted", "looking for a sibling session's still-valid JWT for this subject");
|
|
71659
|
-
const fallbackJwt = usableCmmdUserJwt((yield* readMostRecentJwtForSubject(subject).pipe(orElseSucceed(() => null)))?.jwt);
|
|
71660
|
-
if (!fallbackJwt) {
|
|
71661
|
-
yield* logCmmdJwtRung(context, "subject-fallback", "failed", "no sibling session holds a still-valid JWT for this subject");
|
|
71662
|
-
return null;
|
|
71663
|
-
}
|
|
71664
|
-
yield* storeResolvedJwt(sessionId, fallbackJwt, null);
|
|
71665
|
-
yield* logCmmdJwtRung(context, "subject-fallback", "succeeded", "borrowed a sibling session's still-valid JWT");
|
|
71666
|
-
return fallbackJwt;
|
|
71667
|
-
});
|
|
71668
|
-
}
|
|
71669
|
-
/**
|
|
71670
|
-
* The rungs below the rotation fence: forced own-credential recovery,
|
|
71671
|
-
* bootstrap promotion, subject fallback, then own-credential recovery again
|
|
71672
|
-
* as the last resort. Split out of `resolveCmmdJwtForSession` so each half of
|
|
71673
|
-
* the ladder stays independently readable (and independently testable) —
|
|
71674
|
-
* the fence check above is the one decision the split does not touch.
|
|
71675
|
-
*/
|
|
71676
|
-
function runRecoveryLadder(sessionId, normalizedSubject, forceRefresh, context) {
|
|
71677
|
-
return gen(function* () {
|
|
71678
|
-
if (forceRefresh) {
|
|
71679
|
-
const forced = yield* tryOwnCredentialRecovery(sessionId, context);
|
|
71680
|
-
if (forced) return forced;
|
|
71681
|
-
}
|
|
71682
|
-
if (normalizedSubject) {
|
|
71683
|
-
const promoted = yield* tryPromoteBootstrap(sessionId, normalizedSubject, forceRefresh, context);
|
|
71684
|
-
if (promoted) return promoted;
|
|
71685
|
-
}
|
|
71686
|
-
if (!normalizedSubject) {
|
|
71687
|
-
yield* logCmmdJwtRung(context, "cache", "failed", "no subject to resolve and forced recovery already exhausted");
|
|
71688
|
-
return null;
|
|
71689
|
-
}
|
|
71690
|
-
if (!forceRefresh) {
|
|
71691
|
-
const fallback = yield* trySubjectFallback(sessionId, normalizedSubject, context);
|
|
71692
|
-
if (fallback) return fallback;
|
|
71693
|
-
}
|
|
71694
|
-
const recovered = yield* tryOwnCredentialRecovery(sessionId, context);
|
|
71695
|
-
if (!recovered) yield* logCmmdJwtRung(context, "cache", "failed", "recovery ladder exhausted: no rung produced a usable JWT");
|
|
71696
|
-
return recovered;
|
|
71697
|
-
});
|
|
71698
|
-
}
|
|
71699
|
-
/**
|
|
71700
|
-
* Resolve the CMMD REST JWT for a Forge session.
|
|
71701
|
-
*
|
|
71702
|
-
* Forge session auth is long-lived compared with the short-lived CMMD JWT.
|
|
71703
|
-
* When the session JWT expires, recover by minting a fresh short-lived token
|
|
71704
|
-
* from the session grant (the mint lane; default-off, forge#3518), or, on
|
|
71705
|
-
* the legacy lane, by promoting a valid subject-level JWT or rotating the
|
|
71706
|
-
* stored CMMD refresh token.
|
|
71707
|
-
*
|
|
71708
|
-
* Fail-closed: never returns a non-user / malformed Bearer. Callers must map
|
|
71709
|
-
* null to CMMD_AUTH_REQUIRED and must not call CMMD upstream with a bad token.
|
|
71710
|
-
*/
|
|
71711
|
-
function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false, staleAccessToken = null }) {
|
|
71712
|
-
const cached = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
|
|
71713
|
-
if (cached && !isJwtExpiredForSession(sessionId)) {
|
|
71714
|
-
if (!forceRefresh) return succeed(cached);
|
|
71715
|
-
if (staleAccessToken && cached !== staleAccessToken) return succeed(cached).pipe(tap(() => gen(function* () {
|
|
71716
|
-
const deviceType = yield* readClientDeviceTypeForSession(sessionId);
|
|
71717
|
-
yield* logCmmdJwtRung({
|
|
71718
|
-
sessionId,
|
|
71719
|
-
subject: subject?.trim() || null,
|
|
71720
|
-
deviceType,
|
|
71721
|
-
caller: logPrefix
|
|
71722
|
-
}, "cache", "skipped", "forced refresh skipped: cache already holds a token another caller rotated");
|
|
71723
|
-
})));
|
|
71724
|
-
}
|
|
71725
|
-
const normalizedSubject = subject?.trim() || null;
|
|
71726
|
-
const mintLaneEnabled = isCmmdTokenMintLaneEnabled();
|
|
71727
|
-
if (!normalizedSubject && !forceRefresh && !mintLaneEnabled) return succeed(null);
|
|
71728
|
-
return gen(function* () {
|
|
71729
|
-
const context = {
|
|
71730
|
-
sessionId,
|
|
71731
|
-
subject: normalizedSubject,
|
|
71732
|
-
deviceType: yield* readClientDeviceTypeForSession(sessionId),
|
|
71733
|
-
caller: logPrefix
|
|
71734
|
-
};
|
|
71735
|
-
if (mintLaneEnabled) {
|
|
71736
|
-
const minted = yield* tryMintShortLivedToken(sessionId, context);
|
|
71737
|
-
if (minted) return minted;
|
|
71738
|
-
if (!normalizedSubject && !forceRefresh) {
|
|
71739
|
-
yield* logCmmdJwtRung(context, "mint", "failed", "no subject to resolve and the mint lane produced nothing", "mint");
|
|
71740
|
-
return null;
|
|
71741
|
-
}
|
|
71742
|
-
}
|
|
71743
|
-
if (yield* isRotationFencedSession(sessionId)) {
|
|
71744
|
-
yield* logCmmdJwtRung(context, "rotation-fence", "failed", "session is fenced from rotating CMMD credentials");
|
|
71745
|
-
return null;
|
|
71746
|
-
}
|
|
71747
|
-
return yield* runRecoveryLadder(sessionId, normalizedSubject, forceRefresh, context);
|
|
71748
|
-
});
|
|
71749
|
-
}
|
|
71750
|
-
//#endregion
|
|
71751
71328
|
//#region src/cmmdData/cmmdRestProxyDeadline.ts
|
|
71752
71329
|
var CmmdProxyTimeoutError = class extends Error {
|
|
71753
71330
|
status;
|
|
@@ -102310,6 +101887,22 @@ var WorkspaceFileSystem = class extends Service$1()("forge/workspace/Services/Wo
|
|
|
102310
101887
|
//#region src/workspace/Services/WorkspaceBulkTransfer.ts
|
|
102311
101888
|
var WorkspaceBulkTransfer = class extends Service$1()("forge/workspace/Services/WorkspaceBulkTransfer") {};
|
|
102312
101889
|
//#endregion
|
|
101890
|
+
//#region src/workspace/Services/WorkspaceDirectoryListing.ts
|
|
101891
|
+
/**
|
|
101892
|
+
* WorkspaceDirectoryListing - Effect service contract for one directory's
|
|
101893
|
+
* immediate children.
|
|
101894
|
+
*
|
|
101895
|
+
* Owns workspace-root-confined directory listings for the Files tree: the
|
|
101896
|
+
* caller names a workspace root (the thread's checkout) and a root-relative
|
|
101897
|
+
* directory, and gets that directory's children back, one readdir deep.
|
|
101898
|
+
*
|
|
101899
|
+
* @module WorkspaceDirectoryListing
|
|
101900
|
+
*/
|
|
101901
|
+
/**
|
|
101902
|
+
* WorkspaceDirectoryListing - Service tag for workspace directory listings.
|
|
101903
|
+
*/
|
|
101904
|
+
var WorkspaceDirectoryListing = class extends Service$1()("forge/workspace/Services/WorkspaceDirectoryListing") {};
|
|
101905
|
+
//#endregion
|
|
102313
101906
|
//#region src/workspace/Services/WorkspaceReadinessState.ts
|
|
102314
101907
|
var WorkspaceReadinessState = class extends Service$1()("forge/workspace/Services/WorkspaceReadinessState") {};
|
|
102315
101908
|
//#endregion
|
|
@@ -107719,6 +107312,7 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(g
|
|
|
107719
107312
|
const discoverSourceControl = yield* makeCachedSourceControlDiscovery;
|
|
107720
107313
|
const workspaceFileSystem = yield* WorkspaceFileSystem;
|
|
107721
107314
|
const workspaceBulkTransfer = yield* WorkspaceBulkTransfer;
|
|
107315
|
+
const workspaceDirectoryListing = yield* WorkspaceDirectoryListing;
|
|
107722
107316
|
const projectCheckoutDeletion = yield* serviceOption(ProjectCheckoutDeletion);
|
|
107723
107317
|
const workspaceReadinessState = yield* WorkspaceReadinessState;
|
|
107724
107318
|
const projectSetupScriptRunner = yield* ProjectSetupScriptRunner;
|
|
@@ -108067,6 +107661,7 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(g
|
|
|
108067
107661
|
blockers: ["deletion_authority_unavailable"]
|
|
108068
107662
|
}), { "rpc.aggregate": "workspace" }),
|
|
108069
107663
|
[WS_METHODS.workspaceReadFile]: (input) => observeRpcStream(WS_METHODS.workspaceReadFile, workspaceBulkTransfer.readFile(input), { "rpc.aggregate": "workspace" }),
|
|
107664
|
+
[WS_METHODS.workspaceListDirectory]: (input) => observeRpcEffect(WS_METHODS.workspaceListDirectory, workspaceDirectoryListing.list(input), { "rpc.aggregate": "workspace" }),
|
|
108070
107665
|
[WS_METHODS.workspaceBeginWriteFile]: (input) => observeRpcEffect(WS_METHODS.workspaceBeginWriteFile, workspaceBulkTransfer.beginWrite(input), { "rpc.aggregate": "workspace" }),
|
|
108071
107666
|
[WS_METHODS.workspaceWriteFileChunk]: (input) => observeRpcEffect(WS_METHODS.workspaceWriteFileChunk, workspaceBulkTransfer.writeChunk(input), { "rpc.aggregate": "workspace" }),
|
|
108072
107667
|
[WS_METHODS.workspaceCommitWriteFile]: (input) => observeRpcEffect(WS_METHODS.workspaceCommitWriteFile, workspaceBulkTransfer.commitWrite(input), { "rpc.aggregate": "workspace" }),
|
|
@@ -115873,7 +115468,7 @@ const postgresSchemaStatements = [
|
|
|
115873
115468
|
"CREATE TABLE IF NOT EXISTS projection_thread_proposed_plans (\n plan_id TEXT PRIMARY KEY,\n thread_id TEXT NOT NULL,\n turn_id TEXT,\n plan_markdown TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n , implemented_at TEXT, implementation_thread_id TEXT)",
|
|
115874
115469
|
"CREATE TABLE IF NOT EXISTS auth_pairing_links (\n id TEXT PRIMARY KEY,\n credential TEXT NOT NULL UNIQUE,\n method TEXT NOT NULL,\n role TEXT NOT NULL,\n subject TEXT NOT NULL,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n consumed_at TEXT,\n revoked_at TEXT\n , label TEXT, credential_lookup TEXT)",
|
|
115875
115470
|
"CREATE TABLE IF NOT EXISTS auth_sessions (\n session_id TEXT PRIMARY KEY,\n subject TEXT NOT NULL,\n role TEXT NOT NULL,\n method TEXT NOT NULL,\n issued_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n revoked_at TEXT\n , client_label TEXT, client_ip_address TEXT, client_user_agent TEXT, client_device_type TEXT NOT NULL DEFAULT 'unknown', client_os TEXT, client_browser TEXT, last_connected_at TEXT)",
|
|
115876
|
-
"CREATE TABLE IF NOT EXISTS command_access_tokens (\n session_id TEXT PRIMARY KEY,\n access_token TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n , jwt_token TEXT, jwt_expires_at TEXT,
|
|
115471
|
+
"CREATE TABLE IF NOT EXISTS command_access_tokens (\n session_id TEXT PRIMARY KEY,\n access_token TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n , jwt_token TEXT, jwt_expires_at TEXT, cmmd_user_identity TEXT, session_grant TEXT, session_grant_expires_at TEXT)",
|
|
115877
115472
|
"CREATE TABLE IF NOT EXISTS comms_accounts (\n id TEXT PRIMARY KEY,\n provider TEXT NOT NULL,\n email TEXT NOT NULL,\n display_name TEXT,\n composio_entity_id TEXT NOT NULL,\n composio_connected_account_id TEXT NOT NULL,\n is_active INTEGER NOT NULL DEFAULT 1,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )",
|
|
115878
115473
|
"CREATE TABLE IF NOT EXISTS comms_messages (\n id TEXT PRIMARY KEY,\n provider TEXT NOT NULL,\n provider_message_id TEXT NOT NULL,\n provider_thread_id TEXT,\n account_id TEXT NOT NULL REFERENCES comms_accounts(id),\n direction TEXT NOT NULL DEFAULT 'inbound',\n from_address TEXT NOT NULL,\n from_name TEXT,\n to_addresses TEXT NOT NULL DEFAULT '[]',\n cc_addresses TEXT,\n subject TEXT,\n body_text TEXT,\n body_html TEXT,\n labels TEXT,\n sent_at TEXT NOT NULL,\n received_at TEXT,\n is_read INTEGER NOT NULL DEFAULT 0,\n is_starred INTEGER NOT NULL DEFAULT 0,\n has_attachments INTEGER NOT NULL DEFAULT 0,\n raw_payload TEXT,\n synced_at TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )",
|
|
115879
115474
|
"CREATE TABLE IF NOT EXISTS comms_sync_cursors (\n id TEXT PRIMARY KEY,\n provider TEXT NOT NULL,\n account_id TEXT NOT NULL REFERENCES comms_accounts(id),\n cursor_type TEXT NOT NULL,\n cursor_value TEXT NOT NULL,\n last_sync_at TEXT NOT NULL,\n last_sync_status TEXT NOT NULL DEFAULT 'success',\n last_error TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )",
|
|
@@ -116186,7 +115781,8 @@ const postgresUpgradeMigrations = new Map([
|
|
|
116186
115781
|
WHERE model_selection_json IS NULL`
|
|
116187
115782
|
]],
|
|
116188
115783
|
[77, [`ALTER TABLE projection_threads ADD COLUMN IF NOT EXISTS work_session_id TEXT`]],
|
|
116189
|
-
[78, [`ALTER TABLE projection_threads ADD COLUMN IF NOT EXISTS work_session_bound_at TEXT`]]
|
|
115784
|
+
[78, [`ALTER TABLE projection_threads ADD COLUMN IF NOT EXISTS work_session_bound_at TEXT`]],
|
|
115785
|
+
[79, [`ALTER TABLE command_access_tokens DROP COLUMN IF EXISTS refresh_token`]]
|
|
116190
115786
|
]);
|
|
116191
115787
|
function requirePostgresUpgradeStatements(version) {
|
|
116192
115788
|
const statements = postgresUpgradeMigrations.get(version);
|
|
@@ -116212,15 +115808,15 @@ const runPostgresMigrations = fn$1("runPostgresMigrations")(function* () {
|
|
|
116212
115808
|
ORDER BY version DESC
|
|
116213
115809
|
LIMIT 1
|
|
116214
115810
|
`)[0]?.version ?? 0;
|
|
116215
|
-
if (currentVersion >
|
|
115811
|
+
if (currentVersion > 79) return yield* die$1(/* @__PURE__ */ new Error(`Forge Postgres schema ${currentVersion} is newer than server schema 79.`));
|
|
116216
115812
|
if (currentVersion === 0) {
|
|
116217
115813
|
for (const statement of postgresSchemaStatements) yield* sql.unsafe(statement);
|
|
116218
115814
|
yield* sql`
|
|
116219
115815
|
INSERT INTO forge_schema_migrations (version)
|
|
116220
|
-
VALUES (${
|
|
115816
|
+
VALUES (${79})
|
|
116221
115817
|
ON CONFLICT (version) DO NOTHING
|
|
116222
115818
|
`;
|
|
116223
|
-
} else for (let version = currentVersion + 1; version <=
|
|
115819
|
+
} else for (let version = currentVersion + 1; version <= 79; version += 1) {
|
|
116224
115820
|
const statements = yield* sync$1(() => requirePostgresUpgradeStatements(version));
|
|
116225
115821
|
for (const statement of statements) yield* sql.unsafe(statement);
|
|
116226
115822
|
yield* sql`
|
|
@@ -119028,6 +118624,26 @@ var _078_ProjectionThreadWorkSessionBoundAt_default = gen(function* () {
|
|
|
119028
118624
|
`;
|
|
119029
118625
|
});
|
|
119030
118626
|
//#endregion
|
|
118627
|
+
//#region src/persistence/Migrations/079_DropCommandRefreshToken.ts
|
|
118628
|
+
/**
|
|
118629
|
+
* Drop the single-use CMMD refresh token from the session credential row.
|
|
118630
|
+
*
|
|
118631
|
+
* The rotation it powered is gone. forge#3518 added the CMMD mint lane, and
|
|
118632
|
+
* forge#3521 deleted the legacy ladder that sat beside it: the rotation fence,
|
|
118633
|
+
* the refresh single-flight, self-refresh, grant redemption, the subject
|
|
118634
|
+
* fallback, and the desktop refresh sweep. The resolver now mints a short-lived
|
|
118635
|
+
* JWT from the row's durable, non-rotating session grant, so a credential whose
|
|
118636
|
+
* every use invalidates it has nothing left to do on this row.
|
|
118637
|
+
*
|
|
118638
|
+
* Nothing is copied out first. Every surviving read of the column was deleted
|
|
118639
|
+
* in the same change; the value is a dead secret, and the kind of secret best
|
|
118640
|
+
* kept nowhere. Historical installs that ran migration 034 need this to run
|
|
118641
|
+
* after it, not instead of it.
|
|
118642
|
+
*/
|
|
118643
|
+
var _079_DropCommandRefreshToken_default = gen(function* () {
|
|
118644
|
+
yield* (yield* SqlClient)`ALTER TABLE command_access_tokens DROP COLUMN refresh_token`;
|
|
118645
|
+
});
|
|
118646
|
+
//#endregion
|
|
119031
118647
|
//#region src/persistence/Migrations.ts
|
|
119032
118648
|
/**
|
|
119033
118649
|
* MigrationsLive - Migration runner with inline loader
|
|
@@ -119438,6 +119054,11 @@ const migrationEntries = [
|
|
|
119438
119054
|
78,
|
|
119439
119055
|
"ProjectionThreadWorkSessionBoundAt",
|
|
119440
119056
|
_078_ProjectionThreadWorkSessionBoundAt_default
|
|
119057
|
+
],
|
|
119058
|
+
[
|
|
119059
|
+
79,
|
|
119060
|
+
"DropCommandRefreshToken",
|
|
119061
|
+
_079_DropCommandRefreshToken_default
|
|
119441
119062
|
]
|
|
119442
119063
|
];
|
|
119443
119064
|
const makeMigrationLoader = (throughId) => fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
|
|
@@ -298465,6 +298086,104 @@ const makeWorkspaceBulkTransfer = (options = {}) => gen(function* () {
|
|
|
298465
298086
|
});
|
|
298466
298087
|
const WorkspaceBulkTransferLive = effect(WorkspaceBulkTransfer, makeWorkspaceBulkTransfer());
|
|
298467
298088
|
//#endregion
|
|
298089
|
+
//#region src/workspace/Layers/WorkspaceDirectoryListing.ts
|
|
298090
|
+
function isPathWithin(path, root, candidate) {
|
|
298091
|
+
const relative = path.relative(root, candidate);
|
|
298092
|
+
return relative.length === 0 || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
298093
|
+
}
|
|
298094
|
+
/**
|
|
298095
|
+
* Directories sort before files so the tree reads like every other file tree;
|
|
298096
|
+
* names compare case-insensitively, then by raw name so stable order survives
|
|
298097
|
+
* a platform that folds case differently.
|
|
298098
|
+
*/
|
|
298099
|
+
function compareEntries(left, right) {
|
|
298100
|
+
if (left.kind !== right.kind) return left.kind === "directory" ? -1 : 1;
|
|
298101
|
+
const byName = left.name.localeCompare(right.name, void 0, { sensitivity: "accent" });
|
|
298102
|
+
return byName !== 0 ? byName : left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
298103
|
+
}
|
|
298104
|
+
const WorkspaceDirectoryListingLive = effect(WorkspaceDirectoryListing, gen(function* () {
|
|
298105
|
+
const fileSystem = yield* FileSystem;
|
|
298106
|
+
const path = yield* Path$2;
|
|
298107
|
+
const workspacePaths = yield* WorkspacePaths;
|
|
298108
|
+
return { list: fn$1("WorkspaceDirectoryListing.list")(function* (input) {
|
|
298109
|
+
const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.cwd).pipe(mapError(() => new WorkspaceListDirectoryError({
|
|
298110
|
+
cwd: input.cwd,
|
|
298111
|
+
relativePath: input.relativePath,
|
|
298112
|
+
failure: "workspace_root_invalid",
|
|
298113
|
+
detail: `Workspace root is not a usable directory: ${input.cwd}`
|
|
298114
|
+
})));
|
|
298115
|
+
const trimmed = input.relativePath.trim();
|
|
298116
|
+
if (path.isAbsolute(trimmed)) return yield* new WorkspaceListDirectoryError({
|
|
298117
|
+
cwd: input.cwd,
|
|
298118
|
+
relativePath: input.relativePath,
|
|
298119
|
+
failure: "path_outside_workspace",
|
|
298120
|
+
detail: "Directory listings must use workspace-relative paths."
|
|
298121
|
+
});
|
|
298122
|
+
const resolved = trimmed.length === 0 ? {
|
|
298123
|
+
absolutePath: workspaceRoot,
|
|
298124
|
+
relativePath: ""
|
|
298125
|
+
} : yield* workspacePaths.resolveRelativePathWithinRoot({
|
|
298126
|
+
workspaceRoot,
|
|
298127
|
+
relativePath: trimmed
|
|
298128
|
+
}).pipe(mapError(() => new WorkspaceListDirectoryError({
|
|
298129
|
+
cwd: input.cwd,
|
|
298130
|
+
relativePath: input.relativePath,
|
|
298131
|
+
failure: "path_outside_workspace",
|
|
298132
|
+
detail: `Directory is outside the workspace: ${input.relativePath}`
|
|
298133
|
+
})));
|
|
298134
|
+
const canonicalRoot = yield* fileSystem.realPath(workspaceRoot).pipe(orDie);
|
|
298135
|
+
const targetRealPath = yield* fileSystem.realPath(resolved.absolutePath).pipe(catch_(() => succeed(null)));
|
|
298136
|
+
if (targetRealPath === null) return yield* new WorkspaceListDirectoryError({
|
|
298137
|
+
cwd: input.cwd,
|
|
298138
|
+
relativePath: input.relativePath,
|
|
298139
|
+
failure: "read_directory_failed",
|
|
298140
|
+
detail: `Directory does not exist: ${input.relativePath || "."}`
|
|
298141
|
+
});
|
|
298142
|
+
if (!isPathWithin(path, canonicalRoot, targetRealPath)) return yield* new WorkspaceListDirectoryError({
|
|
298143
|
+
cwd: input.cwd,
|
|
298144
|
+
relativePath: input.relativePath,
|
|
298145
|
+
failure: "path_outside_workspace",
|
|
298146
|
+
detail: `Directory does not resolve inside the workspace: ${input.relativePath}`
|
|
298147
|
+
});
|
|
298148
|
+
const targetStat = yield* fileSystem.stat(targetRealPath).pipe(option);
|
|
298149
|
+
if (targetStat._tag === "None" || targetStat.value.type !== "Directory") return yield* new WorkspaceListDirectoryError({
|
|
298150
|
+
cwd: input.cwd,
|
|
298151
|
+
relativePath: input.relativePath,
|
|
298152
|
+
failure: "read_directory_failed",
|
|
298153
|
+
detail: `Not a directory: ${input.relativePath}`
|
|
298154
|
+
});
|
|
298155
|
+
return { entries: (yield* forEach(yield* fileSystem.readDirectory(targetRealPath).pipe(mapError((cause) => new WorkspaceListDirectoryError({
|
|
298156
|
+
cwd: input.cwd,
|
|
298157
|
+
relativePath: input.relativePath,
|
|
298158
|
+
failure: "read_directory_failed",
|
|
298159
|
+
detail: `Could not read directory: ${input.relativePath}`,
|
|
298160
|
+
cause
|
|
298161
|
+
}))), (name) => gen(function* () {
|
|
298162
|
+
const entryPath = path.join(targetRealPath, name);
|
|
298163
|
+
if ((yield* fileSystem.readLink(entryPath).pipe(option))._tag === "Some") return {
|
|
298164
|
+
name,
|
|
298165
|
+
kind: "file",
|
|
298166
|
+
hasChildren: false
|
|
298167
|
+
};
|
|
298168
|
+
const stat = yield* fileSystem.stat(entryPath).pipe(option);
|
|
298169
|
+
if (stat._tag === "None") return void 0;
|
|
298170
|
+
if (stat.value.type === "Directory") {
|
|
298171
|
+
const children = yield* fileSystem.readDirectory(entryPath).pipe(option);
|
|
298172
|
+
return {
|
|
298173
|
+
name,
|
|
298174
|
+
kind: "directory",
|
|
298175
|
+
hasChildren: children._tag === "Some" && children.value.length > 0
|
|
298176
|
+
};
|
|
298177
|
+
}
|
|
298178
|
+
return {
|
|
298179
|
+
name,
|
|
298180
|
+
kind: "file",
|
|
298181
|
+
hasChildren: false
|
|
298182
|
+
};
|
|
298183
|
+
}), { concurrency: 16 })).filter((entry) => entry !== void 0).toSorted(compareEntries) };
|
|
298184
|
+
}) };
|
|
298185
|
+
}));
|
|
298186
|
+
//#endregion
|
|
298468
298187
|
//#region src/workspace/Layers/WorkspacePaths.ts
|
|
298469
298188
|
function toPosixRelativePath(input) {
|
|
298470
298189
|
return input.replaceAll("\\", "/");
|
|
@@ -300058,7 +299777,7 @@ function resolveBuildCommitFromEnv(env) {
|
|
|
300058
299777
|
* environment descriptor down instead of reporting an honest "unknown".
|
|
300059
299778
|
*/
|
|
300060
299779
|
function readBakedBuildCommit() {
|
|
300061
|
-
return "
|
|
299780
|
+
return "15aa06fedcdcf9e165622cf018e31ce724b98ef7";
|
|
300062
299781
|
}
|
|
300063
299782
|
async function resolveServerBuildCommit(input) {
|
|
300064
299783
|
if (isFullCommitSha(input.baked)) return input.baked;
|
|
@@ -300304,14 +300023,13 @@ function identityFromRedeemed(redeemed) {
|
|
|
300304
300023
|
username: redeemed.displayName?.trim() || `user-${redeemed.userId}`
|
|
300305
300024
|
};
|
|
300306
300025
|
}
|
|
300026
|
+
/** Read optional display name, owner assertion, session grant, and expiry from a redeemed handoff. */
|
|
300307
300027
|
function optionalRedeemedFields(body) {
|
|
300308
|
-
const refreshToken = readTrimmedString(body.refreshToken);
|
|
300309
300028
|
const displayName = typeof body.displayName === "string" ? body.displayName : void 0;
|
|
300310
300029
|
const forgeOwnerAssertion = readTrimmedString(body.forgeOwnerAssertion);
|
|
300311
300030
|
const sessionGrant = readTrimmedString(body.sessionGrant);
|
|
300312
300031
|
const sessionGrantExpiresAt = typeof body.sessionGrantExpiresAt === "string" ? body.sessionGrantExpiresAt : void 0;
|
|
300313
300032
|
return {
|
|
300314
|
-
...refreshToken !== void 0 ? { refreshToken } : {},
|
|
300315
300033
|
...displayName !== void 0 ? { displayName } : {},
|
|
300316
300034
|
...forgeOwnerAssertion !== void 0 ? { forgeOwnerAssertion } : {},
|
|
300317
300035
|
...sessionGrant !== void 0 ? { sessionGrant } : {},
|
|
@@ -300379,6 +300097,7 @@ function redeemEmbedCodeWithCmmd(code) {
|
|
|
300379
300097
|
return succeed(redeemed);
|
|
300380
300098
|
}));
|
|
300381
300099
|
}
|
|
300100
|
+
/** Store the redeemed PAT, JWT, and identity for a local embed session, failing on write errors. */
|
|
300382
300101
|
function persistLocalEmbedSession(input) {
|
|
300383
300102
|
return gen(function* () {
|
|
300384
300103
|
yield* setCommandAccessTokenForSession(input.sessionId, input.redeemed.mcpToken).pipe(mapError((cause) => new AuthError({
|
|
@@ -300386,7 +300105,7 @@ function persistLocalEmbedSession(input) {
|
|
|
300386
300105
|
status: 500,
|
|
300387
300106
|
cause
|
|
300388
300107
|
})));
|
|
300389
|
-
yield* setCommandJwtForSession(input.sessionId, input.redeemed.accessToken
|
|
300108
|
+
yield* setCommandJwtForSession(input.sessionId, input.redeemed.accessToken).pipe(mapError((cause) => new AuthError({
|
|
300390
300109
|
message: "Failed to persist CMMD JWT.",
|
|
300391
300110
|
status: 500,
|
|
300392
300111
|
cause
|
|
@@ -300441,7 +300160,6 @@ const authCmmdEmbedExchangeRouteLayer = add$1("POST", "/api/auth/cmmd-embed/exch
|
|
|
300441
300160
|
accessToken: redeemed.mcpToken,
|
|
300442
300161
|
identity,
|
|
300443
300162
|
jwtToken: redeemed.accessToken,
|
|
300444
|
-
refreshToken: redeemed.refreshToken,
|
|
300445
300163
|
sessionGrant,
|
|
300446
300164
|
sessionGrantExpiresAt: redeemed.sessionGrantExpiresAt
|
|
300447
300165
|
});
|
|
@@ -300547,7 +300265,7 @@ const cloudOwnerSessionRedeemRouteLayer = add$1("POST", "/api/auth/cloud/session
|
|
|
300547
300265
|
status: 403
|
|
300548
300266
|
});
|
|
300549
300267
|
const tokenResult = yield* redeemSessionGrantWithCmmd(payload);
|
|
300550
|
-
const { tokenObj, cmmdIdentity, userId, accessToken, mcpToken,
|
|
300268
|
+
const { tokenObj, cmmdIdentity, userId, accessToken, mcpToken, forgeOwnerAssertion, sessionGrantExpiresAt, isJwt, patToStore, jwtToStore } = yield* resolveCmmdTokenBundleCredentials(tokenResult);
|
|
300551
300269
|
if (!cmmdIdentity || !userId || !patToStore) {
|
|
300552
300270
|
yield* logError("Forge Cloud session redeem: CMMD token response incomplete.", {
|
|
300553
300271
|
clientId: payload.clientId,
|
|
@@ -300556,7 +300274,6 @@ const cloudOwnerSessionRedeemRouteLayer = add$1("POST", "/api/auth/cloud/session
|
|
|
300556
300274
|
tokenResult,
|
|
300557
300275
|
accessToken,
|
|
300558
300276
|
mcpToken,
|
|
300559
|
-
refreshToken,
|
|
300560
300277
|
isJwt,
|
|
300561
300278
|
userId,
|
|
300562
300279
|
patToStore,
|
|
@@ -300579,7 +300296,6 @@ const cloudOwnerSessionRedeemRouteLayer = add$1("POST", "/api/auth/cloud/session
|
|
|
300579
300296
|
accessToken: patToStore,
|
|
300580
300297
|
identity: cmmdIdentity,
|
|
300581
300298
|
jwtToken: jwtToStore,
|
|
300582
|
-
refreshToken,
|
|
300583
300299
|
sessionGrant: payload.sessionGrant,
|
|
300584
300300
|
sessionGrantExpiresAt
|
|
300585
300301
|
});
|
|
@@ -324589,7 +324305,7 @@ const ProjectLeadSchedulerRuntimeLayerLive = makeProjectLeadSchedulerRuntimeLive
|
|
|
324589
324305
|
const GitManagerLayerLive = GitManagerLive.pipe(provideMerge(ProjectSetupScriptRunnerLive), provideMerge(GitCoreLive), provideMerge(GitHubCliLive), provideMerge(RoutingTextGenerationLive.pipe(provide$1(TextGenerationUsageLive.pipe(provide$1(RuntimeReplicationOutboxLive), provide$1(TextGenerationSessionRepositoryLive))))));
|
|
324590
324306
|
const GitLayerLive = empty$4.pipe(provideMerge(GitManagerLayerLive), provideMerge(GitStatusBroadcasterLive.pipe(provide$1(GitManagerLayerLive))), provideMerge(GitCoreLive));
|
|
324591
324307
|
const TerminalLayerLive = TerminalManagerLive.pipe(provide$1(PtyAdapterLive));
|
|
324592
|
-
const WorkspaceLayerLive = mergeAll(WorkspacePathsLive, ProjectCheckoutDeletionLive.pipe(provide$1(GitCoreLive), provide$1(ProjectionProjectRepositoryLive)), WorkspaceEntriesLive.pipe(provide$1(WorkspacePathsLive)), WorkspaceFileSystemLive.pipe(provide$1(WorkspacePathsLive), provide$1(WorkspaceEntriesLive.pipe(provide$1(WorkspacePathsLive)))), WorkspaceBulkTransferLive.pipe(provide$1(WorkspacePathsLive), provide$1(WorkspaceEntriesLive.pipe(provide$1(WorkspacePathsLive)))));
|
|
324308
|
+
const WorkspaceLayerLive = mergeAll(WorkspacePathsLive, ProjectCheckoutDeletionLive.pipe(provide$1(GitCoreLive), provide$1(ProjectionProjectRepositoryLive)), WorkspaceEntriesLive.pipe(provide$1(WorkspacePathsLive)), WorkspaceFileSystemLive.pipe(provide$1(WorkspacePathsLive), provide$1(WorkspaceEntriesLive.pipe(provide$1(WorkspacePathsLive)))), WorkspaceBulkTransferLive.pipe(provide$1(WorkspacePathsLive), provide$1(WorkspaceEntriesLive.pipe(provide$1(WorkspacePathsLive)))), WorkspaceDirectoryListingLive.pipe(provide$1(WorkspacePathsLive)));
|
|
324593
324309
|
const AuthLayerLive = ServerAuthLive.pipe(provideMerge(PersistenceLayerLive), provide$1(ServerSecretStoreLayerLive));
|
|
324594
324310
|
const MiscRuntimeLayerLive = mergeAll(AnalyticsServiceLayerLive, OpenLive, ServerLifecycleEventsLive, layer$4);
|
|
324595
324311
|
const RuntimeDependenciesWithoutPullRequestsLive = ReactorLayerLive.pipe(provideMerge(CheckpointingLayerLive), provideMerge(GitLayerLive), provideMerge(OrchestrationLayerLive), provideMerge(ProviderLayerLive), provideMerge(TerminalLayerLive), provideMerge(AgentBrowserManagerLive), provideMerge(mergeAll(ThreadDevRuntimeLive, DevTunnelLive.pipe(provide$1(ThreadDevRuntimeLive)))), provideMerge(PersistenceLayerLive), provideMerge(KeybindingsLive), provideMerge(ProviderRegistryLive), provideMerge(mergeAll(ServerSettingsLive, UsageServiceLive.pipe(provide$1(ServerSettingsLive), provide$1(UsageSessionLookupLive), provide$1(UsageRecordedSourceLive.pipe(provide$1(OrchestrationLayerLive), provide$1(RuntimeReplicationOutboxLive), provide$1(ProviderSessionRuntimeRepositoryLive), provide$1(TextGenerationSessionRepositoryLive)))))), provideMerge(WorkspaceLayerLive), provideMerge(WorkspaceReadinessStateLive), provideMerge(ProjectFaviconResolverLive), provideMerge(RepositoryIdentityResolverLive), provideMerge(ServerEnvironmentLive), provideMerge(AuthLayerLive), provideMerge(ServerSecretStoreLayerLive), provideMerge(mergeAll(CloudRedisCoordinationLive, CloudStateSyncLive.pipe(provide$1(PersistenceLayerLive)))), provideMerge(MiscRuntimeLayerLive));
|