{"record":{"id":"195e76f8da2def3f","repo":"Budibase/budibase","slug":"link-token-is-not-valid-for-this-workspace","errorCode":null,"errorMessage":"Link token is not valid for this workspace","messagePattern":"Link token is not valid for this workspace","errorType":"http","errorClass":"HTTPError","httpStatus":400,"severity":"error","filePath":"packages/server/src/api/controllers/ai/chatIdentityLinks.ts","lineNumber":125,"sourceCode":"      }\n`\n\nconst resolveToken = (token?: string) => {\n  if (!token) {\n    throw new HTTPError(\"token is required\", 400)\n  }\n  return token\n}\n\nconst assertSessionMatchesInstance = ({\n  workspaceId,\n  instance,\n}: {\n  workspaceId?: string\n  instance: string\n}) => {\n  if (!workspaceId || workspaceId !== instance) {\n    throw new HTTPError(\"Link token is not valid for this workspace\", 400)\n  }\n}\n\nconst getCurrentGlobalUserId = (ctx: UserCtx) => {\n  const currentUserId =\n    ctx.user?.globalId ||\n    getGlobalIDFromUserMetadataID(ctx.user?._id || \"\") ||\n    ctx.user?._id\n  if (!currentUserId) {\n    throw new HTTPError(\"Unable to resolve current user\", 400)\n  }\n  return currentUserId\n}\n\nconst providerDisplayName = (provider: ChatIdentityLinkProvider) => {\n  if (provider === AgentChannelProvider.MSTEAMS) {\n    return \"Teams\"\n  }","sourceCodeStart":107,"sourceCodeEnd":143,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/api/controllers/ai/chatIdentityLinks.ts#L107-L143","documentation":"assertSessionMatchesInstance ensures the workspace (instance) encoded in the stored link-token session matches the :instance path parameter of the request. This prevents a token issued for one workspace/app from being redeemed against another. If workspaceId is missing or differs from the instance in the URL, a 400 is thrown.","triggerScenarios":"Redeeming a handoff or confirm link where the instance segment in the URL does not equal session.workspaceId — e.g. swapping the workspace id in the URL, using a token generated in app A while browsing app B, or a session stored without a workspaceId.","commonSituations":"Multi-tenant environments where users belong to several apps and paste a link into the wrong app's domain/URL; environments migrated or renamed instances so stored workspaceId no longer matches; copying an old link after the app was recreated with a new id.","solutions":["Open the link exactly as generated, matching the workspace it was issued for","Regenerate the identity link from within the correct workspace","If the app was recreated/migrated, re-issue the link so session.workspaceId matches the new instance id","Fix link-generation code to embed the current workspace id in the instance path segment"],"exampleFix":"// before\n// token issued in workspaceId=app_dev, but requesting:\nfetch(`/api/chat-links/${otherInstanceId}/${token}/confirm`)\n// after\nfetch(`/api/chat-links/${workspaceId}/${token}/confirm`)","handlingStrategy":"validation","validationCode":"if (session.workspaceId !== instanceId) throw new Error(\"link token belongs to a different workspace\")","typeGuard":"function matchesInstance(session: { workspaceId?: string }, instance: string): session is { workspaceId: string } {\n  return typeof session.workspaceId === \"string\" && session.workspaceId === instance\n}","tryCatchPattern":"try {\n  await api.confirmLink(instance, token)\n} catch (e) {\n  if (e instanceof HTTPError && e.status === 400 && e.message.includes(\"not valid for this workspace\")) {\n    // re-issue the link from the correct workspace\n  }\n  throw e\n}","preventionTips":["Only open chat link URLs from the workspace that issued them","Regenerate links after app migration/recreation","Include the workspace id in link generation from one source of truth","For multi-app users, disambiguate which app a link belongs to before clicking"],"tags":["validation","multi-tenancy","identity-link"],"backgroundTag":"token-workspace-mismatch","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}