managarten/services/mana-auth/src/config.ts
Till JS 9a3025fed8 feat(ai,auth): Mission Grant endpoint + unwrap helper + audit table
Phase 1 of the Mission Key-Grant rollout. Webapp can now request a
wrapped per-mission data key; mana-ai can unwrap and (Phase 2) use it.

mana-auth:
- POST /api/v1/me/ai-mission-grant — HKDF-derives MDK from the user
  master key, RSA-OAEP-2048-wraps with the mana-ai public key, returns
  { wrappedKey, derivation, issuedAt, expiresAt }
- MissionGrantService refuses zero-knowledge users (409 ZK_ACTIVE) and
  returns 503 GRANT_NOT_CONFIGURED when MANA_AI_PUBLIC_KEY_PEM is unset
- TTL clamped to [1h, 30d]

mana-ai:
- configureMissionGrantKey + unwrapMissionGrant with structured failure
  reasons (not-configured / expired / malformed / wrap-rejected)
- mana_ai.decrypt_audit table + RLS policy scoped to
  app.current_user_id — append-only row per server-side decrypt attempt
- MANA_AI_PRIVATE_KEY_PEM env slot; absent = grants silently disabled

No existing behaviour changes: missions without a grant run exactly as
before. Grant flow is wired end-to-end but unused until Phase 2 lands
the encrypted resolver.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:41:59 +02:00

71 lines
2.8 KiB
TypeScript

export interface Config {
port: number;
databaseUrl: string;
syncDatabaseUrl: string;
baseUrl: string;
cookieDomain: string;
nodeEnv: string;
serviceKey: string;
cors: { origins: string[] };
manaNotifyUrl: string;
manaCreditsUrl: string;
manaSubscriptionsUrl: string;
manaMailUrl: string;
/** Base64-encoded 32-byte AES-256 key encryption key (KEK). Wraps each
* user's master key in auth.encryption_vaults. Required in production
* — in development a deterministic dev KEK is auto-generated so the
* service still boots, with a loud warning. */
encryptionKek: string;
/**
* PEM-encoded RSA-OAEP-2048 public key for the mana-ai Mission
* Grant runner. The `/me/ai-mission-grant` endpoint wraps per-
* mission data keys with this public key so only mana-ai (holder
* of the paired private key) can unwrap them. Optional at boot:
* when absent, the endpoint returns 503 so the UI can degrade
* to foreground-only execution.
*/
missionGrantPublicKeyPem?: string;
}
export function loadConfig(): Config {
const env = (key: string, fallback?: string) => process.env[key] || fallback || '';
const nodeEnv = env('NODE_ENV', 'development');
// Encryption KEK: in production a missing/short value is fatal — the
// vault service refuses to mint or unwrap any master keys without a
// real KEK. In development we auto-fill with a deterministic dev key
// so contributors can run the service without setting up a secret.
let encryptionKek = env('MANA_AUTH_KEK');
if (!encryptionKek) {
if (nodeEnv === 'production') {
throw new Error(
'mana-auth: MANA_AUTH_KEK env var is required in production. ' +
'Set it to a base64-encoded 32-byte random value: ' +
'`openssl rand -base64 32`'
);
}
// 32 zero bytes — deterministic, obviously not for production. The
// vault service logs a loud warning at startup when it sees this.
encryptionKek = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
}
return {
port: parseInt(env('PORT', '3001'), 10),
databaseUrl: env('DATABASE_URL', 'postgresql://mana:devpassword@localhost:5432/mana_platform'),
syncDatabaseUrl: env(
'SYNC_DATABASE_URL',
'postgresql://mana:devpassword@localhost:5432/mana_sync'
),
baseUrl: env('BASE_URL', 'http://localhost:3001'),
cookieDomain: env('COOKIE_DOMAIN'),
nodeEnv,
serviceKey: env('MANA_SERVICE_KEY', 'dev-service-key'),
cors: { origins: env('CORS_ORIGINS', 'http://localhost:5173').split(',') },
manaNotifyUrl: env('MANA_NOTIFY_URL', 'http://localhost:3013'),
manaCreditsUrl: env('MANA_CREDITS_URL', 'http://localhost:3061'),
manaSubscriptionsUrl: env('MANA_SUBSCRIPTIONS_URL', 'http://localhost:3063'),
manaMailUrl: env('MANA_MAIL_URL', 'http://localhost:3042'),
encryptionKek,
missionGrantPublicKeyPem: env('MANA_AI_PUBLIC_KEY_PEM') || undefined,
};
}