@cmmd-center/forge 0.13.132 → 0.13.134
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 +279 -732
- package/dist/bin.mjs +303 -756
- package/dist/client/assets/{DiffPanel-Ca2Lz8RB.js → DiffPanel-jTwdJQlt.js} +2 -2
- package/dist/client/assets/{DiffPanel.logic-H_2wKBOQ.js → DiffPanel.logic-DukK-hlH.js} +2 -2
- package/dist/client/assets/{DiffWorkerPoolProvider-DSDTeZXk.js → DiffWorkerPoolProvider-BxK3crqQ.js} +2 -2
- package/dist/client/assets/{PullRequestCodePanel-7YAkNwLW.js → PullRequestCodePanel-D9vLGRM9.js} +2 -2
- package/dist/client/assets/{Virtualizer-BzBcEfxL.js → Virtualizer-pdZw7YnL.js} +2 -2
- package/dist/client/assets/{WorkerPoolContext-8onOdaSo.js → WorkerPoolContext-DfkvYwPY.js} +2 -2
- package/dist/client/assets/{cmmdForgeRuntimeListResource-Bl0ObO-g.js → cmmdForgeRuntimeListResource-6ppYDui_.js} +4 -4
- package/dist/client/assets/{currentForgeWorkspaceStore-BqHfGv0o.js → currentForgeWorkspaceStore-B3v2VP9w.js} +2 -2
- package/dist/client/assets/index-z4eYDXxq.js +301 -0
- package/dist/client/index.html +6 -6
- package/dist/forge-build.json +1 -1
- package/dist/{open-yHTUkd2Y.mjs → open-2-reBrpv.mjs} +3 -3
- package/package.json +1 -1
- package/dist/client/assets/index-zPBt45p3.js +0 -301
package/dist/bin.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { createCipheriv, createDecipheriv, createHash, createHmac, createPublicK
|
|
|
23
23
|
import * as FS$1 from "node:fs";
|
|
24
24
|
import fs, { accessSync, chmodSync, constants, createReadStream, existsSync, mkdirSync, mkdtempSync, promises, readFile, readFileSync, readdir, readdirSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
25
25
|
import * as NodeOS from "node:os";
|
|
26
|
-
import
|
|
26
|
+
import $U, { homedir, networkInterfaces, tmpdir } from "node:os";
|
|
27
27
|
import * as Path$1 from "node:path";
|
|
28
28
|
import path, { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
29
29
|
import { pathToFileURL } from "node:url";
|
|
@@ -50727,7 +50727,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
|
|
|
50727
50727
|
const min = forceColor || 0;
|
|
50728
50728
|
if (env.TERM === "dumb") return min;
|
|
50729
50729
|
if (process$1.platform === "win32") {
|
|
50730
|
-
const osRelease =
|
|
50730
|
+
const osRelease = $U.release().split(".");
|
|
50731
50731
|
if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
|
|
50732
50732
|
return 1;
|
|
50733
50733
|
}
|
|
@@ -53170,7 +53170,7 @@ var require_ProcessDetector = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
53170
53170
|
exports.processDetector = void 0;
|
|
53171
53171
|
const api_1 = (init_esm$1(), __toCommonJS(esm_exports$1));
|
|
53172
53172
|
const semconv_1 = require_semconv$1();
|
|
53173
|
-
const os$
|
|
53173
|
+
const os$3 = __require("os");
|
|
53174
53174
|
/**
|
|
53175
53175
|
* ProcessDetector will be used to detect the resources related current process running
|
|
53176
53176
|
* and being instrumented from the NodeJS Process module.
|
|
@@ -53192,7 +53192,7 @@ var require_ProcessDetector = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
53192
53192
|
};
|
|
53193
53193
|
if (process.argv.length > 1) attributes[semconv_1.ATTR_PROCESS_COMMAND] = process.argv[1];
|
|
53194
53194
|
try {
|
|
53195
|
-
const userInfo = os$
|
|
53195
|
+
const userInfo = os$3.userInfo();
|
|
53196
53196
|
attributes[semconv_1.ATTR_PROCESS_OWNER] = userInfo.username;
|
|
53197
53197
|
} catch (e) {
|
|
53198
53198
|
api_1.diag.debug(`error obtaining process owner: ${e}`);
|
|
@@ -66115,7 +66115,7 @@ function normalizeNumberish(value) {
|
|
|
66115
66115
|
}
|
|
66116
66116
|
//#endregion
|
|
66117
66117
|
//#region package.json
|
|
66118
|
-
var version$1 = "0.13.
|
|
66118
|
+
var version$1 = "0.13.134";
|
|
66119
66119
|
//#endregion
|
|
66120
66120
|
//#region src/sentry.ts
|
|
66121
66121
|
const SERVER_APP_NAME = "forge-server";
|
|
@@ -67074,9 +67074,7 @@ const runtimeReplicationOutboxOldestEventAge = gauge("t3_runtime_replication_out
|
|
|
67074
67074
|
const runtimeReplicationPublicationAttemptsTotal = counter("t3_runtime_replication_publication_attempts_total", { description: "Total canonical runtime replication publication attempts." });
|
|
67075
67075
|
const runtimeReplicationPublicationRetriesTotal = counter("t3_runtime_replication_publication_retries_total", { description: "Total runtime replication retries scheduled after recoverable failures." });
|
|
67076
67076
|
const runtimeReplicationPublicationLatency = timer("t3_runtime_replication_publication_latency", { description: "Time from durable enqueue to canonical CMMD acknowledgement." });
|
|
67077
|
-
const
|
|
67078
|
-
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." });
|
|
67079
|
-
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." });
|
|
67080
67078
|
const cmmdCredentialRowDeletedTotal = counter("t3_cmmd_credential_row_deleted_total", { description: "command_access_tokens rows actually deleted by clearUnrecoverableCmmdAuthForSession, by reason." });
|
|
67081
67079
|
const metricAttributes = (attributes) => Object.entries(compactMetricAttributes(attributes));
|
|
67082
67080
|
const increment = (metric, attributes, amount = 1) => update$3(withAttributes(metric, metricAttributes(attributes)), amount);
|
|
@@ -67157,12 +67155,10 @@ const isPersistenceError = (u) => is$1(PersistenceSqlError)(u) || is$1(Persisten
|
|
|
67157
67155
|
const cache = /* @__PURE__ */ new Map();
|
|
67158
67156
|
const jwtCache = /* @__PURE__ */ new Map();
|
|
67159
67157
|
const jwtExpiryCache = /* @__PURE__ */ new Map();
|
|
67160
|
-
const refreshTokenCache = /* @__PURE__ */ new Map();
|
|
67161
67158
|
const identityCache = /* @__PURE__ */ new Map();
|
|
67162
67159
|
const sessionGrantCache = /* @__PURE__ */ new Map();
|
|
67163
67160
|
const ACCESS_TOKEN_SCOPE = "cmmd-access-token";
|
|
67164
67161
|
const JWT_TOKEN_SCOPE = "cmmd-jwt-token";
|
|
67165
|
-
const REFRESH_TOKEN_SCOPE = "cmmd-refresh-token";
|
|
67166
67162
|
const IDENTITY_SCOPE = "cmmd-user-identity";
|
|
67167
67163
|
const SESSION_GRANT_SCOPE = "cmmd-session-grant";
|
|
67168
67164
|
const CommandAccessTokenRow = Struct({
|
|
@@ -67170,27 +67166,15 @@ const CommandAccessTokenRow = Struct({
|
|
|
67170
67166
|
accessToken: String$1,
|
|
67171
67167
|
jwtToken: NullOr(String$1),
|
|
67172
67168
|
jwtExpiresAt: NullOr(String$1),
|
|
67173
|
-
refreshToken: NullOr(String$1),
|
|
67174
67169
|
cmmdUserIdentity: NullOr(String$1),
|
|
67175
67170
|
sessionGrant: NullOr(String$1)
|
|
67176
67171
|
});
|
|
67177
67172
|
const SubjectCommandAccessTokenRow = Struct({ accessToken: String$1 });
|
|
67178
|
-
const SubjectJwtRow = Struct({
|
|
67179
|
-
accessToken: String$1,
|
|
67180
|
-
jwtToken: NullOr(String$1),
|
|
67181
|
-
jwtExpiresAt: NullOr(String$1),
|
|
67182
|
-
refreshToken: NullOr(String$1),
|
|
67183
|
-
sessionId: String$1
|
|
67184
|
-
});
|
|
67185
|
-
const SubjectRefreshTokenRow = Struct({
|
|
67186
|
-
refreshToken: String$1,
|
|
67187
|
-
sessionId: String$1
|
|
67188
|
-
});
|
|
67189
67173
|
const OwnerCommandAuthBundleRow = Struct({
|
|
67190
67174
|
accessToken: String$1,
|
|
67191
67175
|
jwtToken: NullOr(String$1),
|
|
67192
67176
|
jwtExpiresAt: NullOr(String$1),
|
|
67193
|
-
|
|
67177
|
+
sessionGrant: NullOr(String$1),
|
|
67194
67178
|
sessionId: String$1,
|
|
67195
67179
|
cmmdUserIdentity: String$1
|
|
67196
67180
|
});
|
|
@@ -67235,8 +67219,6 @@ function decryptNullable(scope, value) {
|
|
|
67235
67219
|
}
|
|
67236
67220
|
const decodeTokenRows = decodeUnknownEffect(ArraySchema(CommandAccessTokenRow));
|
|
67237
67221
|
const decodeSubjectTokenRows = decodeUnknownEffect(ArraySchema(SubjectCommandAccessTokenRow));
|
|
67238
|
-
const decodeSubjectJwtRows = decodeUnknownEffect(ArraySchema(SubjectJwtRow));
|
|
67239
|
-
decodeUnknownEffect(ArraySchema(SubjectRefreshTokenRow));
|
|
67240
67222
|
const decodeOwnerCommandAuthBundleRows = decodeUnknownEffect(ArraySchema(OwnerCommandAuthBundleRow));
|
|
67241
67223
|
const decodeOwnerCommandAuthSessionRows = decodeUnknownEffect(ArraySchema(OwnerCommandAuthSessionRow));
|
|
67242
67224
|
const decodeCloudOwnerSessionRows = decodeUnknownEffect(ArraySchema(CloudOwnerSessionRow));
|
|
@@ -67262,9 +67244,9 @@ function extractJwtExpiresAt(jwtToken) {
|
|
|
67262
67244
|
* Comparing against `exp` exactly leaves a window where Forge sends a token it
|
|
67263
67245
|
* believes is good and CMMD has already stopped accepting it: the clocks differ by a
|
|
67264
67246
|
* little, the request spends time in flight, and the answer comes back 401 on a
|
|
67265
|
-
* session
|
|
67266
|
-
*
|
|
67267
|
-
*
|
|
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.
|
|
67268
67250
|
*/
|
|
67269
67251
|
const JWT_EXPIRY_SKEW_MS = 2 * 6e4;
|
|
67270
67252
|
/**
|
|
@@ -67290,7 +67272,6 @@ const hydrateCommandAccessTokenStore = gen(function* () {
|
|
|
67290
67272
|
access_token AS "accessToken",
|
|
67291
67273
|
jwt_token AS "jwtToken",
|
|
67292
67274
|
jwt_expires_at AS "jwtExpiresAt",
|
|
67293
|
-
refresh_token AS "refreshToken",
|
|
67294
67275
|
cmmd_user_identity AS "cmmdUserIdentity",
|
|
67295
67276
|
session_grant AS "sessionGrant"
|
|
67296
67277
|
FROM command_access_tokens
|
|
@@ -67298,7 +67279,6 @@ const hydrateCommandAccessTokenStore = gen(function* () {
|
|
|
67298
67279
|
cache.clear();
|
|
67299
67280
|
jwtCache.clear();
|
|
67300
67281
|
jwtExpiryCache.clear();
|
|
67301
|
-
refreshTokenCache.clear();
|
|
67302
67282
|
identityCache.clear();
|
|
67303
67283
|
sessionGrantCache.clear();
|
|
67304
67284
|
yield* try_({
|
|
@@ -67309,8 +67289,6 @@ const hydrateCommandAccessTokenStore = gen(function* () {
|
|
|
67309
67289
|
const jwtToken = decryptNullable(JWT_TOKEN_SCOPE, row.jwtToken);
|
|
67310
67290
|
if (jwtToken) jwtCache.set(sessionId, jwtToken);
|
|
67311
67291
|
if (row.jwtExpiresAt) jwtExpiryCache.set(sessionId, row.jwtExpiresAt);
|
|
67312
|
-
const refreshToken = decryptNullable(REFRESH_TOKEN_SCOPE, row.refreshToken);
|
|
67313
|
-
if (refreshToken) refreshTokenCache.set(sessionId, refreshToken);
|
|
67314
67292
|
const identity = decodeCmmdUserIdentity(decryptNullable(IDENTITY_SCOPE, row.cmmdUserIdentity));
|
|
67315
67293
|
if (identity) identityCache.set(sessionId, identity);
|
|
67316
67294
|
const sessionGrant = decryptNullable(SESSION_GRANT_SCOPE, row.sessionGrant);
|
|
@@ -67417,19 +67395,11 @@ function readCommandJwtForSession(sessionId) {
|
|
|
67417
67395
|
return jwtCache.get(sessionId) ?? null;
|
|
67418
67396
|
}
|
|
67419
67397
|
/**
|
|
67420
|
-
* Read the CMMD OAuth refresh token for a session. Sync; hits the in-memory
|
|
67421
|
-
* cache only. Used to rotate a REST JWT after CMMD rejects a locally cached JWT
|
|
67422
|
-
* that still appears unexpired.
|
|
67423
|
-
*/
|
|
67424
|
-
function readCommandRefreshTokenForSession(sessionId) {
|
|
67425
|
-
return refreshTokenCache.get(sessionId) ?? null;
|
|
67426
|
-
}
|
|
67427
|
-
/**
|
|
67428
67398
|
* The durable CMMD session grant for a session, if one was issued.
|
|
67429
67399
|
*
|
|
67430
|
-
*
|
|
67431
|
-
*
|
|
67432
|
-
*
|
|
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.
|
|
67433
67403
|
*/
|
|
67434
67404
|
function readCommandSessionGrantForSession(sessionId) {
|
|
67435
67405
|
return sessionGrantCache.get(sessionId) ?? null;
|
|
@@ -67479,77 +67449,26 @@ const setCmmdUserIdentityForSession = (sessionId, identity) => gen(function* ()
|
|
|
67479
67449
|
* `jwt_expires_at` so callers can detect when the token has expired.
|
|
67480
67450
|
* Updates only the JWT columns; the PAT row must already exist
|
|
67481
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.
|
|
67482
67455
|
*/
|
|
67483
|
-
const setCommandJwtForSession = (sessionId, jwtToken
|
|
67456
|
+
const setCommandJwtForSession = (sessionId, jwtToken) => gen(function* () {
|
|
67484
67457
|
const token = jwtToken.trim();
|
|
67485
67458
|
if (token.length === 0) return;
|
|
67486
67459
|
const expiresAt = extractJwtExpiresAt(token);
|
|
67487
|
-
const refreshToken = opts.refreshToken?.trim();
|
|
67488
67460
|
const storedToken = encodeSensitiveValue(JWT_TOKEN_SCOPE, token);
|
|
67489
|
-
|
|
67490
|
-
|
|
67491
|
-
|
|
67492
|
-
|
|
67493
|
-
|
|
67494
|
-
|
|
67495
|
-
|
|
67496
|
-
|
|
67497
|
-
refresh_token = ${storedRefreshToken},
|
|
67498
|
-
updated_at = ${now}
|
|
67499
|
-
WHERE session_id = ${sessionId}
|
|
67500
|
-
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.setJwt:query")));
|
|
67501
|
-
else yield* sql`
|
|
67502
|
-
UPDATE command_access_tokens
|
|
67503
|
-
SET
|
|
67504
|
-
jwt_token = ${storedToken},
|
|
67505
|
-
jwt_expires_at = ${expiresAt},
|
|
67506
|
-
updated_at = ${now}
|
|
67507
|
-
WHERE session_id = ${sessionId}
|
|
67508
|
-
`.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")));
|
|
67509
67469
|
jwtCache.set(sessionId, token);
|
|
67510
67470
|
if (expiresAt) jwtExpiryCache.set(sessionId, expiresAt);
|
|
67511
67471
|
else jwtExpiryCache.delete(sessionId);
|
|
67512
|
-
if (refreshToken) refreshTokenCache.set(sessionId, refreshToken);
|
|
67513
|
-
});
|
|
67514
|
-
/**
|
|
67515
|
-
* Look up the most recent non-expired JWT for an auth subject across active
|
|
67516
|
-
* owner sessions. Used as a continuity fallback when the current session ID
|
|
67517
|
-
* changed (e.g. after re-authentication) but the user's CMMD JWT should still
|
|
67518
|
-
* be usable.
|
|
67519
|
-
*
|
|
67520
|
-
* Also returns the sessionId that owns the JWT so callers can promote it to
|
|
67521
|
-
* the current session, avoiding repeated cross-session lookups.
|
|
67522
|
-
*/
|
|
67523
|
-
const readMostRecentJwtForSubject = (subject) => gen(function* () {
|
|
67524
|
-
const row = (yield* decodeSubjectJwtRows(yield* (yield* SqlClient)`
|
|
67525
|
-
SELECT
|
|
67526
|
-
cat.access_token AS "accessToken",
|
|
67527
|
-
cat.jwt_token AS "jwtToken",
|
|
67528
|
-
cat.jwt_expires_at AS "jwtExpiresAt",
|
|
67529
|
-
cat.refresh_token AS "refreshToken",
|
|
67530
|
-
cat.session_id AS "sessionId"
|
|
67531
|
-
FROM command_access_tokens AS cat
|
|
67532
|
-
JOIN auth_sessions AS s ON s.session_id = cat.session_id
|
|
67533
|
-
WHERE s.subject = ${subject}
|
|
67534
|
-
AND s.role = 'owner'
|
|
67535
|
-
AND s.revoked_at IS NULL
|
|
67536
|
-
AND cat.jwt_token IS NOT NULL
|
|
67537
|
-
AND cat.jwt_expires_at IS NOT NULL
|
|
67538
|
-
AND cat.jwt_expires_at > ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
67539
|
-
ORDER BY s.issued_at DESC
|
|
67540
|
-
LIMIT 1
|
|
67541
|
-
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.readMostRecentJwtBySubject:query")))).pipe(mapError(toPersistenceDecodeError("CommandAccessTokenStore.readMostRecentJwtBySubject:decodeRows"))))[0];
|
|
67542
|
-
if (!row?.jwtToken) return null;
|
|
67543
|
-
const jwtToken = decodeSensitiveValue(JWT_TOKEN_SCOPE, row.jwtToken);
|
|
67544
|
-
const refreshToken = decryptNullable(REFRESH_TOKEN_SCOPE, row.refreshToken);
|
|
67545
|
-
const accessToken = decodeSensitiveValue(ACCESS_TOKEN_SCOPE, row.accessToken);
|
|
67546
|
-
return {
|
|
67547
|
-
jwt: jwtToken,
|
|
67548
|
-
sessionId: row.sessionId,
|
|
67549
|
-
expiresAt: row.jwtExpiresAt ?? null,
|
|
67550
|
-
refreshToken,
|
|
67551
|
-
accessToken
|
|
67552
|
-
};
|
|
67553
67472
|
});
|
|
67554
67473
|
/**
|
|
67555
67474
|
* Look up the latest CMMD PAT for an auth subject across active owner sessions.
|
|
@@ -67588,7 +67507,7 @@ const readMostRecentCommandAccessTokenForSubject = (subject) => gen(function* ()
|
|
|
67588
67507
|
* `desktop-bootstrap` session that minted them, and nothing later rewrites that
|
|
67589
67508
|
* subject. So the clause matched no row on any machine, promotion always
|
|
67590
67509
|
* returned null, and a phone whose short-lived JWT had expired could not reach
|
|
67591
|
-
* the
|
|
67510
|
+
* the credentials sitting on its own previous session. It surfaced as "This
|
|
67592
67511
|
* session is no longer accepted. Sign in again." about a day after signing in,
|
|
67593
67512
|
* with nothing in the log, because the ladder never got as far as a call that
|
|
67594
67513
|
* could fail out loud.
|
|
@@ -67649,6 +67568,9 @@ const readMostRecentOwnerCommandAuthSession = gen(function* () {
|
|
|
67649
67568
|
* a CMMD user subject. The CMMD token is still account-owned, so the trusted
|
|
67650
67569
|
* local owner may inherit the most recent active owner token with an identity.
|
|
67651
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.
|
|
67652
67574
|
*/
|
|
67653
67575
|
const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => gen(function* () {
|
|
67654
67576
|
const sql = yield* SqlClient;
|
|
@@ -67657,7 +67579,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => gen(function*
|
|
|
67657
67579
|
cat.access_token AS "accessToken",
|
|
67658
67580
|
cat.jwt_token AS "jwtToken",
|
|
67659
67581
|
cat.jwt_expires_at AS "jwtExpiresAt",
|
|
67660
|
-
cat.
|
|
67582
|
+
cat.session_grant AS "sessionGrant",
|
|
67661
67583
|
cat.session_id AS "sessionId",
|
|
67662
67584
|
cat.cmmd_user_identity AS "cmmdUserIdentity"
|
|
67663
67585
|
FROM command_access_tokens AS cat
|
|
@@ -67669,7 +67591,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => gen(function*
|
|
|
67669
67591
|
if (!row) return null;
|
|
67670
67592
|
const accessToken = decodeSensitiveValue(ACCESS_TOKEN_SCOPE, row.accessToken);
|
|
67671
67593
|
const jwtToken = decryptNullable(JWT_TOKEN_SCOPE, row.jwtToken);
|
|
67672
|
-
const
|
|
67594
|
+
const sessionGrant = decryptNullable(SESSION_GRANT_SCOPE, row.sessionGrant);
|
|
67673
67595
|
const identity = decodeCmmdUserIdentity(decodeSensitiveValue(IDENTITY_SCOPE, row.cmmdUserIdentity));
|
|
67674
67596
|
if (!identity) return null;
|
|
67675
67597
|
const updateNow = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -67679,7 +67601,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => gen(function*
|
|
|
67679
67601
|
access_token,
|
|
67680
67602
|
jwt_token,
|
|
67681
67603
|
jwt_expires_at,
|
|
67682
|
-
|
|
67604
|
+
session_grant,
|
|
67683
67605
|
cmmd_user_identity,
|
|
67684
67606
|
created_at,
|
|
67685
67607
|
updated_at
|
|
@@ -67689,7 +67611,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => gen(function*
|
|
|
67689
67611
|
${row.accessToken},
|
|
67690
67612
|
${row.jwtToken},
|
|
67691
67613
|
${row.jwtExpiresAt},
|
|
67692
|
-
${row.
|
|
67614
|
+
${row.sessionGrant},
|
|
67693
67615
|
${row.cmmdUserIdentity},
|
|
67694
67616
|
${updateNow},
|
|
67695
67617
|
${updateNow}
|
|
@@ -67698,7 +67620,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => gen(function*
|
|
|
67698
67620
|
access_token = excluded.access_token,
|
|
67699
67621
|
jwt_token = excluded.jwt_token,
|
|
67700
67622
|
jwt_expires_at = excluded.jwt_expires_at,
|
|
67701
|
-
|
|
67623
|
+
session_grant = excluded.session_grant,
|
|
67702
67624
|
cmmd_user_identity = excluded.cmmd_user_identity,
|
|
67703
67625
|
updated_at = excluded.updated_at
|
|
67704
67626
|
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.promoteLatestOwnerAuth:upsert")));
|
|
@@ -67707,14 +67629,14 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => gen(function*
|
|
|
67707
67629
|
else jwtCache.delete(sessionId);
|
|
67708
67630
|
if (row.jwtExpiresAt) jwtExpiryCache.set(sessionId, row.jwtExpiresAt);
|
|
67709
67631
|
else jwtExpiryCache.delete(sessionId);
|
|
67710
|
-
if (
|
|
67711
|
-
else
|
|
67632
|
+
if (sessionGrant) sessionGrantCache.set(sessionId, sessionGrant);
|
|
67633
|
+
else sessionGrantCache.delete(sessionId);
|
|
67712
67634
|
identityCache.set(sessionId, identity);
|
|
67713
67635
|
return {
|
|
67714
67636
|
accessToken,
|
|
67715
67637
|
jwtToken,
|
|
67716
67638
|
jwtExpiresAt: row.jwtExpiresAt ?? null,
|
|
67717
|
-
|
|
67639
|
+
sessionGrant,
|
|
67718
67640
|
sourceSessionId: row.sessionId,
|
|
67719
67641
|
cmmdUserIdentity: identity
|
|
67720
67642
|
};
|
|
@@ -67759,11 +67681,11 @@ const clearCommandAccessTokenForSession = (sessionId) => gen(function* () {
|
|
|
67759
67681
|
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.clear:query")));
|
|
67760
67682
|
forgetCommandAccessTokenCachesForSession(sessionId);
|
|
67761
67683
|
});
|
|
67684
|
+
/** Evict a session's credentials, JWT expiry, identity, and grant from memory without changing storage. */
|
|
67762
67685
|
function forgetCommandAccessTokenCachesForSession(sessionId) {
|
|
67763
67686
|
cache.delete(sessionId);
|
|
67764
67687
|
jwtCache.delete(sessionId);
|
|
67765
67688
|
jwtExpiryCache.delete(sessionId);
|
|
67766
|
-
refreshTokenCache.delete(sessionId);
|
|
67767
67689
|
identityCache.delete(sessionId);
|
|
67768
67690
|
sessionGrantCache.delete(sessionId);
|
|
67769
67691
|
}
|
|
@@ -67795,14 +67717,14 @@ const scheduleExpiredForgeCloudOwnerCredentialMaintenance = sleep$4(minutes(5)).
|
|
|
67795
67717
|
* is dead too, so the promotion sources are cleared with it — otherwise the
|
|
67796
67718
|
* next promotion resurrects the same dead bundle in a loop.
|
|
67797
67719
|
* - **Every other session** owns its credentials outright: each OAuth exchange
|
|
67798
|
-
* mints
|
|
67799
|
-
*
|
|
67720
|
+
* mints its own PAT, JWT and session grant. One session's dead credential
|
|
67721
|
+
* says nothing about a
|
|
67800
67722
|
* sibling browser or device, so the clear stays local to that session.
|
|
67801
67723
|
*
|
|
67802
67724
|
* Clearing every active owner row unconditionally (the pre-CMMD-1871 behaviour)
|
|
67803
67725
|
* turned a single expired tab into a sign-out on every browser and device.
|
|
67804
67726
|
*
|
|
67805
|
-
* Destroys
|
|
67727
|
+
* Destroys durable credentials (the PAT, the grant), so every call is logged with a
|
|
67806
67728
|
* `reason` naming the caller's own decision (never the credential itself) and
|
|
67807
67729
|
* the actual row count deleted, at info level so it shows up without raising
|
|
67808
67730
|
* the server's log level. `reason` is optional only so a caller that has not
|
|
@@ -67843,65 +67765,10 @@ const clearUnrecoverableCmmdAuthForSession = (sessionId, reason) => gen(function
|
|
|
67843
67765
|
}
|
|
67844
67766
|
});
|
|
67845
67767
|
/**
|
|
67846
|
-
* Retire refresh credentials copied into historical desktop bearer sessions.
|
|
67847
|
-
* Electron owns rotation for native desktop OAuth; Forge Local keeps only the
|
|
67848
|
-
* derived access authority needed by the current bearer session.
|
|
67849
|
-
*
|
|
67850
|
-
* Scoped to the bootstrapping client, not to the whole subject. The phone and
|
|
67851
|
-
* Electron sign in as the same `user:<id>`, so a subject-wide sweep let one
|
|
67852
|
-
* desktop launch null out a phone's refresh token and end its session a day
|
|
67853
|
-
* later. Two clauses keep that from happening:
|
|
67854
|
-
*
|
|
67855
|
-
* - `client_device_type = 'desktop'`, which now excludes phones outright,
|
|
67856
|
-
* because the mobile app declares itself at bootstrap.
|
|
67857
|
-
* - the same recorded `client_user_agent` as the bootstrap doing the
|
|
67858
|
-
* retirement, which is the only thing that separates a phone row written
|
|
67859
|
-
* before that stamp existed from a real Electron row. Both are stamped
|
|
67860
|
-
* `desktop`; neither carries any other mark. An empty or unmatched user agent
|
|
67861
|
-
* retires nothing, which is the direction that keeps a live credential.
|
|
67862
|
-
*
|
|
67863
|
-
* Electron re-bootstraps under a stable user agent, so its own historical rows
|
|
67864
|
-
* still match and still get swept.
|
|
67865
|
-
*/
|
|
67866
|
-
const clearDesktopBearerRefreshTokensForSubject = (input) => gen(function* () {
|
|
67867
|
-
const normalizedSubject = input.subject.trim();
|
|
67868
|
-
const normalizedUserAgent = input.clientUserAgent?.trim() ?? "";
|
|
67869
|
-
if (!normalizedSubject || !normalizedUserAgent) return;
|
|
67870
|
-
const rows = yield* (yield* SqlClient)`
|
|
67871
|
-
UPDATE command_access_tokens
|
|
67872
|
-
SET
|
|
67873
|
-
refresh_token = NULL,
|
|
67874
|
-
updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
67875
|
-
WHERE session_id IN (
|
|
67876
|
-
SELECT session_id
|
|
67877
|
-
FROM auth_sessions
|
|
67878
|
-
WHERE subject = ${normalizedSubject}
|
|
67879
|
-
AND role = 'owner'
|
|
67880
|
-
AND method = 'bearer-session-token'
|
|
67881
|
-
AND client_device_type = 'desktop'
|
|
67882
|
-
AND client_user_agent = ${normalizedUserAgent}
|
|
67883
|
-
)
|
|
67884
|
-
RETURNING session_id AS "sessionId"
|
|
67885
|
-
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.clearDesktopBearerRefresh:query")));
|
|
67886
|
-
for (const row of rows) refreshTokenCache.delete(row.sessionId);
|
|
67887
|
-
});
|
|
67888
|
-
const isDesktopBearerAuthSession = (sessionId) => gen(function* () {
|
|
67889
|
-
const rows = yield* (yield* SqlClient)`
|
|
67890
|
-
SELECT
|
|
67891
|
-
method,
|
|
67892
|
-
client_device_type AS "clientDeviceType"
|
|
67893
|
-
FROM auth_sessions
|
|
67894
|
-
WHERE session_id = ${sessionId}
|
|
67895
|
-
LIMIT 1
|
|
67896
|
-
`.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.isDesktopBearerSession:query")));
|
|
67897
|
-
return rows[0]?.method === "bearer-session-token" && rows[0].clientDeviceType === "desktop";
|
|
67898
|
-
});
|
|
67899
|
-
/**
|
|
67900
67768
|
* The device type a session's bearer bootstrap declared, for log lines only.
|
|
67901
67769
|
*
|
|
67902
|
-
*
|
|
67903
|
-
*
|
|
67904
|
-
* 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:
|
|
67905
67772
|
* an unreadable or missing row logs as `"unknown"` rather than losing the
|
|
67906
67773
|
* decision-point log line it is attached to.
|
|
67907
67774
|
*/
|
|
@@ -67954,6 +67821,7 @@ function artifactError(message) {
|
|
|
67954
67821
|
cause
|
|
67955
67822
|
});
|
|
67956
67823
|
}
|
|
67824
|
+
/** Store credentials under the asserted Cloud owner session and retain its durable mint grant. */
|
|
67957
67825
|
function persistCloudOwnerArtifacts(input) {
|
|
67958
67826
|
return gen(function* () {
|
|
67959
67827
|
const sql = yield* SqlClient;
|
|
@@ -67961,7 +67829,7 @@ function persistCloudOwnerArtifacts(input) {
|
|
|
67961
67829
|
yield* setCommandAccessTokenForSession(input.assertion.sessionId, input.accessToken).pipe(mapError(artifactError("Failed to persist CMMD access token.")));
|
|
67962
67830
|
yield* setCmmdUserIdentityForSession(input.assertion.sessionId, input.identity).pipe(mapError(artifactError("Failed to persist CMMD user identity.")));
|
|
67963
67831
|
if (input.sessionGrant) yield* setCommandSessionGrantForSession(input.assertion.sessionId, input.sessionGrant, { expiresAt: input.sessionGrantExpiresAt ?? null }).pipe(mapError(artifactError("Failed to persist CMMD session grant.")));
|
|
67964
|
-
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.")));
|
|
67965
67833
|
});
|
|
67966
67834
|
yield* sql.withTransaction(transaction).pipe(mapError((cause) => cause instanceof AuthError ? cause : new AuthError({
|
|
67967
67835
|
message: "Failed to persist Forge Cloud owner credentials.",
|
|
@@ -68099,7 +67967,6 @@ const authSessionRenewRouteLayer = add$1("POST", "/api/auth/session/renew", gen(
|
|
|
68099
67967
|
accessToken: patToStore,
|
|
68100
67968
|
identity: cmmdIdentity,
|
|
68101
67969
|
jwtToken: credentials.jwtToStore,
|
|
68102
|
-
refreshToken: credentials.refreshToken,
|
|
68103
67970
|
sessionGrant,
|
|
68104
67971
|
sessionGrantExpiresAt: void 0
|
|
68105
67972
|
});
|
|
@@ -68175,7 +68042,6 @@ const PairingCredentialRequestHeaders = Struct({
|
|
|
68175
68042
|
const CommandBootstrapInput = Struct({
|
|
68176
68043
|
accessToken: String$1,
|
|
68177
68044
|
mcpToken: optionalKey(String$1),
|
|
68178
|
-
refreshToken: optionalKey(String$1),
|
|
68179
68045
|
userId: optionalKey(Number$1),
|
|
68180
68046
|
displayName: optionalKey(String$1),
|
|
68181
68047
|
user: optionalKey(Unknown),
|
|
@@ -68409,6 +68275,7 @@ function validateCommandBootstrapPayload(payload) {
|
|
|
68409
68275
|
};
|
|
68410
68276
|
});
|
|
68411
68277
|
}
|
|
68278
|
+
/** Store the bootstrap PAT, optional JWT, and identity; report persistence failures as auth errors. */
|
|
68412
68279
|
function persistCommandBootstrapSessionArtifacts(input) {
|
|
68413
68280
|
return gen(function* () {
|
|
68414
68281
|
const token = input.payload.accessToken.trim();
|
|
@@ -68423,14 +68290,11 @@ function persistCommandBootstrapSessionArtifacts(input) {
|
|
|
68423
68290
|
status: 500,
|
|
68424
68291
|
cause
|
|
68425
68292
|
})));
|
|
68426
|
-
if (isCmmdJwtToken(token)) {
|
|
68427
|
-
|
|
68428
|
-
|
|
68429
|
-
|
|
68430
|
-
|
|
68431
|
-
cause
|
|
68432
|
-
})));
|
|
68433
|
-
}
|
|
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
|
+
})));
|
|
68434
68298
|
const cmmdIdentity = input.cmmdIdentity ?? normalizeCmmdUserIdentity(input.payload.user);
|
|
68435
68299
|
if (cmmdIdentity) yield* setCmmdUserIdentityForSession(input.sessionId, cmmdIdentity).pipe(mapError((cause) => new AuthError({
|
|
68436
68300
|
message: "Failed to persist CMMD user identity.",
|
|
@@ -68532,7 +68396,6 @@ function resolveCmmdTokenBundleCredentials(tokenResult) {
|
|
|
68532
68396
|
userId: cmmdIdentity?.id,
|
|
68533
68397
|
accessToken,
|
|
68534
68398
|
mcpToken,
|
|
68535
|
-
refreshToken: readString("refresh_token"),
|
|
68536
68399
|
sessionGrant: readString("session_grant"),
|
|
68537
68400
|
sessionGrantExpiresAt: readString("session_grant_expires_at"),
|
|
68538
68401
|
forgeOwnerAssertion: readString("forge_owner_assertion"),
|
|
@@ -68554,7 +68417,6 @@ function describeCmmdTokenResponseForLog(input) {
|
|
|
68554
68417
|
accessTokenType: input.tokenObj ? typeof input.tokenObj["access_token"] : "n/a",
|
|
68555
68418
|
hasAccessToken: input.accessToken !== void 0,
|
|
68556
68419
|
hasMcpToken: input.mcpToken !== void 0,
|
|
68557
|
-
hasRefreshToken: input.refreshToken !== void 0,
|
|
68558
68420
|
expiresInType: input.tokenObj ? typeof input.tokenObj["expires_in"] : "n/a",
|
|
68559
68421
|
isJwt: input.isJwt,
|
|
68560
68422
|
userKeys: userObj ? Object.keys(userObj) : [],
|
|
@@ -68782,19 +68644,6 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
|
|
|
68782
68644
|
payload,
|
|
68783
68645
|
cmmdIdentity: validated.cmmdIdentity
|
|
68784
68646
|
});
|
|
68785
|
-
if (deviceType === "desktop" && !payload.refreshToken?.trim()) yield* clearDesktopBearerRefreshTokensForSubject({
|
|
68786
|
-
subject: sessionSubject,
|
|
68787
|
-
clientUserAgent: request.headers["user-agent"]
|
|
68788
|
-
}).pipe(mapError((cause) => new AuthError({
|
|
68789
|
-
message: "Failed to retire legacy desktop refresh credentials.",
|
|
68790
|
-
status: 500,
|
|
68791
|
-
cause
|
|
68792
|
-
})), tapError(() => logWarning$1("CMMD bearer bootstrap failed.", {
|
|
68793
|
-
sessionId: issued.sessionId,
|
|
68794
|
-
deviceType,
|
|
68795
|
-
outcome: "failed",
|
|
68796
|
-
reason: "legacy-desktop-refresh-retirement-failed"
|
|
68797
|
-
})));
|
|
68798
68647
|
yield* logInfo("CMMD bearer bootstrap succeeded.", {
|
|
68799
68648
|
sessionId: issued.sessionId,
|
|
68800
68649
|
deviceType,
|
|
@@ -68892,7 +68741,6 @@ function completeCloudOwnerPasswordLogin(input) {
|
|
|
68892
68741
|
accessToken: patToStore,
|
|
68893
68742
|
identity: cmmdIdentity,
|
|
68894
68743
|
jwtToken: credentials.jwtToStore,
|
|
68895
|
-
refreshToken: credentials.refreshToken,
|
|
68896
68744
|
sessionGrant: credentials.sessionGrant,
|
|
68897
68745
|
sessionGrantExpiresAt: credentials.sessionGrantExpiresAt
|
|
68898
68746
|
});
|
|
@@ -68975,6 +68823,10 @@ const authWebLoginRouteLayer = add$1("GET", "/api/auth/login", gen(function* ()
|
|
|
68975
68823
|
transientCookie
|
|
68976
68824
|
]]));
|
|
68977
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
|
+
*/
|
|
68978
68830
|
function persistLocalOAuthArtifacts(input) {
|
|
68979
68831
|
return gen(function* () {
|
|
68980
68832
|
yield* setCommandAccessTokenForSession(input.sessionId, input.accessToken).pipe(mapError((cause) => new AuthError({
|
|
@@ -68991,7 +68843,7 @@ function persistLocalOAuthArtifacts(input) {
|
|
|
68991
68843
|
route: "oauth-callback",
|
|
68992
68844
|
step: "grant-store"
|
|
68993
68845
|
}))), ignore({ log: true }));
|
|
68994
|
-
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, {
|
|
68995
68847
|
route: "oauth-callback",
|
|
68996
68848
|
step: "jwt-store"
|
|
68997
68849
|
}))), ignore({ log: true }));
|
|
@@ -69045,13 +68897,12 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", gen(functio
|
|
|
69045
68897
|
cause
|
|
69046
68898
|
})
|
|
69047
68899
|
}), "Failed to exchange authorization code with CMMD.");
|
|
69048
|
-
const { tokenObj, cmmdIdentity, userId, accessToken, mcpToken,
|
|
68900
|
+
const { tokenObj, cmmdIdentity, userId, accessToken, mcpToken, sessionGrant, sessionGrantExpiresAt, forgeOwnerAssertion, isJwt, patToStore, jwtToStore } = yield* resolveCmmdTokenBundleCredentials(tokenResult);
|
|
69049
68901
|
const tokenResponseLogDetails = describeCmmdTokenResponseForLog({
|
|
69050
68902
|
tokenObj,
|
|
69051
68903
|
tokenResult,
|
|
69052
68904
|
accessToken,
|
|
69053
68905
|
mcpToken,
|
|
69054
|
-
refreshToken,
|
|
69055
68906
|
isJwt,
|
|
69056
68907
|
userId,
|
|
69057
68908
|
patToStore,
|
|
@@ -69104,7 +68955,6 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", gen(functio
|
|
|
69104
68955
|
accessToken: patToStore,
|
|
69105
68956
|
identity: cmmdIdentity,
|
|
69106
68957
|
jwtToken: jwtToStore,
|
|
69107
|
-
refreshToken,
|
|
69108
68958
|
sessionGrant,
|
|
69109
68959
|
sessionGrantExpiresAt
|
|
69110
68960
|
};
|
|
@@ -71098,51 +70948,83 @@ function isCmmdUserAccessJwt(token) {
|
|
|
71098
70948
|
}
|
|
71099
70949
|
//#endregion
|
|
71100
70950
|
//#region src/command/cmmdJwt.ts
|
|
71101
|
-
function parseCmmdOAuthRefreshResponse(value) {
|
|
71102
|
-
if (!value || typeof value !== "object") return null;
|
|
71103
|
-
const record = value;
|
|
71104
|
-
const accessToken = record["access_token"];
|
|
71105
|
-
if (typeof accessToken !== "string" || accessToken.trim().length === 0) return null;
|
|
71106
|
-
const refreshToken = record["refresh_token"];
|
|
71107
|
-
return {
|
|
71108
|
-
accessToken: accessToken.trim(),
|
|
71109
|
-
refreshToken: typeof refreshToken === "string" && refreshToken.trim().length > 0 ? refreshToken.trim() : null
|
|
71110
|
-
};
|
|
71111
|
-
}
|
|
71112
70951
|
/**
|
|
71113
|
-
* Read
|
|
71114
|
-
*
|
|
71115
|
-
|
|
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
|
+
}
|
|
71116
70985
|
function parseCmmdTokenMintResponse(value) {
|
|
71117
70986
|
if (!value || typeof value !== "object") return null;
|
|
71118
70987
|
const accessToken = value["access_token"];
|
|
71119
70988
|
if (typeof accessToken !== "string" || accessToken.trim().length === 0) return null;
|
|
71120
|
-
|
|
70989
|
+
const trimmed = accessToken.trim();
|
|
70990
|
+
if (!mintLifetimeWithinContract(trimmed)) return null;
|
|
70991
|
+
return { accessToken: trimmed };
|
|
71121
70992
|
}
|
|
71122
70993
|
/**
|
|
71123
|
-
* 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
|
|
71124
70995
|
* grant lookup and one signature on CMMD's side; anything slower is treated
|
|
71125
|
-
* like a failure and the
|
|
70996
|
+
* like a failure and the resolution returns null.
|
|
71126
70997
|
*/
|
|
71127
70998
|
const MINT_TIMEOUT_MS = 1e4;
|
|
71128
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
|
+
/**
|
|
71129
71015
|
* Mint a short-lived forge-scoped access token from a durable session grant
|
|
71130
|
-
* (forge#3518 expand phase).
|
|
71016
|
+
* (forge#3518 expand; the only lane since the forge#3521 contract phase).
|
|
71131
71017
|
*
|
|
71132
|
-
* The grant is non-rotating and redeemable on demand, so this is the
|
|
71133
|
-
*
|
|
71134
|
-
*
|
|
71135
|
-
*
|
|
71136
|
-
*
|
|
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.
|
|
71137
71023
|
*
|
|
71138
71024
|
* Returns null for every failure because the resolver wants a value, not a
|
|
71139
|
-
* thrown error; the failure modes are logged by status and OAuth error code
|
|
71140
|
-
* the same way `refreshCmmdJwt` logs them. A rejection here is never fed to
|
|
71141
|
-
* the credential-rejection ledger: the ledger exists to prove a session's
|
|
71142
|
-
* *rotation* credential is dead, and the grant dying says nothing about the
|
|
71143
|
-
* refresh token the legacy fallback still owns.
|
|
71025
|
+
* thrown error; the failure modes are logged by status and OAuth error code.
|
|
71144
71026
|
*/
|
|
71145
|
-
function mintCmmdShortLivedToken(grantToken) {
|
|
71027
|
+
function mintCmmdShortLivedToken(grantToken, observer) {
|
|
71146
71028
|
return fetch(new URL("/api/mcp-server/oauth/token", resolveCommandAppUrl()).toString(), {
|
|
71147
71029
|
method: "POST",
|
|
71148
71030
|
headers: { "Content-Type": "application/json" },
|
|
@@ -71155,6 +71037,7 @@ function mintCmmdShortLivedToken(grantToken) {
|
|
|
71155
71037
|
}).then(async (response) => {
|
|
71156
71038
|
if (!response.ok) {
|
|
71157
71039
|
const errorCode = await readOAuthErrorCode(response);
|
|
71040
|
+
if (errorCode !== null && REJECTED_OAUTH_ERROR_CODES.has(errorCode)) observer?.onCredentialRejected?.();
|
|
71158
71041
|
console.warn(`[cmmd-auth] CMMD refused a forge token mint with ${response.status}${errorCode === null ? "" : ` (${errorCode})`}`);
|
|
71159
71042
|
return null;
|
|
71160
71043
|
}
|
|
@@ -71167,103 +71050,149 @@ function mintCmmdShortLivedToken(grantToken) {
|
|
|
71167
71050
|
});
|
|
71168
71051
|
}
|
|
71169
71052
|
/**
|
|
71170
|
-
* The
|
|
71171
|
-
*
|
|
71172
|
-
*
|
|
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.
|
|
71173
71059
|
*/
|
|
71174
|
-
function
|
|
71175
|
-
|
|
71176
|
-
|
|
71177
|
-
|
|
71178
|
-
|
|
71179
|
-
|
|
71180
|
-
}
|
|
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
|
+
}
|
|
71181
71067
|
}
|
|
71068
|
+
//#endregion
|
|
71069
|
+
//#region src/cmmdConnection/cmmdCredentialRejection.ts
|
|
71182
71070
|
/**
|
|
71183
|
-
*
|
|
71184
|
-
*
|
|
71185
|
-
* The grant is CMMD's designed recovery path for exactly the case a rotating
|
|
71186
|
-
* refresh token cannot cover: the refresh token is single-use, so one lost or
|
|
71187
|
-
* raced rotation strands the session with no way back. The grant is
|
|
71188
|
-
* NON-rotating — the same token redeems repeatedly until CMMD expires or
|
|
71189
|
-
* revokes it — so redemption is safe to retry and cannot race itself.
|
|
71071
|
+
* How long a recorded rejection stays usable.
|
|
71190
71072
|
*
|
|
71191
|
-
*
|
|
71192
|
-
*
|
|
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.
|
|
71193
71075
|
*/
|
|
71194
|
-
|
|
71195
|
-
|
|
71196
|
-
|
|
71197
|
-
|
|
71198
|
-
|
|
71199
|
-
|
|
71200
|
-
|
|
71201
|
-
|
|
71202
|
-
|
|
71203
|
-
|
|
71204
|
-
|
|
71205
|
-
|
|
71206
|
-
|
|
71207
|
-
|
|
71208
|
-
|
|
71209
|
-
|
|
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);
|
|
71210
71093
|
}
|
|
71211
|
-
const REJECTED_OAUTH_ERROR_CODES = new Set([
|
|
71212
|
-
"invalid_grant",
|
|
71213
|
-
"invalid_client",
|
|
71214
|
-
"invalid_request"
|
|
71215
|
-
]);
|
|
71216
71094
|
/**
|
|
71217
|
-
*
|
|
71095
|
+
* Clear a session's CMMD credentials, but only if CMMD rejected them.
|
|
71218
71096
|
*
|
|
71219
|
-
*
|
|
71220
|
-
*
|
|
71221
|
-
*
|
|
71222
|
-
* phone that had silently stopped accepting its session cost an afternoon
|
|
71223
|
-
* partly because a failed rotation here was indistinguishable from one that
|
|
71224
|
-
* never ran. The token itself is never logged; the status and the OAuth error
|
|
71225
|
-
* code are what identify the failure, and `invalid_grant` in particular means
|
|
71226
|
-
* 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.
|
|
71227
71100
|
*/
|
|
71228
|
-
function
|
|
71229
|
-
|
|
71230
|
-
|
|
71231
|
-
|
|
71232
|
-
|
|
71233
|
-
|
|
71234
|
-
|
|
71235
|
-
|
|
71236
|
-
|
|
71237
|
-
|
|
71238
|
-
|
|
71239
|
-
|
|
71240
|
-
|
|
71241
|
-
|
|
71242
|
-
|
|
71243
|
-
|
|
71244
|
-
|
|
71245
|
-
|
|
71246
|
-
|
|
71247
|
-
|
|
71248
|
-
console.warn(`[cmmd-auth] CMMD JWT rotation could not be completed: ${String(cause)}`);
|
|
71249
|
-
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
|
|
71250
71121
|
});
|
|
71251
71122
|
}
|
|
71123
|
+
/** In-flight mints, keyed by session. */
|
|
71124
|
+
const inFlightMints = /* @__PURE__ */ new Map();
|
|
71252
71125
|
/**
|
|
71253
|
-
*
|
|
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.
|
|
71254
71144
|
*
|
|
71255
|
-
*
|
|
71256
|
-
*
|
|
71257
|
-
*
|
|
71258
|
-
*
|
|
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.
|
|
71259
71154
|
*/
|
|
71260
|
-
|
|
71261
|
-
|
|
71262
|
-
|
|
71263
|
-
|
|
71264
|
-
|
|
71265
|
-
|
|
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
|
+
})));
|
|
71266
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
|
+
});
|
|
71267
71196
|
}
|
|
71268
71197
|
//#endregion
|
|
71269
71198
|
//#region ../../packages/shared/src/cmmdSessionPop.ts
|
|
@@ -71381,10 +71310,6 @@ function resolveCmmdSessionPopSigner() {
|
|
|
71381
71310
|
cachedSigner ??= createServerSigner();
|
|
71382
71311
|
return cachedSigner;
|
|
71383
71312
|
}
|
|
71384
|
-
/** The `pop_public_key` value to present at issuance, or null while disabled. */
|
|
71385
|
-
function cmmdSessionPopPublicKey() {
|
|
71386
|
-
return resolveCmmdSessionPopSigner()?.publicKeyPem ?? null;
|
|
71387
|
-
}
|
|
71388
71313
|
/**
|
|
71389
71314
|
* The three proof headers for one CMMD-origin request, or no headers while
|
|
71390
71315
|
* disabled. Each call carries a fresh nonce; never reuse the result across
|
|
@@ -71400,407 +71325,6 @@ async function cmmdSessionPopRequestHeaders(input) {
|
|
|
71400
71325
|
});
|
|
71401
71326
|
}
|
|
71402
71327
|
//#endregion
|
|
71403
|
-
//#region src/command/cmmdTokenMintLane.ts
|
|
71404
|
-
/**
|
|
71405
|
-
* The CMMD token-mint lane (forge#3518, expand phase).
|
|
71406
|
-
*
|
|
71407
|
-
* A new FIRST rung beside the legacy credential ladder: instead of rotating
|
|
71408
|
-
* CMMD's single-use refresh tokens, the resolver mints a short-lived
|
|
71409
|
-
* forge-scoped access token from the session's durable, non-rotating session
|
|
71410
|
-
* grant (`grant_type: forge_token_mint` on CMMD's token endpoint). The mint
|
|
71411
|
-
* spends nothing that cannot be spent again, so it cannot race itself into
|
|
71412
|
-
* `invalid_grant` the way a rotation can.
|
|
71413
|
-
*
|
|
71414
|
-
* Behind a config flag that is OFF unless explicitly enabled: production
|
|
71415
|
-
* keeps the legacy ladder as the only lane until this is switched on
|
|
71416
|
-
* deliberately. When enabled, the lane is tried before the legacy ladder;
|
|
71417
|
-
* every legacy rung (rotation fence, self-refresh, grant redemption,
|
|
71418
|
-
* bootstrap promotion, subject fallback) still exists below it unchanged and
|
|
71419
|
-
* still guards itself with the rejection ledger.
|
|
71420
|
-
*/
|
|
71421
|
-
/** Env var that enables the mint lane. Unset and anything but an affirmative value is off. */
|
|
71422
|
-
const CMMD_TOKEN_MINT_LANE_ENV = "FORGE_CMMD_TOKEN_MINT_LANE";
|
|
71423
|
-
const AFFIRMATIVE_VALUES = new Set([
|
|
71424
|
-
"1",
|
|
71425
|
-
"on",
|
|
71426
|
-
"true",
|
|
71427
|
-
"yes"
|
|
71428
|
-
]);
|
|
71429
|
-
/**
|
|
71430
|
-
* Whether the mint lane may run. Default off everywhere: a missing env var is
|
|
71431
|
-
* the production posture, and an opt-in flag must not half-default to on in
|
|
71432
|
-
* any environment a misconfigured deploy might land in.
|
|
71433
|
-
*/
|
|
71434
|
-
function isCmmdTokenMintLaneEnabled(env = process.env) {
|
|
71435
|
-
const raw = env[CMMD_TOKEN_MINT_LANE_ENV]?.trim().toLowerCase();
|
|
71436
|
-
return raw !== void 0 && AFFIRMATIVE_VALUES.has(raw);
|
|
71437
|
-
}
|
|
71438
|
-
//#endregion
|
|
71439
|
-
//#region src/command/cmmdRotationFence.ts
|
|
71440
|
-
/**
|
|
71441
|
-
* Log and count a fence application, without changing the verdict it reports.
|
|
71442
|
-
*
|
|
71443
|
-
* The two facts named in the log line are exactly the ones the predicate
|
|
71444
|
-
* above the caller already computed — whether this session ever deposited a
|
|
71445
|
-
* refresh token of its own, and (only when that was false) whether the
|
|
71446
|
-
* session row reads as a desktop bearer session. Neither fact nor the log
|
|
71447
|
-
* line changes what the caller returns.
|
|
71448
|
-
*/
|
|
71449
|
-
function logRotationFenceApplied(input) {
|
|
71450
|
-
return logInfo("CMMD rotation fence applied: session may not rotate its own credential.", {
|
|
71451
|
-
sessionId: input.sessionId,
|
|
71452
|
-
hasStoredRefreshToken: input.hasStoredRefreshToken,
|
|
71453
|
-
isDesktopBearerSession: input.isDesktopBearerSession
|
|
71454
|
-
}).pipe(andThen(() => update$3(withAttributes(cmmdRotationFenceAppliedTotal, metricAttributes({})), 1)));
|
|
71455
|
-
}
|
|
71456
|
-
/**
|
|
71457
|
-
* True when Forge must not rotate this session's CMMD credentials.
|
|
71458
|
-
*
|
|
71459
|
-
* Fails closed: if the session row cannot be read, assume the fence applies.
|
|
71460
|
-
* Spending a token Forge had no right to spend is worse than one extra
|
|
71461
|
-
* reconnect.
|
|
71462
|
-
*/
|
|
71463
|
-
function isRotationFencedSession(sessionId) {
|
|
71464
|
-
const hasStoredRefreshToken = readCommandRefreshTokenForSession(sessionId) !== null;
|
|
71465
|
-
if (hasStoredRefreshToken) return succeed(false);
|
|
71466
|
-
return isDesktopBearerAuthSession(sessionId).pipe(match$1({
|
|
71467
|
-
onFailure: () => ({
|
|
71468
|
-
fenced: true,
|
|
71469
|
-
isDesktopBearerSession: null
|
|
71470
|
-
}),
|
|
71471
|
-
onSuccess: (isDesktopBearerSession) => ({
|
|
71472
|
-
fenced: isDesktopBearerSession,
|
|
71473
|
-
isDesktopBearerSession
|
|
71474
|
-
})
|
|
71475
|
-
}), tap(({ fenced, isDesktopBearerSession }) => fenced ? logRotationFenceApplied({
|
|
71476
|
-
sessionId,
|
|
71477
|
-
hasStoredRefreshToken,
|
|
71478
|
-
isDesktopBearerSession
|
|
71479
|
-
}) : void_$1), map$3(({ fenced }) => fenced));
|
|
71480
|
-
}
|
|
71481
|
-
//#endregion
|
|
71482
|
-
//#region src/cmmdConnection/cmmdCredentialRejection.ts
|
|
71483
|
-
/**
|
|
71484
|
-
* How long a recorded rejection stays usable.
|
|
71485
|
-
*
|
|
71486
|
-
* Long enough to span the resolver ladder and the response that follows it,
|
|
71487
|
-
* short enough that it cannot outlive the credential it describes.
|
|
71488
|
-
*/
|
|
71489
|
-
const REJECTION_TTL_MS = 6e4;
|
|
71490
|
-
const rejectedAt = /* @__PURE__ */ new Map();
|
|
71491
|
-
/** Record that CMMD refused this session's credential outright. */
|
|
71492
|
-
function noteCmmdCredentialRejected(sessionId) {
|
|
71493
|
-
rejectedAt.set(sessionId, Date.now());
|
|
71494
|
-
}
|
|
71495
|
-
/** Whether CMMD refused this session's credential recently enough to act on. */
|
|
71496
|
-
function hasCmmdCredentialRejection(sessionId) {
|
|
71497
|
-
const observedAt = rejectedAt.get(sessionId);
|
|
71498
|
-
if (observedAt === void 0) return false;
|
|
71499
|
-
if (Date.now() - observedAt <= REJECTION_TTL_MS) return true;
|
|
71500
|
-
rejectedAt.delete(sessionId);
|
|
71501
|
-
return false;
|
|
71502
|
-
}
|
|
71503
|
-
/** Drop a recorded rejection, once it has been acted on or superseded. */
|
|
71504
|
-
function forgetCmmdCredentialRejection(sessionId) {
|
|
71505
|
-
rejectedAt.delete(sessionId);
|
|
71506
|
-
}
|
|
71507
|
-
/**
|
|
71508
|
-
* Clear a session's CMMD credentials, but only if CMMD rejected them.
|
|
71509
|
-
*
|
|
71510
|
-
* This is the only door the 401 path may use. A resolver that returned null
|
|
71511
|
-
* because it was not allowed to try is a policy refusal and leaves the stored
|
|
71512
|
-
* credential exactly where it is, so the next request can rotate it.
|
|
71513
|
-
*/
|
|
71514
|
-
const clearCmmdAuthAfterCmmdRejection = (sessionId) => gen(function* () {
|
|
71515
|
-
if (!hasCmmdCredentialRejection(sessionId)) return;
|
|
71516
|
-
yield* clearUnrecoverableCmmdAuthForSession(sessionId, "cmmd-rejected-credential");
|
|
71517
|
-
forgetCmmdCredentialRejection(sessionId);
|
|
71518
|
-
});
|
|
71519
|
-
//#endregion
|
|
71520
|
-
//#region src/command/cmmdJwtResolver.ts
|
|
71521
|
-
function usableCmmdUserJwt(token) {
|
|
71522
|
-
return isCmmdUserAccessJwt(token) ? token : null;
|
|
71523
|
-
}
|
|
71524
|
-
function storeResolvedJwt(sessionId, accessToken, refreshToken) {
|
|
71525
|
-
return setCommandJwtForSession(sessionId, accessToken, refreshToken ? { refreshToken } : {}).pipe(ignore({ log: true }));
|
|
71526
|
-
}
|
|
71527
|
-
function logCmmdJwtRung(context, rung, outcome, reason, lane = "legacy") {
|
|
71528
|
-
return (outcome === "failed" ? logWarning$1 : logInfo)("CMMD JWT resolver rung.", {
|
|
71529
|
-
sessionId: context.sessionId,
|
|
71530
|
-
subject: context.subject,
|
|
71531
|
-
deviceType: context.deviceType,
|
|
71532
|
-
caller: context.caller,
|
|
71533
|
-
lane,
|
|
71534
|
-
rung,
|
|
71535
|
-
outcome,
|
|
71536
|
-
reason
|
|
71537
|
-
});
|
|
71538
|
-
}
|
|
71539
|
-
function recordCmmdCredentialRefreshMetric(rung, outcome) {
|
|
71540
|
-
return update$3(withAttributes(cmmdCredentialRefreshTotal, metricAttributes({
|
|
71541
|
-
rung,
|
|
71542
|
-
outcome
|
|
71543
|
-
})), 1);
|
|
71544
|
-
}
|
|
71545
|
-
const inFlightRefreshes = /* @__PURE__ */ new Map();
|
|
71546
|
-
/** In-flight mints, keyed by session, same shape as `inFlightRefreshes`. */
|
|
71547
|
-
const inFlightMints = /* @__PURE__ */ new Map();
|
|
71548
|
-
/**
|
|
71549
|
-
* Collapse concurrent mints for one session into a single upstream call. A
|
|
71550
|
-
* mint cannot race itself into `invalid_grant` (the grant is non-rotating),
|
|
71551
|
-
* so this is call-rate hygiene, not correctness: two tabs observing the same
|
|
71552
|
-
* expiry must not both hit CMMD when one mint would do.
|
|
71553
|
-
*/
|
|
71554
|
-
function mintOnceForSession(sessionId, grantToken) {
|
|
71555
|
-
return promise(() => {
|
|
71556
|
-
const existing = inFlightMints.get(sessionId);
|
|
71557
|
-
if (existing) return existing;
|
|
71558
|
-
const pending = mintCmmdShortLivedToken(grantToken).catch(() => null).finally(() => {
|
|
71559
|
-
inFlightMints.delete(sessionId);
|
|
71560
|
-
});
|
|
71561
|
-
inFlightMints.set(sessionId, pending);
|
|
71562
|
-
return pending;
|
|
71563
|
-
});
|
|
71564
|
-
}
|
|
71565
|
-
function refreshOnceForSession(sessionId, refreshToken) {
|
|
71566
|
-
return promise(() => {
|
|
71567
|
-
const existing = inFlightRefreshes.get(sessionId);
|
|
71568
|
-
if (existing) return existing;
|
|
71569
|
-
const pending = refreshCmmdJwt(refreshToken, { onCredentialRejected: () => noteCmmdCredentialRejected(sessionId) }).catch(() => null).finally(() => {
|
|
71570
|
-
inFlightRefreshes.delete(sessionId);
|
|
71571
|
-
});
|
|
71572
|
-
inFlightRefreshes.set(sessionId, pending);
|
|
71573
|
-
return pending;
|
|
71574
|
-
});
|
|
71575
|
-
}
|
|
71576
|
-
/**
|
|
71577
|
-
* Rotate this session's **own** CMMD refresh token.
|
|
71578
|
-
*
|
|
71579
|
-
* CMMD rotates refresh tokens single-use — the presented hash is revoked inside
|
|
71580
|
-
* the rotation transaction (CMMD `server/jwt-auth.ts`). A session must
|
|
71581
|
-
* therefore only ever present the refresh token it was issued: rotating a
|
|
71582
|
-
* sibling session's token silently invalidates that sibling's stored copy and
|
|
71583
|
-
* signs it out.
|
|
71584
|
-
*/
|
|
71585
|
-
function trySelfRefresh(sessionId, context) {
|
|
71586
|
-
const currentRefreshToken = readCommandRefreshTokenForSession(sessionId);
|
|
71587
|
-
if (!currentRefreshToken) return logCmmdJwtRung(context, "self-refresh", "skipped", "no refresh token stored for this session").pipe(as$3(null));
|
|
71588
|
-
return gen(function* () {
|
|
71589
|
-
yield* logCmmdJwtRung(context, "self-refresh", "attempted", "rotating this session's own stored refresh token");
|
|
71590
|
-
yield* recordCmmdCredentialRefreshMetric("self-refresh", "attempted");
|
|
71591
|
-
const refreshed = yield* refreshOnceForSession(sessionId, currentRefreshToken);
|
|
71592
|
-
const accessToken = usableCmmdUserJwt(refreshed?.accessToken);
|
|
71593
|
-
if (!accessToken) {
|
|
71594
|
-
yield* logCmmdJwtRung(context, "self-refresh", "failed", "CMMD refresh did not return a usable user access JWT");
|
|
71595
|
-
return null;
|
|
71596
|
-
}
|
|
71597
|
-
yield* storeResolvedJwt(sessionId, accessToken, refreshed?.refreshToken);
|
|
71598
|
-
yield* recordCmmdCredentialRefreshMetric("self-refresh", "succeeded");
|
|
71599
|
-
yield* logCmmdJwtRung(context, "self-refresh", "succeeded", "CMMD rotated the stored refresh token");
|
|
71600
|
-
return accessToken;
|
|
71601
|
-
});
|
|
71602
|
-
}
|
|
71603
|
-
/**
|
|
71604
|
-
* The mint lane's rung (forge#3518, expand phase): mint a short-lived
|
|
71605
|
-
* forge-scoped access token from this session's durable session grant.
|
|
71606
|
-
*
|
|
71607
|
-
* Tried FIRST, before the rotation fence and the whole legacy ladder, when
|
|
71608
|
-
* `FORGE_CMMD_TOKEN_MINT_LANE` is on. Minting spends nothing that cannot be
|
|
71609
|
-
* spent again (the grant is non-rotating), so unlike the legacy rungs it is
|
|
71610
|
-
* safe for a session the rotation fence fences off, and it never feeds the
|
|
71611
|
-
* credential-rejection ledger: this rung failing says nothing about whether
|
|
71612
|
-
* the legacy rungs below it can still succeed.
|
|
71613
|
-
*
|
|
71614
|
-
* Only the JWT is persisted (`setCommandJwtForSession`, no refresh token):
|
|
71615
|
-
* the short lifetime is the expiry story, and re-minting is the refresh
|
|
71616
|
-
* story. A session that lives entirely on this lane accumulates no new
|
|
71617
|
-
* long-lived credential state beyond the grant it already had.
|
|
71618
|
-
*/
|
|
71619
|
-
function tryMintShortLivedToken(sessionId, context) {
|
|
71620
|
-
const grantToken = readCommandSessionGrantForSession(sessionId);
|
|
71621
|
-
if (!grantToken) return logCmmdJwtRung(context, "mint", "skipped", "no session grant stored for this session", "mint").pipe(as$3(null));
|
|
71622
|
-
return gen(function* () {
|
|
71623
|
-
yield* logCmmdJwtRung(context, "mint", "attempted", "minting a short-lived forge-scoped token from this session's grant", "mint");
|
|
71624
|
-
yield* update$3(withAttributes(cmmdTokenMintTotal, metricAttributes({ outcome: "attempted" })), 1);
|
|
71625
|
-
const accessToken = usableCmmdUserJwt((yield* mintOnceForSession(sessionId, grantToken))?.accessToken);
|
|
71626
|
-
if (!accessToken) {
|
|
71627
|
-
yield* logCmmdJwtRung(context, "mint", "failed", "CMMD token mint did not return a usable user access JWT", "mint");
|
|
71628
|
-
return null;
|
|
71629
|
-
}
|
|
71630
|
-
yield* setCommandJwtForSession(sessionId, accessToken, {}).pipe(ignore({ log: true }));
|
|
71631
|
-
yield* update$3(withAttributes(cmmdTokenMintTotal, metricAttributes({ outcome: "succeeded" })), 1);
|
|
71632
|
-
yield* logCmmdJwtRung(context, "mint", "succeeded", "CMMD minted a short-lived forge-scoped token", "mint");
|
|
71633
|
-
return accessToken;
|
|
71634
|
-
});
|
|
71635
|
-
}
|
|
71636
|
-
/**
|
|
71637
|
-
* Last-resort recovery: redeem this session's durable CMMD session grant.
|
|
71638
|
-
*
|
|
71639
|
-
* A refresh token is single-use, so a session that lost a rotation — raced, or
|
|
71640
|
-
* stranded by a redeploy mid-flight — has no way back and the user is bounced
|
|
71641
|
-
* to /login. CMMD mints a long-lived, non-rotating grant at login for exactly
|
|
71642
|
-
* this, and redemption returns a fresh JWT, refresh token AND MCP PAT.
|
|
71643
|
-
*
|
|
71644
|
-
* Only reached after the session's own refresh has already failed, so this
|
|
71645
|
-
* never competes with a live rotation. The grant is not cleared on failure:
|
|
71646
|
-
* CMMD owns its lifetime, and a transient network failure must not throw away
|
|
71647
|
-
* the one credential that can still recover the session.
|
|
71648
|
-
*/
|
|
71649
|
-
function tryGrantRedemption(sessionId, context) {
|
|
71650
|
-
const grantToken = readCommandSessionGrantForSession(sessionId);
|
|
71651
|
-
if (!grantToken) return logCmmdJwtRung(context, "grant-redemption", "skipped", "no session grant stored for this session").pipe(as$3(null));
|
|
71652
|
-
return gen(function* () {
|
|
71653
|
-
yield* logCmmdJwtRung(context, "grant-redemption", "attempted", "redeeming this session's durable CMMD session grant");
|
|
71654
|
-
yield* recordCmmdCredentialRefreshMetric("grant-redemption", "attempted");
|
|
71655
|
-
const redeemed = yield* promise(() => redeemCmmdSessionGrant(grantToken, cmmdSessionPopPublicKey())).pipe(orElseSucceed(() => null));
|
|
71656
|
-
const accessToken = usableCmmdUserJwt(redeemed?.accessToken);
|
|
71657
|
-
if (!accessToken) {
|
|
71658
|
-
yield* logCmmdJwtRung(context, "grant-redemption", "failed", "session grant redemption did not return a usable user access JWT");
|
|
71659
|
-
return null;
|
|
71660
|
-
}
|
|
71661
|
-
if (redeemed?.mcpToken) yield* setCommandAccessTokenForSession(sessionId, redeemed.mcpToken).pipe(ignore({ log: true }));
|
|
71662
|
-
yield* storeResolvedJwt(sessionId, accessToken, redeemed?.refreshToken);
|
|
71663
|
-
yield* recordCmmdCredentialRefreshMetric("grant-redemption", "succeeded");
|
|
71664
|
-
yield* logCmmdJwtRung(context, "grant-redemption", "succeeded", "session grant redeemed");
|
|
71665
|
-
return accessToken;
|
|
71666
|
-
});
|
|
71667
|
-
}
|
|
71668
|
-
/**
|
|
71669
|
-
* Recover using only credentials this session owns, strongest first.
|
|
71670
|
-
*
|
|
71671
|
-
* Rotating its own refresh token is preferred; the durable grant is the
|
|
71672
|
-
* fallback for when that rotation is already gone. Neither touches another
|
|
71673
|
-
* session's credentials, so this ladder is safe to run from any path.
|
|
71674
|
-
*/
|
|
71675
|
-
function tryOwnCredentialRecovery(sessionId, context) {
|
|
71676
|
-
return gen(function* () {
|
|
71677
|
-
const refreshed = yield* trySelfRefresh(sessionId, context);
|
|
71678
|
-
if (refreshed) return refreshed;
|
|
71679
|
-
return yield* tryGrantRedemption(sessionId, context);
|
|
71680
|
-
});
|
|
71681
|
-
}
|
|
71682
|
-
function tryPromoteBootstrap(sessionId, subject, forceRefresh, context) {
|
|
71683
|
-
if (subject !== "owner-bootstrap" && subject !== "desktop-bootstrap") return logCmmdJwtRung(context, "promote-bootstrap", "skipped", "subject is not a bootstrap subject").pipe(as$3(null));
|
|
71684
|
-
return gen(function* () {
|
|
71685
|
-
yield* logCmmdJwtRung(context, "promote-bootstrap", "attempted", "promoting the most recent owner CMMD auth to this bootstrap session");
|
|
71686
|
-
yield* promoteMostRecentOwnerCommandAuthToSession(sessionId).pipe(catch_((error) => logWarning$1("Failed to promote owner CMMD auth for REST JWT resolution.", {
|
|
71687
|
-
cause: error,
|
|
71688
|
-
sessionId,
|
|
71689
|
-
subject
|
|
71690
|
-
})));
|
|
71691
|
-
const promotedJwt = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
|
|
71692
|
-
if (!forceRefresh && promotedJwt && !isJwtExpiredForSession(sessionId)) {
|
|
71693
|
-
yield* logCmmdJwtRung(context, "promote-bootstrap", "succeeded", "promoted owner JWT is usable");
|
|
71694
|
-
return promotedJwt;
|
|
71695
|
-
}
|
|
71696
|
-
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");
|
|
71697
|
-
return null;
|
|
71698
|
-
});
|
|
71699
|
-
}
|
|
71700
|
-
/**
|
|
71701
|
-
* Borrow a sibling session's still-valid CMMD access JWT.
|
|
71702
|
-
*
|
|
71703
|
-
* `readMostRecentJwtForSubject` only returns a JWT that has not expired, so
|
|
71704
|
-
* this is a cheap read-only recovery that avoids an upstream rotation. The
|
|
71705
|
-
* sibling's *refresh* token is deliberately NOT copied: it is single-use and
|
|
71706
|
-
* must stay private to the session it was issued to (see `trySelfRefresh`).
|
|
71707
|
-
* Copying it is what let one expired tab sign the user out everywhere.
|
|
71708
|
-
*/
|
|
71709
|
-
function trySubjectFallback(sessionId, subject, context) {
|
|
71710
|
-
return gen(function* () {
|
|
71711
|
-
yield* logCmmdJwtRung(context, "subject-fallback", "attempted", "looking for a sibling session's still-valid JWT for this subject");
|
|
71712
|
-
const fallbackJwt = usableCmmdUserJwt((yield* readMostRecentJwtForSubject(subject).pipe(orElseSucceed(() => null)))?.jwt);
|
|
71713
|
-
if (!fallbackJwt) {
|
|
71714
|
-
yield* logCmmdJwtRung(context, "subject-fallback", "failed", "no sibling session holds a still-valid JWT for this subject");
|
|
71715
|
-
return null;
|
|
71716
|
-
}
|
|
71717
|
-
yield* storeResolvedJwt(sessionId, fallbackJwt, null);
|
|
71718
|
-
yield* logCmmdJwtRung(context, "subject-fallback", "succeeded", "borrowed a sibling session's still-valid JWT");
|
|
71719
|
-
return fallbackJwt;
|
|
71720
|
-
});
|
|
71721
|
-
}
|
|
71722
|
-
/**
|
|
71723
|
-
* The rungs below the rotation fence: forced own-credential recovery,
|
|
71724
|
-
* bootstrap promotion, subject fallback, then own-credential recovery again
|
|
71725
|
-
* as the last resort. Split out of `resolveCmmdJwtForSession` so each half of
|
|
71726
|
-
* the ladder stays independently readable (and independently testable) —
|
|
71727
|
-
* the fence check above is the one decision the split does not touch.
|
|
71728
|
-
*/
|
|
71729
|
-
function runRecoveryLadder(sessionId, normalizedSubject, forceRefresh, context) {
|
|
71730
|
-
return gen(function* () {
|
|
71731
|
-
if (forceRefresh) {
|
|
71732
|
-
const forced = yield* tryOwnCredentialRecovery(sessionId, context);
|
|
71733
|
-
if (forced) return forced;
|
|
71734
|
-
}
|
|
71735
|
-
if (normalizedSubject) {
|
|
71736
|
-
const promoted = yield* tryPromoteBootstrap(sessionId, normalizedSubject, forceRefresh, context);
|
|
71737
|
-
if (promoted) return promoted;
|
|
71738
|
-
}
|
|
71739
|
-
if (!normalizedSubject) {
|
|
71740
|
-
yield* logCmmdJwtRung(context, "cache", "failed", "no subject to resolve and forced recovery already exhausted");
|
|
71741
|
-
return null;
|
|
71742
|
-
}
|
|
71743
|
-
if (!forceRefresh) {
|
|
71744
|
-
const fallback = yield* trySubjectFallback(sessionId, normalizedSubject, context);
|
|
71745
|
-
if (fallback) return fallback;
|
|
71746
|
-
}
|
|
71747
|
-
const recovered = yield* tryOwnCredentialRecovery(sessionId, context);
|
|
71748
|
-
if (!recovered) yield* logCmmdJwtRung(context, "cache", "failed", "recovery ladder exhausted: no rung produced a usable JWT");
|
|
71749
|
-
return recovered;
|
|
71750
|
-
});
|
|
71751
|
-
}
|
|
71752
|
-
/**
|
|
71753
|
-
* Resolve the CMMD REST JWT for a Forge session.
|
|
71754
|
-
*
|
|
71755
|
-
* Forge session auth is long-lived compared with the short-lived CMMD JWT.
|
|
71756
|
-
* When the session JWT expires, recover by minting a fresh short-lived token
|
|
71757
|
-
* from the session grant (the mint lane; default-off, forge#3518), or, on
|
|
71758
|
-
* the legacy lane, by promoting a valid subject-level JWT or rotating the
|
|
71759
|
-
* stored CMMD refresh token.
|
|
71760
|
-
*
|
|
71761
|
-
* Fail-closed: never returns a non-user / malformed Bearer. Callers must map
|
|
71762
|
-
* null to CMMD_AUTH_REQUIRED and must not call CMMD upstream with a bad token.
|
|
71763
|
-
*/
|
|
71764
|
-
function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false, staleAccessToken = null }) {
|
|
71765
|
-
const cached = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
|
|
71766
|
-
if (cached && !isJwtExpiredForSession(sessionId)) {
|
|
71767
|
-
if (!forceRefresh) return succeed(cached);
|
|
71768
|
-
if (staleAccessToken && cached !== staleAccessToken) return succeed(cached).pipe(tap(() => gen(function* () {
|
|
71769
|
-
const deviceType = yield* readClientDeviceTypeForSession(sessionId);
|
|
71770
|
-
yield* logCmmdJwtRung({
|
|
71771
|
-
sessionId,
|
|
71772
|
-
subject: subject?.trim() || null,
|
|
71773
|
-
deviceType,
|
|
71774
|
-
caller: logPrefix
|
|
71775
|
-
}, "cache", "skipped", "forced refresh skipped: cache already holds a token another caller rotated");
|
|
71776
|
-
})));
|
|
71777
|
-
}
|
|
71778
|
-
const normalizedSubject = subject?.trim() || null;
|
|
71779
|
-
const mintLaneEnabled = isCmmdTokenMintLaneEnabled();
|
|
71780
|
-
if (!normalizedSubject && !forceRefresh && !mintLaneEnabled) return succeed(null);
|
|
71781
|
-
return gen(function* () {
|
|
71782
|
-
const context = {
|
|
71783
|
-
sessionId,
|
|
71784
|
-
subject: normalizedSubject,
|
|
71785
|
-
deviceType: yield* readClientDeviceTypeForSession(sessionId),
|
|
71786
|
-
caller: logPrefix
|
|
71787
|
-
};
|
|
71788
|
-
if (mintLaneEnabled) {
|
|
71789
|
-
const minted = yield* tryMintShortLivedToken(sessionId, context);
|
|
71790
|
-
if (minted) return minted;
|
|
71791
|
-
if (!normalizedSubject && !forceRefresh) {
|
|
71792
|
-
yield* logCmmdJwtRung(context, "mint", "failed", "no subject to resolve and the mint lane produced nothing", "mint");
|
|
71793
|
-
return null;
|
|
71794
|
-
}
|
|
71795
|
-
}
|
|
71796
|
-
if (yield* isRotationFencedSession(sessionId)) {
|
|
71797
|
-
yield* logCmmdJwtRung(context, "rotation-fence", "failed", "session is fenced from rotating CMMD credentials");
|
|
71798
|
-
return null;
|
|
71799
|
-
}
|
|
71800
|
-
return yield* runRecoveryLadder(sessionId, normalizedSubject, forceRefresh, context);
|
|
71801
|
-
});
|
|
71802
|
-
}
|
|
71803
|
-
//#endregion
|
|
71804
71328
|
//#region src/cmmdData/cmmdRestProxyDeadline.ts
|
|
71805
71329
|
var CmmdProxyTimeoutError = class extends Error {
|
|
71806
71330
|
status;
|
|
@@ -76358,7 +75882,7 @@ var require_node_gyp_build = /* @__PURE__ */ __commonJSMin(((exports, module) =>
|
|
|
76358
75882
|
var fs$6 = __require("fs");
|
|
76359
75883
|
var path$5 = __require("path");
|
|
76360
75884
|
var url$1 = __require("url");
|
|
76361
|
-
var os$
|
|
75885
|
+
var os$2 = __require("os");
|
|
76362
75886
|
var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
|
|
76363
75887
|
var vars = process.config && process.config.variables || {};
|
|
76364
75888
|
var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
|
|
@@ -76366,8 +75890,8 @@ var require_node_gyp_build = /* @__PURE__ */ __commonJSMin(((exports, module) =>
|
|
|
76366
75890
|
var abi = versions.modules;
|
|
76367
75891
|
if (versions.deno || process.isBun) abi = "unsupported";
|
|
76368
75892
|
var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
|
|
76369
|
-
var arch = process.env.npm_config_arch || os$
|
|
76370
|
-
var platform = process.env.npm_config_platform || os$
|
|
75893
|
+
var arch = process.env.npm_config_arch || os$2.arch();
|
|
75894
|
+
var platform = process.env.npm_config_platform || os$2.platform();
|
|
76371
75895
|
var libc = process.env.LIBC || (isMusl(platform) ? "musl" : "glibc");
|
|
76372
75896
|
var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
|
|
76373
75897
|
var uv = (versions.uv || "").split(".")[0];
|
|
@@ -94211,7 +93735,7 @@ const launchDetached = (launch) => gen(function* () {
|
|
|
94211
93735
|
});
|
|
94212
93736
|
const OpenLive = effect(Open, gen(function* () {
|
|
94213
93737
|
const open = yield* tryPromise({
|
|
94214
|
-
try: () => import("./open-
|
|
93738
|
+
try: () => import("./open-2-reBrpv.mjs"),
|
|
94215
93739
|
catch: (cause) => new OpenError$1({
|
|
94216
93740
|
message: "failed to load browser opener",
|
|
94217
93741
|
cause
|
|
@@ -115944,7 +115468,7 @@ const postgresSchemaStatements = [
|
|
|
115944
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)",
|
|
115945
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)",
|
|
115946
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)",
|
|
115947
|
-
"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)",
|
|
115948
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 )",
|
|
115949
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 )",
|
|
115950
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 )",
|
|
@@ -116257,7 +115781,8 @@ const postgresUpgradeMigrations = new Map([
|
|
|
116257
115781
|
WHERE model_selection_json IS NULL`
|
|
116258
115782
|
]],
|
|
116259
115783
|
[77, [`ALTER TABLE projection_threads ADD COLUMN IF NOT EXISTS work_session_id TEXT`]],
|
|
116260
|
-
[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`]]
|
|
116261
115786
|
]);
|
|
116262
115787
|
function requirePostgresUpgradeStatements(version) {
|
|
116263
115788
|
const statements = postgresUpgradeMigrations.get(version);
|
|
@@ -116283,15 +115808,15 @@ const runPostgresMigrations = fn$1("runPostgresMigrations")(function* () {
|
|
|
116283
115808
|
ORDER BY version DESC
|
|
116284
115809
|
LIMIT 1
|
|
116285
115810
|
`)[0]?.version ?? 0;
|
|
116286
|
-
if (currentVersion >
|
|
115811
|
+
if (currentVersion > 79) return yield* die$1(/* @__PURE__ */ new Error(`Forge Postgres schema ${currentVersion} is newer than server schema 79.`));
|
|
116287
115812
|
if (currentVersion === 0) {
|
|
116288
115813
|
for (const statement of postgresSchemaStatements) yield* sql.unsafe(statement);
|
|
116289
115814
|
yield* sql`
|
|
116290
115815
|
INSERT INTO forge_schema_migrations (version)
|
|
116291
|
-
VALUES (${
|
|
115816
|
+
VALUES (${79})
|
|
116292
115817
|
ON CONFLICT (version) DO NOTHING
|
|
116293
115818
|
`;
|
|
116294
|
-
} else for (let version = currentVersion + 1; version <=
|
|
115819
|
+
} else for (let version = currentVersion + 1; version <= 79; version += 1) {
|
|
116295
115820
|
const statements = yield* sync$1(() => requirePostgresUpgradeStatements(version));
|
|
116296
115821
|
for (const statement of statements) yield* sql.unsafe(statement);
|
|
116297
115822
|
yield* sql`
|
|
@@ -119099,6 +118624,26 @@ var _078_ProjectionThreadWorkSessionBoundAt_default = gen(function* () {
|
|
|
119099
118624
|
`;
|
|
119100
118625
|
});
|
|
119101
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
|
|
119102
118647
|
//#region src/persistence/Migrations.ts
|
|
119103
118648
|
/**
|
|
119104
118649
|
* MigrationsLive - Migration runner with inline loader
|
|
@@ -119509,6 +119054,11 @@ const migrationEntries = [
|
|
|
119509
119054
|
78,
|
|
119510
119055
|
"ProjectionThreadWorkSessionBoundAt",
|
|
119511
119056
|
_078_ProjectionThreadWorkSessionBoundAt_default
|
|
119057
|
+
],
|
|
119058
|
+
[
|
|
119059
|
+
79,
|
|
119060
|
+
"DropCommandRefreshToken",
|
|
119061
|
+
_079_DropCommandRefreshToken_default
|
|
119512
119062
|
]
|
|
119513
119063
|
];
|
|
119514
119064
|
const makeMigrationLoader = (throughId) => fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
|
|
@@ -192530,7 +192080,7 @@ var Vce = {
|
|
|
192530
192080
|
function Ve() {
|
|
192531
192081
|
return Vce;
|
|
192532
192082
|
}
|
|
192533
|
-
var Us =
|
|
192083
|
+
var Us = $U.homedir(), lk = $U.tmpdir(), { env: Vc } = process$1, Yce = (e) => {
|
|
192534
192084
|
let t = path.join(Us, "Library");
|
|
192535
192085
|
return {
|
|
192536
192086
|
data: path.join(t, "Application Support", e),
|
|
@@ -202730,12 +202280,12 @@ function qZ(e, t) {
|
|
|
202730
202280
|
}
|
|
202731
202281
|
return n;
|
|
202732
202282
|
}
|
|
202733
|
-
var os$
|
|
202283
|
+
var os$1 = "https://code.claude.com/docs/en", kKe = [
|
|
202734
202284
|
{
|
|
202735
202285
|
matches: (e) => e.path === "permissions.defaultMode" && e.code === "invalid_value",
|
|
202736
202286
|
tip: {
|
|
202737
202287
|
suggestion: "Valid modes: \"acceptEdits\" (ask before file changes), \"plan\" (analysis only), \"bypassPermissions\" (auto-accept all), or \"default\" (standard behavior)",
|
|
202738
|
-
docLink: `${os$
|
|
202288
|
+
docLink: `${os$1}/iam#permission-modes`
|
|
202739
202289
|
}
|
|
202740
202290
|
},
|
|
202741
202291
|
{
|
|
@@ -202750,7 +202300,7 @@ var os$2 = "https://code.claude.com/docs/en", kKe = [
|
|
|
202750
202300
|
matches: (e) => e.path.startsWith("env.") && e.code === "invalid_type",
|
|
202751
202301
|
tip: {
|
|
202752
202302
|
suggestion: "Environment variables must be strings. Wrap numbers and booleans in quotes. Example: \"DEBUG\": \"true\", \"PORT\": \"3000\"",
|
|
202753
|
-
docLink: `${os$
|
|
202303
|
+
docLink: `${os$1}/settings#environment-variables`
|
|
202754
202304
|
}
|
|
202755
202305
|
},
|
|
202756
202306
|
{
|
|
@@ -202761,14 +202311,14 @@ var os$2 = "https://code.claude.com/docs/en", kKe = [
|
|
|
202761
202311
|
matches: (e) => e.path.startsWith("hooks.") && e.code === "invalid_key",
|
|
202762
202312
|
tip: {
|
|
202763
202313
|
suggestion: "Not a recognized hook event. Common events: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SessionEnd, Stop. Check spelling and capitalization.",
|
|
202764
|
-
docLink: `${os$
|
|
202314
|
+
docLink: `${os$1}/hooks`
|
|
202765
202315
|
}
|
|
202766
202316
|
},
|
|
202767
202317
|
{
|
|
202768
202318
|
matches: (e) => /\.hooks\.\d+\.command$/.test(e.path) && e.code === "invalid_type" && e.received === "undefined",
|
|
202769
202319
|
tip: {
|
|
202770
202320
|
suggestion: "Command hooks require `command`. For exec form (no shell), set `command` to the executable and `args` to its arguments: {\"type\": \"command\", \"command\": \"echo\", \"args\": [\"hi\"]}. For shell form, set `command` to the full shell string: {\"type\": \"command\", \"command\": \"echo hi\"}.",
|
|
202771
|
-
docLink: `${os$
|
|
202321
|
+
docLink: `${os$1}/hooks#exec-form-and-shell-form`
|
|
202772
202322
|
}
|
|
202773
202323
|
},
|
|
202774
202324
|
{
|
|
@@ -202783,7 +202333,7 @@ var os$2 = "https://code.claude.com/docs/en", kKe = [
|
|
|
202783
202333
|
matches: (e) => e.code === "unrecognized_keys",
|
|
202784
202334
|
tip: {
|
|
202785
202335
|
suggestion: "Check for typos or refer to the documentation for valid fields",
|
|
202786
|
-
docLink: `${os$
|
|
202336
|
+
docLink: `${os$1}/settings`
|
|
202787
202337
|
}
|
|
202788
202338
|
},
|
|
202789
202339
|
{
|
|
@@ -202798,13 +202348,13 @@ var os$2 = "https://code.claude.com/docs/en", kKe = [
|
|
|
202798
202348
|
matches: (e) => e.path === "permissions.additionalDirectories" && e.code === "invalid_type",
|
|
202799
202349
|
tip: {
|
|
202800
202350
|
suggestion: "Must be an array of directory paths. Example: [\"~/projects\", \"/tmp/workspace\"]. You can also use --add-dir flag or /add-dir command",
|
|
202801
|
-
docLink: `${os$
|
|
202351
|
+
docLink: `${os$1}/iam#working-directories`
|
|
202802
202352
|
}
|
|
202803
202353
|
}
|
|
202804
202354
|
], AKe = {
|
|
202805
|
-
permissions: `${os$
|
|
202806
|
-
env: `${os$
|
|
202807
|
-
hooks: `${os$
|
|
202355
|
+
permissions: `${os$1}/iam#configuring-permissions`,
|
|
202356
|
+
env: `${os$1}/settings#environment-variables`,
|
|
202357
|
+
hooks: `${os$1}/hooks`
|
|
202808
202358
|
};
|
|
202809
202359
|
function GZ(e) {
|
|
202810
202360
|
let t = kKe.find((r) => r.matches(e));
|
|
@@ -271348,7 +270898,7 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
|
|
|
271348
270898
|
var dgram = __require("dgram");
|
|
271349
270899
|
var thunky = require_thunky();
|
|
271350
270900
|
var events = __require("events");
|
|
271351
|
-
var os
|
|
270901
|
+
var os = __require("os");
|
|
271352
270902
|
var noop = function() {};
|
|
271353
270903
|
module.exports = function(opts) {
|
|
271354
270904
|
if (!opts) opts = {};
|
|
@@ -271479,14 +271029,14 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
|
|
|
271479
271029
|
return that;
|
|
271480
271030
|
};
|
|
271481
271031
|
function defaultInterface() {
|
|
271482
|
-
var networks = os
|
|
271032
|
+
var networks = os.networkInterfaces();
|
|
271483
271033
|
var names = Object.keys(networks);
|
|
271484
271034
|
for (var i = 0; i < names.length; i++) {
|
|
271485
271035
|
var net = networks[names[i]];
|
|
271486
271036
|
for (var j = 0; j < net.length; j++) {
|
|
271487
271037
|
var iface = net[j];
|
|
271488
271038
|
if (isIPv4(iface.family) && !iface.internal) {
|
|
271489
|
-
if (os
|
|
271039
|
+
if (os.platform() === "darwin" && names[i] === "en0") return iface.address;
|
|
271490
271040
|
return "0.0.0.0";
|
|
271491
271041
|
}
|
|
271492
271042
|
}
|
|
@@ -271494,7 +271044,7 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
|
|
|
271494
271044
|
return "127.0.0.1";
|
|
271495
271045
|
}
|
|
271496
271046
|
function allInterfaces() {
|
|
271497
|
-
var networks = os
|
|
271047
|
+
var networks = os.networkInterfaces();
|
|
271498
271048
|
var names = Object.keys(networks);
|
|
271499
271049
|
var res = [];
|
|
271500
271050
|
for (var i = 0; i < names.length; i++) {
|
|
@@ -300227,7 +299777,7 @@ function resolveBuildCommitFromEnv(env) {
|
|
|
300227
299777
|
* environment descriptor down instead of reporting an honest "unknown".
|
|
300228
299778
|
*/
|
|
300229
299779
|
function readBakedBuildCommit() {
|
|
300230
|
-
return "
|
|
299780
|
+
return "ba1d9d7ef5237a6a7d52a06b10c4d15f8cff00b8";
|
|
300231
299781
|
}
|
|
300232
299782
|
async function resolveServerBuildCommit(input) {
|
|
300233
299783
|
if (isFullCommitSha(input.baked)) return input.baked;
|
|
@@ -300473,14 +300023,13 @@ function identityFromRedeemed(redeemed) {
|
|
|
300473
300023
|
username: redeemed.displayName?.trim() || `user-${redeemed.userId}`
|
|
300474
300024
|
};
|
|
300475
300025
|
}
|
|
300026
|
+
/** Read optional display name, owner assertion, session grant, and expiry from a redeemed handoff. */
|
|
300476
300027
|
function optionalRedeemedFields(body) {
|
|
300477
|
-
const refreshToken = readTrimmedString(body.refreshToken);
|
|
300478
300028
|
const displayName = typeof body.displayName === "string" ? body.displayName : void 0;
|
|
300479
300029
|
const forgeOwnerAssertion = readTrimmedString(body.forgeOwnerAssertion);
|
|
300480
300030
|
const sessionGrant = readTrimmedString(body.sessionGrant);
|
|
300481
300031
|
const sessionGrantExpiresAt = typeof body.sessionGrantExpiresAt === "string" ? body.sessionGrantExpiresAt : void 0;
|
|
300482
300032
|
return {
|
|
300483
|
-
...refreshToken !== void 0 ? { refreshToken } : {},
|
|
300484
300033
|
...displayName !== void 0 ? { displayName } : {},
|
|
300485
300034
|
...forgeOwnerAssertion !== void 0 ? { forgeOwnerAssertion } : {},
|
|
300486
300035
|
...sessionGrant !== void 0 ? { sessionGrant } : {},
|
|
@@ -300548,6 +300097,7 @@ function redeemEmbedCodeWithCmmd(code) {
|
|
|
300548
300097
|
return succeed(redeemed);
|
|
300549
300098
|
}));
|
|
300550
300099
|
}
|
|
300100
|
+
/** Store the redeemed PAT, JWT, and identity for a local embed session, failing on write errors. */
|
|
300551
300101
|
function persistLocalEmbedSession(input) {
|
|
300552
300102
|
return gen(function* () {
|
|
300553
300103
|
yield* setCommandAccessTokenForSession(input.sessionId, input.redeemed.mcpToken).pipe(mapError((cause) => new AuthError({
|
|
@@ -300555,7 +300105,7 @@ function persistLocalEmbedSession(input) {
|
|
|
300555
300105
|
status: 500,
|
|
300556
300106
|
cause
|
|
300557
300107
|
})));
|
|
300558
|
-
yield* setCommandJwtForSession(input.sessionId, input.redeemed.accessToken
|
|
300108
|
+
yield* setCommandJwtForSession(input.sessionId, input.redeemed.accessToken).pipe(mapError((cause) => new AuthError({
|
|
300559
300109
|
message: "Failed to persist CMMD JWT.",
|
|
300560
300110
|
status: 500,
|
|
300561
300111
|
cause
|
|
@@ -300610,7 +300160,6 @@ const authCmmdEmbedExchangeRouteLayer = add$1("POST", "/api/auth/cmmd-embed/exch
|
|
|
300610
300160
|
accessToken: redeemed.mcpToken,
|
|
300611
300161
|
identity,
|
|
300612
300162
|
jwtToken: redeemed.accessToken,
|
|
300613
|
-
refreshToken: redeemed.refreshToken,
|
|
300614
300163
|
sessionGrant,
|
|
300615
300164
|
sessionGrantExpiresAt: redeemed.sessionGrantExpiresAt
|
|
300616
300165
|
});
|
|
@@ -300716,7 +300265,7 @@ const cloudOwnerSessionRedeemRouteLayer = add$1("POST", "/api/auth/cloud/session
|
|
|
300716
300265
|
status: 403
|
|
300717
300266
|
});
|
|
300718
300267
|
const tokenResult = yield* redeemSessionGrantWithCmmd(payload);
|
|
300719
|
-
const { tokenObj, cmmdIdentity, userId, accessToken, mcpToken,
|
|
300268
|
+
const { tokenObj, cmmdIdentity, userId, accessToken, mcpToken, forgeOwnerAssertion, sessionGrantExpiresAt, isJwt, patToStore, jwtToStore } = yield* resolveCmmdTokenBundleCredentials(tokenResult);
|
|
300720
300269
|
if (!cmmdIdentity || !userId || !patToStore) {
|
|
300721
300270
|
yield* logError("Forge Cloud session redeem: CMMD token response incomplete.", {
|
|
300722
300271
|
clientId: payload.clientId,
|
|
@@ -300725,7 +300274,6 @@ const cloudOwnerSessionRedeemRouteLayer = add$1("POST", "/api/auth/cloud/session
|
|
|
300725
300274
|
tokenResult,
|
|
300726
300275
|
accessToken,
|
|
300727
300276
|
mcpToken,
|
|
300728
|
-
refreshToken,
|
|
300729
300277
|
isJwt,
|
|
300730
300278
|
userId,
|
|
300731
300279
|
patToStore,
|
|
@@ -300748,7 +300296,6 @@ const cloudOwnerSessionRedeemRouteLayer = add$1("POST", "/api/auth/cloud/session
|
|
|
300748
300296
|
accessToken: patToStore,
|
|
300749
300297
|
identity: cmmdIdentity,
|
|
300750
300298
|
jwtToken: jwtToStore,
|
|
300751
|
-
refreshToken,
|
|
300752
300299
|
sessionGrant: payload.sessionGrant,
|
|
300753
300300
|
sessionGrantExpiresAt
|
|
300754
300301
|
});
|
|
@@ -325696,7 +325243,7 @@ const connectLoginCommand = make$33("login", {
|
|
|
325696
325243
|
})).json();
|
|
325697
325244
|
},
|
|
325698
325245
|
openBrowser: async (url) => {
|
|
325699
|
-
const open = (await import("./open-
|
|
325246
|
+
const open = (await import("./open-2-reBrpv.mjs")).default;
|
|
325700
325247
|
await open(url);
|
|
325701
325248
|
return true;
|
|
325702
325249
|
},
|