mirror of
https://github.com/hanzoai/chat.git
synced 2026-09-30 20:51:17 +00:00
A green build says an image was PRODUCED. It says nothing about what is RUNNING, and until now nothing here closed that gap: /v1/chat/config reported what the app is configured to do, never which build was doing it. That is not academic. The redirect_uri fix that unblocks serving this app under a directory behaves IDENTICALLY at a root — `apiBaseUrl()` is '' there — so no observable difference separates a pod that has it from one that does not. The move to hanzo.ai/chat was made on a green build alone, the fix had not reached the pod, and every sign-in landed on a page that could not complete it. github.sha -> --build-arg -> ENV -> `build` in the config payload. Empty outside a built image, which is the honest answer for a dev server rather than a guess at one. With this, the next move is gated on evidence: ask the pod what it is serving, and go when the answer is the commit that carries the fix. Co-authored-by: Hanzo Dev <[email protected]>
195 lines
8.1 KiB
JavaScript
195 lines
8.1 KiB
JavaScript
const crypto = require('crypto');
|
|
const express = require('express');
|
|
const { logger } = require('@hanzochat/data-schemas');
|
|
const { isEnabled, getBalanceConfig } = require('@hanzochat/api');
|
|
const { Constants, CacheKeys, defaultSocialLogins } = require('@hanzochat/data-provider');
|
|
const { getLdapConfig } = require('~/server/services/Config/ldap');
|
|
const { getAppConfig } = require('~/server/services/Config/app');
|
|
const { getGuestConfig } = require('~/server/services/guestConfig');
|
|
const { getProjectByName } = require('~/models/Project');
|
|
const { getLogStores } = require('~/cache');
|
|
|
|
const router = express.Router();
|
|
const emailLoginEnabled =
|
|
process.env.ALLOW_EMAIL_LOGIN === undefined || isEnabled(process.env.ALLOW_EMAIL_LOGIN);
|
|
const passwordResetEnabled = isEnabled(process.env.ALLOW_PASSWORD_RESET);
|
|
|
|
const sharedLinksEnabled =
|
|
process.env.ALLOW_SHARED_LINKS === undefined || isEnabled(process.env.ALLOW_SHARED_LINKS);
|
|
|
|
const publicSharedLinksEnabled =
|
|
sharedLinksEnabled &&
|
|
(process.env.ALLOW_SHARED_LINKS_PUBLIC === undefined ||
|
|
isEnabled(process.env.ALLOW_SHARED_LINKS_PUBLIC));
|
|
|
|
const sharePointFilePickerEnabled = isEnabled(process.env.ENABLE_SHAREPOINT_FILEPICKER);
|
|
|
|
router.get('/', async function (req, res) {
|
|
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
|
|
|
const cachedStartupConfig = await cache.get(CacheKeys.STARTUP_CONFIG);
|
|
if (cachedStartupConfig) {
|
|
res.send(cachedStartupConfig);
|
|
return;
|
|
}
|
|
|
|
const isBirthday = () => {
|
|
const today = new Date();
|
|
return today.getMonth() === 1 && today.getDate() === 11;
|
|
};
|
|
|
|
const instanceProject = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, '_id');
|
|
|
|
const ldap = getLdapConfig();
|
|
|
|
try {
|
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
|
|
|
/* Chat signs in as a PUBLIC client: the browser runs Authorization Code +
|
|
PKCE and this server only verifies the resulting token. There is no
|
|
client secret to hold and no session to seal, so requiring either here
|
|
would hide the sign-in button on a deployment that is correctly
|
|
configured. The issuer and the client id are the whole of it. */
|
|
const isOpenIdEnabled = !!process.env.OPENID_CLIENT_ID && !!process.env.OPENID_ISSUER;
|
|
|
|
const isSamlEnabled =
|
|
!!process.env.SAML_ENTRY_POINT &&
|
|
!!process.env.SAML_ISSUER &&
|
|
!!process.env.SAML_CERT &&
|
|
!!process.env.SAML_SESSION_SECRET;
|
|
|
|
const balanceConfig = getBalanceConfig(appConfig);
|
|
|
|
const guestConfig = getGuestConfig();
|
|
|
|
// Strip server-internal fields from balance config before sending to client.
|
|
// commerce.endpoint leaks K8s service URLs; commerce.token is a bearer secret.
|
|
const clientBalanceConfig = balanceConfig
|
|
? {
|
|
...balanceConfig,
|
|
commerce: balanceConfig.commerce
|
|
? { enabled: !!balanceConfig.commerce.enabled }
|
|
: undefined,
|
|
}
|
|
: null;
|
|
|
|
/** @type {TStartupConfig} */
|
|
const payload = {
|
|
appTitle: process.env.APP_TITLE || 'Hanzo Chat',
|
|
/**
|
|
* The commit this process is SERVING, baked into the image (Dockerfile
|
|
* ARG BUILD_SHA) and read back here.
|
|
*
|
|
* A green build says an image was produced. It says nothing about what is
|
|
* running, and the difference is not academic: a change whose only effect
|
|
* is under a subdirectory behaves identically at a root, so nothing
|
|
* observable separates a pod that has it from one that does not. Moving
|
|
* hanzo.chat to hanzo.ai/chat on a green build alone took sign-in down,
|
|
* because the redirect_uri fix had not reached the pod yet.
|
|
*
|
|
* Empty outside a built image, which is the honest answer for a dev server
|
|
* rather than a guess at one.
|
|
*/
|
|
build: process.env.BUILD_SHA || '',
|
|
socialLogins: appConfig?.registration?.socialLogins ?? defaultSocialLogins,
|
|
discordLoginEnabled: !!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET,
|
|
facebookLoginEnabled:
|
|
!!process.env.FACEBOOK_CLIENT_ID && !!process.env.FACEBOOK_CLIENT_SECRET,
|
|
githubLoginEnabled: !!process.env.GITHUB_CLIENT_ID && !!process.env.GITHUB_CLIENT_SECRET,
|
|
googleLoginEnabled: !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET,
|
|
appleLoginEnabled:
|
|
!!process.env.APPLE_CLIENT_ID &&
|
|
!!process.env.APPLE_TEAM_ID &&
|
|
!!process.env.APPLE_KEY_ID &&
|
|
!!process.env.APPLE_PRIVATE_KEY_PATH,
|
|
openidLoginEnabled: isOpenIdEnabled,
|
|
openidLabel: process.env.OPENID_BUTTON_LABEL || 'Continue with OpenID',
|
|
openidImageUrl: process.env.OPENID_IMAGE_URL,
|
|
openidAutoRedirect: isEnabled(process.env.OPENID_AUTO_REDIRECT),
|
|
samlLoginEnabled: !isOpenIdEnabled && isSamlEnabled,
|
|
samlLabel: process.env.SAML_BUTTON_LABEL,
|
|
samlImageUrl: process.env.SAML_IMAGE_URL,
|
|
serverDomain: process.env.DOMAIN_CLIENT || process.env.DOMAIN_SERVER || 'http://localhost:3080',
|
|
emailLoginEnabled,
|
|
registrationEnabled: !ldap?.enabled && isEnabled(process.env.ALLOW_REGISTRATION),
|
|
socialLoginEnabled: true,
|
|
emailEnabled:
|
|
(!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) &&
|
|
!!process.env.EMAIL_USERNAME &&
|
|
!!process.env.EMAIL_PASSWORD &&
|
|
!!process.env.EMAIL_FROM,
|
|
passwordResetEnabled,
|
|
showBirthdayIcon:
|
|
isBirthday() ||
|
|
isEnabled(process.env.SHOW_BIRTHDAY_ICON) ||
|
|
process.env.SHOW_BIRTHDAY_ICON === '',
|
|
// The docs site serves this page at /docs/chat; the bare /chat is a 404.
|
|
helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://docs.hanzo.ai/docs/chat',
|
|
interface: appConfig?.interfaceConfig,
|
|
turnstile: appConfig?.turnstileConfig,
|
|
modelSpecs: appConfig?.modelSpecs,
|
|
balance: clientBalanceConfig,
|
|
sharedLinksEnabled,
|
|
publicSharedLinksEnabled,
|
|
instanceProjectId: crypto.createHash('sha256').update(instanceProject._id.toString()).digest('hex').slice(0, 16),
|
|
bundlerURL: process.env.SANDPACK_BUNDLER_URL,
|
|
staticBundlerURL: process.env.SANDPACK_STATIC_BUNDLER_URL,
|
|
sharePointFilePickerEnabled,
|
|
sharePointBaseUrl: process.env.SHAREPOINT_BASE_URL,
|
|
sharePointPickerGraphScope: process.env.SHAREPOINT_PICKER_GRAPH_SCOPE,
|
|
sharePointPickerSharePointScope: process.env.SHAREPOINT_PICKER_SHAREPOINT_SCOPE,
|
|
allowGuestChat: guestConfig.enabled,
|
|
guestMessageMax: guestConfig.enabled ? guestConfig.messageMax : undefined,
|
|
// The free model a guest is served, so the client can open the guest
|
|
// conversation on it and name it, rather than a stale default the server
|
|
// would only override.
|
|
guestEndpoint: guestConfig.enabled ? guestConfig.endpoint : undefined,
|
|
guestModel: guestConfig.enabled ? guestConfig.model : undefined,
|
|
conversationImportMaxFileSize: process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES
|
|
? parseInt(process.env.CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES, 10)
|
|
: 0,
|
|
};
|
|
|
|
const minPasswordLength = parseInt(process.env.MIN_PASSWORD_LENGTH, 10);
|
|
if (minPasswordLength && !isNaN(minPasswordLength)) {
|
|
payload.minPasswordLength = minPasswordLength;
|
|
}
|
|
|
|
const webSearchConfig = appConfig?.webSearch;
|
|
if (
|
|
webSearchConfig != null &&
|
|
(webSearchConfig.searchProvider ||
|
|
webSearchConfig.scraperProvider ||
|
|
webSearchConfig.rerankerType)
|
|
) {
|
|
payload.webSearch = {};
|
|
}
|
|
|
|
if (webSearchConfig?.searchProvider) {
|
|
payload.webSearch.searchProvider = webSearchConfig.searchProvider;
|
|
}
|
|
if (webSearchConfig?.scraperProvider) {
|
|
payload.webSearch.scraperProvider = webSearchConfig.scraperProvider;
|
|
}
|
|
if (webSearchConfig?.rerankerType) {
|
|
payload.webSearch.rerankerType = webSearchConfig.rerankerType;
|
|
}
|
|
|
|
if (ldap) {
|
|
payload.ldap = ldap;
|
|
}
|
|
|
|
if (typeof process.env.CUSTOM_FOOTER === 'string') {
|
|
payload.customFooter = process.env.CUSTOM_FOOTER;
|
|
}
|
|
|
|
await cache.set(CacheKeys.STARTUP_CONFIG, payload);
|
|
return res.status(200).send(payload);
|
|
} catch (err) {
|
|
logger.error('Error in startup config', err);
|
|
return res.status(500).send({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|