@zaaxch/tailframe 4.0.4 → 4.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaaxch/tailframe",
3
- "version": "4.0.4",
3
+ "version": "4.0.6",
4
4
  "description": "Tailframe architecture toolkit: validates the Tailframe structure, import-boundary, and file-convention contracts. The package version is the contract version.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/new.mjs CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  healthRoutesSource,
19
19
  healthSchemasSource,
20
20
  httpErrorsSource,
21
+ observabilitySource,
21
22
  mongoReadinessProbeSource,
22
23
  postgresReadinessProbeSource,
23
24
  readinessProbeSource,
@@ -221,6 +222,7 @@ const svcDeps = {
221
222
  joi: "^17.13.3",
222
223
  "reflect-metadata": "^0.2.2",
223
224
  tsyringe: "^4.9.1",
225
+ winston: "^3.19.0",
224
226
  ...(postgres ? { pg: "^8.16.3" } : { mongodb: "^6.17.0" }),
225
227
  ...(options.auth === "firebase" ? { "firebase-admin": "^13.0.0" } : {}),
226
228
  ...(options.redis ? { "rate-limiter-flexible": "^11.2.0", redis: "^5.10.0" } : {})
@@ -290,7 +292,7 @@ export const env = {
290
292
  firebaseServiceAccountPath: process.env.FIREBASE_SERVICE_ACCOUNT_PATH,` : ""}
291
293
  ${postgres ? `postgresUri: process.env.POSTGRES_URI ?? "postgresql://${options.name}-app:development@localhost:5432/${databaseName}",` : `mongodbUri: process.env.MONGODB_URI ?? "mongodb://localhost:27017/?replicaSet=rs0&directConnection=true",
292
294
  mongodbDbName: process.env.MONGODB_DB_NAME ?? "${databaseName}",`}
293
- ${options.redis ? `redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379",` : ""}
295
+ ${options.redis ? `redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379"` : ""}
294
296
  };`);
295
297
  const databaseSource = postgres ? `import { Pool } from "pg";
296
298
  import { env } from "@/platform/config/env";
@@ -331,6 +333,7 @@ if (options.redis) add(`${svc}/src/platform/redis/index.ts`, redisSource);
331
333
  add(`${svc}/src/core/RequestContext.ts`, requestContextSource);
332
334
  add(`${svc}/src/core/UseCase.ts`, useCaseSource);
333
335
  add(`${svc}/src/core/errors.ts`, appErrorSource);
336
+ add(`${svc}/src/app/observability.ts`, observabilitySource);
334
337
  add(`${svc}/src/platform/http/createRequestContext.ts`, createRequestContextSource(options.auth));
335
338
  if (options.auth === "firebase") add(`${svc}/src/platform/auth/firebase.ts`, firebaseSource);
336
339
  add(`${svc}/src/platform/http/rpc.ts`, rpcSource);
@@ -355,15 +358,36 @@ if (options.worker) add(`${svc}/src/worker.ts`, `import "reflect-metadata";\nimp
355
358
  add(`${svc}/src/modules/health/__tests__/GetHealth.test.ts`, getHealthTestSource);
356
359
  add(`${svc}/src/modules/health/__tests__/GetReadiness.test.ts`, getReadinessTestSource);
357
360
  add(`${svc}/src/__tests__/testDb.ts`, postgres ? `import { Pool } from "pg";
358
- const pool = new Pool({ connectionString: process.env.TEST_POSTGRES_URI ?? "postgresql://postgres:postgres@localhost:5433/${databaseName}_test" });
359
- export async function openTestDatabase() { await pool.query("SELECT 1"); return pool; }
360
- export async function resetTestDatabase() { await pool.query("DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public"); return pool; }
361
- export async function closeTestDatabase() { await pool.end(); }
361
+ const pool = new Pool({
362
+ connectionString: process.env.TEST_POSTGRES_URI ?? "postgresql://postgres:postgres@localhost:5433/${databaseName}_test"
363
+ });
364
+ export async function openTestDatabase() {
365
+ await pool.query("SELECT 1");
366
+ return pool;
367
+ }
368
+ export async function resetTestDatabase() {
369
+ await pool.query("DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public");
370
+ return pool;
371
+ }
372
+ export async function closeTestDatabase() {
373
+ await pool.end();
374
+ }
362
375
  ` : `import { MongoClient } from "mongodb";
363
- const client = new MongoClient(process.env.TEST_MONGODB_URI ?? "mongodb://localhost:27018/?replicaSet=rs0&directConnection=true");
364
- export async function openTestDatabase() { await client.connect(); return client.db(process.env.TEST_MONGODB_DB_NAME ?? "${options.name.replaceAll("-", "_")}_test"); }
365
- export async function resetTestDatabase() { const database = await openTestDatabase(); await database.dropDatabase(); return database; }
366
- export async function closeTestDatabase() { await client.close(); }
376
+ const client = new MongoClient(
377
+ process.env.TEST_MONGODB_URI ?? "mongodb://localhost:27018/?replicaSet=rs0&directConnection=true"
378
+ );
379
+ export async function openTestDatabase() {
380
+ await client.connect();
381
+ return client.db(process.env.TEST_MONGODB_DB_NAME ?? "${options.name.replaceAll("-", "_")}_test");
382
+ }
383
+ export async function resetTestDatabase() {
384
+ const database = await openTestDatabase();
385
+ await database.dropDatabase();
386
+ return database;
387
+ }
388
+ export async function closeTestDatabase() {
389
+ await client.close();
390
+ }
367
391
  `);
368
392
  add(`${svc}/docker-compose.test.yml`, postgres ? `name: ${options.name}-test
369
393
  services:
@@ -465,13 +489,20 @@ ${csrf ? `
465
489
  ` : ""}${options.ui ? `
466
490
  it("serves the production SPA without falling through for API paths", async () => {
467
491
  const app = createApplication({ production: true, publicPath: path.join(__dirname, "fixtures/public") });
468
- await request(app).get("/dashboard").expect(200).expect(({ text }) => expect(text).toContain("boundary-test-spa"));
492
+ await request(app)
493
+ .get("/dashboard")
494
+ .expect(200)
495
+ .expect(({ text }) => expect(text).toContain("boundary-test-spa"));
469
496
  await request(app).get("/api/v1/does-not-exist").expect(404);
470
- });
471
- ` : ""}
497
+ });` : ""}
472
498
  });
473
499
  `);
