@cmmd-center/forge 0.13.125 → 0.13.126

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.
Files changed (36) hide show
  1. package/dist/bin.cjs +138 -41
  2. package/dist/bin.mjs +138 -41
  3. package/dist/client/assets/{CanvasPanel-BOh2xUZv.js → CanvasPanel-Db1Iz4JN.js} +2 -2
  4. package/dist/client/assets/CompositeItem-BxPZ_9BJ.js +2 -0
  5. package/dist/client/assets/DiffPanel-CNil6OV-.js +64 -0
  6. package/dist/client/assets/{DiffPanel.logic-DS1mmUZB.js → DiffPanel.logic-yEOXCs0u.js} +2 -2
  7. package/dist/client/assets/{DiffWorkerPoolProvider-C2tnM5gq.js → DiffWorkerPoolProvider-BF-pZpUl.js} +11 -11
  8. package/dist/client/assets/PullRequestCodePanel-eldrSbZ3.js +5 -0
  9. package/dist/client/assets/{ThemeEditorPanel-v2mnY18G.js → ThemeEditorPanel-DTfrlPBc.js} +2 -2
  10. package/dist/client/assets/{Virtualizer-Ds5-GzY3.js → Virtualizer-BvsCWf4E.js} +2 -2
  11. package/dist/client/assets/WorkerPoolContext-CaivT_VA.js +1533 -0
  12. package/dist/client/assets/{canvasStateStore-xaZAenqH.js → canvasStateStore-DKtQcBVc.js} +2 -2
  13. package/dist/client/assets/cmmdForgeRuntimeListResource-CRVF6pin.js +1008 -0
  14. package/dist/client/assets/currentForgeWorkspaceStore-DH-DGTJm.js +2 -0
  15. package/dist/client/assets/{index-Rx6Z6FPS.js → index-CIM7fDFV.js} +68 -68
  16. package/dist/client/assets/popover-Cvb1Axgx.js +2 -0
  17. package/dist/client/assets/scroll-area-DzReJmNn.js +2 -0
  18. package/dist/client/assets/switch-RuRK1onH.js +2 -0
  19. package/dist/client/assets/{themePalette-eOA8OvYM.js → themePalette-gr6ZYaOx.js} +2 -2
  20. package/dist/client/assets/toggle-D8xaJAgP.js +2 -0
  21. package/dist/client/assets/toggle-group-CwSbXP07.js +2 -0
  22. package/dist/client/assets/useOpenInteractionType-B983ff5H.js +2 -0
  23. package/dist/client/assets/utils-B4G-Q0s7.js +2 -0
  24. package/dist/client/index.html +16 -13
  25. package/dist/forge-build.json +1 -1
  26. package/package.json +1 -1
  27. package/dist/client/assets/CompositeItem-DNVaubQt.js +0 -2
  28. package/dist/client/assets/DiffPanel-BM-qanPT.js +0 -64
  29. package/dist/client/assets/PullRequestCodePanel-zxkVGUeq.js +0 -5
  30. package/dist/client/assets/WorkerPoolContext-BWuPUnhX.js +0 -2538
  31. package/dist/client/assets/popover-Cxnw5kve.js +0 -2
  32. package/dist/client/assets/scroll-area-DDcW71H4.js +0 -2
  33. package/dist/client/assets/switch-Wi-2Dkh9.js +0 -2
  34. package/dist/client/assets/toggle-group-Cqh7gA8I.js +0 -2
  35. package/dist/client/assets/toggle-m8ncTpRB.js +0 -2
  36. package/dist/client/assets/useOpenInteractionType-CN3hnnfp.js +0 -2
package/dist/bin.cjs CHANGED
@@ -66337,7 +66337,7 @@ function normalizeNumberish(value) {
66337
66337
  }
66338
66338
  //#endregion
66339
66339
  //#region package.json
66340
- var version$1 = "0.13.125";
66340
+ var version$1 = "0.13.126";
66341
66341
  //#endregion
66342
66342
  //#region src/sentry.ts
66343
66343
  const SERVER_APP_NAME = "forge-server";
@@ -72634,20 +72634,25 @@ const legacyCmmdToolBridge = {
72634
72634
  };
72635
72635
  //#endregion
72636
72636
  //#region src/command/workIntakeCache.ts
72637
- const cacheBySessionId = /* @__PURE__ */ new Map();
72637
+ const cacheByKey = /* @__PURE__ */ new Map();
72638
+ function cacheKeyFor(sessionId, orgId) {
72639
+ return `${sessionId}\u0000${orgId ?? ""}`;
72640
+ }
72638
72641
  function resolveCmmdWorkIntakeCacheIdentity(subject, cmmdUserId) {
72639
72642
  return cmmdUserId ? `cmmd-user:${cmmdUserId}` : `forge-subject:${subject}`;
72640
72643
  }
72641
72644
  function fingerprintToken(token) {
72642
72645
  return (0, node_crypto.createHash)("sha256").update(token).digest("hex");
72643
72646
  }
