-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathmain.ts
More file actions
135 lines (120 loc) · 5.79 KB
/
Copy pathmain.ts
File metadata and controls
135 lines (120 loc) · 5.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { NestFactory } from "@nestjs/core";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { AppModule } from "./app.module";
import { configureApp } from "./bootstrap";
import { ApiErrorDto, FieldViolationDto } from "./common/dto/api-error.dto";
import {
API_KEY_AUTH_SCHEME,
API_KEY_AUTH_SCHEME_DEFINITION,
GLOBAL_API_RESPONSES,
SESSION_AUTH_SCHEME,
SESSION_AUTH_SCHEME_DEFINITION,
} from "./common/swagger/security-schemes";
import { HealthService } from "./health/health.service";
async function bootstrap() {
let app;
try {
// ── Configuration validation happens during module initialization ──
// The validateEnv() hook in AppModule runs immediately, checking both
// individual field constraints and cross-variable invariants. If validation
// fails, NestFactory.create() throws and execution never reaches app.listen().
app = await NestFactory.create(AppModule);
} catch (error) {
// ── FAIL FAST: Configuration errors prevent startup entirely ──
// This ensures the server never listens with bad configuration.
const logger = new Logger("Bootstrap");
const message = error instanceof Error ? error.message : String(error);
logger.error(`Configuration validation failed: ${message}`);
process.exit(1);
}
const configService = app.get(ConfigService);
const port = configService.getOrThrow<number>("port");
// Body limits, structural limits, security headers, CORS, interceptors, the
// error filter and validation, in the order a request meets them. See
// `src/bootstrap.ts`; kept there so tests can exercise the same pipeline.
configureApp(app, { corsOrigin: configService.getOrThrow<string>("appUrl") });
// ── Swagger / OpenAPI ────────────────────────────────────────────────────
const documentConfig = new DocumentBuilder()
.setTitle("EarnProof API")
.setDescription(
"Stellar testnet income proof API.\n\n" +
"## Error contract\n" +
"All non-2xx responses use the `ApiErrorDto` envelope:\n" +
"```json\n" +
"{\n" +
' "statusCode": 401,\n' +
' "code": "INVALID_TOKEN",\n' +
' "message": "Authentication token is invalid.",\n' +
' "requestId": "01hwzxyz..."\n' +
"}\n" +
"```\n" +
"The `code` field is stable across minor versions. Branch on `code`, not `message`.\n\n" +
"## Request IDs\n" +
"Pass `X-Request-ID` with any request to correlate logs. " +
"A generated ID is returned in the `X-Request-ID` response header when none is supplied.\n\n" +
"## Authentication\n" +
"Two credentials exist and are not interchangeable.\n\n" +
"- **Session token** — a wallet holder authenticates with `POST /api/v1/auth/verify` " +
"and sends `Authorization: Bearer <token>`. Used by the dashboard and by anything " +
"acting on behalf of a person.\n" +
"- **API key** — a machine integration sends `Authorization: Bearer <secret>` " +
"together with `X-Organization-Id`, and is limited to the scopes the key was " +
"created with. Start at `GET /api/v1/integrations/auth-context` to confirm a key " +
"works and see its scopes.\n\n" +
"Public routes — credential verification and proof verification — take neither.",
)
.setVersion("0.1.0")
// The security scheme name must match the argument passed to @ApiBearerAuth()
// decorators (default is 'bearer' when no name is given), so both ends read
// it from `common/swagger/security-schemes.ts`.
.addBearerAuth(SESSION_AUTH_SCHEME_DEFINITION, SESSION_AUTH_SCHEME)
.addBearerAuth(API_KEY_AUTH_SCHEME_DEFINITION, API_KEY_AUTH_SCHEME)
.addGlobalResponse(...GLOBAL_API_RESPONSES)
.build();
const document = SwaggerModule.createDocument(app, documentConfig, {
// Ensure ApiErrorDto and FieldViolationDto are always included in the
// generated schema even if they're only referenced via `type` strings.
extraModels: [ApiErrorDto, FieldViolationDto],
});
SwaggerModule.setup("docs", app, document, {
swaggerOptions: {
// Persist auth token across page reloads in the Swagger UI.
persistAuthorization: true,
},
});
// ── Graceful shutdown (earnproof-backend#68) ────────────────────────────
// enableShutdownHooks() is what makes Nest actually call each provider's
// onModuleDestroy/onApplicationShutdown on SIGTERM/SIGINT — without it,
// those lifecycle hooks never fire and the process exits mid-work. See
// docs/shutdown.md for the full runbook (what each worker drains, how to
// verify it, how to force-terminate safely).
app.enableShutdownHooks();
const shutdownLogger = new Logger("Shutdown");
const health = app.get(HealthService);
const shutdown = async (signal: string) => {
shutdownLogger.log(`Received ${signal} — starting graceful shutdown`);
// Flip readiness to not_ready FIRST, before Nest's own module-destroy
// sequence runs, so a load balancer stops routing new traffic here as
// early in the sequence as possible — new work stops arriving before
// any draining begins.
health.beginShutdown();
try {
await app.close();
shutdownLogger.log("Shutdown complete");
process.exit(0);
} catch (err) {
shutdownLogger.error(
`Shutdown did not complete cleanly: ${
err instanceof Error ? err.message : String(err)
}`,
);
process.exit(1);
}
};
process.on("SIGTERM", () => void shutdown("SIGTERM"));
process.on("SIGINT", () => void shutdown("SIGINT"));
await app.listen(port);
}
void bootstrap();