Bumps backend for the prisma-client TS-extension rewrite fix that
unblocks the production image bootloop. Versioning is kept in
lockstep with frontend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Production image bootlooped with:
Cannot find module './internal/class.ts'
at /app/dist/generated/prisma/client.js:40:29
The Prisma 7 `prisma-client` generator (provider="prisma-client" in
schema.prisma) emits TypeScript source whose internal imports use
explicit `.ts` extensions (import x from "./internal/class.ts").
With module=nodenext, tsc preserves those extensions in the emitted
JS unless rewriteRelativeImportExtensions is on, so the compiled
client did `require("./internal/class.ts")` at runtime → ENOENT.
Set rewriteRelativeImportExtensions: true in tsconfig.json (added in
TS 5.7, we're on 5.7.3). tsc now rewrites .ts → .js in the emitted
require calls, matching the .js files actually present.
Also add a .dockerignore. The local smoke test was passing because
the COPY . . step was overlaying a pre-existing generated/prisma/
(left over from a previous prisma-client-js install with .js output)
on top of the fresh .ts output from `npm ci`'s postinstall. CI's
clean checkout had no override, so the bug was visible there first.
Excluding generated/, node_modules/, and dist/ from the build
context forces every build (local and CI) to use the fresh
regenerated output — same code path everywhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HARBOR_HOST is configured as a Gitea Actions *variable*, not a
secret. Gitea (like GitHub) keeps the two stores separate — secrets.X
resolves to empty if X is a variable. Every prior CI failure on the
push job (docker/login-action exit 1, curl exit 6, docker login
falling back to registry-1.docker.io, the recent empty-check) was
the same root cause: secrets.HARBOR_HOST was always empty.
Switch the env binding to vars.HARBOR_HOST and drop the protocol-
strip defensive code that was added based on a wrong premise — the
value (harbor.tehriehldeal.com) is already a clean bare hostname.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous run made it past the pre-check (which cleaned the host
locally) but failed at docker login with "Get registry-1.docker.io"
— docker fell back to Docker Hub because HARBOR_HOST still had the
protocol prefix the pre-check stripped. Subsequent steps (login,
push, cosign) read the raw secret directly.
Move the cleanup into the Compute step and write the normalized
value to \$GITHUB_ENV. That overrides the job-level env: HARBOR_HOST
for every step that runs after Compute, so login/push/cosign all
see the same clean hostname.
Also fail fast with a clear message if HARBOR_HOST is empty, so a
missing secret never silently lands us at Docker Hub again.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Harbor pre-check died with bare exit code 6 (curl: couldn't
resolve host) and no diagnostic. Make it actually useful:
- Echo the URL being checked so a malformed HARBOR_HOST is obvious.
- Strip https://, http://, and trailing slash from HARBOR_HOST
defensively — covers the common case of the secret being pasted as
a full URL.
- Capture curl's exit status with || echo "000" and use a case
statement to handle each outcome: 200 fails (exists), 404 proceeds
(clean), 000 fails with a "check HARBOR_HOST" message, 401/403
fails with a "check creds / robot perms" message, other statuses
warn but proceed.
- Drop `set -e`; the script handles errors explicitly now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The push job's docker/login-action@v3 was failing with a fast 1s exit
on the Gitea runner — the same nested-checkout/wrapper flakiness that
already burned us on gitleaks-action, trivy-action, and semgrep-action.
Replace it with a plain shell `docker login --password-stdin`.
While here, restructure the push job to match the pattern from the
companion project so subsequent steps work the same way across repos:
- Read VERSION from package.json (jq), SHA_SHORT from git, and use
those as the published tags (:VERSION, :SHA_SHORT, :latest).
- Pre-check Harbor's API for an existing artifact at :VERSION and
fail early with a clear "bump package.json" message, instead of
silently overwriting a published version.
- Sign each pushed image with cosign (private key in env://COSIGN_KEY).
Cosign resolves the SHA tag to a digest, so one signature covers
all tags pointing at that digest.
- After a successful image push, create and push a `v${VERSION}` git
tag back to origin using the auto-injected GITEA token. A failed
tag push is downgraded to a warning so it can't undo a successful
image push.
- Drop `tags: ["v*"]` from the workflow trigger — CI now creates the
tag itself, so re-triggering on tag push would just loop.
- Drop the docker/metadata-action and the floating semver tags
(:1.2, :1); the new scheme is :VERSION, :SHA_SHORT, :latest only.
Replaces the previous docker/build-push-action push with an explicit
buildx build + docker tag + docker push chain so cosign has a named
local reference (movieloop-backend:${SHA_SHORT}) to sign against.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The lint job set cache-dependency-path: package-lock.json explicitly;
typecheck and test left it to setup-node's auto-discovery, which can
fail on the Gitea runner (1s exit from the setup step). Make the
path explicit on every actions/setup-node@v4 invocation so cache key
resolution is deterministic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trivy's image scan flagged picomatch CVE-2026-33671 (ReDoS via crafted
extglob patterns) in the npm CLI bundled inside node:22-alpine
(/usr/local/lib/node_modules/npm/node_modules/picomatch). Our app's
own picomatch is clean — only npm's vendored copy is vulnerable, and
upstream npm hasn't shipped a fixed bundle yet.
The production container only needs `node` and the prisma CLI binary
at runtime. Switch entrypoint.sh from `npx prisma migrate deploy` to
calling ./node_modules/.bin/prisma directly, then delete the bundled
npm/yarn/corepack trees in the production stage. This removes the
vulnerable file rather than waiting on the upstream base image.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Gitea runner timed out reaching the GHA artifact cache backend
during cache-to export, failing the build job after a fully successful
build. Drop type=gha cache-from/cache-to entirely; with no usable
cache backend, image-scan was also wasting ~5 min rebuilding from
scratch — fold the trivy image scan into the build job so it scans
the image that's already loaded in the local daemon.
Also chown at COPY time in the production Dockerfile stage. The
trailing `chown -R node:node /app` was taking 255s on CI because it
recursed the full node_modules tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trivy fs-scan flagged 12 HIGH-severity vulnerabilities in
package-lock.json (axios prototype pollution, lodash RCE, hono auth
bypass, path-to-regexp DoS, socket.io-parser DoS, and others). All
fixes are within existing semver ranges in package.json — only the
lockfile changed, no direct dependency bumps.
Verified locally:
- npm run build (nest build) — green
- eslint — 0 errors
- /tmp/trivy fs --severity HIGH,CRITICAL — 0 vulnerabilities
- scripts/local-build-test.sh — full stack boots and serves /health
Remaining moderate-severity findings (@hono/node-server via @prisma/dev
chain, brace-expansion) are gated behind a Prisma major-version bump
that's out of scope here. CI scans HIGH,CRITICAL only, so these don't
block the pipeline.
The previous pin to v0.58.1 returned a 404 — that tag doesn't exist in
the trivy releases. Latest is v0.70.0; pinning to that. Verified the
asset naming convention (trivy_X.Y.Z_Linux-64bit.tar.gz) is unchanged.
The NestJS starter README includes a placeholder CircleCI badge token
(?token=abc123def456) on a reference-style image link. README files
commonly contain placeholder secrets and badge URLs, so allowlist
README.md by path. Same pattern as the existing .env.example allowlist.
Verified locally: gitleaks scan now reports no leaks.
Same pattern as the gitleaks fix: aquasecurity/trivy-action@master does
a nested actions/checkout to fetch its install script, which fails on
the Gitea runner. Switch fs-scan and image-scan to download the trivy
binary release directly and invoke it. Pinned to v0.58.1.
- package.json: add `postinstall: prisma generate` so npm ci (CI and
Docker) auto-generates the Prisma client. Without this, lint and
typecheck failed in CI with "Cannot find module
'../../generated/prisma/client.js'" and a flood of "Property X does
not exist on type 'PrismaService'" errors.
- Dockerfile: copy `prisma/` and `prisma.config.ts` before `npm ci` so
the postinstall hook finds the schema during the Docker build.
Removed the now-redundant explicit `RUN npx prisma generate`.
- ci.yml test job: switched DATABASE_URL host from `localhost` to
`postgres` and REDIS_HOST from `localhost` to `redis`. In Gitea
Actions, service containers are reachable by service name on the
job's bridge network, not via localhost. Removed unused `ports:`
mappings.
- ci.yml secrets-scan: replaced gitleaks/gitleaks-action@v2 with a
direct binary install + invocation. The action's license-key check
flakes on Gitea; running the binary is more reliable.
Verified locally: postinstall regenerates the client cleanly, and
scripts/local-build-test.sh still passes end-to-end.
Features:
- "Keep me signed in" — login/register accept a rememberMe flag and the
backend signs JWTs with JWT_LONG_EXPIRATION (30d) when set, otherwise
JWT_EXPIRATION (1d). DTOs, controller, and AuthService updated.
- Movie release dates — DailyChallenge, GameSession, and VersusMatch get
nullable movieAReleaseDate / movieBReleaseDate columns (forward-only
migration). Daily-challenges, games, versus-match, and versus-async
services now persist and return release_date from TMDB. Versus waiting
lobby payloads strip the new fields alongside the titles to avoid
leaking the puzzle to non-joined players.
Lint cleanup (132 → 0 errors):
- Shared utilities: src/common/utils/error.util.ts (getErrorMessage) and
src/auth/types/jwt-payload.ts (JwtPayload).
- Replaced catch(error: any){error.message} with getErrorMessage(error)
across services and gateways.
- jwtService.verify<JwtPayload>(token) in versus / game-night / chat /
notifications gateways.
- Typed Prisma JSON columns where they're read (game-night, leaderboards,
admin score blobs).
- Removed redundant async on sync handlers, wrapped setTimeout/setInterval
Promise callbacks with void IIFEs to satisfy no-misused-promises.
- eslint.config.mjs: allow `_`-prefixed unused vars (industry standard).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The leaderboard update step could set gameMode to 'freeplay' for entries
lacking a dailyChallengeId, causing them to disappear from the leaderboard
which filters by gameMode 'daily'. Since game completion now properly tags
leaderboard entries at submission time, this step is no longer needed.
Reviewed-on: #1
Co-authored-by: Kevin Riehl <kevinriehl@gmail.com>
Co-committed-by: Kevin Riehl <kevinriehl@gmail.com>
* Add undoLink method to GamesService
* Add POST :id/undo endpoint to GamesController
* Format achievements service line wrapping
* Format admin service line wrapping
* Format versus match service line wrapping
* Return full achievement details from checkAndAward
* Include new achievements in game completion response
* Include new achievements in versus match-finished result
* Fix admin service for updated checkAndAward return type
* Extract getUtcDate() into shared date utility
* Extract pickRandomPair() into shared movie-pair utility
* Extract SHA256 hashing into shared hash utility
* Extract streak calculation into shared streak utility
* Use shared date and movie-pair utils in daily-challenges
* Add missing import in daily-challenges controller
* Use shared date and streak utils in leaderboards
* Use Difficulty enum in leaderboard query DTO
* Update leaderboards controller imports
* Use shared hash and movie-pair utils in versus service
* Add VersusMatchService for sync match operations
* Add VersusAsyncService for async match operations
* Register split versus services in module
* Use CreateFriendChallengeDto in versus controller
* Use Difficulty enum in create-match DTO
* Use ScoreBreakdownDto in submit-async-score DTO
* Add CreateFriendChallengeDto
* Use VersusAsyncService in async-expiration
* Use shared hash utility in game-night service
* Use shared streak utility in admin service
* Use validated DTOs in admin controller
* Add admin DTOs for challenge and promote endpoints
* Add shared Difficulty enum
* Add ScoreBreakdownDto with class-validator decorators
* Fix currentPassword MinLength from 1 to 8
* Add ValidateLinkDto for chain validation endpoint
* Add POST validate-link endpoint to games controller
* Add server-side chain link validation logic
* Import MoviesModule in games module for TMDB validation
* Add UserRelationshipModule to resolve chat-friends circular dep
* Replace forwardRef FriendsModule with UserRelationshipModule
* Use UserRelationshipService in chat service
* Remove unused FriendsService import from chat gateway
* Import UserRelationshipModule in friends module
* Delegate areFriends to UserRelationshipService, add error logging
* Register UserRelationshipModule in app module
* Exported ChainItem Interface
* Fixed first actor in new game validation