474
- if (options.ui) add(`${svc}/src/app/__tests__/fixtures/public/index.html`, `<!doctype html><html><body><div id="app">boundary-test-spa</div></body></html>`);
500
+ if (options.ui) add(`${svc}/src/app/__tests__/fixtures/public/index.html`, `<!doctype html>
501
+ <html>
502
+ <body>
503
+ <div id="app">boundary-test-spa</div>
504
+ </body>
505
+ </html>`);
475
506
  add(`${svc}/AGENTS.md`, `# ${title} service guidance
476
507
 
477
508
  ## Scope
@@ -480,7 +511,7 @@ These conventions apply to \`${svc}\`. This service is a modular monolith organi
480
511
  ## Architecture vocabulary
481
512
  - Use \`src/app\` for process assembly, server creation, route mounting, and dependency registration.
482
513
  - Use \`src/core\` only for small technology-neutral contracts such as \`RequestContext\` and \`UseCase\`.
483
- - Use \`src/platform\` for technical mechanisms such as Express, authentication, configuration, databases, Redis, and shared HTTP behavior.
514
+ - Use \`src/platform\` for technical mechanisms such as Express, authentication, configuration, databases, Redis, structured logging, and shared HTTP behavior.
484
515
  - Use \`src/modules/<module>\` for product capabilities.
485
516
  - These are the only service architecture roots. Keep \`src/core\` flat. Put external-system adapters under \`src/platform/integrations/<provider>\`; directories immediately under \`src/app\` are limited to \`cli\`, \`jobs\`, \`mcp\`, \`scheduled-tasks\`, \`workers\`, and \`__tests__\`.
486
517
  - Inside a module, use \`use-cases/\`, \`http/\`, and tests first. Add \`domain/\` or \`persistence/\` only when behavior requires them.
@@ -496,6 +527,7 @@ These conventions apply to \`${svc}\`. This service is a modular monolith organi
496
527
  | Express route or request schema | Owning module's \`http\` |
497
528
  | Vendor or external-system adapter | \`src/platform/integrations/<provider>\` |
498
529
  | Database, Redis, authentication, or HTTP mechanism | \`src/platform\` |
530
+ | Product-specific Winston transport | \`src/app/observability.ts\` |
499
531
  | Process lifecycle, registration, schedule, or consumer | \`src/app/<entry-point-kind>\` |
500
532
  | Cross-entry-point technology-neutral contract | Flat \`src/core\` |
501
533
  | Root executable | Bootstrap of matching \`src/app\` assembly only |
@@ -544,6 +576,7 @@ ${postgres
544
576
  - Keep optional entry-point assembly under \`src/app/<entry-point-kind>\`; root files such as \`src/server.ts\` and \`src/worker.ts\` may only bootstrap that assembly.
545
577
  - Initialize infrastructure first, then call \`registerDependencies(...)\` from every process entry point. The composition root resets the container, explicitly constructs dependencies, and registers class-token instances. Keep tsyringe, injection tokens, decorators, and container resolution out of modules and platform adapters.
546
578
  - Register routes, workers, and jobs explicitly. Shutdown paths are awaitable and close resources without forcing \`process.exit\`.
579
+ - The canonical Winston JSON console transport logs every HTTP failure with safe request metadata. Add product-specific transports only in \`src/app/observability.ts\`; do not remove, silence, or reconfigure the canonical transport. Preserve diagnostic causes on translated errors without exposing them in client envelopes.
547
580
  - Scheduled work prevents unintended overlap, stops intake before shutdown, awaits the active run, destroys its scheduler or consumer, and only then closes Redis and database clients.
548
581
  ${options.ui ? "- In production, serve the compiled UI from `dist/public` with a non-API SPA fallback. Never return the SPA for `/api` requests." : ""}
549
582
 
@@ -654,8 +687,22 @@ import NotificationHost from "@/app/components/NotificationHost.vue";
654
687
  </script>`);
