feat(portal-bff): entra config foundation — boot validator + auth module #102
@@ -34,16 +34,42 @@ OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
|||||||
# Collector (per ADR-0012). Override only for spike investigations.
|
# Collector (per ADR-0012). Override only for spike investigations.
|
||||||
OTEL_TRACES_SAMPLER=always_on
|
OTEL_TRACES_SAMPLER=always_on
|
||||||
|
|
||||||
|
# Identity / Entra ID app registration (per ADR-0008 / ADR-0009)
|
||||||
|
# Values come from the project's Entra application registration in the
|
||||||
|
# Azure Admin Center → App registrations → APF Portal. The four
|
||||||
|
# *_INSTANCE_URL / *_TENANT_ID / *_CLIENT_ID / *_CLIENT_SECRET keys
|
||||||
|
# are mandatory; the BFF refuses to boot without them (see
|
||||||
|
# apps/portal-bff/src/config/check-entra-config.ts).
|
||||||
|
#
|
||||||
|
# ENTRA_INSTANCE_URL is the Microsoft login endpoint — usually
|
||||||
|
# https://login.microsoftonline.com/. The authority used by MSAL is
|
||||||
|
# `${ENTRA_INSTANCE_URL}${ENTRA_TENANT_ID}` for single-tenant flows,
|
||||||
|
# or `${ENTRA_INSTANCE_URL}organizations` / `common` for multi-tenant
|
||||||
|
# (per ADR-0008's dual-audience design). v1 uses the tenant-scoped
|
||||||
|
# authority; the multi-tenant switch lands when External ID activation
|
||||||
|
# is needed.
|
||||||
|
#
|
||||||
|
# ENTRA_CLIENT_SECRET is the high-value secret of this set. Never
|
||||||
|
# commit a real value. Production manages it via the deploy platform's
|
||||||
|
# secret manager (future infrastructure ADR).
|
||||||
|
ENTRA_INSTANCE_URL=https://login.microsoftonline.com/
|
||||||
|
ENTRA_TENANT_ID=00000000-0000-0000-0000-000000000000
|
||||||
|
ENTRA_CLIENT_ID=00000000-0000-0000-0000-000000000000
|
||||||
|
ENTRA_CLIENT_SECRET=replace_with_real_value
|
||||||
|
# Redirect URIs registered in Entra alongside the same client id. Both
|
||||||
|
# `/auth/callback` and `/auth/logout` paths are mounted by the BFF
|
||||||
|
# once the OIDC routes land in a subsequent PR.
|
||||||
|
ENTRA_REDIRECT_URI=http://localhost:3000/api/auth/callback
|
||||||
|
ENTRA_POST_LOGOUT_REDIRECT_URI=http://localhost:4200/
|
||||||
|
|
||||||
# Future env vars introduced by upcoming phases / ADRs:
|
# Future env vars introduced by upcoming phases / ADRs:
|
||||||
#
|
#
|
||||||
# Auth flow (ADR-0009):
|
# Auth flow (ADR-0009) — additional keys wired as the routes land:
|
||||||
# ENTRA_TENANT_ID
|
# ENTRA_CLIENT_CERT_PATH (alternative to ENTRA_CLIENT_SECRET)
|
||||||
# ENTRA_CLIENT_ID
|
# ENTRA_ACCEPTED_TENANT_IDS (CSV; restricts which tenants can sign in
|
||||||
# ENTRA_CLIENT_SECRET (or ENTRA_CLIENT_CERT_PATH)
|
# in the multi-tenant phase — empty means
|
||||||
# ENTRA_ACCEPTED_TENANT_IDS
|
# "only ENTRA_TENANT_ID is accepted")
|
||||||
# ENTRA_REDIRECT_URI
|
# SESSION_SECRET (cookie signing key, see Sessions below)
|
||||||
# ENTRA_POST_LOGOUT_REDIRECT_URI
|
|
||||||
# SESSION_SECRET
|
|
||||||
#
|
#
|
||||||
# Sessions (ADR-0010):
|
# Sessions (ADR-0010):
|
||||||
# REDIS_URL (or REDIS_SENTINEL_HOSTS + REDIS_SENTINEL_NAME)
|
# REDIS_URL (or REDIS_SENTINEL_HOSTS + REDIS_SENTINEL_NAME)
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import { AppService } from './app.service';
|
|||||||
import { ObservabilityModule } from '../observability/observability.module';
|
import { ObservabilityModule } from '../observability/observability.module';
|
||||||
import { HealthModule } from '../health/health.module';
|
import { HealthModule } from '../health/health.module';
|
||||||
import { AuditModule } from '../audit/audit.module';
|
import { AuditModule } from '../audit/audit.module';
|
||||||
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ObservabilityModule,
|
ObservabilityModule,
|
||||||
PrismaModule.forRoot({ isGlobal: true }),
|
PrismaModule.forRoot({ isGlobal: true }),
|
||||||
AuditModule,
|
AuditModule,
|
||||||
|
AuthModule,
|
||||||
HealthModule,
|
HealthModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
import { AuthModule } from './auth.module';
|
||||||
|
import { ENTRA_CONFIG, type EntraConfig } from './entra-config.token';
|
||||||
|
|
||||||
|
const VALID = {
|
||||||
|
ENTRA_INSTANCE_URL: 'https://login.microsoftonline.com/',
|
||||||
|
ENTRA_TENANT_ID: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
||||||
|
ENTRA_CLIENT_ID: '11111111-2222-3333-4444-555555555555',
|
||||||
|
ENTRA_CLIENT_SECRET: 's3cret-value-from-entra',
|
||||||
|
ENTRA_REDIRECT_URI: 'http://localhost:3000/api/auth/callback',
|
||||||
|
ENTRA_POST_LOGOUT_REDIRECT_URI: 'http://localhost:4200/',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('AuthModule', () => {
|
||||||
|
const originalEnv: Record<string, string | undefined> = {};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
for (const key of Object.keys(VALID) as Array<keyof typeof VALID>) {
|
||||||
|
originalEnv[key] = process.env[key];
|
||||||
|
process.env[key] = VALID[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const key of Object.keys(VALID) as Array<keyof typeof VALID>) {
|
||||||
|
const saved = originalEnv[key];
|
||||||
|
if (saved === undefined) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = saved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides EntraConfig via the ENTRA_CONFIG token', async () => {
|
||||||
|
const ref = await Test.createTestingModule({ imports: [AuthModule] }).compile();
|
||||||
|
const config = ref.get<EntraConfig>(ENTRA_CONFIG);
|
||||||
|
expect(config.clientId).toBe(VALID.ENTRA_CLIENT_ID);
|
||||||
|
expect(config.authority).toBe(`${VALID.ENTRA_INSTANCE_URL}${VALID.ENTRA_TENANT_ID}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails to compile when an env var is missing', async () => {
|
||||||
|
delete process.env['ENTRA_CLIENT_SECRET'];
|
||||||
|
await expect(Test.createTestingModule({ imports: [AuthModule] }).compile()).rejects.toThrow(
|
||||||
|
/ENTRA_CLIENT_SECRET/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { assertEntraConfig } from '../config/check-entra-config';
|
||||||
|
import { ENTRA_CONFIG } from './entra-config.token';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth module — owns the Entra ID configuration and (in subsequent
|
||||||
|
* PRs) the MSAL Node confidential client, the OIDC routes, the
|
||||||
|
* session integration, and the route guards. Per ADR-0009.
|
||||||
|
*
|
||||||
|
* v1 of this module exposes a single provider: the parsed Entra
|
||||||
|
* config, available via the `ENTRA_CONFIG` injection token. The
|
||||||
|
* factory delegates to `assertEntraConfig()` — the same validator
|
||||||
|
* `main.ts` calls at boot — so the typed config object that
|
||||||
|
* consumers receive is guaranteed to be complete and well-formed.
|
||||||
|
* The duplicate call is intentional: boot-time fails fast, the
|
||||||
|
* factory parses again for the DI result. Both reads are
|
||||||
|
* idempotent and trivially cheap.
|
||||||
|
*/
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ENTRA_CONFIG,
|
||||||
|
useFactory: () => assertEntraConfig(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: [ENTRA_CONFIG],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { EntraConfig } from '../config/check-entra-config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DI token used to inject the parsed Entra configuration.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* @Inject(ENTRA_CONFIG) private readonly entra: EntraConfig
|
||||||
|
*
|
||||||
|
* The provider lives in `AuthModule` — modules that need the config
|
||||||
|
* import `AuthModule` (not a global module on purpose; "I depend on
|
||||||
|
* auth" is worth stating in code).
|
||||||
|
*/
|
||||||
|
export const ENTRA_CONFIG = 'ENTRA_CONFIG';
|
||||||
|
|
||||||
|
export type { EntraConfig };
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { assertEntraConfig } from './check-entra-config';
|
||||||
|
|
||||||
|
const VALID = {
|
||||||
|
ENTRA_INSTANCE_URL: 'https://login.microsoftonline.com/',
|
||||||
|
ENTRA_TENANT_ID: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
||||||
|
ENTRA_CLIENT_ID: '11111111-2222-3333-4444-555555555555',
|
||||||
|
ENTRA_CLIENT_SECRET: 's3cret-value-from-entra',
|
||||||
|
ENTRA_REDIRECT_URI: 'http://localhost:3000/api/auth/callback',
|
||||||
|
ENTRA_POST_LOGOUT_REDIRECT_URI: 'http://localhost:4200/',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('assertEntraConfig', () => {
|
||||||
|
const originalEnv: Record<string, string | undefined> = {};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
for (const key of Object.keys(VALID) as Array<keyof typeof VALID>) {
|
||||||
|
originalEnv[key] = process.env[key];
|
||||||
|
process.env[key] = VALID[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const key of Object.keys(VALID) as Array<keyof typeof VALID>) {
|
||||||
|
const saved = originalEnv[key];
|
||||||
|
if (saved === undefined) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = saved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the parsed config when every key is present and well-formed', () => {
|
||||||
|
const config = assertEntraConfig();
|
||||||
|
expect(config.instanceUrl).toBe(VALID.ENTRA_INSTANCE_URL);
|
||||||
|
expect(config.tenantId).toBe(VALID.ENTRA_TENANT_ID);
|
||||||
|
expect(config.clientId).toBe(VALID.ENTRA_CLIENT_ID);
|
||||||
|
expect(config.clientSecret).toBe(VALID.ENTRA_CLIENT_SECRET);
|
||||||
|
expect(config.redirectUri).toBe(VALID.ENTRA_REDIRECT_URI);
|
||||||
|
expect(config.postLogoutRedirectUri).toBe(VALID.ENTRA_POST_LOGOUT_REDIRECT_URI);
|
||||||
|
expect(config.authority).toBe(`${VALID.ENTRA_INSTANCE_URL}${VALID.ENTRA_TENANT_ID}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws listing every missing required key', () => {
|
||||||
|
delete process.env['ENTRA_CLIENT_ID'];
|
||||||
|
delete process.env['ENTRA_CLIENT_SECRET'];
|
||||||
|
expect(() => assertEntraConfig()).toThrow(/ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when ENTRA_INSTANCE_URL is not https', () => {
|
||||||
|
process.env['ENTRA_INSTANCE_URL'] = 'http://login.microsoftonline.com/';
|
||||||
|
expect(() => assertEntraConfig()).toThrow(/ENTRA_INSTANCE_URL must use one of \[https:\]/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when ENTRA_INSTANCE_URL is missing the trailing slash', () => {
|
||||||
|
process.env['ENTRA_INSTANCE_URL'] = 'https://login.microsoftonline.com';
|
||||||
|
expect(() => assertEntraConfig()).toThrow(/must end with a trailing "\/"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when ENTRA_TENANT_ID is not a UUID', () => {
|
||||||
|
process.env['ENTRA_TENANT_ID'] = 'not-a-uuid';
|
||||||
|
expect(() => assertEntraConfig()).toThrow(/ENTRA_TENANT_ID must be a UUID/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when ENTRA_CLIENT_ID is still the .env.example placeholder', () => {
|
||||||
|
process.env['ENTRA_CLIENT_ID'] = '00000000-0000-0000-0000-000000000000';
|
||||||
|
expect(() => assertEntraConfig()).toThrow(/placeholder/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when ENTRA_CLIENT_SECRET is still the .env.example placeholder', () => {
|
||||||
|
process.env['ENTRA_CLIENT_SECRET'] = 'replace_with_real_value';
|
||||||
|
expect(() => assertEntraConfig()).toThrow(/placeholder/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts http for the redirect URIs (local dev case)', () => {
|
||||||
|
process.env['ENTRA_REDIRECT_URI'] = 'http://localhost:3000/api/auth/callback';
|
||||||
|
expect(() => assertEntraConfig()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a redirect URI that is not a valid URL', () => {
|
||||||
|
process.env['ENTRA_REDIRECT_URI'] = 'not-a-url';
|
||||||
|
expect(() => assertEntraConfig()).toThrow(/ENTRA_REDIRECT_URI is not a valid URL/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/**
|
||||||
|
* Sanity-check the Entra ID app-registration env vars early in
|
||||||
|
* bootstrap so a missing or malformed value fails fast with a clear,
|
||||||
|
* actionable message instead of a deep `@azure/msal-node` error
|
||||||
|
* thrown on the first auth request.
|
||||||
|
*
|
||||||
|
* Wired in `main.ts` alongside `assertDatabaseUrl()` — same family
|
||||||
|
* of "fail before any request lands" guard recommended by ADR-0018
|
||||||
|
* §"BFF env-var loading".
|
||||||
|
*
|
||||||
|
* The four `*_INSTANCE_URL` / `*_TENANT_ID` / `*_CLIENT_ID` /
|
||||||
|
* `*_CLIENT_SECRET` keys are mandatory; the two redirect URIs are
|
||||||
|
* mandatory once the OIDC routes ship (next PR). Until then the
|
||||||
|
* validator returns the parsed config but does not consume it; the
|
||||||
|
* `AuthModule` exposes it via DI so the future MSAL client factory
|
||||||
|
* picks it up by constructor injection.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface EntraConfig {
|
||||||
|
/**
|
||||||
|
* Microsoft login endpoint, e.g. `https://login.microsoftonline.com/`.
|
||||||
|
* Combined with `tenantId` to build the MSAL `authority`.
|
||||||
|
*/
|
||||||
|
readonly instanceUrl: string;
|
||||||
|
/** Directory (tenant) UUID. */
|
||||||
|
readonly tenantId: string;
|
||||||
|
/** App registration UUID. */
|
||||||
|
readonly clientId: string;
|
||||||
|
/** Confidential client secret. */
|
||||||
|
readonly clientSecret: string;
|
||||||
|
/** Where Entra sends the user after authentication — `/auth/callback`. */
|
||||||
|
readonly redirectUri: string;
|
||||||
|
/** Where Entra sends the user after RP-initiated logout. */
|
||||||
|
readonly postLogoutRedirectUri: string;
|
||||||
|
/**
|
||||||
|
* Convenience: the full authority URL passed to MSAL Node
|
||||||
|
* (`${instanceUrl}${tenantId}`). Computed once so the rest of the
|
||||||
|
* codebase does not re-derive it on every call.
|
||||||
|
*/
|
||||||
|
readonly authority: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const REQUIRED_KEYS = [
|
||||||
|
'ENTRA_INSTANCE_URL',
|
||||||
|
'ENTRA_TENANT_ID',
|
||||||
|
'ENTRA_CLIENT_ID',
|
||||||
|
'ENTRA_CLIENT_SECRET',
|
||||||
|
'ENTRA_REDIRECT_URI',
|
||||||
|
'ENTRA_POST_LOGOUT_REDIRECT_URI',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
const PLACEHOLDER_TENANT_OR_CLIENT = '00000000-0000-0000-0000-000000000000';
|
||||||
|
const PLACEHOLDER_SECRET = 'replace_with_real_value';
|
||||||
|
|
||||||
|
export function assertEntraConfig(): EntraConfig {
|
||||||
|
const missing = REQUIRED_KEYS.filter((k) => !process.env[k] || process.env[k] === '');
|
||||||
|
if (missing.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Missing Entra config env vars: ${missing.join(', ')}. ` +
|
||||||
|
`Copy apps/portal-bff/.env.example to apps/portal-bff/.env ` +
|
||||||
|
`and fill in the values from the Entra app registration.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// After the `missing` check, every required key is present.
|
||||||
|
const instanceUrl = process.env['ENTRA_INSTANCE_URL'] as string;
|
||||||
|
const tenantId = process.env['ENTRA_TENANT_ID'] as string;
|
||||||
|
const clientId = process.env['ENTRA_CLIENT_ID'] as string;
|
||||||
|
const clientSecret = process.env['ENTRA_CLIENT_SECRET'] as string;
|
||||||
|
const redirectUri = process.env['ENTRA_REDIRECT_URI'] as string;
|
||||||
|
const postLogoutRedirectUri = process.env['ENTRA_POST_LOGOUT_REDIRECT_URI'] as string;
|
||||||
|
|
||||||
|
assertUrl('ENTRA_INSTANCE_URL', instanceUrl, ['https:']);
|
||||||
|
if (!instanceUrl.endsWith('/')) {
|
||||||
|
throw new Error(
|
||||||
|
`ENTRA_INSTANCE_URL must end with a trailing "/" so the authority ` +
|
||||||
|
`(${instanceUrl}<tenant-id>) concatenates correctly. Got: ${instanceUrl}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertUuid('ENTRA_TENANT_ID', tenantId);
|
||||||
|
assertUuid('ENTRA_CLIENT_ID', clientId);
|
||||||
|
assertNotPlaceholder('ENTRA_TENANT_ID', tenantId, PLACEHOLDER_TENANT_OR_CLIENT);
|
||||||
|
assertNotPlaceholder('ENTRA_CLIENT_ID', clientId, PLACEHOLDER_TENANT_OR_CLIENT);
|
||||||
|
assertNotPlaceholder('ENTRA_CLIENT_SECRET', clientSecret, PLACEHOLDER_SECRET);
|
||||||
|
|
||||||
|
assertUrl('ENTRA_REDIRECT_URI', redirectUri, ['http:', 'https:']);
|
||||||
|
assertUrl('ENTRA_POST_LOGOUT_REDIRECT_URI', postLogoutRedirectUri, ['http:', 'https:']);
|
||||||
|
|
||||||
|
return {
|
||||||
|
instanceUrl,
|
||||||
|
tenantId,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
redirectUri,
|
||||||
|
postLogoutRedirectUri,
|
||||||
|
authority: `${instanceUrl}${tenantId}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertUrl(key: string, value: string, allowedProtocols: readonly string[]): void {
|
||||||
|
let parsed: URL;
|
||||||
|
try {
|
||||||
|
parsed = new URL(value);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`${key} is not a valid URL. Got: ${value}`);
|
||||||
|
}
|
||||||
|
if (!allowedProtocols.includes(parsed.protocol)) {
|
||||||
|
throw new Error(
|
||||||
|
`${key} must use one of [${allowedProtocols.join(', ')}]; got "${parsed.protocol}".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertUuid(key: string, value: string): void {
|
||||||
|
if (!UUID_RE.test(value)) {
|
||||||
|
throw new Error(
|
||||||
|
`${key} must be a UUID (e.g. "0123abcd-0123-4567-89ab-cdef01234567"). Got: ${value}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNotPlaceholder(key: string, value: string, placeholder: string): void {
|
||||||
|
if (value === placeholder) {
|
||||||
|
throw new Error(
|
||||||
|
`${key} is still set to the .env.example placeholder ("${placeholder}"). ` +
|
||||||
|
`Replace with the real value from the Entra app registration.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,12 +8,18 @@ import { NestFactory } from '@nestjs/core';
|
|||||||
import { Logger } from 'nestjs-pino';
|
import { Logger } from 'nestjs-pino';
|
||||||
import { AppModule } from './app/app.module';
|
import { AppModule } from './app/app.module';
|
||||||
import { assertDatabaseUrl } from './config/check-database-url';
|
import { assertDatabaseUrl } from './config/check-database-url';
|
||||||
|
import { assertEntraConfig } from './config/check-entra-config';
|
||||||
|
|
||||||
// Fail fast on a malformed DATABASE_URL (most often a special char in
|
// Fail fast on a malformed DATABASE_URL (most often a special char in
|
||||||
// the password that needs URL-encoding) rather than letting Prisma
|
// the password that needs URL-encoding) rather than letting Prisma
|
||||||
// surface a cryptic "invalid connection string" error mid-request.
|
// surface a cryptic "invalid connection string" error mid-request.
|
||||||
assertDatabaseUrl();
|
assertDatabaseUrl();
|
||||||
|
|
||||||
|
// Same family of pre-flight check for the Entra app-registration env
|
||||||
|
// vars (per ADR-0009). Missing / placeholder values fail here rather
|
||||||
|
// than deep inside the first auth request.
|
||||||
|
assertEntraConfig();
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
// `bufferLogs: true` holds early-bootstrap log lines until the
|
// `bufferLogs: true` holds early-bootstrap log lines until the
|
||||||
// Pino-based Logger is wired in below, so we don't lose anything
|
// Pino-based Logger is wired in below, so we don't lose anything
|
||||||
|
|||||||
Reference in New Issue
Block a user