Skip to content

Commit 9bb42c7

Browse files
authored
fix: Swap stored password for Matrix access token (CS-10725) (#4779)
1 parent 6faf4e8 commit 9bb42c7

9 files changed

Lines changed: 1082 additions & 498 deletions

File tree

packages/boxel-cli/src/commands/profile.ts

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ function validateUrl(input: string, label: string): string {
8080

8181
// Matches scripts/env-slug.sh: lowercase, "/" -> "-", strip chars outside
8282
// [a-z0-9-], collapse runs of "-", trim leading/trailing "-".
83-
function computeEnvSlug(name: string): string {
83+
export function computeEnvSlug(name: string): string {
8484
return name
8585
.toLowerCase()
8686
.replace(/\//g, '-')
@@ -91,7 +91,7 @@ function computeEnvSlug(name: string): string {
9191

9292
// Derive URLs from BOXEL_ENVIRONMENT using the same ".${slug}.localhost"
9393
// pattern that mise-tasks/lib/env-vars.sh produces for env-mode local dev.
94-
function resolveBoxelEnvironment(): EnvironmentDefaults | null {
94+
export function resolveBoxelEnvironment(): EnvironmentDefaults | null {
9595
const raw = process.env.BOXEL_ENVIRONMENT;
9696
if (!raw || !raw.trim()) return null;
9797
const slug = computeEnvSlug(raw);
@@ -458,14 +458,28 @@ async function addProfileNonInteractive(
458458
process.exit(1);
459459
}
460460

461-
if (manager.getProfile(matrixId)) {
462-
console.log(
463-
`${FG_YELLOW}Profile ${matrixId} already exists. Updating password.${RESET}`,
461+
const isUpdate = Boolean(manager.getProfile(matrixId));
462+
463+
// addProfile performs a real matrixLogin and persists the resulting
464+
// access token (the password never lands on disk). It also handles the
465+
// create-vs-reauth split uniformly: re-running it on an existing profile
466+
// refreshes the stored token while preserving cached realm tokens.
467+
try {
468+
await manager.addProfile(
469+
matrixId,
470+
password,
471+
displayName,
472+
matrixUrl,
473+
realmServerUrl,
464474
);
465-
await manager.updatePassword(matrixId, password);
466-
if (displayName) {
467-
manager.updateDisplayName(matrixId, displayName);
468-
}
475+
} catch (err) {
476+
console.error(
477+
`${FG_RED}Error:${RESET} ${err instanceof Error ? err.message : String(err)}`,
478+
);
479+
process.exit(1);
480+
}
481+
482+
if (isUpdate) {
469483
if (matrixUrl || realmServerUrl) {
470484
const urlsChanged = manager.updateUrls(matrixId, {
471485
matrixUrl,
@@ -483,20 +497,6 @@ async function addProfileNonInteractive(
483497
return;
484498
}
485499

486-
try {
487-
await manager.addProfile(
488-
matrixId,
489-
password,
490-
displayName,
491-
matrixUrl,
492-
realmServerUrl,
493-
);
494-
} catch (err) {
495-
console.error(
496-
`${FG_RED}Error:${RESET} ${err instanceof Error ? err.message : String(err)}`,
497-
);
498-
process.exit(1);
499-
}
500500
console.log(
501501
`${FG_GREEN}\u2713${RESET} Profile created: ${formatProfileBadge(matrixId)}`,
502502
);
@@ -538,7 +538,7 @@ async function migrateFromEnv(manager: ProfileManager): Promise<void> {
538538
);
539539
} else {
540540
console.log(
541-
`${FG_YELLOW}Profile ${formatProfileBadge(result.profileId)} already exists.${RESET} Password has been updated if it changed.`,
541+
`${FG_GREEN}\u2713${RESET} Refreshed profile: ${formatProfileBadge(result.profileId)}`,
542542
);
543543
console.log(
544544
`\n${DIM}Use 'boxel profile add -u ${result.profileId} -p <password>' to update other fields.${RESET}`,

packages/boxel-cli/src/lib/auth.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ export interface MatrixAuth {
77

88
export type RealmTokens = Record<string, string>;
99

10+
// Thrown when Matrix rejects an access token (401/403). Callers can catch
11+
// this specifically to drive interactive re-auth without parsing messages.
12+
export class MatrixAuthError extends Error {
13+
status: number;
14+
constructor(status: number, message: string) {
15+
super(message);
16+
this.name = 'MatrixAuthError';
17+
this.status = status;
18+
}
19+
}
20+
1021
interface MatrixLoginResponse {
1122
access_token: string;
1223
device_id: string;
@@ -69,6 +80,12 @@ async function getOpenIdToken(
6980

7081
if (!response.ok) {
7182
let text = await response.text();
83+
if (response.status === 401 || response.status === 403) {
84+
throw new MatrixAuthError(
85+
response.status,
86+
`OpenID token request failed: ${response.status} ${text}`,
87+
);
88+
}
7289
throw new Error(`OpenID token request failed: ${response.status} ${text}`);
7390
}
7491

@@ -138,17 +155,30 @@ function userRealmsAccountDataUrl(matrixAuth: MatrixAuth): string {
138155
export async function getUserRealmsFromMatrixAccountData(
139156
matrixAuth: MatrixAuth,
140157
): Promise<string[]> {
158+
let response: Response;
141159
try {
142-
let response = await fetch(userRealmsAccountDataUrl(matrixAuth), {
160+
response = await fetch(userRealmsAccountDataUrl(matrixAuth), {
143161
headers: { Authorization: `Bearer ${matrixAuth.accessToken}` },
144162
});
145-
if (!response.ok) {
146-
return [];
147-
}
163+
} catch {
164+
// Network unreachable / DNS / similar — treat as empty (best-effort).
165+
return [];
166+
}
167+
if (response.status === 401 || response.status === 403) {
168+
let text = await response.text();
169+
throw new MatrixAuthError(
170+
response.status,
171+
`Matrix account_data fetch failed: ${response.status} ${text}`,
172+
);
173+
}
174+
if (!response.ok) {
175+
// 404 just means the event has never been set — return empty list.
176+
return [];
177+
}
178+
try {
148179
let data = (await response.json()) as { realms?: string[] };
149180
return Array.isArray(data.realms) ? [...data.realms] : [];
150181
} catch {
151-
// Best-effort — treat unreachable account data as an empty list
152182
return [];
153183
}
154184
}
@@ -171,6 +201,12 @@ export async function addRealmToMatrixAccountData(
171201
});
172202
if (!putResponse.ok) {
173203
let text = await putResponse.text();
204+
if (putResponse.status === 401 || putResponse.status === 403) {
205+
throw new MatrixAuthError(
206+
putResponse.status,
207+
`Failed to update Matrix account data: ${putResponse.status} ${text}`,
208+
);
209+
}
174210
throw new Error(
175211
`Failed to update Matrix account data: ${putResponse.status} ${text}`,
176212
);
@@ -205,6 +241,12 @@ export async function removeRealmFromMatrixAccountData(
205241
});
206242
if (!putResponse.ok) {
207243
let text = await putResponse.text();
244+
if (putResponse.status === 401 || putResponse.status === 403) {
245+
throw new MatrixAuthError(
246+
putResponse.status,
247+
`Failed to update Matrix account data: ${putResponse.status} ${text}`,
248+
);
249+
}
208250
throw new Error(
209251
`Failed to update Matrix account data: ${putResponse.status} ${text}`,
210252
);

0 commit comments

Comments
 (0)