@cmmd-center/forge 0.13.132 → 0.13.133
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.cjs +279 -732
- package/dist/bin.mjs +303 -756
- package/dist/client/assets/{DiffPanel-Ca2Lz8RB.js → DiffPanel-RwcR62h1.js} +2 -2
- package/dist/client/assets/{DiffPanel.logic-H_2wKBOQ.js → DiffPanel.logic-p0v4whSE.js} +2 -2
- package/dist/client/assets/{DiffWorkerPoolProvider-DSDTeZXk.js → DiffWorkerPoolProvider-DuQCCe4H.js} +2 -2
- package/dist/client/assets/{PullRequestCodePanel-7YAkNwLW.js → PullRequestCodePanel-dQ0P1BYv.js} +2 -2
- package/dist/client/assets/{Virtualizer-BzBcEfxL.js → Virtualizer-B7qA3afH.js} +2 -2
- package/dist/client/assets/{WorkerPoolContext-8onOdaSo.js → WorkerPoolContext-DJIHDFVZ.js} +2 -2
- package/dist/client/assets/{cmmdForgeRuntimeListResource-Bl0ObO-g.js → cmmdForgeRuntimeListResource-uAVdZfT3.js} +4 -4
- package/dist/client/assets/{currentForgeWorkspaceStore-BqHfGv0o.js → currentForgeWorkspaceStore-DgPGQMqz.js} +2 -2
- package/dist/client/assets/{index-zPBt45p3.js → index-BSqrLyPh.js} +6 -6
- 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/bin.cjs
CHANGED
|
@@ -66407,7 +66407,7 @@ function normalizeNumberish(value) {
|
|
|
66407
66407
|
}
|
|
66408
66408
|
//#endregion
|
|
66409
66409
|
//#region package.json
|
|
66410
|
-
var version$1 = "0.13.
|
|
66410
|
+
var version$1 = "0.13.133";
|
|
66411
66411
|
//#endregion
|
|
66412
66412
|
//#region src/sentry.ts
|
|
66413
66413
|
const SERVER_APP_NAME = "forge-server";
|
|
@@ -67366,9 +67366,7 @@ const runtimeReplicationOutboxOldestEventAge = require_Schema$1.gauge("t3_runtim
|
|
|
67366
67366
|
const runtimeReplicationPublicationAttemptsTotal = require_Schema$1.counter("t3_runtime_replication_publication_attempts_total", { description: "Total canonical runtime replication publication attempts." });
|
|
67367
67367
|
const runtimeReplicationPublicationRetriesTotal = require_Schema$1.counter("t3_runtime_replication_publication_retries_total", { description: "Total runtime replication retries scheduled after recoverable failures." });
|
|
67368
67368
|
const runtimeReplicationPublicationLatency = require_Schema$1.timer("t3_runtime_replication_publication_latency", { description: "Time from durable enqueue to canonical CMMD acknowledgement." });
|
|
67369
|
-
const
|
|
67370
|
-
const cmmdTokenMintTotal = require_Schema$1.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." });
|
|
67371
|
-
const cmmdRotationFenceAppliedTotal = require_Schema$1.counter("t3_cmmd_rotation_fence_applied_total", { description: "Sessions Forge refused to rotate CMMD credentials for, because the rotation fence applied." });
|
|
67369
|
+
const cmmdTokenMintTotal = require_Schema$1.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." });
|
|
67372
67370
|
const cmmdCredentialRowDeletedTotal = require_Schema$1.counter("t3_cmmd_credential_row_deleted_total", { description: "command_access_tokens rows actually deleted by clearUnrecoverableCmmdAuthForSession, by reason." });
|
|
67373
67371
|
const metricAttributes = (attributes) => Object.entries(compactMetricAttributes(attributes));
|
|
67374
67372
|
const increment = (metric, attributes, amount = 1) => require_Schema$1.update(require_Schema$1.withAttributes(metric, metricAttributes(attributes)), amount);
|
|
@@ -67449,12 +67447,10 @@ const isPersistenceError = (u) => require_Schema$1.is(PersistenceSqlError)(u) ||
|
|
|
67449
67447
|
const cache = /* @__PURE__ */ new Map();
|
|
67450
67448
|
const jwtCache = /* @__PURE__ */ new Map();
|
|
67451
67449
|
const jwtExpiryCache = /* @__PURE__ */ new Map();
|
|
67452
|
-
const refreshTokenCache = /* @__PURE__ */ new Map();
|
|
67453
67450
|
const identityCache = /* @__PURE__ */ new Map();
|
|
67454
67451
|
const sessionGrantCache = /* @__PURE__ */ new Map();
|
|
67455
67452
|
const ACCESS_TOKEN_SCOPE = "cmmd-access-token";
|
|
67456
67453
|
const JWT_TOKEN_SCOPE = "cmmd-jwt-token";
|
|
67457
|
-
const REFRESH_TOKEN_SCOPE = "cmmd-refresh-token";
|
|
67458
67454
|
const IDENTITY_SCOPE = "cmmd-user-identity";
|
|
67459
67455
|
const SESSION_GRANT_SCOPE = "cmmd-session-grant";
|
|
67460
67456
|
const CommandAccessTokenRow = require_Schema$1.Struct({
|
|
@@ -67462,27 +67458,15 @@ const CommandAccessTokenRow = require_Schema$1.Struct({
|
|
|
67462
67458
|
accessToken: require_Schema$1.String,
|
|
67463
67459
|
jwtToken: require_Schema$1.NullOr(require_Schema$1.String),
|
|
67464
67460
|
jwtExpiresAt: require_Schema$1.NullOr(require_Schema$1.String),
|
|
67465
|
-
refreshToken: require_Schema$1.NullOr(require_Schema$1.String),
|
|
67466
67461
|
cmmdUserIdentity: require_Schema$1.NullOr(require_Schema$1.String),
|
|
67467
67462
|
sessionGrant: require_Schema$1.NullOr(require_Schema$1.String)
|
|
67468
67463
|
});
|
|
67469
67464
|
const SubjectCommandAccessTokenRow = require_Schema$1.Struct({ accessToken: require_Schema$1.String });
|
|
67470
|
-
const SubjectJwtRow = require_Schema$1.Struct({
|
|
67471
|
-
accessToken: require_Schema$1.String,
|
|
67472
|
-
jwtToken: require_Schema$1.NullOr(require_Schema$1.String),
|
|
67473
|
-
jwtExpiresAt: require_Schema$1.NullOr(require_Schema$1.String),
|
|
67474
|
-
refreshToken: require_Schema$1.NullOr(require_Schema$1.String),
|
|
67475
|
-
sessionId: require_Schema$1.String
|
|
67476
|
-
});
|
|
67477
|
-
const SubjectRefreshTokenRow = require_Schema$1.Struct({
|
|
67478
|
-
refreshToken: require_Schema$1.String,
|
|
67479
|
-
sessionId: require_Schema$1.String
|
|
67480
|
-
});
|
|
67481
67465
|
const OwnerCommandAuthBundleRow = require_Schema$1.Struct({
|
|
67482
67466
|
accessToken: require_Schema$1.String,
|
|
67483
67467
|
jwtToken: require_Schema$1.NullOr(require_Schema$1.String),
|
|
67484
67468
|
jwtExpiresAt: require_Schema$1.NullOr(require_Schema$1.String),
|
|
67485
|
-
|
|
67469
|
+
sessionGrant: require_Schema$1.NullOr(require_Schema$1.String),
|
|
67486
67470
|
sessionId: require_Schema$1.String,
|
|
67487
67471
|
cmmdUserIdentity: require_Schema$1.String
|
|
67488
67472
|
});
|
|
@@ -67527,8 +67511,6 @@ function decryptNullable(scope, value) {
|
|
|
67527
67511
|
}
|
|
67528
67512
|
const decodeTokenRows = require_Schema$1.decodeUnknownEffect(require_Schema$1.ArraySchema(CommandAccessTokenRow));
|
|
67529
67513
|
const decodeSubjectTokenRows = require_Schema$1.decodeUnknownEffect(require_Schema$1.ArraySchema(SubjectCommandAccessTokenRow));
|
|
67530
|
-
const decodeSubjectJwtRows = require_Schema$1.decodeUnknownEffect(require_Schema$1.ArraySchema(SubjectJwtRow));
|
|
67531
|
-
require_Schema$1.decodeUnknownEffect(require_Schema$1.ArraySchema(SubjectRefreshTokenRow));
|
|
67532
67514
|
const decodeOwnerCommandAuthBundleRows = require_Schema$1.decodeUnknownEffect(require_Schema$1.ArraySchema(OwnerCommandAuthBundleRow));
|
|
67533
67515
|
const decodeOwnerCommandAuthSessionRows = require_Schema$1.decodeUnknownEffect(require_Schema$1.ArraySchema(OwnerCommandAuthSessionRow));
|
|
67534
67516
|
const decodeCloudOwnerSessionRows = require_Schema$1.decodeUnknownEffect(require_Schema$1.ArraySchema(CloudOwnerSessionRow));
|
|
@@ -67554,9 +67536,9 @@ function extractJwtExpiresAt(jwtToken) {
|
|
|
67554
67536
|
* Comparing against `exp` exactly leaves a window where Forge sends a token it
|
|
67555
67537
|
* believes is good and CMMD has already stopped accepting it: the clocks differ by a
|
|
67556
67538
|
* little, the request spends time in flight, and the answer comes back 401 on a
|
|
67557
|
-
* session
|
|
67558
|
-
*
|
|
67559
|
-
*
|
|
67539
|
+
* session whose grant could have minted a good replacement. Two minutes is far
|
|
67540
|
+
* inside the JWT's own lifetime, so this costs one early mint and removes the
|
|
67541
|
+
* race.
|
|
67560
67542
|
*/
|
|
67561
67543
|
const JWT_EXPIRY_SKEW_MS = 2 * 6e4;
|
|
67562
67544
|
/**
|
|
@@ -67582,7 +67564,6 @@ const hydrateCommandAccessTokenStore = require_Schema$1.gen(function* () {
|
|
|
67582
67564
|
access_token AS "accessToken",
|
|
67583
67565
|
jwt_token AS "jwtToken",
|
|
67584
67566
|
jwt_expires_at AS "jwtExpiresAt",
|
|
67585
|
-
refresh_token AS "refreshToken",
|
|
67586
67567
|
cmmd_user_identity AS "cmmdUserIdentity",
|
|
67587
67568
|
session_grant AS "sessionGrant"
|
|
67588
67569
|
FROM command_access_tokens
|
|
@@ -67590,7 +67571,6 @@ const hydrateCommandAccessTokenStore = require_Schema$1.gen(function* () {
|
|
|
67590
67571
|
cache.clear();
|
|
67591
67572
|
jwtCache.clear();
|
|
67592
67573
|
jwtExpiryCache.clear();
|
|
67593
|
-
refreshTokenCache.clear();
|
|
67594
67574
|
identityCache.clear();
|
|
67595
67575
|
sessionGrantCache.clear();
|
|
67596
67576
|
yield* require_Schema$1.try_({
|
|
@@ -67601,8 +67581,6 @@ const hydrateCommandAccessTokenStore = require_Schema$1.gen(function* () {
|
|
|
67601
67581
|
const jwtToken = decryptNullable(JWT_TOKEN_SCOPE, row.jwtToken);
|
|
67602
67582
|
if (jwtToken) jwtCache.set(sessionId, jwtToken);
|
|
67603
67583
|
if (row.jwtExpiresAt) jwtExpiryCache.set(sessionId, row.jwtExpiresAt);
|
|
67604
|
-
const refreshToken = decryptNullable(REFRESH_TOKEN_SCOPE, row.refreshToken);
|
|
67605
|
-
if (refreshToken) refreshTokenCache.set(sessionId, refreshToken);
|
|
67606
67584
|
const identity = decodeCmmdUserIdentity(decryptNullable(IDENTITY_SCOPE, row.cmmdUserIdentity));
|
|
67607
67585
|
if (identity) identityCache.set(sessionId, identity);
|
|
67608
67586
|
const sessionGrant = decryptNullable(SESSION_GRANT_SCOPE, row.sessionGrant);
|
|
@@ -67709,19 +67687,11 @@ function readCommandJwtForSession(sessionId) {
|
|
|
67709
67687
|
return jwtCache.get(sessionId) ?? null;
|
|
67710
67688
|
}
|
|
67711
67689
|
/**
|
|
67712
|
-
* Read the CMMD OAuth refresh token for a session. Sync; hits the in-memory
|
|
67713
|
-
* cache only. Used to rotate a REST JWT after CMMD rejects a locally cached JWT
|
|
67714
|
-
* that still appears unexpired.
|
|
67715
|
-
*/
|
|
67716
|
-
function readCommandRefreshTokenForSession(sessionId) {
|
|
67717
|
-
return refreshTokenCache.get(sessionId) ?? null;
|
|
67718
|
-
}
|
|
67719
|
-
/**
|
|
67720
67690
|
* The durable CMMD session grant for a session, if one was issued.
|
|
67721
67691
|
*
|
|
67722
|
-
*
|
|
67723
|
-
*
|
|
67724
|
-
*
|
|
67692
|
+
* The grant is NON-rotating: minting from it does not invalidate it, so it
|
|
67693
|
+
* stays valid until CMMD expires or revokes it. That is what makes it a safe
|
|
67694
|
+
* long-lived credential: the short-lived JWT it mints can always be replaced.
|
|
67725
67695
|
*/
|
|
67726
67696
|
function readCommandSessionGrantForSession(sessionId) {
|
|
67727
67697
|
return sessionGrantCache.get(sessionId) ?? null;
|
|
@@ -67771,77 +67741,26 @@ const setCmmdUserIdentityForSession = (sessionId, identity) => require_Schema$1.
|
|
|
67771
67741
|
* `jwt_expires_at` so callers can detect when the token has expired.
|
|
67772
67742
|
* Updates only the JWT columns; the PAT row must already exist
|
|
67773
67743
|
* (call `setCommandAccessTokenForSession` first in the OAuth callback).
|
|
67744
|
+
*
|
|
67745
|
+
* The JWT is short-lived by contract (CMMD clamps the mint TTL to one hour);
|
|
67746
|
+
* replacing one is the resolver's mint, so no sibling credential rides along.
|
|
67774
67747
|
*/
|
|
67775
|
-
const setCommandJwtForSession = (sessionId, jwtToken
|
|
67748
|
+
const setCommandJwtForSession = (sessionId, jwtToken) => require_Schema$1.gen(function* () {
|
|
67776
67749
|
const token = jwtToken.trim();
|
|
67777
67750
|
if (token.length === 0) return;
|
|
67778
67751
|
const expiresAt = extractJwtExpiresAt(token);
|
|
67779
|
-
const refreshToken = opts.refreshToken?.trim();
|
|
67780
67752
|
const storedToken = encodeSensitiveValue(JWT_TOKEN_SCOPE, token);
|
|
67781
|
-
|
|
67782
|
-
|
|
67783
|
-
|
|
67784
|
-
|
|
67785
|
-
|
|
67786
|
-
|
|
67787
|
-
|
|
67788
|
-
|
|
67789
|
-
refresh_token = ${storedRefreshToken},
|
|
67790
|
-
updated_at = ${now}
|
|
67791
|
-
WHERE session_id = ${sessionId}
|
|
67792
|
-
`.pipe(require_Schema$1.mapError(toPersistenceSqlError("CommandAccessTokenStore.setJwt:query")));
|
|
67793
|
-
else yield* sql`
|
|
67794
|
-
UPDATE command_access_tokens
|
|
67795
|
-
SET
|
|
67796
|
-
jwt_token = ${storedToken},
|
|
67797
|
-
jwt_expires_at = ${expiresAt},
|
|
67798
|
-
updated_at = ${now}
|
|
67799
|
-
WHERE session_id = ${sessionId}
|
|
67800
|
-
`.pipe(require_Schema$1.mapError(toPersistenceSqlError("CommandAccessTokenStore.setJwt:query")));
|
|
67753
|
+
yield* (yield* require_SqlError.SqlClient)`
|
|
67754
|
+
UPDATE command_access_tokens
|
|
67755
|
+
SET
|
|
67756
|
+
jwt_token = ${storedToken},
|
|
67757
|
+
jwt_expires_at = ${expiresAt},
|
|
67758
|
+
updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
67759
|
+
WHERE session_id = ${sessionId}
|
|
67760
|
+
`.pipe(require_Schema$1.mapError(toPersistenceSqlError("CommandAccessTokenStore.setJwt:query")));
|
|
67801
67761
|
jwtCache.set(sessionId, token);
|
|
67802
67762
|
if (expiresAt) jwtExpiryCache.set(sessionId, expiresAt);
|
|
67803
67763
|
else jwtExpiryCache.delete(sessionId);
|
|
67804
|
-
if (refreshToken) refreshTokenCache.set(sessionId, refreshToken);
|
|
67805
|
-
});
|
|
67806
|
-
/**
|
|
67807
|
-
* Look up the most recent non-expired JWT for an auth subject across active
|
|
67808
|
-
* owner sessions. Used as a continuity fallback when the current session ID
|
|
67809
|
-
* changed (e.g. after re-authentication) but the user's CMMD JWT should still
|
|
67810
|
-
* be usable.
|
|
67811
|
-
*
|
|
67812
|
-
* Also returns the sessionId that owns the JWT so callers can promote it to
|
|
67813
|
-
* the current session, avoiding repeated cross-session lookups.
|
|
67814
|
-
*/
|
|
67815
|
-
const readMostRecentJwtForSubject = (subject) => require_Schema$1.gen(function* () {
|
|
67816
|
-
const row = (yield* decodeSubjectJwtRows(yield* (yield* require_SqlError.SqlClient)`
|
|
67817
|
-
SELECT
|
|
67818
|
-
cat.access_token AS "accessToken",
|
|
67819
|
-
cat.jwt_token AS "jwtToken",
|
|
67820
|
-
cat.jwt_expires_at AS "jwtExpiresAt",
|
|
67821
|
-
cat.refresh_token AS "refreshToken",
|
|
67822
|
-
cat.session_id AS "sessionId"
|
|
67823
|
-
FROM command_access_tokens AS cat
|
|
67824
|
-
JOIN auth_sessions AS s ON s.session_id = cat.session_id
|
|
67825
|
-
WHERE s.subject = ${subject}
|
|
67826
|
-
AND s.role = 'owner'
|
|
67827
|
-
AND s.revoked_at IS NULL
|
|
67828
|
-
AND cat.jwt_token IS NOT NULL
|
|
67829
|
-
AND cat.jwt_expires_at IS NOT NULL
|
|
67830
|
-
AND cat.jwt_expires_at > ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
67831
|
-
ORDER BY s.issued_at DESC
|
|
67832
|
-
LIMIT 1
|
|
67833
|
-
`.pipe(require_Schema$1.mapError(toPersistenceSqlError("CommandAccessTokenStore.readMostRecentJwtBySubject:query")))).pipe(require_Schema$1.mapError(toPersistenceDecodeError("CommandAccessTokenStore.readMostRecentJwtBySubject:decodeRows"))))[0];
|
|
67834
|
-
if (!row?.jwtToken) return null;
|
|
67835
|
-
const jwtToken = decodeSensitiveValue(JWT_TOKEN_SCOPE, row.jwtToken);
|
|
67836
|
-
const refreshToken = decryptNullable(REFRESH_TOKEN_SCOPE, row.refreshToken);
|
|
67837
|
-
const accessToken = decodeSensitiveValue(ACCESS_TOKEN_SCOPE, row.accessToken);
|
|
67838
|
-
return {
|
|
67839
|
-
jwt: jwtToken,
|
|
67840
|
-
sessionId: row.sessionId,
|
|
67841
|
-
expiresAt: row.jwtExpiresAt ?? null,
|
|
67842
|
-
refreshToken,
|
|
67843
|
-
accessToken
|
|
67844
|
-
};
|
|
67845
67764
|
});
|
|
67846
67765
|
/**
|
|
67847
67766
|
* Look up the latest CMMD PAT for an auth subject across active owner sessions.
|
|
@@ -67880,7 +67799,7 @@ const readMostRecentCommandAccessTokenForSubject = (subject) => require_Schema$1
|
|
|
67880
67799
|
* `desktop-bootstrap` session that minted them, and nothing later rewrites that
|
|
67881
67800
|
* subject. So the clause matched no row on any machine, promotion always
|
|
67882
67801
|
* returned null, and a phone whose short-lived JWT had expired could not reach
|
|
67883
|
-
* the
|
|
67802
|
+
* the credentials sitting on its own previous session. It surfaced as "This
|
|
67884
67803
|
* session is no longer accepted. Sign in again." about a day after signing in,
|
|
67885
67804
|
* with nothing in the log, because the ladder never got as far as a call that
|
|
67886
67805
|
* could fail out loud.
|
|
@@ -67941,6 +67860,9 @@ const readMostRecentOwnerCommandAuthSession = require_Schema$1.gen(function* ()
|
|
|
67941
67860
|
* a CMMD user subject. The CMMD token is still account-owned, so the trusted
|
|
67942
67861
|
* local owner may inherit the most recent active owner token with an identity.
|
|
67943
67862
|
* Client pairings must not call this.
|
|
67863
|
+
*
|
|
67864
|
+
* The copy includes the session grant: the promoted session can then mint its
|
|
67865
|
+
* own short-lived JWT, which since forge#3521 is the only refresh story.
|
|
67944
67866
|
*/
|
|
67945
67867
|
const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => require_Schema$1.gen(function* () {
|
|
67946
67868
|
const sql = yield* require_SqlError.SqlClient;
|
|
@@ -67949,7 +67871,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => require_Schema
|
|
|
67949
67871
|
cat.access_token AS "accessToken",
|
|
67950
67872
|
cat.jwt_token AS "jwtToken",
|
|
67951
67873
|
cat.jwt_expires_at AS "jwtExpiresAt",
|
|
67952
|
-
cat.
|
|
67874
|
+
cat.session_grant AS "sessionGrant",
|
|
67953
67875
|
cat.session_id AS "sessionId",
|
|
67954
67876
|
cat.cmmd_user_identity AS "cmmdUserIdentity"
|
|
67955
67877
|
FROM command_access_tokens AS cat
|
|
@@ -67961,7 +67883,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => require_Schema
|
|
|
67961
67883
|
if (!row) return null;
|
|
67962
67884
|
const accessToken = decodeSensitiveValue(ACCESS_TOKEN_SCOPE, row.accessToken);
|
|
67963
67885
|
const jwtToken = decryptNullable(JWT_TOKEN_SCOPE, row.jwtToken);
|
|
67964
|
-
const
|
|
67886
|
+
const sessionGrant = decryptNullable(SESSION_GRANT_SCOPE, row.sessionGrant);
|
|
67965
67887
|
const identity = decodeCmmdUserIdentity(decodeSensitiveValue(IDENTITY_SCOPE, row.cmmdUserIdentity));
|
|
67966
67888
|
if (!identity) return null;
|
|
67967
67889
|
const updateNow = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -67971,7 +67893,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => require_Schema
|
|
|
67971
67893
|
access_token,
|
|
67972
67894
|
jwt_token,
|
|
67973
67895
|
jwt_expires_at,
|
|
67974
|
-
|
|
67896
|
+
session_grant,
|
|
67975
67897
|
cmmd_user_identity,
|
|
67976
67898
|
created_at,
|
|
67977
67899
|
updated_at
|
|
@@ -67981,7 +67903,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => require_Schema
|
|
|
67981
67903
|
${row.accessToken},
|
|
67982
67904
|
${row.jwtToken},
|
|
67983
67905
|
${row.jwtExpiresAt},
|
|
67984
|
-
${row.
|
|
67906
|
+
${row.sessionGrant},
|
|
67985
67907
|
${row.cmmdUserIdentity},
|
|
67986
67908
|
${updateNow},
|
|
67987
67909
|
${updateNow}
|
|
@@ -67990,7 +67912,7 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => require_Schema
|
|
|
67990
67912
|
access_token = excluded.access_token,
|
|
67991
67913
|
jwt_token = excluded.jwt_token,
|
|
67992
67914
|
jwt_expires_at = excluded.jwt_expires_at,
|
|
67993
|
-
|
|
67915
|
+
session_grant = excluded.session_grant,
|
|
67994
67916
|
cmmd_user_identity = excluded.cmmd_user_identity,
|
|
67995
67917
|
updated_at = excluded.updated_at
|
|
67996
67918
|
`.pipe(require_Schema$1.mapError(toPersistenceSqlError("CommandAccessTokenStore.promoteLatestOwnerAuth:upsert")));
|
|
@@ -67999,14 +67921,14 @@ const promoteMostRecentOwnerCommandAuthToSession = (sessionId) => require_Schema
|
|
|
67999
67921
|
else jwtCache.delete(sessionId);
|
|
68000
67922
|
if (row.jwtExpiresAt) jwtExpiryCache.set(sessionId, row.jwtExpiresAt);
|
|
68001
67923
|
else jwtExpiryCache.delete(sessionId);
|
|
68002
|
-
if (
|
|
68003
|
-
else
|
|
67924
|
+
if (sessionGrant) sessionGrantCache.set(sessionId, sessionGrant);
|
|
67925
|
+
else sessionGrantCache.delete(sessionId);
|
|
68004
67926
|
identityCache.set(sessionId, identity);
|
|
68005
67927
|
return {
|
|
68006
67928
|
accessToken,
|
|
68007
67929
|
jwtToken,
|
|
68008
67930
|
jwtExpiresAt: row.jwtExpiresAt ?? null,
|
|
68009
|
-
|
|
67931
|
+
sessionGrant,
|
|
68010
67932
|
sourceSessionId: row.sessionId,
|
|
68011
67933
|
cmmdUserIdentity: identity
|
|
68012
67934
|
};
|
|
@@ -68051,11 +67973,11 @@ const clearCommandAccessTokenForSession = (sessionId) => require_Schema$1.gen(fu
|
|
|
68051
67973
|
`.pipe(require_Schema$1.mapError(toPersistenceSqlError("CommandAccessTokenStore.clear:query")));
|
|
68052
67974
|
forgetCommandAccessTokenCachesForSession(sessionId);
|
|
68053
67975
|
});
|
|
67976
|
+
/** Evict a session's credentials, JWT expiry, identity, and grant from memory without changing storage. */
|
|
68054
67977
|
function forgetCommandAccessTokenCachesForSession(sessionId) {
|
|
68055
67978
|
cache.delete(sessionId);
|
|
68056
67979
|
jwtCache.delete(sessionId);
|
|
68057
67980
|
jwtExpiryCache.delete(sessionId);
|
|
68058
|
-
refreshTokenCache.delete(sessionId);
|
|
68059
67981
|
identityCache.delete(sessionId);
|
|
68060
67982
|
sessionGrantCache.delete(sessionId);
|
|
68061
67983
|
}
|
|
@@ -68087,14 +68009,14 @@ const scheduleExpiredForgeCloudOwnerCredentialMaintenance = require_Schema$1.sle
|
|
|
68087
68009
|
* is dead too, so the promotion sources are cleared with it — otherwise the
|
|
68088
68010
|
* next promotion resurrects the same dead bundle in a loop.
|
|
68089
68011
|
* - **Every other session** owns its credentials outright: each OAuth exchange
|
|
68090
|
-
* mints
|
|
68091
|
-
*
|
|
68012
|
+
* mints its own PAT, JWT and session grant. One session's dead credential
|
|
68013
|
+
* says nothing about a
|
|
68092
68014
|
* sibling browser or device, so the clear stays local to that session.
|
|
68093
68015
|
*
|
|
68094
68016
|
* Clearing every active owner row unconditionally (the pre-CMMD-1871 behaviour)
|
|
68095
68017
|
* turned a single expired tab into a sign-out on every browser and device.
|
|
68096
68018
|
*
|
|
68097
|
-
* Destroys
|
|
68019
|
+
* Destroys durable credentials (the PAT, the grant), so every call is logged with a
|
|
68098
68020
|
* `reason` naming the caller's own decision (never the credential itself) and
|
|
68099
68021
|
* the actual row count deleted, at info level so it shows up without raising
|
|
68100
68022
|
* the server's log level. `reason` is optional only so a caller that has not
|
|
@@ -68135,65 +68057,10 @@ const clearUnrecoverableCmmdAuthForSession = (sessionId, reason) => require_Sche
|
|
|
68135
68057
|
}
|
|
68136
68058
|
});
|
|
68137
68059
|
/**
|
|
68138
|
-
* Retire refresh credentials copied into historical desktop bearer sessions.
|
|
68139
|
-
* Electron owns rotation for native desktop OAuth; Forge Local keeps only the
|
|
68140
|
-
* derived access authority needed by the current bearer session.
|
|
68141
|
-
*
|
|
68142
|
-
* Scoped to the bootstrapping client, not to the whole subject. The phone and
|
|
68143
|
-
* Electron sign in as the same `user:<id>`, so a subject-wide sweep let one
|
|
68144
|
-
* desktop launch null out a phone's refresh token and end its session a day
|
|
68145
|
-
* later. Two clauses keep that from happening:
|
|
68146
|
-
*
|
|
68147
|
-
* - `client_device_type = 'desktop'`, which now excludes phones outright,
|
|
68148
|
-
* because the mobile app declares itself at bootstrap.
|
|
68149
|
-
* - the same recorded `client_user_agent` as the bootstrap doing the
|
|
68150
|
-
* retirement, which is the only thing that separates a phone row written
|
|
68151
|
-
* before that stamp existed from a real Electron row. Both are stamped
|
|
68152
|
-
* `desktop`; neither carries any other mark. An empty or unmatched user agent
|
|
68153
|
-
* retires nothing, which is the direction that keeps a live credential.
|
|
68154
|
-
*
|
|
68155
|
-
* Electron re-bootstraps under a stable user agent, so its own historical rows
|
|
68156
|
-
* still match and still get swept.
|
|
68157
|
-
*/
|
|
68158
|
-
const clearDesktopBearerRefreshTokensForSubject = (input) => require_Schema$1.gen(function* () {
|
|
68159
|
-
const normalizedSubject = input.subject.trim();
|
|
68160
|
-
const normalizedUserAgent = input.clientUserAgent?.trim() ?? "";
|
|
68161
|
-
if (!normalizedSubject || !normalizedUserAgent) return;
|
|
68162
|
-
const rows = yield* (yield* require_SqlError.SqlClient)`
|
|
68163
|
-
UPDATE command_access_tokens
|
|
68164
|
-
SET
|
|
68165
|
-
refresh_token = NULL,
|
|
68166
|
-
updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
68167
|
-
WHERE session_id IN (
|
|
68168
|
-
SELECT session_id
|
|
68169
|
-
FROM auth_sessions
|
|
68170
|
-
WHERE subject = ${normalizedSubject}
|
|
68171
|
-
AND role = 'owner'
|
|
68172
|
-
AND method = 'bearer-session-token'
|
|
68173
|
-
AND client_device_type = 'desktop'
|
|
68174
|
-
AND client_user_agent = ${normalizedUserAgent}
|
|
68175
|
-
)
|
|
68176
|
-
RETURNING session_id AS "sessionId"
|
|
68177
|
-
`.pipe(require_Schema$1.mapError(toPersistenceSqlError("CommandAccessTokenStore.clearDesktopBearerRefresh:query")));
|
|
68178
|
-
for (const row of rows) refreshTokenCache.delete(row.sessionId);
|
|
68179
|
-
});
|
|
68180
|
-
const isDesktopBearerAuthSession = (sessionId) => require_Schema$1.gen(function* () {
|
|
68181
|
-
const rows = yield* (yield* require_SqlError.SqlClient)`
|
|
68182
|
-
SELECT
|
|
68183
|
-
method,
|
|
68184
|
-
client_device_type AS "clientDeviceType"
|
|
68185
|
-
FROM auth_sessions
|
|
68186
|
-
WHERE session_id = ${sessionId}
|
|
68187
|
-
LIMIT 1
|
|
68188
|
-
`.pipe(require_Schema$1.mapError(toPersistenceSqlError("CommandAccessTokenStore.isDesktopBearerSession:query")));
|
|
68189
|
-
return rows[0]?.method === "bearer-session-token" && rows[0].clientDeviceType === "desktop";
|
|
68190
|
-
});
|
|
68191
|
-
/**
|
|
68192
68060
|
* The device type a session's bearer bootstrap declared, for log lines only.
|
|
68193
68061
|
*
|
|
68194
|
-
*
|
|
68195
|
-
*
|
|
68196
|
-
* what made the request, not whether Forge may rotate for it. Never fails:
|
|
68062
|
+
* Returns the raw column: a log line wants to say what made the request.
|
|
68063
|
+
* Never fails:
|
|
68197
68064
|
* an unreadable or missing row logs as `"unknown"` rather than losing the
|
|
68198
68065
|
* decision-point log line it is attached to.
|
|
68199
68066
|
*/
|
|
@@ -68246,6 +68113,7 @@ function artifactError(message) {
|
|
|
68246
68113
|
cause
|
|
68247
68114
|
});
|
|
68248
68115
|
}
|
|
68116
|
+
/** Store credentials under the asserted Cloud owner session and retain its durable mint grant. */
|
|
68249
68117
|
function persistCloudOwnerArtifacts(input) {
|
|
68250
68118
|
return require_Schema$1.gen(function* () {
|
|
68251
68119
|
const sql = yield* require_SqlError.SqlClient;
|
|
@@ -68253,7 +68121,7 @@ function persistCloudOwnerArtifacts(input) {
|
|
|
68253
68121
|
yield* setCommandAccessTokenForSession(input.assertion.sessionId, input.accessToken).pipe(require_Schema$1.mapError(artifactError("Failed to persist CMMD access token.")));
|
|
68254
68122
|
yield* setCmmdUserIdentityForSession(input.assertion.sessionId, input.identity).pipe(require_Schema$1.mapError(artifactError("Failed to persist CMMD user identity.")));
|
|
68255
68123
|
if (input.sessionGrant) yield* setCommandSessionGrantForSession(input.assertion.sessionId, input.sessionGrant, { expiresAt: input.sessionGrantExpiresAt ?? null }).pipe(require_Schema$1.mapError(artifactError("Failed to persist CMMD session grant.")));
|
|
68256
|
-
if (input.jwtToken) yield* setCommandJwtForSession(input.assertion.sessionId, input.jwtToken
|
|
68124
|
+
if (input.jwtToken) yield* setCommandJwtForSession(input.assertion.sessionId, input.jwtToken).pipe(require_Schema$1.mapError(artifactError("Failed to persist CMMD JWT.")));
|
|
68257
68125
|
});
|
|
68258
68126
|
yield* sql.withTransaction(transaction).pipe(require_Schema$1.mapError((cause) => cause instanceof AuthError ? cause : new AuthError({
|
|
68259
68127
|
message: "Failed to persist Forge Cloud owner credentials.",
|
|
@@ -68391,7 +68259,6 @@ const authSessionRenewRouteLayer = add$1("POST", "/api/auth/session/renew", requ
|
|
|
68391
68259
|
accessToken: patToStore,
|
|
68392
68260
|
identity: cmmdIdentity,
|
|
68393
68261
|
jwtToken: credentials.jwtToStore,
|
|
68394
|
-
refreshToken: credentials.refreshToken,
|
|
68395
68262
|
sessionGrant,
|
|
68396
68263
|
sessionGrantExpiresAt: void 0
|
|
68397
68264
|
});
|
|
@@ -68468,7 +68335,6 @@ const PairingCredentialRequestHeaders = require_Schema$1.Struct({
|
|
|
68468
68335
|
const CommandBootstrapInput = require_Schema$1.Struct({
|
|
68469
68336
|
accessToken: require_Schema$1.String,
|
|
68470
68337
|
mcpToken: require_Schema$1.optionalKey(require_Schema$1.String),
|
|
68471
|
-
refreshToken: require_Schema$1.optionalKey(require_Schema$1.String),
|
|
68472
68338
|
userId: require_Schema$1.optionalKey(require_Schema$1.Number),
|
|
68473
68339
|
displayName: require_Schema$1.optionalKey(require_Schema$1.String),
|
|
68474
68340
|
user: require_Schema$1.optionalKey(require_Schema$1.Unknown),
|
|
@@ -68702,6 +68568,7 @@ function validateCommandBootstrapPayload(payload) {
|
|
|
68702
68568
|
};
|
|
68703
68569
|
});
|
|
68704
68570
|
}
|
|
68571
|
+
/** Store the bootstrap PAT, optional JWT, and identity; report persistence failures as auth errors. */
|
|
68705
68572
|
function persistCommandBootstrapSessionArtifacts(input) {
|
|
68706
68573
|
return require_Schema$1.gen(function* () {
|
|
68707
68574
|
const token = input.payload.accessToken.trim();
|
|
@@ -68716,14 +68583,11 @@ function persistCommandBootstrapSessionArtifacts(input) {
|
|
|
68716
68583
|
status: 500,
|
|
68717
68584
|
cause
|
|
68718
68585
|
})));
|
|
68719
|
-
if (isCmmdJwtToken(token)) {
|
|
68720
|
-
|
|
68721
|
-
|
|
68722
|
-
|
|
68723
|
-
|
|
68724
|
-
cause
|
|
68725
|
-
})));
|
|
68726
|
-
}
|
|
68586
|
+
if (isCmmdJwtToken(token)) yield* setCommandJwtForSession(input.sessionId, token).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
68587
|
+
message: "Failed to persist CMMD JWT.",
|
|
68588
|
+
status: 500,
|
|
68589
|
+
cause
|
|
68590
|
+
})));
|
|
68727
68591
|
const cmmdIdentity = input.cmmdIdentity ?? normalizeCmmdUserIdentity(input.payload.user);
|
|
68728
68592
|
if (cmmdIdentity) yield* setCmmdUserIdentityForSession(input.sessionId, cmmdIdentity).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
68729
68593
|
message: "Failed to persist CMMD user identity.",
|
|
@@ -68825,7 +68689,6 @@ function resolveCmmdTokenBundleCredentials(tokenResult) {
|
|
|
68825
68689
|
userId: cmmdIdentity?.id,
|
|
68826
68690
|
accessToken,
|
|
68827
68691
|
mcpToken,
|
|
68828
|
-
refreshToken: readString("refresh_token"),
|
|
68829
68692
|
sessionGrant: readString("session_grant"),
|
|
68830
68693
|
sessionGrantExpiresAt: readString("session_grant_expires_at"),
|
|
68831
68694
|
forgeOwnerAssertion: readString("forge_owner_assertion"),
|
|
@@ -68847,7 +68710,6 @@ function describeCmmdTokenResponseForLog(input) {
|
|
|
68847
68710
|
accessTokenType: input.tokenObj ? typeof input.tokenObj["access_token"] : "n/a",
|
|
68848
68711
|
hasAccessToken: input.accessToken !== void 0,
|
|
68849
68712
|
hasMcpToken: input.mcpToken !== void 0,
|
|
68850
|
-
hasRefreshToken: input.refreshToken !== void 0,
|
|
68851
68713
|
expiresInType: input.tokenObj ? typeof input.tokenObj["expires_in"] : "n/a",
|
|
68852
68714
|
isJwt: input.isJwt,
|
|
68853
68715
|
userKeys: userObj ? Object.keys(userObj) : [],
|
|
@@ -69075,19 +68937,6 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
|
|
|
69075
68937
|
payload,
|
|
69076
68938
|
cmmdIdentity: validated.cmmdIdentity
|
|
69077
68939
|
});
|
|
69078
|
-
if (deviceType === "desktop" && !payload.refreshToken?.trim()) yield* clearDesktopBearerRefreshTokensForSubject({
|
|
69079
|
-
subject: sessionSubject,
|
|
69080
|
-
clientUserAgent: request.headers["user-agent"]
|
|
69081
|
-
}).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
69082
|
-
message: "Failed to retire legacy desktop refresh credentials.",
|
|
69083
|
-
status: 500,
|
|
69084
|
-
cause
|
|
69085
|
-
})), require_Schema$1.tapError(() => require_Schema$1.logWarning("CMMD bearer bootstrap failed.", {
|
|
69086
|
-
sessionId: issued.sessionId,
|
|
69087
|
-
deviceType,
|
|
69088
|
-
outcome: "failed",
|
|
69089
|
-
reason: "legacy-desktop-refresh-retirement-failed"
|
|
69090
|
-
})));
|
|
69091
68940
|
yield* require_Schema$1.logInfo("CMMD bearer bootstrap succeeded.", {
|
|
69092
68941
|
sessionId: issued.sessionId,
|
|
69093
68942
|
deviceType,
|
|
@@ -69185,7 +69034,6 @@ function completeCloudOwnerPasswordLogin(input) {
|
|
|
69185
69034
|
accessToken: patToStore,
|
|
69186
69035
|
identity: cmmdIdentity,
|
|
69187
69036
|
jwtToken: credentials.jwtToStore,
|
|
69188
|
-
refreshToken: credentials.refreshToken,
|
|
69189
69037
|
sessionGrant: credentials.sessionGrant,
|
|
69190
69038
|
sessionGrantExpiresAt: credentials.sessionGrantExpiresAt
|
|
69191
69039
|
});
|
|
@@ -69268,6 +69116,10 @@ const authWebLoginRouteLayer = add$1("GET", "/api/auth/login", require_Schema$1.
|
|
|
69268
69116
|
transientCookie
|
|
69269
69117
|
]]));
|
|
69270
69118
|
}));
|
|
69119
|
+
/**
|
|
69120
|
+
* Persist the local OAuth PAT and identity, failing if either write fails.
|
|
69121
|
+
* Capture and ignore failures while storing the optional mint grant and JWT.
|
|
69122
|
+
*/
|
|
69271
69123
|
function persistLocalOAuthArtifacts(input) {
|
|
69272
69124
|
return require_Schema$1.gen(function* () {
|
|
69273
69125
|
yield* setCommandAccessTokenForSession(input.sessionId, input.accessToken).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
@@ -69284,7 +69136,7 @@ function persistLocalOAuthArtifacts(input) {
|
|
|
69284
69136
|
route: "oauth-callback",
|
|
69285
69137
|
step: "grant-store"
|
|
69286
69138
|
}))), require_Schema$1.ignore({ log: true }));
|
|
69287
|
-
if (input.jwtToken) yield* setCommandJwtForSession(input.sessionId, input.jwtToken
|
|
69139
|
+
if (input.jwtToken) yield* setCommandJwtForSession(input.sessionId, input.jwtToken).pipe(require_Schema$1.tapError((error) => require_Schema$1.sync(() => captureServerError(error, {
|
|
69288
69140
|
route: "oauth-callback",
|
|
69289
69141
|
step: "jwt-store"
|
|
69290
69142
|
}))), require_Schema$1.ignore({ log: true }));
|
|
@@ -69338,13 +69190,12 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", require_Sch
|
|
|
69338
69190
|
cause
|
|
69339
69191
|
})
|
|
69340
69192
|
}), "Failed to exchange authorization code with CMMD.");
|
|
69341
|
-
const { tokenObj, cmmdIdentity, userId, accessToken, mcpToken,
|
|
69193
|
+
const { tokenObj, cmmdIdentity, userId, accessToken, mcpToken, sessionGrant, sessionGrantExpiresAt, forgeOwnerAssertion, isJwt, patToStore, jwtToStore } = yield* resolveCmmdTokenBundleCredentials(tokenResult);
|
|
69342
69194
|
const tokenResponseLogDetails = describeCmmdTokenResponseForLog({
|
|
69343
69195
|
tokenObj,
|
|
69344
69196
|
tokenResult,
|
|
69345
69197
|
accessToken,
|
|
69346
69198
|
mcpToken,
|
|
69347
|
-
refreshToken,
|
|
69348
69199
|
isJwt,
|
|
69349
69200
|
userId,
|
|
69350
69201
|
patToStore,
|
|
@@ -69397,7 +69248,6 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", require_Sch
|
|
|
69397
69248
|
accessToken: patToStore,
|
|
69398
69249
|
identity: cmmdIdentity,
|
|
69399
69250
|
jwtToken: jwtToStore,
|
|
69400
|
-
refreshToken,
|
|
69401
69251
|
sessionGrant,
|
|
69402
69252
|
sessionGrantExpiresAt
|
|
69403
69253
|
};
|
|
@@ -71415,51 +71265,83 @@ function isCmmdUserAccessJwt(token) {
|
|
|
71415
71265
|
}
|
|
71416
71266
|
//#endregion
|
|
71417
71267
|
//#region src/command/cmmdJwt.ts
|
|
71418
|
-
function parseCmmdOAuthRefreshResponse(value) {
|
|
71419
|
-
if (!value || typeof value !== "object") return null;
|
|
71420
|
-
const record = value;
|
|
71421
|
-
const accessToken = record["access_token"];
|
|
71422
|
-
if (typeof accessToken !== "string" || accessToken.trim().length === 0) return null;
|
|
71423
|
-
const refreshToken = record["refresh_token"];
|
|
71424
|
-
return {
|
|
71425
|
-
accessToken: accessToken.trim(),
|
|
71426
|
-
refreshToken: typeof refreshToken === "string" && refreshToken.trim().length > 0 ? refreshToken.trim() : null
|
|
71427
|
-
};
|
|
71428
|
-
}
|
|
71429
71268
|
/**
|
|
71430
|
-
* Read
|
|
71431
|
-
*
|
|
71432
|
-
|
|
71269
|
+
* Read a minted JWT with finite issuance and expiry times and a lifetime of
|
|
71270
|
+
* at most one hour. Reject expired tokens and tokens issued in the future.
|
|
71271
|
+
* This checks the mint contract only; CMMD verifies signatures on use.
|
|
71272
|
+
*/
|
|
71273
|
+
/** Machine-client tokens live at most one hour; anything longer is a CMMD
|
|
71274
|
+
* contract violation that must not silently extend forge sessions. */
|
|
71275
|
+
const MAX_MACHINE_TOKEN_LIFETIME_SECONDS = 3600;
|
|
71276
|
+
/**
|
|
71277
|
+
* The minted access token's lifetime in seconds, read from its JWT `exp` and
|
|
71278
|
+
* `iat` claims without verifying the signature (the signature is checked on
|
|
71279
|
+
* every use at the guard; here the claims only measure the lifetime). `iat`
|
|
71280
|
+
* falls back to the current time when absent. Null when the payload cannot be
|
|
71281
|
+
* read, which is itself a contract violation.
|
|
71282
|
+
*/
|
|
71283
|
+
function readJwtLifetimeSeconds(accessToken) {
|
|
71284
|
+
const claims = decodeJwtPayload(accessToken);
|
|
71285
|
+
const expiresAt = claims?.["exp"];
|
|
71286
|
+
const issuedAtRaw = claims?.["iat"];
|
|
71287
|
+
if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return null;
|
|
71288
|
+
const issuedAt = typeof issuedAtRaw === "number" && Number.isFinite(issuedAtRaw) ? issuedAtRaw : Math.floor(Date.now() / 1e3);
|
|
71289
|
+
const now = Date.now() / 1e3;
|
|
71290
|
+
if (expiresAt <= now || issuedAt > now) return null;
|
|
71291
|
+
return expiresAt - issuedAt;
|
|
71292
|
+
}
|
|
71293
|
+
/** True when the minted token's lifetime honors the one-hour contract. A
|
|
71294
|
+
* refusal is logged here because the reason is mint-contract specific and the
|
|
71295
|
+
* caller only wants the token-or-null answer. */
|
|
71296
|
+
function mintLifetimeWithinContract(accessToken) {
|
|
71297
|
+
const lifetime = readJwtLifetimeSeconds(accessToken);
|
|
71298
|
+
if (lifetime !== null && lifetime <= MAX_MACHINE_TOKEN_LIFETIME_SECONDS) return true;
|
|
71299
|
+
console.warn(`[cmmd-auth] CMMD minted a forge token outside the one-hour contract (lifetime=${lifetime === null ? "unreadable" : `${lifetime}s`}); refusing to persist it`);
|
|
71300
|
+
return false;
|
|
71301
|
+
}
|
|
71433
71302
|
function parseCmmdTokenMintResponse(value) {
|
|
71434
71303
|
if (!value || typeof value !== "object") return null;
|
|
71435
71304
|
const accessToken = value["access_token"];
|
|
71436
71305
|
if (typeof accessToken !== "string" || accessToken.trim().length === 0) return null;
|
|
71437
|
-
|
|
71306
|
+
const trimmed = accessToken.trim();
|
|
71307
|
+
if (!mintLifetimeWithinContract(trimmed)) return null;
|
|
71308
|
+
return { accessToken: trimmed };
|
|
71438
71309
|
}
|
|
71439
71310
|
/**
|
|
71440
|
-
* How long the mint may take before the
|
|
71311
|
+
* How long the mint may take before the caller gives up on it. Minting is one
|
|
71441
71312
|
* grant lookup and one signature on CMMD's side; anything slower is treated
|
|
71442
|
-
* like a failure and the
|
|
71313
|
+
* like a failure and the resolution returns null.
|
|
71443
71314
|
*/
|
|
71444
71315
|
const MINT_TIMEOUT_MS = 1e4;
|
|
71445
71316
|
/**
|
|
71317
|
+
* The OAuth error codes that are evidence the grant itself is dead, and the
|
|
71318
|
+
* only mint refusals allowed to feed the credential-rejection ledger.
|
|
71319
|
+
*
|
|
71320
|
+
* This set is grant-specific on purpose. Per RFC 6749, `invalid_grant` names
|
|
71321
|
+
* the grant. `invalid_client` names the client (CMMD did not accept
|
|
71322
|
+
* `cmmd-forge-web`) and `invalid_request` names the request shape (a missing or
|
|
71323
|
+
* unsupported parameter, for example after a CMMD contract change); both would
|
|
71324
|
+
* fire for every session at once, so a single misconfiguration or
|
|
71325
|
+
* deploy-ordering mistake could delete every stored credential. Those codes
|
|
71326
|
+
* stay logged refusals that resolve null and leave the row in place. If CMMD's
|
|
71327
|
+
* contract ever makes one of them grant-specific, document that here before
|
|
71328
|
+
* widening this set.
|
|
71329
|
+
*/
|
|
71330
|
+
const REJECTED_OAUTH_ERROR_CODES = new Set(["invalid_grant"]);
|
|
71331
|
+
/**
|
|
71446
71332
|
* Mint a short-lived forge-scoped access token from a durable session grant
|
|
71447
|
-
* (forge#3518 expand phase).
|
|
71333
|
+
* (forge#3518 expand; the only lane since the forge#3521 contract phase).
|
|
71448
71334
|
*
|
|
71449
|
-
* The grant is non-rotating and redeemable on demand, so this is the
|
|
71450
|
-
*
|
|
71451
|
-
*
|
|
71452
|
-
*
|
|
71453
|
-
*
|
|
71335
|
+
* The grant is non-rotating and redeemable on demand, so this is the entire
|
|
71336
|
+
* refresh story: call it again when the short-lived token expires. CMMD answers
|
|
71337
|
+
* with `access_token` only (no refresh token, no MCP PAT, nothing durable), so
|
|
71338
|
+
* a session living on this lane never accumulates long-lived credential state
|
|
71339
|
+
* beyond the grant it already holds.
|
|
71454
71340
|
*
|
|
71455
71341
|
* Returns null for every failure because the resolver wants a value, not a
|
|
71456
|
-
* thrown error; the failure modes are logged by status and OAuth error code
|
|
71457
|
-
* the same way `refreshCmmdJwt` logs them. A rejection here is never fed to
|
|
71458
|
-
* the credential-rejection ledger: the ledger exists to prove a session's
|
|
71459
|
-
* *rotation* credential is dead, and the grant dying says nothing about the
|
|
71460
|
-
* refresh token the legacy fallback still owns.
|
|
71342
|
+
* thrown error; the failure modes are logged by status and OAuth error code.
|
|
71461
71343
|
*/
|
|
71462
|
-
function mintCmmdShortLivedToken(grantToken) {
|
|
71344
|
+
function mintCmmdShortLivedToken(grantToken, observer) {
|
|
71463
71345
|
return fetch(new URL("/api/mcp-server/oauth/token", resolveCommandAppUrl()).toString(), {
|
|
71464
71346
|
method: "POST",
|
|
71465
71347
|
headers: { "Content-Type": "application/json" },
|
|
@@ -71472,6 +71354,7 @@ function mintCmmdShortLivedToken(grantToken) {
|
|
|
71472
71354
|
}).then(async (response) => {
|
|
71473
71355
|
if (!response.ok) {
|
|
71474
71356
|
const errorCode = await readOAuthErrorCode(response);
|
|
71357
|
+
if (errorCode !== null && REJECTED_OAUTH_ERROR_CODES.has(errorCode)) observer?.onCredentialRejected?.();
|
|
71475
71358
|
console.warn(`[cmmd-auth] CMMD refused a forge token mint with ${response.status}${errorCode === null ? "" : ` (${errorCode})`}`);
|
|
71476
71359
|
return null;
|
|
71477
71360
|
}
|
|
@@ -71484,103 +71367,149 @@ function mintCmmdShortLivedToken(grantToken) {
|
|
|
71484
71367
|
});
|
|
71485
71368
|
}
|
|
71486
71369
|
/**
|
|
71487
|
-
* The
|
|
71488
|
-
*
|
|
71489
|
-
*
|
|
71370
|
+
* The OAuth `error` code from a refusal, lowercased, or null when there is none.
|
|
71371
|
+
*
|
|
71372
|
+
* Best effort by design: a body that is not JSON, or is already consumed, must
|
|
71373
|
+
* not turn a logged failure into an unlogged one. Null also reads correctly as
|
|
71374
|
+
* "no evidence the grant is dead", which is the safe answer for a caller
|
|
71375
|
+
* deciding whether to keep the session's stored credential.
|
|
71490
71376
|
*/
|
|
71491
|
-
function
|
|
71492
|
-
|
|
71493
|
-
|
|
71494
|
-
|
|
71495
|
-
|
|
71496
|
-
|
|
71497
|
-
}
|
|
71377
|
+
async function readOAuthErrorCode(response) {
|
|
71378
|
+
try {
|
|
71379
|
+
const code = (await response.json())["error"];
|
|
71380
|
+
return typeof code === "string" && code.trim().length > 0 ? code.trim().toLowerCase() : null;
|
|
71381
|
+
} catch {
|
|
71382
|
+
return null;
|
|
71383
|
+
}
|
|
71498
71384
|
}
|
|
71385
|
+
//#endregion
|
|
71386
|
+
//#region src/cmmdConnection/cmmdCredentialRejection.ts
|
|
71499
71387
|
/**
|
|
71500
|
-
*
|
|
71501
|
-
*
|
|
71502
|
-
* The grant is CMMD's designed recovery path for exactly the case a rotating
|
|
71503
|
-
* refresh token cannot cover: the refresh token is single-use, so one lost or
|
|
71504
|
-
* raced rotation strands the session with no way back. The grant is
|
|
71505
|
-
* NON-rotating — the same token redeems repeatedly until CMMD expires or
|
|
71506
|
-
* revokes it — so redemption is safe to retry and cannot race itself.
|
|
71388
|
+
* How long a recorded rejection stays usable.
|
|
71507
71389
|
*
|
|
71508
|
-
*
|
|
71509
|
-
*
|
|
71390
|
+
* Long enough to span the resolver ladder and the response that follows it,
|
|
71391
|
+
* short enough that it cannot outlive the credential it describes.
|
|
71510
71392
|
*/
|
|
71511
|
-
|
|
71512
|
-
|
|
71513
|
-
|
|
71514
|
-
|
|
71515
|
-
|
|
71516
|
-
|
|
71517
|
-
|
|
71518
|
-
|
|
71519
|
-
|
|
71520
|
-
|
|
71521
|
-
|
|
71522
|
-
|
|
71523
|
-
|
|
71524
|
-
|
|
71525
|
-
|
|
71526
|
-
|
|
71393
|
+
const REJECTION_TTL_MS = 6e4;
|
|
71394
|
+
const rejectedAt = /* @__PURE__ */ new Map();
|
|
71395
|
+
/** Record that CMMD refused this session's credential outright. */
|
|
71396
|
+
function noteCmmdCredentialRejected(sessionId) {
|
|
71397
|
+
rejectedAt.set(sessionId, Date.now());
|
|
71398
|
+
}
|
|
71399
|
+
/** Whether CMMD refused this session's credential recently enough to act on. */
|
|
71400
|
+
function hasCmmdCredentialRejection(sessionId) {
|
|
71401
|
+
const observedAt = rejectedAt.get(sessionId);
|
|
71402
|
+
if (observedAt === void 0) return false;
|
|
71403
|
+
if (Date.now() - observedAt <= REJECTION_TTL_MS) return true;
|
|
71404
|
+
rejectedAt.delete(sessionId);
|
|
71405
|
+
return false;
|
|
71406
|
+
}
|
|
71407
|
+
/** Drop a recorded rejection, once it has been acted on or superseded. */
|
|
71408
|
+
function forgetCmmdCredentialRejection(sessionId) {
|
|
71409
|
+
rejectedAt.delete(sessionId);
|
|
71527
71410
|
}
|
|
71528
|
-
const REJECTED_OAUTH_ERROR_CODES = new Set([
|
|
71529
|
-
"invalid_grant",
|
|
71530
|
-
"invalid_client",
|
|
71531
|
-
"invalid_request"
|
|
71532
|
-
]);
|
|
71533
71411
|
/**
|
|
71534
|
-
*
|
|
71412
|
+
* Clear a session's CMMD credentials, but only if CMMD rejected them.
|
|
71535
71413
|
*
|
|
71536
|
-
*
|
|
71537
|
-
*
|
|
71538
|
-
*
|
|
71539
|
-
* phone that had silently stopped accepting its session cost an afternoon
|
|
71540
|
-
* partly because a failed rotation here was indistinguishable from one that
|
|
71541
|
-
* never ran. The token itself is never logged; the status and the OAuth error
|
|
71542
|
-
* code are what identify the failure, and `invalid_grant` in particular means
|
|
71543
|
-
* the token was already spent, which is a different fix from a 500.
|
|
71414
|
+
* This is the only door the 401 path may use. A resolver that returned null
|
|
71415
|
+
* without a recorded rejection is a transient failure and leaves the stored
|
|
71416
|
+
* credential exactly where it is, so the next request can mint from it again.
|
|
71544
71417
|
*/
|
|
71545
|
-
function
|
|
71546
|
-
|
|
71547
|
-
|
|
71548
|
-
|
|
71549
|
-
|
|
71550
|
-
|
|
71551
|
-
|
|
71552
|
-
|
|
71553
|
-
|
|
71554
|
-
|
|
71555
|
-
|
|
71556
|
-
|
|
71557
|
-
|
|
71558
|
-
|
|
71559
|
-
|
|
71560
|
-
|
|
71561
|
-
|
|
71562
|
-
|
|
71563
|
-
|
|
71564
|
-
|
|
71565
|
-
console.warn(`[cmmd-auth] CMMD JWT rotation could not be completed: ${String(cause)}`);
|
|
71566
|
-
return null;
|
|
71418
|
+
const clearCmmdAuthAfterCmmdRejection = (sessionId) => require_Schema$1.gen(function* () {
|
|
71419
|
+
if (!hasCmmdCredentialRejection(sessionId)) return;
|
|
71420
|
+
yield* clearUnrecoverableCmmdAuthForSession(sessionId, "cmmd-rejected-credential");
|
|
71421
|
+
forgetCmmdCredentialRejection(sessionId);
|
|
71422
|
+
});
|
|
71423
|
+
//#endregion
|
|
71424
|
+
//#region src/command/cmmdJwtResolver.ts
|
|
71425
|
+
function usableCmmdUserJwt(token) {
|
|
71426
|
+
return isCmmdUserAccessJwt(token) ? token : null;
|
|
71427
|
+
}
|
|
71428
|
+
/** Log a resolver decision with session context and a fixed reason, excluding credential values. */
|
|
71429
|
+
function logCmmdJwtRung(context, rung, outcome, reason) {
|
|
71430
|
+
return (outcome === "failed" ? require_Schema$1.logWarning : require_Schema$1.logInfo)("CMMD JWT resolver rung.", {
|
|
71431
|
+
sessionId: context.sessionId,
|
|
71432
|
+
subject: context.subject,
|
|
71433
|
+
deviceType: context.deviceType,
|
|
71434
|
+
caller: context.caller,
|
|
71435
|
+
rung,
|
|
71436
|
+
outcome,
|
|
71437
|
+
reason
|
|
71567
71438
|
});
|
|
71568
71439
|
}
|
|
71440
|
+
/** In-flight mints, keyed by session. */
|
|
71441
|
+
const inFlightMints = /* @__PURE__ */ new Map();
|
|
71569
71442
|
/**
|
|
71570
|
-
*
|
|
71443
|
+
* Collapse concurrent mints for one session into a single upstream call. A
|
|
71444
|
+
* mint cannot race itself into `invalid_grant` (the grant is non-rotating),
|
|
71445
|
+
* so this is call-rate hygiene, not correctness: two tabs observing the same
|
|
71446
|
+
* expiry must not both hit CMMD when one mint would do.
|
|
71447
|
+
*/
|
|
71448
|
+
function mintOnceForSession(sessionId, grantToken) {
|
|
71449
|
+
return require_Schema$1.promise(() => {
|
|
71450
|
+
const existing = inFlightMints.get(sessionId);
|
|
71451
|
+
if (existing) return existing;
|
|
71452
|
+
const pending = mintCmmdShortLivedToken(grantToken, { onCredentialRejected: () => noteCmmdCredentialRejected(sessionId) }).catch(() => null).finally(() => {
|
|
71453
|
+
inFlightMints.delete(sessionId);
|
|
71454
|
+
});
|
|
71455
|
+
inFlightMints.set(sessionId, pending);
|
|
71456
|
+
return pending;
|
|
71457
|
+
});
|
|
71458
|
+
}
|
|
71459
|
+
/**
|
|
71460
|
+
* Resolve the CMMD REST JWT for a Forge session.
|
|
71571
71461
|
*
|
|
71572
|
-
*
|
|
71573
|
-
*
|
|
71574
|
-
*
|
|
71575
|
-
*
|
|
71462
|
+
* Forge session auth is long-lived compared with the short-lived CMMD JWT. When
|
|
71463
|
+
* the session JWT expires, mint a fresh short-lived token from the session's
|
|
71464
|
+
* durable, non-rotating grant: CMMD answers with one forge-scoped access token
|
|
71465
|
+
* (TTL at most one hour) and nothing else, and minting is the entire refresh
|
|
71466
|
+
* story (forge#3521 contract phase). There is no rotation to fence, no
|
|
71467
|
+
* single-use token to single-flight, and no sibling credential to borrow.
|
|
71468
|
+
*
|
|
71469
|
+
* Fail-closed: never returns a non-user / malformed Bearer. Callers must map
|
|
71470
|
+
* null to CMMD_AUTH_REQUIRED and must not call CMMD upstream with a bad token.
|
|
71576
71471
|
*/
|
|
71577
|
-
|
|
71578
|
-
|
|
71579
|
-
|
|
71580
|
-
|
|
71581
|
-
|
|
71582
|
-
|
|
71472
|
+
function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false, staleAccessToken = null }) {
|
|
71473
|
+
const cached = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
|
|
71474
|
+
if (cached && !isJwtExpiredForSession(sessionId)) {
|
|
71475
|
+
if (!forceRefresh) return require_Schema$1.succeed(cached);
|
|
71476
|
+
if (staleAccessToken && cached !== staleAccessToken) return require_Schema$1.succeed(cached).pipe(require_Schema$1.tap(() => require_Schema$1.gen(function* () {
|
|
71477
|
+
const deviceType = yield* readClientDeviceTypeForSession(sessionId);
|
|
71478
|
+
yield* logCmmdJwtRung({
|
|
71479
|
+
sessionId,
|
|
71480
|
+
subject: subject?.trim() || null,
|
|
71481
|
+
deviceType,
|
|
71482
|
+
caller: logPrefix
|
|
71483
|
+
}, "cache", "skipped", "forced refresh skipped: cache already holds a token another caller minted");
|
|
71484
|
+
})));
|
|
71583
71485
|
}
|
|
71486
|
+
const normalizedSubject = subject?.trim() || null;
|
|
71487
|
+
if (!normalizedSubject && !forceRefresh && readCommandSessionGrantForSession(sessionId) === null) return require_Schema$1.succeed(null);
|
|
71488
|
+
return require_Schema$1.gen(function* () {
|
|
71489
|
+
const context = {
|
|
71490
|
+
sessionId,
|
|
71491
|
+
subject: normalizedSubject,
|
|
71492
|
+
deviceType: yield* readClientDeviceTypeForSession(sessionId),
|
|
71493
|
+
caller: logPrefix
|
|
71494
|
+
};
|
|
71495
|
+
const grantToken = readCommandSessionGrantForSession(sessionId);
|
|
71496
|
+
if (!grantToken) {
|
|
71497
|
+
yield* logCmmdJwtRung(context, "mint", "skipped", "no session grant stored for this session");
|
|
71498
|
+
return null;
|
|
71499
|
+
}
|
|
71500
|
+
yield* logCmmdJwtRung(context, "mint", "attempted", "minting a short-lived forge-scoped token from this session's grant");
|
|
71501
|
+
yield* require_Schema$1.update(require_Schema$1.withAttributes(cmmdTokenMintTotal, metricAttributes({ outcome: "attempted" })), 1);
|
|
71502
|
+
const accessToken = usableCmmdUserJwt((yield* mintOnceForSession(sessionId, grantToken))?.accessToken);
|
|
71503
|
+
if (!accessToken) {
|
|
71504
|
+
yield* require_Schema$1.update(require_Schema$1.withAttributes(cmmdTokenMintTotal, metricAttributes({ outcome: "failed" })), 1);
|
|
71505
|
+
yield* logCmmdJwtRung(context, "mint", "failed", "CMMD token mint did not return a usable user access JWT");
|
|
71506
|
+
return null;
|
|
71507
|
+
}
|
|
71508
|
+
yield* setCommandJwtForSession(sessionId, accessToken).pipe(require_Schema$1.ignore({ log: true }));
|
|
71509
|
+
yield* require_Schema$1.update(require_Schema$1.withAttributes(cmmdTokenMintTotal, metricAttributes({ outcome: "succeeded" })), 1);
|
|
71510
|
+
yield* logCmmdJwtRung(context, "mint", "succeeded", "CMMD minted a short-lived forge-scoped token");
|
|
71511
|
+
return accessToken;
|
|
71512
|
+
});
|
|
71584
71513
|
}
|
|
71585
71514
|
//#endregion
|
|
71586
71515
|
//#region ../../packages/shared/src/cmmdSessionPop.ts
|
|
@@ -71698,10 +71627,6 @@ function resolveCmmdSessionPopSigner() {
|
|
|
71698
71627
|
cachedSigner ??= createServerSigner();
|
|
71699
71628
|
return cachedSigner;
|
|
71700
71629
|
}
|
|
71701
|
-
/** The `pop_public_key` value to present at issuance, or null while disabled. */
|
|
71702
|
-
function cmmdSessionPopPublicKey() {
|
|
71703
|
-
return resolveCmmdSessionPopSigner()?.publicKeyPem ?? null;
|
|
71704
|
-
}
|
|
71705
71630
|
/**
|
|
71706
71631
|
* The three proof headers for one CMMD-origin request, or no headers while
|
|
71707
71632
|
* disabled. Each call carries a fresh nonce; never reuse the result across
|
|
@@ -71717,407 +71642,6 @@ async function cmmdSessionPopRequestHeaders(input) {
|
|
|
71717
71642
|
});
|
|
71718
71643
|
}
|
|
71719
71644
|
//#endregion
|
|
71720
|
-
//#region src/command/cmmdTokenMintLane.ts
|
|
71721
|
-
/**
|
|
71722
|
-
* The CMMD token-mint lane (forge#3518, expand phase).
|
|
71723
|
-
*
|
|
71724
|
-
* A new FIRST rung beside the legacy credential ladder: instead of rotating
|
|
71725
|
-
* CMMD's single-use refresh tokens, the resolver mints a short-lived
|
|
71726
|
-
* forge-scoped access token from the session's durable, non-rotating session
|
|
71727
|
-
* grant (`grant_type: forge_token_mint` on CMMD's token endpoint). The mint
|
|
71728
|
-
* spends nothing that cannot be spent again, so it cannot race itself into
|
|
71729
|
-
* `invalid_grant` the way a rotation can.
|
|
71730
|
-
*
|
|
71731
|
-
* Behind a config flag that is OFF unless explicitly enabled: production
|
|
71732
|
-
* keeps the legacy ladder as the only lane until this is switched on
|
|
71733
|
-
* deliberately. When enabled, the lane is tried before the legacy ladder;
|
|
71734
|
-
* every legacy rung (rotation fence, self-refresh, grant redemption,
|
|
71735
|
-
* bootstrap promotion, subject fallback) still exists below it unchanged and
|
|
71736
|
-
* still guards itself with the rejection ledger.
|
|
71737
|
-
*/
|
|
71738
|
-
/** Env var that enables the mint lane. Unset and anything but an affirmative value is off. */
|
|
71739
|
-
const CMMD_TOKEN_MINT_LANE_ENV = "FORGE_CMMD_TOKEN_MINT_LANE";
|
|
71740
|
-
const AFFIRMATIVE_VALUES = new Set([
|
|
71741
|
-
"1",
|
|
71742
|
-
"on",
|
|
71743
|
-
"true",
|
|
71744
|
-
"yes"
|
|
71745
|
-
]);
|
|
71746
|
-
/**
|
|
71747
|
-
* Whether the mint lane may run. Default off everywhere: a missing env var is
|
|
71748
|
-
* the production posture, and an opt-in flag must not half-default to on in
|
|
71749
|
-
* any environment a misconfigured deploy might land in.
|
|
71750
|
-
*/
|
|
71751
|
-
function isCmmdTokenMintLaneEnabled(env = process.env) {
|
|
71752
|
-
const raw = env[CMMD_TOKEN_MINT_LANE_ENV]?.trim().toLowerCase();
|
|
71753
|
-
return raw !== void 0 && AFFIRMATIVE_VALUES.has(raw);
|
|
71754
|
-
}
|
|
71755
|
-
//#endregion
|
|
71756
|
-
//#region src/command/cmmdRotationFence.ts
|
|
71757
|
-
/**
|
|
71758
|
-
* Log and count a fence application, without changing the verdict it reports.
|
|
71759
|
-
*
|
|
71760
|
-
* The two facts named in the log line are exactly the ones the predicate
|
|
71761
|
-
* above the caller already computed — whether this session ever deposited a
|
|
71762
|
-
* refresh token of its own, and (only when that was false) whether the
|
|
71763
|
-
* session row reads as a desktop bearer session. Neither fact nor the log
|
|
71764
|
-
* line changes what the caller returns.
|
|
71765
|
-
*/
|
|
71766
|
-
function logRotationFenceApplied(input) {
|
|
71767
|
-
return require_Schema$1.logInfo("CMMD rotation fence applied: session may not rotate its own credential.", {
|
|
71768
|
-
sessionId: input.sessionId,
|
|
71769
|
-
hasStoredRefreshToken: input.hasStoredRefreshToken,
|
|
71770
|
-
isDesktopBearerSession: input.isDesktopBearerSession
|
|
71771
|
-
}).pipe(require_Schema$1.andThen(() => require_Schema$1.update(require_Schema$1.withAttributes(cmmdRotationFenceAppliedTotal, metricAttributes({})), 1)));
|
|
71772
|
-
}
|
|
71773
|
-
/**
|
|
71774
|
-
* True when Forge must not rotate this session's CMMD credentials.
|
|
71775
|
-
*
|
|
71776
|
-
* Fails closed: if the session row cannot be read, assume the fence applies.
|
|
71777
|
-
* Spending a token Forge had no right to spend is worse than one extra
|
|
71778
|
-
* reconnect.
|
|
71779
|
-
*/
|
|
71780
|
-
function isRotationFencedSession(sessionId) {
|
|
71781
|
-
const hasStoredRefreshToken = readCommandRefreshTokenForSession(sessionId) !== null;
|
|
71782
|
-
if (hasStoredRefreshToken) return require_Schema$1.succeed(false);
|
|
71783
|
-
return isDesktopBearerAuthSession(sessionId).pipe(require_Schema$1.match({
|
|
71784
|
-
onFailure: () => ({
|
|
71785
|
-
fenced: true,
|
|
71786
|
-
isDesktopBearerSession: null
|
|
71787
|
-
}),
|
|
71788
|
-
onSuccess: (isDesktopBearerSession) => ({
|
|
71789
|
-
fenced: isDesktopBearerSession,
|
|
71790
|
-
isDesktopBearerSession
|
|
71791
|
-
})
|
|
71792
|
-
}), require_Schema$1.tap(({ fenced, isDesktopBearerSession }) => fenced ? logRotationFenceApplied({
|
|
71793
|
-
sessionId,
|
|
71794
|
-
hasStoredRefreshToken,
|
|
71795
|
-
isDesktopBearerSession
|
|
71796
|
-
}) : require_Schema$1.void_), require_Schema$1.map(({ fenced }) => fenced));
|
|
71797
|
-
}
|
|
71798
|
-
//#endregion
|
|
71799
|
-
//#region src/cmmdConnection/cmmdCredentialRejection.ts
|
|
71800
|
-
/**
|
|
71801
|
-
* How long a recorded rejection stays usable.
|
|
71802
|
-
*
|
|
71803
|
-
* Long enough to span the resolver ladder and the response that follows it,
|
|
71804
|
-
* short enough that it cannot outlive the credential it describes.
|
|
71805
|
-
*/
|
|
71806
|
-
const REJECTION_TTL_MS = 6e4;
|
|
71807
|
-
const rejectedAt = /* @__PURE__ */ new Map();
|
|
71808
|
-
/** Record that CMMD refused this session's credential outright. */
|
|
71809
|
-
function noteCmmdCredentialRejected(sessionId) {
|
|
71810
|
-
rejectedAt.set(sessionId, Date.now());
|
|
71811
|
-
}
|
|
71812
|
-
/** Whether CMMD refused this session's credential recently enough to act on. */
|
|
71813
|
-
function hasCmmdCredentialRejection(sessionId) {
|
|
71814
|
-
const observedAt = rejectedAt.get(sessionId);
|
|
71815
|
-
if (observedAt === void 0) return false;
|
|
71816
|
-
if (Date.now() - observedAt <= REJECTION_TTL_MS) return true;
|
|
71817
|
-
rejectedAt.delete(sessionId);
|
|
71818
|
-
return false;
|
|
71819
|
-
}
|
|
71820
|
-
/** Drop a recorded rejection, once it has been acted on or superseded. */
|
|
71821
|
-
function forgetCmmdCredentialRejection(sessionId) {
|
|
71822
|
-
rejectedAt.delete(sessionId);
|
|
71823
|
-
}
|
|
71824
|
-
/**
|
|
71825
|
-
* Clear a session's CMMD credentials, but only if CMMD rejected them.
|
|
71826
|
-
*
|
|
71827
|
-
* This is the only door the 401 path may use. A resolver that returned null
|
|
71828
|
-
* because it was not allowed to try is a policy refusal and leaves the stored
|
|
71829
|
-
* credential exactly where it is, so the next request can rotate it.
|
|
71830
|
-
*/
|
|
71831
|
-
const clearCmmdAuthAfterCmmdRejection = (sessionId) => require_Schema$1.gen(function* () {
|
|
71832
|
-
if (!hasCmmdCredentialRejection(sessionId)) return;
|
|
71833
|
-
yield* clearUnrecoverableCmmdAuthForSession(sessionId, "cmmd-rejected-credential");
|
|
71834
|
-
forgetCmmdCredentialRejection(sessionId);
|
|
71835
|
-
});
|
|
71836
|
-
//#endregion
|
|
71837
|
-
//#region src/command/cmmdJwtResolver.ts
|
|
71838
|
-
function usableCmmdUserJwt(token) {
|
|
71839
|
-
return isCmmdUserAccessJwt(token) ? token : null;
|
|
71840
|
-
}
|
|
71841
|
-
function storeResolvedJwt(sessionId, accessToken, refreshToken) {
|
|
71842
|
-
return setCommandJwtForSession(sessionId, accessToken, refreshToken ? { refreshToken } : {}).pipe(require_Schema$1.ignore({ log: true }));
|
|
71843
|
-
}
|
|
71844
|
-
function logCmmdJwtRung(context, rung, outcome, reason, lane = "legacy") {
|
|
71845
|
-
return (outcome === "failed" ? require_Schema$1.logWarning : require_Schema$1.logInfo)("CMMD JWT resolver rung.", {
|
|
71846
|
-
sessionId: context.sessionId,
|
|
71847
|
-
subject: context.subject,
|
|
71848
|
-
deviceType: context.deviceType,
|
|
71849
|
-
caller: context.caller,
|
|
71850
|
-
lane,
|
|
71851
|
-
rung,
|
|
71852
|
-
outcome,
|
|
71853
|
-
reason
|
|
71854
|
-
});
|
|
71855
|
-
}
|
|
71856
|
-
function recordCmmdCredentialRefreshMetric(rung, outcome) {
|
|
71857
|
-
return require_Schema$1.update(require_Schema$1.withAttributes(cmmdCredentialRefreshTotal, metricAttributes({
|
|
71858
|
-
rung,
|
|
71859
|
-
outcome
|
|
71860
|
-
})), 1);
|
|
71861
|
-
}
|
|
71862
|
-
const inFlightRefreshes = /* @__PURE__ */ new Map();
|
|
71863
|
-
/** In-flight mints, keyed by session, same shape as `inFlightRefreshes`. */
|
|
71864
|
-
const inFlightMints = /* @__PURE__ */ new Map();
|
|
71865
|
-
/**
|
|
71866
|
-
* Collapse concurrent mints for one session into a single upstream call. A
|
|
71867
|
-
* mint cannot race itself into `invalid_grant` (the grant is non-rotating),
|
|
71868
|
-
* so this is call-rate hygiene, not correctness: two tabs observing the same
|
|
71869
|
-
* expiry must not both hit CMMD when one mint would do.
|
|
71870
|
-
*/
|
|
71871
|
-
function mintOnceForSession(sessionId, grantToken) {
|
|
71872
|
-
return require_Schema$1.promise(() => {
|
|
71873
|
-
const existing = inFlightMints.get(sessionId);
|
|
71874
|
-
if (existing) return existing;
|
|
71875
|
-
const pending = mintCmmdShortLivedToken(grantToken).catch(() => null).finally(() => {
|
|
71876
|
-
inFlightMints.delete(sessionId);
|
|
71877
|
-
});
|
|
71878
|
-
inFlightMints.set(sessionId, pending);
|
|
71879
|
-
return pending;
|
|
71880
|
-
});
|
|
71881
|
-
}
|
|
71882
|
-
function refreshOnceForSession(sessionId, refreshToken) {
|
|
71883
|
-
return require_Schema$1.promise(() => {
|
|
71884
|
-
const existing = inFlightRefreshes.get(sessionId);
|
|
71885
|
-
if (existing) return existing;
|
|
71886
|
-
const pending = refreshCmmdJwt(refreshToken, { onCredentialRejected: () => noteCmmdCredentialRejected(sessionId) }).catch(() => null).finally(() => {
|
|
71887
|
-
inFlightRefreshes.delete(sessionId);
|
|
71888
|
-
});
|
|
71889
|
-
inFlightRefreshes.set(sessionId, pending);
|
|
71890
|
-
return pending;
|
|
71891
|
-
});
|
|
71892
|
-
}
|
|
71893
|
-
/**
|
|
71894
|
-
* Rotate this session's **own** CMMD refresh token.
|
|
71895
|
-
*
|
|
71896
|
-
* CMMD rotates refresh tokens single-use — the presented hash is revoked inside
|
|
71897
|
-
* the rotation transaction (CMMD `server/jwt-auth.ts`). A session must
|
|
71898
|
-
* therefore only ever present the refresh token it was issued: rotating a
|
|
71899
|
-
* sibling session's token silently invalidates that sibling's stored copy and
|
|
71900
|
-
* signs it out.
|
|
71901
|
-
*/
|
|
71902
|
-
function trySelfRefresh(sessionId, context) {
|
|
71903
|
-
const currentRefreshToken = readCommandRefreshTokenForSession(sessionId);
|
|
71904
|
-
if (!currentRefreshToken) return logCmmdJwtRung(context, "self-refresh", "skipped", "no refresh token stored for this session").pipe(require_Schema$1.as(null));
|
|
71905
|
-
return require_Schema$1.gen(function* () {
|
|
71906
|
-
yield* logCmmdJwtRung(context, "self-refresh", "attempted", "rotating this session's own stored refresh token");
|
|
71907
|
-
yield* recordCmmdCredentialRefreshMetric("self-refresh", "attempted");
|
|
71908
|
-
const refreshed = yield* refreshOnceForSession(sessionId, currentRefreshToken);
|
|
71909
|
-
const accessToken = usableCmmdUserJwt(refreshed?.accessToken);
|
|
71910
|
-
if (!accessToken) {
|
|
71911
|
-
yield* logCmmdJwtRung(context, "self-refresh", "failed", "CMMD refresh did not return a usable user access JWT");
|
|
71912
|
-
return null;
|
|
71913
|
-
}
|
|
71914
|
-
yield* storeResolvedJwt(sessionId, accessToken, refreshed?.refreshToken);
|
|
71915
|
-
yield* recordCmmdCredentialRefreshMetric("self-refresh", "succeeded");
|
|
71916
|
-
yield* logCmmdJwtRung(context, "self-refresh", "succeeded", "CMMD rotated the stored refresh token");
|
|
71917
|
-
return accessToken;
|
|
71918
|
-
});
|
|
71919
|
-
}
|
|
71920
|
-
/**
|
|
71921
|
-
* The mint lane's rung (forge#3518, expand phase): mint a short-lived
|
|
71922
|
-
* forge-scoped access token from this session's durable session grant.
|
|
71923
|
-
*
|
|
71924
|
-
* Tried FIRST, before the rotation fence and the whole legacy ladder, when
|
|
71925
|
-
* `FORGE_CMMD_TOKEN_MINT_LANE` is on. Minting spends nothing that cannot be
|
|
71926
|
-
* spent again (the grant is non-rotating), so unlike the legacy rungs it is
|
|
71927
|
-
* safe for a session the rotation fence fences off, and it never feeds the
|
|
71928
|
-
* credential-rejection ledger: this rung failing says nothing about whether
|
|
71929
|
-
* the legacy rungs below it can still succeed.
|
|
71930
|
-
*
|
|
71931
|
-
* Only the JWT is persisted (`setCommandJwtForSession`, no refresh token):
|
|
71932
|
-
* the short lifetime is the expiry story, and re-minting is the refresh
|
|
71933
|
-
* story. A session that lives entirely on this lane accumulates no new
|
|
71934
|
-
* long-lived credential state beyond the grant it already had.
|
|
71935
|
-
*/
|
|
71936
|
-
function tryMintShortLivedToken(sessionId, context) {
|
|
71937
|
-
const grantToken = readCommandSessionGrantForSession(sessionId);
|
|
71938
|
-
if (!grantToken) return logCmmdJwtRung(context, "mint", "skipped", "no session grant stored for this session", "mint").pipe(require_Schema$1.as(null));
|
|
71939
|
-
return require_Schema$1.gen(function* () {
|
|
71940
|
-
yield* logCmmdJwtRung(context, "mint", "attempted", "minting a short-lived forge-scoped token from this session's grant", "mint");
|
|
71941
|
-
yield* require_Schema$1.update(require_Schema$1.withAttributes(cmmdTokenMintTotal, metricAttributes({ outcome: "attempted" })), 1);
|
|
71942
|
-
const accessToken = usableCmmdUserJwt((yield* mintOnceForSession(sessionId, grantToken))?.accessToken);
|
|
71943
|
-
if (!accessToken) {
|
|
71944
|
-
yield* logCmmdJwtRung(context, "mint", "failed", "CMMD token mint did not return a usable user access JWT", "mint");
|
|
71945
|
-
return null;
|
|
71946
|
-
}
|
|
71947
|
-
yield* setCommandJwtForSession(sessionId, accessToken, {}).pipe(require_Schema$1.ignore({ log: true }));
|
|
71948
|
-
yield* require_Schema$1.update(require_Schema$1.withAttributes(cmmdTokenMintTotal, metricAttributes({ outcome: "succeeded" })), 1);
|
|
71949
|
-
yield* logCmmdJwtRung(context, "mint", "succeeded", "CMMD minted a short-lived forge-scoped token", "mint");
|
|
71950
|
-
return accessToken;
|
|
71951
|
-
});
|
|
71952
|
-
}
|
|
71953
|
-
/**
|
|
71954
|
-
* Last-resort recovery: redeem this session's durable CMMD session grant.
|
|
71955
|
-
*
|
|
71956
|
-
* A refresh token is single-use, so a session that lost a rotation — raced, or
|
|
71957
|
-
* stranded by a redeploy mid-flight — has no way back and the user is bounced
|
|
71958
|
-
* to /login. CMMD mints a long-lived, non-rotating grant at login for exactly
|
|
71959
|
-
* this, and redemption returns a fresh JWT, refresh token AND MCP PAT.
|
|
71960
|
-
*
|
|
71961
|
-
* Only reached after the session's own refresh has already failed, so this
|
|
71962
|
-
* never competes with a live rotation. The grant is not cleared on failure:
|
|
71963
|
-
* CMMD owns its lifetime, and a transient network failure must not throw away
|
|
71964
|
-
* the one credential that can still recover the session.
|
|
71965
|
-
*/
|
|
71966
|
-
function tryGrantRedemption(sessionId, context) {
|
|
71967
|
-
const grantToken = readCommandSessionGrantForSession(sessionId);
|
|
71968
|
-
if (!grantToken) return logCmmdJwtRung(context, "grant-redemption", "skipped", "no session grant stored for this session").pipe(require_Schema$1.as(null));
|
|
71969
|
-
return require_Schema$1.gen(function* () {
|
|
71970
|
-
yield* logCmmdJwtRung(context, "grant-redemption", "attempted", "redeeming this session's durable CMMD session grant");
|
|
71971
|
-
yield* recordCmmdCredentialRefreshMetric("grant-redemption", "attempted");
|
|
71972
|
-
const redeemed = yield* require_Schema$1.promise(() => redeemCmmdSessionGrant(grantToken, cmmdSessionPopPublicKey())).pipe(require_Schema$1.orElseSucceed(() => null));
|
|
71973
|
-
const accessToken = usableCmmdUserJwt(redeemed?.accessToken);
|
|
71974
|
-
if (!accessToken) {
|
|
71975
|
-
yield* logCmmdJwtRung(context, "grant-redemption", "failed", "session grant redemption did not return a usable user access JWT");
|
|
71976
|
-
return null;
|
|
71977
|
-
}
|
|
71978
|
-
if (redeemed?.mcpToken) yield* setCommandAccessTokenForSession(sessionId, redeemed.mcpToken).pipe(require_Schema$1.ignore({ log: true }));
|
|
71979
|
-
yield* storeResolvedJwt(sessionId, accessToken, redeemed?.refreshToken);
|
|
71980
|
-
yield* recordCmmdCredentialRefreshMetric("grant-redemption", "succeeded");
|
|
71981
|
-
yield* logCmmdJwtRung(context, "grant-redemption", "succeeded", "session grant redeemed");
|
|
71982
|
-
return accessToken;
|
|
71983
|
-
});
|
|
71984
|
-
}
|
|
71985
|
-
/**
|
|
71986
|
-
* Recover using only credentials this session owns, strongest first.
|
|
71987
|
-
*
|
|
71988
|
-
* Rotating its own refresh token is preferred; the durable grant is the
|
|
71989
|
-
* fallback for when that rotation is already gone. Neither touches another
|
|
71990
|
-
* session's credentials, so this ladder is safe to run from any path.
|
|
71991
|
-
*/
|
|
71992
|
-
function tryOwnCredentialRecovery(sessionId, context) {
|
|
71993
|
-
return require_Schema$1.gen(function* () {
|
|
71994
|
-
const refreshed = yield* trySelfRefresh(sessionId, context);
|
|
71995
|
-
if (refreshed) return refreshed;
|
|
71996
|
-
return yield* tryGrantRedemption(sessionId, context);
|
|
71997
|
-
});
|
|
71998
|
-
}
|
|
71999
|
-
function tryPromoteBootstrap(sessionId, subject, forceRefresh, context) {
|
|
72000
|
-
if (subject !== "owner-bootstrap" && subject !== "desktop-bootstrap") return logCmmdJwtRung(context, "promote-bootstrap", "skipped", "subject is not a bootstrap subject").pipe(require_Schema$1.as(null));
|
|
72001
|
-
return require_Schema$1.gen(function* () {
|
|
72002
|
-
yield* logCmmdJwtRung(context, "promote-bootstrap", "attempted", "promoting the most recent owner CMMD auth to this bootstrap session");
|
|
72003
|
-
yield* promoteMostRecentOwnerCommandAuthToSession(sessionId).pipe(require_Schema$1.catch_((error) => require_Schema$1.logWarning("Failed to promote owner CMMD auth for REST JWT resolution.", {
|
|
72004
|
-
cause: error,
|
|
72005
|
-
sessionId,
|
|
72006
|
-
subject
|
|
72007
|
-
})));
|
|
72008
|
-
const promotedJwt = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
|
|
72009
|
-
if (!forceRefresh && promotedJwt && !isJwtExpiredForSession(sessionId)) {
|
|
72010
|
-
yield* logCmmdJwtRung(context, "promote-bootstrap", "succeeded", "promoted owner JWT is usable");
|
|
72011
|
-
return promotedJwt;
|
|
72012
|
-
}
|
|
72013
|
-
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");
|
|
72014
|
-
return null;
|
|
72015
|
-
});
|
|
72016
|
-
}
|
|
72017
|
-
/**
|
|
72018
|
-
* Borrow a sibling session's still-valid CMMD access JWT.
|
|
72019
|
-
*
|
|
72020
|
-
* `readMostRecentJwtForSubject` only returns a JWT that has not expired, so
|
|
72021
|
-
* this is a cheap read-only recovery that avoids an upstream rotation. The
|
|
72022
|
-
* sibling's *refresh* token is deliberately NOT copied: it is single-use and
|
|
72023
|
-
* must stay private to the session it was issued to (see `trySelfRefresh`).
|
|
72024
|
-
* Copying it is what let one expired tab sign the user out everywhere.
|
|
72025
|
-
*/
|
|
72026
|
-
function trySubjectFallback(sessionId, subject, context) {
|
|
72027
|
-
return require_Schema$1.gen(function* () {
|
|
72028
|
-
yield* logCmmdJwtRung(context, "subject-fallback", "attempted", "looking for a sibling session's still-valid JWT for this subject");
|
|
72029
|
-
const fallbackJwt = usableCmmdUserJwt((yield* readMostRecentJwtForSubject(subject).pipe(require_Schema$1.orElseSucceed(() => null)))?.jwt);
|
|
72030
|
-
if (!fallbackJwt) {
|
|
72031
|
-
yield* logCmmdJwtRung(context, "subject-fallback", "failed", "no sibling session holds a still-valid JWT for this subject");
|
|
72032
|
-
return null;
|
|
72033
|
-
}
|
|
72034
|
-
yield* storeResolvedJwt(sessionId, fallbackJwt, null);
|
|
72035
|
-
yield* logCmmdJwtRung(context, "subject-fallback", "succeeded", "borrowed a sibling session's still-valid JWT");
|
|
72036
|
-
return fallbackJwt;
|
|
72037
|
-
});
|
|
72038
|
-
}
|
|
72039
|
-
/**
|
|
72040
|
-
* The rungs below the rotation fence: forced own-credential recovery,
|
|
72041
|
-
* bootstrap promotion, subject fallback, then own-credential recovery again
|
|
72042
|
-
* as the last resort. Split out of `resolveCmmdJwtForSession` so each half of
|
|
72043
|
-
* the ladder stays independently readable (and independently testable) —
|
|
72044
|
-
* the fence check above is the one decision the split does not touch.
|
|
72045
|
-
*/
|
|
72046
|
-
function runRecoveryLadder(sessionId, normalizedSubject, forceRefresh, context) {
|
|
72047
|
-
return require_Schema$1.gen(function* () {
|
|
72048
|
-
if (forceRefresh) {
|
|
72049
|
-
const forced = yield* tryOwnCredentialRecovery(sessionId, context);
|
|
72050
|
-
if (forced) return forced;
|
|
72051
|
-
}
|
|
72052
|
-
if (normalizedSubject) {
|
|
72053
|
-
const promoted = yield* tryPromoteBootstrap(sessionId, normalizedSubject, forceRefresh, context);
|
|
72054
|
-
if (promoted) return promoted;
|
|
72055
|
-
}
|
|
72056
|
-
if (!normalizedSubject) {
|
|
72057
|
-
yield* logCmmdJwtRung(context, "cache", "failed", "no subject to resolve and forced recovery already exhausted");
|
|
72058
|
-
return null;
|
|
72059
|
-
}
|
|
72060
|
-
if (!forceRefresh) {
|
|
72061
|
-
const fallback = yield* trySubjectFallback(sessionId, normalizedSubject, context);
|
|
72062
|
-
if (fallback) return fallback;
|
|
72063
|
-
}
|
|
72064
|
-
const recovered = yield* tryOwnCredentialRecovery(sessionId, context);
|
|
72065
|
-
if (!recovered) yield* logCmmdJwtRung(context, "cache", "failed", "recovery ladder exhausted: no rung produced a usable JWT");
|
|
72066
|
-
return recovered;
|
|
72067
|
-
});
|
|
72068
|
-
}
|
|
72069
|
-
/**
|
|
72070
|
-
* Resolve the CMMD REST JWT for a Forge session.
|
|
72071
|
-
*
|
|
72072
|
-
* Forge session auth is long-lived compared with the short-lived CMMD JWT.
|
|
72073
|
-
* When the session JWT expires, recover by minting a fresh short-lived token
|
|
72074
|
-
* from the session grant (the mint lane; default-off, forge#3518), or, on
|
|
72075
|
-
* the legacy lane, by promoting a valid subject-level JWT or rotating the
|
|
72076
|
-
* stored CMMD refresh token.
|
|
72077
|
-
*
|
|
72078
|
-
* Fail-closed: never returns a non-user / malformed Bearer. Callers must map
|
|
72079
|
-
* null to CMMD_AUTH_REQUIRED and must not call CMMD upstream with a bad token.
|
|
72080
|
-
*/
|
|
72081
|
-
function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false, staleAccessToken = null }) {
|
|
72082
|
-
const cached = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
|
|
72083
|
-
if (cached && !isJwtExpiredForSession(sessionId)) {
|
|
72084
|
-
if (!forceRefresh) return require_Schema$1.succeed(cached);
|
|
72085
|
-
if (staleAccessToken && cached !== staleAccessToken) return require_Schema$1.succeed(cached).pipe(require_Schema$1.tap(() => require_Schema$1.gen(function* () {
|
|
72086
|
-
const deviceType = yield* readClientDeviceTypeForSession(sessionId);
|
|
72087
|
-
yield* logCmmdJwtRung({
|
|
72088
|
-
sessionId,
|
|
72089
|
-
subject: subject?.trim() || null,
|
|
72090
|
-
deviceType,
|
|
72091
|
-
caller: logPrefix
|
|
72092
|
-
}, "cache", "skipped", "forced refresh skipped: cache already holds a token another caller rotated");
|
|
72093
|
-
})));
|
|
72094
|
-
}
|
|
72095
|
-
const normalizedSubject = subject?.trim() || null;
|
|
72096
|
-
const mintLaneEnabled = isCmmdTokenMintLaneEnabled();
|
|
72097
|
-
if (!normalizedSubject && !forceRefresh && !mintLaneEnabled) return require_Schema$1.succeed(null);
|
|
72098
|
-
return require_Schema$1.gen(function* () {
|
|
72099
|
-
const context = {
|
|
72100
|
-
sessionId,
|
|
72101
|
-
subject: normalizedSubject,
|
|
72102
|
-
deviceType: yield* readClientDeviceTypeForSession(sessionId),
|
|
72103
|
-
caller: logPrefix
|
|
72104
|
-
};
|
|
72105
|
-
if (mintLaneEnabled) {
|
|
72106
|
-
const minted = yield* tryMintShortLivedToken(sessionId, context);
|
|
72107
|
-
if (minted) return minted;
|
|
72108
|
-
if (!normalizedSubject && !forceRefresh) {
|
|
72109
|
-
yield* logCmmdJwtRung(context, "mint", "failed", "no subject to resolve and the mint lane produced nothing", "mint");
|
|
72110
|
-
return null;
|
|
72111
|
-
}
|
|
72112
|
-
}
|
|
72113
|
-
if (yield* isRotationFencedSession(sessionId)) {
|
|
72114
|
-
yield* logCmmdJwtRung(context, "rotation-fence", "failed", "session is fenced from rotating CMMD credentials");
|
|
72115
|
-
return null;
|
|
72116
|
-
}
|
|
72117
|
-
return yield* runRecoveryLadder(sessionId, normalizedSubject, forceRefresh, context);
|
|
72118
|
-
});
|
|
72119
|
-
}
|
|
72120
|
-
//#endregion
|
|
72121
71645
|
//#region src/cmmdData/cmmdRestProxyDeadline.ts
|
|
72122
71646
|
var CmmdProxyTimeoutError = class extends Error {
|
|
72123
71647
|
status;
|
|
@@ -116357,7 +115881,7 @@ const postgresSchemaStatements = [
|
|
|
116357
115881
|
"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)",
|
|
116358
115882
|
"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)",
|
|
116359
115883
|
"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)",
|
|
116360
|
-
"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,
|
|
115884
|
+
"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)",
|
|
116361
115885
|
"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 )",
|
|
116362
115886
|
"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 )",
|
|
116363
115887
|
"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 )",
|
|
@@ -116670,7 +116194,8 @@ const postgresUpgradeMigrations = new Map([
|
|
|
116670
116194
|
WHERE model_selection_json IS NULL`
|
|
116671
116195
|
]],
|
|
116672
116196
|
[77, [`ALTER TABLE projection_threads ADD COLUMN IF NOT EXISTS work_session_id TEXT`]],
|
|
116673
|
-
[78, [`ALTER TABLE projection_threads ADD COLUMN IF NOT EXISTS work_session_bound_at TEXT`]]
|
|
116197
|
+
[78, [`ALTER TABLE projection_threads ADD COLUMN IF NOT EXISTS work_session_bound_at TEXT`]],
|
|
116198
|
+
[79, [`ALTER TABLE command_access_tokens DROP COLUMN IF EXISTS refresh_token`]]
|
|
116674
116199
|
]);
|
|
116675
116200
|
function requirePostgresUpgradeStatements(version) {
|
|
116676
116201
|
const statements = postgresUpgradeMigrations.get(version);
|
|
@@ -116696,15 +116221,15 @@ const runPostgresMigrations = require_Schema$1.fn("runPostgresMigrations")(funct
|
|
|
116696
116221
|
ORDER BY version DESC
|
|
116697
116222
|
LIMIT 1
|
|
116698
116223
|
`)[0]?.version ?? 0;
|
|
116699
|
-
if (currentVersion >
|
|
116224
|
+
if (currentVersion > 79) return yield* require_Schema$1.die(/* @__PURE__ */ new Error(`Forge Postgres schema ${currentVersion} is newer than server schema 79.`));
|
|
116700
116225
|
if (currentVersion === 0) {
|
|
116701
116226
|
for (const statement of postgresSchemaStatements) yield* sql.unsafe(statement);
|
|
116702
116227
|
yield* sql`
|
|
116703
116228
|
INSERT INTO forge_schema_migrations (version)
|
|
116704
|
-
VALUES (${
|
|
116229
|
+
VALUES (${79})
|
|
116705
116230
|
ON CONFLICT (version) DO NOTHING
|
|
116706
116231
|
`;
|
|
116707
|
-
} else for (let version = currentVersion + 1; version <=
|
|
116232
|
+
} else for (let version = currentVersion + 1; version <= 79; version += 1) {
|
|
116708
116233
|
const statements = yield* require_Schema$1.sync(() => requirePostgresUpgradeStatements(version));
|
|
116709
116234
|
for (const statement of statements) yield* sql.unsafe(statement);
|
|
116710
116235
|
yield* sql`
|
|
@@ -119513,6 +119038,26 @@ var _078_ProjectionThreadWorkSessionBoundAt_default = require_Schema$1.gen(funct
|
|
|
119513
119038
|
`;
|
|
119514
119039
|
});
|
|
119515
119040
|
//#endregion
|
|
119041
|
+
//#region src/persistence/Migrations/079_DropCommandRefreshToken.ts
|
|
119042
|
+
/**
|
|
119043
|
+
* Drop the single-use CMMD refresh token from the session credential row.
|
|
119044
|
+
*
|
|
119045
|
+
* The rotation it powered is gone. forge#3518 added the CMMD mint lane, and
|
|
119046
|
+
* forge#3521 deleted the legacy ladder that sat beside it: the rotation fence,
|
|
119047
|
+
* the refresh single-flight, self-refresh, grant redemption, the subject
|
|
119048
|
+
* fallback, and the desktop refresh sweep. The resolver now mints a short-lived
|
|
119049
|
+
* JWT from the row's durable, non-rotating session grant, so a credential whose
|
|
119050
|
+
* every use invalidates it has nothing left to do on this row.
|
|
119051
|
+
*
|
|
119052
|
+
* Nothing is copied out first. Every surviving read of the column was deleted
|
|
119053
|
+
* in the same change; the value is a dead secret, and the kind of secret best
|
|
119054
|
+
* kept nowhere. Historical installs that ran migration 034 need this to run
|
|
119055
|
+
* after it, not instead of it.
|
|
119056
|
+
*/
|
|
119057
|
+
var _079_DropCommandRefreshToken_default = require_Schema$1.gen(function* () {
|
|
119058
|
+
yield* (yield* require_SqlError.SqlClient)`ALTER TABLE command_access_tokens DROP COLUMN refresh_token`;
|
|
119059
|
+
});
|
|
119060
|
+
//#endregion
|
|
119516
119061
|
//#region src/persistence/Migrations.ts
|
|
119517
119062
|
/**
|
|
119518
119063
|
* MigrationsLive - Migration runner with inline loader
|
|
@@ -119923,6 +119468,11 @@ const migrationEntries = [
|
|
|
119923
119468
|
78,
|
|
119924
119469
|
"ProjectionThreadWorkSessionBoundAt",
|
|
119925
119470
|
_078_ProjectionThreadWorkSessionBoundAt_default
|
|
119471
|
+
],
|
|
119472
|
+
[
|
|
119473
|
+
79,
|
|
119474
|
+
"DropCommandRefreshToken",
|
|
119475
|
+
_079_DropCommandRefreshToken_default
|
|
119926
119476
|
]
|
|
119927
119477
|
];
|
|
119928
119478
|
const makeMigrationLoader = (throughId) => fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
|
|
@@ -300783,7 +300333,7 @@ function resolveBuildCommitFromEnv(env) {
|
|
|
300783
300333
|
* environment descriptor down instead of reporting an honest "unknown".
|
|
300784
300334
|
*/
|
|
300785
300335
|
function readBakedBuildCommit() {
|
|
300786
|
-
return "
|
|
300336
|
+
return "15aa06fedcdcf9e165622cf018e31ce724b98ef7";
|
|
300787
300337
|
}
|
|
300788
300338
|
async function resolveServerBuildCommit(input) {
|
|
300789
300339
|
if (isFullCommitSha(input.baked)) return input.baked;
|
|
@@ -301031,14 +300581,13 @@ function identityFromRedeemed(redeemed) {
|
|
|
301031
300581
|
username: redeemed.displayName?.trim() || `user-${redeemed.userId}`
|
|
301032
300582
|
};
|
|
301033
300583
|
}
|
|
300584
|
+
/** Read optional display name, owner assertion, session grant, and expiry from a redeemed handoff. */
|
|
301034
300585
|
function optionalRedeemedFields(body) {
|
|
301035
|
-
const refreshToken = readTrimmedString(body.refreshToken);
|
|
301036
300586
|
const displayName = typeof body.displayName === "string" ? body.displayName : void 0;
|
|
301037
300587
|
const forgeOwnerAssertion = readTrimmedString(body.forgeOwnerAssertion);
|
|
301038
300588
|
const sessionGrant = readTrimmedString(body.sessionGrant);
|
|
301039
300589
|
const sessionGrantExpiresAt = typeof body.sessionGrantExpiresAt === "string" ? body.sessionGrantExpiresAt : void 0;
|
|
301040
300590
|
return {
|
|
301041
|
-
...refreshToken !== void 0 ? { refreshToken } : {},
|
|
301042
300591
|
...displayName !== void 0 ? { displayName } : {},
|
|
301043
300592
|
...forgeOwnerAssertion !== void 0 ? { forgeOwnerAssertion } : {},
|
|
301044
300593
|
...sessionGrant !== void 0 ? { sessionGrant } : {},
|
|
@@ -301106,6 +300655,7 @@ function redeemEmbedCodeWithCmmd(code) {
|
|
|
301106
300655
|
return require_Schema$1.succeed(redeemed);
|
|
301107
300656
|
}));
|
|
301108
300657
|
}
|
|
300658
|
+
/** Store the redeemed PAT, JWT, and identity for a local embed session, failing on write errors. */
|
|
301109
300659
|
function persistLocalEmbedSession(input) {
|
|
301110
300660
|
return require_Schema$1.gen(function* () {
|
|
301111
300661
|
yield* setCommandAccessTokenForSession(input.sessionId, input.redeemed.mcpToken).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
@@ -301113,7 +300663,7 @@ function persistLocalEmbedSession(input) {
|
|
|
301113
300663
|
status: 500,
|
|
301114
300664
|
cause
|
|
301115
300665
|
})));
|
|
301116
|
-
yield* setCommandJwtForSession(input.sessionId, input.redeemed.accessToken
|
|
300666
|
+
yield* setCommandJwtForSession(input.sessionId, input.redeemed.accessToken).pipe(require_Schema$1.mapError((cause) => new AuthError({
|
|
301117
300667
|
message: "Failed to persist CMMD JWT.",
|
|
301118
300668
|
status: 500,
|
|
301119
300669
|
cause
|
|
@@ -301168,7 +300718,6 @@ const authCmmdEmbedExchangeRouteLayer = add$1("POST", "/api/auth/cmmd-embed/exch
|
|
|
301168
300718
|
accessToken: redeemed.mcpToken,
|
|
301169
300719
|
identity,
|
|
301170
300720
|
jwtToken: redeemed.accessToken,
|
|
301171
|
-
refreshToken: redeemed.refreshToken,
|
|
301172
300721
|
sessionGrant,
|
|
301173
300722
|
sessionGrantExpiresAt: redeemed.sessionGrantExpiresAt
|
|
301174
300723
|
});
|
|
@@ -301274,7 +300823,7 @@ const cloudOwnerSessionRedeemRouteLayer = add$1("POST", "/api/auth/cloud/session
|
|
|
301274
300823
|
status: 403
|
|
301275
300824
|
});
|
|
301276
300825
|
const tokenResult = yield* redeemSessionGrantWithCmmd(payload);
|
|
301277
|
-
const { tokenObj, cmmdIdentity, userId, accessToken, mcpToken,
|
|
300826
|
+
const { tokenObj, cmmdIdentity, userId, accessToken, mcpToken, forgeOwnerAssertion, sessionGrantExpiresAt, isJwt, patToStore, jwtToStore } = yield* resolveCmmdTokenBundleCredentials(tokenResult);
|
|
301278
300827
|
if (!cmmdIdentity || !userId || !patToStore) {
|
|
301279
300828
|
yield* require_Schema$1.logError("Forge Cloud session redeem: CMMD token response incomplete.", {
|
|
301280
300829
|
clientId: payload.clientId,
|
|
@@ -301283,7 +300832,6 @@ const cloudOwnerSessionRedeemRouteLayer = add$1("POST", "/api/auth/cloud/session
|
|
|
301283
300832
|
tokenResult,
|
|
301284
300833
|
accessToken,
|
|
301285
300834
|
mcpToken,
|
|
301286
|
-
refreshToken,
|
|
301287
300835
|
isJwt,
|
|
301288
300836
|
userId,
|
|
301289
300837
|
patToStore,
|
|
@@ -301306,7 +300854,6 @@ const cloudOwnerSessionRedeemRouteLayer = add$1("POST", "/api/auth/cloud/session
|
|
|
301306
300854
|
accessToken: patToStore,
|
|
301307
300855
|
identity: cmmdIdentity,
|
|
301308
300856
|
jwtToken: jwtToStore,
|
|
301309
|
-
refreshToken,
|
|
301310
300857
|
sessionGrant: payload.sessionGrant,
|
|
301311
300858
|
sessionGrantExpiresAt
|
|
301312
300859
|
});
|