From 9ded612e7fb466549d4514e223273c252a27aa58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20J=C3=A4ckle?= Date: Wed, 9 Sep 2026 16:58:04 +0200 Subject: [PATCH 1/3] fix(ui): package the OIDC silent-callback page so it actually works silent-callback.html has never worked in any environment. It was added by c2eda4fa96 "support silent token refresh" together with the OIDC wiring in authorization.ts, but the packaging half was never done, and the page had two further defects that only a real browser would surface: 1. It never reaches the image. ui/Dockerfile copies only ./index.html and ./dist; the page sits at ui/ root, so it is copied by neither. Confirmed in a running eclipse/ditto-ui container - the web root holds index.html, 50x.html and dist/{main.css,main.js}, and a filesystem-wide search for *callback* returns nothing. 2. build.mjs could not have supplied it either. It has a single entry point (main.ts), and its `.html` loader is `text`, which inlines HTML imported from TypeScript rather than emitting standalone HTML. Nothing imported the page, so `COPY ./dist` had nothing to pick up. 3. The page could not run even if served. It did `import('oidc-client-ts')` - a bare ESM specifier, unresolvable without an import map, and the page had none. It also called `new UserManager()` with no argument, which throws: UserManagerSettingsStore dereferences `args.redirect_uri` before any defaulting. The only reference to the page anywhere is the environmentTemplates.json default, http://localhost:8000/silent-callback.html - the esbuild dev server, whose `servedir: '.'` made the file reachable during local development and masked all of the above. Fix all three: move the script into silent-callback.ts and add it to entryPoints so esbuild resolves and bundles oidc-client-ts, point the page at ./dist/silent-callback.js the way index.html points at ./dist/main.js, and copy the page in the Dockerfile. The placeholder settings in silent-callback.ts are deliberate. signinSilentCallback() delegates to IFrameNavigator.callback(), which reads nothing from the settings but the optional iframeNotifyParentOrigin; authority and client_id are required by UserManagerSettings yet never touched on this path. Passing an object at all is what avoids the constructor throw. Verified end to end: * `npm run build` (tsc -noEmit && node build.mjs) passes and emits dist/silent-callback.js (65 kB). * The bundle carries the real contract - the "oidc-client" message source, postMessage and keepOpen are all present - with 0 dynamic imports and 0 unresolved bare specifiers left. * Building ui/Dockerfile and serving the image returns 200 for both /silent-callback.html (text/html) and /dist/silent-callback.js (application/javascript), alongside the existing index.html and main.js. Note the renewal path itself is only exercised when no refresh token is present: UserManager.signinSilent() returns from its refresh-token branch whenever `user?.refresh_token` is set, so a client requesting offline_access never loads this page. That is why the breakage went unnoticed. Co-Authored-By: Claude Opus 5 --- ui/Dockerfile | 1 + ui/build.mjs | 2 +- ui/silent-callback.html | 24 +----------------------- ui/silent-callback.ts | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 24 deletions(-) create mode 100644 ui/silent-callback.ts diff --git a/ui/Dockerfile b/ui/Dockerfile index aae579baa67..7bc9fbc23bd 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -14,4 +14,5 @@ FROM nginxinc/nginx-unprivileged:alpine WORKDIR /usr/share/nginx/html COPY ./index.html . +COPY ./silent-callback.html . COPY ./dist ./dist diff --git a/ui/build.mjs b/ui/build.mjs index 9c504936a06..e2fe3b3c3b3 100644 --- a/ui/build.mjs +++ b/ui/build.mjs @@ -3,7 +3,7 @@ import * as esbuild from 'esbuild'; import {sassPlugin} from 'esbuild-sass-plugin'; const config = { - entryPoints: ['main.ts'], + entryPoints: ['main.ts', 'silent-callback.ts'], bundle: true, outdir: 'dist', loader: { diff --git a/ui/silent-callback.html b/ui/silent-callback.html index 1cf061774ad..e77fde2ec55 100644 --- a/ui/silent-callback.html +++ b/ui/silent-callback.html @@ -4,28 +4,6 @@ Silent Refresh Callback - + - - diff --git a/ui/silent-callback.ts b/ui/silent-callback.ts new file mode 100644 index 00000000000..686f7e62f68 --- /dev/null +++ b/ui/silent-callback.ts @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +import { UserManager, UserManagerSettings } from 'oidc-client-ts'; + +/* + * Entry point for silent-callback.html, the page oidc-client-ts loads in a hidden iframe as + * `silent_redirect_uri` when renewing an access token without a refresh token. + * + * The page's only job is to hand the response URL back to the parent frame; + * `signinSilentCallback()` delegates to IFrameNavigator.callback(), which reads nothing from + * the settings except the optional `iframeNotifyParentOrigin` (defaulting to this page's own + * origin). Hence the placeholder settings below: `authority` and `client_id` are required by + * UserManagerSettings but are never touched on this path. + * + * Passing an object at all is what matters - `new UserManager()` throws, because + * UserManagerSettingsStore dereferences `args.redirect_uri` before any defaulting. + */ +const callbackOnlySettings: UserManagerSettings = { + authority: '', + client_id: '', +}; + +new UserManager(callbackOnlySettings) + .signinSilentCallback() + .catch((error) => { + // Nothing is recoverable from inside the iframe: oidc-client-ts times the silent + // request out and raises a SilentRenewError on the UserManager that started it. + console.error('Silent refresh callback failed:', error); + }); From 4b97d55c228590a3f764248aebfc8eabcf338949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20J=C3=A4ckle?= Date: Wed, 9 Sep 2026 17:17:19 +0200 Subject: [PATCH 2/3] build(ui): add a .dockerignore so the build context is not the whole tree The image serves three things - index.html, silent-callback.html and dist/ - but with no .dockerignore the entire ui/ tree was sent to the daemon as build context, node_modules included. A three-COPY build took minutes. Allowlist rather than denylist, so adding a COPY without extending this list fails the build instead of silently depending on whatever happens to be lying around in the working tree. Build time drops from minutes to ~10s, and the image is byte-for-byte the same set of files: 50x.html, index.html, silent-callback.html and dist/{main.css, main.css.map, main.js, main.js.map, silent-callback.js} at identical sizes. Co-Authored-By: Claude Opus 5 --- ui/.dockerignore | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 ui/.dockerignore diff --git a/ui/.dockerignore b/ui/.dockerignore new file mode 100644 index 00000000000..1081cbd3e4e --- /dev/null +++ b/ui/.dockerignore @@ -0,0 +1,13 @@ +# The image serves static files only: the Dockerfile copies index.html, +# silent-callback.html and dist/, and nothing else is needed at build time. +# +# Without this file the whole ui/ tree is sent to the daemon as build context - +# node_modules alone dwarfs everything the image actually contains, which makes a +# three-COPY build take minutes. +# +# Allowlist rather than denylist: adding a COPY without extending this list fails +# the build instead of silently depending on whatever happens to be lying around. +* +!index.html +!silent-callback.html +!dist From 1a9269346859c631348d4ef198699ac70e5d05b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20J=C3=A4ckle?= Date: Wed, 30 Sep 2026 16:41:10 +0200 Subject: [PATCH 3/3] fix(ui): type-check silent-callback.ts and address review notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add silent-callback.ts to tsconfig "include" so `tsc -noEmit` checks it, and add the then-required `redirect_uri` placeholder (TS2741) - document that hard-coded settings mean `iframeNotifyParentOrigin` is not applied, so the silent callback must be served from the UI's origin - exclude dist/**/*.map from the Docker build context so local builds do not ship stale source maps from `npm start` Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Thomas Jäckle --- ui/.dockerignore | 4 ++++ ui/silent-callback.ts | 9 +++++++-- ui/tsconfig.json | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ui/.dockerignore b/ui/.dockerignore index 1081cbd3e4e..dcb7653c9c6 100644 --- a/ui/.dockerignore +++ b/ui/.dockerignore @@ -11,3 +11,7 @@ !index.html !silent-callback.html !dist + +# Source maps are only produced by `npm start` (esbuild serve); keep stale ones left in a +# local dist/ out of locally built images. +dist/**/*.map diff --git a/ui/silent-callback.ts b/ui/silent-callback.ts index 686f7e62f68..11918ba5b88 100644 --- a/ui/silent-callback.ts +++ b/ui/silent-callback.ts @@ -19,8 +19,12 @@ import { UserManager, UserManagerSettings } from 'oidc-client-ts'; * The page's only job is to hand the response URL back to the parent frame; * `signinSilentCallback()` delegates to IFrameNavigator.callback(), which reads nothing from * the settings except the optional `iframeNotifyParentOrigin` (defaulting to this page's own - * origin). Hence the placeholder settings below: `authority` and `client_id` are required by - * UserManagerSettings but are never touched on this path. + * origin). Hence the placeholder settings below: `authority`, `client_id` and `redirect_uri` are + * required by UserManagerSettings but are never touched on this path. + * + * Because the settings are hard-coded, a provider's configured `iframeNotifyParentOrigin` is not + * applied here, so this page only works when served from the same origin as the UI itself (as + * the Docker image does). A `silent_redirect_uri` on a different origin is not supported. * * Passing an object at all is what matters - `new UserManager()` throws, because * UserManagerSettingsStore dereferences `args.redirect_uri` before any defaulting. @@ -28,6 +32,7 @@ import { UserManager, UserManagerSettings } from 'oidc-client-ts'; const callbackOnlySettings: UserManagerSettings = { authority: '', client_id: '', + redirect_uri: '', }; new UserManager(callbackOnlySettings) diff --git a/ui/tsconfig.json b/ui/tsconfig.json index cfe06c895bb..a4979b77142 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -7,5 +7,5 @@ "resolveJsonModule": true, "allowSyntheticDefaultImports": true }, - "include": ["main.ts", "./custom.d.ts", "./modules/**/*", "__tests__/utils"] + "include": ["main.ts", "silent-callback.ts", "./custom.d.ts", "./modules/**/*", "__tests__/utils"] } \ No newline at end of file