Skip to content

Commit 65d1f2f

Browse files
committed
fix(auth): hydrate workspace list names
1 parent f9ca072 commit 65d1f2f

4 files changed

Lines changed: 402 additions & 9 deletions

File tree

packages/cli/src/adapters/token-storage.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const EMPTY_AUTH_CONTEXT: AuthContextState = {
5858
activeWorkspaceId: null,
5959
workspaces: {},
6060
};
61+
const UNKNOWN_WORKSPACE_NAME = "Unknown workspace";
6162

6263
export function getAuthContextFilePath(authFilePath: string): string {
6364
const extension = path.extname(authFilePath);
@@ -283,8 +284,8 @@ export class FileTokenStorage implements TokenStorage {
283284
: tokens.workspaceId;
284285
const name =
285286
typeof cached?.name === "string" && cached.name.trim().length > 0
286-
? cached.name.trim()
287-
: id;
287+
? workspaceDisplayName(cached.name.trim(), tokens.workspaceId)
288+
: UNKNOWN_WORKSPACE_NAME;
288289
const lastSeenAt =
289290
typeof cached?.lastSeenAt === "string" &&
290291
cached.lastSeenAt.trim().length > 0
@@ -301,6 +302,14 @@ export class FileTokenStorage implements TokenStorage {
301302
});
302303
}
303304

