@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 CHANGED
@@ -66407,7 +66407,7 @@ function normalizeNumberish(value) {
66407
66407
  }
66408
66408
  //#endregion
66409
66409
  //#region package.json
66410
- var version$1 = "0.13.132";
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 cmmdCredentialRefreshTotal = require_Schema$1.counter("t3_cmmd_credential_refresh_total", { description: "CMMD JWT refresh attempts for a Forge session, by rung (self-refresh | grant-redemption) and outcome (attempted | succeeded)." });
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
- refreshToken: require_Schema$1.NullOr(require_Schema$1.String),
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 that had a perfectly good refresh token sitting next to it. Two
67558
- * minutes is far inside the JWT's own lifetime, so this costs one early
67559
- * rotation and removes the race.
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
- * Unlike the refresh token this is NON-rotating: redeeming it does not
67723
- * invalidate it, so it stays valid until CMMD expires or revokes it. That is
67724
- * what makes it a safe recovery credential after a rotation is lost.
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, opts = {}) => require_Schema$1.gen(function* () {
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
- const storedRefreshToken = refreshToken ? encodeSensitiveValue(REFRESH_TOKEN_SCOPE, refreshToken) : null;
67782
- const sql = yield* require_SqlError.SqlClient;
67783
- const now = (/* @__PURE__ */ new Date()).toISOString();
67784
- if (refreshToken) yield* sql`
67785
- UPDATE command_access_tokens
67786
- SET
67787
- jwt_token = ${storedToken},
67788
- jwt_expires_at = ${expiresAt},
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 refresh token sitting on its own previous session. It surfaced as "This
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.refresh_token AS "refreshToken",
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 refreshToken = decryptNullable(REFRESH_TOKEN_SCOPE, row.refreshToken);
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
- refresh_token,
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.refreshToken},
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
- refresh_token = excluded.refresh_token,
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 (refreshToken) refreshTokenCache.set(sessionId, refreshToken);
68003
- else refreshTokenCache.delete(sessionId);
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
- refreshToken,
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 a distinct PAT and refresh token (CMMD `createAccountScopedToken` /
68091
- * `issueTokenPair`). One session's dead credential says nothing about a
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 a refresh token good for months, so every call is logged with a
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
- * Mirrors `isDesktopBearerAuthSession`'s read but returns the raw column
68195
- * instead of collapsing it into a rotation verdict — a log line wants to say
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, input.refreshToken ? { refreshToken: input.refreshToken } : {}).pipe(require_Schema$1.mapError(artifactError("Failed to persist CMMD JWT.")));
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
- const refreshToken = input.payload.refreshToken?.trim();
68721
- yield* setCommandJwtForSession(input.sessionId, token, refreshToken ? { refreshToken } : {}).pipe(require_Schema$1.mapError((cause) => new AuthError({
68722
- message: "Failed to persist CMMD JWT.",
68723
- status: 500,
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, input.refreshToken ? { refreshToken: input.refreshToken } : {}).pipe(require_Schema$1.tapError((error) => require_Schema$1.sync(() => captureServerError(error, {
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, refreshToken, sessionGrant, sessionGrantExpiresAt, forgeOwnerAssertion, isJwt, patToStore, jwtToStore } = yield* resolveCmmdTokenBundleCredentials(tokenResult);
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 the one field the mint lane consumes from CMMD's mint response.
71431
- * Returns null unless `access_token` is present and non-empty.
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
- return { accessToken: accessToken.trim() };
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 lane gives up on it. Minting is one
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 resolver falls through to the legacy ladder.
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 mint
71450
- * lane's entire refresh story: call it again when the short-lived token
71451
- * expires. CMMD answers with `access_token` only (no refresh token, no MCP
71452
- * PAT, nothing durable), so a session that lives entirely on this lane never
71453
- * accumulates long-lived credential state on Forge's side.
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 redemption request body. `popPublicKeyPem` is the server's PoP identity
71488
- * (forge#3534): presented when bound sessions are enabled so CMMD stamps the
71489
- * returned JWT with this server's key (`cnf.jkt`), omitted otherwise.
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 buildSessionGrantRedemptionBody(grantToken, popPublicKeyPem) {
71492
- return {
71493
- grant_type: "forge_session_grant",
71494
- grant_token: grantToken,
71495
- client_id: "cmmd-forge-web",
71496
- ...popPublicKeyPem ? { pop_public_key: popPublicKeyPem } : {}
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
- * Redeem a durable CMMD session grant for a fresh credential set.
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
- * Returns the new MCP PAT alongside the JWT/refresh pair, because CMMD mints a
71509
- * fresh PAT on redemption and the old one is not carried over.
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
- function redeemCmmdSessionGrant(grantToken, popPublicKeyPem) {
71512
- return fetch(new URL("/api/mcp-server/oauth/token", resolveCommandAppUrl()).toString(), {
71513
- method: "POST",
71514
- headers: { "Content-Type": "application/json" },
71515
- body: JSON.stringify(buildSessionGrantRedemptionBody(grantToken, popPublicKeyPem))
71516
- }).then(async (response) => {
71517
- if (!response.ok) return null;
71518
- const payload = await response.json();
71519
- const parsed = parseCmmdOAuthRefreshResponse(payload);
71520
- if (!parsed) return null;
71521
- const mcpToken = payload["mcp_token"];
71522
- return {
71523
- ...parsed,
71524
- mcpToken: typeof mcpToken === "string" && mcpToken.trim().length > 0 ? mcpToken.trim() : null
71525
- };
71526
- }).catch(() => null);
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
- * Rotate a CMMD refresh token into a fresh access JWT.
71412
+ * Clear a session's CMMD credentials, but only if CMMD rejected them.
71535
71413
  *
71536
- * Returns null for every failure, because the caller's recovery ladder wants a
71537
- * value rather than a thrown error. Null is nearly the whole diagnostic value
71538
- * of this call though, so each way of reaching it says so first. Chasing a
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 refreshCmmdJwt(refreshToken, observer) {
71546
- return fetch(new URL("/api/mcp-server/oauth/token", resolveCommandAppUrl()).toString(), {
71547
- method: "POST",
71548
- headers: { "Content-Type": "application/json" },
71549
- body: JSON.stringify({
71550
- grant_type: "refresh_token",
71551
- refresh_token: refreshToken,
71552
- client_id: "cmmd-forge-web"
71553
- })
71554
- }).then(async (response) => {
71555
- if (!response.ok) {
71556
- const errorCode = await readOAuthErrorCode(response);
71557
- if (errorCode !== null && REJECTED_OAUTH_ERROR_CODES.has(errorCode)) observer?.onCredentialRejected?.();
71558
- console.warn(`[cmmd-auth] CMMD refused a JWT rotation with ${response.status}${errorCode === null ? "" : ` (${errorCode})`}`);
71559
- return null;
71560
- }
71561
- const parsed = parseCmmdOAuthRefreshResponse(await response.json());
71562
- if (!parsed) console.warn("[cmmd-auth] CMMD accepted a JWT rotation but returned no usable access token");
71563
- return parsed;
71564
- }).catch((cause) => {
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
- * The OAuth `error` code from a refusal, lowercased, or null when there is none.
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
- * Best effort by design: a body that is not JSON, or is already consumed, must
71573
- * not turn a logged failure into an unlogged one. Null also reads correctly as
71574
- * "no evidence the credential is dead", which is the safe answer for a caller
71575
- * deciding whether to keep a refresh token.
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
- async function readOAuthErrorCode(response) {
71578
- try {
71579
- const code = (await response.json())["error"];
71580
- return typeof code === "string" && code.trim().length > 0 ? code.trim().toLowerCase() : null;
71581
- } catch {
71582
- return null;
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, refresh_token TEXT, cmmd_user_identity TEXT, session_grant TEXT, session_grant_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 > 78) return yield* require_Schema$1.die(/* @__PURE__ */ new Error(`Forge Postgres schema ${currentVersion} is newer than server schema 78.`));
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 (${78})
116229
+ VALUES (${79})
116705
116230
  ON CONFLICT (version) DO NOTHING
116706
116231
  `;
116707
- } else for (let version = currentVersion + 1; version <= 78; version += 1) {
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 "b5d9623e21d1d5b372b4133d72c12136356d744e";
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, input.redeemed.refreshToken ? { refreshToken: input.redeemed.refreshToken } : {}).pipe(require_Schema$1.mapError((cause) => new AuthError({
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, refreshToken, forgeOwnerAssertion, sessionGrantExpiresAt, isJwt, patToStore, jwtToStore } = yield* resolveCmmdTokenBundleCredentials(tokenResult);
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
  });