bopodev-api 0.1.15 → 0.1.17

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.
@@ -18,6 +18,11 @@ function normalizePath(raw: string) {
18
18
  return resolve(expandHomePrefix(raw.trim()));
19
19
  }
20
20
 
21
+ type NormalizeAbsolutePathOptions = {
22
+ requireAbsoluteInput?: boolean;
23
+ baseDir?: string;
24
+ };
25
+
21
26
  export function resolveBopoHomeDir() {
22
27
  const configured = process.env.BOPO_HOME?.trim();
23
28
  if (configured) {
@@ -46,20 +51,27 @@ export function resolveBopoInstanceRoot() {
46
51
  }
47
52
 
48
53
  export function resolveProjectWorkspacePath(companyId: string, projectId: string) {
49
- assertPathSegment(companyId, "companyId");
50
- assertPathSegment(projectId, "projectId");
51
- return join(resolveBopoInstanceRoot(), "workspaces", companyId, "projects", projectId);
54
+ const safeCompanyId = assertPathSegment(companyId, "companyId");
55
+ const safeProjectId = assertPathSegment(projectId, "projectId");
56
+ return join(resolveBopoInstanceRoot(), "workspaces", safeCompanyId, "projects", safeProjectId);
52
57
  }
53
58
 
54
59
  export function resolveCompanyProjectsWorkspacePath(companyId: string) {
55
- assertPathSegment(companyId, "companyId");
56
- return join(resolveBopoInstanceRoot(), "workspaces", companyId);
60
+ const safeCompanyId = assertPathSegment(companyId, "companyId");
61
+ return join(resolveBopoInstanceRoot(), "workspaces", safeCompanyId);
57
62
  }
58
63
 
59
64
  export function resolveAgentFallbackWorkspacePath(companyId: string, agentId: string) {
60
- assertPathSegment(companyId, "companyId");
61
- assertPathSegment(agentId, "agentId");
62
- return join(resolveBopoInstanceRoot(), "workspaces", companyId, "agents", agentId);
65
+ const safeCompanyId = assertPathSegment(companyId, "companyId");
66
+ const safeAgentId = assertPathSegment(agentId, "agentId");
67
+ return join(resolveBopoInstanceRoot(), "workspaces", safeCompanyId, "agents", safeAgentId);
68
+ }
69
+
70
+ export function resolveAgentProjectWorktreeRootPath(companyId: string, agentId: string, projectId: string) {
71
+ const safeCompanyId = assertPathSegment(companyId, "companyId");
72
+ const safeAgentId = assertPathSegment(agentId, "agentId");
73
+ const safeProjectId = assertPathSegment(projectId, "projectId");
74
+ return join(resolveBopoInstanceRoot(), "workspaces", safeCompanyId, "agents", safeAgentId, "worktrees", safeProjectId);
63
75
  }
64
76
 
65
77
  export function resolveAgentMemoryRootPath(companyId: string, agentId: string) {
@@ -78,8 +90,19 @@ export function resolveStorageRoot() {
78
90
  return join(resolveBopoInstanceRoot(), "data", "storage");
79
91
  }
80
92
 
81
- export function normalizeAbsolutePath(value: string) {
82
- return normalizePath(value);
93
+ export function normalizeAbsolutePath(value: string, options?: NormalizeAbsolutePathOptions) {
94
+ const trimmed = value.trim();
95
+ const expanded = expandHomePrefix(trimmed);
96
+ if (options?.requireAbsoluteInput && !isAbsolute(expanded)) {
97
+ throw new Error(`Expected absolute path input, received '${value}'.`);
98
+ }
99
+ if (isAbsolute(expanded)) {
100
+ return resolve(expanded);
101
+ }
102
+ if (options?.baseDir) {
103
+ return resolve(options.baseDir, expanded);
104
+ }
105
+ return resolve(expanded);
83
106
  }
84
107
 
85
108
  export function isInsidePath(parent: string, child: string) {
@@ -92,9 +115,51 @@ export function isInsidePath(parent: string, child: string) {
92
115
  return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
93
116
  }
94
117
 
118
+ export function resolveWorkspaceRootPath() {
119
+ return join(resolveBopoInstanceRoot(), "workspaces");
120
+ }
121
+
122
+ export function resolveCompanyWorkspaceRootPath(companyId: string) {
123
+ return resolveCompanyProjectsWorkspacePath(companyId);
124
+ }
125
+
126
+ export function assertPathInsidePath(parent: string, candidate: string, label = "path") {
127
+ const normalizedParent = normalizeAbsolutePath(parent, { requireAbsoluteInput: true });
128
+ const normalizedCandidate = normalizeAbsolutePath(candidate, { requireAbsoluteInput: true });
129
+ if (!isInsidePath(normalizedParent, normalizedCandidate)) {
130
+ throw new Error(`Invalid ${label} '${candidate}': must be inside '${normalizedParent}'.`);
131
+ }
132
+ return normalizedCandidate;
133
+ }
134
+
135
+ export function assertPathInsideWorkspaceRoot(candidate: string, label = "path") {
136
+ return assertPathInsidePath(resolveWorkspaceRootPath(), candidate, label);
137
+ }
138
+
139
+ export function assertPathInsideCompanyWorkspaceRoot(companyId: string, candidate: string, label = "path") {
140
+ return assertPathInsidePath(resolveCompanyWorkspaceRootPath(companyId), candidate, label);
141
+ }
142
+
143
+ export function normalizeCompanyWorkspacePath(
144
+ companyId: string,
145
+ value: string,
146
+ options?: { requireAbsoluteInput?: boolean }
147
+ ) {
148
+ const companyWorkspaceRoot = resolveCompanyWorkspaceRootPath(companyId);
149
+ const normalized = normalizeAbsolutePath(value, {
150
+ requireAbsoluteInput: options?.requireAbsoluteInput ?? false,
151
+ baseDir: companyWorkspaceRoot
152
+ });
153
+ return assertPathInsideCompanyWorkspaceRoot(companyId, normalized, "workspace path");
154
+ }
155
+
95
156
  function assertPathSegment(value: string, label: string) {
96
157
  const trimmed = value.trim();
158
+ if (trimmed !== value) {
159
+ throw new Error(`Invalid ${label} for workspace path '${value}'.`);
160
+ }
97
161
  if (!SAFE_PATH_SEGMENT_RE.test(trimmed)) {
98
162
  throw new Error(`Invalid ${label} for workspace path '${value}'.`);
99
163
  }
164
+ return trimmed;
100
165
  }
@@ -1,8 +1,10 @@
1
1
  import { and, eq, inArray } from "drizzle-orm";
2
2
  import type { BopoDb } from "bopodev-db";
3
- import { projects } from "bopodev-db";
3
+ import { projectWorkspaces, projects } from "bopodev-db";
4
4
  import {
5
+ assertPathInsideCompanyWorkspaceRoot,
5
6
  isInsidePath,
7
+ normalizeCompanyWorkspacePath,
6
8
  normalizeAbsolutePath,
7
9
  resolveAgentFallbackWorkspacePath,
8
10
  resolveAgentMemoryRootPath,
@@ -13,6 +15,24 @@ export function hasText(value: string | null | undefined) {
13
15
  return Boolean(value && value.trim().length > 0);
14
16
  }
15
17
 
18
+ export type ExecutionWorkspaceMode = "project_primary" | "isolated" | "agent_default";
19
+ export type ExecutionWorkspaceStrategyType = "git_worktree";
20
+ export interface ProjectExecutionWorkspacePolicy {
21
+ mode?: ExecutionWorkspaceMode;
22
+ strategy?: {
23
+ type?: ExecutionWorkspaceStrategyType;
24
+ rootDir?: string | null;
25
+ branchPrefix?: string | null;
26
+ } | null;
27
+ credentials?: {
28
+ mode?: "host" | "env_token";
29
+ tokenEnvVar?: string | null;
30
+ username?: string | null;
31
+ } | null;
32
+ allowRemotes?: string[] | null;
33
+ allowBranchPrefixes?: string[] | null;
34
+ }
35
+
16
36
  export function parseRuntimeCwd(stateBlob: string | null | undefined) {
17
37
  if (!stateBlob) {
18
38
  return null;
@@ -28,18 +48,18 @@ export function parseRuntimeCwd(stateBlob: string | null | undefined) {
28
48
 
29
49
  export async function inferSingleWorkspaceLocalPath(db: BopoDb, companyId: string) {
30
50
  const rows = await db
31
- .select({ workspaceLocalPath: projects.workspaceLocalPath })
32
- .from(projects)
33
- .where(eq(projects.companyId, companyId));
51
+ .select({ cwd: projectWorkspaces.cwd })
52
+ .from(projectWorkspaces)
53
+ .where(and(eq(projectWorkspaces.companyId, companyId), eq(projectWorkspaces.isPrimary, true)));
34
54
  const paths = Array.from(
35
55
  new Set(
36
56
  rows
37
- .map((row) => row.workspaceLocalPath?.trim() ?? "")
57
+ .map((row) => row.cwd?.trim() ?? "")
38
58
  .filter((value) => value.length > 0)
39
59
  )
40
60
  );
41
61
  const singlePath = paths.length === 1 ? paths[0] : null;
42
- return singlePath ? normalizeAbsolutePath(singlePath) : null;
62
+ return singlePath ? normalizeCompanyWorkspacePath(companyId, singlePath) : null;
43
63
  }
44
64
 
45
65
  export async function resolveDefaultRuntimeCwdForCompany(db: BopoDb, companyId: string) {
@@ -51,14 +71,74 @@ export async function resolveDefaultRuntimeCwdForCompany(db: BopoDb, companyId:
51
71
  }
52
72
 
53
73
  export async function getProjectWorkspaceMap(db: BopoDb, companyId: string, projectIds: string[]) {
74
+ const context = await getProjectWorkspaceContextMap(db, companyId, projectIds);
75
+ return new Map(Array.from(context.entries()).map(([projectId, value]) => [projectId, value.cwd]));
76
+ }
77
+
78
+ export async function getProjectWorkspaceContextMap(db: BopoDb, companyId: string, projectIds: string[]) {
54
79
  if (projectIds.length === 0) {
55
- return new Map<string, string | null>();
80
+ return new Map<
81
+ string,
82
+ {
83
+ workspaceId: string | null;
84
+ workspaceName: string | null;
85
+ cwd: string | null;
86
+ repoUrl: string | null;
87
+ repoRef: string | null;
88
+ policy: ProjectExecutionWorkspacePolicy | null;
89
+ }
90
+ >();
56
91
  }
57
- const rows = await db
58
- .select({ id: projects.id, workspaceLocalPath: projects.workspaceLocalPath })
92
+ const workspaceRows = await db
93
+ .select({
94
+ id: projectWorkspaces.id,
95
+ projectId: projectWorkspaces.projectId,
96
+ name: projectWorkspaces.name,
97
+ cwd: projectWorkspaces.cwd,
98
+ repoUrl: projectWorkspaces.repoUrl,
99
+ repoRef: projectWorkspaces.repoRef
100
+ })
101
+ .from(projectWorkspaces)
102
+ .where(
103
+ and(
104
+ eq(projectWorkspaces.companyId, companyId),
105
+ inArray(projectWorkspaces.projectId, projectIds),
106
+ eq(projectWorkspaces.isPrimary, true)
107
+ )
108
+ );
109
+ const projectRows = await db
110
+ .select({ id: projects.id, executionWorkspacePolicy: projects.executionWorkspacePolicy })
59
111
  .from(projects)
60
112
  .where(and(eq(projects.companyId, companyId), inArray(projects.id, projectIds)));
61
- return new Map(rows.map((row) => [row.id, row.workspaceLocalPath ? normalizeAbsolutePath(row.workspaceLocalPath) : null]));
113
+
114
+ const workspaceMap = new Map(
115
+ workspaceRows.map((row) => [row.projectId, row.cwd ? normalizeCompanyWorkspacePath(companyId, row.cwd) : null])
116
+ );
117
+ const workspaceByProject = new Map(workspaceRows.map((row) => [row.projectId, row]));
118
+ const policyMap = new Map(projectRows.map((row) => [row.id, parseProjectExecutionWorkspacePolicy(row.executionWorkspacePolicy)]));
119
+ const result = new Map<
120
+ string,
121
+ {
122
+ workspaceId: string | null;
123
+ workspaceName: string | null;
124
+ cwd: string | null;
125
+ repoUrl: string | null;
126
+ repoRef: string | null;
127
+ policy: ProjectExecutionWorkspacePolicy | null;
128
+ }
129
+ >();
130
+ for (const projectId of projectIds) {
131
+ const workspace = workspaceByProject.get(projectId);
132
+ result.set(projectId, {
133
+ workspaceId: workspace?.id ?? null,
134
+ workspaceName: workspace?.name ?? null,
135
+ cwd: workspaceMap.get(projectId) ?? null,
136
+ repoUrl: workspace?.repoUrl?.trim() ? workspace.repoUrl.trim() : null,
137
+ repoRef: workspace?.repoRef?.trim() ? workspace.repoRef.trim() : null,
138
+ policy: policyMap.get(projectId) ?? null
139
+ });
140
+ }
141
+ return result;
62
142
  }
63
143
 
64
144
  export function ensureRuntimeInsideWorkspace(projectWorkspacePath: string, runtimeCwd: string) {
@@ -78,3 +158,66 @@ export function resolveAgentFallbackWorkspace(companyId: string, agentId: string
78
158
  export function resolveAgentMemoryRoot(companyId: string, agentId: string) {
79
159
  return resolveAgentMemoryRootPath(companyId, agentId);
80
160
  }
161
+
162
+ export function parseProjectExecutionWorkspacePolicy(
163
+ value: string | Record<string, unknown> | null | undefined
164
+ ): ProjectExecutionWorkspacePolicy | null {
165
+ if (!value) {
166
+ return null;
167
+ }
168
+ const parsedValue =
169
+ typeof value === "string"
170
+ ? (() => {
171
+ try {
172
+ return JSON.parse(value) as Record<string, unknown>;
173
+ } catch {
174
+ return null;
175
+ }
176
+ })()
177
+ : value;
178
+ if (!parsedValue || typeof parsedValue !== "object") {
179
+ return null;
180
+ }
181
+ const mode = parsedValue.mode;
182
+ const strategy = parsedValue.strategy as Record<string, unknown> | undefined;
183
+ const normalizedMode: ExecutionWorkspaceMode | undefined =
184
+ mode === "project_primary" || mode === "isolated" || mode === "agent_default" ? mode : undefined;
185
+ const normalizedStrategy =
186
+ strategy && typeof strategy === "object"
187
+ ? {
188
+ type: strategy.type === "git_worktree" ? ("git_worktree" as const) : undefined,
189
+ rootDir: typeof strategy.rootDir === "string" ? strategy.rootDir : null,
190
+ branchPrefix: typeof strategy.branchPrefix === "string" ? strategy.branchPrefix : null
191
+ }
192
+ : null;
193
+ const credentials = parsedValue.credentials as Record<string, unknown> | undefined;
194
+ const normalizedCredentials =
195
+ credentials && typeof credentials === "object"
196
+ ? {
197
+ mode:
198
+ credentials.mode === "host" || credentials.mode === "env_token"
199
+ ? (credentials.mode as "host" | "env_token")
200
+ : undefined,
201
+ tokenEnvVar: typeof credentials.tokenEnvVar === "string" ? credentials.tokenEnvVar : null,
202
+ username: typeof credentials.username === "string" ? credentials.username : null
203
+ }
204
+ : null;
205
+ const allowRemotes = Array.isArray(parsedValue.allowRemotes)
206
+ ? parsedValue.allowRemotes.map((entry) => String(entry).trim()).filter((entry) => entry.length > 0)
207
+ : null;
208
+ const allowBranchPrefixes = Array.isArray(parsedValue.allowBranchPrefixes)
209
+ ? parsedValue.allowBranchPrefixes.map((entry) => String(entry).trim()).filter((entry) => entry.length > 0)
210
+ : null;
211
+ return {
212
+ mode: normalizedMode,
213
+ strategy: normalizedStrategy,
214
+ credentials: normalizedCredentials,
215
+ allowRemotes,
216
+ allowBranchPrefixes
217
+ };
218
+ }
219
+
220
+ export function assertRuntimeCwdForCompany(companyId: string, runtimeCwd: string, label = "runtimeCwd") {
221
+ const normalized = normalizeAbsolutePath(runtimeCwd, { requireAbsoluteInput: true });
222
+ return assertPathInsideCompanyWorkspaceRoot(companyId, normalized, label);
223
+ }
@@ -1,6 +1,8 @@
1
1
  import type { NextFunction, Request, Response } from "express";
2
2
  import { RequestActorHeadersSchema } from "bopodev-contracts";
3
3
  import { sendError } from "../http";
4
+ import { verifyActorToken } from "../security/actor-token";
5
+ import { isAuthenticatedMode, resolveDeploymentMode } from "../security/deployment-mode";
4
6
 
5
7
  export type RequestActor = {
6
8
  type: "board" | "member" | "agent";
@@ -20,6 +22,22 @@ declare global {
20
22
  }
21
23
 
22
24
  export function attachRequestActor(req: Request, res: Response, next: NextFunction) {
25
+ const deploymentMode = resolveDeploymentMode();
26
+ const tokenSecret = process.env.BOPO_AUTH_TOKEN_SECRET?.trim() || "";
27
+ const tokenIdentity = tokenSecret
28
+ ? verifyActorToken(readBearerToken(req.header("authorization")) ?? req.header("x-bopo-actor-token"), tokenSecret)
29
+ : null;
30
+ if (tokenIdentity) {
31
+ req.actor = {
32
+ type: tokenIdentity.type,
33
+ id: tokenIdentity.id,
34
+ companyIds: tokenIdentity.companyIds,
35
+ permissions: tokenIdentity.permissions
36
+ };
37
+ next();
38
+ return;
39
+ }
40
+
23
41
  const actorHeadersResult = RequestActorHeadersSchema.safeParse({
24
42
  "x-actor-type": req.header("x-actor-type")?.trim().toLowerCase(),
25
43
  "x-actor-id": req.header("x-actor-id")?.trim(),
@@ -39,9 +57,44 @@ export function attachRequestActor(req: Request, res: Response, next: NextFuncti
39
57
  const companyIds = parseCommaList(actorHeaders["x-actor-companies"]);
40
58
  const permissions = parseCommaList(actorHeaders["x-actor-permissions"]) ?? [];
41
59
  const hasActorHeaders = Boolean(
42
- actorHeaders["x-actor-type"] || actorHeaders["x-actor-id"] || companyIds || permissions.length > 0
60
+ actorHeaders["x-actor-type"] ||
61
+ actorHeaders["x-actor-id"] ||
62
+ (companyIds && companyIds.length > 0) ||
63
+ permissions.length > 0
43
64
  );
44
- const allowLocalBoardFallback = process.env.NODE_ENV !== "production" && process.env.BOPO_ALLOW_LOCAL_BOARD_FALLBACK !== "0";
65
+ const allowLocalBoardFallback =
66
+ deploymentMode === "local" && process.env.NODE_ENV !== "production" && process.env.BOPO_ALLOW_LOCAL_BOARD_FALLBACK !== "0";
67
+ const trustActorHeaders = deploymentMode === "local" || process.env.BOPO_TRUST_ACTOR_HEADERS === "1";
68
+ const bootstrapSecret = process.env.BOPO_AUTH_BOOTSTRAP_SECRET?.trim();
69
+ const bootstrapHeader = req.header("x-bopo-bootstrap-secret")?.trim();
70
+ const allowBootstrapTokenIssue =
71
+ req.method === "POST" &&
72
+ req.path === "/auth/actor-token" &&
73
+ Boolean(bootstrapSecret && bootstrapHeader && bootstrapSecret === bootstrapHeader);
74
+ if (isAuthenticatedMode(deploymentMode) && !hasActorHeaders && !tokenIdentity) {
75
+ if (allowBootstrapTokenIssue) {
76
+ req.actor = {
77
+ type: "board",
78
+ id: "bootstrap-secret",
79
+ companyIds: null,
80
+ permissions: []
81
+ };
82
+ next();
83
+ return;
84
+ }
85
+ return sendError(
86
+ res,
87
+ "Missing actor identity. Provide Authorization Bearer actor token or trusted x-actor-* headers.",
88
+ 401
89
+ );
90
+ }
91
+ if (isAuthenticatedMode(deploymentMode) && hasActorHeaders && !trustActorHeaders) {
92
+ return sendError(
93
+ res,
94
+ "x-actor-* headers are disabled in authenticated mode. Set BOPO_TRUST_ACTOR_HEADERS=1 only behind a trusted proxy.",
95
+ 401
96
+ );
97
+ }
45
98
  const actorType = hasActorHeaders
46
99
  ? actorHeaders["x-actor-type"] ?? "member"
47
100
  : allowLocalBoardFallback
@@ -62,6 +115,18 @@ export function attachRequestActor(req: Request, res: Response, next: NextFuncti
62
115
  next();
63
116
  }
64
117
 
118
+ function readBearerToken(value: string | undefined) {
119
+ const trimmed = value?.trim();
120
+ if (!trimmed) {
121
+ return null;
122
+ }
123
+ const lower = trimmed.toLowerCase();
124
+ if (!lower.startsWith("bearer ")) {
125
+ return null;
126
+ }
127
+ return trimmed.slice("bearer ".length).trim() || null;
128
+ }
129
+
65
130
  export function requirePermission(permission: string) {
66
131
  return (req: Request, res: Response, next: NextFunction) => {
67
132
  if (!hasPermission(req, permission)) {
@@ -5,6 +5,8 @@ import {
5
5
  type RealtimeMessage
6
6
  } from "bopodev-contracts";
7
7
  import { WebSocket, WebSocketServer } from "ws";
8
+ import { verifyActorToken } from "../security/actor-token";
9
+ import { isAuthenticatedMode, resolveDeploymentMode } from "../security/deployment-mode";
8
10
 
9
11
  type RealtimeEventMessage = Extract<RealtimeMessage, { kind: "event" }>;
10
12
  type RealtimeBootstrapLoader = (companyId: string) => Promise<RealtimeEventMessage>;
@@ -25,7 +27,7 @@ export function attachRealtimeHub(
25
27
 
26
28
  wss.on("connection", async (socket, request) => {
27
29
  const subscription = getSubscription(request.url);
28
- if (!subscription || !canSubscribeToCompany(request.headers, subscription.companyId)) {
30
+ if (!subscription || !canSubscribeToCompany(request.url, request.headers, subscription.companyId)) {
29
31
  socket.close(1008, "Invalid realtime subscription");
30
32
  return;
31
33
  }
@@ -132,13 +134,31 @@ function getSubscription(requestUrl: string | undefined) {
132
134
  }
133
135
 
134
136
  function canSubscribeToCompany(
137
+ requestUrl: string | undefined,
135
138
  headers: Record<string, string | string[] | undefined>,
136
139
  companyId: string
137
140
  ) {
141
+ const deploymentMode = resolveDeploymentMode();
142
+ const tokenSecret = process.env.BOPO_AUTH_TOKEN_SECRET?.trim() || "";
143
+ const tokenIdentity = tokenSecret ? verifyActorToken(readRealtimeTokenFromUrl(requestUrl), tokenSecret) : null;
144
+ if (tokenIdentity) {
145
+ if (tokenIdentity.type === "board") {
146
+ return true;
147
+ }
148
+ return tokenIdentity.companyIds?.includes(companyId) ?? false;
149
+ }
138
150
  const actorType = readHeader(headers, "x-actor-type")?.toLowerCase();
139
151
  const actorCompanies = parseCommaList(readHeader(headers, "x-actor-companies"));
140
152
  const hasActorHeaders = Boolean(actorType || actorCompanies.length > 0);
141
- const allowLocalBoardFallback = process.env.NODE_ENV !== "production" && process.env.BOPO_ALLOW_LOCAL_BOARD_FALLBACK !== "0";
153
+ const allowLocalBoardFallback =
154
+ deploymentMode === "local" && process.env.NODE_ENV !== "production" && process.env.BOPO_ALLOW_LOCAL_BOARD_FALLBACK !== "0";
155
+ const trustActorHeaders = deploymentMode === "local" || process.env.BOPO_TRUST_ACTOR_HEADERS === "1";
156
+ if (isAuthenticatedMode(deploymentMode) && !hasActorHeaders) {
157
+ return false;
158
+ }
159
+ if (isAuthenticatedMode(deploymentMode) && hasActorHeaders && !trustActorHeaders) {
160
+ return false;
161
+ }
142
162
 
143
163
  if (!hasActorHeaders) {
144
164
  return allowLocalBoardFallback;
@@ -149,6 +169,15 @@ function canSubscribeToCompany(
149
169
  return actorCompanies.includes(companyId);
150
170
  }
151
171
 
172
+ function readRealtimeTokenFromUrl(requestUrl: string | undefined) {
173
+ if (!requestUrl) {
174
+ return null;
175
+ }
176
+ const url = new URL(requestUrl, "http://localhost");
177
+ const token = url.searchParams.get("authToken")?.trim();
178
+ return token && token.length > 0 ? token : null;
179
+ }
180
+
152
181
  function readHeader(headers: Record<string, string | string[] | undefined>, name: string) {
153
182
  const value = headers[name];
154
183
  if (Array.isArray(value)) {