From c8ecd97b14f7270a9923a1e1042a0b3bb9aa5bfa Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 10:23:18 +0000 Subject: [PATCH] fix(app-registry): drop the erroneous /assets/ segment from clock's remoteEntry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The built-in `clock` app's remoteEntry has always been /apps/clock/assets/remoteEntry.js (008_same_origin_federated_remotes.ts, mirrored into builtins.ts) — but clock-app/vite.config.ts sets `assetsDir: ''` specifically so remoteEntry.js lands at the ROOT of the `base` path, and clock-app/nginx.conf agrees (`location = /remoteEntry.js`, not /assets/remoteEntry.js). 008's SAME_ORIGIN_REMOTES table used the same /assets/ path shape for both `fuzequality` (which genuinely nests under assets/, since its vite config never overrides Vite's default assetsDir) and `clock` (which does not) — conflating two apps that made different build choices. The result: the platform's own canonical federation reference app served a 200 (nginx's SPA fallback answering the unmatched path with index.html) instead of the real remoteEntry.js. - builtins.ts + the seed fixture: /apps/clock/remoteEntry.js (also drops the seed's stale absolute https://app.fuzefront.com/... form to match the same-origin relative form builtins.ts already uses). - New migration 010 re-points any already-registered `clock` row whose remote_url is exactly the known-wrong value (upsertBuiltin never touches an existing row, so the builtins.ts edit alone is inert on a live database) — mirrors 009's shape for the analogous fuzeagent fix. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv --- .../applications/src/app-registry/builtins.ts | 7 +- ...10_clock_remoteentry_assets_segment_fix.ts | 81 +++++++++ ...-010-clock-assets-segment-fix.unit.test.ts | 162 ++++++++++++++++++ .../seed/clock.manifest.json | 2 +- 4 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 backend/applications/src/migrations/010_clock_remoteentry_assets_segment_fix.ts create mode 100644 backend/applications/tests/migration-010-clock-assets-segment-fix.unit.test.ts diff --git a/backend/applications/src/app-registry/builtins.ts b/backend/applications/src/app-registry/builtins.ts index fd2894006..3d7d218be 100644 --- a/backend/applications/src/app-registry/builtins.ts +++ b/backend/applications/src/app-registry/builtins.ts @@ -70,7 +70,12 @@ const BUILTIN_MANIFESTS: unknown[] = [ type: 'module-federation', // Same-origin path, not an absolute app-host URL: identical value works on // app.fuzefront.com, a tenant wildcard host, and localhost. - remoteEntry: '/apps/clock/assets/remoteEntry.js', + // NO `assets/` segment: clock-app/vite.config.ts sets `assetsDir: ''` + // (flat build output), so remoteEntry.js is served at the root of + // `/apps/clock/`, matching clock-app/nginx.conf's `location = /remoteEntry.js`. + // Existing rows are re-pointed by migration 010 (upsertBuiltin never + // touches an already-registered row, so this seed alone is inert on prod). + remoteEntry: '/apps/clock/remoteEntry.js', scope: 'clockApp', module: './ClockApp', }, diff --git a/backend/applications/src/migrations/010_clock_remoteentry_assets_segment_fix.ts b/backend/applications/src/migrations/010_clock_remoteentry_assets_segment_fix.ts new file mode 100644 index 000000000..b8ef6485b --- /dev/null +++ b/backend/applications/src/migrations/010_clock_remoteentry_assets_segment_fix.ts @@ -0,0 +1,81 @@ +import { Knex } from 'knex' + +/** + * Fixes an erroneous `/assets/` segment that 008_same_origin_federated_remotes.ts + * baked into the built-in `clock` app's remoteEntry: `/apps/clock/assets/remoteEntry.js`. + * + * WHY THIS IS WRONG: `clock-app/vite.config.ts` sets `assetsDir: ''` (flat + * build output) specifically so `remoteEntry.js` lands at the ROOT of the + * `base` path, not under a nested `assets/` directory — its own comment says + * so ("Output all chunks to dist/ directly (not dist/assets/) so + * remoteEntry.js is served at /apps/clock/remoteEntry.js"). `clock-app/nginx.conf` + * agrees: it declares `location = /remoteEntry.js` (exact match, no `assets/` + * prefix), not `location = /assets/remoteEntry.js`. 008's SAME_ORIGIN_REMOTES + * table conflated clock with fuzequality, which DOES nest under `assets/` + * (fuzequality/apps/web/vite.config.ts has no `assetsDir` override, so Vite's + * default `assets/` applies there) — but the two apps made different build + * choices, and the migration used one path shape for both. Result: remoteEntry.js + * 200s (nginx's `location /` SPA fallback serves index.html for the unmatched + * `/assets/remoteEntry.js` request) but the host's module loader gets HTML + * where it expected JS — the built-in reference app for federation was itself + * broken. + * + * WHY A MIGRATION IS REQUIRED, NOT JUST A builtins.ts EDIT: `upsertBuiltin` is + * `if (existing) return` — deliberately, so a seed rerun never clobbers + * operator state. `clock` is already registered (seeded by 008 with the wrong + * path), so the builtins.ts fix alone is inert on an existing database. + * + * Only rewrites a row whose remote_url is EXACTLY the known-wrong value 008 + * produced. A relative URL never matches 008/009's REWRITABLE_HOST regex + * (anchored on `http(s)://`), so this migration checks the literal wrong + * relative path instead — anything else (an operator customisation, or an + * already-correct row) is left alone. + */ + +const SLUG = 'clock' +const WRONG_ENTRY = '/apps/clock/assets/remoteEntry.js' +const CORRECT_ENTRY = '/apps/clock/remoteEntry.js' + +export async function up(knex: Knex): Promise { + const app = await knex('apps').where('slug', SLUG).first() + if (!app) return + + const current: string = app.remote_url ?? '' + if (current !== WRONG_ENTRY) { + console.log( + `[010] ${SLUG}: remote_url is not the known-wrong value, leaving untouched (${current || ''})` + ) + return + } + + let manifest: Record | null = null + try { + manifest = typeof app.manifest === 'string' ? JSON.parse(app.manifest) : app.manifest + } catch { + manifest = null + } + + const update: Record = { + remote_url: CORRECT_ENTRY, + url: CORRECT_ENTRY, + updated_at: new Date(), + } + + if (manifest?.integration) { + manifest.integration.remoteEntry = CORRECT_ENTRY + update.manifest = JSON.stringify(manifest) + } else { + console.log( + `[010] ${SLUG}: columns updated but manifest jsonb was unreadable — the host reads the manifest, so this row still needs a re-register` + ) + } + + await knex('apps').where('id', app.id).update(update) + console.log(`[010] ${SLUG}: ${current} → ${CORRECT_ENTRY}`) +} + +export async function down(_knex: Knex): Promise { + // Intentionally irreversible: the previous value is known-broken (nginx has + // no /assets/remoteEntry.js to serve it from). Rolling back would restore a + // URL that is known not to load. +} diff --git a/backend/applications/tests/migration-010-clock-assets-segment-fix.unit.test.ts b/backend/applications/tests/migration-010-clock-assets-segment-fix.unit.test.ts new file mode 100644 index 000000000..a23cac120 --- /dev/null +++ b/backend/applications/tests/migration-010-clock-assets-segment-fix.unit.test.ts @@ -0,0 +1,162 @@ +/** + * Unit tests for migrations/010_clock_remoteentry_assets_segment_fix.ts — + * corrects the erroneous `/assets/` segment 008_same_origin_federated_remotes.ts + * baked into the built-in `clock` app's remoteEntry. + * + * No DB, no network — pure unit tests using an in-memory fake knex builder + * that supports exactly the two call shapes the migration issues: + * knex('apps').where('slug', slug).first() + * knex('apps').where('id', id).update(patch) + */ + +import { up } from '../src/migrations/010_clock_remoteentry_assets_segment_fix' + +type AppRow = { + id: string + slug: string + remote_url: string | null + url: string | null + manifest: any + updated_at?: Date +} + +function makeFakeKnex(rows: AppRow[]) { + const table = new Map(rows.map((r) => [r.id, r])) + const updateCalls: Array<{ id: string; patch: Record }> = [] + + function knex(tableName: string) { + if (tableName !== 'apps') throw new Error(`unexpected table: ${tableName}`) + + return { + where(col: string, val: string) { + return { + first: async () => { + if (col !== 'slug') throw new Error(`unexpected where col in first(): ${col}`) + return [...table.values()].find((r) => r.slug === val) + }, + update: async (patch: Record) => { + if (col !== 'id') throw new Error(`unexpected where col in update(): ${col}`) + const row = table.get(val) + if (!row) return 0 + Object.assign(row, patch) + updateCalls.push({ id: val, patch }) + return 1 + }, + } + }, + } + } + + return { knex: knex as any, table, updateCalls } +} + +const WRONG_ENTRY = '/apps/clock/assets/remoteEntry.js' +const CORRECT_ENTRY = '/apps/clock/remoteEntry.js' + +function clockRow(overrides: Partial = {}): AppRow { + return { + id: 'app-1', + slug: 'clock', + remote_url: WRONG_ENTRY, + url: WRONG_ENTRY, + manifest: { + slug: 'clock', + integration: { type: 'module-federation', remoteEntry: WRONG_ENTRY, scope: 'clockApp' }, + }, + ...overrides, + } +} + +describe('migration 010: clock remoteEntry assets-segment fix', () => { + test('rewrites remote_url, url, and manifest.integration.remoteEntry for the known-wrong value', async () => { + const { knex, table, updateCalls } = makeFakeKnex([clockRow()]) + + await up(knex) + + const row = table.get('app-1')! + expect(row.remote_url).toBe(CORRECT_ENTRY) + expect(row.url).toBe(CORRECT_ENTRY) + const manifest = typeof row.manifest === 'string' ? JSON.parse(row.manifest) : row.manifest + expect(manifest.integration.remoteEntry).toBe(CORRECT_ENTRY) + expect(updateCalls).toHaveLength(1) + }) + + test('does not touch slug', async () => { + const { knex, table } = makeFakeKnex([clockRow()]) + await up(knex) + expect(table.get('app-1')!.slug).toBe('clock') + }) + + test('is a no-op when no clock row exists (fresh DB, builtins.ts seeds it directly)', async () => { + const { knex, updateCalls } = makeFakeKnex([]) + await expect(up(knex)).resolves.toBeUndefined() + expect(updateCalls).toHaveLength(0) + }) + + test('idempotent: re-running after the row is already correct is a no-op', async () => { + const { knex, table, updateCalls } = makeFakeKnex([ + clockRow({ remote_url: CORRECT_ENTRY, url: CORRECT_ENTRY }), + ]) + + await up(knex) + + expect(updateCalls).toHaveLength(0) + expect(table.get('app-1')!.remote_url).toBe(CORRECT_ENTRY) + }) + + test('leaves an operator-customised remote_url untouched', async () => { + const customUrl = '/apps/clock-canary/remoteEntry.js' + const { knex, table, updateCalls } = makeFakeKnex([ + clockRow({ remote_url: customUrl, url: customUrl }), + ]) + + await up(knex) + + expect(updateCalls).toHaveLength(0) + expect(table.get('app-1')!.remote_url).toBe(customUrl) + }) + + test('does not rewrite unrelated apps (only touches slug=clock)', async () => { + const { knex, table, updateCalls } = makeFakeKnex([ + { + id: 'app-2', + slug: 'fuzequality', + remote_url: '/apps/fuzequality/assets/remoteEntry.js', + url: '/apps/fuzequality/assets/remoteEntry.js', + manifest: null, + }, + ]) + + await up(knex) + + expect(updateCalls).toHaveLength(0) + expect(table.get('app-2')!.remote_url).toBe('/apps/fuzequality/assets/remoteEntry.js') + }) + + test('handles a manifest stored as a JSON string (jsonb round-trip) and updates it', async () => { + const { knex, table } = makeFakeKnex([ + clockRow({ + manifest: JSON.stringify({ + slug: 'clock', + integration: { type: 'module-federation', remoteEntry: WRONG_ENTRY, scope: 'clockApp' }, + }), + }), + ]) + + await up(knex) + + const row = table.get('app-1')! + const parsed = typeof row.manifest === 'string' ? JSON.parse(row.manifest) : row.manifest + expect(parsed.integration.remoteEntry).toBe(CORRECT_ENTRY) + }) + + test('rewrites columns but does not crash when manifest jsonb is unreadable', async () => { + const { knex, table } = makeFakeKnex([clockRow({ manifest: '{not valid json' })]) + + await expect(up(knex)).resolves.toBeUndefined() + + const row = table.get('app-1')! + expect(row.remote_url).toBe(CORRECT_ENTRY) + expect(row.url).toBe(CORRECT_ENTRY) + }) +}) diff --git a/services/app-registry-service/seed/clock.manifest.json b/services/app-registry-service/seed/clock.manifest.json index 29cb5f829..b34a2be35 100644 --- a/services/app-registry-service/seed/clock.manifest.json +++ b/services/app-registry-service/seed/clock.manifest.json @@ -9,7 +9,7 @@ "builtin": true, "integration": { "type": "module-federation", - "remoteEntry": "https://app.fuzefront.com/apps/clock/assets/remoteEntry.js", + "remoteEntry": "/apps/clock/remoteEntry.js", "scope": "clockApp", "module": "./ClockApp" },