From abcbca4f65c5fe5ac6b552da5dd3ae6798d980af Mon Sep 17 00:00:00 2001 From: Jose Gaston Date: Tue, 12 May 2026 19:34:36 -0700 Subject: [PATCH 1/3] feat(auth): accept optional features in prefetch() prefetch() relied on this.options.features which is only set by show(). Callers that prefetch before show() (e.g. folder hover prefetch) had no way to pass features into the viewer, so featureEnabled() checks in viewer prefetch() methods always read defaults. Add an optional features param to prefetch() and merge it into per-call viewer options without mutating this.options. --- src/lib/Preview.js | 9 +++++++ src/lib/__tests__/Preview-test.js | 45 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/lib/Preview.js b/src/lib/Preview.js index 0dc2651d9..1aafe1a00 100644 --- a/src/lib/Preview.js +++ b/src/lib/Preview.js @@ -693,6 +693,7 @@ class Preview extends EventEmitter { * @param {string} options.sharedLink - Shared link * @param {string} options.sharedLinkPassword - Shared link password * @param {boolean} options.preload - Is this prefetch for a preload + * @param {Object} [options.features] - Feature flags to merge into viewer options for this prefetch * @param {string} token - Access token * @return {void} */ @@ -705,6 +706,7 @@ class Preview extends EventEmitter { preload = false, isDocFirstPrefetchEnabled = false, docFirstPagesConfig = null, + features, }) { let file; let loader; @@ -747,6 +749,13 @@ class Preview extends EventEmitter { options.docFirstPagesConfig = docFirstPagesConfig; } + // If features are passed in for this prefetch call (e.g. from callers that + // invoke prefetch() before show() has set this.options.features), merge them + // into the viewer options for this call without mutating this.options. + if (features) { + options.features = { ...this.options.features, ...features }; + } + const viewerInstance = new viewer.CONSTRUCTOR(this.createViewerOptions(options)); if (typeof viewerInstance.prefetch === 'function') { viewerInstance.prefetch({ diff --git a/src/lib/__tests__/Preview-test.js b/src/lib/__tests__/Preview-test.js index 62533c92f..1232e08b0 100644 --- a/src/lib/__tests__/Preview-test.js +++ b/src/lib/__tests__/Preview-test.js @@ -798,6 +798,51 @@ describe('lib/Preview', () => { preview.prefetch({ fileId, token, sharedLink, sharedLinkPassword, preload: true }); }); + + test('should merge per-call features into viewer options when features are passed', () => { + jest.spyOn(loader, 'determineViewer').mockReturnValue(viewer); + jest.spyOn(preview, 'createViewerOptions'); + preview.options.features = { existingFeature: true }; + + preview.prefetch({ + fileId, + token, + sharedLink, + sharedLinkPassword, + features: { migrateAccessTokenToHeader: true }, + }); + + expect(preview.createViewerOptions).toHaveBeenCalledWith( + expect.objectContaining({ + features: { existingFeature: true, migrateAccessTokenToHeader: true }, + }), + ); + }); + + test('should not set features on viewer options when features are omitted', () => { + jest.spyOn(loader, 'determineViewer').mockReturnValue(viewer); + jest.spyOn(preview, 'createViewerOptions'); + + preview.prefetch({ fileId, token, sharedLink, sharedLinkPassword }); + + const callArgs = preview.createViewerOptions.mock.calls[0][0]; + expect(callArgs).not.toHaveProperty('features'); + }); + + test('should not mutate this.options.features when per-call features are passed', () => { + jest.spyOn(loader, 'determineViewer').mockReturnValue(viewer); + preview.options.features = { existingFeature: true }; + + preview.prefetch({ + fileId, + token, + sharedLink, + sharedLinkPassword, + features: { migrateAccessTokenToHeader: true }, + }); + + expect(preview.options.features).toEqual({ existingFeature: true }); + }); }); describe('prefetchViewers()', () => { From cb43514d27c739995ca23a0b9d4aa750888231e8 Mon Sep 17 00:00:00 2001 From: Jose Gaston Date: Tue, 12 May 2026 19:34:38 -0700 Subject: [PATCH 2/3] feat(auth): accept isAccessTokenHeaderEnabled in prefetch() prefetch() relied on this.options.features which is only set by show(). Callers that prefetch before show() (e.g. folder hover prefetch) had no way to opt into auth-header routing, so featureEnabled() checks in viewer prefetch() methods always read defaults. Add an optional isAccessTokenHeaderEnabled top-level param to prefetch() (matching the isDocFirstPrefetchEnabled shape). When set inside the preload branch, the viewer's options.features.migrateAccessTokenToHeader gets the threaded value without mutating this.options. --- src/lib/Preview.js | 14 +++++--------- src/lib/__tests__/Preview-test.js | 31 ++++++++++++------------------- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/src/lib/Preview.js b/src/lib/Preview.js index 1aafe1a00..ae8c455dc 100644 --- a/src/lib/Preview.js +++ b/src/lib/Preview.js @@ -693,7 +693,6 @@ class Preview extends EventEmitter { * @param {string} options.sharedLink - Shared link * @param {string} options.sharedLinkPassword - Shared link password * @param {boolean} options.preload - Is this prefetch for a preload - * @param {Object} [options.features] - Feature flags to merge into viewer options for this prefetch * @param {string} token - Access token * @return {void} */ @@ -706,7 +705,7 @@ class Preview extends EventEmitter { preload = false, isDocFirstPrefetchEnabled = false, docFirstPagesConfig = null, - features, + isAccessTokenHeaderEnabled = false, }) { let file; let loader; @@ -747,13 +746,10 @@ class Preview extends EventEmitter { options.sharedLinkPassword = sharedLinkPassword; options.isDocFirstPrefetchEnabled = isDocFirstPrefetchEnabled; options.docFirstPagesConfig = docFirstPagesConfig; - } - - // If features are passed in for this prefetch call (e.g. from callers that - // invoke prefetch() before show() has set this.options.features), merge them - // into the viewer options for this call without mutating this.options. - if (features) { - options.features = { ...this.options.features, ...features }; + options.features = { + ...this.options.features, + migrateAccessTokenToHeader: isAccessTokenHeaderEnabled, + }; } const viewerInstance = new viewer.CONSTRUCTOR(this.createViewerOptions(options)); diff --git a/src/lib/__tests__/Preview-test.js b/src/lib/__tests__/Preview-test.js index 1232e08b0..cd06f85e8 100644 --- a/src/lib/__tests__/Preview-test.js +++ b/src/lib/__tests__/Preview-test.js @@ -737,6 +737,7 @@ describe('lib/Preview', () => { sharedLinkPassword, isDocFirstPrefetchEnabled: false, docFirstPagesConfig: null, + features: { migrateAccessTokenToHeader: false }, }); }); @@ -799,7 +800,7 @@ describe('lib/Preview', () => { preview.prefetch({ fileId, token, sharedLink, sharedLinkPassword, preload: true }); }); - test('should merge per-call features into viewer options when features are passed', () => { + test('should pass migrateAccessTokenToHeader feature to viewer options when isAccessTokenHeaderEnabled is true', () => { jest.spyOn(loader, 'determineViewer').mockReturnValue(viewer); jest.spyOn(preview, 'createViewerOptions'); preview.options.features = { existingFeature: true }; @@ -809,7 +810,8 @@ describe('lib/Preview', () => { token, sharedLink, sharedLinkPassword, - features: { migrateAccessTokenToHeader: true }, + preload: true, + isAccessTokenHeaderEnabled: true, }); expect(preview.createViewerOptions).toHaveBeenCalledWith( @@ -817,30 +819,21 @@ describe('lib/Preview', () => { features: { existingFeature: true, migrateAccessTokenToHeader: true }, }), ); + expect(preview.options.features).toEqual({ existingFeature: true }); }); - test('should not set features on viewer options when features are omitted', () => { + test('should pass migrateAccessTokenToHeader=false to viewer options when isAccessTokenHeaderEnabled is false', () => { jest.spyOn(loader, 'determineViewer').mockReturnValue(viewer); jest.spyOn(preview, 'createViewerOptions'); - - preview.prefetch({ fileId, token, sharedLink, sharedLinkPassword }); - - const callArgs = preview.createViewerOptions.mock.calls[0][0]; - expect(callArgs).not.toHaveProperty('features'); - }); - - test('should not mutate this.options.features when per-call features are passed', () => { - jest.spyOn(loader, 'determineViewer').mockReturnValue(viewer); preview.options.features = { existingFeature: true }; - preview.prefetch({ - fileId, - token, - sharedLink, - sharedLinkPassword, - features: { migrateAccessTokenToHeader: true }, - }); + preview.prefetch({ fileId, token, sharedLink, sharedLinkPassword, preload: true }); + expect(preview.createViewerOptions).toHaveBeenCalledWith( + expect.objectContaining({ + features: { existingFeature: true, migrateAccessTokenToHeader: false }, + }), + ); expect(preview.options.features).toEqual({ existingFeature: true }); }); }); From 54b9c9b74667a189ec61d6e3d61ec7e3e44d0a49 Mon Sep 17 00:00:00 2001 From: Jose Gaston Date: Thu, 14 May 2026 13:07:07 -0400 Subject: [PATCH 3/3] chore: force push