305+
async listWorkspaceTokens(): Promise<Tokens[]> {
306+
this.signal?.throwIfAborted();
307+
const credentials = await this.readCredentialsFromDisk();
308+
return credentials
309+
.map((credential) => storedCredentialToTokens(credential))
310+
.filter((tokens): tokens is Tokens => tokens !== null);
311+
}
312+
304313
async useWorkspace(workspaceRef: string): Promise<{
305314
previous: StoredAuthWorkspace | null;
306315
selected: StoredAuthWorkspace;
@@ -705,3 +714,7 @@ function workspaceMatchesRef(
705714
function stripWorkspacePrefix(value: string): string {
706715
return value.startsWith("wksp_") ? value.slice("wksp_".length) : value;
707716
}
717+
718+
function workspaceDisplayName(name: string, credentialWorkspaceId: string) {
719+
return name === credentialWorkspaceId ? UNKNOWN_WORKSPACE_NAME : name;
720+
}

packages/cli/src/controllers/auth.ts

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
createManagementApiSdk,
3+
type TokenStorage,
4+
type Tokens,
5+
} from "@prisma/management-api-sdk";
16
import {
27
FileTokenStorage,
38
type StoredAuthWorkspace,
@@ -8,7 +13,11 @@ import {
813
performLogout,
914
readAuthState,
1015
} from "../lib/auth/auth-ops";
11-
import { SERVICE_TOKEN_ENV_VAR } from "../lib/auth/client";
16+
import {
17+
CLIENT_ID,
18+
getApiBaseUrl,
19+
SERVICE_TOKEN_ENV_VAR,
20+
} from "../lib/auth/client";
1221
import {
1322
authRequiredError,
1423
usageError,
@@ -217,7 +226,11 @@ async function listRealAuthWorkspaces(
217226
context.runtime.env,
218227
context.runtime.signal,
219228
);
220-
const localWorkspaces = await storage.listWorkspaces();
229+
const localWorkspaces = await hydrateLocalAuthWorkspaces(
230+
context,
231+
storage,
232+
await storage.listWorkspaces(),
233+
);
221234

222235
if (rawServiceToken !== undefined) {
223236
const authState = await readAuthState(
@@ -279,6 +292,11 @@ async function useRealAuthWorkspace(
279292
context.runtime.env,
280293
context.runtime.signal,
281294
);
295+
await hydrateLocalAuthWorkspaces(
296+
context,
297+
storage,
298+
await storage.listWorkspaces(),
299+
);
282300

283301
try {
284302
const result = await storage.useWorkspace(workspaceRef);
@@ -316,6 +334,11 @@ async function logoutRealAuthWorkspace(
316334
context.runtime.env,
317335
context.runtime.signal,
318336
);
337+
await hydrateLocalAuthWorkspaces(
338+
context,
339+
storage,
340+
await storage.listWorkspaces(),
341+
);
319342

320343
try {
321344
const result = await storage.logoutWorkspace(workspaceRef);
@@ -399,6 +422,135 @@ async function selectWorkspaceSession(
399422
return selected.id;
400423
}
401424

425+
async function hydrateLocalAuthWorkspaces(
426+
context: CommandContext,
427+
storage: FileTokenStorage,
428+
workspaces: StoredAuthWorkspace[],
429+
): Promise<StoredAuthWorkspace[]> {
430+
const candidates = workspaces.filter(needsWorkspaceMetadataHydration);
431+
if (candidates.length === 0) return workspaces;
432+
433+
const tokensByCredentialWorkspaceId = new Map(
434+
(await storage.listWorkspaceTokens()).map((tokens) => [
435+
tokens.workspaceId,
436+
tokens,
437+
]),
438+
);
439+
let nextWorkspaces = workspaces;
440+
441+
for (const workspace of candidates) {
442+
const tokens = tokensByCredentialWorkspaceId.get(
443+
workspace.credentialWorkspaceId,
444+
);
445+
if (!tokens) continue;
446+
447+
const resolved = await resolveOAuthWorkspaceMetadata(
448+
context,
449+
tokens,
450+
);
451+
if (!resolved) continue;
452+
453+
await rememberResolvedWorkspaceMetadata(context, storage, tokens, resolved);
454+
nextWorkspaces = nextWorkspaces.map((candidate) =>
455+
candidate.credentialWorkspaceId === workspace.credentialWorkspaceId
456+
? {
457+
...candidate,
458+
id: resolved.id,
459+
name: resolved.name,
460+
lastSeenAt: new Date().toISOString(),
461+
}
462+
: candidate,
463+
);
464+
}
465+
466+
return nextWorkspaces;
467+
}
468+
469+
async function rememberResolvedWorkspaceMetadata(
470+
context: CommandContext,
471+
storage: FileTokenStorage,
472+
tokens: Tokens,
473+
resolved: { id: string; name: string },
474+
): Promise<void> {
475+
try {
476+
await storage.rememberWorkspace(tokens.workspaceId, resolved);
477+
} catch {
478+
context.runtime.signal?.throwIfAborted();
479+
}
480+
}
481+
482+
function needsWorkspaceMetadataHydration(workspace: StoredAuthWorkspace) {
483+
return (
484+
workspace.id === workspace.credentialWorkspaceId ||
485+
workspace.name === "Unknown workspace" ||
486+
workspace.name === workspace.credentialWorkspaceId
487+
);
488+
}
489+
490+
async function resolveOAuthWorkspaceMetadata(
491+
context: CommandContext,
492+
tokens: Tokens,
493+
): Promise<{ id: string; name: string } | null> {
494+
const refreshStorage = new FileTokenStorage(
495+
context.runtime.env,
496+
context.runtime.signal,
497+
{ activateOnSetTokens: false },
498+
);
499+
const tokenStorage = createSingleWorkspaceTokenStorage(refreshStorage, tokens);
500+
const sdk = createManagementApiSdk({
501+
clientId: CLIENT_ID,
502+
redirectUri: "http://localhost:0/auth/callback",
503+
tokenStorage,
504+
apiBaseUrl: getApiBaseUrl(context.runtime.env),
505+
});
506+
507+
try {
508+
const { data } = await sdk.client.GET("/v1/workspaces/{id}", {
509+
params: { path: { id: tokens.workspaceId } },
510+
signal: context.runtime.signal,
511+
});
512+
const id = stringOrNull(data?.data?.id) ?? tokens.workspaceId;
513+
const name = stringOrNull(data?.data?.name) ?? id;
514+
515+
if (id === tokens.workspaceId && name === tokens.workspaceId) {
516+
return null;
517+
}
518+
519+
return { id, name };
520+
} catch {
521+
context.runtime.signal?.throwIfAborted();
522+
return null;
523+
}
524+
}
525+
526+
function createSingleWorkspaceTokenStorage(
527+
storage: FileTokenStorage,
528+
initialTokens: Tokens,
529+
): TokenStorage {
530+
let currentTokens: Tokens | null = initialTokens;
531+
532+
return {
533+
getTokens: async () => currentTokens,
534+
setTokens: async (tokens) => {
535+
currentTokens = tokens;
536+
await storage.setTokens(tokens);
537+
},
538+
clearTokens: async () => {
539+
const tokens = currentTokens;
540+
currentTokens = null;
541+
if (tokens) {
542+
await storage.clearTokensIfCurrent(tokens);
543+
}
544+
},
545+
};
546+
}
547+
548+
function stringOrNull(value: unknown): string | null {
549+
return typeof value === "string" && value.trim().length > 0
550+
? value.trim()
551+
: null;
552+
}
553+
402554
function toAuthWorkspace(workspace: StoredAuthWorkspace): AuthWorkspace {
403555
return {
404556
id: workspace.id,

0 commit comments

Comments
 (0)