@edynamix/mf-isolation 0.0.19 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -130,7 +130,7 @@ const localHostNavigationUrl = new URL(
130
130
  ).href;
131
131
  ```
132
132
 
133
- `hostNavigationUrl` selects the host environment. Load the browser entry from `localHostNavigationUrl`; the BFF serves the selected bundle and its host-owned logo assets through the secure local origin, avoiding mixed-content failures when the selected host is an HTTP localhost instance. Host API calls go to the selected host origin, while module API calls go to the explicit `moduleApiOrigin`. The BFF performs OIDC authorization code with PKCE, retains tokens and module API cookies in the development process, and forwards authenticated API requests without exposing either to module JavaScript. It also forwards the host-validated navigation selection through `X-Edyn-Navigation-Group-Id` and `X-Edyn-Navigation-Dealer-Id`; module backends must still authorize those values before constructing module-specific session state.
133
+ `hostNavigationUrl` selects the host environment. Load the browser entry from `localHostNavigationUrl`; the BFF serves the selected bundle and its host-owned logo assets through the secure local origin, avoiding mixed-content failures when the selected host is an HTTP localhost instance. Host API calls go to the selected host origin, while module API calls go to the explicit `moduleApiOrigin`. The BFF performs OIDC authorization code with PKCE, retains tokens and module API cookies in the development process, and forwards authenticated API requests without exposing either to module JavaScript. The refresh token is also kept in an HttpOnly `__Host-edyn_dev_refresh` cookie for the local origin, so one sign-in survives dev server restarts and switching between modules until the token expires. Host API requests keep the browser's navigation selection headers, which the host validates. Module API requests instead carry the selection the host last confirmed, read from `/api/navigation` responses and navigation stream events, through `X-Edyn-Navigation-Group-Id`, `X-Edyn-Navigation-Dealer-Ids` and, for a single dealer, `X-Edyn-Navigation-Dealer-Id`; module backends must still authorize those values before constructing module-specific session state. Request bodies are forwarded with a fixed length (up to 100 MB), because deployed module front ends reject chunked bodies.
134
134
 
135
135
  ## Angular CDK overlays
136
136
 
@@ -1 +1 @@
1
- {"version":3,"file":"vite-development.d.ts","sourceRoot":"","sources":["../src/vite-development.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAQnC,eAAO,MAAM,mCAAmC,sCAAsC,CAAC;AAyEvF,MAAM,WAAW,8BAA8B;IAC7C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACvB;AA0sBD,eAAO,MAAM,sBAAsB,GAAI,SAAS,8BAA8B,KAAG,MAmBhF,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,CAAC;AAExI,eAAO,MAAM,gCAAgC,GAAI,SAAS,8BAA8B,KAAG,2BAK1F,CAAC","sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { Readable } from 'node:stream';\nimport { createRemoteJWKSet, jwtVerify } from 'jose';\nimport type { Plugin } from 'vite';\n\nconst SESSION_COOKIE = '__Host-edyn_dev_session';\nconst FLOW_COOKIE = '__Host-edyn_oidc_flow';\nconst LOGOUT_COOKIE = '__Host-edyn_oidc_logout';\nconst CALLBACK_PATH = '/callback';\nconst LOGGED_OUT_PATH = '/logged-out';\nconst LOGIN_PATH = '/api/dev-auth/login';\nexport const MF_DEVELOPMENT_HOST_NAVIGATION_PATH = '/api/dev-host-navigation/index.js';\nconst HOST_ICON_PATH = '/exsto-icon.svg';\nconst HOST_NAVIGATION_ASSET_PATHS = new Set([HOST_ICON_PATH, '/exsto-text.svg']);\nconst ICON_LINK_PATTERN = /<link\\b[^>]*\\brel=[\"'](?:shortcut\\s+)?icon[\"'][^>]*>\\s*/gi;\nconst MAX_HOST_NAVIGATION_BYTES = 5 * 1024 * 1024;\nconst MAX_HOST_NAVIGATION_ASSET_BYTES = 512 * 1024;\nconst SESSION_MAX_AGE_SECONDS = 8 * 60 * 60;\nconst FLOW_MAX_AGE_SECONDS = 10 * 60;\nconst TOKEN_REFRESH_SKEW_MS = 30_000;\nconst NAVIGATION_GROUP_HEADER = 'X-Edyn-Navigation-Group-Id';\nconst NAVIGATION_DEALER_HEADER = 'X-Edyn-Navigation-Dealer-Id';\nconst COOKIE_NAME_PATTERN = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\nconst COOKIE_VALUE_PATTERN = /^[\\x20-\\x7e]*$/;\nconst ALLOWED_HOST_API_PATHS = [\n '/api/auth/me',\n '/api/navigation',\n '/api/navigation/access-check',\n '/api/navigation/access-request',\n '/api/navigation/counter-stream',\n] as const;\n\ninterface IOidcClientConfiguration {\n clientId: string;\n issuer: string;\n scope: string;\n}\n\ninterface IOidcDiscovery {\n authorizationEndpoint: string;\n endSessionEndpoint: string;\n issuer: string;\n jwksUri: string;\n revocationEndpoint: string | null;\n tokenEndpoint: string;\n}\n\ninterface IOidcFlow {\n codeVerifier: string;\n createdAt: number;\n nonce: string;\n returnTo: string;\n state: string;\n}\n\ninterface IModuleNavigationContext {\n dealerId: number;\n groupId: number;\n}\n\ninterface ITokenSession {\n accessToken: string;\n expiresAt: number;\n idToken: string;\n moduleBootstrapTail: Promise<void>;\n moduleCookies: Map<string, string>;\n moduleNavigationContext: IModuleNavigationContext | null;\n refreshPromise?: Promise<void>;\n refreshToken: string | null;\n}\n\ninterface ILogoutFlow {\n createdAt: number;\n idToken: string | null;\n}\n\ninterface ITokenResponse {\n accessToken: string;\n expiresIn: number;\n idToken: string | null;\n refreshToken: string | null;\n tokenType: string;\n}\n\nexport interface IMfDevelopmentBffPluginOptions {\n developmentOrigin?: string;\n hostNavigationUrl: string;\n moduleApiOrigin: string;\n moduleApiPath: string;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst requiredString = (value: Record<string, unknown>, key: string): string => {\n const candidate = value[key];\n if (typeof candidate !== 'string' || candidate.trim() === '') {\n throw new Error(`OIDC response is missing ${key}.`);\n }\n return candidate;\n};\n\nconst optionalString = (value: Record<string, unknown>, key: string): string | null => {\n const candidate = value[key];\n if (candidate === undefined || candidate === null) {\n return null;\n }\n if (typeof candidate !== 'string' || candidate.trim() === '') {\n throw new Error(`OIDC response contains an invalid ${key}.`);\n }\n return candidate;\n};\n\nconst absoluteHttpsUrl = (candidate: string, label: string, allowLocalHttp = false): URL => {\n let url: URL;\n try {\n url = new URL(candidate);\n } catch {\n throw new Error(`${label} must be an absolute URL.`);\n }\n const isLocalHttp = allowLocalHttp && url.protocol === 'http:' && (url.hostname === 'localhost' || url.hostname === '127.0.0.1');\n if (url.protocol !== 'https:' && !isLocalHttp) {\n throw new Error(`${label} must use HTTPS.`);\n }\n if (url.username || url.password || url.hash) {\n throw new Error(`${label} must not contain credentials or a fragment.`);\n }\n return url;\n};\n\nconst moduleApiPrefix = (candidate: string): string => {\n if (!/^\\/[a-z][a-z0-9-]*\\/api\\/$/.test(candidate)) {\n throw new Error('moduleApiPath must use the canonical /module/api/ boundary.');\n }\n return candidate;\n};\n\nconst base64Url = (size = 32): string => randomBytes(size).toString('base64url');\nconst codeChallengeOf = (verifier: string): string => createHash('sha256').update(verifier).digest('base64url');\n\nconst cookiesOf = (request: IncomingMessage): ReadonlyMap<string, string> => {\n const cookies = new Map<string, string>();\n for (const part of (request.headers.cookie ?? '').split(';')) {\n const separator = part.indexOf('=');\n if (separator <= 0) {\n continue;\n }\n const name = part.slice(0, separator).trim();\n const value = part.slice(separator + 1).trim();\n if (name && value) {\n cookies.set(name, value);\n }\n }\n return cookies;\n};\n\nconst cookie = (name: string, value: string, maxAge: number): string => `${name}=${value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`;\n\nconst clearCookie = (name: string): string => cookie(name, '', 0);\n\nconst writeJson = (response: ServerResponse, status: number, body: unknown): void => {\n response.statusCode = status;\n response.setHeader('Cache-Control', 'no-store');\n response.setHeader('Content-Type', 'application/json; charset=utf-8');\n response.end(JSON.stringify(body));\n};\n\nconst redirect = (response: ServerResponse, location: string, cookies: string[] = []): void => {\n response.statusCode = 302;\n response.setHeader('Cache-Control', 'no-store');\n response.setHeader('Location', location);\n if (cookies.length > 0) {\n response.setHeader('Set-Cookie', cookies);\n }\n response.end();\n};\n\nconst safeReturnTo = (candidate: string | null): string => {\n if (!candidate?.startsWith('/') || candidate.startsWith('//')) {\n return '/';\n }\n try {\n const parsed = new URL(candidate, 'https://localhost');\n return `${parsed.pathname}${parsed.search}${parsed.hash}`;\n } catch {\n return '/';\n }\n};\n\nconst parseTokenResponse = (value: unknown, requireIdToken: boolean): ITokenResponse => {\n if (!isRecord(value)) {\n throw new Error('OIDC token endpoint returned an invalid response.');\n }\n const expiresIn = value.expires_in;\n if (typeof expiresIn !== 'number' || !Number.isFinite(expiresIn) || expiresIn <= 0) {\n throw new Error('OIDC token endpoint returned an invalid expires_in value.');\n }\n const idToken = optionalString(value, 'id_token');\n if (requireIdToken && !idToken) {\n throw new Error('OIDC token endpoint did not return an ID token.');\n }\n const tokenType = requiredString(value, 'token_type');\n if (tokenType.toLowerCase() !== 'bearer') {\n throw new Error('OIDC token endpoint returned an unsupported token type.');\n }\n return {\n accessToken: requiredString(value, 'access_token'),\n expiresIn,\n idToken,\n refreshToken: optionalString(value, 'refresh_token'),\n tokenType,\n };\n};\n\nconst pathIsHostApi = (path: string): boolean => {\n return ALLOWED_HOST_API_PATHS.some((candidate) => path === candidate || path.startsWith(`${candidate}/`));\n};\n\nconst proxyHeaders = (\n request: IncomingMessage,\n accessToken: string,\n moduleCookies?: Map<string, string>,\n navigationContext?: IModuleNavigationContext | null,\n): Headers => {\n const headers = new Headers();\n for (const [name, value] of Object.entries(request.headers)) {\n if (\n !value ||\n name.startsWith(':') ||\n [\n 'authorization',\n 'connection',\n 'content-length',\n 'cookie',\n 'host',\n 'origin',\n 'transfer-encoding',\n NAVIGATION_GROUP_HEADER.toLowerCase(),\n NAVIGATION_DEALER_HEADER.toLowerCase(),\n ].includes(name.toLowerCase())\n ) {\n continue;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n headers.append(name, item);\n }\n } else {\n headers.set(name, value);\n }\n }\n headers.set('Authorization', `Bearer ${accessToken}`);\n if (moduleCookies?.size) {\n headers.set('Cookie', [...moduleCookies.values()].join('; '));\n }\n if (navigationContext) {\n headers.set(NAVIGATION_GROUP_HEADER, String(navigationContext.groupId));\n headers.set(NAVIGATION_DEALER_HEADER, String(navigationContext.dealerId));\n }\n return headers;\n};\n\nconst navigationContextIn = (value: unknown): IModuleNavigationContext | null => {\n if (!isRecord(value) || !isRecord(value.context)) {\n return null;\n }\n const groupId = value.context.selectedGroupId;\n const dealerId = value.context.selectedDealerId;\n if (!Number.isSafeInteger(groupId) || (groupId as number) <= 0 || !Number.isSafeInteger(dealerId) || (dealerId as number) < 0) {\n return null;\n }\n return { dealerId: dealerId as number, groupId: groupId as number };\n};\n\nconst updateModuleCookies = (cookies: Map<string, string>, headers: Headers): void => {\n const values = headers.getSetCookie();\n for (const value of values) {\n if (value.length > 4096) {\n continue;\n }\n const [pair, ...attributes] = value.split(';');\n const separator = pair?.indexOf('=') ?? -1;\n if (!pair || separator <= 0) {\n continue;\n }\n const name = pair.slice(0, separator).trim();\n const cookieValue = pair.slice(separator + 1).trim();\n if (!COOKIE_NAME_PATTERN.test(name) || !COOKIE_VALUE_PATTERN.test(cookieValue) || cookieValue.includes(';')) {\n continue;\n }\n const deleted = cookieValue === '' || attributes.some((attribute) => /^\\s*max-age\\s*=\\s*0\\s*$/i.test(attribute));\n if (deleted) {\n cookies.delete(name);\n continue;\n }\n if (!cookies.has(name) && cookies.size >= 32) {\n continue;\n }\n cookies.set(name, `${name}=${cookieValue}`);\n }\n};\n\nconst copyProxyResponse = async (upstream: Response, response: ServerResponse): Promise<void> => {\n response.statusCode = upstream.status;\n for (const [name, value] of upstream.headers) {\n if (!['connection', 'content-encoding', 'content-length', 'set-cookie', 'transfer-encoding'].includes(name.toLowerCase())) {\n response.setHeader(name, value);\n }\n }\n response.setHeader('Cache-Control', upstream.headers.get('cache-control') ?? 'no-store');\n if (!upstream.body) {\n response.end();\n return;\n }\n await new Promise<void>((resolve, reject) => {\n const readable = Readable.fromWeb(upstream.body as never);\n readable.once('error', reject);\n response.once('error', reject);\n response.once('finish', resolve);\n readable.pipe(response);\n });\n};\n\nconst readResponseBody = async (response: Response, maximumBytes: number, label: string): Promise<Buffer> => {\n const declaredLength = Number(response.headers.get('content-length'));\n if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {\n await response.body?.cancel();\n throw new Error(`${label} exceeded the configured size limit.`);\n }\n if (!response.body) {\n return Buffer.alloc(0);\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let totalBytes = 0;\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n totalBytes += value.byteLength;\n if (totalBytes > maximumBytes) {\n await reader.cancel();\n throw new Error(`${label} exceeded the configured size limit.`);\n }\n chunks.push(value);\n }\n } finally {\n reader.releaseLock();\n }\n return Buffer.concat(chunks, totalBytes);\n};\n\nclass MfDevelopmentBff {\n readonly #configurationUrl: URL;\n readonly #developmentOrigin: URL;\n readonly #flows = new Map<string, IOidcFlow>();\n readonly #hostOrigin: URL;\n readonly #hostNavigationUrl: URL;\n readonly #jwks = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n readonly #logoutFlows = new Map<string, ILogoutFlow>();\n readonly #moduleApiOrigin: URL;\n readonly #moduleApiPath: string;\n readonly #sessions = new Map<string, ITokenSession>();\n #configurationPromise: Promise<IOidcClientConfiguration> | null = null;\n #discoveryPromise: Promise<IOidcDiscovery> | null = null;\n\n constructor(options: IMfDevelopmentBffPluginOptions) {\n const hostNavigation = absoluteHttpsUrl(options.hostNavigationUrl, 'hostNavigationUrl', true);\n this.#hostNavigationUrl = hostNavigation;\n this.#hostOrigin = new URL(hostNavigation.origin);\n this.#configurationUrl = new URL('/api/auth/oidc-configuration', this.#hostOrigin);\n this.#developmentOrigin = absoluteHttpsUrl(options.developmentOrigin ?? 'https://localhost:4200', 'developmentOrigin');\n if (this.#developmentOrigin.pathname !== '/' || this.#developmentOrigin.search || this.#developmentOrigin.hash) {\n throw new Error('developmentOrigin must be an origin without a path, query, or fragment.');\n }\n this.#moduleApiOrigin = absoluteHttpsUrl(options.moduleApiOrigin, 'moduleApiOrigin', true);\n if (this.#moduleApiOrigin.pathname !== '/' || this.#moduleApiOrigin.search || this.#moduleApiOrigin.hash) {\n throw new Error('moduleApiOrigin must be an origin without a path, query, or fragment.');\n }\n this.#moduleApiPath = moduleApiPrefix(options.moduleApiPath);\n }\n\n async handle(request: IncomingMessage, response: ServerResponse, next: (error?: unknown) => void): Promise<void> {\n if (!request.url || !request.method) {\n next();\n return;\n }\n const url = new URL(request.url, this.#developmentOrigin);\n this.#prune();\n\n try {\n if (request.method === 'GET' && url.pathname === MF_DEVELOPMENT_HOST_NAVIGATION_PATH) {\n await this.#hostNavigationResource(response, this.#hostNavigationUrl, 'script');\n return;\n }\n if (request.method === 'GET' && HOST_NAVIGATION_ASSET_PATHS.has(url.pathname)) {\n await this.#hostNavigationResource(response, new URL(url.pathname, this.#hostOrigin), 'asset');\n return;\n }\n if (request.method === 'GET' && url.pathname === LOGIN_PATH) {\n await this.#login(request, response, safeReturnTo(url.searchParams.get('returnTo')));\n return;\n }\n if (request.method === 'GET' && url.pathname === CALLBACK_PATH) {\n await this.#callback(request, response, url);\n return;\n }\n if (request.method === 'GET' && url.pathname === LOGGED_OUT_PATH) {\n const returnTo = safeReturnTo(url.searchParams.get('state'));\n redirect(response, `${LOGIN_PATH}?returnTo=${encodeURIComponent(returnTo)}`, [clearCookie(LOGOUT_COOKIE)]);\n return;\n }\n if (request.method === 'GET' && url.pathname === '/frontchannel-logout') {\n const sessionId = cookiesOf(request).get(SESSION_COOKIE);\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n response.statusCode = 204;\n response.setHeader('Set-Cookie', clearCookie(SESSION_COOKIE));\n response.end();\n return;\n }\n if (request.method === 'POST' && url.pathname === '/api/auth/logout') {\n await this.#logout(request, response);\n return;\n }\n if (pathIsHostApi(url.pathname) || url.pathname.startsWith(this.#moduleApiPath)) {\n await this.#proxy(request, response, url);\n return;\n }\n next();\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Development authentication failed.';\n writeJson(response, 502, { code: 'development_authentication_failed', error: message });\n }\n }\n\n async #hostNavigationResource(response: ServerResponse, upstreamUrl: URL, kind: 'asset' | 'script'): Promise<void> {\n const upstream = await fetch(upstreamUrl, {\n headers: { Accept: kind === 'script' ? 'text/javascript, application/javascript' : 'image/svg+xml' },\n redirect: 'error',\n });\n if (!upstream.ok) {\n throw new Error(`Host navigation ${kind} failed (${upstream.status}).`);\n }\n const contentType = upstream.headers.get('content-type') ?? '';\n const validContentType =\n kind === 'script' ? /^(?:text|application)\\/javascript(?:;|$)/i.test(contentType) : /^image\\/svg\\+xml(?:;|$)/i.test(contentType);\n if (!validContentType) {\n throw new Error(`Host navigation ${kind} returned an invalid content type.`);\n }\n const maximumBytes = kind === 'script' ? MAX_HOST_NAVIGATION_BYTES : MAX_HOST_NAVIGATION_ASSET_BYTES;\n const body = await readResponseBody(upstream, maximumBytes, `Host navigation ${kind}`);\n response.statusCode = 200;\n response.setHeader('Cache-Control', 'no-store');\n response.setHeader('Content-Type', contentType);\n response.setHeader('X-Content-Type-Options', 'nosniff');\n response.end(body);\n }\n\n async #configuration(): Promise<IOidcClientConfiguration> {\n this.#configurationPromise ??= fetch(this.#configurationUrl, { headers: { Accept: 'application/json' }, redirect: 'error' })\n .then(async (response) => {\n if (!response.ok) {\n throw new Error(`Host OIDC configuration failed (${response.status}).`);\n }\n const value: unknown = await response.json();\n if (!isRecord(value)) {\n throw new Error('Host OIDC configuration returned an invalid response.');\n }\n const issuer = absoluteHttpsUrl(requiredString(value, 'issuer'), 'OIDC issuer', true).href.replace(/\\/$/, '');\n return { clientId: requiredString(value, 'clientId'), issuer, scope: requiredString(value, 'scope') };\n })\n .catch((error) => {\n this.#configurationPromise = null;\n throw error;\n });\n return this.#configurationPromise;\n }\n\n async #discovery(): Promise<IOidcDiscovery> {\n this.#discoveryPromise ??= this.#configuration()\n .then(async (configuration) => {\n const response = await fetch(`${configuration.issuer}/.well-known/openid-configuration`, {\n headers: { Accept: 'application/json' },\n redirect: 'error',\n });\n if (!response.ok) {\n throw new Error(`OIDC discovery failed (${response.status}).`);\n }\n const value: unknown = await response.json();\n if (!isRecord(value)) {\n throw new Error('OIDC discovery returned an invalid response.');\n }\n const issuer = absoluteHttpsUrl(requiredString(value, 'issuer'), 'OIDC discovery issuer', true).href.replace(/\\/$/, '');\n if (issuer !== configuration.issuer) {\n throw new Error('OIDC discovery issuer does not match the host configuration.');\n }\n const endpoint = (key: string): string => {\n const url = absoluteHttpsUrl(requiredString(value, key), `OIDC ${key}`, true);\n if (url.origin !== new URL(issuer).origin) {\n throw new Error(`OIDC ${key} must use the configured issuer origin.`);\n }\n return url.href;\n };\n const revocation = optionalString(value, 'revocation_endpoint');\n return {\n authorizationEndpoint: endpoint('authorization_endpoint'),\n endSessionEndpoint: endpoint('end_session_endpoint'),\n issuer,\n jwksUri: endpoint('jwks_uri'),\n revocationEndpoint: revocation ? endpoint('revocation_endpoint') : null,\n tokenEndpoint: endpoint('token_endpoint'),\n };\n })\n .catch((error) => {\n this.#discoveryPromise = null;\n throw error;\n });\n return this.#discoveryPromise;\n }\n\n async #login(request: IncomingMessage, response: ServerResponse, returnTo: string): Promise<void> {\n const logoutId = cookiesOf(request).get(LOGOUT_COOKIE);\n if (logoutId) {\n const logout = this.#logoutFlows.get(logoutId);\n this.#logoutFlows.delete(logoutId);\n if (logout) {\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const endpoint = new URL(discovery.endSessionEndpoint);\n endpoint.searchParams.set('client_id', configuration.clientId);\n endpoint.searchParams.set('post_logout_redirect_uri', new URL(LOGGED_OUT_PATH, this.#developmentOrigin).href);\n endpoint.searchParams.set('state', returnTo);\n if (logout.idToken) {\n endpoint.searchParams.set('id_token_hint', logout.idToken);\n }\n redirect(response, endpoint.href, [clearCookie(LOGOUT_COOKIE)]);\n return;\n }\n }\n\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const flowId = base64Url();\n const codeVerifier = base64Url(48);\n const flow: IOidcFlow = { codeVerifier, createdAt: Date.now(), nonce: base64Url(), returnTo, state: base64Url() };\n this.#flows.set(flowId, flow);\n\n const endpoint = new URL(discovery.authorizationEndpoint);\n endpoint.searchParams.set('response_type', 'code');\n endpoint.searchParams.set('client_id', configuration.clientId);\n endpoint.searchParams.set('redirect_uri', new URL(CALLBACK_PATH, this.#developmentOrigin).href);\n endpoint.searchParams.set('scope', configuration.scope);\n endpoint.searchParams.set('state', flow.state);\n endpoint.searchParams.set('nonce', flow.nonce);\n endpoint.searchParams.set('code_challenge', codeChallengeOf(codeVerifier));\n endpoint.searchParams.set('code_challenge_method', 'S256');\n redirect(response, endpoint.href, [cookie(FLOW_COOKIE, flowId, FLOW_MAX_AGE_SECONDS)]);\n }\n\n async #callback(request: IncomingMessage, response: ServerResponse, url: URL): Promise<void> {\n const flowId = cookiesOf(request).get(FLOW_COOKIE);\n const flow = flowId ? this.#flows.get(flowId) : undefined;\n if (flowId) {\n this.#flows.delete(flowId);\n }\n if (!flow || url.searchParams.get('state') !== flow.state) {\n writeJson(response, 400, { code: 'invalid_oidc_state', error: 'The OIDC callback state is invalid or expired.' });\n return;\n }\n const providerError = url.searchParams.get('error');\n if (providerError) {\n writeJson(response, 401, { code: providerError, error: url.searchParams.get('error_description') ?? 'Sign-in was rejected.' });\n return;\n }\n const code = url.searchParams.get('code');\n if (!code) {\n writeJson(response, 400, { code: 'missing_authorization_code', error: 'The OIDC callback did not include an authorization code.' });\n return;\n }\n\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const fields = new URLSearchParams({\n client_id: configuration.clientId,\n code,\n code_verifier: flow.codeVerifier,\n grant_type: 'authorization_code',\n redirect_uri: new URL(CALLBACK_PATH, this.#developmentOrigin).href,\n });\n const tokenResponse = await fetch(discovery.tokenEndpoint, {\n body: fields,\n headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },\n method: 'POST',\n redirect: 'error',\n });\n if (!tokenResponse.ok) {\n throw new Error(`OIDC token exchange failed (${tokenResponse.status}).`);\n }\n const tokens = parseTokenResponse(await tokenResponse.json(), true);\n const idToken = tokens.idToken;\n if (!idToken) {\n throw new Error('OIDC token endpoint did not return an ID token.');\n }\n const jwks = this.#jwks.get(discovery.jwksUri) ?? createRemoteJWKSet(new URL(discovery.jwksUri));\n this.#jwks.set(discovery.jwksUri, jwks);\n const verified = await jwtVerify(idToken, jwks, {\n audience: configuration.clientId,\n issuer: discovery.issuer,\n requiredClaims: ['sub', 'nonce'],\n });\n if (verified.payload.nonce !== flow.nonce) {\n throw new Error('OIDC ID token nonce does not match the authorization request.');\n }\n\n const sessionId = base64Url();\n this.#sessions.set(sessionId, {\n accessToken: tokens.accessToken,\n expiresAt: Date.now() + tokens.expiresIn * 1000,\n idToken,\n moduleBootstrapTail: Promise.resolve(),\n moduleCookies: new Map(),\n moduleNavigationContext: null,\n refreshToken: tokens.refreshToken,\n });\n redirect(response, flow.returnTo, [cookie(SESSION_COOKIE, sessionId, SESSION_MAX_AGE_SECONDS), clearCookie(FLOW_COOKIE)]);\n }\n\n async #logout(request: IncomingMessage, response: ServerResponse): Promise<void> {\n const sessionId = cookiesOf(request).get(SESSION_COOKIE);\n const session = sessionId ? this.#sessions.get(sessionId) : undefined;\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n if (session) {\n void this.#revoke(session).catch(() => undefined);\n }\n const logoutId = base64Url();\n this.#logoutFlows.set(logoutId, { createdAt: Date.now(), idToken: session?.idToken ?? null });\n response.setHeader('Set-Cookie', [clearCookie(SESSION_COOKIE), cookie(LOGOUT_COOKIE, logoutId, FLOW_MAX_AGE_SECONDS)]);\n writeJson(response, 200, { loggedOut: true });\n }\n\n async #revoke(session: ITokenSession): Promise<void> {\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const revocationEndpoint = discovery.revocationEndpoint;\n if (!revocationEndpoint) {\n return;\n }\n const revoke = async (token: string, hint: string): Promise<void> => {\n await fetch(revocationEndpoint, {\n body: new URLSearchParams({ client_id: configuration.clientId, token, token_type_hint: hint }),\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n method: 'POST',\n redirect: 'error',\n });\n };\n await Promise.all([\n revoke(session.accessToken, 'access_token'),\n session.refreshToken ? revoke(session.refreshToken, 'refresh_token') : Promise.resolve(),\n ]);\n }\n\n async #authenticatedSession(request: IncomingMessage): Promise<ITokenSession | null> {\n const sessionId = cookiesOf(request).get(SESSION_COOKIE);\n const session = sessionId ? this.#sessions.get(sessionId) : undefined;\n if (!session) {\n return null;\n }\n if (session.expiresAt > Date.now() + TOKEN_REFRESH_SKEW_MS) {\n return session;\n }\n if (!session.refreshToken) {\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n return null;\n }\n session.refreshPromise ??= this.#refresh(session).finally(() => {\n session.refreshPromise = undefined;\n });\n try {\n await session.refreshPromise;\n return session;\n } catch {\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n return null;\n }\n }\n\n async #refresh(session: ITokenSession): Promise<void> {\n const refreshToken = session.refreshToken;\n if (!refreshToken) {\n throw new Error('OIDC refresh token is unavailable.');\n }\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const response = await fetch(discovery.tokenEndpoint, {\n body: new URLSearchParams({\n client_id: configuration.clientId,\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n }),\n headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },\n method: 'POST',\n redirect: 'error',\n });\n if (!response.ok) {\n throw new Error(`OIDC refresh failed (${response.status}).`);\n }\n const tokens = parseTokenResponse(await response.json(), false);\n session.accessToken = tokens.accessToken;\n session.expiresAt = Date.now() + tokens.expiresIn * 1000;\n session.refreshToken = tokens.refreshToken ?? session.refreshToken;\n session.idToken = tokens.idToken ?? session.idToken;\n }\n\n async #proxy(request: IncomingMessage, response: ServerResponse, localUrl: URL): Promise<void> {\n const session = await this.#authenticatedSession(request);\n if (!session) {\n // This server refreshes the session's tokens itself, so the browser can only sign in again. A refreshable code\n // would send the host client to a refresh endpoint that only the host has.\n writeJson(response, 401, { code: 'sign_in_required', error: 'Sign in is required.' });\n return;\n }\n const isModuleApi = localUrl.pathname.startsWith(this.#moduleApiPath);\n if (isModuleApi && session.moduleCookies.size === 0) {\n const previous = session.moduleBootstrapTail;\n let release = (): void => {};\n const current = new Promise<void>((resolve) => {\n release = resolve;\n });\n session.moduleBootstrapTail = previous.catch(() => undefined).then(() => current);\n await previous.catch(() => undefined);\n try {\n await this.#proxyAuthenticated(request, response, localUrl, session, true);\n } finally {\n release();\n }\n return;\n }\n await this.#proxyAuthenticated(request, response, localUrl, session, isModuleApi);\n }\n\n async #proxyAuthenticated(\n request: IncomingMessage,\n response: ServerResponse,\n localUrl: URL,\n session: ITokenSession,\n isModuleApi: boolean,\n ): Promise<void> {\n const upstreamOrigin = isModuleApi ? this.#moduleApiOrigin : this.#hostOrigin;\n const upstreamUrl = new URL(`${localUrl.pathname}${localUrl.search}`, upstreamOrigin);\n const method = request.method ?? 'GET';\n const init: RequestInit & { duplex?: 'half' } = {\n headers: proxyHeaders(\n request,\n session.accessToken,\n isModuleApi ? session.moduleCookies : undefined,\n isModuleApi ? session.moduleNavigationContext : undefined,\n ),\n method,\n redirect: 'manual',\n };\n if (method !== 'GET' && method !== 'HEAD') {\n init.body = Readable.toWeb(request) as unknown as BodyInit;\n init.duplex = 'half';\n }\n const upstream = await fetch(upstreamUrl, init);\n if (isModuleApi) {\n updateModuleCookies(session.moduleCookies, upstream.headers);\n } else if (localUrl.pathname === '/api/navigation' && upstream.ok) {\n session.moduleNavigationContext = navigationContextIn(\n await upstream\n .clone()\n .json()\n .catch(() => null),\n );\n }\n await copyProxyResponse(upstream, response);\n }\n\n #prune(): void {\n const now = Date.now();\n for (const [id, flow] of this.#flows) {\n if (flow.createdAt + FLOW_MAX_AGE_SECONDS * 1000 < now) {\n this.#flows.delete(id);\n }\n }\n for (const [id, flow] of this.#logoutFlows) {\n if (flow.createdAt + FLOW_MAX_AGE_SECONDS * 1000 < now) {\n this.#logoutFlows.delete(id);\n }\n }\n for (const [id, session] of this.#sessions) {\n if (!session.refreshToken && session.expiresAt + TOKEN_REFRESH_SKEW_MS < now) {\n this.#sessions.delete(id);\n }\n }\n }\n}\n\nexport const mfDevelopmentBffPlugin = (options: IMfDevelopmentBffPluginOptions): Plugin => {\n const middleware = createMfDevelopmentBffMiddleware(options);\n return {\n apply: 'serve',\n name: 'edynamix-mf-development-bff',\n configureServer(server) {\n server.middlewares.use((request, response, next) => {\n middleware(request, response, next);\n });\n },\n // Locally the module runs inside the host shell, so its page shows the host's icon. The module's own favicon is\n // only copied into the build output, so this server has none to answer the browser's favicon request with.\n transformIndexHtml(html) {\n return {\n html: html.replace(ICON_LINK_PATTERN, ''),\n tags: [{ tag: 'link', attrs: { rel: 'icon', type: 'image/svg+xml', href: HOST_ICON_PATH }, injectTo: 'head' }],\n };\n },\n };\n};\n\nexport type TMfDevelopmentBffMiddleware = (request: IncomingMessage, response: ServerResponse, next: (error?: unknown) => void) => void;\n\nexport const createMfDevelopmentBffMiddleware = (options: IMfDevelopmentBffPluginOptions): TMfDevelopmentBffMiddleware => {\n const bff = new MfDevelopmentBff(options);\n return (request, response, next) => {\n void bff.handle(request, response, next);\n };\n};\n"]}
1
+ {"version":3,"file":"vite-development.d.ts","sourceRoot":"","sources":["../src/vite-development.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AASnC,eAAO,MAAM,mCAAmC,sCAAsC,CAAC;AAoFvF,MAAM,WAAW,8BAA8B;IAC7C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACvB;AA20BD,eAAO,MAAM,sBAAsB,GAAI,SAAS,8BAA8B,KAAG,MAmBhF,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,CAAC;AAExI,eAAO,MAAM,gCAAgC,GAAI,SAAS,8BAA8B,KAAG,2BAK1F,CAAC","sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { Readable } from 'node:stream';\nimport { createRemoteJWKSet, jwtVerify } from 'jose';\nimport type { Plugin } from 'vite';\n\nconst SESSION_COOKIE = '__Host-edyn_dev_session';\nconst REFRESH_COOKIE = '__Host-edyn_dev_refresh';\nconst FLOW_COOKIE = '__Host-edyn_oidc_flow';\nconst LOGOUT_COOKIE = '__Host-edyn_oidc_logout';\nconst CALLBACK_PATH = '/callback';\nconst LOGGED_OUT_PATH = '/logged-out';\nconst LOGIN_PATH = '/api/dev-auth/login';\nexport const MF_DEVELOPMENT_HOST_NAVIGATION_PATH = '/api/dev-host-navigation/index.js';\nconst HOST_ICON_PATH = '/exsto-icon.svg';\nconst HOST_NAVIGATION_ASSET_PATHS = new Set([HOST_ICON_PATH, '/exsto-text.svg']);\nconst ICON_LINK_PATTERN = /<link\\b[^>]*\\brel=[\"'](?:shortcut\\s+)?icon[\"'][^>]*>\\s*/gi;\nconst MAX_HOST_NAVIGATION_BYTES = 5 * 1024 * 1024;\nconst MAX_HOST_NAVIGATION_ASSET_BYTES = 512 * 1024;\nconst SESSION_MAX_AGE_SECONDS = 8 * 60 * 60;\nconst REFRESH_COOKIE_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;\nconst FLOW_MAX_AGE_SECONDS = 10 * 60;\nconst TOKEN_REFRESH_SKEW_MS = 30_000;\nconst NAVIGATION_GROUP_HEADER = 'X-Edyn-Navigation-Group-Id';\nconst NAVIGATION_DEALER_HEADER = 'X-Edyn-Navigation-Dealer-Id';\nconst NAVIGATION_DEALERS_HEADER = 'X-Edyn-Navigation-Dealer-Ids';\nconst NAVIGATION_HEADERS = [NAVIGATION_GROUP_HEADER, NAVIGATION_DEALER_HEADER, NAVIGATION_DEALERS_HEADER].map((name) => name.toLowerCase());\nconst MAX_NAVIGATION_EVENT_BYTES = 5 * 1024 * 1024;\nconst MAX_REQUEST_BODY_BYTES = 100 * 1024 * 1024;\nconst EVENT_SEPARATOR = /\\r?\\n\\r?\\n/;\nconst COOKIE_NAME_PATTERN = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\nconst COOKIE_VALUE_PATTERN = /^[\\x20-\\x7e]*$/;\nconst ALLOWED_HOST_API_PATHS = [\n '/api/auth/me',\n '/api/navigation',\n '/api/navigation/access-check',\n '/api/navigation/access-request',\n '/api/navigation/counter-stream',\n] as const;\n\ninterface IOidcClientConfiguration {\n clientId: string;\n issuer: string;\n scope: string;\n}\n\ninterface IOidcDiscovery {\n authorizationEndpoint: string;\n endSessionEndpoint: string;\n issuer: string;\n jwksUri: string;\n revocationEndpoint: string | null;\n tokenEndpoint: string;\n}\n\ninterface IOidcFlow {\n codeVerifier: string;\n createdAt: number;\n nonce: string;\n returnTo: string;\n state: string;\n}\n\ninterface IModuleNavigationContext {\n dealerIds: readonly number[];\n groupId: number;\n}\n\ninterface IModuleProxyContext {\n cookies: Map<string, string>;\n navigationContext: IModuleNavigationContext | null;\n}\n\ninterface ITokenSession {\n accessToken: string;\n expiresAt: number;\n idToken: string | null;\n moduleBootstrapTail: Promise<void>;\n moduleCookies: Map<string, string>;\n moduleNavigationContext: IModuleNavigationContext | null;\n refreshPromise?: Promise<void>;\n refreshToken: string | null;\n}\n\ninterface ILogoutFlow {\n createdAt: number;\n idToken: string | null;\n}\n\ninterface ITokenResponse {\n accessToken: string;\n expiresIn: number;\n idToken: string | null;\n refreshToken: string | null;\n tokenType: string;\n}\n\nexport interface IMfDevelopmentBffPluginOptions {\n developmentOrigin?: string;\n hostNavigationUrl: string;\n moduleApiOrigin: string;\n moduleApiPath: string;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst requiredString = (value: Record<string, unknown>, key: string): string => {\n const candidate = value[key];\n if (typeof candidate !== 'string' || candidate.trim() === '') {\n throw new Error(`OIDC response is missing ${key}.`);\n }\n return candidate;\n};\n\nconst optionalString = (value: Record<string, unknown>, key: string): string | null => {\n const candidate = value[key];\n if (candidate === undefined || candidate === null) {\n return null;\n }\n if (typeof candidate !== 'string' || candidate.trim() === '') {\n throw new Error(`OIDC response contains an invalid ${key}.`);\n }\n return candidate;\n};\n\nconst absoluteHttpsUrl = (candidate: string, label: string, allowLocalHttp = false): URL => {\n let url: URL;\n try {\n url = new URL(candidate);\n } catch {\n throw new Error(`${label} must be an absolute URL.`);\n }\n const isLocalHttp = allowLocalHttp && url.protocol === 'http:' && (url.hostname === 'localhost' || url.hostname === '127.0.0.1');\n if (url.protocol !== 'https:' && !isLocalHttp) {\n throw new Error(`${label} must use HTTPS.`);\n }\n if (url.username || url.password || url.hash) {\n throw new Error(`${label} must not contain credentials or a fragment.`);\n }\n return url;\n};\n\nconst moduleApiPrefix = (candidate: string): string => {\n if (!/^\\/[a-z][a-z0-9-]*\\/api\\/$/.test(candidate)) {\n throw new Error('moduleApiPath must use the canonical /module/api/ boundary.');\n }\n return candidate;\n};\n\nconst base64Url = (size = 32): string => randomBytes(size).toString('base64url');\nconst codeChallengeOf = (verifier: string): string => createHash('sha256').update(verifier).digest('base64url');\n\nconst cookiesOf = (request: IncomingMessage): ReadonlyMap<string, string> => {\n const cookies = new Map<string, string>();\n for (const part of (request.headers.cookie ?? '').split(';')) {\n const separator = part.indexOf('=');\n if (separator <= 0) {\n continue;\n }\n const name = part.slice(0, separator).trim();\n const value = part.slice(separator + 1).trim();\n if (name && value) {\n cookies.set(name, value);\n }\n }\n return cookies;\n};\n\nconst cookie = (name: string, value: string, maxAge: number): string => `${name}=${value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`;\n\nconst clearCookie = (name: string): string => cookie(name, '', 0);\n\n// The refresh token stays in an HttpOnly cookie for this origin, which every module's development server shares, so\n// one sign-in survives restarts and module switches until the token expires; page scripts can never read it.\nconst refreshCookieOf = (session: ITokenSession): string =>\n session.refreshToken\n ? cookie(REFRESH_COOKIE, encodeURIComponent(session.refreshToken), REFRESH_COOKIE_MAX_AGE_SECONDS)\n : clearCookie(REFRESH_COOKIE);\n\nconst decodedCookieValue = (value: string | undefined): string | null => {\n if (!value) {\n return null;\n }\n try {\n return decodeURIComponent(value);\n } catch {\n return null;\n }\n};\n\nconst writeJson = (response: ServerResponse, status: number, body: unknown): void => {\n response.statusCode = status;\n response.setHeader('Cache-Control', 'no-store');\n response.setHeader('Content-Type', 'application/json; charset=utf-8');\n response.end(JSON.stringify(body));\n};\n\nconst redirect = (response: ServerResponse, location: string, cookies: string[] = []): void => {\n response.statusCode = 302;\n response.setHeader('Cache-Control', 'no-store');\n response.setHeader('Location', location);\n if (cookies.length > 0) {\n response.setHeader('Set-Cookie', cookies);\n }\n response.end();\n};\n\nconst safeReturnTo = (candidate: string | null): string => {\n if (!candidate?.startsWith('/') || candidate.startsWith('//')) {\n return '/';\n }\n try {\n const parsed = new URL(candidate, 'https://localhost');\n return `${parsed.pathname}${parsed.search}${parsed.hash}`;\n } catch {\n return '/';\n }\n};\n\nconst parseTokenResponse = (value: unknown, requireIdToken: boolean): ITokenResponse => {\n if (!isRecord(value)) {\n throw new Error('OIDC token endpoint returned an invalid response.');\n }\n const expiresIn = value.expires_in;\n if (typeof expiresIn !== 'number' || !Number.isFinite(expiresIn) || expiresIn <= 0) {\n throw new Error('OIDC token endpoint returned an invalid expires_in value.');\n }\n const idToken = optionalString(value, 'id_token');\n if (requireIdToken && !idToken) {\n throw new Error('OIDC token endpoint did not return an ID token.');\n }\n const tokenType = requiredString(value, 'token_type');\n if (tokenType.toLowerCase() !== 'bearer') {\n throw new Error('OIDC token endpoint returned an unsupported token type.');\n }\n return {\n accessToken: requiredString(value, 'access_token'),\n expiresIn,\n idToken,\n refreshToken: optionalString(value, 'refresh_token'),\n tokenType,\n };\n};\n\nconst pathIsHostApi = (path: string): boolean => {\n return ALLOWED_HOST_API_PATHS.some((candidate) => path === candidate || path.startsWith(`${candidate}/`));\n};\n\n// The host validates a selection the browser sends it, so host requests keep the navigation headers. A module trusts\n// them, so its requests carry only the selection the host last confirmed.\nconst proxyHeaders = (request: IncomingMessage, accessToken: string, module?: IModuleProxyContext): Headers => {\n const headers = new Headers();\n for (const [name, value] of Object.entries(request.headers)) {\n const lowerName = name.toLowerCase();\n if (\n !value ||\n name.startsWith(':') ||\n ['authorization', 'connection', 'content-length', 'cookie', 'host', 'origin', 'transfer-encoding'].includes(lowerName) ||\n (module && NAVIGATION_HEADERS.includes(lowerName))\n ) {\n continue;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n headers.append(name, item);\n }\n } else {\n headers.set(name, value);\n }\n }\n headers.set('Authorization', `Bearer ${accessToken}`);\n if (module?.cookies.size) {\n headers.set('Cookie', [...module.cookies.values()].join('; '));\n }\n const navigationContext = module?.navigationContext;\n if (navigationContext) {\n headers.set(NAVIGATION_GROUP_HEADER, String(navigationContext.groupId));\n headers.set(NAVIGATION_DEALERS_HEADER, navigationContext.dealerIds.join(','));\n if (navigationContext.dealerIds.length === 1) {\n headers.set(NAVIGATION_DEALER_HEADER, String(navigationContext.dealerIds[0]));\n }\n }\n return headers;\n};\n\nconst navigationContextIn = (value: unknown): IModuleNavigationContext | null => {\n if (!isRecord(value) || !isRecord(value.context)) {\n return null;\n }\n const { selectedDealerId, selectedDealerIds, selectedGroupId } = value.context;\n const dealerIds: unknown[] = Array.isArray(selectedDealerIds) ? selectedDealerIds : [selectedDealerId];\n if (\n !Number.isSafeInteger(selectedGroupId) ||\n (selectedGroupId as number) <= 0 ||\n dealerIds.length === 0 ||\n !dealerIds.every((dealerId) => Number.isSafeInteger(dealerId) && (dealerId as number) >= 0)\n ) {\n return null;\n }\n return { dealerIds: dealerIds as number[], groupId: selectedGroupId as number };\n};\n\n// Reads the selection each `navigation` event of the host's navigation stream confirms, chunk by chunk as it passes.\nconst navigationEventReader = (onContext: (context: IModuleNavigationContext) => void): ((chunk: Buffer) => void) => {\n const decoder = new TextDecoder();\n let pending = '';\n return (chunk) => {\n pending += decoder.decode(chunk, { stream: true });\n for (let separator = EVENT_SEPARATOR.exec(pending); separator; separator = EVENT_SEPARATOR.exec(pending)) {\n const block = pending.slice(0, separator.index);\n pending = pending.slice(separator.index + separator[0].length);\n const lines = block.split(/\\r?\\n/);\n if (!lines.includes('event: navigation') && !lines.includes('event:navigation')) {\n continue;\n }\n const data = lines\n .filter((line) => line.startsWith('data:'))\n .map((line) => line.slice(5).replace(/^ /, ''))\n .join('\\n');\n let payload: unknown = null;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n const context = navigationContextIn(payload);\n if (context) {\n onContext(context);\n }\n }\n if (pending.length > MAX_NAVIGATION_EVENT_BYTES) {\n pending = '';\n }\n };\n};\n\nconst updateModuleCookies = (cookies: Map<string, string>, headers: Headers): void => {\n const values = headers.getSetCookie();\n for (const value of values) {\n if (value.length > 4096) {\n continue;\n }\n const [pair, ...attributes] = value.split(';');\n const separator = pair?.indexOf('=') ?? -1;\n if (!pair || separator <= 0) {\n continue;\n }\n const name = pair.slice(0, separator).trim();\n const cookieValue = pair.slice(separator + 1).trim();\n if (!COOKIE_NAME_PATTERN.test(name) || !COOKIE_VALUE_PATTERN.test(cookieValue) || cookieValue.includes(';')) {\n continue;\n }\n const deleted = cookieValue === '' || attributes.some((attribute) => /^\\s*max-age\\s*=\\s*0\\s*$/i.test(attribute));\n if (deleted) {\n cookies.delete(name);\n continue;\n }\n if (!cookies.has(name) && cookies.size >= 32) {\n continue;\n }\n cookies.set(name, `${name}=${cookieValue}`);\n }\n};\n\nconst requestBodyOf = async (request: IncomingMessage): Promise<Buffer | null> => {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of request) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string);\n size += buffer.length;\n if (size > MAX_REQUEST_BODY_BYTES) {\n return null;\n }\n chunks.push(buffer);\n }\n return Buffer.concat(chunks);\n};\n\nconst copyProxyResponse = async (upstream: Response, response: ServerResponse, onChunk?: (chunk: Buffer) => void): Promise<void> => {\n response.statusCode = upstream.status;\n for (const [name, value] of upstream.headers) {\n if (!['connection', 'content-encoding', 'content-length', 'set-cookie', 'transfer-encoding'].includes(name.toLowerCase())) {\n response.setHeader(name, value);\n }\n }\n response.setHeader('Cache-Control', upstream.headers.get('cache-control') ?? 'no-store');\n if (!upstream.body) {\n response.end();\n return;\n }\n await new Promise<void>((resolve, reject) => {\n const readable = Readable.fromWeb(upstream.body as never);\n if (onChunk) {\n readable.on('data', onChunk);\n }\n readable.once('error', reject);\n response.once('error', reject);\n response.once('finish', resolve);\n readable.pipe(response);\n });\n};\n\nconst readResponseBody = async (response: Response, maximumBytes: number, label: string): Promise<Buffer> => {\n const declaredLength = Number(response.headers.get('content-length'));\n if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {\n await response.body?.cancel();\n throw new Error(`${label} exceeded the configured size limit.`);\n }\n if (!response.body) {\n return Buffer.alloc(0);\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let totalBytes = 0;\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n totalBytes += value.byteLength;\n if (totalBytes > maximumBytes) {\n await reader.cancel();\n throw new Error(`${label} exceeded the configured size limit.`);\n }\n chunks.push(value);\n }\n } finally {\n reader.releaseLock();\n }\n return Buffer.concat(chunks, totalBytes);\n};\n\nclass MfDevelopmentBff {\n readonly #configurationUrl: URL;\n readonly #developmentOrigin: URL;\n readonly #flows = new Map<string, IOidcFlow>();\n readonly #hostOrigin: URL;\n readonly #hostNavigationUrl: URL;\n readonly #jwks = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n readonly #logoutFlows = new Map<string, ILogoutFlow>();\n readonly #moduleApiOrigin: URL;\n readonly #moduleApiPath: string;\n readonly #sessions = new Map<string, ITokenSession>();\n readonly #restores = new Map<string, Promise<{ session: ITokenSession; sessionId: string } | null>>();\n #configurationPromise: Promise<IOidcClientConfiguration> | null = null;\n #discoveryPromise: Promise<IOidcDiscovery> | null = null;\n\n constructor(options: IMfDevelopmentBffPluginOptions) {\n const hostNavigation = absoluteHttpsUrl(options.hostNavigationUrl, 'hostNavigationUrl', true);\n this.#hostNavigationUrl = hostNavigation;\n this.#hostOrigin = new URL(hostNavigation.origin);\n this.#configurationUrl = new URL('/api/auth/oidc-configuration', this.#hostOrigin);\n this.#developmentOrigin = absoluteHttpsUrl(options.developmentOrigin ?? 'https://localhost:4200', 'developmentOrigin');\n if (this.#developmentOrigin.pathname !== '/' || this.#developmentOrigin.search || this.#developmentOrigin.hash) {\n throw new Error('developmentOrigin must be an origin without a path, query, or fragment.');\n }\n this.#moduleApiOrigin = absoluteHttpsUrl(options.moduleApiOrigin, 'moduleApiOrigin', true);\n if (this.#moduleApiOrigin.pathname !== '/' || this.#moduleApiOrigin.search || this.#moduleApiOrigin.hash) {\n throw new Error('moduleApiOrigin must be an origin without a path, query, or fragment.');\n }\n this.#moduleApiPath = moduleApiPrefix(options.moduleApiPath);\n }\n\n async handle(request: IncomingMessage, response: ServerResponse, next: (error?: unknown) => void): Promise<void> {\n if (!request.url || !request.method) {\n next();\n return;\n }\n const url = new URL(request.url, this.#developmentOrigin);\n this.#prune();\n\n try {\n if (request.method === 'GET' && url.pathname === MF_DEVELOPMENT_HOST_NAVIGATION_PATH) {\n await this.#hostNavigationResource(response, this.#hostNavigationUrl, 'script');\n return;\n }\n if (request.method === 'GET' && HOST_NAVIGATION_ASSET_PATHS.has(url.pathname)) {\n await this.#hostNavigationResource(response, new URL(url.pathname, this.#hostOrigin), 'asset');\n return;\n }\n if (request.method === 'GET' && url.pathname === LOGIN_PATH) {\n await this.#login(request, response, safeReturnTo(url.searchParams.get('returnTo')));\n return;\n }\n if (request.method === 'GET' && url.pathname === CALLBACK_PATH) {\n await this.#callback(request, response, url);\n return;\n }\n if (request.method === 'GET' && url.pathname === LOGGED_OUT_PATH) {\n const returnTo = safeReturnTo(url.searchParams.get('state'));\n redirect(response, `${LOGIN_PATH}?returnTo=${encodeURIComponent(returnTo)}`, [clearCookie(LOGOUT_COOKIE)]);\n return;\n }\n if (request.method === 'GET' && url.pathname === '/frontchannel-logout') {\n const sessionId = cookiesOf(request).get(SESSION_COOKIE);\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n response.statusCode = 204;\n response.setHeader('Set-Cookie', [clearCookie(SESSION_COOKIE), clearCookie(REFRESH_COOKIE)]);\n response.end();\n return;\n }\n if (request.method === 'POST' && url.pathname === '/api/auth/logout') {\n await this.#logout(request, response);\n return;\n }\n if (pathIsHostApi(url.pathname) || url.pathname.startsWith(this.#moduleApiPath)) {\n await this.#proxy(request, response, url);\n return;\n }\n next();\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Development authentication failed.';\n writeJson(response, 502, { code: 'development_authentication_failed', error: message });\n }\n }\n\n async #hostNavigationResource(response: ServerResponse, upstreamUrl: URL, kind: 'asset' | 'script'): Promise<void> {\n const upstream = await fetch(upstreamUrl, {\n headers: { Accept: kind === 'script' ? 'text/javascript, application/javascript' : 'image/svg+xml' },\n redirect: 'error',\n });\n if (!upstream.ok) {\n throw new Error(`Host navigation ${kind} failed (${upstream.status}).`);\n }\n const contentType = upstream.headers.get('content-type') ?? '';\n const validContentType =\n kind === 'script' ? /^(?:text|application)\\/javascript(?:;|$)/i.test(contentType) : /^image\\/svg\\+xml(?:;|$)/i.test(contentType);\n if (!validContentType) {\n throw new Error(`Host navigation ${kind} returned an invalid content type.`);\n }\n const maximumBytes = kind === 'script' ? MAX_HOST_NAVIGATION_BYTES : MAX_HOST_NAVIGATION_ASSET_BYTES;\n const body = await readResponseBody(upstream, maximumBytes, `Host navigation ${kind}`);\n response.statusCode = 200;\n response.setHeader('Cache-Control', 'no-store');\n response.setHeader('Content-Type', contentType);\n response.setHeader('X-Content-Type-Options', 'nosniff');\n response.end(body);\n }\n\n async #configuration(): Promise<IOidcClientConfiguration> {\n this.#configurationPromise ??= fetch(this.#configurationUrl, { headers: { Accept: 'application/json' }, redirect: 'error' })\n .then(async (response) => {\n if (!response.ok) {\n throw new Error(`Host OIDC configuration failed (${response.status}).`);\n }\n const value: unknown = await response.json();\n if (!isRecord(value)) {\n throw new Error('Host OIDC configuration returned an invalid response.');\n }\n const issuer = absoluteHttpsUrl(requiredString(value, 'issuer'), 'OIDC issuer', true).href.replace(/\\/$/, '');\n return { clientId: requiredString(value, 'clientId'), issuer, scope: requiredString(value, 'scope') };\n })\n .catch((error) => {\n this.#configurationPromise = null;\n throw error;\n });\n return this.#configurationPromise;\n }\n\n async #discovery(): Promise<IOidcDiscovery> {\n this.#discoveryPromise ??= this.#configuration()\n .then(async (configuration) => {\n const response = await fetch(`${configuration.issuer}/.well-known/openid-configuration`, {\n headers: { Accept: 'application/json' },\n redirect: 'error',\n });\n if (!response.ok) {\n throw new Error(`OIDC discovery failed (${response.status}).`);\n }\n const value: unknown = await response.json();\n if (!isRecord(value)) {\n throw new Error('OIDC discovery returned an invalid response.');\n }\n const issuer = absoluteHttpsUrl(requiredString(value, 'issuer'), 'OIDC discovery issuer', true).href.replace(/\\/$/, '');\n if (issuer !== configuration.issuer) {\n throw new Error('OIDC discovery issuer does not match the host configuration.');\n }\n const endpoint = (key: string): string => {\n const url = absoluteHttpsUrl(requiredString(value, key), `OIDC ${key}`, true);\n if (url.origin !== new URL(issuer).origin) {\n throw new Error(`OIDC ${key} must use the configured issuer origin.`);\n }\n return url.href;\n };\n const revocation = optionalString(value, 'revocation_endpoint');\n return {\n authorizationEndpoint: endpoint('authorization_endpoint'),\n endSessionEndpoint: endpoint('end_session_endpoint'),\n issuer,\n jwksUri: endpoint('jwks_uri'),\n revocationEndpoint: revocation ? endpoint('revocation_endpoint') : null,\n tokenEndpoint: endpoint('token_endpoint'),\n };\n })\n .catch((error) => {\n this.#discoveryPromise = null;\n throw error;\n });\n return this.#discoveryPromise;\n }\n\n async #login(request: IncomingMessage, response: ServerResponse, returnTo: string): Promise<void> {\n const logoutId = cookiesOf(request).get(LOGOUT_COOKIE);\n if (logoutId) {\n const logout = this.#logoutFlows.get(logoutId);\n this.#logoutFlows.delete(logoutId);\n if (logout) {\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const endpoint = new URL(discovery.endSessionEndpoint);\n endpoint.searchParams.set('client_id', configuration.clientId);\n endpoint.searchParams.set('post_logout_redirect_uri', new URL(LOGGED_OUT_PATH, this.#developmentOrigin).href);\n endpoint.searchParams.set('state', returnTo);\n if (logout.idToken) {\n endpoint.searchParams.set('id_token_hint', logout.idToken);\n }\n redirect(response, endpoint.href, [clearCookie(LOGOUT_COOKIE)]);\n return;\n }\n }\n\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const flowId = base64Url();\n const codeVerifier = base64Url(48);\n const flow: IOidcFlow = { codeVerifier, createdAt: Date.now(), nonce: base64Url(), returnTo, state: base64Url() };\n this.#flows.set(flowId, flow);\n\n const endpoint = new URL(discovery.authorizationEndpoint);\n endpoint.searchParams.set('response_type', 'code');\n endpoint.searchParams.set('client_id', configuration.clientId);\n endpoint.searchParams.set('redirect_uri', new URL(CALLBACK_PATH, this.#developmentOrigin).href);\n endpoint.searchParams.set('scope', configuration.scope);\n endpoint.searchParams.set('state', flow.state);\n endpoint.searchParams.set('nonce', flow.nonce);\n endpoint.searchParams.set('code_challenge', codeChallengeOf(codeVerifier));\n endpoint.searchParams.set('code_challenge_method', 'S256');\n redirect(response, endpoint.href, [cookie(FLOW_COOKIE, flowId, FLOW_MAX_AGE_SECONDS)]);\n }\n\n async #callback(request: IncomingMessage, response: ServerResponse, url: URL): Promise<void> {\n const flowId = cookiesOf(request).get(FLOW_COOKIE);\n const flow = flowId ? this.#flows.get(flowId) : undefined;\n if (flowId) {\n this.#flows.delete(flowId);\n }\n if (!flow || url.searchParams.get('state') !== flow.state) {\n writeJson(response, 400, { code: 'invalid_oidc_state', error: 'The OIDC callback state is invalid or expired.' });\n return;\n }\n const providerError = url.searchParams.get('error');\n if (providerError) {\n writeJson(response, 401, { code: providerError, error: url.searchParams.get('error_description') ?? 'Sign-in was rejected.' });\n return;\n }\n const code = url.searchParams.get('code');\n if (!code) {\n writeJson(response, 400, { code: 'missing_authorization_code', error: 'The OIDC callback did not include an authorization code.' });\n return;\n }\n\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const fields = new URLSearchParams({\n client_id: configuration.clientId,\n code,\n code_verifier: flow.codeVerifier,\n grant_type: 'authorization_code',\n redirect_uri: new URL(CALLBACK_PATH, this.#developmentOrigin).href,\n });\n const tokenResponse = await fetch(discovery.tokenEndpoint, {\n body: fields,\n headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },\n method: 'POST',\n redirect: 'error',\n });\n if (!tokenResponse.ok) {\n throw new Error(`OIDC token exchange failed (${tokenResponse.status}).`);\n }\n const tokens = parseTokenResponse(await tokenResponse.json(), true);\n const idToken = tokens.idToken;\n if (!idToken) {\n throw new Error('OIDC token endpoint did not return an ID token.');\n }\n const jwks = this.#jwks.get(discovery.jwksUri) ?? createRemoteJWKSet(new URL(discovery.jwksUri));\n this.#jwks.set(discovery.jwksUri, jwks);\n const verified = await jwtVerify(idToken, jwks, {\n audience: configuration.clientId,\n issuer: discovery.issuer,\n requiredClaims: ['sub', 'nonce'],\n });\n if (verified.payload.nonce !== flow.nonce) {\n throw new Error('OIDC ID token nonce does not match the authorization request.');\n }\n\n const sessionId = base64Url();\n const session: ITokenSession = {\n accessToken: tokens.accessToken,\n expiresAt: Date.now() + tokens.expiresIn * 1000,\n idToken,\n moduleBootstrapTail: Promise.resolve(),\n moduleCookies: new Map(),\n moduleNavigationContext: null,\n refreshToken: tokens.refreshToken,\n };\n this.#sessions.set(sessionId, session);\n redirect(response, flow.returnTo, [\n cookie(SESSION_COOKIE, sessionId, SESSION_MAX_AGE_SECONDS),\n refreshCookieOf(session),\n clearCookie(FLOW_COOKIE),\n ]);\n }\n\n async #logout(request: IncomingMessage, response: ServerResponse): Promise<void> {\n const sessionId = cookiesOf(request).get(SESSION_COOKIE);\n const session = sessionId ? this.#sessions.get(sessionId) : undefined;\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n if (session) {\n void this.#revoke(session).catch(() => undefined);\n }\n const logoutId = base64Url();\n this.#logoutFlows.set(logoutId, { createdAt: Date.now(), idToken: session?.idToken ?? null });\n response.setHeader('Set-Cookie', [\n clearCookie(SESSION_COOKIE),\n clearCookie(REFRESH_COOKIE),\n cookie(LOGOUT_COOKIE, logoutId, FLOW_MAX_AGE_SECONDS),\n ]);\n writeJson(response, 200, { loggedOut: true });\n }\n\n async #revoke(session: ITokenSession): Promise<void> {\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const revocationEndpoint = discovery.revocationEndpoint;\n if (!revocationEndpoint) {\n return;\n }\n const revoke = async (token: string, hint: string): Promise<void> => {\n await fetch(revocationEndpoint, {\n body: new URLSearchParams({ client_id: configuration.clientId, token, token_type_hint: hint }),\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n method: 'POST',\n redirect: 'error',\n });\n };\n await Promise.all([\n revoke(session.accessToken, 'access_token'),\n session.refreshToken ? revoke(session.refreshToken, 'refresh_token') : Promise.resolve(),\n ]);\n }\n\n async #authenticatedSession(request: IncomingMessage, response: ServerResponse): Promise<ITokenSession | null> {\n const cookies = cookiesOf(request);\n const sessionId = cookies.get(SESSION_COOKIE);\n const session = sessionId ? this.#sessions.get(sessionId) : undefined;\n if (!session) {\n return this.#restoredSession(decodedCookieValue(cookies.get(REFRESH_COOKIE)), response);\n }\n if (session.expiresAt > Date.now() + TOKEN_REFRESH_SKEW_MS) {\n return session;\n }\n if (!session.refreshToken) {\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n return null;\n }\n session.refreshPromise ??= this.#refresh(session).finally(() => {\n session.refreshPromise = undefined;\n });\n try {\n await session.refreshPromise;\n response.setHeader('Set-Cookie', refreshCookieOf(session));\n return session;\n } catch {\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n response.setHeader('Set-Cookie', clearCookie(REFRESH_COOKIE));\n return null;\n }\n }\n\n // Starts a session from the browser's refresh cookie after this server restarted or another module's server signed\n // the developer in. Parallel requests carrying the same token share one refresh.\n async #restoredSession(refreshToken: string | null, response: ServerResponse): Promise<ITokenSession | null> {\n if (!refreshToken) {\n return null;\n }\n let restore = this.#restores.get(refreshToken);\n if (!restore) {\n restore = this.#restore(refreshToken).finally(() => this.#restores.delete(refreshToken));\n this.#restores.set(refreshToken, restore);\n }\n const restored = await restore;\n if (!restored) {\n response.setHeader('Set-Cookie', clearCookie(REFRESH_COOKIE));\n return null;\n }\n response.setHeader('Set-Cookie', [cookie(SESSION_COOKIE, restored.sessionId, SESSION_MAX_AGE_SECONDS), refreshCookieOf(restored.session)]);\n return restored.session;\n }\n\n async #restore(refreshToken: string): Promise<{ session: ITokenSession; sessionId: string } | null> {\n const session: ITokenSession = {\n accessToken: '',\n expiresAt: 0,\n idToken: null,\n moduleBootstrapTail: Promise.resolve(),\n moduleCookies: new Map(),\n moduleNavigationContext: null,\n refreshToken,\n };\n try {\n await this.#refresh(session);\n } catch {\n return null;\n }\n const sessionId = base64Url();\n this.#sessions.set(sessionId, session);\n return { session, sessionId };\n }\n\n async #refresh(session: ITokenSession): Promise<void> {\n const refreshToken = session.refreshToken;\n if (!refreshToken) {\n throw new Error('OIDC refresh token is unavailable.');\n }\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const response = await fetch(discovery.tokenEndpoint, {\n body: new URLSearchParams({\n client_id: configuration.clientId,\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n }),\n headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },\n method: 'POST',\n redirect: 'error',\n });\n if (!response.ok) {\n throw new Error(`OIDC refresh failed (${response.status}).`);\n }\n const tokens = parseTokenResponse(await response.json(), false);\n session.accessToken = tokens.accessToken;\n session.expiresAt = Date.now() + tokens.expiresIn * 1000;\n session.refreshToken = tokens.refreshToken ?? session.refreshToken;\n session.idToken = tokens.idToken ?? session.idToken;\n }\n\n async #proxy(request: IncomingMessage, response: ServerResponse, localUrl: URL): Promise<void> {\n const session = await this.#authenticatedSession(request, response);\n if (!session) {\n // This server refreshes the session's tokens itself, so the browser can only sign in again. A refreshable code\n // would send the host client to a refresh endpoint that only the host has.\n writeJson(response, 401, { code: 'sign_in_required', error: 'Sign in is required.' });\n return;\n }\n const isModuleApi = localUrl.pathname.startsWith(this.#moduleApiPath);\n if (isModuleApi && session.moduleCookies.size === 0) {\n const previous = session.moduleBootstrapTail;\n let release = (): void => {};\n const current = new Promise<void>((resolve) => {\n release = resolve;\n });\n session.moduleBootstrapTail = previous.catch(() => undefined).then(() => current);\n await previous.catch(() => undefined);\n try {\n await this.#proxyAuthenticated(request, response, localUrl, session, true);\n } finally {\n release();\n }\n return;\n }\n await this.#proxyAuthenticated(request, response, localUrl, session, isModuleApi);\n }\n\n async #proxyAuthenticated(\n request: IncomingMessage,\n response: ServerResponse,\n localUrl: URL,\n session: ITokenSession,\n isModuleApi: boolean,\n ): Promise<void> {\n const upstreamOrigin = isModuleApi ? this.#moduleApiOrigin : this.#hostOrigin;\n const upstreamUrl = new URL(`${localUrl.pathname}${localUrl.search}`, upstreamOrigin);\n const method = request.method ?? 'GET';\n const init: RequestInit = {\n headers: proxyHeaders(\n request,\n session.accessToken,\n isModuleApi ? { cookies: session.moduleCookies, navigationContext: session.moduleNavigationContext } : undefined,\n ),\n method,\n redirect: 'manual',\n };\n if (method !== 'GET' && method !== 'HEAD') {\n // A fixed-length body: the web front ends of the deployed module servers answer chunked request bodies with 404.\n const body = await requestBodyOf(request);\n if (!body) {\n writeJson(response, 413, { code: 'request_body_too_large', error: 'The request body exceeds the development proxy limit.' });\n return;\n }\n init.body = body;\n }\n const upstream = await fetch(upstreamUrl, init);\n let onChunk: ((chunk: Buffer) => void) | undefined;\n if (isModuleApi) {\n updateModuleCookies(session.moduleCookies, upstream.headers);\n } else if (localUrl.pathname === '/api/navigation' && upstream.ok) {\n session.moduleNavigationContext = navigationContextIn(\n await upstream\n .clone()\n .json()\n .catch(() => null),\n );\n } else if (localUrl.pathname === '/api/navigation/stream' && upstream.ok) {\n onChunk = navigationEventReader((context) => {\n session.moduleNavigationContext = context;\n });\n }\n await copyProxyResponse(upstream, response, onChunk);\n }\n\n #prune(): void {\n const now = Date.now();\n for (const [id, flow] of this.#flows) {\n if (flow.createdAt + FLOW_MAX_AGE_SECONDS * 1000 < now) {\n this.#flows.delete(id);\n }\n }\n for (const [id, flow] of this.#logoutFlows) {\n if (flow.createdAt + FLOW_MAX_AGE_SECONDS * 1000 < now) {\n this.#logoutFlows.delete(id);\n }\n }\n for (const [id, session] of this.#sessions) {\n if (!session.refreshToken && session.expiresAt + TOKEN_REFRESH_SKEW_MS < now) {\n this.#sessions.delete(id);\n }\n }\n }\n}\n\nexport const mfDevelopmentBffPlugin = (options: IMfDevelopmentBffPluginOptions): Plugin => {\n const middleware = createMfDevelopmentBffMiddleware(options);\n return {\n apply: 'serve',\n name: 'edynamix-mf-development-bff',\n configureServer(server) {\n server.middlewares.use((request, response, next) => {\n middleware(request, response, next);\n });\n },\n // Locally the module runs inside the host shell, so its page shows the host's icon. The module's own favicon is\n // only copied into the build output, so this server has none to answer the browser's favicon request with.\n transformIndexHtml(html) {\n return {\n html: html.replace(ICON_LINK_PATTERN, ''),\n tags: [{ tag: 'link', attrs: { rel: 'icon', type: 'image/svg+xml', href: HOST_ICON_PATH }, injectTo: 'head' }],\n };\n },\n };\n};\n\nexport type TMfDevelopmentBffMiddleware = (request: IncomingMessage, response: ServerResponse, next: (error?: unknown) => void) => void;\n\nexport const createMfDevelopmentBffMiddleware = (options: IMfDevelopmentBffPluginOptions): TMfDevelopmentBffMiddleware => {\n const bff = new MfDevelopmentBff(options);\n return (request, response, next) => {\n void bff.handle(request, response, next);\n };\n};\n"]}
@@ -2,6 +2,7 @@ import { createHash, randomBytes } from 'node:crypto';
2
2
  import { Readable } from 'node:stream';
3
3
  import { createRemoteJWKSet, jwtVerify } from 'jose';
4
4
  const SESSION_COOKIE = '__Host-edyn_dev_session';
5
+ const REFRESH_COOKIE = '__Host-edyn_dev_refresh';
5
6
  const FLOW_COOKIE = '__Host-edyn_oidc_flow';
6
7
  const LOGOUT_COOKIE = '__Host-edyn_oidc_logout';
7
8
  const CALLBACK_PATH = '/callback';
@@ -14,10 +15,16 @@ const ICON_LINK_PATTERN = /<link\b[^>]*\brel=["'](?:shortcut\s+)?icon["'][^>]*>\
14
15
  const MAX_HOST_NAVIGATION_BYTES = 5 * 1024 * 1024;
15
16
  const MAX_HOST_NAVIGATION_ASSET_BYTES = 512 * 1024;
16
17
  const SESSION_MAX_AGE_SECONDS = 8 * 60 * 60;
18
+ const REFRESH_COOKIE_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;
17
19
  const FLOW_MAX_AGE_SECONDS = 10 * 60;
18
20
  const TOKEN_REFRESH_SKEW_MS = 30_000;
19
21
  const NAVIGATION_GROUP_HEADER = 'X-Edyn-Navigation-Group-Id';
20
22
  const NAVIGATION_DEALER_HEADER = 'X-Edyn-Navigation-Dealer-Id';
23
+ const NAVIGATION_DEALERS_HEADER = 'X-Edyn-Navigation-Dealer-Ids';
24
+ const NAVIGATION_HEADERS = [NAVIGATION_GROUP_HEADER, NAVIGATION_DEALER_HEADER, NAVIGATION_DEALERS_HEADER].map((name) => name.toLowerCase());
25
+ const MAX_NAVIGATION_EVENT_BYTES = 5 * 1024 * 1024;
26
+ const MAX_REQUEST_BODY_BYTES = 100 * 1024 * 1024;
27
+ const EVENT_SEPARATOR = /\r?\n\r?\n/;
21
28
  const COOKIE_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
22
29
  const COOKIE_VALUE_PATTERN = /^[\x20-\x7e]*$/;
23
30
  const ALLOWED_HOST_API_PATHS = [
@@ -87,6 +94,22 @@ const cookiesOf = (request) => {
87
94
  };
88
95
  const cookie = (name, value, maxAge) => `${name}=${value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`;
89
96
  const clearCookie = (name) => cookie(name, '', 0);
97
+ // The refresh token stays in an HttpOnly cookie for this origin, which every module's development server shares, so
98
+ // one sign-in survives restarts and module switches until the token expires; page scripts can never read it.
99
+ const refreshCookieOf = (session) => session.refreshToken
100
+ ? cookie(REFRESH_COOKIE, encodeURIComponent(session.refreshToken), REFRESH_COOKIE_MAX_AGE_SECONDS)
101
+ : clearCookie(REFRESH_COOKIE);
102
+ const decodedCookieValue = (value) => {
103
+ if (!value) {
104
+ return null;
105
+ }
106
+ try {
107
+ return decodeURIComponent(value);
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ };
90
113
  const writeJson = (response, status, body) => {
91
114
  response.statusCode = status;
92
115
  response.setHeader('Cache-Control', 'no-store');
@@ -141,22 +164,16 @@ const parseTokenResponse = (value, requireIdToken) => {
141
164
  const pathIsHostApi = (path) => {
142
165
  return ALLOWED_HOST_API_PATHS.some((candidate) => path === candidate || path.startsWith(`${candidate}/`));
143
166
  };
144
- const proxyHeaders = (request, accessToken, moduleCookies, navigationContext) => {
167
+ // The host validates a selection the browser sends it, so host requests keep the navigation headers. A module trusts
168
+ // them, so its requests carry only the selection the host last confirmed.
169
+ const proxyHeaders = (request, accessToken, module) => {
145
170
  const headers = new Headers();
146
171
  for (const [name, value] of Object.entries(request.headers)) {
172
+ const lowerName = name.toLowerCase();
147
173
  if (!value ||
148
174
  name.startsWith(':') ||
149
- [
150
- 'authorization',
151
- 'connection',
152
- 'content-length',
153
- 'cookie',
154
- 'host',
155
- 'origin',
156
- 'transfer-encoding',
157
- NAVIGATION_GROUP_HEADER.toLowerCase(),
158
- NAVIGATION_DEALER_HEADER.toLowerCase(),
159
- ].includes(name.toLowerCase())) {
175
+ ['authorization', 'connection', 'content-length', 'cookie', 'host', 'origin', 'transfer-encoding'].includes(lowerName) ||
176
+ (module && NAVIGATION_HEADERS.includes(lowerName))) {
160
177
  continue;
161
178
  }
162
179
  if (Array.isArray(value)) {
@@ -169,12 +186,16 @@ const proxyHeaders = (request, accessToken, moduleCookies, navigationContext) =>
169
186
  }
170
187
  }
171
188
  headers.set('Authorization', `Bearer ${accessToken}`);
172
- if (moduleCookies?.size) {
173
- headers.set('Cookie', [...moduleCookies.values()].join('; '));
189
+ if (module?.cookies.size) {
190
+ headers.set('Cookie', [...module.cookies.values()].join('; '));
174
191
  }
192
+ const navigationContext = module?.navigationContext;
175
193
  if (navigationContext) {
176
194
  headers.set(NAVIGATION_GROUP_HEADER, String(navigationContext.groupId));
177
- headers.set(NAVIGATION_DEALER_HEADER, String(navigationContext.dealerId));
195
+ headers.set(NAVIGATION_DEALERS_HEADER, navigationContext.dealerIds.join(','));
196
+ if (navigationContext.dealerIds.length === 1) {
197
+ headers.set(NAVIGATION_DEALER_HEADER, String(navigationContext.dealerIds[0]));
198
+ }
178
199
  }
179
200
  return headers;
180
201
  };
@@ -182,12 +203,49 @@ const navigationContextIn = (value) => {
182
203
  if (!isRecord(value) || !isRecord(value.context)) {
183
204
  return null;
184
205
  }
185
- const groupId = value.context.selectedGroupId;
186
- const dealerId = value.context.selectedDealerId;
187
- if (!Number.isSafeInteger(groupId) || groupId <= 0 || !Number.isSafeInteger(dealerId) || dealerId < 0) {
206
+ const { selectedDealerId, selectedDealerIds, selectedGroupId } = value.context;
207
+ const dealerIds = Array.isArray(selectedDealerIds) ? selectedDealerIds : [selectedDealerId];
208
+ if (!Number.isSafeInteger(selectedGroupId) ||
209
+ selectedGroupId <= 0 ||
210
+ dealerIds.length === 0 ||
211
+ !dealerIds.every((dealerId) => Number.isSafeInteger(dealerId) && dealerId >= 0)) {
188
212
  return null;
189
213
  }
190
- return { dealerId: dealerId, groupId: groupId };
214
+ return { dealerIds: dealerIds, groupId: selectedGroupId };
215
+ };
216
+ // Reads the selection each `navigation` event of the host's navigation stream confirms, chunk by chunk as it passes.
217
+ const navigationEventReader = (onContext) => {
218
+ const decoder = new TextDecoder();
219
+ let pending = '';
220
+ return (chunk) => {
221
+ pending += decoder.decode(chunk, { stream: true });
222
+ for (let separator = EVENT_SEPARATOR.exec(pending); separator; separator = EVENT_SEPARATOR.exec(pending)) {
223
+ const block = pending.slice(0, separator.index);
224
+ pending = pending.slice(separator.index + separator[0].length);
225
+ const lines = block.split(/\r?\n/);
226
+ if (!lines.includes('event: navigation') && !lines.includes('event:navigation')) {
227
+ continue;
228
+ }
229
+ const data = lines
230
+ .filter((line) => line.startsWith('data:'))
231
+ .map((line) => line.slice(5).replace(/^ /, ''))
232
+ .join('\n');
233
+ let payload = null;
234
+ try {
235
+ payload = JSON.parse(data);
236
+ }
237
+ catch {
238
+ continue;
239
+ }
240
+ const context = navigationContextIn(payload);
241
+ if (context) {
242
+ onContext(context);
243
+ }
244
+ }
245
+ if (pending.length > MAX_NAVIGATION_EVENT_BYTES) {
246
+ pending = '';
247
+ }
248
+ };
191
249
  };
192
250
  const updateModuleCookies = (cookies, headers) => {
193
251
  const values = headers.getSetCookie();
@@ -216,7 +274,20 @@ const updateModuleCookies = (cookies, headers) => {
216
274
  cookies.set(name, `${name}=${cookieValue}`);
217
275
  }
218
276
  };
219
- const copyProxyResponse = async (upstream, response) => {
277
+ const requestBodyOf = async (request) => {
278
+ const chunks = [];
279
+ let size = 0;
280
+ for await (const chunk of request) {
281
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
282
+ size += buffer.length;
283
+ if (size > MAX_REQUEST_BODY_BYTES) {
284
+ return null;
285
+ }
286
+ chunks.push(buffer);
287
+ }
288
+ return Buffer.concat(chunks);
289
+ };
290
+ const copyProxyResponse = async (upstream, response, onChunk) => {
220
291
  response.statusCode = upstream.status;
221
292
  for (const [name, value] of upstream.headers) {
222
293
  if (!['connection', 'content-encoding', 'content-length', 'set-cookie', 'transfer-encoding'].includes(name.toLowerCase())) {
@@ -230,6 +301,9 @@ const copyProxyResponse = async (upstream, response) => {
230
301
  }
231
302
  await new Promise((resolve, reject) => {
232
303
  const readable = Readable.fromWeb(upstream.body);
304
+ if (onChunk) {
305
+ readable.on('data', onChunk);
306
+ }
233
307
  readable.once('error', reject);
234
308
  response.once('error', reject);
235
309
  response.once('finish', resolve);
@@ -278,6 +352,7 @@ class MfDevelopmentBff {
278
352
  #moduleApiOrigin;
279
353
  #moduleApiPath;
280
354
  #sessions = new Map();
355
+ #restores = new Map();
281
356
  #configurationPromise = null;
282
357
  #discoveryPromise = null;
283
358
  constructor(options) {
@@ -330,7 +405,7 @@ class MfDevelopmentBff {
330
405
  this.#sessions.delete(sessionId);
331
406
  }
332
407
  response.statusCode = 204;
333
- response.setHeader('Set-Cookie', clearCookie(SESSION_COOKIE));
408
+ response.setHeader('Set-Cookie', [clearCookie(SESSION_COOKIE), clearCookie(REFRESH_COOKIE)]);
334
409
  response.end();
335
410
  return;
336
411
  }
@@ -517,7 +592,7 @@ class MfDevelopmentBff {
517
592
  throw new Error('OIDC ID token nonce does not match the authorization request.');
518
593
  }
519
594
  const sessionId = base64Url();
520
- this.#sessions.set(sessionId, {
595
+ const session = {
521
596
  accessToken: tokens.accessToken,
522
597
  expiresAt: Date.now() + tokens.expiresIn * 1000,
523
598
  idToken,
@@ -525,8 +600,13 @@ class MfDevelopmentBff {
525
600
  moduleCookies: new Map(),
526
601
  moduleNavigationContext: null,
527
602
  refreshToken: tokens.refreshToken,
528
- });
529
- redirect(response, flow.returnTo, [cookie(SESSION_COOKIE, sessionId, SESSION_MAX_AGE_SECONDS), clearCookie(FLOW_COOKIE)]);
603
+ };
604
+ this.#sessions.set(sessionId, session);
605
+ redirect(response, flow.returnTo, [
606
+ cookie(SESSION_COOKIE, sessionId, SESSION_MAX_AGE_SECONDS),
607
+ refreshCookieOf(session),
608
+ clearCookie(FLOW_COOKIE),
609
+ ]);
530
610
  }
531
611
  async #logout(request, response) {
532
612
  const sessionId = cookiesOf(request).get(SESSION_COOKIE);
@@ -539,7 +619,11 @@ class MfDevelopmentBff {
539
619
  }
540
620
  const logoutId = base64Url();
541
621
  this.#logoutFlows.set(logoutId, { createdAt: Date.now(), idToken: session?.idToken ?? null });
542
- response.setHeader('Set-Cookie', [clearCookie(SESSION_COOKIE), cookie(LOGOUT_COOKIE, logoutId, FLOW_MAX_AGE_SECONDS)]);
622
+ response.setHeader('Set-Cookie', [
623
+ clearCookie(SESSION_COOKIE),
624
+ clearCookie(REFRESH_COOKIE),
625
+ cookie(LOGOUT_COOKIE, logoutId, FLOW_MAX_AGE_SECONDS),
626
+ ]);
543
627
  writeJson(response, 200, { loggedOut: true });
544
628
  }
545
629
  async #revoke(session) {
@@ -561,11 +645,12 @@ class MfDevelopmentBff {
561
645
  session.refreshToken ? revoke(session.refreshToken, 'refresh_token') : Promise.resolve(),
562
646
  ]);
563
647
  }
564
- async #authenticatedSession(request) {
565
- const sessionId = cookiesOf(request).get(SESSION_COOKIE);
648
+ async #authenticatedSession(request, response) {
649
+ const cookies = cookiesOf(request);
650
+ const sessionId = cookies.get(SESSION_COOKIE);
566
651
  const session = sessionId ? this.#sessions.get(sessionId) : undefined;
567
652
  if (!session) {
568
- return null;
653
+ return this.#restoredSession(decodedCookieValue(cookies.get(REFRESH_COOKIE)), response);
569
654
  }
570
655
  if (session.expiresAt > Date.now() + TOKEN_REFRESH_SKEW_MS) {
571
656
  return session;
@@ -581,14 +666,55 @@ class MfDevelopmentBff {
581
666
  });
582
667
  try {
583
668
  await session.refreshPromise;
669
+ response.setHeader('Set-Cookie', refreshCookieOf(session));
584
670
  return session;
585
671
  }
586
672
  catch {
587
673
  if (sessionId) {
588
674
  this.#sessions.delete(sessionId);
589
675
  }
676
+ response.setHeader('Set-Cookie', clearCookie(REFRESH_COOKIE));
677
+ return null;
678
+ }
679
+ }
680
+ // Starts a session from the browser's refresh cookie after this server restarted or another module's server signed
681
+ // the developer in. Parallel requests carrying the same token share one refresh.
682
+ async #restoredSession(refreshToken, response) {
683
+ if (!refreshToken) {
684
+ return null;
685
+ }
686
+ let restore = this.#restores.get(refreshToken);
687
+ if (!restore) {
688
+ restore = this.#restore(refreshToken).finally(() => this.#restores.delete(refreshToken));
689
+ this.#restores.set(refreshToken, restore);
690
+ }
691
+ const restored = await restore;
692
+ if (!restored) {
693
+ response.setHeader('Set-Cookie', clearCookie(REFRESH_COOKIE));
694
+ return null;
695
+ }
696
+ response.setHeader('Set-Cookie', [cookie(SESSION_COOKIE, restored.sessionId, SESSION_MAX_AGE_SECONDS), refreshCookieOf(restored.session)]);
697
+ return restored.session;
698
+ }
699
+ async #restore(refreshToken) {
700
+ const session = {
701
+ accessToken: '',
702
+ expiresAt: 0,
703
+ idToken: null,
704
+ moduleBootstrapTail: Promise.resolve(),
705
+ moduleCookies: new Map(),
706
+ moduleNavigationContext: null,
707
+ refreshToken,
708
+ };
709
+ try {
710
+ await this.#refresh(session);
711
+ }
712
+ catch {
590
713
  return null;
591
714
  }
715
+ const sessionId = base64Url();
716
+ this.#sessions.set(sessionId, session);
717
+ return { session, sessionId };
592
718
  }
593
719
  async #refresh(session) {
594
720
  const refreshToken = session.refreshToken;
@@ -616,7 +742,7 @@ class MfDevelopmentBff {
616
742
  session.idToken = tokens.idToken ?? session.idToken;
617
743
  }
618
744
  async #proxy(request, response, localUrl) {
619
- const session = await this.#authenticatedSession(request);
745
+ const session = await this.#authenticatedSession(request, response);
620
746
  if (!session) {
621
747
  // This server refreshes the session's tokens itself, so the browser can only sign in again. A refreshable code
622
748
  // would send the host client to a refresh endpoint that only the host has.
@@ -647,15 +773,21 @@ class MfDevelopmentBff {
647
773
  const upstreamUrl = new URL(`${localUrl.pathname}${localUrl.search}`, upstreamOrigin);
648
774
  const method = request.method ?? 'GET';
649
775
  const init = {
650
- headers: proxyHeaders(request, session.accessToken, isModuleApi ? session.moduleCookies : undefined, isModuleApi ? session.moduleNavigationContext : undefined),
776
+ headers: proxyHeaders(request, session.accessToken, isModuleApi ? { cookies: session.moduleCookies, navigationContext: session.moduleNavigationContext } : undefined),
651
777
  method,
652
778
  redirect: 'manual',
653
779
  };
654
780
  if (method !== 'GET' && method !== 'HEAD') {
655
- init.body = Readable.toWeb(request);
656
- init.duplex = 'half';
781
+ // A fixed-length body: the web front ends of the deployed module servers answer chunked request bodies with 404.
782
+ const body = await requestBodyOf(request);
783
+ if (!body) {
784
+ writeJson(response, 413, { code: 'request_body_too_large', error: 'The request body exceeds the development proxy limit.' });
785
+ return;
786
+ }
787
+ init.body = body;
657
788
  }
658
789
  const upstream = await fetch(upstreamUrl, init);
790
+ let onChunk;
659
791
  if (isModuleApi) {
660
792
  updateModuleCookies(session.moduleCookies, upstream.headers);
661
793
  }
@@ -665,7 +797,12 @@ class MfDevelopmentBff {
665
797
  .json()
666
798
  .catch(() => null));
667
799
  }
668
- await copyProxyResponse(upstream, response);
800
+ else if (localUrl.pathname === '/api/navigation/stream' && upstream.ok) {
801
+ onChunk = navigationEventReader((context) => {
802
+ session.moduleNavigationContext = context;
803
+ });
804
+ }
805
+ await copyProxyResponse(upstream, response, onChunk);
669
806
  }
670
807
  #prune() {
671
808
  const now = Date.now();
@@ -1 +1 @@
1
- {"version":3,"file":"vite-development.js","sourceRoot":"","sources":["../src/vite-development.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { Readable } from 'node:stream';\nimport { createRemoteJWKSet, jwtVerify } from 'jose';\nimport type { Plugin } from 'vite';\n\nconst SESSION_COOKIE = '__Host-edyn_dev_session';\nconst FLOW_COOKIE = '__Host-edyn_oidc_flow';\nconst LOGOUT_COOKIE = '__Host-edyn_oidc_logout';\nconst CALLBACK_PATH = '/callback';\nconst LOGGED_OUT_PATH = '/logged-out';\nconst LOGIN_PATH = '/api/dev-auth/login';\nexport const MF_DEVELOPMENT_HOST_NAVIGATION_PATH = '/api/dev-host-navigation/index.js';\nconst HOST_ICON_PATH = '/exsto-icon.svg';\nconst HOST_NAVIGATION_ASSET_PATHS = new Set([HOST_ICON_PATH, '/exsto-text.svg']);\nconst ICON_LINK_PATTERN = /<link\\b[^>]*\\brel=[\"'](?:shortcut\\s+)?icon[\"'][^>]*>\\s*/gi;\nconst MAX_HOST_NAVIGATION_BYTES = 5 * 1024 * 1024;\nconst MAX_HOST_NAVIGATION_ASSET_BYTES = 512 * 1024;\nconst SESSION_MAX_AGE_SECONDS = 8 * 60 * 60;\nconst FLOW_MAX_AGE_SECONDS = 10 * 60;\nconst TOKEN_REFRESH_SKEW_MS = 30_000;\nconst NAVIGATION_GROUP_HEADER = 'X-Edyn-Navigation-Group-Id';\nconst NAVIGATION_DEALER_HEADER = 'X-Edyn-Navigation-Dealer-Id';\nconst COOKIE_NAME_PATTERN = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\nconst COOKIE_VALUE_PATTERN = /^[\\x20-\\x7e]*$/;\nconst ALLOWED_HOST_API_PATHS = [\n '/api/auth/me',\n '/api/navigation',\n '/api/navigation/access-check',\n '/api/navigation/access-request',\n '/api/navigation/counter-stream',\n] as const;\n\ninterface IOidcClientConfiguration {\n clientId: string;\n issuer: string;\n scope: string;\n}\n\ninterface IOidcDiscovery {\n authorizationEndpoint: string;\n endSessionEndpoint: string;\n issuer: string;\n jwksUri: string;\n revocationEndpoint: string | null;\n tokenEndpoint: string;\n}\n\ninterface IOidcFlow {\n codeVerifier: string;\n createdAt: number;\n nonce: string;\n returnTo: string;\n state: string;\n}\n\ninterface IModuleNavigationContext {\n dealerId: number;\n groupId: number;\n}\n\ninterface ITokenSession {\n accessToken: string;\n expiresAt: number;\n idToken: string;\n moduleBootstrapTail: Promise<void>;\n moduleCookies: Map<string, string>;\n moduleNavigationContext: IModuleNavigationContext | null;\n refreshPromise?: Promise<void>;\n refreshToken: string | null;\n}\n\ninterface ILogoutFlow {\n createdAt: number;\n idToken: string | null;\n}\n\ninterface ITokenResponse {\n accessToken: string;\n expiresIn: number;\n idToken: string | null;\n refreshToken: string | null;\n tokenType: string;\n}\n\nexport interface IMfDevelopmentBffPluginOptions {\n developmentOrigin?: string;\n hostNavigationUrl: string;\n moduleApiOrigin: string;\n moduleApiPath: string;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst requiredString = (value: Record<string, unknown>, key: string): string => {\n const candidate = value[key];\n if (typeof candidate !== 'string' || candidate.trim() === '') {\n throw new Error(`OIDC response is missing ${key}.`);\n }\n return candidate;\n};\n\nconst optionalString = (value: Record<string, unknown>, key: string): string | null => {\n const candidate = value[key];\n if (candidate === undefined || candidate === null) {\n return null;\n }\n if (typeof candidate !== 'string' || candidate.trim() === '') {\n throw new Error(`OIDC response contains an invalid ${key}.`);\n }\n return candidate;\n};\n\nconst absoluteHttpsUrl = (candidate: string, label: string, allowLocalHttp = false): URL => {\n let url: URL;\n try {\n url = new URL(candidate);\n } catch {\n throw new Error(`${label} must be an absolute URL.`);\n }\n const isLocalHttp = allowLocalHttp && url.protocol === 'http:' && (url.hostname === 'localhost' || url.hostname === '127.0.0.1');\n if (url.protocol !== 'https:' && !isLocalHttp) {\n throw new Error(`${label} must use HTTPS.`);\n }\n if (url.username || url.password || url.hash) {\n throw new Error(`${label} must not contain credentials or a fragment.`);\n }\n return url;\n};\n\nconst moduleApiPrefix = (candidate: string): string => {\n if (!/^\\/[a-z][a-z0-9-]*\\/api\\/$/.test(candidate)) {\n throw new Error('moduleApiPath must use the canonical /module/api/ boundary.');\n }\n return candidate;\n};\n\nconst base64Url = (size = 32): string => randomBytes(size).toString('base64url');\nconst codeChallengeOf = (verifier: string): string => createHash('sha256').update(verifier).digest('base64url');\n\nconst cookiesOf = (request: IncomingMessage): ReadonlyMap<string, string> => {\n const cookies = new Map<string, string>();\n for (const part of (request.headers.cookie ?? '').split(';')) {\n const separator = part.indexOf('=');\n if (separator <= 0) {\n continue;\n }\n const name = part.slice(0, separator).trim();\n const value = part.slice(separator + 1).trim();\n if (name && value) {\n cookies.set(name, value);\n }\n }\n return cookies;\n};\n\nconst cookie = (name: string, value: string, maxAge: number): string => `${name}=${value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`;\n\nconst clearCookie = (name: string): string => cookie(name, '', 0);\n\nconst writeJson = (response: ServerResponse, status: number, body: unknown): void => {\n response.statusCode = status;\n response.setHeader('Cache-Control', 'no-store');\n response.setHeader('Content-Type', 'application/json; charset=utf-8');\n response.end(JSON.stringify(body));\n};\n\nconst redirect = (response: ServerResponse, location: string, cookies: string[] = []): void => {\n response.statusCode = 302;\n response.setHeader('Cache-Control', 'no-store');\n response.setHeader('Location', location);\n if (cookies.length > 0) {\n response.setHeader('Set-Cookie', cookies);\n }\n response.end();\n};\n\nconst safeReturnTo = (candidate: string | null): string => {\n if (!candidate?.startsWith('/') || candidate.startsWith('//')) {\n return '/';\n }\n try {\n const parsed = new URL(candidate, 'https://localhost');\n return `${parsed.pathname}${parsed.search}${parsed.hash}`;\n } catch {\n return '/';\n }\n};\n\nconst parseTokenResponse = (value: unknown, requireIdToken: boolean): ITokenResponse => {\n if (!isRecord(value)) {\n throw new Error('OIDC token endpoint returned an invalid response.');\n }\n const expiresIn = value.expires_in;\n if (typeof expiresIn !== 'number' || !Number.isFinite(expiresIn) || expiresIn <= 0) {\n throw new Error('OIDC token endpoint returned an invalid expires_in value.');\n }\n const idToken = optionalString(value, 'id_token');\n if (requireIdToken && !idToken) {\n throw new Error('OIDC token endpoint did not return an ID token.');\n }\n const tokenType = requiredString(value, 'token_type');\n if (tokenType.toLowerCase() !== 'bearer') {\n throw new Error('OIDC token endpoint returned an unsupported token type.');\n }\n return {\n accessToken: requiredString(value, 'access_token'),\n expiresIn,\n idToken,\n refreshToken: optionalString(value, 'refresh_token'),\n tokenType,\n };\n};\n\nconst pathIsHostApi = (path: string): boolean => {\n return ALLOWED_HOST_API_PATHS.some((candidate) => path === candidate || path.startsWith(`${candidate}/`));\n};\n\nconst proxyHeaders = (\n request: IncomingMessage,\n accessToken: string,\n moduleCookies?: Map<string, string>,\n navigationContext?: IModuleNavigationContext | null,\n): Headers => {\n const headers = new Headers();\n for (const [name, value] of Object.entries(request.headers)) {\n if (\n !value ||\n name.startsWith(':') ||\n [\n 'authorization',\n 'connection',\n 'content-length',\n 'cookie',\n 'host',\n 'origin',\n 'transfer-encoding',\n NAVIGATION_GROUP_HEADER.toLowerCase(),\n NAVIGATION_DEALER_HEADER.toLowerCase(),\n ].includes(name.toLowerCase())\n ) {\n continue;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n headers.append(name, item);\n }\n } else {\n headers.set(name, value);\n }\n }\n headers.set('Authorization', `Bearer ${accessToken}`);\n if (moduleCookies?.size) {\n headers.set('Cookie', [...moduleCookies.values()].join('; '));\n }\n if (navigationContext) {\n headers.set(NAVIGATION_GROUP_HEADER, String(navigationContext.groupId));\n headers.set(NAVIGATION_DEALER_HEADER, String(navigationContext.dealerId));\n }\n return headers;\n};\n\nconst navigationContextIn = (value: unknown): IModuleNavigationContext | null => {\n if (!isRecord(value) || !isRecord(value.context)) {\n return null;\n }\n const groupId = value.context.selectedGroupId;\n const dealerId = value.context.selectedDealerId;\n if (!Number.isSafeInteger(groupId) || (groupId as number) <= 0 || !Number.isSafeInteger(dealerId) || (dealerId as number) < 0) {\n return null;\n }\n return { dealerId: dealerId as number, groupId: groupId as number };\n};\n\nconst updateModuleCookies = (cookies: Map<string, string>, headers: Headers): void => {\n const values = headers.getSetCookie();\n for (const value of values) {\n if (value.length > 4096) {\n continue;\n }\n const [pair, ...attributes] = value.split(';');\n const separator = pair?.indexOf('=') ?? -1;\n if (!pair || separator <= 0) {\n continue;\n }\n const name = pair.slice(0, separator).trim();\n const cookieValue = pair.slice(separator + 1).trim();\n if (!COOKIE_NAME_PATTERN.test(name) || !COOKIE_VALUE_PATTERN.test(cookieValue) || cookieValue.includes(';')) {\n continue;\n }\n const deleted = cookieValue === '' || attributes.some((attribute) => /^\\s*max-age\\s*=\\s*0\\s*$/i.test(attribute));\n if (deleted) {\n cookies.delete(name);\n continue;\n }\n if (!cookies.has(name) && cookies.size >= 32) {\n continue;\n }\n cookies.set(name, `${name}=${cookieValue}`);\n }\n};\n\nconst copyProxyResponse = async (upstream: Response, response: ServerResponse): Promise<void> => {\n response.statusCode = upstream.status;\n for (const [name, value] of upstream.headers) {\n if (!['connection', 'content-encoding', 'content-length', 'set-cookie', 'transfer-encoding'].includes(name.toLowerCase())) {\n response.setHeader(name, value);\n }\n }\n response.setHeader('Cache-Control', upstream.headers.get('cache-control') ?? 'no-store');\n if (!upstream.body) {\n response.end();\n return;\n }\n await new Promise<void>((resolve, reject) => {\n const readable = Readable.fromWeb(upstream.body as never);\n readable.once('error', reject);\n response.once('error', reject);\n response.once('finish', resolve);\n readable.pipe(response);\n });\n};\n\nconst readResponseBody = async (response: Response, maximumBytes: number, label: string): Promise<Buffer> => {\n const declaredLength = Number(response.headers.get('content-length'));\n if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {\n await response.body?.cancel();\n throw new Error(`${label} exceeded the configured size limit.`);\n }\n if (!response.body) {\n return Buffer.alloc(0);\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let totalBytes = 0;\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n totalBytes += value.byteLength;\n if (totalBytes > maximumBytes) {\n await reader.cancel();\n throw new Error(`${label} exceeded the configured size limit.`);\n }\n chunks.push(value);\n }\n } finally {\n reader.releaseLock();\n }\n return Buffer.concat(chunks, totalBytes);\n};\n\nclass MfDevelopmentBff {\n readonly #configurationUrl: URL;\n readonly #developmentOrigin: URL;\n readonly #flows = new Map<string, IOidcFlow>();\n readonly #hostOrigin: URL;\n readonly #hostNavigationUrl: URL;\n readonly #jwks = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n readonly #logoutFlows = new Map<string, ILogoutFlow>();\n readonly #moduleApiOrigin: URL;\n readonly #moduleApiPath: string;\n readonly #sessions = new Map<string, ITokenSession>();\n #configurationPromise: Promise<IOidcClientConfiguration> | null = null;\n #discoveryPromise: Promise<IOidcDiscovery> | null = null;\n\n constructor(options: IMfDevelopmentBffPluginOptions) {\n const hostNavigation = absoluteHttpsUrl(options.hostNavigationUrl, 'hostNavigationUrl', true);\n this.#hostNavigationUrl = hostNavigation;\n this.#hostOrigin = new URL(hostNavigation.origin);\n this.#configurationUrl = new URL('/api/auth/oidc-configuration', this.#hostOrigin);\n this.#developmentOrigin = absoluteHttpsUrl(options.developmentOrigin ?? 'https://localhost:4200', 'developmentOrigin');\n if (this.#developmentOrigin.pathname !== '/' || this.#developmentOrigin.search || this.#developmentOrigin.hash) {\n throw new Error('developmentOrigin must be an origin without a path, query, or fragment.');\n }\n this.#moduleApiOrigin = absoluteHttpsUrl(options.moduleApiOrigin, 'moduleApiOrigin', true);\n if (this.#moduleApiOrigin.pathname !== '/' || this.#moduleApiOrigin.search || this.#moduleApiOrigin.hash) {\n throw new Error('moduleApiOrigin must be an origin without a path, query, or fragment.');\n }\n this.#moduleApiPath = moduleApiPrefix(options.moduleApiPath);\n }\n\n async handle(request: IncomingMessage, response: ServerResponse, next: (error?: unknown) => void): Promise<void> {\n if (!request.url || !request.method) {\n next();\n return;\n }\n const url = new URL(request.url, this.#developmentOrigin);\n this.#prune();\n\n try {\n if (request.method === 'GET' && url.pathname === MF_DEVELOPMENT_HOST_NAVIGATION_PATH) {\n await this.#hostNavigationResource(response, this.#hostNavigationUrl, 'script');\n return;\n }\n if (request.method === 'GET' && HOST_NAVIGATION_ASSET_PATHS.has(url.pathname)) {\n await this.#hostNavigationResource(response, new URL(url.pathname, this.#hostOrigin), 'asset');\n return;\n }\n if (request.method === 'GET' && url.pathname === LOGIN_PATH) {\n await this.#login(request, response, safeReturnTo(url.searchParams.get('returnTo')));\n return;\n }\n if (request.method === 'GET' && url.pathname === CALLBACK_PATH) {\n await this.#callback(request, response, url);\n return;\n }\n if (request.method === 'GET' && url.pathname === LOGGED_OUT_PATH) {\n const returnTo = safeReturnTo(url.searchParams.get('state'));\n redirect(response, `${LOGIN_PATH}?returnTo=${encodeURIComponent(returnTo)}`, [clearCookie(LOGOUT_COOKIE)]);\n return;\n }\n if (request.method === 'GET' && url.pathname === '/frontchannel-logout') {\n const sessionId = cookiesOf(request).get(SESSION_COOKIE);\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n response.statusCode = 204;\n response.setHeader('Set-Cookie', clearCookie(SESSION_COOKIE));\n response.end();\n return;\n }\n if (request.method === 'POST' && url.pathname === '/api/auth/logout') {\n await this.#logout(request, response);\n return;\n }\n if (pathIsHostApi(url.pathname) || url.pathname.startsWith(this.#moduleApiPath)) {\n await this.#proxy(request, response, url);\n return;\n }\n next();\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Development authentication failed.';\n writeJson(response, 502, { code: 'development_authentication_failed', error: message });\n }\n }\n\n async #hostNavigationResource(response: ServerResponse, upstreamUrl: URL, kind: 'asset' | 'script'): Promise<void> {\n const upstream = await fetch(upstreamUrl, {\n headers: { Accept: kind === 'script' ? 'text/javascript, application/javascript' : 'image/svg+xml' },\n redirect: 'error',\n });\n if (!upstream.ok) {\n throw new Error(`Host navigation ${kind} failed (${upstream.status}).`);\n }\n const contentType = upstream.headers.get('content-type') ?? '';\n const validContentType =\n kind === 'script' ? /^(?:text|application)\\/javascript(?:;|$)/i.test(contentType) : /^image\\/svg\\+xml(?:;|$)/i.test(contentType);\n if (!validContentType) {\n throw new Error(`Host navigation ${kind} returned an invalid content type.`);\n }\n const maximumBytes = kind === 'script' ? MAX_HOST_NAVIGATION_BYTES : MAX_HOST_NAVIGATION_ASSET_BYTES;\n const body = await readResponseBody(upstream, maximumBytes, `Host navigation ${kind}`);\n response.statusCode = 200;\n response.setHeader('Cache-Control', 'no-store');\n response.setHeader('Content-Type', contentType);\n response.setHeader('X-Content-Type-Options', 'nosniff');\n response.end(body);\n }\n\n async #configuration(): Promise<IOidcClientConfiguration> {\n this.#configurationPromise ??= fetch(this.#configurationUrl, { headers: { Accept: 'application/json' }, redirect: 'error' })\n .then(async (response) => {\n if (!response.ok) {\n throw new Error(`Host OIDC configuration failed (${response.status}).`);\n }\n const value: unknown = await response.json();\n if (!isRecord(value)) {\n throw new Error('Host OIDC configuration returned an invalid response.');\n }\n const issuer = absoluteHttpsUrl(requiredString(value, 'issuer'), 'OIDC issuer', true).href.replace(/\\/$/, '');\n return { clientId: requiredString(value, 'clientId'), issuer, scope: requiredString(value, 'scope') };\n })\n .catch((error) => {\n this.#configurationPromise = null;\n throw error;\n });\n return this.#configurationPromise;\n }\n\n async #discovery(): Promise<IOidcDiscovery> {\n this.#discoveryPromise ??= this.#configuration()\n .then(async (configuration) => {\n const response = await fetch(`${configuration.issuer}/.well-known/openid-configuration`, {\n headers: { Accept: 'application/json' },\n redirect: 'error',\n });\n if (!response.ok) {\n throw new Error(`OIDC discovery failed (${response.status}).`);\n }\n const value: unknown = await response.json();\n if (!isRecord(value)) {\n throw new Error('OIDC discovery returned an invalid response.');\n }\n const issuer = absoluteHttpsUrl(requiredString(value, 'issuer'), 'OIDC discovery issuer', true).href.replace(/\\/$/, '');\n if (issuer !== configuration.issuer) {\n throw new Error('OIDC discovery issuer does not match the host configuration.');\n }\n const endpoint = (key: string): string => {\n const url = absoluteHttpsUrl(requiredString(value, key), `OIDC ${key}`, true);\n if (url.origin !== new URL(issuer).origin) {\n throw new Error(`OIDC ${key} must use the configured issuer origin.`);\n }\n return url.href;\n };\n const revocation = optionalString(value, 'revocation_endpoint');\n return {\n authorizationEndpoint: endpoint('authorization_endpoint'),\n endSessionEndpoint: endpoint('end_session_endpoint'),\n issuer,\n jwksUri: endpoint('jwks_uri'),\n revocationEndpoint: revocation ? endpoint('revocation_endpoint') : null,\n tokenEndpoint: endpoint('token_endpoint'),\n };\n })\n .catch((error) => {\n this.#discoveryPromise = null;\n throw error;\n });\n return this.#discoveryPromise;\n }\n\n async #login(request: IncomingMessage, response: ServerResponse, returnTo: string): Promise<void> {\n const logoutId = cookiesOf(request).get(LOGOUT_COOKIE);\n if (logoutId) {\n const logout = this.#logoutFlows.get(logoutId);\n this.#logoutFlows.delete(logoutId);\n if (logout) {\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const endpoint = new URL(discovery.endSessionEndpoint);\n endpoint.searchParams.set('client_id', configuration.clientId);\n endpoint.searchParams.set('post_logout_redirect_uri', new URL(LOGGED_OUT_PATH, this.#developmentOrigin).href);\n endpoint.searchParams.set('state', returnTo);\n if (logout.idToken) {\n endpoint.searchParams.set('id_token_hint', logout.idToken);\n }\n redirect(response, endpoint.href, [clearCookie(LOGOUT_COOKIE)]);\n return;\n }\n }\n\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const flowId = base64Url();\n const codeVerifier = base64Url(48);\n const flow: IOidcFlow = { codeVerifier, createdAt: Date.now(), nonce: base64Url(), returnTo, state: base64Url() };\n this.#flows.set(flowId, flow);\n\n const endpoint = new URL(discovery.authorizationEndpoint);\n endpoint.searchParams.set('response_type', 'code');\n endpoint.searchParams.set('client_id', configuration.clientId);\n endpoint.searchParams.set('redirect_uri', new URL(CALLBACK_PATH, this.#developmentOrigin).href);\n endpoint.searchParams.set('scope', configuration.scope);\n endpoint.searchParams.set('state', flow.state);\n endpoint.searchParams.set('nonce', flow.nonce);\n endpoint.searchParams.set('code_challenge', codeChallengeOf(codeVerifier));\n endpoint.searchParams.set('code_challenge_method', 'S256');\n redirect(response, endpoint.href, [cookie(FLOW_COOKIE, flowId, FLOW_MAX_AGE_SECONDS)]);\n }\n\n async #callback(request: IncomingMessage, response: ServerResponse, url: URL): Promise<void> {\n const flowId = cookiesOf(request).get(FLOW_COOKIE);\n const flow = flowId ? this.#flows.get(flowId) : undefined;\n if (flowId) {\n this.#flows.delete(flowId);\n }\n if (!flow || url.searchParams.get('state') !== flow.state) {\n writeJson(response, 400, { code: 'invalid_oidc_state', error: 'The OIDC callback state is invalid or expired.' });\n return;\n }\n const providerError = url.searchParams.get('error');\n if (providerError) {\n writeJson(response, 401, { code: providerError, error: url.searchParams.get('error_description') ?? 'Sign-in was rejected.' });\n return;\n }\n const code = url.searchParams.get('code');\n if (!code) {\n writeJson(response, 400, { code: 'missing_authorization_code', error: 'The OIDC callback did not include an authorization code.' });\n return;\n }\n\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const fields = new URLSearchParams({\n client_id: configuration.clientId,\n code,\n code_verifier: flow.codeVerifier,\n grant_type: 'authorization_code',\n redirect_uri: new URL(CALLBACK_PATH, this.#developmentOrigin).href,\n });\n const tokenResponse = await fetch(discovery.tokenEndpoint, {\n body: fields,\n headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },\n method: 'POST',\n redirect: 'error',\n });\n if (!tokenResponse.ok) {\n throw new Error(`OIDC token exchange failed (${tokenResponse.status}).`);\n }\n const tokens = parseTokenResponse(await tokenResponse.json(), true);\n const idToken = tokens.idToken;\n if (!idToken) {\n throw new Error('OIDC token endpoint did not return an ID token.');\n }\n const jwks = this.#jwks.get(discovery.jwksUri) ?? createRemoteJWKSet(new URL(discovery.jwksUri));\n this.#jwks.set(discovery.jwksUri, jwks);\n const verified = await jwtVerify(idToken, jwks, {\n audience: configuration.clientId,\n issuer: discovery.issuer,\n requiredClaims: ['sub', 'nonce'],\n });\n if (verified.payload.nonce !== flow.nonce) {\n throw new Error('OIDC ID token nonce does not match the authorization request.');\n }\n\n const sessionId = base64Url();\n this.#sessions.set(sessionId, {\n accessToken: tokens.accessToken,\n expiresAt: Date.now() + tokens.expiresIn * 1000,\n idToken,\n moduleBootstrapTail: Promise.resolve(),\n moduleCookies: new Map(),\n moduleNavigationContext: null,\n refreshToken: tokens.refreshToken,\n });\n redirect(response, flow.returnTo, [cookie(SESSION_COOKIE, sessionId, SESSION_MAX_AGE_SECONDS), clearCookie(FLOW_COOKIE)]);\n }\n\n async #logout(request: IncomingMessage, response: ServerResponse): Promise<void> {\n const sessionId = cookiesOf(request).get(SESSION_COOKIE);\n const session = sessionId ? this.#sessions.get(sessionId) : undefined;\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n if (session) {\n void this.#revoke(session).catch(() => undefined);\n }\n const logoutId = base64Url();\n this.#logoutFlows.set(logoutId, { createdAt: Date.now(), idToken: session?.idToken ?? null });\n response.setHeader('Set-Cookie', [clearCookie(SESSION_COOKIE), cookie(LOGOUT_COOKIE, logoutId, FLOW_MAX_AGE_SECONDS)]);\n writeJson(response, 200, { loggedOut: true });\n }\n\n async #revoke(session: ITokenSession): Promise<void> {\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const revocationEndpoint = discovery.revocationEndpoint;\n if (!revocationEndpoint) {\n return;\n }\n const revoke = async (token: string, hint: string): Promise<void> => {\n await fetch(revocationEndpoint, {\n body: new URLSearchParams({ client_id: configuration.clientId, token, token_type_hint: hint }),\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n method: 'POST',\n redirect: 'error',\n });\n };\n await Promise.all([\n revoke(session.accessToken, 'access_token'),\n session.refreshToken ? revoke(session.refreshToken, 'refresh_token') : Promise.resolve(),\n ]);\n }\n\n async #authenticatedSession(request: IncomingMessage): Promise<ITokenSession | null> {\n const sessionId = cookiesOf(request).get(SESSION_COOKIE);\n const session = sessionId ? this.#sessions.get(sessionId) : undefined;\n if (!session) {\n return null;\n }\n if (session.expiresAt > Date.now() + TOKEN_REFRESH_SKEW_MS) {\n return session;\n }\n if (!session.refreshToken) {\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n return null;\n }\n session.refreshPromise ??= this.#refresh(session).finally(() => {\n session.refreshPromise = undefined;\n });\n try {\n await session.refreshPromise;\n return session;\n } catch {\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n return null;\n }\n }\n\n async #refresh(session: ITokenSession): Promise<void> {\n const refreshToken = session.refreshToken;\n if (!refreshToken) {\n throw new Error('OIDC refresh token is unavailable.');\n }\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const response = await fetch(discovery.tokenEndpoint, {\n body: new URLSearchParams({\n client_id: configuration.clientId,\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n }),\n headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },\n method: 'POST',\n redirect: 'error',\n });\n if (!response.ok) {\n throw new Error(`OIDC refresh failed (${response.status}).`);\n }\n const tokens = parseTokenResponse(await response.json(), false);\n session.accessToken = tokens.accessToken;\n session.expiresAt = Date.now() + tokens.expiresIn * 1000;\n session.refreshToken = tokens.refreshToken ?? session.refreshToken;\n session.idToken = tokens.idToken ?? session.idToken;\n }\n\n async #proxy(request: IncomingMessage, response: ServerResponse, localUrl: URL): Promise<void> {\n const session = await this.#authenticatedSession(request);\n if (!session) {\n // This server refreshes the session's tokens itself, so the browser can only sign in again. A refreshable code\n // would send the host client to a refresh endpoint that only the host has.\n writeJson(response, 401, { code: 'sign_in_required', error: 'Sign in is required.' });\n return;\n }\n const isModuleApi = localUrl.pathname.startsWith(this.#moduleApiPath);\n if (isModuleApi && session.moduleCookies.size === 0) {\n const previous = session.moduleBootstrapTail;\n let release = (): void => {};\n const current = new Promise<void>((resolve) => {\n release = resolve;\n });\n session.moduleBootstrapTail = previous.catch(() => undefined).then(() => current);\n await previous.catch(() => undefined);\n try {\n await this.#proxyAuthenticated(request, response, localUrl, session, true);\n } finally {\n release();\n }\n return;\n }\n await this.#proxyAuthenticated(request, response, localUrl, session, isModuleApi);\n }\n\n async #proxyAuthenticated(\n request: IncomingMessage,\n response: ServerResponse,\n localUrl: URL,\n session: ITokenSession,\n isModuleApi: boolean,\n ): Promise<void> {\n const upstreamOrigin = isModuleApi ? this.#moduleApiOrigin : this.#hostOrigin;\n const upstreamUrl = new URL(`${localUrl.pathname}${localUrl.search}`, upstreamOrigin);\n const method = request.method ?? 'GET';\n const init: RequestInit & { duplex?: 'half' } = {\n headers: proxyHeaders(\n request,\n session.accessToken,\n isModuleApi ? session.moduleCookies : undefined,\n isModuleApi ? session.moduleNavigationContext : undefined,\n ),\n method,\n redirect: 'manual',\n };\n if (method !== 'GET' && method !== 'HEAD') {\n init.body = Readable.toWeb(request) as unknown as BodyInit;\n init.duplex = 'half';\n }\n const upstream = await fetch(upstreamUrl, init);\n if (isModuleApi) {\n updateModuleCookies(session.moduleCookies, upstream.headers);\n } else if (localUrl.pathname === '/api/navigation' && upstream.ok) {\n session.moduleNavigationContext = navigationContextIn(\n await upstream\n .clone()\n .json()\n .catch(() => null),\n );\n }\n await copyProxyResponse(upstream, response);\n }\n\n #prune(): void {\n const now = Date.now();\n for (const [id, flow] of this.#flows) {\n if (flow.createdAt + FLOW_MAX_AGE_SECONDS * 1000 < now) {\n this.#flows.delete(id);\n }\n }\n for (const [id, flow] of this.#logoutFlows) {\n if (flow.createdAt + FLOW_MAX_AGE_SECONDS * 1000 < now) {\n this.#logoutFlows.delete(id);\n }\n }\n for (const [id, session] of this.#sessions) {\n if (!session.refreshToken && session.expiresAt + TOKEN_REFRESH_SKEW_MS < now) {\n this.#sessions.delete(id);\n }\n }\n }\n}\n\nexport const mfDevelopmentBffPlugin = (options: IMfDevelopmentBffPluginOptions): Plugin => {\n const middleware = createMfDevelopmentBffMiddleware(options);\n return {\n apply: 'serve',\n name: 'edynamix-mf-development-bff',\n configureServer(server) {\n server.middlewares.use((request, response, next) => {\n middleware(request, response, next);\n });\n },\n // Locally the module runs inside the host shell, so its page shows the host's icon. The module's own favicon is\n // only copied into the build output, so this server has none to answer the browser's favicon request with.\n transformIndexHtml(html) {\n return {\n html: html.replace(ICON_LINK_PATTERN, ''),\n tags: [{ tag: 'link', attrs: { rel: 'icon', type: 'image/svg+xml', href: HOST_ICON_PATH }, injectTo: 'head' }],\n };\n },\n };\n};\n\nexport type TMfDevelopmentBffMiddleware = (request: IncomingMessage, response: ServerResponse, next: (error?: unknown) => void) => void;\n\nexport const createMfDevelopmentBffMiddleware = (options: IMfDevelopmentBffPluginOptions): TMfDevelopmentBffMiddleware => {\n const bff = new MfDevelopmentBff(options);\n return (request, response, next) => {\n void bff.handle(request, response, next);\n };\n};\n"]}
1
+ {"version":3,"file":"vite-development.js","sourceRoot":"","sources":["../src/vite-development.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { Readable } from 'node:stream';\nimport { createRemoteJWKSet, jwtVerify } from 'jose';\nimport type { Plugin } from 'vite';\n\nconst SESSION_COOKIE = '__Host-edyn_dev_session';\nconst REFRESH_COOKIE = '__Host-edyn_dev_refresh';\nconst FLOW_COOKIE = '__Host-edyn_oidc_flow';\nconst LOGOUT_COOKIE = '__Host-edyn_oidc_logout';\nconst CALLBACK_PATH = '/callback';\nconst LOGGED_OUT_PATH = '/logged-out';\nconst LOGIN_PATH = '/api/dev-auth/login';\nexport const MF_DEVELOPMENT_HOST_NAVIGATION_PATH = '/api/dev-host-navigation/index.js';\nconst HOST_ICON_PATH = '/exsto-icon.svg';\nconst HOST_NAVIGATION_ASSET_PATHS = new Set([HOST_ICON_PATH, '/exsto-text.svg']);\nconst ICON_LINK_PATTERN = /<link\\b[^>]*\\brel=[\"'](?:shortcut\\s+)?icon[\"'][^>]*>\\s*/gi;\nconst MAX_HOST_NAVIGATION_BYTES = 5 * 1024 * 1024;\nconst MAX_HOST_NAVIGATION_ASSET_BYTES = 512 * 1024;\nconst SESSION_MAX_AGE_SECONDS = 8 * 60 * 60;\nconst REFRESH_COOKIE_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;\nconst FLOW_MAX_AGE_SECONDS = 10 * 60;\nconst TOKEN_REFRESH_SKEW_MS = 30_000;\nconst NAVIGATION_GROUP_HEADER = 'X-Edyn-Navigation-Group-Id';\nconst NAVIGATION_DEALER_HEADER = 'X-Edyn-Navigation-Dealer-Id';\nconst NAVIGATION_DEALERS_HEADER = 'X-Edyn-Navigation-Dealer-Ids';\nconst NAVIGATION_HEADERS = [NAVIGATION_GROUP_HEADER, NAVIGATION_DEALER_HEADER, NAVIGATION_DEALERS_HEADER].map((name) => name.toLowerCase());\nconst MAX_NAVIGATION_EVENT_BYTES = 5 * 1024 * 1024;\nconst MAX_REQUEST_BODY_BYTES = 100 * 1024 * 1024;\nconst EVENT_SEPARATOR = /\\r?\\n\\r?\\n/;\nconst COOKIE_NAME_PATTERN = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\nconst COOKIE_VALUE_PATTERN = /^[\\x20-\\x7e]*$/;\nconst ALLOWED_HOST_API_PATHS = [\n '/api/auth/me',\n '/api/navigation',\n '/api/navigation/access-check',\n '/api/navigation/access-request',\n '/api/navigation/counter-stream',\n] as const;\n\ninterface IOidcClientConfiguration {\n clientId: string;\n issuer: string;\n scope: string;\n}\n\ninterface IOidcDiscovery {\n authorizationEndpoint: string;\n endSessionEndpoint: string;\n issuer: string;\n jwksUri: string;\n revocationEndpoint: string | null;\n tokenEndpoint: string;\n}\n\ninterface IOidcFlow {\n codeVerifier: string;\n createdAt: number;\n nonce: string;\n returnTo: string;\n state: string;\n}\n\ninterface IModuleNavigationContext {\n dealerIds: readonly number[];\n groupId: number;\n}\n\ninterface IModuleProxyContext {\n cookies: Map<string, string>;\n navigationContext: IModuleNavigationContext | null;\n}\n\ninterface ITokenSession {\n accessToken: string;\n expiresAt: number;\n idToken: string | null;\n moduleBootstrapTail: Promise<void>;\n moduleCookies: Map<string, string>;\n moduleNavigationContext: IModuleNavigationContext | null;\n refreshPromise?: Promise<void>;\n refreshToken: string | null;\n}\n\ninterface ILogoutFlow {\n createdAt: number;\n idToken: string | null;\n}\n\ninterface ITokenResponse {\n accessToken: string;\n expiresIn: number;\n idToken: string | null;\n refreshToken: string | null;\n tokenType: string;\n}\n\nexport interface IMfDevelopmentBffPluginOptions {\n developmentOrigin?: string;\n hostNavigationUrl: string;\n moduleApiOrigin: string;\n moduleApiPath: string;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst requiredString = (value: Record<string, unknown>, key: string): string => {\n const candidate = value[key];\n if (typeof candidate !== 'string' || candidate.trim() === '') {\n throw new Error(`OIDC response is missing ${key}.`);\n }\n return candidate;\n};\n\nconst optionalString = (value: Record<string, unknown>, key: string): string | null => {\n const candidate = value[key];\n if (candidate === undefined || candidate === null) {\n return null;\n }\n if (typeof candidate !== 'string' || candidate.trim() === '') {\n throw new Error(`OIDC response contains an invalid ${key}.`);\n }\n return candidate;\n};\n\nconst absoluteHttpsUrl = (candidate: string, label: string, allowLocalHttp = false): URL => {\n let url: URL;\n try {\n url = new URL(candidate);\n } catch {\n throw new Error(`${label} must be an absolute URL.`);\n }\n const isLocalHttp = allowLocalHttp && url.protocol === 'http:' && (url.hostname === 'localhost' || url.hostname === '127.0.0.1');\n if (url.protocol !== 'https:' && !isLocalHttp) {\n throw new Error(`${label} must use HTTPS.`);\n }\n if (url.username || url.password || url.hash) {\n throw new Error(`${label} must not contain credentials or a fragment.`);\n }\n return url;\n};\n\nconst moduleApiPrefix = (candidate: string): string => {\n if (!/^\\/[a-z][a-z0-9-]*\\/api\\/$/.test(candidate)) {\n throw new Error('moduleApiPath must use the canonical /module/api/ boundary.');\n }\n return candidate;\n};\n\nconst base64Url = (size = 32): string => randomBytes(size).toString('base64url');\nconst codeChallengeOf = (verifier: string): string => createHash('sha256').update(verifier).digest('base64url');\n\nconst cookiesOf = (request: IncomingMessage): ReadonlyMap<string, string> => {\n const cookies = new Map<string, string>();\n for (const part of (request.headers.cookie ?? '').split(';')) {\n const separator = part.indexOf('=');\n if (separator <= 0) {\n continue;\n }\n const name = part.slice(0, separator).trim();\n const value = part.slice(separator + 1).trim();\n if (name && value) {\n cookies.set(name, value);\n }\n }\n return cookies;\n};\n\nconst cookie = (name: string, value: string, maxAge: number): string => `${name}=${value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`;\n\nconst clearCookie = (name: string): string => cookie(name, '', 0);\n\n// The refresh token stays in an HttpOnly cookie for this origin, which every module's development server shares, so\n// one sign-in survives restarts and module switches until the token expires; page scripts can never read it.\nconst refreshCookieOf = (session: ITokenSession): string =>\n session.refreshToken\n ? cookie(REFRESH_COOKIE, encodeURIComponent(session.refreshToken), REFRESH_COOKIE_MAX_AGE_SECONDS)\n : clearCookie(REFRESH_COOKIE);\n\nconst decodedCookieValue = (value: string | undefined): string | null => {\n if (!value) {\n return null;\n }\n try {\n return decodeURIComponent(value);\n } catch {\n return null;\n }\n};\n\nconst writeJson = (response: ServerResponse, status: number, body: unknown): void => {\n response.statusCode = status;\n response.setHeader('Cache-Control', 'no-store');\n response.setHeader('Content-Type', 'application/json; charset=utf-8');\n response.end(JSON.stringify(body));\n};\n\nconst redirect = (response: ServerResponse, location: string, cookies: string[] = []): void => {\n response.statusCode = 302;\n response.setHeader('Cache-Control', 'no-store');\n response.setHeader('Location', location);\n if (cookies.length > 0) {\n response.setHeader('Set-Cookie', cookies);\n }\n response.end();\n};\n\nconst safeReturnTo = (candidate: string | null): string => {\n if (!candidate?.startsWith('/') || candidate.startsWith('//')) {\n return '/';\n }\n try {\n const parsed = new URL(candidate, 'https://localhost');\n return `${parsed.pathname}${parsed.search}${parsed.hash}`;\n } catch {\n return '/';\n }\n};\n\nconst parseTokenResponse = (value: unknown, requireIdToken: boolean): ITokenResponse => {\n if (!isRecord(value)) {\n throw new Error('OIDC token endpoint returned an invalid response.');\n }\n const expiresIn = value.expires_in;\n if (typeof expiresIn !== 'number' || !Number.isFinite(expiresIn) || expiresIn <= 0) {\n throw new Error('OIDC token endpoint returned an invalid expires_in value.');\n }\n const idToken = optionalString(value, 'id_token');\n if (requireIdToken && !idToken) {\n throw new Error('OIDC token endpoint did not return an ID token.');\n }\n const tokenType = requiredString(value, 'token_type');\n if (tokenType.toLowerCase() !== 'bearer') {\n throw new Error('OIDC token endpoint returned an unsupported token type.');\n }\n return {\n accessToken: requiredString(value, 'access_token'),\n expiresIn,\n idToken,\n refreshToken: optionalString(value, 'refresh_token'),\n tokenType,\n };\n};\n\nconst pathIsHostApi = (path: string): boolean => {\n return ALLOWED_HOST_API_PATHS.some((candidate) => path === candidate || path.startsWith(`${candidate}/`));\n};\n\n// The host validates a selection the browser sends it, so host requests keep the navigation headers. A module trusts\n// them, so its requests carry only the selection the host last confirmed.\nconst proxyHeaders = (request: IncomingMessage, accessToken: string, module?: IModuleProxyContext): Headers => {\n const headers = new Headers();\n for (const [name, value] of Object.entries(request.headers)) {\n const lowerName = name.toLowerCase();\n if (\n !value ||\n name.startsWith(':') ||\n ['authorization', 'connection', 'content-length', 'cookie', 'host', 'origin', 'transfer-encoding'].includes(lowerName) ||\n (module && NAVIGATION_HEADERS.includes(lowerName))\n ) {\n continue;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n headers.append(name, item);\n }\n } else {\n headers.set(name, value);\n }\n }\n headers.set('Authorization', `Bearer ${accessToken}`);\n if (module?.cookies.size) {\n headers.set('Cookie', [...module.cookies.values()].join('; '));\n }\n const navigationContext = module?.navigationContext;\n if (navigationContext) {\n headers.set(NAVIGATION_GROUP_HEADER, String(navigationContext.groupId));\n headers.set(NAVIGATION_DEALERS_HEADER, navigationContext.dealerIds.join(','));\n if (navigationContext.dealerIds.length === 1) {\n headers.set(NAVIGATION_DEALER_HEADER, String(navigationContext.dealerIds[0]));\n }\n }\n return headers;\n};\n\nconst navigationContextIn = (value: unknown): IModuleNavigationContext | null => {\n if (!isRecord(value) || !isRecord(value.context)) {\n return null;\n }\n const { selectedDealerId, selectedDealerIds, selectedGroupId } = value.context;\n const dealerIds: unknown[] = Array.isArray(selectedDealerIds) ? selectedDealerIds : [selectedDealerId];\n if (\n !Number.isSafeInteger(selectedGroupId) ||\n (selectedGroupId as number) <= 0 ||\n dealerIds.length === 0 ||\n !dealerIds.every((dealerId) => Number.isSafeInteger(dealerId) && (dealerId as number) >= 0)\n ) {\n return null;\n }\n return { dealerIds: dealerIds as number[], groupId: selectedGroupId as number };\n};\n\n// Reads the selection each `navigation` event of the host's navigation stream confirms, chunk by chunk as it passes.\nconst navigationEventReader = (onContext: (context: IModuleNavigationContext) => void): ((chunk: Buffer) => void) => {\n const decoder = new TextDecoder();\n let pending = '';\n return (chunk) => {\n pending += decoder.decode(chunk, { stream: true });\n for (let separator = EVENT_SEPARATOR.exec(pending); separator; separator = EVENT_SEPARATOR.exec(pending)) {\n const block = pending.slice(0, separator.index);\n pending = pending.slice(separator.index + separator[0].length);\n const lines = block.split(/\\r?\\n/);\n if (!lines.includes('event: navigation') && !lines.includes('event:navigation')) {\n continue;\n }\n const data = lines\n .filter((line) => line.startsWith('data:'))\n .map((line) => line.slice(5).replace(/^ /, ''))\n .join('\\n');\n let payload: unknown = null;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n const context = navigationContextIn(payload);\n if (context) {\n onContext(context);\n }\n }\n if (pending.length > MAX_NAVIGATION_EVENT_BYTES) {\n pending = '';\n }\n };\n};\n\nconst updateModuleCookies = (cookies: Map<string, string>, headers: Headers): void => {\n const values = headers.getSetCookie();\n for (const value of values) {\n if (value.length > 4096) {\n continue;\n }\n const [pair, ...attributes] = value.split(';');\n const separator = pair?.indexOf('=') ?? -1;\n if (!pair || separator <= 0) {\n continue;\n }\n const name = pair.slice(0, separator).trim();\n const cookieValue = pair.slice(separator + 1).trim();\n if (!COOKIE_NAME_PATTERN.test(name) || !COOKIE_VALUE_PATTERN.test(cookieValue) || cookieValue.includes(';')) {\n continue;\n }\n const deleted = cookieValue === '' || attributes.some((attribute) => /^\\s*max-age\\s*=\\s*0\\s*$/i.test(attribute));\n if (deleted) {\n cookies.delete(name);\n continue;\n }\n if (!cookies.has(name) && cookies.size >= 32) {\n continue;\n }\n cookies.set(name, `${name}=${cookieValue}`);\n }\n};\n\nconst requestBodyOf = async (request: IncomingMessage): Promise<Buffer | null> => {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of request) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string);\n size += buffer.length;\n if (size > MAX_REQUEST_BODY_BYTES) {\n return null;\n }\n chunks.push(buffer);\n }\n return Buffer.concat(chunks);\n};\n\nconst copyProxyResponse = async (upstream: Response, response: ServerResponse, onChunk?: (chunk: Buffer) => void): Promise<void> => {\n response.statusCode = upstream.status;\n for (const [name, value] of upstream.headers) {\n if (!['connection', 'content-encoding', 'content-length', 'set-cookie', 'transfer-encoding'].includes(name.toLowerCase())) {\n response.setHeader(name, value);\n }\n }\n response.setHeader('Cache-Control', upstream.headers.get('cache-control') ?? 'no-store');\n if (!upstream.body) {\n response.end();\n return;\n }\n await new Promise<void>((resolve, reject) => {\n const readable = Readable.fromWeb(upstream.body as never);\n if (onChunk) {\n readable.on('data', onChunk);\n }\n readable.once('error', reject);\n response.once('error', reject);\n response.once('finish', resolve);\n readable.pipe(response);\n });\n};\n\nconst readResponseBody = async (response: Response, maximumBytes: number, label: string): Promise<Buffer> => {\n const declaredLength = Number(response.headers.get('content-length'));\n if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {\n await response.body?.cancel();\n throw new Error(`${label} exceeded the configured size limit.`);\n }\n if (!response.body) {\n return Buffer.alloc(0);\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let totalBytes = 0;\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n totalBytes += value.byteLength;\n if (totalBytes > maximumBytes) {\n await reader.cancel();\n throw new Error(`${label} exceeded the configured size limit.`);\n }\n chunks.push(value);\n }\n } finally {\n reader.releaseLock();\n }\n return Buffer.concat(chunks, totalBytes);\n};\n\nclass MfDevelopmentBff {\n readonly #configurationUrl: URL;\n readonly #developmentOrigin: URL;\n readonly #flows = new Map<string, IOidcFlow>();\n readonly #hostOrigin: URL;\n readonly #hostNavigationUrl: URL;\n readonly #jwks = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n readonly #logoutFlows = new Map<string, ILogoutFlow>();\n readonly #moduleApiOrigin: URL;\n readonly #moduleApiPath: string;\n readonly #sessions = new Map<string, ITokenSession>();\n readonly #restores = new Map<string, Promise<{ session: ITokenSession; sessionId: string } | null>>();\n #configurationPromise: Promise<IOidcClientConfiguration> | null = null;\n #discoveryPromise: Promise<IOidcDiscovery> | null = null;\n\n constructor(options: IMfDevelopmentBffPluginOptions) {\n const hostNavigation = absoluteHttpsUrl(options.hostNavigationUrl, 'hostNavigationUrl', true);\n this.#hostNavigationUrl = hostNavigation;\n this.#hostOrigin = new URL(hostNavigation.origin);\n this.#configurationUrl = new URL('/api/auth/oidc-configuration', this.#hostOrigin);\n this.#developmentOrigin = absoluteHttpsUrl(options.developmentOrigin ?? 'https://localhost:4200', 'developmentOrigin');\n if (this.#developmentOrigin.pathname !== '/' || this.#developmentOrigin.search || this.#developmentOrigin.hash) {\n throw new Error('developmentOrigin must be an origin without a path, query, or fragment.');\n }\n this.#moduleApiOrigin = absoluteHttpsUrl(options.moduleApiOrigin, 'moduleApiOrigin', true);\n if (this.#moduleApiOrigin.pathname !== '/' || this.#moduleApiOrigin.search || this.#moduleApiOrigin.hash) {\n throw new Error('moduleApiOrigin must be an origin without a path, query, or fragment.');\n }\n this.#moduleApiPath = moduleApiPrefix(options.moduleApiPath);\n }\n\n async handle(request: IncomingMessage, response: ServerResponse, next: (error?: unknown) => void): Promise<void> {\n if (!request.url || !request.method) {\n next();\n return;\n }\n const url = new URL(request.url, this.#developmentOrigin);\n this.#prune();\n\n try {\n if (request.method === 'GET' && url.pathname === MF_DEVELOPMENT_HOST_NAVIGATION_PATH) {\n await this.#hostNavigationResource(response, this.#hostNavigationUrl, 'script');\n return;\n }\n if (request.method === 'GET' && HOST_NAVIGATION_ASSET_PATHS.has(url.pathname)) {\n await this.#hostNavigationResource(response, new URL(url.pathname, this.#hostOrigin), 'asset');\n return;\n }\n if (request.method === 'GET' && url.pathname === LOGIN_PATH) {\n await this.#login(request, response, safeReturnTo(url.searchParams.get('returnTo')));\n return;\n }\n if (request.method === 'GET' && url.pathname === CALLBACK_PATH) {\n await this.#callback(request, response, url);\n return;\n }\n if (request.method === 'GET' && url.pathname === LOGGED_OUT_PATH) {\n const returnTo = safeReturnTo(url.searchParams.get('state'));\n redirect(response, `${LOGIN_PATH}?returnTo=${encodeURIComponent(returnTo)}`, [clearCookie(LOGOUT_COOKIE)]);\n return;\n }\n if (request.method === 'GET' && url.pathname === '/frontchannel-logout') {\n const sessionId = cookiesOf(request).get(SESSION_COOKIE);\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n response.statusCode = 204;\n response.setHeader('Set-Cookie', [clearCookie(SESSION_COOKIE), clearCookie(REFRESH_COOKIE)]);\n response.end();\n return;\n }\n if (request.method === 'POST' && url.pathname === '/api/auth/logout') {\n await this.#logout(request, response);\n return;\n }\n if (pathIsHostApi(url.pathname) || url.pathname.startsWith(this.#moduleApiPath)) {\n await this.#proxy(request, response, url);\n return;\n }\n next();\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Development authentication failed.';\n writeJson(response, 502, { code: 'development_authentication_failed', error: message });\n }\n }\n\n async #hostNavigationResource(response: ServerResponse, upstreamUrl: URL, kind: 'asset' | 'script'): Promise<void> {\n const upstream = await fetch(upstreamUrl, {\n headers: { Accept: kind === 'script' ? 'text/javascript, application/javascript' : 'image/svg+xml' },\n redirect: 'error',\n });\n if (!upstream.ok) {\n throw new Error(`Host navigation ${kind} failed (${upstream.status}).`);\n }\n const contentType = upstream.headers.get('content-type') ?? '';\n const validContentType =\n kind === 'script' ? /^(?:text|application)\\/javascript(?:;|$)/i.test(contentType) : /^image\\/svg\\+xml(?:;|$)/i.test(contentType);\n if (!validContentType) {\n throw new Error(`Host navigation ${kind} returned an invalid content type.`);\n }\n const maximumBytes = kind === 'script' ? MAX_HOST_NAVIGATION_BYTES : MAX_HOST_NAVIGATION_ASSET_BYTES;\n const body = await readResponseBody(upstream, maximumBytes, `Host navigation ${kind}`);\n response.statusCode = 200;\n response.setHeader('Cache-Control', 'no-store');\n response.setHeader('Content-Type', contentType);\n response.setHeader('X-Content-Type-Options', 'nosniff');\n response.end(body);\n }\n\n async #configuration(): Promise<IOidcClientConfiguration> {\n this.#configurationPromise ??= fetch(this.#configurationUrl, { headers: { Accept: 'application/json' }, redirect: 'error' })\n .then(async (response) => {\n if (!response.ok) {\n throw new Error(`Host OIDC configuration failed (${response.status}).`);\n }\n const value: unknown = await response.json();\n if (!isRecord(value)) {\n throw new Error('Host OIDC configuration returned an invalid response.');\n }\n const issuer = absoluteHttpsUrl(requiredString(value, 'issuer'), 'OIDC issuer', true).href.replace(/\\/$/, '');\n return { clientId: requiredString(value, 'clientId'), issuer, scope: requiredString(value, 'scope') };\n })\n .catch((error) => {\n this.#configurationPromise = null;\n throw error;\n });\n return this.#configurationPromise;\n }\n\n async #discovery(): Promise<IOidcDiscovery> {\n this.#discoveryPromise ??= this.#configuration()\n .then(async (configuration) => {\n const response = await fetch(`${configuration.issuer}/.well-known/openid-configuration`, {\n headers: { Accept: 'application/json' },\n redirect: 'error',\n });\n if (!response.ok) {\n throw new Error(`OIDC discovery failed (${response.status}).`);\n }\n const value: unknown = await response.json();\n if (!isRecord(value)) {\n throw new Error('OIDC discovery returned an invalid response.');\n }\n const issuer = absoluteHttpsUrl(requiredString(value, 'issuer'), 'OIDC discovery issuer', true).href.replace(/\\/$/, '');\n if (issuer !== configuration.issuer) {\n throw new Error('OIDC discovery issuer does not match the host configuration.');\n }\n const endpoint = (key: string): string => {\n const url = absoluteHttpsUrl(requiredString(value, key), `OIDC ${key}`, true);\n if (url.origin !== new URL(issuer).origin) {\n throw new Error(`OIDC ${key} must use the configured issuer origin.`);\n }\n return url.href;\n };\n const revocation = optionalString(value, 'revocation_endpoint');\n return {\n authorizationEndpoint: endpoint('authorization_endpoint'),\n endSessionEndpoint: endpoint('end_session_endpoint'),\n issuer,\n jwksUri: endpoint('jwks_uri'),\n revocationEndpoint: revocation ? endpoint('revocation_endpoint') : null,\n tokenEndpoint: endpoint('token_endpoint'),\n };\n })\n .catch((error) => {\n this.#discoveryPromise = null;\n throw error;\n });\n return this.#discoveryPromise;\n }\n\n async #login(request: IncomingMessage, response: ServerResponse, returnTo: string): Promise<void> {\n const logoutId = cookiesOf(request).get(LOGOUT_COOKIE);\n if (logoutId) {\n const logout = this.#logoutFlows.get(logoutId);\n this.#logoutFlows.delete(logoutId);\n if (logout) {\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const endpoint = new URL(discovery.endSessionEndpoint);\n endpoint.searchParams.set('client_id', configuration.clientId);\n endpoint.searchParams.set('post_logout_redirect_uri', new URL(LOGGED_OUT_PATH, this.#developmentOrigin).href);\n endpoint.searchParams.set('state', returnTo);\n if (logout.idToken) {\n endpoint.searchParams.set('id_token_hint', logout.idToken);\n }\n redirect(response, endpoint.href, [clearCookie(LOGOUT_COOKIE)]);\n return;\n }\n }\n\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const flowId = base64Url();\n const codeVerifier = base64Url(48);\n const flow: IOidcFlow = { codeVerifier, createdAt: Date.now(), nonce: base64Url(), returnTo, state: base64Url() };\n this.#flows.set(flowId, flow);\n\n const endpoint = new URL(discovery.authorizationEndpoint);\n endpoint.searchParams.set('response_type', 'code');\n endpoint.searchParams.set('client_id', configuration.clientId);\n endpoint.searchParams.set('redirect_uri', new URL(CALLBACK_PATH, this.#developmentOrigin).href);\n endpoint.searchParams.set('scope', configuration.scope);\n endpoint.searchParams.set('state', flow.state);\n endpoint.searchParams.set('nonce', flow.nonce);\n endpoint.searchParams.set('code_challenge', codeChallengeOf(codeVerifier));\n endpoint.searchParams.set('code_challenge_method', 'S256');\n redirect(response, endpoint.href, [cookie(FLOW_COOKIE, flowId, FLOW_MAX_AGE_SECONDS)]);\n }\n\n async #callback(request: IncomingMessage, response: ServerResponse, url: URL): Promise<void> {\n const flowId = cookiesOf(request).get(FLOW_COOKIE);\n const flow = flowId ? this.#flows.get(flowId) : undefined;\n if (flowId) {\n this.#flows.delete(flowId);\n }\n if (!flow || url.searchParams.get('state') !== flow.state) {\n writeJson(response, 400, { code: 'invalid_oidc_state', error: 'The OIDC callback state is invalid or expired.' });\n return;\n }\n const providerError = url.searchParams.get('error');\n if (providerError) {\n writeJson(response, 401, { code: providerError, error: url.searchParams.get('error_description') ?? 'Sign-in was rejected.' });\n return;\n }\n const code = url.searchParams.get('code');\n if (!code) {\n writeJson(response, 400, { code: 'missing_authorization_code', error: 'The OIDC callback did not include an authorization code.' });\n return;\n }\n\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const fields = new URLSearchParams({\n client_id: configuration.clientId,\n code,\n code_verifier: flow.codeVerifier,\n grant_type: 'authorization_code',\n redirect_uri: new URL(CALLBACK_PATH, this.#developmentOrigin).href,\n });\n const tokenResponse = await fetch(discovery.tokenEndpoint, {\n body: fields,\n headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },\n method: 'POST',\n redirect: 'error',\n });\n if (!tokenResponse.ok) {\n throw new Error(`OIDC token exchange failed (${tokenResponse.status}).`);\n }\n const tokens = parseTokenResponse(await tokenResponse.json(), true);\n const idToken = tokens.idToken;\n if (!idToken) {\n throw new Error('OIDC token endpoint did not return an ID token.');\n }\n const jwks = this.#jwks.get(discovery.jwksUri) ?? createRemoteJWKSet(new URL(discovery.jwksUri));\n this.#jwks.set(discovery.jwksUri, jwks);\n const verified = await jwtVerify(idToken, jwks, {\n audience: configuration.clientId,\n issuer: discovery.issuer,\n requiredClaims: ['sub', 'nonce'],\n });\n if (verified.payload.nonce !== flow.nonce) {\n throw new Error('OIDC ID token nonce does not match the authorization request.');\n }\n\n const sessionId = base64Url();\n const session: ITokenSession = {\n accessToken: tokens.accessToken,\n expiresAt: Date.now() + tokens.expiresIn * 1000,\n idToken,\n moduleBootstrapTail: Promise.resolve(),\n moduleCookies: new Map(),\n moduleNavigationContext: null,\n refreshToken: tokens.refreshToken,\n };\n this.#sessions.set(sessionId, session);\n redirect(response, flow.returnTo, [\n cookie(SESSION_COOKIE, sessionId, SESSION_MAX_AGE_SECONDS),\n refreshCookieOf(session),\n clearCookie(FLOW_COOKIE),\n ]);\n }\n\n async #logout(request: IncomingMessage, response: ServerResponse): Promise<void> {\n const sessionId = cookiesOf(request).get(SESSION_COOKIE);\n const session = sessionId ? this.#sessions.get(sessionId) : undefined;\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n if (session) {\n void this.#revoke(session).catch(() => undefined);\n }\n const logoutId = base64Url();\n this.#logoutFlows.set(logoutId, { createdAt: Date.now(), idToken: session?.idToken ?? null });\n response.setHeader('Set-Cookie', [\n clearCookie(SESSION_COOKIE),\n clearCookie(REFRESH_COOKIE),\n cookie(LOGOUT_COOKIE, logoutId, FLOW_MAX_AGE_SECONDS),\n ]);\n writeJson(response, 200, { loggedOut: true });\n }\n\n async #revoke(session: ITokenSession): Promise<void> {\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const revocationEndpoint = discovery.revocationEndpoint;\n if (!revocationEndpoint) {\n return;\n }\n const revoke = async (token: string, hint: string): Promise<void> => {\n await fetch(revocationEndpoint, {\n body: new URLSearchParams({ client_id: configuration.clientId, token, token_type_hint: hint }),\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n method: 'POST',\n redirect: 'error',\n });\n };\n await Promise.all([\n revoke(session.accessToken, 'access_token'),\n session.refreshToken ? revoke(session.refreshToken, 'refresh_token') : Promise.resolve(),\n ]);\n }\n\n async #authenticatedSession(request: IncomingMessage, response: ServerResponse): Promise<ITokenSession | null> {\n const cookies = cookiesOf(request);\n const sessionId = cookies.get(SESSION_COOKIE);\n const session = sessionId ? this.#sessions.get(sessionId) : undefined;\n if (!session) {\n return this.#restoredSession(decodedCookieValue(cookies.get(REFRESH_COOKIE)), response);\n }\n if (session.expiresAt > Date.now() + TOKEN_REFRESH_SKEW_MS) {\n return session;\n }\n if (!session.refreshToken) {\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n return null;\n }\n session.refreshPromise ??= this.#refresh(session).finally(() => {\n session.refreshPromise = undefined;\n });\n try {\n await session.refreshPromise;\n response.setHeader('Set-Cookie', refreshCookieOf(session));\n return session;\n } catch {\n if (sessionId) {\n this.#sessions.delete(sessionId);\n }\n response.setHeader('Set-Cookie', clearCookie(REFRESH_COOKIE));\n return null;\n }\n }\n\n // Starts a session from the browser's refresh cookie after this server restarted or another module's server signed\n // the developer in. Parallel requests carrying the same token share one refresh.\n async #restoredSession(refreshToken: string | null, response: ServerResponse): Promise<ITokenSession | null> {\n if (!refreshToken) {\n return null;\n }\n let restore = this.#restores.get(refreshToken);\n if (!restore) {\n restore = this.#restore(refreshToken).finally(() => this.#restores.delete(refreshToken));\n this.#restores.set(refreshToken, restore);\n }\n const restored = await restore;\n if (!restored) {\n response.setHeader('Set-Cookie', clearCookie(REFRESH_COOKIE));\n return null;\n }\n response.setHeader('Set-Cookie', [cookie(SESSION_COOKIE, restored.sessionId, SESSION_MAX_AGE_SECONDS), refreshCookieOf(restored.session)]);\n return restored.session;\n }\n\n async #restore(refreshToken: string): Promise<{ session: ITokenSession; sessionId: string } | null> {\n const session: ITokenSession = {\n accessToken: '',\n expiresAt: 0,\n idToken: null,\n moduleBootstrapTail: Promise.resolve(),\n moduleCookies: new Map(),\n moduleNavigationContext: null,\n refreshToken,\n };\n try {\n await this.#refresh(session);\n } catch {\n return null;\n }\n const sessionId = base64Url();\n this.#sessions.set(sessionId, session);\n return { session, sessionId };\n }\n\n async #refresh(session: ITokenSession): Promise<void> {\n const refreshToken = session.refreshToken;\n if (!refreshToken) {\n throw new Error('OIDC refresh token is unavailable.');\n }\n const [configuration, discovery] = await Promise.all([this.#configuration(), this.#discovery()]);\n const response = await fetch(discovery.tokenEndpoint, {\n body: new URLSearchParams({\n client_id: configuration.clientId,\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n }),\n headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },\n method: 'POST',\n redirect: 'error',\n });\n if (!response.ok) {\n throw new Error(`OIDC refresh failed (${response.status}).`);\n }\n const tokens = parseTokenResponse(await response.json(), false);\n session.accessToken = tokens.accessToken;\n session.expiresAt = Date.now() + tokens.expiresIn * 1000;\n session.refreshToken = tokens.refreshToken ?? session.refreshToken;\n session.idToken = tokens.idToken ?? session.idToken;\n }\n\n async #proxy(request: IncomingMessage, response: ServerResponse, localUrl: URL): Promise<void> {\n const session = await this.#authenticatedSession(request, response);\n if (!session) {\n // This server refreshes the session's tokens itself, so the browser can only sign in again. A refreshable code\n // would send the host client to a refresh endpoint that only the host has.\n writeJson(response, 401, { code: 'sign_in_required', error: 'Sign in is required.' });\n return;\n }\n const isModuleApi = localUrl.pathname.startsWith(this.#moduleApiPath);\n if (isModuleApi && session.moduleCookies.size === 0) {\n const previous = session.moduleBootstrapTail;\n let release = (): void => {};\n const current = new Promise<void>((resolve) => {\n release = resolve;\n });\n session.moduleBootstrapTail = previous.catch(() => undefined).then(() => current);\n await previous.catch(() => undefined);\n try {\n await this.#proxyAuthenticated(request, response, localUrl, session, true);\n } finally {\n release();\n }\n return;\n }\n await this.#proxyAuthenticated(request, response, localUrl, session, isModuleApi);\n }\n\n async #proxyAuthenticated(\n request: IncomingMessage,\n response: ServerResponse,\n localUrl: URL,\n session: ITokenSession,\n isModuleApi: boolean,\n ): Promise<void> {\n const upstreamOrigin = isModuleApi ? this.#moduleApiOrigin : this.#hostOrigin;\n const upstreamUrl = new URL(`${localUrl.pathname}${localUrl.search}`, upstreamOrigin);\n const method = request.method ?? 'GET';\n const init: RequestInit = {\n headers: proxyHeaders(\n request,\n session.accessToken,\n isModuleApi ? { cookies: session.moduleCookies, navigationContext: session.moduleNavigationContext } : undefined,\n ),\n method,\n redirect: 'manual',\n };\n if (method !== 'GET' && method !== 'HEAD') {\n // A fixed-length body: the web front ends of the deployed module servers answer chunked request bodies with 404.\n const body = await requestBodyOf(request);\n if (!body) {\n writeJson(response, 413, { code: 'request_body_too_large', error: 'The request body exceeds the development proxy limit.' });\n return;\n }\n init.body = body;\n }\n const upstream = await fetch(upstreamUrl, init);\n let onChunk: ((chunk: Buffer) => void) | undefined;\n if (isModuleApi) {\n updateModuleCookies(session.moduleCookies, upstream.headers);\n } else if (localUrl.pathname === '/api/navigation' && upstream.ok) {\n session.moduleNavigationContext = navigationContextIn(\n await upstream\n .clone()\n .json()\n .catch(() => null),\n );\n } else if (localUrl.pathname === '/api/navigation/stream' && upstream.ok) {\n onChunk = navigationEventReader((context) => {\n session.moduleNavigationContext = context;\n });\n }\n await copyProxyResponse(upstream, response, onChunk);\n }\n\n #prune(): void {\n const now = Date.now();\n for (const [id, flow] of this.#flows) {\n if (flow.createdAt + FLOW_MAX_AGE_SECONDS * 1000 < now) {\n this.#flows.delete(id);\n }\n }\n for (const [id, flow] of this.#logoutFlows) {\n if (flow.createdAt + FLOW_MAX_AGE_SECONDS * 1000 < now) {\n this.#logoutFlows.delete(id);\n }\n }\n for (const [id, session] of this.#sessions) {\n if (!session.refreshToken && session.expiresAt + TOKEN_REFRESH_SKEW_MS < now) {\n this.#sessions.delete(id);\n }\n }\n }\n}\n\nexport const mfDevelopmentBffPlugin = (options: IMfDevelopmentBffPluginOptions): Plugin => {\n const middleware = createMfDevelopmentBffMiddleware(options);\n return {\n apply: 'serve',\n name: 'edynamix-mf-development-bff',\n configureServer(server) {\n server.middlewares.use((request, response, next) => {\n middleware(request, response, next);\n });\n },\n // Locally the module runs inside the host shell, so its page shows the host's icon. The module's own favicon is\n // only copied into the build output, so this server has none to answer the browser's favicon request with.\n transformIndexHtml(html) {\n return {\n html: html.replace(ICON_LINK_PATTERN, ''),\n tags: [{ tag: 'link', attrs: { rel: 'icon', type: 'image/svg+xml', href: HOST_ICON_PATH }, injectTo: 'head' }],\n };\n },\n };\n};\n\nexport type TMfDevelopmentBffMiddleware = (request: IncomingMessage, response: ServerResponse, next: (error?: unknown) => void) => void;\n\nexport const createMfDevelopmentBffMiddleware = (options: IMfDevelopmentBffPluginOptions): TMfDevelopmentBffMiddleware => {\n const bff = new MfDevelopmentBff(options);\n return (request, response, next) => {\n void bff.handle(request, response, next);\n };\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edynamix/mf-isolation",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "description": "CSS and overlay isolation helpers for eDynamix microfrontends.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",