From 8d7a7be596bdacf3b1e2728288946077688431e5 Mon Sep 17 00:00:00 2001 From: Kai Haase Date: Sun, 14 Jun 2026 21:36:46 +0200 Subject: [PATCH] 11.27.0: expose build identity (commit SHA + version) via health check so drifted containers are detectable after partial rollouts --- FRAMEWORK-API.md | 5 +- migration-guides/11.26.3-to-11.27.0.md | 249 ++++++++++++++++ package.json | 5 +- pnpm-lock.yaml | 271 +++++++++--------- spectaql.yml | 2 +- src/core/common/helpers/meta.helper.ts | 69 +++++ .../interfaces/server-options.interface.ts | 29 ++ .../health-check/core-health-check.service.ts | 13 + src/index.ts | 1 + tests/unit/meta-helper.spec.ts | 64 +++++ 10 files changed, 568 insertions(+), 140 deletions(-) create mode 100644 migration-guides/11.26.3-to-11.27.0.md create mode 100644 src/core/common/helpers/meta.helper.ts create mode 100644 tests/unit/meta-helper.spec.ts diff --git a/FRAMEWORK-API.md b/FRAMEWORK-API.md index 6b81902..fc01b87 100644 --- a/FRAMEWORK-API.md +++ b/FRAMEWORK-API.md @@ -1,6 +1,6 @@ # @lenne.tech/nest-server — Framework API Reference -> Auto-generated from source code on 2026-06-03 (v11.26.3) +> Auto-generated from source code on 2026-06-14 (v11.27.0) > File: `FRAMEWORK-API.md` — compact, machine-readable API surface for Claude Code ## CoreModule.forRoot() @@ -26,11 +26,12 @@ - `debugProcessInput?`: `boolean` (default: `false`) — When true, logs a debug message when prepareInput() changes the input type during process(). - `email?`: `{ defaultSender?: { email?: string; name?: string; }; mailjet?: MailjetOption...` — SMTP and template configuration for sending emails - `env?`: `string` — Environment + - `version?`: `string` — Semantic version of the running build (e.g. from package.json / meta.json). - `errorCode?`: `IErrorCode` — Configuration for the error code module - `execAfterInit?`: `string` — Exec a command after server is initialized - `filter?`: `{ maxLimit?: number; }` — Filter configuration and defaults - `graphQl?`: `false | { driver?: ApolloDriverConfig; enableSubscriptionAuth?: boolean; maxC...` — Configuration of the GraphQL module - - `healthCheck?`: `{ configs?: { database?: { enabled?: boolean; key?: string; options?: Mongoos...` — Whether to activate health check endpoints + - `healthCheck?`: `{ configs?: { build?: { enabled?: boolean; }; database?: { enabled?: boolean;...` — Whether to activate health check endpoints - `hostname?`: `string` — Hostname of the server - `ignoreSelectionsForPopulate?`: `boolean` — Ignore selections in fieldSelection - `jwt?`: `IJwt & JwtModuleOptions & { refresh?: IJwt & { renewal?: boolean; }; sameToke...` — Configuration of JavaScript Web Token (JWT) module diff --git a/migration-guides/11.26.3-to-11.27.0.md b/migration-guides/11.26.3-to-11.27.0.md new file mode 100644 index 0000000..2bcfb59 --- /dev/null +++ b/migration-guides/11.26.3-to-11.27.0.md @@ -0,0 +1,249 @@ +# Migration Guide: 11.26.3 → 11.27.0 + +## Overview + +| Category | Details | +|----------|---------| +| **Breaking Changes** | None | +| **New Features** | Build-identity helpers (`getCommit()` / `getBuildInfo()`); the running build's commit / version / environment is now surfaced in the `/health-check` response; new `IServerOptions.version` and `healthCheck.configs.build` options | +| **Bugfixes** | None | +| **Migration Effort** | 0 minutes (automatic) — the health check exposes the build identity on update. ~5 minutes optional to bake the real commit SHA into your image so it stops reporting `unknown` | + +This release makes the **running build identifiable at runtime**. App and API are +typically deployed together but versioned independently, so a partial / stale +rollout (one container older than the other) is otherwise hard to spot. The build +**commit SHA** is the drift detector: bake the same CI commit into both images and +compare them. + +--- + +## Quick Migration + +No code changes required. The build indicator is added to `/health-check` +automatically — the commit just reports `'unknown'` until you bake it into the +image (see [Adopt the full drift detection](#adopt-the-full-drift-detection-optional)). + +```bash +# Update package +pnpm add @lenne.tech/nest-server@11.27.0 + +# Verify build +pnpm run build + +# Run tests +pnpm test +``` + +--- + +## What's New in 11.27.0 + +### 1. Build-identity helpers: `getCommit()` / `getBuildInfo()` + +Two pure helpers are now exported from `@lenne.tech/nest-server`. They give every +project ONE canonical way to resolve which build is running, instead of re-reading +`process.env` ad hoc. + +```typescript +import { getBuildInfo, getCommit } from '@lenne.tech/nest-server'; + +// Commit SHA the build was produced from. Reads process.env.APP_VERSION_COMMIT +// (override the var name if you like), falling back to 'unknown' for local builds. +getCommit(); // → 'a1b2c3d…' or 'unknown' +getCommit('MY_COMMIT_ENV'); // → reads a custom env var + +// Full build identity, ready to surface via a meta / info endpoint. +getBuildInfo({ env: 'production', version: '1.4.0' }); +// → { commit: 'a1b2c3d…', env: 'production', version: '1.4.0' } +``` + +Also exported: `BuildInfo` (type), `DEFAULT_COMMIT_ENV` (`'APP_VERSION_COMMIT'`), +`UNKNOWN_COMMIT` (`'unknown'`). + +### 2. Build identity in the `/health-check` response + +The core health check now includes a `build` indicator. It is **always** reported +with status `up`, so it surfaces under `info`/`details` **without ever affecting +the overall health status**. Ops and monitoring can detect a drifted / stale +container, not just the admin UI. + +```jsonc +// GET /health-check (and the GraphQL `healthCheck` query) +{ + "status": "ok", + "info": { + "database": { "status": "up" }, + "build": { + "status": "up", + "commit": "a1b2c3d4e5f6…", // process.env.APP_VERSION_COMMIT, or "unknown" + "env": "production", // from IServerOptions.env + "version": "1.4.0" // from IServerOptions.version (see #3), or "unknown" + } + }, + "details": { "database": { "status": "up" }, "build": { /* same */ } } +} +``` + +Opt out (e.g. if you prefer to keep build info out of an unauthenticated probe): + +```typescript +// config.env.ts +healthCheck: { + configs: { + build: { enabled: false }, + }, +} +``` + +### 3. New `IServerOptions.version` config field + +So the health-check build indicator can report your app version, set it from your +`package.json` / `meta.json`: + +```typescript +// config.env.ts +import metaData = require('./meta.json'); + +const base: Partial = { + env: envName, + version: metaData.version, // ← surfaced in /health-check build identity + // … +}; +``` + +Without it, the indicator reports `version: 'unknown'` — harmless, but less useful. + +### Adopt the full drift detection (optional) + +For the end-to-end "is App on the same build as API?" check, the commit must be +baked into the image at build time and exposed publicly. The +[nest-server-starter](https://github.com/lenneTech/nest-server-starter) reference +implementation wires all three layers: + +1. **Image** — bake the commit at build time: + ```dockerfile + # In the runtime stage (the API reads it at runtime): + ARG APP_VERSION_COMMIT=unknown + ENV APP_VERSION_COMMIT=$APP_VERSION_COMMIT + ``` +2. **Build arg** — feed the CI commit SHA: + ```yaml + # docker-compose.yml + build: + args: + APP_VERSION_COMMIT: ${IMAGE_TAG:-unknown} # IMAGE_TAG = CI_COMMIT_SHA + ``` +3. **Public endpoint** — the starter's `meta` module exposes `GET /meta` + (`S_EVERYONE`) returning `{ version, commit, environment, package, title }`, + so the frontend can read the API commit and compare it against its own. + +The contract end to end: + +``` +CI commit SHA → IMAGE_TAG (CI) → APP_VERSION_COMMIT build arg (compose) + → ENV in the image (Dockerfile) → getCommit() → /health-check + /meta +``` + +Versions (semver) are per-component and may legitimately differ — only the +**commit** is compared. Local builds without CI report `unknown`, which clients +use to suppress the drift warning. + +--- + +## Breaking Changes + +None. All additions are backward compatible. + +--- + +## Compatibility Notes + +- **Existing `/health-check` consumers:** The response gains one extra key + (`build`) under `info` and `details`. `status` is unchanged and the `build` + indicator can never flip the overall status to `error` (it always reports `up`). + Only consumers that assert the *exact* set of keys need updating; disable it via + `healthCheck.configs.build.enabled: false` if you must keep the old shape. +- **GraphQL `healthCheck` query:** `info`/`details` are `JSON` scalars, so the new + `build` key flows through without any schema change. +- **`IServerOptions`:** `version` and `healthCheck.configs.build` are new optional + fields. Existing configs compile unchanged. +- **Commit resolution:** If you previously read `process.env.APP_VERSION_COMMIT` + yourself, you can switch to `getCommit()` for the identical result (with the + `'unknown'` fallback) — optional, not required. +- **Vendor-mode consumers:** The same additions land in + `src/core/common/helpers/meta.helper.ts` and + `src/core/modules/health-check/core-health-check.service.ts`. Sync via the + vendored-core updater. No flatten-fix change required. + +--- + +## Verifying + +```bash +# Boot the API and read the health check +pnpm start & +curl -s http://localhost:3000/health-check | jq '.info.build' +# → { "status": "up", "commit": "unknown", "env": "local", "version": "1.4.0" } + +# After a CI build that passes APP_VERSION_COMMIT, "commit" is the real SHA: +APP_VERSION_COMMIT=$(git rev-parse HEAD) pnpm start & +curl -s http://localhost:3000/health-check | jq '.info.build.commit' +``` + +A unit test for the helpers ships in `tests/unit/meta-helper.spec.ts`. + +--- + +## Troubleshooting + +### `commit` is always `"unknown"` + +The build arg never reached the process. Check, in order: + +1. CI passes the commit SHA into the build (`IMAGE_TAG` / `APP_VERSION_COMMIT`). +2. `docker-compose.yml` forwards it as a `build.args.APP_VERSION_COMMIT`. +3. The **API** Dockerfile declares `ARG`/`ENV APP_VERSION_COMMIT` in the **runtime + stage** (the API reads it at runtime, not build time). +4. For a frontend, the commit must be set **before** the bundler runs (e.g. Nuxt + freezes `runtimeConfig.public` at build time) — there the `ARG`/`ENV` belongs in + the **build stage**, not the runtime stage. + +### `version` is `"unknown"` + +Set `IServerOptions.version` (see [#3](#3-new-iserveroptionsversion-config-field)). +The health check reads `config.version`; nothing else populates it. + +### I don't want build info on an unauthenticated endpoint + +`/health-check` is `S_EVERYONE` by design (probes need it). Disable just the build +indicator with `healthCheck.configs.build.enabled: false`, or restrict the route +in your project. + +--- + +## Module Documentation + +### Core Common — build-identity helpers + +- **Helpers:** `src/core/common/helpers/meta.helper.ts` (`getCommit`, `getBuildInfo`, `BuildInfo`, `DEFAULT_COMMIT_ENV`, `UNKNOWN_COMMIT`) +- **Unit tests:** `tests/unit/meta-helper.spec.ts` + +### Core Health Check + +- **Service:** `src/core/modules/health-check/core-health-check.service.ts` (build indicator) +- **Controller / Resolver:** `src/core/modules/health-check/core-health-check.controller.ts`, `core-health-check.resolver.ts` +- **Config:** `IServerOptions.healthCheck.configs.build` + `IServerOptions.version` in `src/core/common/interfaces/server-options.interface.ts` + +### Reference implementation (full drift detection) + +- **API meta module + Dockerfile build arg:** [nest-server-starter](https://github.com/lenneTech/nest-server-starter) — `src/server/modules/meta/` exposes `GET /meta` with `commit` +- **Frontend system page:** [nuxt-base-starter](https://github.com/lenneTech/nuxt-base-starter) — `/app/admin/system` compares App vs. API builds + +--- + +## References + +- [Migration Guide 11.26.2 → 11.26.3](./11.26.2-to-11.26.3.md) — Previous release (OpenAPI enum `$ref` fix) +- [nest-server-starter](https://github.com/lenneTech/nest-server-starter) (reference implementation) +- [nuxt-base-starter](https://github.com/lenneTech/nuxt-base-starter) (frontend drift detection) +- [lt-monorepo](https://github.com/lenneTech/lt-monorepo) — `docker-compose.yml` + CI wiring for the `APP_VERSION_COMMIT` contract diff --git a/package.json b/package.json index a7df719..a1b1055 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lenne.tech/nest-server", - "version": "11.26.3", + "version": "11.27.0", "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).", "keywords": [ "node", @@ -232,7 +232,8 @@ "defu@<=6.1.6": "6.1.7", "follow-redirects@<=1.15.11": "1.16.0", "uuid@<14.0.0": "14.0.0", - "postcss@<8.5.10": "8.5.12" + "postcss@<8.5.10": "8.5.12", + "esbuild@>=0.17.0 <0.28.1": "0.28.1" }, "//peerDependencyRules": "allowedVersions: deps lag behind our newer majors (graphql-upload wants @types/express@^4, the deprecated apollo playground plugin wants @apollo/server@^4) — both work with our v5. ignoreMissing: browser-only vis-network peers pulled in transitively via yuml-diagram (server-side UML generation never renders, so these are not needed).", "peerDependencyRules": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b60af3..875f41d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,7 @@ overrides: follow-redirects@<=1.15.11: 1.16.0 uuid@<14.0.0: 14.0.0 postcss@<8.5.10: 8.5.12 + esbuild@>=0.17.0 <0.28.1: 0.28.1 importers: @@ -187,7 +188,7 @@ importers: version: 1.2.1(component-emitter@1.3.1)(typescript@5.9.3) '@nestjs/cli': specifier: 11.0.21 - version: 11.0.21(@swc/cli@0.8.1(@swc/core@1.15.40))(@swc/core@1.15.40)(@types/node@25.9.1)(esbuild@0.28.0) + version: 11.0.21(@swc/cli@0.8.1(@swc/core@1.15.40))(@swc/core@1.15.40)(@types/node@25.9.1)(esbuild@0.28.1) '@nestjs/schematics': specifier: 11.1.0 version: 11.1.0(chokidar@4.0.3)(typescript@5.9.3) @@ -280,13 +281,13 @@ importers: version: 1.5.9(@swc/core@1.15.40) vite: specifier: 8.0.14 - version: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.22.3) + version: 8.0.14(@types/node@25.9.1)(esbuild@0.28.1)(terser@5.46.0)(tsx@4.22.3) vite-plugin-node: specifier: 8.0.0 - version: 8.0.0(@swc/core@1.15.40)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.22.3)) + version: 8.0.0(@swc/core@1.15.40)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.1)(terser@5.46.0)(tsx@4.22.3)) vitest: specifier: 4.1.7 - version: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(@vitest/ui@4.1.7)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.22.3)) + version: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(@vitest/ui@4.1.7)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.1)(terser@5.46.0)(tsx@4.22.3)) packages: @@ -1054,158 +1055,158 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.28.0': - resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.28.0': - resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.28.0': - resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.28.0': - resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.28.0': - resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.28.0': - resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.28.0': - resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.28.0': - resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.28.0': - resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.28.0': - resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.28.0': - resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.28.0': - resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.28.0': - resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.28.0': - resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.28.0': - resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.28.0': - resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.28.0': - resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.28.0': - resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.28.0': - resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.28.0': - resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.28.0': - resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.28.0': - resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.28.0': - resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.28.0': - resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.28.0': - resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.28.0': - resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -3553,8 +3554,8 @@ packages: es6-shim@0.35.8: resolution: {integrity: sha512-Twf7I2v4/1tLoIXMT8HlqaBSS5H2wQTs2wx3MNYCI8K1R1/clXyCazrcVCPm/FuO9cyV8+leEaZOWD5C253NDg==} - esbuild@0.28.0: - resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -5839,7 +5840,7 @@ packages: peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 '@vitejs/devtools': ^0.1.18 - esbuild: ^0.27.0 || ^0.28.0 + esbuild: 0.28.1 jiti: '>=1.21.0' less: ^4.0.0 sass: ^1.70.0 @@ -7096,82 +7097,82 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.28.0': + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.28.0': + '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.28.0': + '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.28.0': + '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.28.0': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.28.0': + '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.28.0': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.28.0': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.28.0': + '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.28.0': + '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.28.0': + '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.28.0': + '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.28.0': + '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.28.0': + '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.28.0': + '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.28.0': + '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.28.0': + '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.28.0': + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.28.0': + '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.28.0': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.28.0': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.28.0': + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.28.0': + '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.28.0': + '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.28.0': + '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.28.0': + '@esbuild/win32-x64@0.28.1': optional: true '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': @@ -7569,7 +7570,7 @@ snapshots: optionalDependencies: '@as-integrations/express5': 1.1.2(@apollo/server@5.5.1(graphql@16.14.0))(express@5.2.1) - '@nestjs/cli@11.0.21(@swc/cli@0.8.1(@swc/core@1.15.40))(@swc/core@1.15.40)(@types/node@25.9.1)(esbuild@0.28.0)': + '@nestjs/cli@11.0.21(@swc/cli@0.8.1(@swc/core@1.15.40))(@swc/core@1.15.40)(@types/node@25.9.1)(esbuild@0.28.1)': dependencies: '@angular-devkit/core': 19.2.24(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) @@ -7580,14 +7581,14 @@ snapshots: chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.40)(esbuild@0.28.0)) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.40)(esbuild@0.28.1)) glob: 13.0.6 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.106.0(@swc/core@1.15.40)(esbuild@0.28.0) + webpack: 5.106.0(@swc/core@1.15.40)(esbuild@0.28.1) webpack-node-externals: 3.0.0 optionalDependencies: '@swc/cli': 0.8.1(@swc/core@1.15.40) @@ -8397,7 +8398,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(@vitest/ui@4.1.7)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.22.3)) + vitest: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(@vitest/ui@4.1.7)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.1)(terser@5.46.0)(tsx@4.22.3)) '@vitest/expect@4.1.7': dependencies: @@ -8408,13 +8409,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.22.3))': + '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.1)(terser@5.46.0)(tsx@4.22.3))': dependencies: '@vitest/spy': 4.1.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.22.3) + vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.1)(terser@5.46.0)(tsx@4.22.3) '@vitest/pretty-format@4.1.7': dependencies: @@ -8443,7 +8444,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vitest: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(@vitest/ui@4.1.7)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.22.3)) + vitest: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(@vitest/ui@4.1.7)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.1)(terser@5.46.0)(tsx@4.22.3)) '@vitest/utils@4.1.7': dependencies: @@ -8828,7 +8829,7 @@ snapshots: zod: 4.3.6 optionalDependencies: mongodb: 7.2.0 - vitest: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(@vitest/ui@4.1.7)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.22.3)) + vitest: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(@vitest/ui@4.1.7)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.1)(terser@5.46.0)(tsx@4.22.3)) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -9380,34 +9381,34 @@ snapshots: es6-shim@0.35.8: {} - esbuild@0.28.0: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.28.0 - '@esbuild/android-arm': 0.28.0 - '@esbuild/android-arm64': 0.28.0 - '@esbuild/android-x64': 0.28.0 - '@esbuild/darwin-arm64': 0.28.0 - '@esbuild/darwin-x64': 0.28.0 - '@esbuild/freebsd-arm64': 0.28.0 - '@esbuild/freebsd-x64': 0.28.0 - '@esbuild/linux-arm': 0.28.0 - '@esbuild/linux-arm64': 0.28.0 - '@esbuild/linux-ia32': 0.28.0 - '@esbuild/linux-loong64': 0.28.0 - '@esbuild/linux-mips64el': 0.28.0 - '@esbuild/linux-ppc64': 0.28.0 - '@esbuild/linux-riscv64': 0.28.0 - '@esbuild/linux-s390x': 0.28.0 - '@esbuild/linux-x64': 0.28.0 - '@esbuild/netbsd-arm64': 0.28.0 - '@esbuild/netbsd-x64': 0.28.0 - '@esbuild/openbsd-arm64': 0.28.0 - '@esbuild/openbsd-x64': 0.28.0 - '@esbuild/openharmony-arm64': 0.28.0 - '@esbuild/sunos-x64': 0.28.0 - '@esbuild/win32-arm64': 0.28.0 - '@esbuild/win32-ia32': 0.28.0 - '@esbuild/win32-x64': 0.28.0 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 escalade@3.2.0: {} @@ -9734,7 +9735,7 @@ snapshots: dependencies: is-callable: 1.2.7 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.40)(esbuild@0.28.0)): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.40)(esbuild@0.28.1)): dependencies: '@babel/code-frame': 7.29.0 chalk: 4.1.2 @@ -9749,7 +9750,7 @@ snapshots: semver: 7.7.4 tapable: 2.3.0 typescript: 5.9.3 - webpack: 5.106.0(@swc/core@1.15.40)(esbuild@0.28.0) + webpack: 5.106.0(@swc/core@1.15.40)(esbuild@0.28.1) form-data-encoder@4.1.0: {} @@ -11407,16 +11408,16 @@ snapshots: - bare-abort-controller - react-native-b4a - terser-webpack-plugin@5.3.17(@swc/core@1.15.40)(esbuild@0.28.0)(webpack@5.106.0(@swc/core@1.15.40)(esbuild@0.28.0)): + terser-webpack-plugin@5.3.17(@swc/core@1.15.40)(esbuild@0.28.1)(webpack@5.106.0(@swc/core@1.15.40)(esbuild@0.28.1)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.46.0 - webpack: 5.106.0(@swc/core@1.15.40)(esbuild@0.28.0) + webpack: 5.106.0(@swc/core@1.15.40)(esbuild@0.28.1) optionalDependencies: '@swc/core': 1.15.40 - esbuild: 0.28.0 + esbuild: 0.28.1 terser@5.46.0: dependencies: @@ -11535,7 +11536,7 @@ snapshots: tsx@4.22.3: dependencies: - esbuild: 0.28.0 + esbuild: 0.28.1 optionalDependencies: fsevents: 2.3.3 @@ -11671,18 +11672,18 @@ snapshots: component-emitter: 1.3.1 uuid: 14.0.0 - vite-plugin-node@8.0.0(@swc/core@1.15.40)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.22.3)): + vite-plugin-node@8.0.0(@swc/core@1.15.40)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.1)(terser@5.46.0)(tsx@4.22.3)): dependencies: chalk: 4.1.2 debounce: 2.2.0 debug: 4.4.3(supports-color@5.5.0) - vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.22.3) + vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.1)(terser@5.46.0)(tsx@4.22.3) optionalDependencies: '@swc/core': 1.15.40 transitivePeerDependencies: - supports-color - vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.22.3): + vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.1)(terser@5.46.0)(tsx@4.22.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -11691,15 +11692,15 @@ snapshots: tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.9.1 - esbuild: 0.28.0 + esbuild: 0.28.1 fsevents: 2.3.3 terser: 5.46.0 tsx: 4.22.3 - vitest@4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(@vitest/ui@4.1.7)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.22.3)): + vitest@4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(@vitest/ui@4.1.7)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.1)(terser@5.46.0)(tsx@4.22.3)): dependencies: '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.22.3)) + '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.1)(terser@5.46.0)(tsx@4.22.3)) '@vitest/pretty-format': 4.1.7 '@vitest/runner': 4.1.7 '@vitest/snapshot': 4.1.7 @@ -11716,7 +11717,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.22.3) + vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.1)(terser@5.46.0)(tsx@4.22.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.9.1 @@ -11744,7 +11745,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.106.0(@swc/core@1.15.40)(esbuild@0.28.0): + webpack@5.106.0(@swc/core@1.15.40)(esbuild@0.28.1): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -11768,7 +11769,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.17(@swc/core@1.15.40)(esbuild@0.28.0)(webpack@5.106.0(@swc/core@1.15.40)(esbuild@0.28.0)) + terser-webpack-plugin: 5.3.17(@swc/core@1.15.40)(esbuild@0.28.1)(webpack@5.106.0(@swc/core@1.15.40)(esbuild@0.28.1)) watchpack: 2.5.1 webpack-sources: 3.3.4 transitivePeerDependencies: diff --git a/spectaql.yml b/spectaql.yml index fa7e183..842f277 100644 --- a/spectaql.yml +++ b/spectaql.yml @@ -19,7 +19,7 @@ servers: info: title: lT Nest Server description: Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases). - version: 11.26.3 + version: 11.27.0 contact: name: lenne.Tech GmbH url: https://lenne.tech diff --git a/src/core/common/helpers/meta.helper.ts b/src/core/common/helpers/meta.helper.ts new file mode 100644 index 0000000..99d483c --- /dev/null +++ b/src/core/common/helpers/meta.helper.ts @@ -0,0 +1,69 @@ +/** + * Build identity helpers. + * + * A deployed image's exact build is identified by its git commit SHA, baked in + * at build time via an environment variable (default `APP_VERSION_COMMIT`, fed + * from the CI commit SHA — the same value typically used as the image tag). + * + * Unlike the semantic `version` (a rarely-bumped semver that may legitimately + * differ between an API and its frontend, since each is versioned independently) + * the commit SHA uniquely pins the exact running build. A frontend and backend + * deployed together bake the SAME commit, but each reads its own at runtime — so + * comparing them detects a drifted / stale container after a partial rollout. + * + * @see getCommit + * @see getBuildInfo + */ + +/** Default environment variable the build commit SHA is baked into. */ +export const DEFAULT_COMMIT_ENV = 'APP_VERSION_COMMIT'; + +/** Defined value returned when no commit could be resolved. */ +export const UNKNOWN_COMMIT = 'unknown'; + +/** + * Build identity of the running process. + */ +export interface BuildInfo { + /** Git commit SHA the build was produced from, or `'unknown'`. */ + commit: string; + + /** Environment the process runs in (e.g. `'production'`), if provided. */ + env?: string; + + /** Semantic version of the build, or `'unknown'`. */ + version?: string; +} + +/** + * Resolve the git commit SHA the running build was produced from. + * + * Reads `process.env[envName]` (default `APP_VERSION_COMMIT`). Falls back to + * `'unknown'` so local / un-tagged builds still return a defined value — clients + * use `'unknown'` to suppress the "builds drifted" warning instead of comparing + * against an empty string. + * + * @param envName Name of the environment variable holding the commit SHA. + */ +export function getCommit(envName: string = DEFAULT_COMMIT_ENV): string { + return process.env[envName] || UNKNOWN_COMMIT; +} + +/** + * Assemble the build identity of the running process. + * + * Combines the commit SHA (from the environment) with an optionally supplied + * `version` and `env`. Designed to be surfaced via a public meta / info endpoint + * and the health check so deployments can be compared at a glance. + * + * @param options.commitEnvName Override the env var the commit SHA is read from. + * @param options.env Environment label to include (e.g. from the config). + * @param options.version Semantic version to include (e.g. from package.json). + */ +export function getBuildInfo(options: { commitEnvName?: string; env?: string; version?: string } = {}): BuildInfo { + return { + commit: getCommit(options.commitEnvName), + env: options.env, + version: options.version || UNKNOWN_COMMIT, + }; +} diff --git a/src/core/common/interfaces/server-options.interface.ts b/src/core/common/interfaces/server-options.interface.ts index 24b0ac6..d7501b2 100644 --- a/src/core/common/interfaces/server-options.interface.ts +++ b/src/core/common/interfaces/server-options.interface.ts @@ -1591,6 +1591,17 @@ export interface IServerOptions { */ env?: string; + /** + * Semantic version of the running build (e.g. from package.json / meta.json). + * + * Surfaced via the build-identity health indicator alongside the commit SHA. + * Unlike the commit (which uniquely pins the exact build), the version is a + * rarely-bumped semver and may legitimately differ between API and frontend. + * + * @since 11.27.0 + */ + version?: string; + /** * Configuration for the error code module * @@ -1671,6 +1682,24 @@ export interface IServerOptions { * Configuration of single health checks */ configs?: { + /** + * Configuration for the build-identity health indicator. + * + * Always reports status "up" and surfaces the running build's commit SHA, + * version and environment under the health check's `info`/`details`, so a + * drifted / stale container can be detected after a partial rollout. The + * commit is read from `process.env.APP_VERSION_COMMIT` (baked at build + * time from the CI commit SHA); `version` comes from {@link IServerOptions.version}. + * + * @since 11.27.0 + */ + build?: { + /** + * Whether to include build identity in the health check (default: true) + */ + enabled?: boolean; + }; + /** * Configuration for database health check */ diff --git a/src/core/modules/health-check/core-health-check.service.ts b/src/core/modules/health-check/core-health-check.service.ts index ba1f366..514d175 100644 --- a/src/core/modules/health-check/core-health-check.service.ts +++ b/src/core/modules/health-check/core-health-check.service.ts @@ -9,6 +9,7 @@ import { import type { MongoosePingCheckSettings } from '@nestjs/terminus/dist/health-indicator/database/mongoose.health.js'; import type { DiskHealthIndicatorOptions } from '@nestjs/terminus/dist/health-indicator/disk/disk-health-options.type.js'; +import { getBuildInfo } from '../../common/helpers/meta.helper'; import { ConfigService } from '../../common/services/config.service'; /** @@ -64,6 +65,18 @@ export class CoreHealthCheckService { ), ); } + // Build identity (commit / version / env) — always reported as "up", so it + // surfaces under `info`/`details` without ever affecting the overall health + // status. Lets ops/monitoring detect a drifted or stale container after a + // partial rollout (the same commit-SHA signal the admin UI compares). The + // commit is baked into the image at build time (APP_VERSION_COMMIT, fed from + // the CI commit SHA); `version`/`env` come from the config. Opt out with + // `healthCheck.configs.build.enabled: false`. + if (this.config.get('healthCheck.configs.build.enabled') !== false) { + const build = getBuildInfo({ env: this.config.get('env'), version: this.config.get('version') }); + healthIndicatorFunctions.push(async () => ({ build: { ...build, status: 'up' as const } })); + } + return this.health.check(healthIndicatorFunctions); } } diff --git a/src/index.ts b/src/index.ts index bdaf513..108cf00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,6 +40,7 @@ export * from './core/common/helpers/interceptor.helper'; export * from './core/common/helpers/gridfs.helper'; export * from './core/common/helpers/input.helper'; export * from './core/common/helpers/logging.helper'; +export * from './core/common/helpers/meta.helper'; export * from './core/common/helpers/model.helper'; export * from './core/common/helpers/register-enum.helper'; export * from './core/common/helpers/scim.helper'; diff --git a/tests/unit/meta-helper.spec.ts b/tests/unit/meta-helper.spec.ts new file mode 100644 index 0000000..4262a15 --- /dev/null +++ b/tests/unit/meta-helper.spec.ts @@ -0,0 +1,64 @@ +/** + * Unit Tests: build identity helpers + * + * Verifies that the commit SHA is resolved from the environment with a defined + * `'unknown'` fallback, and that `getBuildInfo` assembles commit/version/env. + */ + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { DEFAULT_COMMIT_ENV, getBuildInfo, getCommit, UNKNOWN_COMMIT } from '../../src/core/common/helpers/meta.helper'; + +describe('meta.helper', () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + delete process.env[DEFAULT_COMMIT_ENV]; + delete process.env.CUSTOM_COMMIT; + }); + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + describe('getCommit', () => { + it('reads the commit from the default env var', () => { + process.env[DEFAULT_COMMIT_ENV] = 'abc1234'; + expect(getCommit()).toBe('abc1234'); + }); + + it('reads the commit from a custom env var', () => { + process.env.CUSTOM_COMMIT = 'def5678'; + expect(getCommit('CUSTOM_COMMIT')).toBe('def5678'); + }); + + it('falls back to "unknown" when the env var is unset', () => { + expect(getCommit()).toBe(UNKNOWN_COMMIT); + expect(getCommit()).toBe('unknown'); + }); + + it('falls back to "unknown" when the env var is empty', () => { + process.env[DEFAULT_COMMIT_ENV] = ''; + expect(getCommit()).toBe(UNKNOWN_COMMIT); + }); + }); + + describe('getBuildInfo', () => { + it('assembles commit, version and env', () => { + process.env[DEFAULT_COMMIT_ENV] = 'abc1234'; + expect(getBuildInfo({ env: 'production', version: '1.2.3' })).toEqual({ + commit: 'abc1234', + env: 'production', + version: '1.2.3', + }); + }); + + it('defaults version to "unknown" and leaves env undefined', () => { + expect(getBuildInfo()).toEqual({ + commit: UNKNOWN_COMMIT, + env: undefined, + version: UNKNOWN_COMMIT, + }); + }); + }); +});