Skip to content

Commit 6d437cb

Browse files
FadhlanRclaude
andcommitted
Address PR feedback: split authedFetch, fix token refresh and delete-only checkpoints
- Split authedFetch into authedRealmFetch and authedRealmServerFetch for clarity - Fix P1: handle expired server token during realm-auth prefetch gracefully so the 401 retry path still works - Fix P2: persist checkpoint after delete-only pulls (not just downloads) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7cf1540 commit 6d437cb

4 files changed

Lines changed: 101 additions & 41 deletions

File tree

packages/boxel-cli/src/commands/realm/create.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,16 @@ export async function createRealm(
6868

6969
let response: Response;
7070
try {
71-
response = await pm.authedFetch(`${realmServerUrl}/_create-realm`, {
72-
method: 'POST',
73-
headers: { 'Content-Type': 'application/vnd.api+json' },
74-
body: JSON.stringify({
75-
data: { type: 'realm', attributes },
76-
}),
77-
});
71+
response = await pm.authedRealmServerFetch(
72+
`${realmServerUrl}/_create-realm`,
73+
{
74+
method: 'POST',
75+
headers: { 'Content-Type': 'application/vnd.api+json' },
76+
body: JSON.stringify({
77+
data: { type: 'realm', attributes },
78+
}),
79+
},
80+
);
7881
} catch (e: unknown) {
7982
console.error(`Error: failed to connect to realm server`);
8083
console.error(e instanceof Error ? e.message : String(e));

packages/boxel-cli/src/commands/realm/pull.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class RealmPuller extends RealmSyncBase {
7171
}
7272
}
7373

74+
const deletedFiles: string[] = [];
7475
if (this.pullOptions.deleteLocal) {
7576
const filesToDelete = new Set(localFiles.keys());
7677
for (const relativePath of remoteFiles.keys()) {
@@ -105,6 +106,7 @@ class RealmPuller extends RealmSyncBase {
105106
const localPath = localFiles.get(relativePath);
106107
if (localPath) {
107108
await this.deleteLocalFile(localPath);
109+
deletedFiles.push(relativePath);
108110
console.log(` Deleted: ${relativePath}`);
109111
}
110112
} catch (error) {
@@ -121,6 +123,11 @@ class RealmPuller extends RealmSyncBase {
121123
file: f,
122124
status: 'modified' as const,
123125
}));
126+
if (deletedFiles.length > 0) {
127+
for (const f of deletedFiles) {
128+
pullChanges.push({ file: f, status: 'deleted' as const });
129+
}
130+
}
124131
const checkpoint = checkpointManager.createCheckpoint(
125132
'remote',
126133
pullChanges,
@@ -131,6 +138,22 @@ class RealmPuller extends RealmSyncBase {
131138
`\nCheckpoint created: ${checkpoint.shortHash} ${tag} ${checkpoint.message}`,
132139
);
133140
}
141+
} else if (!this.options.dryRun && deletedFiles.length > 0) {
142+
const checkpointManager = new CheckpointManager(this.options.localDir);
143+
const deleteChanges: CheckpointChange[] = deletedFiles.map((f) => ({
144+
file: f,
145+
status: 'deleted' as const,
146+
}));
147+
const checkpoint = checkpointManager.createCheckpoint(
148+
'remote',
149+
deleteChanges,
150+
);
151+
if (checkpoint) {
152+
const tag = checkpoint.isMajor ? '[MAJOR]' : '[minor]';
153+
console.log(
154+
`\nCheckpoint created: ${checkpoint.shortHash} ${tag} ${checkpoint.message}`,
155+
);
156+
}
134157
}
135158

136159
console.log('Pull completed');

packages/boxel-cli/src/lib/profile-manager.ts

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -377,25 +377,39 @@ export class ProfileManager {
377377
}
378378
}
379379