72644
- async function fetchBlockingSnapshot(input, identityKey, tokenFingerprint, existing) {
72647
+ async function fetchBlockingSnapshot(input, cacheKey, identityKey, tokenFingerprint, existing) {
72645
72648
  if (existing?.inFlight) return existing.inFlight;
72646
72649
  const nowMs = input.nowMs ?? Date.now;
72647
72650
  let promise;
72648
72651
  promise = input.fetchFresh().then((snapshot) => {
72649
- const current = cacheBySessionId.get(input.sessionId);
72650
- if (current?.tokenFingerprint === tokenFingerprint && current.inFlight === promise) cacheBySessionId.set(input.sessionId, {
72652
+ const current = cacheByKey.get(cacheKey);
72653
+ if (current?.tokenFingerprint === tokenFingerprint && current.inFlight === promise) cacheByKey.set(cacheKey, {
72654
+ sessionId: input.sessionId,
72655
+ orgId: input.orgId,
72651
72656
  identityKey,
72652
72657
  tokenFingerprint,
72653
72658
  snapshot,
@@ -72656,12 +72661,14 @@ async function fetchBlockingSnapshot(input, identityKey, tokenFingerprint, exist
72656
72661
  });
72657
72662
  return snapshot;
72658
72663
  }).catch((error) => {
72659
- const current = cacheBySessionId.get(input.sessionId);
72660
- if (current?.tokenFingerprint === tokenFingerprint && current.inFlight === promise) if (legacyCmmdToolBridge.isAuthError(error)) cacheBySessionId.delete(input.sessionId);
72664
+ const current = cacheByKey.get(cacheKey);
72665
+ if (current?.tokenFingerprint === tokenFingerprint && current.inFlight === promise) if (legacyCmmdToolBridge.isAuthError(error)) cacheByKey.delete(cacheKey);
72661
72666
  else current.inFlight = null;
72662
72667
  throw error;
72663
72668
  });
72664
- cacheBySessionId.set(input.sessionId, {
72669
+ cacheByKey.set(cacheKey, {
72670
+ sessionId: input.sessionId,
72671
+ orgId: input.orgId,
72665
72672
  identityKey,
72666
72673
  tokenFingerprint,
72667
72674
  snapshot: existing?.snapshot ?? null,
@@ -72677,9 +72684,9 @@ async function fetchBlockingSnapshot(input, identityKey, tokenFingerprint, exist
72677
72684
  * opportunistically on every read.
72678
72685
  */
72679
72686
  function sweepExpiredWorkIntakeEntries(nowMs) {
72680
- for (const [sessionId, entry] of cacheBySessionId) {
72687
+ for (const [key, entry] of cacheByKey) {
72681
72688
  if (entry.inFlight) continue;
72682
- if (nowMs - entry.cachedAtMs > 3e5) cacheBySessionId.delete(sessionId);
72689
+ if (nowMs - entry.cachedAtMs > 3e5) cacheByKey.delete(key);
72683
72690
  }
72684
72691
  }
72685
72692
  async function readCachedCmmdWorkIntakeForSession(input) {
@@ -72688,17 +72695,18 @@ async function readCachedCmmdWorkIntakeForSession(input) {
72688
72695
  const identityKey = input.identityKey ?? input.sessionId;
72689
72696
  if (input.forceFresh) invalidateCachedCmmdWorkIntakeForIdentity(identityKey);
72690
72697
  const tokenFingerprint = fingerprintToken(input.token);
72691
- const existing = cacheBySessionId.get(input.sessionId);
72692
- if (existing && (existing.identityKey !== identityKey || existing.tokenFingerprint !== tokenFingerprint)) cacheBySessionId.delete(input.sessionId);
72693
- const current = cacheBySessionId.get(input.sessionId);
72694
- if (!current?.snapshot) return fetchBlockingSnapshot(input, identityKey, tokenFingerprint, current);
72698
+ const cacheKey = cacheKeyFor(input.sessionId, input.orgId);
72699
+ const existing = cacheByKey.get(cacheKey);
72700
+ if (existing && (existing.identityKey !== identityKey || existing.tokenFingerprint !== tokenFingerprint)) cacheByKey.delete(cacheKey);
72701
+ const current = cacheByKey.get(cacheKey);
72702
+ if (!current?.snapshot) return fetchBlockingSnapshot(input, cacheKey, identityKey, tokenFingerprint, current);
72695
72703
  const ageMs = Math.max(0, nowMs() - current.cachedAtMs);
72696
72704
  if (ageMs <= 12e4) return current.snapshot;
72697
72705
  if (ageMs <= 3e5) {
72698
72706
  if (!current.inFlight) {
72699
72707
  let refreshPromise;
72700
72708
  refreshPromise = input.fetchFresh().then((snapshot) => {
72701
- const latest = cacheBySessionId.get(input.sessionId);
72709
+ const latest = cacheByKey.get(cacheKey);
72702
72710
  if (latest?.tokenFingerprint === tokenFingerprint && latest.inFlight === refreshPromise) {
72703
72711
  latest.snapshot = snapshot;
72704
72712
  latest.cachedAtMs = nowMs();
@@ -72706,25 +72714,25 @@ async function readCachedCmmdWorkIntakeForSession(input) {
72706
72714
  return snapshot;
72707
72715
  }).catch((error) => {
72708
72716
  if (legacyCmmdToolBridge.isAuthError(error)) {
72709
- const latest = cacheBySessionId.get(input.sessionId);
72710
- if (latest?.tokenFingerprint === tokenFingerprint && latest.inFlight === refreshPromise) cacheBySessionId.delete(input.sessionId);
72717
+ const latest = cacheByKey.get(cacheKey);
72718
+ if (latest?.tokenFingerprint === tokenFingerprint && latest.inFlight === refreshPromise) cacheByKey.delete(cacheKey);
72711
72719
  }
72712
72720
  return current.snapshot;
72713
72721
  }).finally(() => {
72714
- const latest = cacheBySessionId.get(input.sessionId);
72722
+ const latest = cacheByKey.get(cacheKey);
72715
72723
  if (latest?.tokenFingerprint === tokenFingerprint && latest.inFlight === refreshPromise) latest.inFlight = null;
72716
72724
  });
72717
72725
  current.inFlight = refreshPromise;
72718
72726
  }
72719
72727
  return current.snapshot;
72720
72728
  }
72721
- return fetchBlockingSnapshot(input, identityKey, tokenFingerprint, current);
72729
+ return fetchBlockingSnapshot(input, cacheKey, identityKey, tokenFingerprint, current);
72722
72730
  }
72723
72731
  function invalidateCachedCmmdWorkIntakeForSession(sessionId) {
72724
- cacheBySessionId.delete(sessionId);
72732
+ for (const [key, entry] of cacheByKey) if (entry.sessionId === sessionId) cacheByKey.delete(key);
72725
72733
  }
72726
72734
  function invalidateCachedCmmdWorkIntakeForIdentity(identityKey) {
72727
- for (const [sessionId, entry] of cacheBySessionId) if (entry.identityKey === identityKey) cacheBySessionId.delete(sessionId);
72735
+ for (const [key, entry] of cacheByKey) if (entry.identityKey === identityKey) cacheByKey.delete(key);
72728
72736
  }
72729
72737
  //#endregion
72730
72738
  //#region src/cmmdData/proxyRouteRegistry.ts
@@ -105231,6 +105239,29 @@ function warn$1(label, cause) {
105231
105239
  return `${label} fetch failed: ${cause instanceof Error ? cause.message : String(cause)}`;
105232
105240
  }
105233
105241
  /**
105242
+ * Reads the workspace catalog off a settled bootstrap call. No explicit org
105243
+ * means CMMD answered `activeWorkspace` with its own default-organization
105244
+ * guess, not a confirmed fact -- leave it null rather than publish it
105245
+ * (forge#3636).
105246
+ */
105247
+ function resolveWorkspacesFromBootstrap(bootstrapResult, orgId, warnings) {
105248
+ if (bootstrapResult.status !== "fulfilled") {
105249
+ warnings.push(warn$1("Workspaces (bootstrap)", bootstrapResult.reason));
105250
+ return {
105251
+ workspaces: [],
105252
+ activeWorkspace: null
105253
+ };
105254
+ }
105255
+ const bootstrap = unwrapRestObject(bootstrapResult.value);
105256
+ const rawWorkspaces = bootstrap?.["workspaces"];
105257
+ const workspaces = Array.isArray(rawWorkspaces) ? rawWorkspaces.map(adaptBootstrapWorkspace) : [];
105258
+ const rawActive = orgId !== void 0 ? bootstrap?.["activeWorkspace"] : void 0;
105259
+ return {
105260
+ workspaces,
105261
+ activeWorkspace: rawActive && typeof rawActive === "object" ? adaptBootstrapWorkspace(rawActive) : null
105262
+ };
105263
+ }
105264
+ /**
105234
105265
  * Build the CMMD work-intake snapshot via the JWT REST API:
105235
105266
  * bootstrap (workspaces) + campaigns + projects + center-board (tasks WITH deps).
105236
105267
  * `identity` is supplied by the session (resolved at login), matching the MCP builder.
@@ -105238,21 +105269,13 @@ function warn$1(label, cause) {
105238
105269
  async function fetchCmmdWorkIntakeViaRest(jwt, identity, deps = {}) {
105239
105270
  const get = deps.proxyGet ?? proxyGet;
105240
105271
  const [bootstrapResult, campaignsResult, projectsResult, boardResult] = await Promise.allSettled([
105241
- get(jwt, "/api/forge/context/bootstrap"),
105272
+ get(jwt, "/api/forge/context/bootstrap", void 0, deps.orgId),
105242
105273
  get(jwt, "/api/forge/context/campaigns"),
105243
105274
  fetchAllProjects(get, jwt),
105244
105275
  get(jwt, "/api/forge/context/work-items/board")
105245
105276
  ]);
105246
105277
  const warnings = [];
105247
- let workspaces = [];
105248
- let activeWorkspace = null;
105249
- if (bootstrapResult.status === "fulfilled") {
105250
- const bootstrap = unwrapRestObject(bootstrapResult.value);
105251
- const rawWorkspaces = bootstrap?.["workspaces"];
105252
- if (Array.isArray(rawWorkspaces)) workspaces = rawWorkspaces.map(adaptBootstrapWorkspace);
105253
- const rawActive = bootstrap?.["activeWorkspace"];
105254
- if (rawActive && typeof rawActive === "object") activeWorkspace = adaptBootstrapWorkspace(rawActive);
105255
- } else warnings.push(warn$1("Workspaces (bootstrap)", bootstrapResult.reason));
105278
+ const { workspaces, activeWorkspace } = resolveWorkspacesFromBootstrap(bootstrapResult, deps.orgId, warnings);
105256
105279
  if (projectsResult.status === "rejected") throw projectsResult.reason;
105257
105280
  if (campaignsResult.status === "rejected") warnings.push(warn$1("Campaigns", campaignsResult.reason));
105258
105281
  if (boardResult.status === "rejected") warnings.push(warn$1("Tasks (board)", boardResult.reason));
@@ -300572,7 +300595,7 @@ function resolveBuildCommitFromEnv(env) {
300572
300595
  * environment descriptor down instead of reporting an honest "unknown".
300573
300596
  */
300574
300597
  function readBakedBuildCommit() {
300575
- return "2b217d6b85f7c98cf77e2a23bce2ef8102d3a0a5";
300598
+ return "8a4057a4a58eeb014b73143ffbb1bb091089d750";
300576
300599
  }
300577
300600
  async function resolveServerBuildCommit(input) {
300578
300601
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -310992,6 +311015,24 @@ function logWorkIntakeFailure(cause) {
310992
311015
  }
310993
311016
  console.error("[work-intake] fetch failed:", cause);
310994
311017
  }
311018
+ /**
311019
+ * `?orgId=` present but empty (a stripped or malformed query string) is not
311020
+ * an explicit org. Treat it the same as an absent param rather than sending
311021
+ * CMMD an empty `X-Organization-ID` header (forge#3636).
311022
+ */
311023
+ function normalizeOrgIdQueryParam(raw) {
311024
+ const trimmed = raw?.trim();
311025
+ return trimmed ? trimmed : void 0;
311026
+ }
311027
+ /**
311028
+ * `{ orgId }` when present, `{}` when not -- the `exactOptionalPropertyTypes`
311029
+ * shape every `orgId?: string` deps object here needs, since spreading
311030
+ * `{ orgId: possiblyUndefined }` inline assigns `undefined` to a property
311031
+ * the type says is only ever absent or a string.
311032
+ */
311033
+ function orgIdDeps(orgId) {
311034
+ return orgId !== void 0 ? { orgId } : {};
311035
+ }
310995
311036
  function fetchRestFallbackWorkIntakeForSession(session, dependencies = {}) {
310996
311037
  return require_Schema$1.gen(function* () {
310997
311038
  const jwt = yield* (dependencies.resolveJwt ?? resolveCmmdJwtForSession)({
@@ -311003,7 +311044,7 @@ function fetchRestFallbackWorkIntakeForSession(session, dependencies = {}) {
311003
311044
  const identity = (dependencies.readIdentity ?? readCmmdUserIdentityForSession)(session.sessionId);
311004
311045
  const fetchFallback = dependencies.fetchFallback ?? fetchCmmdWorkIntakeViaRest;
311005
311046
  return yield* require_Schema$1.tryPromise({
311006
- try: () => fetchFallback(jwt, identity),
311047
+ try: () => fetchFallback(jwt, identity, orgIdDeps(dependencies.orgId)),
311007
311048
  catch: (cause) => new WorkIntakeSnapshotReadError({
311008
311049
  detail: "Failed to build CMMD REST work-intake fallback.",
311009
311050
  cause
@@ -311115,7 +311156,9 @@ function unwrapForgeEnvelopeJsonText(text) {
311115
311156
  const cmmdWorkIntakeRouteLayer = add$1("GET", "/api/cmmd/work-intake", require_Schema$1.gen(function* () {
311116
311157
  const request = yield* require_HttpServer.HttpServerRequest;
311117
311158
  const session = yield* (yield* ServerAuth).authenticateHttpRequest(request);
311118
- const forceFresh = new URL(request.url, "http://localhost").searchParams.get("fresh") === "1";
311159
+ const requestUrl = new URL(request.url, "http://localhost");
311160
+ const forceFresh = requestUrl.searchParams.get("fresh") === "1";
311161
+ const orgId = normalizeOrgIdQueryParam(requestUrl.searchParams.get("orgId"));
311119
311162
  let accessToken = readCommandAccessTokenForSession(session.sessionId);
311120
311163
  if (!accessToken && session.role === "owner" && (session.subject === "owner-bootstrap" || session.subject === "desktop-bootstrap")) accessToken = (yield* promoteMostRecentOwnerCommandAuthToSession(session.sessionId).pipe(require_Schema$1.catch_((error) => require_Schema$1.logWarning("Failed to promote owner CMMD auth for work intake.", {
311121
311164
  cause: error,
@@ -311160,8 +311203,9 @@ const cmmdWorkIntakeRouteLayer = add$1("GET", "/api/cmmd/work-intake", require_S
311160
311203
  sessionId: session.sessionId,
311161
311204
  identityKey: workIntakeCacheIdentity,
311162
311205
  token: accessToken,
311206
+ ...orgIdDeps(orgId),
311163
311207
  forceFresh,
311164
- fetchFresh: () => cmmdJwt ? fetchCmmdWorkIntakeViaRest(cmmdJwt, sessionIdentity) : Promise.reject(/* @__PURE__ */ new Error("CMMD JWT unavailable for work-intake fetch."))
311208
+ fetchFresh: () => cmmdJwt ? fetchCmmdWorkIntakeViaRest(cmmdJwt, sessionIdentity, orgIdDeps(orgId)) : Promise.reject(/* @__PURE__ */ new Error("CMMD JWT unavailable for work-intake fetch."))
311165
311209
  }),
311166
311210
  catch: (cause) => new WorkIntakeSnapshotReadError({
311167
311211
  detail: "Failed to read CMMD work intake snapshot.",
@@ -311179,7 +311223,7 @@ const cmmdWorkIntakeRouteLayer = add$1("GET", "/api/cmmd/work-intake", require_S
311179
311223
  if (isCmmdMcpAuthError(cause)) {
311180
311224
  console.error("[WORK_INTAKE] CMMD_AUTH_EXPIRED mcp_error:", cause instanceof Error ? cause.message : String(cause));
311181
311225
  invalidateCachedCmmdWorkIntakeForSession(session.sessionId);
311182
- const fallback = yield* fetchRestFallbackWorkIntakeForSession(session);
311226
+ const fallback = yield* fetchRestFallbackWorkIntakeForSession(session, orgIdDeps(orgId));
311183
311227
  yield* clearCommandAccessTokenForSession(session.sessionId).pipe(require_Schema$1.catch_((error) => require_Schema$1.logWarning("Failed to clear expired CMMD token after MCP auth failure.", {
311184
311228
  cause: error,
311185
311229
  sessionId: session.sessionId
@@ -311204,7 +311248,7 @@ const cmmdWorkIntakeRouteLayer = add$1("GET", "/api/cmmd/work-intake", require_S
311204
311248
  connected: snapshot.connected
311205
311249
  });
311206
311250
  invalidateCachedCmmdWorkIntakeForSession(session.sessionId);
311207
- const fallback = yield* fetchRestFallbackWorkIntakeForSession(session);
311251
+ const fallback = yield* fetchRestFallbackWorkIntakeForSession(session, orgIdDeps(orgId));
311208
311252
  yield* clearCommandAccessTokenForSession(session.sessionId).pipe(require_Schema$1.catch_((error) => require_Schema$1.logWarning("Failed to clear expired CMMD token after auth warning.", {
311209
311253
  cause: error,
311210
311254
  sessionId: session.sessionId
@@ -312560,12 +312604,65 @@ registerProxyRoute({
312560
312604
  responseShape: "unwrapData",
312561
312605
  requestBody: "none"
312562
312606
  });
312563
- registerProxyRoute({
312607
+ function parseCmmdBootstrapRequest(request) {
312608
+ return require_Schema$1.sync(() => {
312609
+ return {
312610
+ ok: true,
312611
+ value: normalizeOrgIdQueryParam(new URL(request.url, "http://localhost").searchParams.get("orgId"))
312612
+ };
312613
+ });
312614
+ }
312615
+ /**
312616
+ * Nulls `activeWorkspace` on a bootstrap body, wherever it sits. CMMD answers
312617
+ * `GET /api/forge/context/bootstrap` either flat (`{ activeWorkspace, ... }`)
312618
+ * or enveloped (`{ data: { activeWorkspace, ... } }`) -- `workIntakeRest.ts`'s
312619
+ * `unwrapRestObject` already treats both as live shapes for this exact
312620
+ * endpoint. Nulling only the top level left `data.activeWorkspace` (CMMD's
312621
+ * guess) untouched on the enveloped shape.
312622
+ */
312623
+ function nullActiveWorkspace(body) {
312624
+ const data = body["data"];
312625
+ const nulledData = typeof data === "object" && data !== null && !Array.isArray(data) ? {
312626
+ ...data,
312627
+ activeWorkspace: null
312628
+ } : data;
312629
+ return {
312630
+ ...body,
312631
+ activeWorkspace: null,
312632
+ ...data !== void 0 ? { data: nulledData } : {}
312633
+ };
312634
+ }
312635
+ /**
312636
+ * CMMD resolves `activeWorkspace` to the caller's own default organization
312637
+ * whenever the request carries no `X-Organization-ID` header (forge#3636:
312638
+ * the owner working in org 2119 kept getting org 2142, CMMD's default, back).
312639
+ * Passing the client's explicit `?orgId=` scopes CMMD's answer to that
312640
+ * workspace; without one, CMMD still guesses, so `activeWorkspace` is nulled
312641
+ * here rather than forwarded as fact. Everything else in the bootstrap
312642
+ * response passes through untouched.
312643
+ */
312644
+ function runCmmdBootstrap(orgId, { session }) {
312645
+ return runCmmdRestProxyAsHandlerResult(session, (jwt) => cmmdGateway.request({
312646
+ method: "GET",
312647
+ jwt,
312648
+ path: "/api/forge/context/bootstrap",
312649
+ ...orgId !== void 0 ? { orgId } : {}
312650
+ }), (value) => {
312651
+ if (orgId !== void 0 || value === null || typeof value !== "object") return {
312652
+ status: 200,
312653
+ body: value
312654
+ };
312655
+ return {
312656
+ status: 200,
312657
+ body: nullActiveWorkspace(value)
312658
+ };
312659
+ });
312660
+ }
312661
+ registerProxyHandler({
312564
312662
  forgePath: "/api/cmmd/bootstrap",
312565
312663
  method: "GET",
312566
- cmmdPath: "/api/forge/context/bootstrap",
312567
- orgIdSource: "none",
312568
- responseShape: "passthrough"
312664
+ parse: parseCmmdBootstrapRequest,
312665
+ run: runCmmdBootstrap
312569
312666
  });
312570
312667
  registerProxyRoute({
312571
312668
  forgePath: "/api/cmmd/account/preferences",
package/dist/bin.mjs CHANGED
@@ -66046,7 +66046,7 @@ function normalizeNumberish(value) {
66046
66046
  }
66047
66047
  //#endregion
66048
66048
  //#region package.json
66049
- var version$1 = "0.13.125";
66049
+ var version$1 = "0.13.126";
66050
66050
  //#endregion
66051
66051
  //#region src/sentry.ts
66052
66052
  const SERVER_APP_NAME = "forge-server";
@@ -72318,20 +72318,25 @@ const legacyCmmdToolBridge = {
72318
72318
  };
72319
72319
  //#endregion
72320
72320
  //#region src/command/workIntakeCache.ts
72321
- const cacheBySessionId = /* @__PURE__ */ new Map();
72321
+ const cacheByKey = /* @__PURE__ */ new Map();
72322
+ function cacheKeyFor(sessionId, orgId) {
72323
+ return `${sessionId}\u0000${orgId ?? ""}`;
72324
+ }
72322
72325
  function resolveCmmdWorkIntakeCacheIdentity(subject, cmmdUserId) {
72323
72326
  return cmmdUserId ? `cmmd-user:${cmmdUserId}` : `forge-subject:${subject}`;
72324
72327
  }
72325
72328
  function fingerprintToken(token) {
72326
72329
  return createHash("sha256").update(token).digest("hex");
72327
72330
  }
72328
- async function fetchBlockingSnapshot(input, identityKey, tokenFingerprint, existing) {
72331
+ async function fetchBlockingSnapshot(input, cacheKey, identityKey, tokenFingerprint, existing) {
72329
72332
  if (existing?.inFlight) return existing.inFlight;
72330
72333
  const nowMs = input.nowMs ?? Date.now;
72331
72334
  let promise;
72332
72335
  promise = input.fetchFresh().then((snapshot) => {
72333
- const current = cacheBySessionId.get(input.sessionId);
72334
- if (current?.tokenFingerprint === tokenFingerprint && current.inFlight === promise) cacheBySessionId.set(input.sessionId, {
72336
+ const current = cacheByKey.get(cacheKey);
72337
+ if (current?.tokenFingerprint === tokenFingerprint && current.inFlight === promise) cacheByKey.set(cacheKey, {
72338
+ sessionId: input.sessionId,
72339
+ orgId: input.orgId,
72335
72340
  identityKey,
72336
72341
  tokenFingerprint,
72337
72342
  snapshot,
@@ -72340,12 +72345,14 @@ async function fetchBlockingSnapshot(input, identityKey, tokenFingerprint, exist
72340
72345
  });
72341
72346
  return snapshot;
72342
72347
  }).catch((error) => {
72343
- const current = cacheBySessionId.get(input.sessionId);
72344
- if (current?.tokenFingerprint === tokenFingerprint && current.inFlight === promise) if (legacyCmmdToolBridge.isAuthError(error)) cacheBySessionId.delete(input.sessionId);
72348
+ const current = cacheByKey.get(cacheKey);
72349
+ if (current?.tokenFingerprint === tokenFingerprint && current.inFlight === promise) if (legacyCmmdToolBridge.isAuthError(error)) cacheByKey.delete(cacheKey);
72345
72350
  else current.inFlight = null;
72346
72351
  throw error;
72347
72352
  });
72348
- cacheBySessionId.set(input.sessionId, {
72353
+ cacheByKey.set(cacheKey, {
72354
+ sessionId: input.sessionId,
72355
+ orgId: input.orgId,
72349
72356
  identityKey,
72350
72357
  tokenFingerprint,
72351
72358
  snapshot: existing?.snapshot ?? null,
@@ -72361,9 +72368,9 @@ async function fetchBlockingSnapshot(input, identityKey, tokenFingerprint, exist
72361
72368
  * opportunistically on every read.
72362
72369
  */
72363
72370
  function sweepExpiredWorkIntakeEntries(nowMs) {
72364
- for (const [sessionId, entry] of cacheBySessionId) {
72371
+ for (const [key, entry] of cacheByKey) {
72365
72372
  if (entry.inFlight) continue;
72366
- if (nowMs - entry.cachedAtMs > 3e5) cacheBySessionId.delete(sessionId);
72373
+ if (nowMs - entry.cachedAtMs > 3e5) cacheByKey.delete(key);
72367
72374
  }
72368
72375
  }
72369
72376
  async function readCachedCmmdWorkIntakeForSession(input) {
@@ -72372,17 +72379,18 @@ async function readCachedCmmdWorkIntakeForSession(input) {
72372
72379
  const identityKey = input.identityKey ?? input.sessionId;
72373
72380
  if (input.forceFresh) invalidateCachedCmmdWorkIntakeForIdentity(identityKey);
72374
72381
  const tokenFingerprint = fingerprintToken(input.token);
72375
- const existing = cacheBySessionId.get(input.sessionId);
72376
- if (existing && (existing.identityKey !== identityKey || existing.tokenFingerprint !== tokenFingerprint)) cacheBySessionId.delete(input.sessionId);
72377
- const current = cacheBySessionId.get(input.sessionId);
72378
- if (!current?.snapshot) return fetchBlockingSnapshot(input, identityKey, tokenFingerprint, current);
72382
+ const cacheKey = cacheKeyFor(input.sessionId, input.orgId);
72383
+ const existing = cacheByKey.get(cacheKey);
72384
+ if (existing && (existing.identityKey !== identityKey || existing.tokenFingerprint !== tokenFingerprint)) cacheByKey.delete(cacheKey);
72385
+ const current = cacheByKey.get(cacheKey);
72386
+ if (!current?.snapshot) return fetchBlockingSnapshot(input, cacheKey, identityKey, tokenFingerprint, current);
72379
72387
  const ageMs = Math.max(0, nowMs() - current.cachedAtMs);
72380
72388
  if (ageMs <= 12e4) return current.snapshot;
72381
72389
  if (ageMs <= 3e5) {
72382
72390
  if (!current.inFlight) {
72383
72391
  let refreshPromise;
72384
72392
  refreshPromise = input.fetchFresh().then((snapshot) => {
72385
- const latest = cacheBySessionId.get(input.sessionId);
72393
+ const latest = cacheByKey.get(cacheKey);
72386
72394
  if (latest?.tokenFingerprint === tokenFingerprint && latest.inFlight === refreshPromise) {
72387
72395
  latest.snapshot = snapshot;
72388
72396
  latest.cachedAtMs = nowMs();
@@ -72390,25 +72398,25 @@ async function readCachedCmmdWorkIntakeForSession(input) {
72390
72398
  return snapshot;
72391
72399
  }).catch((error) => {
72392
72400
  if (legacyCmmdToolBridge.isAuthError(error)) {
72393
- const latest = cacheBySessionId.get(input.sessionId);
72394
- if (latest?.tokenFingerprint === tokenFingerprint && latest.inFlight === refreshPromise) cacheBySessionId.delete(input.sessionId);
72401
+ const latest = cacheByKey.get(cacheKey);
72402
+ if (latest?.tokenFingerprint === tokenFingerprint && latest.inFlight === refreshPromise) cacheByKey.delete(cacheKey);
72395
72403
  }
72396
72404
  return current.snapshot;
72397
72405
  }).finally(() => {
72398
- const latest = cacheBySessionId.get(input.sessionId);
72406
+ const latest = cacheByKey.get(cacheKey);
72399
72407
  if (latest?.tokenFingerprint === tokenFingerprint && latest.inFlight === refreshPromise) latest.inFlight = null;
72400
72408
  });
72401
72409
  current.inFlight = refreshPromise;
72402
72410
  }
72403
72411
  return current.snapshot;
72404
72412
  }
72405
- return fetchBlockingSnapshot(input, identityKey, tokenFingerprint, current);
72413
+ return fetchBlockingSnapshot(input, cacheKey, identityKey, tokenFingerprint, current);
72406
72414
  }
72407
72415
  function invalidateCachedCmmdWorkIntakeForSession(sessionId) {
72408
- cacheBySessionId.delete(sessionId);
72416
+ for (const [key, entry] of cacheByKey) if (entry.sessionId === sessionId) cacheByKey.delete(key);
72409
72417
  }
72410
72418
  function invalidateCachedCmmdWorkIntakeForIdentity(identityKey) {
72411
- for (const [sessionId, entry] of cacheBySessionId) if (entry.identityKey === identityKey) cacheBySessionId.delete(sessionId);
72419
+ for (const [key, entry] of cacheByKey) if (entry.identityKey === identityKey) cacheByKey.delete(key);
72412
72420
  }
72413
72421
  //#endregion
72414
72422
  //#region src/cmmdData/proxyRouteRegistry.ts
@@ -104835,6 +104843,29 @@ function warn$1(label, cause) {
104835
104843
  return `${label} fetch failed: ${cause instanceof Error ? cause.message : String(cause)}`;
104836
104844
  }
104837
104845
  /**
104846
+ * Reads the workspace catalog off a settled bootstrap call. No explicit org
104847
+ * means CMMD answered `activeWorkspace` with its own default-organization
104848
+ * guess, not a confirmed fact -- leave it null rather than publish it
104849
+ * (forge#3636).
104850
+ */
104851
+ function resolveWorkspacesFromBootstrap(bootstrapResult, orgId, warnings) {
104852
+ if (bootstrapResult.status !== "fulfilled") {
104853
+ warnings.push(warn$1("Workspaces (bootstrap)", bootstrapResult.reason));
104854
+ return {
104855
+ workspaces: [],
104856
+ activeWorkspace: null
104857
+ };
104858
+ }
104859
+ const bootstrap = unwrapRestObject(bootstrapResult.value);
104860
+ const rawWorkspaces = bootstrap?.["workspaces"];
104861
+ const workspaces = Array.isArray(rawWorkspaces) ? rawWorkspaces.map(adaptBootstrapWorkspace) : [];
104862
+ const rawActive = orgId !== void 0 ? bootstrap?.["activeWorkspace"] : void 0;
104863
+ return {
104864
+ workspaces,
104865
+ activeWorkspace: rawActive && typeof rawActive === "object" ? adaptBootstrapWorkspace(rawActive) : null
104866
+ };
104867
+ }
104868
+ /**
104838
104869
  * Build the CMMD work-intake snapshot via the JWT REST API:
104839
104870
  * bootstrap (workspaces) + campaigns + projects + center-board (tasks WITH deps).
104840
104871
  * `identity` is supplied by the session (resolved at login), matching the MCP builder.
@@ -104842,21 +104873,13 @@ function warn$1(label, cause) {
104842
104873
  async function fetchCmmdWorkIntakeViaRest(jwt, identity, deps = {}) {
104843
104874
  const get = deps.proxyGet ?? proxyGet;
104844
104875
  const [bootstrapResult, campaignsResult, projectsResult, boardResult] = await Promise.allSettled([
104845
- get(jwt, "/api/forge/context/bootstrap"),
104876
+ get(jwt, "/api/forge/context/bootstrap", void 0, deps.orgId),
104846
104877
  get(jwt, "/api/forge/context/campaigns"),
104847
104878
  fetchAllProjects(get, jwt),
104848
104879
  get(jwt, "/api/forge/context/work-items/board")
104849
104880
  ]);
104850
104881
  const warnings = [];
104851
- let workspaces = [];
104852
- let activeWorkspace = null;
104853
- if (bootstrapResult.status === "fulfilled") {
104854
- const bootstrap = unwrapRestObject(bootstrapResult.value);
104855
- const rawWorkspaces = bootstrap?.["workspaces"];
104856
- if (Array.isArray(rawWorkspaces)) workspaces = rawWorkspaces.map(adaptBootstrapWorkspace);
104857
- const rawActive = bootstrap?.["activeWorkspace"];
104858
- if (rawActive && typeof rawActive === "object") activeWorkspace = adaptBootstrapWorkspace(rawActive);
104859
- } else warnings.push(warn$1("Workspaces (bootstrap)", bootstrapResult.reason));
104882
+ const { workspaces, activeWorkspace } = resolveWorkspacesFromBootstrap(bootstrapResult, deps.orgId, warnings);
104860
104883
  if (projectsResult.status === "rejected") throw projectsResult.reason;
104861
104884
  if (campaignsResult.status === "rejected") warnings.push(warn$1("Campaigns", campaignsResult.reason));
104862
104885
  if (boardResult.status === "rejected") warnings.push(warn$1("Tasks (board)", boardResult.reason));
@@ -300019,7 +300042,7 @@ function resolveBuildCommitFromEnv(env) {
300019
300042
  * environment descriptor down instead of reporting an honest "unknown".
300020
300043
  */
300021
300044
  function readBakedBuildCommit() {
300022
- return "2b217d6b85f7c98cf77e2a23bce2ef8102d3a0a5";
300045
+ return "8a4057a4a58eeb014b73143ffbb1bb091089d750";
300023
300046
  }
300024
300047
  async function resolveServerBuildCommit(input) {
300025
300048
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -310369,6 +310392,24 @@ function logWorkIntakeFailure(cause) {
310369
310392
  }
310370
310393
  console.error("[work-intake] fetch failed:", cause);
310371
310394
  }
310395
+ /**
310396
+ * `?orgId=` present but empty (a stripped or malformed query string) is not
310397
+ * an explicit org. Treat it the same as an absent param rather than sending
310398
+ * CMMD an empty `X-Organization-ID` header (forge#3636).
310399
+ */
310400
+ function normalizeOrgIdQueryParam(raw) {
310401
+ const trimmed = raw?.trim();
310402
+ return trimmed ? trimmed : void 0;
310403
+ }
310404
+ /**
310405
+ * `{ orgId }` when present, `{}` when not -- the `exactOptionalPropertyTypes`
310406
+ * shape every `orgId?: string` deps object here needs, since spreading
310407
+ * `{ orgId: possiblyUndefined }` inline assigns `undefined` to a property
310408
+ * the type says is only ever absent or a string.
310409
+ */
310410
+ function orgIdDeps(orgId) {
310411
+ return orgId !== void 0 ? { orgId } : {};
310412
+ }
310372
310413
  function fetchRestFallbackWorkIntakeForSession(session, dependencies = {}) {
310373
310414
  return gen(function* () {
310374
310415
  const jwt = yield* (dependencies.resolveJwt ?? resolveCmmdJwtForSession)({
@@ -310380,7 +310421,7 @@ function fetchRestFallbackWorkIntakeForSession(session, dependencies = {}) {
310380
310421
  const identity = (dependencies.readIdentity ?? readCmmdUserIdentityForSession)(session.sessionId);
310381
310422
  const fetchFallback = dependencies.fetchFallback ?? fetchCmmdWorkIntakeViaRest;
310382
310423
  return yield* tryPromise({
310383
- try: () => fetchFallback(jwt, identity),
310424
+ try: () => fetchFallback(jwt, identity, orgIdDeps(dependencies.orgId)),
310384
310425
  catch: (cause) => new WorkIntakeSnapshotReadError({
310385
310426
  detail: "Failed to build CMMD REST work-intake fallback.",
310386
310427
  cause
@@ -310492,7 +310533,9 @@ function unwrapForgeEnvelopeJsonText(text) {
310492
310533
  const cmmdWorkIntakeRouteLayer = add$1("GET", "/api/cmmd/work-intake", gen(function* () {
310493
310534
  const request = yield* HttpServerRequest;
310494
310535
  const session = yield* (yield* ServerAuth).authenticateHttpRequest(request);
310495
- const forceFresh = new URL(request.url, "http://localhost").searchParams.get("fresh") === "1";
310536
+ const requestUrl = new URL(request.url, "http://localhost");
310537
+ const forceFresh = requestUrl.searchParams.get("fresh") === "1";
310538
+ const orgId = normalizeOrgIdQueryParam(requestUrl.searchParams.get("orgId"));
310496
310539
  let accessToken = readCommandAccessTokenForSession(session.sessionId);
310497
310540
  if (!accessToken && session.role === "owner" && (session.subject === "owner-bootstrap" || session.subject === "desktop-bootstrap")) accessToken = (yield* promoteMostRecentOwnerCommandAuthToSession(session.sessionId).pipe(catch_((error) => logWarning$1("Failed to promote owner CMMD auth for work intake.", {
310498
310541
  cause: error,
@@ -310537,8 +310580,9 @@ const cmmdWorkIntakeRouteLayer = add$1("GET", "/api/cmmd/work-intake", gen(funct
310537
310580
  sessionId: session.sessionId,
310538
310581
  identityKey: workIntakeCacheIdentity,
310539
310582
  token: accessToken,
310583
+ ...orgIdDeps(orgId),
310540
310584
  forceFresh,
310541
- fetchFresh: () => cmmdJwt ? fetchCmmdWorkIntakeViaRest(cmmdJwt, sessionIdentity) : Promise.reject(/* @__PURE__ */ new Error("CMMD JWT unavailable for work-intake fetch."))
310585
+ fetchFresh: () => cmmdJwt ? fetchCmmdWorkIntakeViaRest(cmmdJwt, sessionIdentity, orgIdDeps(orgId)) : Promise.reject(/* @__PURE__ */ new Error("CMMD JWT unavailable for work-intake fetch."))
310542
310586
  }),
310543
310587
  catch: (cause) => new WorkIntakeSnapshotReadError({
310544
310588
  detail: "Failed to read CMMD work intake snapshot.",
@@ -310556,7 +310600,7 @@ const cmmdWorkIntakeRouteLayer = add$1("GET", "/api/cmmd/work-intake", gen(funct
310556
310600
  if (isCmmdMcpAuthError(cause)) {
310557
310601
  console.error("[WORK_INTAKE] CMMD_AUTH_EXPIRED mcp_error:", cause instanceof Error ? cause.message : String(cause));
310558
310602
  invalidateCachedCmmdWorkIntakeForSession(session.sessionId);
310559
- const fallback = yield* fetchRestFallbackWorkIntakeForSession(session);
310603
+ const fallback = yield* fetchRestFallbackWorkIntakeForSession(session, orgIdDeps(orgId));
310560
310604
  yield* clearCommandAccessTokenForSession(session.sessionId).pipe(catch_((error) => logWarning$1("Failed to clear expired CMMD token after MCP auth failure.", {
310561
310605
  cause: error,
310562
310606
  sessionId: session.sessionId
@@ -310581,7 +310625,7 @@ const cmmdWorkIntakeRouteLayer = add$1("GET", "/api/cmmd/work-intake", gen(funct
310581
310625
  connected: snapshot.connected
310582
310626
  });
310583
310627
  invalidateCachedCmmdWorkIntakeForSession(session.sessionId);
310584
- const fallback = yield* fetchRestFallbackWorkIntakeForSession(session);
310628
+ const fallback = yield* fetchRestFallbackWorkIntakeForSession(session, orgIdDeps(orgId));
310585
310629
  yield* clearCommandAccessTokenForSession(session.sessionId).pipe(catch_((error) => logWarning$1("Failed to clear expired CMMD token after auth warning.", {
310586
310630
  cause: error,
310587
310631
  sessionId: session.sessionId
@@ -311936,12 +311980,65 @@ registerProxyRoute({
311936
311980
  responseShape: "unwrapData",
311937
311981
  requestBody: "none"
311938
311982
  });
311939
- registerProxyRoute({
311983
+ function parseCmmdBootstrapRequest(request) {
311984
+ return sync$1(() => {
311985
+ return {
311986
+ ok: true,
311987
+ value: normalizeOrgIdQueryParam(new URL(request.url, "http://localhost").searchParams.get("orgId"))
311988
+ };
311989
+ });
311990
+ }
311991
+ /**
311992
+ * Nulls `activeWorkspace` on a bootstrap body, wherever it sits. CMMD answers
311993
+ * `GET /api/forge/context/bootstrap` either flat (`{ activeWorkspace, ... }`)
311994
+ * or enveloped (`{ data: { activeWorkspace, ... } }`) -- `workIntakeRest.ts`'s
311995
+ * `unwrapRestObject` already treats both as live shapes for this exact
311996
+ * endpoint. Nulling only the top level left `data.activeWorkspace` (CMMD's
311997
+ * guess) untouched on the enveloped shape.
311998
+ */
311999
+ function nullActiveWorkspace(body) {
312000
+ const data = body["data"];
312001
+ const nulledData = typeof data === "object" && data !== null && !Array.isArray(data) ? {
312002
+ ...data,
312003
+ activeWorkspace: null
312004
+ } : data;
312005
+ return {
312006
+ ...body,
312007
+ activeWorkspace: null,
312008
+ ...data !== void 0 ? { data: nulledData } : {}
312009
+ };
312010
+ }
312011
+ /**
312012
+ * CMMD resolves `activeWorkspace` to the caller's own default organization
312013
+ * whenever the request carries no `X-Organization-ID` header (forge#3636:
312014
+ * the owner working in org 2119 kept getting org 2142, CMMD's default, back).
312015
+ * Passing the client's explicit `?orgId=` scopes CMMD's answer to that
312016
+ * workspace; without one, CMMD still guesses, so `activeWorkspace` is nulled
312017
+ * here rather than forwarded as fact. Everything else in the bootstrap
312018
+ * response passes through untouched.
312019
+ */
312020
+ function runCmmdBootstrap(orgId, { session }) {
312021
+ return runCmmdRestProxyAsHandlerResult(session, (jwt) => cmmdGateway.request({
312022
+ method: "GET",
312023
+ jwt,
312024
+ path: "/api/forge/context/bootstrap",
312025
+ ...orgId !== void 0 ? { orgId } : {}
312026
+ }), (value) => {
312027
+ if (orgId !== void 0 || value === null || typeof value !== "object") return {
312028
+ status: 200,
312029
+ body: value
312030
+ };
312031
+ return {
312032
+ status: 200,
312033
+ body: nullActiveWorkspace(value)
312034
+ };
312035
+ });
312036
+ }
312037
+ registerProxyHandler({
311940
312038
  forgePath: "/api/cmmd/bootstrap",
311941
312039
  method: "GET",
311942
- cmmdPath: "/api/forge/context/bootstrap",
311943
- orgIdSource: "none",
311944
- responseShape: "passthrough"
312040
+ parse: parseCmmdBootstrapRequest,
312041
+ run: runCmmdBootstrap
311945
312042
  });
311946
312043
  registerProxyRoute({
311947
312044
  forgePath: "/api/cmmd/account/preferences",