655
688
  add(`${ui}/src/app/startApplication.ts`, `import { createApp } from "vue";\nimport { createPinia } from "pinia";\nimport PrimeVue from "primevue/config";\nimport App from "@/app/App.vue";\n${options.auth === "firebase" ? 'import { configureHttp } from "@/app/configureHttp";\n' : ""}import router from "@/app/router";\nimport "@/app/styles.css";\nexport function startApplication() {\n\tconst app = createApp(App);\n\tconst pinia = createPinia();\n\tapp.use(pinia).use(PrimeVue).use(router);\n\t${options.auth === "firebase" ? "configureHttp(pinia);\n\t" : ""}app.mount("#app");\n}\n`);
656
689
  add(`${ui}/src/main.ts`, `import { startApplication } from "@/app/startApplication";\nstartApplication();\n`);
657
- add(`${ui}/src/app/styles.css`, `@import "tailwindcss";\n:root { font-family: Inter, ui-sans-serif, system-ui; color: #17221c; background: #f5f3ec; }\nbody { margin: 0; }`);
658
- add(`${ui}/src/app/__tests__/App.spec.ts`, `import { mount } from "@vue/test-utils";\nimport App from "@/app/App.vue";\ndescribe("App", () => { it("renders a router view", () => { expect(mount(App, { global: { stubs: ["RouterView", "NotificationHost"] } }).exists()).toBe(true); }); });`);
690
+ add(`${ui}/src/app/styles.css`, `@import "tailwindcss";
691
+ :root {
692
+ font-family: Inter, ui-sans-serif, system-ui;
693
+ color: #17221c;
694
+ background: #f5f3ec;
695
+ }
696
+ body {
697
+ margin: 0;
698
+ }`);
699
+ add(`${ui}/src/app/__tests__/App.spec.ts`, `import { mount } from "@vue/test-utils";
700
+ import App from "@/app/App.vue";
701
+ describe("App", () => {
702
+ it("renders a router view", () => {
703
+ expect(mount(App, { global: { stubs: ["RouterView", "NotificationHost"] } }).exists()).toBe(true);
704
+ });
705
+ });`);
659
706
  add(`${ui}/AGENTS.md`, `# ${title} UI guidance
660
707
 
661
708
  ## Scope
@@ -5,6 +5,7 @@ import {
5
5
  csrfSource,
6
6
  firebaseSource,
7
7
  httpErrorsSource,
8
+ loggerSource,
8
9
  mongoUsersSource,
9
10
  rateLimitSource,
10
11
  readEnvSource,
@@ -77,6 +78,14 @@ it("maps verified and unverified Firebase email claims and verifies only once",
77
78
 
78
79
  const errorTranslationTestSource = `import { AppError, type FailureKind } from "@/core/errors";
79
80
  import { errorHandler } from "@/platform/http/errors";
81
+ import { logger } from "@/platform/logging/logger";
82
+
83
+ jest.mock("@/platform/logging/logger", () => ({
84
+ logger: { log: jest.fn() },
85
+ serializeError: (error: unknown) => error
86
+ }));
87
+
88
+ const log = logger.log as jest.MockedFunction<typeof logger.log>;
80
89
 
81
90
  const statuses: Array<[FailureKind, number]> = [
82
91
  ["invalid", 400],
@@ -91,11 +100,58 @@ const statuses: Array<[FailureKind, number]> = [
91
100
  ];
92
101
 
93
102
  it.each(statuses)("translates %s to HTTP %i", (kind, status) => {
103
+ log.mockClear();
94
104
  const json = jest.fn();
95
105
  const response = { status: jest.fn(() => ({ json })) } as never;
96
- errorHandler(new AppError("CODE", "message", kind), {} as never, response, jest.fn());
106
+ const request = { method: "POST", path: "/health.get", requestContext: { requestId: "request-1" } } as never;
107
+ errorHandler(new AppError({ code: "CODE", message: "message", kind }), request, response, jest.fn());
97
108
  expect((response as { status: jest.Mock }).status).toHaveBeenCalledWith(status);
98
109
  expect(json).toHaveBeenCalledWith({ error: { code: "CODE", message: "message" } });
110
+ expect(log).toHaveBeenCalledWith(
111
+ expect.objectContaining({
112
+ level: status >= 500 ? "error" : "warn",
113
+ event: "http.request.failed",
114
+ requestId: "request-1",
115
+ code: "CODE",
116
+ kind
117
+ })
118
+ );
119
+ });
120
+
121
+ it("logs an unknown failure without exposing it to the client", () => {
122
+ log.mockClear();
123
+ const json = jest.fn();
124
+ const response = { status: jest.fn(() => ({ json })) } as never;
125
+ const error = new Error("database disconnected");
126
+ errorHandler(error, { method: "POST", path: "/health.get" } as never, response, jest.fn());
127
+ expect(json).toHaveBeenCalledWith({ error: { code: "INTERNAL", message: "Internal server error" } });
128
+ expect(log).toHaveBeenCalledWith(
129
+ expect.objectContaining({
130
+ level: "error",
131
+ code: "INTERNAL",
132
+ error: expect.objectContaining({ message: "database disconnected" })
133
+ })
134
+ );
135
+ });
136
+
137
+ it("logs a preserved application-error cause", () => {
138
+ log.mockClear();
139
+ const json = jest.fn();
140
+ const response = { status: jest.fn(() => ({ json })) } as never;
141
+ const cause = new Error("Firebase token verification failed");
142
+ errorHandler(
143
+ AppError.unauthenticated(cause),
144
+ { method: "POST", path: "/health.get" } as never,
145
+ response,
146
+ jest.fn()
147
+ );
148
+ expect(log).toHaveBeenCalledWith(
149
+ expect.objectContaining({
150
+ error: expect.objectContaining({
151
+ cause: expect.objectContaining({ message: "Firebase token verification failed" })
152
+ })
153
+ })
154
+ );
99
155
  });
100
156
  `;
101
157
 
@@ -188,6 +244,7 @@ export function ownedSources(config) {
188
244
  "src/core/errors.ts": appErrorSource,
189
245
  "src/core/SchemaLifecycle.ts": schemaLifecycleSource,
190
246
  "src/platform/config/readEnv.ts": readEnvSource,
247
+ "src/platform/logging/logger.ts": loggerSource,
191
248
  "src/platform/http/createRequestContext.ts": createRequestContextSource(profiles.has("firebase") ? "firebase" : "none"),
192
249
  "src/platform/http/csrf.ts": csrfSource,
193
250
  "src/platform/http/errors.ts": httpErrorsSource,
@@ -43,20 +43,61 @@ export const appErrorSource = `export type FailureKind =
43
43
  | "unavailable"
44
44
  | "internal";
45
45
 
46
+ export interface AppErrorOptions {
47
+ code: string;
48
+ message: string;
49
+ kind: FailureKind;
50
+ details?: unknown;
51
+ cause?: unknown;
52
+ }
53
+
46
54
  export class AppError extends Error {
47
55
  readonly name = "AppError";
56
+ readonly code: string;
57
+ readonly kind: FailureKind;
58
+ readonly details: unknown;
59
+
60
+ constructor({ code, message, kind, details, cause }: AppErrorOptions) {
61
+ super(message, cause === undefined ? undefined : { cause });
62
+ this.code = code;
63
+ this.kind = kind;
64
+ this.details = details;
65
+ }
48
66
 
49
- constructor(
50
- readonly code: string,
51
- message: string,
52
- readonly kind: FailureKind,
53
- readonly details?: unknown
54
- ) {
55
- super(message);
67
+ static unauthenticated(cause?: unknown): AppError {
68
+ return new AppError({ code: "UNAUTHENTICATED", message: "Unauthorized", kind: "unauthenticated", cause });
56
69
  }
57
70
  }
58
71
  `;
59
72
 
73
+ export const loggerSource = `import { createLogger, format, transports } from "winston";
74
+
75
+ export function serializeError(error: unknown, depth = 0): unknown {
76
+ if (!(error instanceof Error)) return error;
77
+ return {
78
+ name: error.name,
79
+ message: error.message,
80
+ stack: error.stack,
81
+ ...(error.cause === undefined || depth >= 3 ? {} : { cause: serializeError(error.cause, depth + 1) })
82
+ };
83
+ }
84
+
85
+ const serializeErrorMetadata = format((info) => {
86
+ if (info.error !== undefined) info.error = serializeError(info.error);
87
+ return info;
88
+ });
89
+
90
+ /**
91
+ * The service-wide logger. Tailframe always retains the JSON console transport so
92
+ * canonical failure logging cannot be disabled by application assembly.
93
+ */
94
+ export const logger = createLogger({
95
+ level: "info",
96
+ format: format.combine(format.timestamp(), format.errors({ stack: true }), serializeErrorMetadata(), format.json()),
97
+ transports: [new transports.Console()]
98
+ });
99
+ `;
100
+
60
101
  export function createRequestContextSource(auth) {
61
102
  if (auth === "firebase") {
62
103
  return `import { randomUUID } from "node:crypto";
@@ -88,7 +129,7 @@ export async function createRequestContext(req: Request): Promise<RequestContext
88
129
  return context;
89
130
  }
90
131
  if (!/^Bearer [^\\s]+$/.test(header)) {
91
- throw new AppError("UNAUTHENTICATED", "Unauthorized", "unauthenticated");
132
+ throw AppError.unauthenticated();
92
133
  }
93
134
 
94
135
  try {
@@ -99,8 +140,8 @@ export async function createRequestContext(req: Request): Promise<RequestContext
99
140
  };
100
141
  req.requestContext = context;
101
142
  return context;
102
- } catch {
103
- throw new AppError("UNAUTHENTICATED", "Unauthorized", "unauthenticated");
143
+ } catch (error) {
144
+ throw AppError.unauthenticated(error);
104
145
  }
105
146
  }
