From 696dcc5758fe73374684056a2c9e9057a0bf68e6 Mon Sep 17 00:00:00 2001 From: Ben Peter Date: Tue, 14 Jul 2026 19:05:13 +0200 Subject: [PATCH 1/2] test(shared): local dev should default DA backends to stage On localhost the IMS token is always minted against IMS stage (da-nx env resolves localhost to 'dev' -> stage). da-live's own env resolver defaults localhost to prod, so a stage token hits prod admin.da.live -> 401 -> /not-found. Add a failing test asserting localhost defaults to stage-admin. --- test/unit/blocks/shared/constants.test.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/unit/blocks/shared/constants.test.js b/test/unit/blocks/shared/constants.test.js index 9b35f2add..d189513bf 100644 --- a/test/unit/blocks/shared/constants.test.js +++ b/test/unit/blocks/shared/constants.test.js @@ -35,6 +35,26 @@ describe('DA Admin', () => { }); }); +describe('Local dev defaults to stage', () => { + it('Defaults localhost to stage admin', () => { + localStorage.removeItem('da-admin'); + const env = getDaAdmin({ href: 'http://localhost:3000/' }); + expect(env).to.equal('https://stage-admin.da.live'); + }); + + it('Keeps prod default for non-local hosts', () => { + localStorage.removeItem('da-admin'); + const env = getDaAdmin({ href: 'https://da.live/' }); + expect(env).to.equal('https://admin.da.live'); + }); + + it('Honors an explicit override on localhost', () => { + const env = getDaAdmin({ href: 'http://localhost:3000/?da-admin=prod' }); + expect(env).to.equal('https://admin.da.live'); + getDaAdmin({ href: 'http://localhost:3000/?da-admin=reset' }); + }); +}); + describe('Other origins', () => { it('Sets DA Origin', () => { expect(DA_ORIGIN).to.equal('https://admin.da.live'); From 216d5cca547ca0c9aa7d82e8bc7df624960e8f64 Mon Sep 17 00:00:00 2001 From: Ben Peter Date: Tue, 14 Jul 2026 19:12:57 +0200 Subject: [PATCH 2/2] fix(shared): default DA admin to stage on localhost On localhost the IMS token is always minted against IMS stage (da-nx resolves a localhost host to env 'dev', which targets stage for both the token and nx2's DA_ADMIN). da-live's own env resolver defaulted localhost to prod, so the stage token hit prod admin.da.live, got a 401, and da-nx's daFetch redirected the browser to /not-found. Align da-live with da-nx by defaulting the da-admin backend to stage on localhost (content/collab/preview keep their existing prod default and the documented ?da-collab=stage override). An explicit ?da-admin= param still wins, and non-local hosts are unaffected. Update tests that derived URLs from the localhost DA_ORIGIN to build the expected value from DA_ORIGIN instead of hardcoding the prod host. --- blocks/shared/constants.js | 15 +++++--- test/unit/blocks/edit/prose/index.test.js | 5 ++- test/unit/blocks/shared/constants.test.js | 8 ++-- test/unit/blocks/shared/pathDetails.test.js | 41 +++++++++++---------- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/blocks/shared/constants.js b/blocks/shared/constants.js index a92261267..d7d329df3 100644 --- a/blocks/shared/constants.js +++ b/blocks/shared/constants.js @@ -42,15 +42,20 @@ const DA_ETC_ENVS = { local: 'http://localhost:8787', }; -function getDaEnv(location, key, envs) { +function getDaEnv(location, key, envs, localDefault = 'prod') { const { href } = location; - const query = new URL(href).searchParams.get(key); + const url = new URL(href); + const query = url.searchParams.get(key); if (query && query === 'reset') { localStorage.removeItem(key); } else if (query) { localStorage.setItem(key, query); } - const env = envs[localStorage.getItem(key) || 'prod']; + // On localhost the IMS token is always minted against IMS stage, so the DA + // admin default must be stage there too — otherwise a stage token hits prod + // admin, 401s, and da-nx redirects to /not-found. + const isLocal = url.hostname.includes('local'); + const env = envs[localStorage.getItem(key)] || envs[isLocal ? localDefault : 'prod'] || envs.prod; // TODO: INFRA return location.origin === 'https://da.page' ? env.replace('.live', '.page') : env; } @@ -59,12 +64,12 @@ export const getDaAdmin = (() => { let daAdmin; return (location) => { if (!location && daAdmin) return daAdmin; - daAdmin = getDaEnv(location || window.location, 'da-admin', DA_ADMIN_ENVS); + daAdmin = getDaEnv(location || window.location, 'da-admin', DA_ADMIN_ENVS, 'stage'); return daAdmin; }; })(); -export const DA_ORIGIN = (() => getDaEnv(window.location, 'da-admin', DA_ADMIN_ENVS))(); +export const DA_ORIGIN = (() => getDaEnv(window.location, 'da-admin', DA_ADMIN_ENVS, 'stage'))(); export const COLLAB_ORIGIN = (() => getDaEnv(window.location, 'da-collab', DA_COLLAB_ENVS))(); export const CON_ORIGIN = (() => getDaEnv(window.location, 'da-content', DA_CONTENT_ENVS))(); export const LIVE_PREVIEW_DOMAIN = (() => getDaEnv(window.location, 'da-live-preview', DA_LIVE_PREVIEW_ENVS))(); diff --git a/test/unit/blocks/edit/prose/index.test.js b/test/unit/blocks/edit/prose/index.test.js index a90500135..7156277ad 100644 --- a/test/unit/blocks/edit/prose/index.test.js +++ b/test/unit/blocks/edit/prose/index.test.js @@ -8,6 +8,7 @@ import initProse, { createAwarenessStatusWidget, } from '../../../../../blocks/edit/prose/index.js'; import { forceSave } from '../../../../../blocks/edit/prose/forcesave.js'; +import { DA_ORIGIN } from '../../../../../blocks/shared/constants.js'; // initProse lazily imports da-library.js, which (a) builds URLs from // `${getNx()}/...` and (b) calls loadLibrary() at module import time. @@ -98,8 +99,8 @@ describe('prose/index createConnection', () => { expect(result.wsProvider).to.exist; expect(result.ydoc).to.exist; expect(result.wsProvider.maxBackoffTime).to.equal(30000); - // Legacy (non-hlx6) docs keep the admin.da.live collab room. - expect(result.wsProvider.roomname).to.equal('https://admin.da.live/source/org/repo/page.html'); + // Legacy (non-hlx6) docs keep the DA admin collab room (stage on localhost). + expect(result.wsProvider.roomname).to.equal(`${DA_ORIGIN}/source/org/repo/page.html`); // Clean up the underlying WS connection result.wsProvider.disconnect({ data: 'Client navigation' }); result.wsProvider.destroy?.(); diff --git a/test/unit/blocks/shared/constants.test.js b/test/unit/blocks/shared/constants.test.js index d189513bf..6441dc123 100644 --- a/test/unit/blocks/shared/constants.test.js +++ b/test/unit/blocks/shared/constants.test.js @@ -16,7 +16,7 @@ import '../../milo.js'; describe('DA Admin', () => { it('Sets DA admin default', () => { const env = getDaAdmin(); - expect(env).to.equal('https://admin.da.live'); + expect(env).to.equal('https://stage-admin.da.live'); }); it('Sets DA admin stage', () => { @@ -29,9 +29,9 @@ describe('DA Admin', () => { expect(env).to.equal('https://stage-admin.da.live'); }); - it('Resets DA admin', () => { + it('Resets DA admin to the localhost default', () => { const env = getDaAdmin({ href: 'http://localhost:3000/?da-admin=reset' }); - expect(env).to.equal('https://admin.da.live'); + expect(env).to.equal('https://stage-admin.da.live'); }); }); @@ -57,7 +57,7 @@ describe('Local dev defaults to stage', () => { describe('Other origins', () => { it('Sets DA Origin', () => { - expect(DA_ORIGIN).to.equal('https://admin.da.live'); + expect(DA_ORIGIN).to.equal('https://stage-admin.da.live'); }); it('Sets Content Origin', () => { diff --git a/test/unit/blocks/shared/pathDetails.test.js b/test/unit/blocks/shared/pathDetails.test.js index 2d951da06..20e1b7ba6 100644 --- a/test/unit/blocks/shared/pathDetails.test.js +++ b/test/unit/blocks/shared/pathDetails.test.js @@ -1,5 +1,6 @@ import { expect } from '@esm-bundle/chai'; import '../../milo.js'; +import { DA_ORIGIN } from '../../../../blocks/shared/constants.js'; // Dynamic import because Milo dependency const { default: getPathDetails } = await import('../../../../blocks/shared/pathDetails.js'); @@ -66,10 +67,10 @@ describe('Path details', () => { it('Handles folder config (/)', () => { const loc = { pathname: '/config', hash: '#/adobe/' }; const details = getPathDetails(loc); - expect(details.origin).to.equal('https://admin.da.live'); + expect(details.origin).to.equal(DA_ORIGIN); expect(details.fullpath).to.equal('/adobe/'); expect(details.repo).to.equal(undefined); - expect(details.sourceUrl).to.equal('https://admin.da.live/config/adobe'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/config/adobe`); expect(details.name).to.equal('config'); expect(details.parent).to.equal('/adobe'); expect(details.parentName).to.equal('adobe'); @@ -79,7 +80,7 @@ describe('Path details', () => { const loc = { pathname: '/config', hash: '#/adobe.json' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe.json'); - expect(details.sourceUrl).to.equal('https://admin.da.live/config/adobe.json'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/config/adobe.json`); expect(details.parent).to.equal('/'); expect(details.parentName).to.equal('Root'); }); @@ -88,7 +89,7 @@ describe('Path details', () => { const loc = { pathname: '/config', hash: '#/adobe' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe.html'); - expect(details.sourceUrl).to.equal('https://admin.da.live/config/adobe.html'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/config/adobe.html`); }); }); @@ -97,7 +98,7 @@ describe('Path details', () => { const loc = { pathname: '/sheet', hash: '#/adobe' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe.json'); - expect(details.sourceUrl).to.equal('https://admin.da.live/source/adobe.json'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/source/adobe.json`); }); }); @@ -106,7 +107,7 @@ describe('Path details', () => { const loc = { pathname: '/edit', hash: '#/adobe' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe.html'); - expect(details.sourceUrl).to.equal('https://admin.da.live/source/adobe.html'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/source/adobe.html`); }); }); @@ -128,7 +129,7 @@ describe('Path details', () => { const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe/geometrixx/'); expect(details.repo).to.equal('geometrixx'); - expect(details.sourceUrl).to.equal('https://admin.da.live/config/adobe/geometrixx'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/config/adobe/geometrixx`); expect(details.name).to.equal('geometrixx config'); expect(details.parent).to.equal('/adobe/geometrixx'); expect(details.parentName).to.equal('geometrixx'); @@ -138,7 +139,7 @@ describe('Path details', () => { const loc = { pathname: '/config', hash: '#/adobe/geometrixx.json' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe/geometrixx.json'); - expect(details.sourceUrl).to.equal('https://admin.da.live/config/adobe/geometrixx.json'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/config/adobe/geometrixx.json`); expect(details.parent).to.equal('/adobe'); expect(details.parentName).to.equal('adobe'); }); @@ -147,7 +148,7 @@ describe('Path details', () => { const loc = { pathname: '/config', hash: '#/adobe/geometrixx' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe/geometrixx.html'); - expect(details.sourceUrl).to.equal('https://admin.da.live/config/adobe/geometrixx.html'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/config/adobe/geometrixx.html`); }); }); @@ -156,7 +157,7 @@ describe('Path details', () => { const loc = { pathname: '/sheet', hash: '#/adobe/geometrixx' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe/geometrixx.json'); - expect(details.sourceUrl).to.equal('https://admin.da.live/source/adobe/geometrixx.json'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/source/adobe/geometrixx.json`); }); }); @@ -165,7 +166,7 @@ describe('Path details', () => { const loc = { pathname: '/edit', hash: '#/adobe/geometrixx' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe/geometrixx.html'); - expect(details.sourceUrl).to.equal('https://admin.da.live/source/adobe/geometrixx.html'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/source/adobe/geometrixx.html`); }); }); }); @@ -177,7 +178,7 @@ describe('Path details', () => { const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe/geometrixx/testing-123/'); expect(details.repo).to.equal('geometrixx'); - expect(details.sourceUrl).to.equal('https://admin.da.live/config/adobe/geometrixx/testing-123'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/config/adobe/geometrixx/testing-123`); expect(details.name).to.equal('config'); expect(details.parent).to.equal('/adobe/geometrixx/testing-123'); expect(details.parentName).to.equal('testing-123'); @@ -187,7 +188,7 @@ describe('Path details', () => { const loc = { pathname: '/config', hash: '#/adobe/geometrixx/testing-123.json' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe/geometrixx/testing-123.json'); - expect(details.sourceUrl).to.equal('https://admin.da.live/config/adobe/geometrixx/testing-123.json'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/config/adobe/geometrixx/testing-123.json`); expect(details.parent).to.equal('/adobe/geometrixx'); expect(details.parentName).to.equal('geometrixx'); }); @@ -196,7 +197,7 @@ describe('Path details', () => { const loc = { pathname: '/config', hash: '#/adobe/geometrixx/testing-123' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe/geometrixx/testing-123.html'); - expect(details.sourceUrl).to.equal('https://admin.da.live/config/adobe/geometrixx/testing-123.html'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/config/adobe/geometrixx/testing-123.html`); }); }); @@ -205,7 +206,7 @@ describe('Path details', () => { const loc = { pathname: '/sheet', hash: '#/adobe/geometrixx/testing-123' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe/geometrixx/testing-123.json'); - expect(details.sourceUrl).to.equal('https://admin.da.live/source/adobe/geometrixx/testing-123.json'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/source/adobe/geometrixx/testing-123.json`); }); }); @@ -214,35 +215,35 @@ describe('Path details', () => { const loc = { pathname: '/edit', hash: '#/adobe/geometrixx/testing-123' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe/geometrixx/testing-123.html'); - expect(details.sourceUrl).to.equal('https://admin.da.live/source/adobe/geometrixx/testing-123.html'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/source/adobe/geometrixx/testing-123.html`); }); it('Handles HTML edit if page has .html extension ()', () => { const loc = { pathname: '/edit', hash: '#/adobe/geometrixx/testing-123.html' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe/geometrixx/testing-123.html'); - expect(details.sourceUrl).to.equal('https://admin.da.live/source/adobe/geometrixx/testing-123.html'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/source/adobe/geometrixx/testing-123.html`); }); it('Handles HTML edit if page name is html and no extension ()', () => { const loc = { pathname: '/edit', hash: '#/adobe/geometrixx/html' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe/geometrixx/html.html'); - expect(details.sourceUrl).to.equal('https://admin.da.live/source/adobe/geometrixx/html.html'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/source/adobe/geometrixx/html.html`); }); it('Handles HTML edit if page name is "ilikehtml" and no extension ()', () => { const loc = { pathname: '/edit', hash: '#/adobe/geometrixx/ilikehtml' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe/geometrixx/ilikehtml.html'); - expect(details.sourceUrl).to.equal('https://admin.da.live/source/adobe/geometrixx/ilikehtml.html'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/source/adobe/geometrixx/ilikehtml.html`); }); it('Handles HTML edit if page name is html and no extension and has .html extension ()', () => { const loc = { pathname: '/edit', hash: '#/adobe/geometrixx/html.html' }; const details = getPathDetails(loc); expect(details.fullpath).to.equal('/adobe/geometrixx/html.html'); - expect(details.sourceUrl).to.equal('https://admin.da.live/source/adobe/geometrixx/html.html'); + expect(details.sourceUrl).to.equal(`${DA_ORIGIN}/source/adobe/geometrixx/html.html`); }); }); });