feat: rebrand Hemmelig to paste.es for cloudhost.es

- Set Spanish as default language with ephemeral/encrypted privacy focus
- Translate all user-facing strings and legal pages to Spanish
- Replace Norwegian flag with Spanish flag in footer
- Remove Hemmelig/terces.cloud links, add cloudhost.es sponsorship
- Rewrite PrivacyPage: zero data collection, ephemeral design emphasis
- Rewrite TermsPage: Spanish law, RGPD, paste.es/CloudHost.es references
- Update PWA manifest, HTML meta tags, package.json branding
- Rename webhook headers to X-Paste-Event / X-Paste-Signature
- Update API docs title and contact to paste.es / cloudhost.es

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 09:30:19 +01:00
commit bc9f96cbd4
268 changed files with 45773 additions and 0 deletions

255
api/config.ts Normal file
View File

@@ -0,0 +1,255 @@
import dlv from 'dlv';
const isProduction = process.env.NODE_ENV === 'production';
// Helper to parse boolean from env, returns undefined if not set
const parseBoolean = (value: string | undefined): boolean | undefined => {
if (value === undefined || value === null || value === '') return undefined;
return value.toLowerCase() === 'true';
};
// Helper to parse integer from env, returns undefined if not set
const parseInteger = (value: string | undefined): number | undefined => {
if (value === undefined || value === null || value === '') return undefined;
const parsed = parseInt(value, 10);
return isNaN(parsed) ? undefined : parsed;
};
// Social provider configuration type
export interface SocialProviderConfig {
clientId: string;
clientSecret: string;
tenantId?: string; // For Microsoft/Azure AD
issuer?: string; // For self-hosted instances (e.g., GitLab)
}
// Generic OAuth provider configuration type (for better-auth genericOAuth plugin)
export interface GenericOAuthProviderConfig {
providerId: string;
discoveryUrl?: string;
authorizationUrl?: string;
tokenUrl?: string;
userInfoUrl?: string;
clientId: string;
clientSecret: string;
scopes?: string[];
pkce?: boolean;
}
// Build social providers config dynamically from env vars
const buildSocialProviders = () => {
const providers: Record<string, SocialProviderConfig> = {};
// GitHub
if (process.env.HEMMELIG_AUTH_GITHUB_ID && process.env.HEMMELIG_AUTH_GITHUB_SECRET) {
providers.github = {
clientId: process.env.HEMMELIG_AUTH_GITHUB_ID,
clientSecret: process.env.HEMMELIG_AUTH_GITHUB_SECRET,
};
}
// Google
if (process.env.HEMMELIG_AUTH_GOOGLE_ID && process.env.HEMMELIG_AUTH_GOOGLE_SECRET) {
providers.google = {
clientId: process.env.HEMMELIG_AUTH_GOOGLE_ID,
clientSecret: process.env.HEMMELIG_AUTH_GOOGLE_SECRET,
};
}
// Microsoft (Azure AD)
if (process.env.HEMMELIG_AUTH_MICROSOFT_ID && process.env.HEMMELIG_AUTH_MICROSOFT_SECRET) {
providers.microsoft = {
clientId: process.env.HEMMELIG_AUTH_MICROSOFT_ID,
clientSecret: process.env.HEMMELIG_AUTH_MICROSOFT_SECRET,
tenantId: process.env.HEMMELIG_AUTH_MICROSOFT_TENANT_ID,
};
}
// Discord
if (process.env.HEMMELIG_AUTH_DISCORD_ID && process.env.HEMMELIG_AUTH_DISCORD_SECRET) {
providers.discord = {
clientId: process.env.HEMMELIG_AUTH_DISCORD_ID,
clientSecret: process.env.HEMMELIG_AUTH_DISCORD_SECRET,
};
}
// GitLab
if (process.env.HEMMELIG_AUTH_GITLAB_ID && process.env.HEMMELIG_AUTH_GITLAB_SECRET) {
providers.gitlab = {
clientId: process.env.HEMMELIG_AUTH_GITLAB_ID,
clientSecret: process.env.HEMMELIG_AUTH_GITLAB_SECRET,
issuer: process.env.HEMMELIG_AUTH_GITLAB_ISSUER,
};
}
// Apple
if (process.env.HEMMELIG_AUTH_APPLE_ID && process.env.HEMMELIG_AUTH_APPLE_SECRET) {
providers.apple = {
clientId: process.env.HEMMELIG_AUTH_APPLE_ID,
clientSecret: process.env.HEMMELIG_AUTH_APPLE_SECRET,
};
}
// Twitter/X
if (process.env.HEMMELIG_AUTH_TWITTER_ID && process.env.HEMMELIG_AUTH_TWITTER_SECRET) {
providers.twitter = {
clientId: process.env.HEMMELIG_AUTH_TWITTER_ID,
clientSecret: process.env.HEMMELIG_AUTH_TWITTER_SECRET,
};
}
return providers;
};
// Build generic OAuth providers from JSON env var
const buildGenericOAuthProviders = (): GenericOAuthProviderConfig[] => {
const genericOAuthEnv = process.env.HEMMELIG_AUTH_GENERIC_OAUTH;
if (!genericOAuthEnv) {
return [];
}
try {
const parsed = JSON.parse(genericOAuthEnv);
if (!Array.isArray(parsed)) {
console.error('HEMMELIG_AUTH_GENERIC_OAUTH must be a JSON array');
return [];
}
// Validate each provider config
return parsed.filter((provider: any) => {
if (!provider.providerId || !provider.clientId || !provider.clientSecret) {
console.error(
`Invalid generic OAuth provider config: missing required fields (providerId, clientId, or clientSecret)`
);
return false;
}
// Must have either discoveryUrl OR all three URLs (authorization, token, userInfo)
const hasDiscoveryUrl = !!provider.discoveryUrl;
const hasManualUrls = !!(
provider.authorizationUrl &&
provider.tokenUrl &&
provider.userInfoUrl
);
if (!hasDiscoveryUrl && !hasManualUrls) {
console.error(
`Invalid generic OAuth provider config for "${provider.providerId}": must provide either discoveryUrl OR all of (authorizationUrl, tokenUrl, userInfoUrl)`
);
return false;
}
return true;
}) as GenericOAuthProviderConfig[];
} catch (error) {
console.error('Failed to parse HEMMELIG_AUTH_GENERIC_OAUTH:', error);
return [];
}
};
const socialProviders = buildSocialProviders();
const genericOAuthProviders = buildGenericOAuthProviders();
// Managed mode: all settings are controlled via environment variables
const isManaged = parseBoolean(process.env.HEMMELIG_MANAGED) ?? false;
// Managed mode settings (only used when HEMMELIG_MANAGED=true)
const managedSettings = isManaged
? {
// General settings
instanceName: process.env.HEMMELIG_INSTANCE_NAME ?? '',
instanceDescription: process.env.HEMMELIG_INSTANCE_DESCRIPTION ?? '',
instanceLogo: process.env.HEMMELIG_INSTANCE_LOGO ?? '',
allowRegistration: parseBoolean(process.env.HEMMELIG_ALLOW_REGISTRATION) ?? true,
requireEmailVerification:
parseBoolean(process.env.HEMMELIG_REQUIRE_EMAIL_VERIFICATION) ?? false,
defaultSecretExpiration:
parseInteger(process.env.HEMMELIG_DEFAULT_SECRET_EXPIRATION) ?? 72,
maxSecretSize: parseInteger(process.env.HEMMELIG_MAX_SECRET_SIZE) ?? 1024,
importantMessage: process.env.HEMMELIG_IMPORTANT_MESSAGE ?? '',
// Security settings
allowPasswordProtection:
parseBoolean(process.env.HEMMELIG_ALLOW_PASSWORD_PROTECTION) ?? true,
allowIpRestriction: parseBoolean(process.env.HEMMELIG_ALLOW_IP_RESTRICTION) ?? true,
enableRateLimiting: parseBoolean(process.env.HEMMELIG_ENABLE_RATE_LIMITING) ?? true,
rateLimitRequests: parseInteger(process.env.HEMMELIG_RATE_LIMIT_REQUESTS) ?? 100,
rateLimitWindow: parseInteger(process.env.HEMMELIG_RATE_LIMIT_WINDOW) ?? 60,
// Organization settings
requireInviteCode: parseBoolean(process.env.HEMMELIG_REQUIRE_INVITE_CODE) ?? false,
allowedEmailDomains: process.env.HEMMELIG_ALLOWED_EMAIL_DOMAINS ?? '',
requireRegisteredUser:
parseBoolean(process.env.HEMMELIG_REQUIRE_REGISTERED_USER) ?? false,
disableEmailPasswordSignup:
parseBoolean(process.env.HEMMELIG_DISABLE_EMAIL_PASSWORD_SIGNUP) ?? false,
// Webhook settings
webhookEnabled: parseBoolean(process.env.HEMMELIG_WEBHOOK_ENABLED) ?? false,
webhookUrl: process.env.HEMMELIG_WEBHOOK_URL ?? '',
webhookSecret: process.env.HEMMELIG_WEBHOOK_SECRET ?? '',
webhookOnView: parseBoolean(process.env.HEMMELIG_WEBHOOK_ON_VIEW) ?? true,
webhookOnBurn: parseBoolean(process.env.HEMMELIG_WEBHOOK_ON_BURN) ?? true,
// Metrics settings
metricsEnabled: parseBoolean(process.env.HEMMELIG_METRICS_ENABLED) ?? false,
metricsSecret: process.env.HEMMELIG_METRICS_SECRET ?? '',
// File upload settings
allowFileUploads: parseBoolean(process.env.HEMMELIG_ALLOW_FILE_UPLOADS) ?? true,
}
: null;
const config = {
server: {
port: Number(process.env.HEMMELIG_PORT) || 3000,
},
trustedOrigins: [
...(!isProduction ? ['http://localhost:5173'] : []),
process.env.HEMMELIG_TRUSTED_ORIGIN || '',
].filter(Boolean),
general: {
instanceName: process.env.HEMMELIG_INSTANCE_NAME,
instanceDescription: process.env.HEMMELIG_INSTANCE_DESCRIPTION,
instanceLogo: process.env.HEMMELIG_INSTANCE_LOGO,
allowRegistration: parseBoolean(process.env.HEMMELIG_ALLOW_REGISTRATION),
},
security: {
allowPasswordProtection: parseBoolean(process.env.HEMMELIG_ALLOW_PASSWORD_PROTECTION),
allowIpRestriction: parseBoolean(process.env.HEMMELIG_ALLOW_IP_RESTRICTION),
},
analytics: {
enabled: parseBoolean(process.env.HEMMELIG_ANALYTICS_ENABLED) ?? true,
hmacSecret:
process.env.HEMMELIG_ANALYTICS_HMAC_SECRET || 'default-analytics-secret-change-me',
},
socialProviders,
};
if (!process.env.HEMMELIG_ANALYTICS_HMAC_SECRET && config.analytics.enabled) {
console.warn(
'WARNING: HEMMELIG_ANALYTICS_HMAC_SECRET is not set. Analytics visitor IDs are generated ' +
'with a default secret, making them predictable. Set a random secret for production use.'
);
}
/**
* A type-safe utility to get a value from the configuration.
* Its return type is inferred from the type of the default value.
* @param path The dot-notation path to the config value (e.g., 'server.port').
* @param defaultValue A default value to return if the path is not found.
* @returns The found configuration value or the default value.
*/
function get<T>(path: string, defaultValue?: T): T {
return dlv(config, path, defaultValue) as T;
}
// Export the get function and social providers helper
export default {
get,
getSocialProviders: () => config.socialProviders,
getGenericOAuthProviders: () => genericOAuthProviders,
isManaged: () => isManaged,
getManagedSettings: () => managedSettings,
};