106
147
 
@@ -200,9 +241,10 @@ export function rpcHandler<Input, Output, WireInput = Input>(
200
241
  }
201
242
  `;
202
243
 
203
- export const httpErrorsSource = `import type { ErrorRequestHandler } from "express";
244
+ export const httpErrorsSource = `import type { ErrorRequestHandler, Request } from "express";
204
245
  import Joi from "joi";
205
246
  import { AppError } from "@/core/errors";
247
+ import { logger, serializeError } from "@/platform/logging/logger";
206
248
 
207
249
  const statusByKind: Readonly<Record<AppError["kind"], number>> = {
208
250
  invalid: 400,
@@ -216,9 +258,26 @@ const statusByKind: Readonly<Record<AppError["kind"], number>> = {
216
258
  internal: 500
217
259
  };
218
260
 
219
- export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
261
+ function logFailure(req: Request, error: unknown, status: number, code: string, kind?: AppError["kind"]) {
262
+ logger.log({
263
+ level: status >= 500 ? "error" : "warn",
264
+ message: "HTTP request failed",
265
+ event: "http.request.failed",
266
+ requestId: req.requestContext?.requestId,
267
+ method: req.method,
268
+ path: req.path,
269
+ status,
270
+ code,
271
+ ...(kind === undefined ? {} : { kind }),
272
+ error: serializeError(error)
273
+ });
274
+ }
275
+
276
+ export const errorHandler: ErrorRequestHandler = (error, req, res, _next) => {
220
277
  if (error instanceof AppError) {
221
- res.status(statusByKind[error.kind]).json({
278
+ const status = statusByKind[error.kind];
279
+ logFailure(req, error, status, error.code, error.kind);
280
+ res.status(status).json({
222
281
  error: {
223
282
  code: error.code,
224
283
  message: error.message,
@@ -228,6 +287,7 @@ export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
228
287
  return;
229
288
  }
230
289
  if (Joi.isError(error)) {
290
+ logFailure(req, error, 400, "VALIDATION_FAILED", "invalid");
231
291
  res.status(400).json({
232
292
  error: {
233
293
  code: "VALIDATION_FAILED",
@@ -237,11 +297,22 @@ export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
237
297
  });
238
298
  return;
239
299
  }
240
- console.error(error);
300
+ logFailure(req, error, 500, "INTERNAL", "internal");
241
301
  res.status(500).json({ error: { code: "INTERNAL", message: "Internal server error" } });
242
302
  };
243
303
  `;
