Skip to content

Commit b407dcf

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 b407dcf

4 files changed

Lines changed: 102 additions & 37 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: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -377,35 +377,26 @@ 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
}
393-
394-
// Fall back to server token for server-level endpoints
395-
return this.getOrRefreshServerToken();
392+
return this.findRealmTokenForUrl(url);
396393
}
397394

398-
async authedFetch(
395+
private buildHeaders(
399396
input: string | URL | Request,
400-
init?: RequestInit,
401-
): Promise<Response> {
402-
let url =
403-
input instanceof Request
404-
? input.url
405-
: input instanceof URL
406-
? input.href
407-
: input;
408-
let token = await this.resolveTokenForUrl(url);
397+
init: RequestInit | undefined,
398+
token: string,
399+
): Headers {
409400
let baseHeaders =
410401
input instanceof Request ? new Headers(input.headers) : new Headers();
411402
let initHeaders = new Headers(init?.headers);
@@ -415,20 +406,68 @@ export class ProfileManager {
415406
if (!baseHeaders.has('Authorization')) {
416407
baseHeaders.set('Authorization', token);
417408
}
409+
return baseHeaders;
410+
}
418411

419-
let response = await fetch(input, { ...init, headers: baseHeaders });
412+
async authedRealmFetch(
413+
input: string | URL | Request,
414+
init?: RequestInit,
415+
): Promise<Response> {
416+
let url =
417+
input instanceof Request
418+
? input.url
419+
: input instanceof URL
420+
? input.href
421+
: input;
422+
423+
let token = await this.getRealmTokenForUrl(url);
424+
if (!token) {
425+
throw new Error(
426+
`No realm token available for ${url}. The server token may have expired or the realm is not accessible.`,
427+
);
428+
}
429+
let headers = this.buildHeaders(input, init, token);
430+
let response = await fetch(input, { ...init, headers });
420431

421432
if (response.status === 401) {
422-
// Clear cached tokens and retry
423433
let active = this.getActiveProfile();
424434
if (active) {
425435
active.profile.realmTokens = {};
426436
active.profile.realmServerToken = undefined;
427437
this.saveConfig();
428438
}
429-
token = await this.resolveTokenForUrl(url);
430-
baseHeaders.set('Authorization', token);
431-
response = await fetch(input, { ...init, headers: baseHeaders });
439+
try {
440+
await this.fetchAndStoreAllRealmTokens();
441+
} catch {
442+
throw new Error(
443+
`Failed to refresh realm token for ${url}. The server token may have expired.`,
444+
);
445+
}
446+
token = this.findRealmTokenForUrl(url);
447+
if (!token) {
448+
throw new Error(
449+
`No realm token available for ${url} after refresh. The realm may not be accessible.`,
450+
);
451+
}
452+
headers = this.buildHeaders(input, init, token);
453+
response = await fetch(input, { ...init, headers });
454+
}
455+
456+
return response;
457+
}
458+
459+
async authedRealmServerFetch(
460+
input: string | URL | Request,
461+
init?: RequestInit,
462+
): Promise<Response> {
463+
let token = await this.getOrRefreshServerToken();
464+
let headers = this.buildHeaders(input, init, token);
465+
let response = await fetch(input, { ...init, headers });
466+
467+
if (response.status === 401) {
468+
token = await this.refreshServerToken();
469+
headers = this.buildHeaders(input, init, token);
470+
response = await fetch(input, { ...init, headers });
432471
}
433472

434473
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)