chore: consolidate new foundation and archive v1 (#1495)

This commit is contained in:
2026-09-07 12:32:57 -05:00
3511 changed files with 727899 additions and 10 deletions
+41
View File
@@ -0,0 +1,41 @@
{
"name": "@mosaicstack/auth",
"version": "0.0.2",
"repository": {
"type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
"directory": "packages/auth"
},
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"lint": "eslint src",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsx": "^4.0.0",
"typescript": "^5.8.0",
"vitest": "^2.0.0"
},
"dependencies": {
"@mosaicstack/db": "workspace:^",
"better-auth": "^1.5.5"
},
"publishConfig": {
"registry": "https://git.mosaicstack.dev/api/packages/mosaicstack/npm/",
"access": "public"
},
"files": [
"dist"
]
}
+161
View File
@@ -0,0 +1,161 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { buildOAuthProviders } from './auth.js';
describe('buildOAuthProviders', () => {
const originalEnv = process.env;
beforeEach(() => {
process.env = { ...originalEnv };
delete process.env['AUTHENTIK_CLIENT_ID'];
delete process.env['AUTHENTIK_CLIENT_SECRET'];
delete process.env['AUTHENTIK_ISSUER'];
delete process.env['WORKOS_CLIENT_ID'];
delete process.env['WORKOS_CLIENT_SECRET'];
delete process.env['WORKOS_ISSUER'];
delete process.env['KEYCLOAK_CLIENT_ID'];
delete process.env['KEYCLOAK_CLIENT_SECRET'];
delete process.env['KEYCLOAK_ISSUER'];
delete process.env['KEYCLOAK_URL'];
delete process.env['KEYCLOAK_REALM'];
});
afterEach(() => {
process.env = originalEnv;
});
it('returns empty array when no SSO env vars are set', () => {
const providers = buildOAuthProviders();
expect(providers).toHaveLength(0);
});
describe('WorkOS', () => {
it('includes workos provider when all required env vars are set', () => {
process.env['WORKOS_CLIENT_ID'] = 'client_test123';
process.env['WORKOS_CLIENT_SECRET'] = 'sk_live_test';
process.env['WORKOS_ISSUER'] = 'https://example.authkit.app/';
const providers = buildOAuthProviders();
const workos = providers.find((p) => p.providerId === 'workos');
expect(workos).toBeDefined();
expect(workos?.clientId).toBe('client_test123');
expect(workos?.issuer).toBe('https://example.authkit.app');
expect(workos?.discoveryUrl).toBe(
'https://example.authkit.app/.well-known/openid-configuration',
);
expect(workos?.scopes).toEqual(['openid', 'email', 'profile']);
});
it('throws when WorkOS is partially configured', () => {
process.env['WORKOS_CLIENT_ID'] = 'client_test123';
expect(() => buildOAuthProviders()).toThrow(
'@mosaicstack/auth: WorkOS SSO requires WORKOS_ISSUER, WORKOS_CLIENT_ID, WORKOS_CLIENT_SECRET.',
);
});
it('excludes workos provider when WorkOS is not configured', () => {
const providers = buildOAuthProviders();
const workos = providers.find((p) => p.providerId === 'workos');
expect(workos).toBeUndefined();
});
});
describe('Keycloak', () => {
it('includes keycloak provider when KEYCLOAK_ISSUER is set', () => {
process.env['KEYCLOAK_CLIENT_ID'] = 'mosaic';
process.env['KEYCLOAK_CLIENT_SECRET'] = 'secret123';
process.env['KEYCLOAK_ISSUER'] = 'https://auth.example.com/realms/myrealm/';
const providers = buildOAuthProviders();
const keycloakProvider = providers.find((p) => p.providerId === 'keycloak');
expect(keycloakProvider).toBeDefined();
expect(keycloakProvider?.clientId).toBe('mosaic');
expect(keycloakProvider?.discoveryUrl).toBe(
'https://auth.example.com/realms/myrealm/.well-known/openid-configuration',
);
expect(keycloakProvider?.scopes).toEqual(['openid', 'email', 'profile']);
});
it('supports deriving the Keycloak issuer from KEYCLOAK_URL and KEYCLOAK_REALM', () => {
process.env['KEYCLOAK_CLIENT_ID'] = 'mosaic';
process.env['KEYCLOAK_CLIENT_SECRET'] = 'secret123';
process.env['KEYCLOAK_URL'] = 'https://auth.example.com/';
process.env['KEYCLOAK_REALM'] = 'myrealm';
const providers = buildOAuthProviders();
const keycloakProvider = providers.find((p) => p.providerId === 'keycloak');
expect(keycloakProvider?.discoveryUrl).toBe(
'https://auth.example.com/realms/myrealm/.well-known/openid-configuration',
);
});
it('throws when Keycloak is partially configured', () => {
process.env['KEYCLOAK_CLIENT_ID'] = 'mosaic';
process.env['KEYCLOAK_CLIENT_SECRET'] = 'secret123';
expect(() => buildOAuthProviders()).toThrow(
'@mosaicstack/auth: Keycloak SSO requires KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET, KEYCLOAK_ISSUER.',
);
});
it('excludes keycloak provider when Keycloak is not configured', () => {
const providers = buildOAuthProviders();
const keycloakProvider = providers.find((p) => p.providerId === 'keycloak');
expect(keycloakProvider).toBeUndefined();
});
});
describe('Authentik', () => {
it('includes authentik provider when all required env vars are set', () => {
process.env['AUTHENTIK_CLIENT_ID'] = 'authentik-client';
process.env['AUTHENTIK_CLIENT_SECRET'] = 'authentik-secret';
process.env['AUTHENTIK_ISSUER'] = 'https://auth.example.com/application/o/mosaic/';
const providers = buildOAuthProviders();
const authentik = providers.find((p) => p.providerId === 'authentik');
expect(authentik).toBeDefined();
expect(authentik?.clientId).toBe('authentik-client');
expect(authentik?.issuer).toBe('https://auth.example.com/application/o/mosaic');
expect(authentik?.discoveryUrl).toBe(
'https://auth.example.com/application/o/mosaic/.well-known/openid-configuration',
);
});
it('throws when Authentik is partially configured', () => {
process.env['AUTHENTIK_CLIENT_ID'] = 'authentik-client';
expect(() => buildOAuthProviders()).toThrow(
'@mosaicstack/auth: Authentik SSO requires AUTHENTIK_ISSUER, AUTHENTIK_CLIENT_ID, AUTHENTIK_CLIENT_SECRET.',
);
});
it('excludes authentik provider when Authentik is not configured', () => {
const providers = buildOAuthProviders();
const authentik = providers.find((p) => p.providerId === 'authentik');
expect(authentik).toBeUndefined();
});
});
it('registers all three providers when all env vars are set', () => {
process.env['AUTHENTIK_CLIENT_ID'] = 'a-id';
process.env['AUTHENTIK_CLIENT_SECRET'] = 'a-secret';
process.env['AUTHENTIK_ISSUER'] = 'https://auth.example.com/application/o/mosaic';
process.env['WORKOS_CLIENT_ID'] = 'w-id';
process.env['WORKOS_CLIENT_SECRET'] = 'w-secret';
process.env['WORKOS_ISSUER'] = 'https://example.authkit.app';
process.env['KEYCLOAK_CLIENT_ID'] = 'k-id';
process.env['KEYCLOAK_CLIENT_SECRET'] = 'k-secret';
process.env['KEYCLOAK_ISSUER'] = 'https://kc.example.com/realms/test';
const providers = buildOAuthProviders();
expect(providers).toHaveLength(3);
const ids = providers.map((p) => p.providerId);
expect(ids).toContain('authentik');
expect(ids).toContain('workos');
expect(ids).toContain('keycloak');
});
});
+63
View File
@@ -0,0 +1,63 @@
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { admin } from 'better-auth/plugins';
import { genericOAuth, type GenericOAuthConfig } from 'better-auth/plugins/generic-oauth';
import type { Db } from '@mosaicstack/db';
import { buildGenericOidcProviderConfigs } from './sso.js';
export interface AuthConfig {
db: Db;
baseURL?: string;
secret?: string;
}
export function buildOAuthProviders(): GenericOAuthConfig[] {
return buildGenericOidcProviderConfigs() as GenericOAuthConfig[];
}
export function createAuth(config: AuthConfig) {
const { db, baseURL, secret } = config;
const oidcConfigs = buildOAuthProviders();
const plugins =
oidcConfigs.length > 0
? [
genericOAuth({
config: oidcConfigs,
}),
]
: undefined;
const corsOrigin = process.env['GATEWAY_CORS_ORIGIN'] ?? 'http://localhost:3000';
const trustedOrigins = corsOrigin.split(',').map((o) => o.trim());
return betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
usePlural: true,
}),
baseURL: baseURL ?? process.env['BETTER_AUTH_URL'] ?? 'http://localhost:14242',
secret: secret ?? process.env['BETTER_AUTH_SECRET'],
basePath: '/api/auth',
trustedOrigins,
emailAndPassword: {
enabled: true,
},
user: {
additionalFields: {
role: {
type: 'string',
required: false,
defaultValue: 'member',
input: false,
},
},
},
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // refresh daily
},
plugins: [...(plugins ?? []), admin({ defaultRole: 'member', adminRoles: ['admin'] })],
});
}
export type Auth = ReturnType<typeof createAuth>;
+13
View File
@@ -0,0 +1,13 @@
export { createAuth, type Auth, type AuthConfig } from './auth.js';
export {
buildGenericOidcProviderConfigs,
buildSsoDiscovery,
listSsoStartupWarnings,
type GenericOidcProviderConfig,
type SsoLoginMode,
type SsoProtocol,
type SsoProviderDiscovery,
type SsoTeamSyncConfig,
type SupportedSsoProviderId,
} from './sso.js';
export { seal, unseal } from './seal.js';
+52
View File
@@ -0,0 +1,52 @@
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 12; // 96-bit IV for GCM
const TAG_LENGTH = 16; // 128-bit auth tag
/**
* Derive a 32-byte AES-256 key from BETTER_AUTH_SECRET using SHA-256.
* Throws if BETTER_AUTH_SECRET is not set.
*/
function deriveKey(): Buffer {
const secret = process.env['BETTER_AUTH_SECRET'];
if (!secret) {
throw new Error('BETTER_AUTH_SECRET is not set — cannot derive encryption key');
}
return createHash('sha256').update(secret).digest();
}
/**
* Seal a plaintext string using AES-256-GCM.
* Output format: base64(IV || authTag || ciphertext)
*/
export function seal(plaintext: string): string {
const key = deriveKey();
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
const combined = Buffer.concat([iv, authTag, encrypted]);
return combined.toString('base64');
}
/**
* Unseal a value sealed by `seal()`.
* Throws on authentication failure (tampered data) or if BETTER_AUTH_SECRET is unset.
*/
export function unseal(encoded: string): string {
const key = deriveKey();
const combined = Buffer.from(encoded, 'base64');
const iv = combined.subarray(0, IV_LENGTH);
const authTag = combined.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
const ciphertext = combined.subarray(IV_LENGTH + TAG_LENGTH);
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
return decrypted.toString('utf8');
}
+62
View File
@@ -0,0 +1,62 @@
import { describe, expect, it } from 'vitest';
import {
buildGenericOidcProviderConfigs,
buildSsoDiscovery,
listSsoStartupWarnings,
} from './sso.js';
describe('SSO provider config helpers', () => {
it('builds OIDC configs for Authentik, WorkOS, and Keycloak when fully configured', () => {
const configs = buildGenericOidcProviderConfigs({
AUTHENTIK_CLIENT_ID: 'authentik-client',
AUTHENTIK_CLIENT_SECRET: 'authentik-secret',
AUTHENTIK_ISSUER: 'https://authentik.example.com',
WORKOS_CLIENT_ID: 'workos-client',
WORKOS_CLIENT_SECRET: 'workos-secret',
WORKOS_ISSUER: 'https://auth.workos.com/sso/client_123',
KEYCLOAK_CLIENT_ID: 'keycloak-client',
KEYCLOAK_CLIENT_SECRET: 'keycloak-secret',
KEYCLOAK_ISSUER: 'https://sso.example.com/realms/mosaic',
});
expect(configs.map((config) => config.providerId)).toEqual(['authentik', 'workos', 'keycloak']);
expect(configs.find((config) => config.providerId === 'workos')).toMatchObject({
discoveryUrl: 'https://auth.workos.com/sso/client_123/.well-known/openid-configuration',
pkce: true,
requireIssuerValidation: true,
});
expect(configs.find((config) => config.providerId === 'keycloak')).toMatchObject({
discoveryUrl: 'https://sso.example.com/realms/mosaic/.well-known/openid-configuration',
pkce: true,
});
});
it('exposes Keycloak SAML fallback when OIDC is not configured', () => {
const providers = buildSsoDiscovery({
KEYCLOAK_SAML_LOGIN_URL: 'https://sso.example.com/realms/mosaic/protocol/saml',
});
expect(providers.find((provider) => provider.id === 'keycloak')).toMatchObject({
configured: true,
loginMode: 'saml',
samlFallback: {
configured: true,
loginUrl: 'https://sso.example.com/realms/mosaic/protocol/saml',
},
});
});
it('reports partial provider configuration as startup warnings', () => {
const warnings = listSsoStartupWarnings({
WORKOS_CLIENT_ID: 'workos-client',
KEYCLOAK_CLIENT_ID: 'keycloak-client',
});
expect(warnings).toContain(
'workos OIDC is partially configured. Missing: WORKOS_CLIENT_SECRET, WORKOS_ISSUER',
);
expect(warnings).toContain(
'keycloak OIDC is partially configured. Missing: KEYCLOAK_CLIENT_SECRET, KEYCLOAK_ISSUER',
);
});
});
+241
View File
@@ -0,0 +1,241 @@
export type SupportedSsoProviderId = 'authentik' | 'workos' | 'keycloak';
export type SsoProtocol = 'oidc' | 'saml';
export type SsoLoginMode = 'oidc' | 'saml' | null;
type EnvMap = Record<string, string | undefined>;
export interface GenericOidcProviderConfig {
providerId: SupportedSsoProviderId;
clientId: string;
clientSecret: string;
discoveryUrl?: string;
issuer?: string;
authorizationUrl?: string;
tokenUrl?: string;
userInfoUrl?: string;
scopes: string[];
pkce?: boolean;
requireIssuerValidation?: boolean;
}
export interface SsoTeamSyncConfig {
enabled: boolean;
claim: string | null;
}
export interface SsoProviderDiscovery {
id: SupportedSsoProviderId;
name: string;
protocols: SsoProtocol[];
configured: boolean;
loginMode: SsoLoginMode;
callbackPath: string | null;
teamSync: SsoTeamSyncConfig;
samlFallback: {
configured: boolean;
loginUrl: string | null;
};
warnings: string[];
}
const DEFAULT_SCOPES = ['openid', 'email', 'profile'];
function readEnv(env: EnvMap, key: string): string | undefined {
const value = env[key]?.trim();
return value ? value : undefined;
}
function toDiscoveryUrl(issuer: string): string {
return `${issuer.replace(/\/$/, '')}/.well-known/openid-configuration`;
}
function getTeamSyncClaim(env: EnvMap, envKey: string, fallbackClaim?: string): SsoTeamSyncConfig {
const claim = readEnv(env, envKey) ?? fallbackClaim ?? null;
return {
enabled: claim !== null,
claim,
};
}
function buildAuthentikConfig(env: EnvMap): GenericOidcProviderConfig | null {
const issuer = readEnv(env, 'AUTHENTIK_ISSUER');
const clientId = readEnv(env, 'AUTHENTIK_CLIENT_ID');
const clientSecret = readEnv(env, 'AUTHENTIK_CLIENT_SECRET');
const fields = [issuer, clientId, clientSecret];
const presentCount = fields.filter(Boolean).length;
if (presentCount > 0 && presentCount < fields.length) {
throw new Error(
'@mosaicstack/auth: Authentik SSO requires AUTHENTIK_ISSUER, AUTHENTIK_CLIENT_ID, AUTHENTIK_CLIENT_SECRET.',
);
}
if (!issuer || !clientId || !clientSecret) {
return null;
}
const baseIssuer = issuer.replace(/\/$/, '');
return {
providerId: 'authentik',
issuer: baseIssuer,
clientId,
clientSecret,
discoveryUrl: toDiscoveryUrl(baseIssuer),
authorizationUrl: `${baseIssuer}/application/o/authorize/`,
tokenUrl: `${baseIssuer}/application/o/token/`,
userInfoUrl: `${baseIssuer}/application/o/userinfo/`,
scopes: DEFAULT_SCOPES,
};
}
function buildWorkosConfig(env: EnvMap): GenericOidcProviderConfig | null {
const issuer = readEnv(env, 'WORKOS_ISSUER');
const clientId = readEnv(env, 'WORKOS_CLIENT_ID');
const clientSecret = readEnv(env, 'WORKOS_CLIENT_SECRET');
const fields = [issuer, clientId, clientSecret];
const presentCount = fields.filter(Boolean).length;
if (presentCount > 0 && presentCount < fields.length) {
throw new Error(
'@mosaicstack/auth: WorkOS SSO requires WORKOS_ISSUER, WORKOS_CLIENT_ID, WORKOS_CLIENT_SECRET.',
);
}
if (!issuer || !clientId || !clientSecret) {
return null;
}
const normalizedIssuer = issuer.replace(/\/$/, '');
return {
providerId: 'workos',
issuer: normalizedIssuer,
clientId,
clientSecret,
discoveryUrl: toDiscoveryUrl(normalizedIssuer),
scopes: DEFAULT_SCOPES,
pkce: true,
requireIssuerValidation: true,
};
}
function buildKeycloakConfig(env: EnvMap): GenericOidcProviderConfig | null {
const explicitIssuer = readEnv(env, 'KEYCLOAK_ISSUER');
const keycloakUrl = readEnv(env, 'KEYCLOAK_URL');
const keycloakRealm = readEnv(env, 'KEYCLOAK_REALM');
const clientId = readEnv(env, 'KEYCLOAK_CLIENT_ID');
const clientSecret = readEnv(env, 'KEYCLOAK_CLIENT_SECRET');
// Derive issuer from KEYCLOAK_URL + KEYCLOAK_REALM if KEYCLOAK_ISSUER not set
const issuer =
explicitIssuer ??
(keycloakUrl && keycloakRealm
? `${keycloakUrl.replace(/\/$/, '')}/realms/${keycloakRealm}`
: undefined);
const anySet = !!(issuer || clientId || clientSecret);
if (anySet && (!issuer || !clientId || !clientSecret)) {
throw new Error(
'@mosaicstack/auth: Keycloak SSO requires KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET, KEYCLOAK_ISSUER.',
);
}
if (!issuer || !clientId || !clientSecret) {
return null;
}
const normalizedIssuer = issuer.replace(/\/$/, '');
return {
providerId: 'keycloak',
issuer: normalizedIssuer,
clientId,
clientSecret,
discoveryUrl: toDiscoveryUrl(normalizedIssuer),
scopes: DEFAULT_SCOPES,
pkce: true,
requireIssuerValidation: true,
};
}
function collectWarnings(env: EnvMap, provider: SupportedSsoProviderId): string[] {
const prefix = provider.toUpperCase();
const oidcFields = [
`${prefix}_CLIENT_ID`,
`${prefix}_CLIENT_SECRET`,
`${prefix}_ISSUER`,
] as const;
const presentOidcFields = oidcFields.filter((field) => readEnv(env, field));
const warnings: string[] = [];
if (presentOidcFields.length > 0 && presentOidcFields.length < oidcFields.length) {
const missing = oidcFields.filter((field) => !readEnv(env, field));
warnings.push(`${provider} OIDC is partially configured. Missing: ${missing.join(', ')}`);
}
return warnings;
}
export function buildGenericOidcProviderConfigs(
env: EnvMap = process.env,
): GenericOidcProviderConfig[] {
return [buildAuthentikConfig(env), buildWorkosConfig(env), buildKeycloakConfig(env)].filter(
(config): config is GenericOidcProviderConfig => config !== null,
);
}
export function listSsoStartupWarnings(env: EnvMap = process.env): string[] {
return ['authentik', 'workos', 'keycloak'].flatMap((provider) =>
collectWarnings(env, provider as SupportedSsoProviderId),
);
}
export function buildSsoDiscovery(env: EnvMap = process.env): SsoProviderDiscovery[] {
const oidcConfigs = new Map(
buildGenericOidcProviderConfigs(env).map((config) => [config.providerId, config]),
);
const keycloakSamlLoginUrl = readEnv(env, 'KEYCLOAK_SAML_LOGIN_URL') ?? null;
return [
{
id: 'authentik',
name: 'Authentik',
protocols: ['oidc'],
configured: oidcConfigs.has('authentik'),
loginMode: oidcConfigs.has('authentik') ? 'oidc' : null,
callbackPath: oidcConfigs.has('authentik') ? '/api/auth/oauth2/callback/authentik' : null,
teamSync: getTeamSyncClaim(env, 'AUTHENTIK_TEAM_SYNC_CLAIM', 'groups'),
samlFallback: {
configured: false,
loginUrl: null,
},
warnings: collectWarnings(env, 'authentik'),
},
{
id: 'workos',
name: 'WorkOS',
protocols: ['oidc'],
configured: oidcConfigs.has('workos'),
loginMode: oidcConfigs.has('workos') ? 'oidc' : null,
callbackPath: oidcConfigs.has('workos') ? '/api/auth/oauth2/callback/workos' : null,
teamSync: getTeamSyncClaim(env, 'WORKOS_TEAM_SYNC_CLAIM', 'organization_id'),
samlFallback: {
configured: false,
loginUrl: null,
},
warnings: collectWarnings(env, 'workos'),
},
{
id: 'keycloak',
name: 'Keycloak',
protocols: ['oidc', 'saml'],
configured: oidcConfigs.has('keycloak') || keycloakSamlLoginUrl !== null,
loginMode: oidcConfigs.has('keycloak') ? 'oidc' : keycloakSamlLoginUrl ? 'saml' : null,
callbackPath: oidcConfigs.has('keycloak') ? '/api/auth/oauth2/callback/keycloak' : null,
teamSync: getTeamSyncClaim(env, 'KEYCLOAK_TEAM_SYNC_CLAIM', 'groups'),
samlFallback: {
configured: keycloakSamlLoginUrl !== null,
loginUrl: keycloakSamlLoginUrl,
},
warnings: collectWarnings(env, 'keycloak'),
},
];
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});