mirror of
https://github.com/hanzoai/chat.git
synced 2026-09-30 20:51:17 +00:00
The shell hands the browser its IAM identity in a global that was called __HANZO_IAM__. Every value inside it was already correct — on lux.chat it reads serverUrl https://lux.id, clientId lux-chat, organization lux — so the last thing saying "Hanzo" on a Lux page was the name we picked for the container, and it is visible in view-source on the customer's own domain. That is the same class as a wrong mark or a wrong link, spelled as an identifier. __IAM__ says what it holds and belongs to no brand. Renamed at both ends in one change, because a half-rename does not degrade — it breaks login outright: the writer would publish a name the reader never looks up, every visitor would fall through to the bundle's compiled default, and lux.chat would send its customers to hanzo.id as hanzo-chat with a redirect_uri that client has never registered. There are exactly six sites and no others: the writer, the reader, two assertions and two comments. Co-authored-by: Hanzo Dev <[email protected]>
83 lines
3.5 KiB
JavaScript
83 lines
3.5 KiB
JavaScript
/**
|
|
* The SPA's IAM identity, resolved at RUNTIME and handed to the browser in the
|
|
* HTML shell.
|
|
*
|
|
* `client/src/utils/iam.ts` used to read this from `import.meta.env.VITE_*`,
|
|
* which Vite INLINES at build time — so the identity of the login client was
|
|
* frozen into the bundle when the image was built. One image could therefore
|
|
* only ever serve one brand's login, no matter what the server's environment
|
|
* said, and the failure was not a refusal to start: a second brand booted
|
|
* cleanly, served its own pages, and then sent every visitor to the FIRST
|
|
* brand's issuer with the first brand's `client_id`. The issuer refuses the
|
|
* unregistered `redirect_uri` and, being unable to redirect anywhere safely,
|
|
* renders a dead end — so the product is unreachable while every health check
|
|
* stays green.
|
|
*
|
|
* The server already knows the right answer: `OPENID_ISSUER` and
|
|
* `OPENID_CLIENT_ID` are what the backend's own passport strategy registers
|
|
* with, so taking the browser's copy from the same two variables makes the two
|
|
* halves of one login incapable of disagreeing. That is the point of doing it
|
|
* here rather than adding a third place to configure.
|
|
*
|
|
* Anything unset falls through to the bundle's compiled default, so a
|
|
* deployment that sets none of this behaves exactly as it did before.
|
|
*/
|
|
|
|
/**
|
|
* The global the SPA reads before its own build-time defaults.
|
|
*
|
|
* Named for WHAT it carries, not for whose it is. It was `__HANZO_IAM__`, and
|
|
* every value inside it was already correct Lux — `serverUrl: lux.id`,
|
|
* `clientId: lux-chat` — so the only thing left saying "Hanzo" on a Lux page was
|
|
* the name we chose for the box. That is the same defect as a mark or a link,
|
|
* just spelled as an identifier, and it is visible in `view-source` on a
|
|
* customer's own domain.
|
|
*/
|
|
const GLOBAL = '__IAM__';
|
|
|
|
/**
|
|
* The browser's IAM config, or null when the environment states none.
|
|
*
|
|
* Keys are omitted rather than emitted empty: the client falls back per KEY, so
|
|
* an empty string would override a good compiled default with nothing.
|
|
*/
|
|
function iamConfig() {
|
|
const config = {};
|
|
if (process.env.OPENID_ISSUER) {
|
|
config.serverUrl = process.env.OPENID_ISSUER;
|
|
}
|
|
if (process.env.OPENID_CLIENT_ID) {
|
|
config.clientId = process.env.OPENID_CLIENT_ID;
|
|
}
|
|
/* The org owning the application. IAM names applications `<org>-<app>`, but a
|
|
convention is a poor thing to derive an identity from, so it is read and
|
|
never inferred. */
|
|
if (process.env.OPENID_ORG) {
|
|
config.organization = process.env.OPENID_ORG;
|
|
}
|
|
return Object.keys(config).length > 0 ? config : null;
|
|
}
|
|
|
|
/**
|
|
* Write the config into the HTML shell, ahead of every module script.
|
|
*
|
|
* `</head>` is the anchor because the scripts are declared in `<body>` and the
|
|
* SPA reads this synchronously as it boots — there is no fetch to await, so the
|
|
* OAuth callback route, which runs before any config request, sees the same
|
|
* values as everything else.
|
|
*/
|
|
function injectIamConfig(html) {
|
|
const config = iamConfig();
|
|
if (!config) {
|
|
return html;
|
|
}
|
|
/* `<` is escaped so a value can never close this script element early. The
|
|
values are our own environment rather than user input, which makes this a
|
|
cheap invariant to keep rather than a live threat to answer. */
|
|
const json = JSON.stringify(config).replace(/</g, '\\u003c');
|
|
const tag = `<script>window.${GLOBAL}=${json}</script>`;
|
|
return html.includes('</head>') ? html.replace('</head>', `${tag}</head>`) : `${tag}${html}`;
|
|
}
|
|
|
|
module.exports = { iamConfig, injectIamConfig, GLOBAL };
|