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