244
304
 
305
+ export const observabilitySource = `import type { Logger } from "winston";
306
+
307
+ /**
308
+ * Add product-specific Winston transports here. Do not remove or reconfigure the
309
+ * Tailframe console transport: it is the required baseline for canonical errors.
310
+ */
311
+ export function configureObservability(_logger: Pick<Logger, "add">): void {
312
+ // Add product-specific transports and leave the canonical console transport intact.
313
+ }
314
+ `;
315
+
245
316
  export const csrfSource = `import type { RequestHandler } from "express";
246
317
  import { AppError } from "@/core/errors";
247
318
 
@@ -255,7 +326,7 @@ export function csrfHeaderGuard(exemptPaths: readonly string[] = []): RequestHan
255
326
  !exempt.has(req.path) &&
256
327
  req.headers["x-requested-with"] !== "XMLHttpRequest"
257
328
  ) {
258
- next(new AppError("CSRF_HEADER_MISSING", "CSRF header missing", "forbidden"));
329
+ next(new AppError({ code: "CSRF_HEADER_MISSING", message: "CSRF header missing", kind: "forbidden" }));
259
330
  return;
260
331
  }
261
332
  next();
@@ -271,6 +342,7 @@ export const schemaLifecycleSource = `export interface SchemaLifecycle {
271
342
 
272
343
  export const applySchemaCliSource = `import "reflect-metadata";
273
344
  import { schemaLifecycle } from "@/app/schema";
345
+ import { logger } from "@/platform/logging/logger";
274
346
 
275
347
  async function main() {
276
348
  try {
@@ -281,7 +353,7 @@ async function main() {
281
353
  }
282
354
 
283
355
  void main().catch((error) => {
284
- console.error(error instanceof Error ? error.message : error);
356
+ logger.error("Schema application failed", { event: "schema.apply.failed", error });
285
357
  process.exitCode = 1;
286
358
  });
287
359
  `;
@@ -343,9 +415,17 @@ export function createRateLimiter(client: RedisClientType, policy: RateLimitPoli
343
415
  next();
344
416
  } catch (error) {
345
417
  if (error instanceof RateLimiterRes)
346
- next(new AppError("RATE_LIMITED", "Too many requests", "rate_limited"));
418
+ next(new AppError({ code: "RATE_LIMITED", message: "Too many requests", kind: "rate_limited" }));
347
419
  else if (error instanceof AppError) next(error);
348
- else next(new AppError("RATE_LIMIT_UNAVAILABLE", "Request protection unavailable", "unavailable"));
420
+ else
421
+ next(
422
+ new AppError({
423
+ code: "RATE_LIMIT_UNAVAILABLE",
424
+ message: "Request protection unavailable",
425
+ kind: "unavailable",
426
+ cause: error
427
+ })
428
+ );
349
429
  }
350
430
  };