380-
private async resolveTokenForUrl(url: string): Promise<string> {
381-
// Check if URL matches a cached realm token
380+
private async getRealmTokenForUrl(url: string): Promise<string | undefined> {
382381
let realmToken = this.findRealmTokenForUrl(url);
383382
if (realmToken) {
384383
return realmToken;
385384
}
386385

387-
// Fetch all realm tokens and try again
388-
await this.fetchAndStoreAllRealmTokens();
389-
realmToken = this.findRealmTokenForUrl(url);
390-
if (realmToken) {
391-
return realmToken;
386+
try {
387+
await this.fetchAndStoreAllRealmTokens();
388+
} catch {
389+
// Token prefetch failed (e.g. expired server token) — caller will handle 401 retry
390+
return undefined;
392391
}
392+
return this.findRealmTokenForUrl(url);
393+
}
393394

394-
// Fall back to server token for server-level endpoints
395-
return this.getOrRefreshServerToken();
395+
private buildHeaders(
396+
input: string | URL | Request,
397+
init: RequestInit | undefined,
398+
token: string,
399+
): Headers {
400+
let baseHeaders =
401+
input instanceof Request ? new Headers(input.headers) : new Headers();
402+
let initHeaders = new Headers(init?.headers);
403+
for (let [key, value] of initHeaders) {
404+
baseHeaders.set(key, value);
405+
}
406+
if (!baseHeaders.has('Authorization')) {
407+
baseHeaders.set('Authorization', token);
408+
}
409+
return baseHeaders;
396410
}
397411

398-
async authedFetch(
412+
async authedRealmFetch(
399413
input: string | URL | Request,
400414
init?: RequestInit,
401415
): Promise<Response> {
@@ -405,30 +419,50 @@ export class ProfileManager {
405419
: input instanceof URL
406420
? input.href
407421
: input;
408-
let token = await this.resolveTokenForUrl(url);
409-
let baseHeaders =
410-
input instanceof Request ? new Headers(input.headers) : new Headers();
411-
let initHeaders = new Headers(init?.headers);
412-
for (let [key, value] of initHeaders) {
413-
baseHeaders.set(key, value);
422+
423+
let token = await this.getRealmTokenForUrl(url);
424+
if (token) {
425+
let headers = this.buildHeaders(input, init, token);
426+
let response = await fetch(input, { ...init, headers });
427+
428+
if (response.status !== 401) {
429+
return response;
430+
}
414431
}
415-
if (!baseHeaders.has('Authorization')) {
416-
baseHeaders.set('Authorization', token);
432+
433+
// Either no cached realm token (e.g. server token was expired during
434+
// prefetch) or the request got a 401. Refresh everything and retry.
435+
let active = this.getActiveProfile();
436+
if (active) {
437+
active.profile.realmTokens = {};
438+
active.profile.realmServerToken = undefined;
439+
this.saveConfig();
440+
}
441+
await this.fetchAndStoreAllRealmTokens();
442+
token = this.findRealmTokenForUrl(url);
443+
if (!token) {
444+
throw new Error(
445+
`No realm token available for ${url}. The realm may not be accessible.`,
446+
);
417447
}
448+
let headers = this.buildHeaders(input, init, token);
449+
let response = await fetch(input, { ...init, headers });
418450

419-
let response = await fetch(input, { ...init, headers: baseHeaders });
451+
return response;
452+
}
453+
454+
async authedRealmServerFetch(
455+
input: string | URL | Request,
456+
init?: RequestInit,
457+
): Promise<Response> {
458+
let token = await this.getOrRefreshServerToken();
459+
let headers = this.buildHeaders(input, init, token);
460+
let response = await fetch(input, { ...init, headers });
420461

421462
if (response.status === 401) {
422-
// Clear cached tokens and retry
423-
let active = this.getActiveProfile();
424-
if (active) {
425-
active.profile.realmTokens = {};
426-
active.profile.realmServerToken = undefined;
427-
this.saveConfig();
428-
}
429-
token = await this.resolveTokenForUrl(url);
430-
baseHeaders.set('Authorization', token);
431-
response = await fetch(input, { ...init, headers: baseHeaders });
463+
token = await this.refreshServerToken();
464+
headers = this.buildHeaders(input, init, token);
465+
response = await fetch(input, { ...init, headers });
432466
}
433467

434468
return response;

packages/boxel-cli/src/lib/realm-sync-base.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export abstract class RealmSyncBase {
8181
try {
8282
const url = this.buildDirectoryUrl(dir);
8383

84-
const response = await this.profileManager.authedFetch(url, {
84+
const response = await this.profileManager.authedRealmFetch(url, {
8585
headers: {
8686
Accept: 'application/vnd.api+json',
8787
},
@@ -149,7 +149,7 @@ export abstract class RealmSyncBase {
149149
try {
150150
const url = `${this.normalizedRealmUrl}_mtimes`;
151151

152-
const response = await this.profileManager.authedFetch(url, {
152+
const response = await this.profileManager.authedRealmFetch(url, {
153153
headers: {
154154
Accept: SupportedMimeType.Mtimes,
155155
},
@@ -297,7 +297,7 @@ export abstract class RealmSyncBase {
297297
const content = fs.readFileSync(localPath, 'utf8');
298298
const url = this.buildFileUrl(relativePath);
299299

300-
const response = await this.profileManager.authedFetch(url, {
300+
const response = await this.profileManager.authedRealmFetch(url, {
301301
method: 'POST',
302302
headers: {
303303
'Content-Type': 'text/plain;charset=UTF-8',
@@ -328,7 +328,7 @@ export abstract class RealmSyncBase {
328328

329329
const url = this.buildFileUrl(relativePath);
330330

331-
const response = await this.profileManager.authedFetch(url, {
331+
const response = await this.profileManager.authedRealmFetch(url, {
332332
headers: {
333333
Accept: SupportedMimeType.CardSource,
334334
},
@@ -366,7 +366,7 @@ export abstract class RealmSyncBase {
366366

367367
const url = this.buildFileUrl(relativePath);
368368

369-
const response = await this.profileManager.authedFetch(url, {
369+
const response = await this.profileManager.authedRealmFetch(url, {
370370
method: 'DELETE',
371371
headers: {
372372
Accept: SupportedMimeType.CardSource,

0 commit comments

Comments
 (0)