351
431
  }
@@ -426,8 +506,13 @@ export class GetReadiness implements UseCase<Record<string, never>, ReadinessRes
426
506
  try {
427
507
  await Promise.all(this.probes.map((probe) => probe.check()));
428
508
  return { status: "ready" };
429
- } catch {
430
- throw new AppError("NOT_READY", "Service is not ready", "unavailable");
509
+ } catch (error) {
510
+ throw new AppError({
511
+ code: "NOT_READY",
512
+ message: "Service is not ready",
513
+ kind: "unavailable",
514
+ cause: error
515
+ });
431
516
  }
432
517
  }
433
518
  }
@@ -496,9 +581,10 @@ export class RedisReadinessProbe implements ReadinessProbe {
496
581
 
497
582
  export const redisSource = `import { createClient, type RedisClientType } from "redis";
498
583
  import { env } from "@/platform/config/env";
584
+ import { logger } from "@/platform/logging/logger";
499
585
 
500
586
  export const redis: RedisClientType = createClient({ url: env.redisUrl });
501
- redis.on("error", (error) => console.error("Redis client error", error));
587
+ redis.on("error", (error) => logger.error("Redis client error", { event: "redis.client.error", error }));
502
588
 
503
589
  export async function connectRedis() {
504
590
  if (!redis.isOpen) await redis.connect();
@@ -563,10 +649,12 @@ export function serverSource({ ui, redis, csrf }) {
563
649
  return `${ui ? 'import path from "node:path";\n' : ""}import express from "express";
564
650
  import cors from "cors";
565
651
  import { registerDependencies } from "@/app/container";
652
+ import { configureObservability } from "@/app/observability";
566
653
  import { applicationRoutes } from "@/app/routes";
567
654
  import { env } from "@/platform/config/env";
568
655
  import { closeDatabase, connectDatabase } from "@/platform/database";
569
656
  ${redis ? 'import { closeRedis, connectRedis } from "@/platform/redis";\n' : ""}${csrf ? 'import { csrfHeaderGuard } from "@/platform/http/csrf";\n' : ""}import { errorHandler } from "@/platform/http/errors";
657
+ import { logger } from "@/platform/logging/logger";
570
658
 
571
659
  export interface ApplicationOptions {
572
660
  ${ui ? "\tpublicPath?: string;\n\tproduction?: boolean;\n" : ""}}
@@ -582,43 +670,57 @@ ${ui ? '\tif (production) {\n\t\tapp.get(/^(?!\\/api(?:\\/|$)).*/, (_req, res) =
582
670
  }
583
671
 
584
672
  export async function startServer() {
585
- const db = await connectDatabase();
586
- ${redis ? "\tconst redis = await connectRedis();\n" : ""} registerDependencies({ db${redis ? ", redis" : ""} });
587
- const app = createApplication();
588
- const server = app.listen(env.port);
589
- const shutdown = async () => {
590
- await new Promise<void>((resolve, reject) => {
591
- server.close((error) => (error ? reject(error) : resolve()));
592
- });
593
- ${redis ? "\t\tawait closeRedis();\n" : ""} await closeDatabase();
594
- };
595
- process.once("SIGINT", () => void shutdown());
596
- process.once("SIGTERM", () => void shutdown());
597
- return { app, server, shutdown };
673
+ try {
674
+ configureObservability(logger);
675
+ const db = await connectDatabase();
676
+ ${redis ? "\t\tconst redis = await connectRedis();\n" : ""} registerDependencies({ db${redis ? ", redis" : ""} });
677
+ const app = createApplication();
678
+ const server = app.listen(env.port);
679
+ const shutdown = async () => {
680
+ await new Promise<void>((resolve, reject) => {
681
+ server.close((error) => (error ? reject(error) : resolve()));
682
+ });
683
+ ${redis ? "\t\t\tawait closeRedis();\n" : ""} await closeDatabase();
684
+ };
685
+ process.once("SIGINT", () => void shutdown());
686
+ process.once("SIGTERM", () => void shutdown());
687
+ return { app, server, shutdown };
688
+ } catch (error) {
689
+ logger.error("Server startup failed", { event: "server.start.failed", error });
690
+ throw error;
691
+ }
598
692
  }
599
693
  `;
600
694
  }
601
695
 
602
696
  export function workerSource() {
603
697
  return `import { container, registerDependencies } from "@/app/container";
698
+ import { configureObservability } from "@/app/observability";
604
699
  import { GetReadiness } from "@/modules/health/use-cases/GetReadiness";
605
700
  import { closeDatabase, connectDatabase } from "@/platform/database";
606
701
  import { systemRequestContext } from "@/platform/http/createRequestContext";
702
+ import { logger } from "@/platform/logging/logger";
607
703
  import { closeRedis, connectRedis } from "@/platform/redis";
608
704
 
609
705
  export async function startWorker() {
610
- const db = await connectDatabase();
611
- const redis = await connectRedis();
612
- registerDependencies({ db, redis });
613
- await container.resolve(GetReadiness).execute(systemRequestContext(), {});
614
-
615
- const shutdown = async () => {
616
- await closeRedis();
617
- await closeDatabase();
618
- };
619
- process.once("SIGINT", () => void shutdown());
620
- process.once("SIGTERM", () => void shutdown());
621
- return { shutdown };
706
+ try {
707
+ configureObservability(logger);
708
+ const db = await connectDatabase();
709
+ const redis = await connectRedis();
710
+ registerDependencies({ db, redis });
711
+ await container.resolve(GetReadiness).execute(systemRequestContext(), {});
712
+
713
+ const shutdown = async () => {
714
+ await closeRedis();
715
+ await closeDatabase();
716
+ };
717
+ process.once("SIGINT", () => void shutdown());
718
+ process.once("SIGTERM", () => void shutdown());
719
+ return { shutdown };
720
+ } catch (error) {
721
+ logger.error("Worker startup failed", { event: "worker.start.failed", error });
722
+ throw error;
723
+ }
622
724
  }
623
725
  `;
624
726
  }