L0 T0 #6 PR-OPAQUE-5 — initial Centrifugo proxy + Validate gRPC client #1

Merged
hibryda merged 6 commits from feat/l0-t0-opaque-5-validate-proxy into main 2026-05-25 01:35:52 +02:00
Owner

Header

  • Project: im2be-realtime-service
  • PR: PR-OPAQUE-5 (L0 Tranche-0 #6 of the Centrifugo opaque-ticket workstream)
  • Round/Run: R1 — initial scaffold pass
  • Mode: authoring (human, autonomous-execution per rule 65)

TL;DR

CONDITIONAL_APPROVE — scaffolds the realtime-service Node.js + TypeScript + Fastify worker with ONE fully-wired route (POST /centrifugo/connect) that bridges Centrifugo v6 connect_proxy HTTP callbacks to identity-service.ConnectProxyService.Validate gRPC. Subscribe/publish/rpc are 501 stubs per scope. 36/36 vitest tests passing, zero TypeScript warnings, fastify bumped to 5.8.5 to clear the high-sev content-type CVE.

Summary

Fresh-tree fork of patterns from hib-pr-reviewer (Node 22 + ESM + tsx + vitest layout per rule 53), instantiated for the realtime-service slot at realtime-service/. Single commit 7190de7 adds: src/{config,types,otel,metrics,server,main}.ts, src/grpc/identityValidateClient.ts, src/handlers/connect.ts, vendored proto at proto/realtime/v1/connect.proto + proto/shared/v1/ids.proto, 5 vitest test files (config / metrics / handler / server / projectResponse) covering happy / degraded / single-use-reject / UNAVAILABLE / DEADLINE_EXCEEDED / identity-Error / null-body / empty-token / UNKNOWN-gRPC / 501-stubs / 413-oversized / /health / /metrics, Dockerfile (multi-stage, non-root, tini PID-1, healthcheck), README + .env.example + .dockerignore.

Commit SHA reviewed: 7190de7 (HEAD of feat/l0-t0-opaque-5-validate-proxy).

Findings

Severity File:line Title Body
MINOR package.json:24-25 7 moderate dev-dep CVEs deferred vitest 2.x → vite/esbuild dev-server CORS advisory. Fix requires vitest 3.x major bump; deferred to a follow-up PR since the dep is dev-only and not shipped in the runtime image. fastify high-sev was patched (5.2.1 → 5.8.5).
MINOR src/grpc/identityValidateClient.ts:148-152 gRPC error.message replaced by codeName The wire err.message from a remote gRPC server can echo caller-supplied strings (matches identity-service ConnectProxyServiceImpl R0-MAJOR-1 finding); we surface only the symbolic codeName to avoid log injection. Documented inline.
INFO src/config.ts:5-9 Port 9730 reserved but not yet added to global table The global ~/.config/claude/CLAUDE.md ports table needs a row for im2be-realtime-service | HTTP proxy | 9730. Add when this PR lands.

No new BLOCKING findings.

Verdict

  • CONDITIONAL_APPROVE pending: (a) the autonomous PR reviewer's R1 pass against this PR, (b) global ports-table update on landing, (c) follow-up PR for vitest 3.x dev-dep CVE cleanup.
  • Ready for the autonomous reviewer's R-cycle.

Blast Radius

0/10 — net-new submodule, only inbound edges land here: future Centrifugo deployment overlays in flux-applications/apps/realtime-service/ will reference the image; identity-service already exposes the gRPC server consumed by this PR (PR-OPAQUE-3 shipped). No outbound edges into other submodules.

Risk Indicators

Indicator Count
Sensitive-function touches 0
Migration touches 0
Test delta +36
Dependency changes +12 (Fastify 5.8.5, @grpc/grpc-js + @grpc/proto-loader, @opentelemetry/* x5, tsx, vitest/coverage/types/typescript dev)

Test plan

  • npm install — clean install from package-lock.
  • npx tsc --noEmit — zero errors, zero warnings.
  • npm test — 36/36 passing across 5 test files in 505 ms.
  • npm run build — produces dist/ with 7 source files.
  • docker build -t aim2be-realtime-service:test . — local smoke (deferred; CI will exercise on merge).
  • End-to-end against a running identity-service ConnectProxyService — deferred to L-1 integration phase.
  • PR-OPAQUE-3 (identity-service ConnectProxyService.Validate gRPC server) — shipped.
  • PR-OPAQUE-4 (mint-time fan-out — family + user context aggregation) — shipped.
  • PR-OPAQUE-5 (this PR — proxy + Validate client).
  • PR-OPAQUE-6 (planned — subscribe_proxy + family-service IsMember client).
  • PR-OPAQUE-7 (planned — static fallback ticket pool, ADR-0002 §3.4).

im2be-realtime-service • authoring • R1 • 36 new tests / 0 new findings / 0 carried • 2026-05-25

## Header - **Project:** im2be-realtime-service - **PR:** PR-OPAQUE-5 (L0 Tranche-0 #6 of the Centrifugo opaque-ticket workstream) - **Round/Run:** R1 — initial scaffold pass - **Mode:** authoring (human, autonomous-execution per rule 65) ## TL;DR `CONDITIONAL_APPROVE` — scaffolds the realtime-service Node.js + TypeScript + Fastify worker with ONE fully-wired route (`POST /centrifugo/connect`) that bridges Centrifugo v6 `connect_proxy` HTTP callbacks to `identity-service.ConnectProxyService.Validate` gRPC. Subscribe/publish/rpc are 501 stubs per scope. 36/36 vitest tests passing, zero TypeScript warnings, fastify bumped to 5.8.5 to clear the high-sev content-type CVE. ## Summary Fresh-tree fork of patterns from `hib-pr-reviewer` (Node 22 + ESM + tsx + vitest layout per rule 53), instantiated for the realtime-service slot at `realtime-service/`. Single commit `7190de7` adds: `src/{config,types,otel,metrics,server,main}.ts`, `src/grpc/identityValidateClient.ts`, `src/handlers/connect.ts`, vendored proto at `proto/realtime/v1/connect.proto` + `proto/shared/v1/ids.proto`, 5 vitest test files (config / metrics / handler / server / projectResponse) covering happy / degraded / single-use-reject / UNAVAILABLE / DEADLINE_EXCEEDED / identity-Error / null-body / empty-token / UNKNOWN-gRPC / 501-stubs / 413-oversized / /health / /metrics, Dockerfile (multi-stage, non-root, tini PID-1, healthcheck), README + .env.example + .dockerignore. Commit SHA reviewed: `7190de7` (HEAD of `feat/l0-t0-opaque-5-validate-proxy`). ## Findings | Severity | File:line | Title | Body | |---|---|---|---| | MINOR | `package.json:24-25` | 7 moderate dev-dep CVEs deferred | vitest 2.x → vite/esbuild dev-server CORS advisory. Fix requires vitest 3.x major bump; deferred to a follow-up PR since the dep is dev-only and not shipped in the runtime image. fastify high-sev was patched (5.2.1 → 5.8.5). | | MINOR | `src/grpc/identityValidateClient.ts:148-152` | gRPC error.message replaced by codeName | The wire `err.message` from a remote gRPC server can echo caller-supplied strings (matches identity-service ConnectProxyServiceImpl R0-MAJOR-1 finding); we surface only the symbolic `codeName` to avoid log injection. Documented inline. | | INFO | `src/config.ts:5-9` | Port 9730 reserved but not yet added to global table | The global `~/.config/claude/CLAUDE.md` ports table needs a row for `im2be-realtime-service \| HTTP proxy \| 9730`. Add when this PR lands. | **No new BLOCKING findings.** ## Verdict - `CONDITIONAL_APPROVE` pending: (a) the autonomous PR reviewer's R1 pass against this PR, (b) global ports-table update on landing, (c) follow-up PR for vitest 3.x dev-dep CVE cleanup. - Ready for the autonomous reviewer's R-cycle. ## Blast Radius **0/10** — net-new submodule, only inbound edges land here: future Centrifugo deployment overlays in `flux-applications/apps/realtime-service/` will reference the image; `identity-service` already exposes the gRPC server consumed by this PR (PR-OPAQUE-3 shipped). No outbound edges into other submodules. ## Risk Indicators | Indicator | Count | |---|---:| | Sensitive-function touches | 0 | | Migration touches | 0 | | Test delta | +36 | | Dependency changes | +12 (Fastify 5.8.5, @grpc/grpc-js + @grpc/proto-loader, @opentelemetry/* x5, tsx, vitest/coverage/types/typescript dev) | ## Test plan - [x] `npm install` — clean install from package-lock. - [x] `npx tsc --noEmit` — zero errors, zero warnings. - [x] `npm test` — 36/36 passing across 5 test files in 505 ms. - [x] `npm run build` — produces `dist/` with 7 source files. - [ ] `docker build -t aim2be-realtime-service:test .` — local smoke (deferred; CI will exercise on merge). - [ ] End-to-end against a running identity-service ConnectProxyService — deferred to L-1 integration phase. ## Related PRs - **PR-OPAQUE-3** (identity-service ConnectProxyService.Validate gRPC server) — shipped. - **PR-OPAQUE-4** (mint-time fan-out — family + user context aggregation) — shipped. - **PR-OPAQUE-5** (this PR — proxy + Validate client). - **PR-OPAQUE-6** (planned — subscribe_proxy + family-service IsMember client). - **PR-OPAQUE-7** (planned — static fallback ticket pool, ADR-0002 §3.4). ## Footer im2be-realtime-service • authoring • R1 • 36 new tests / 0 new findings / 0 carried • 2026-05-25
R1 verdict — scaffold of im2be-realtime-service:

Scope (this PR ships ONLY the connect_proxy callback):
- POST /centrifugo/connect — opaque-ticket Validate via gRPC to
  identity-service.ConnectProxyService.Validate.
- POST /centrifugo/{subscribe,publish,rpc} — stubs returning 501.
- GET /health — ok | degraded (sliding-window of last 10 transient outcomes).
- GET /metrics — Prometheus text exposition of
  realtime_centrifugo_connect_total{outcome}.

Design decisions:
- @grpc/proto-loader (dynamic) over codegen — one service method, no
  ongoing buf generate step. Proto vendored at proto/. Documented in
  src/grpc/identityValidateClient.ts header.
- @opentelemetry/sdk-trace-node + OTLP HTTP exporter; tracer name
  aim2be-realtime-service; ticket.validate span on hot path; span
  attributes ticket.length / centrifugo.client_id / validate.outcome /
  validate.degraded / grpc.status_code. OTel HTTP semconv §1.7 honoured —
  setStatus(ERROR) ONLY for 5xx-equivalents (UNAVAILABLE / DEADLINE_
  EXCEEDED), NOT for 4xx-equivalents (NOT_FOUND / INVALID_ARGUMENT).
- Single gRPC client instance per process; @grpc/grpc-js manages
  subchannel pool internally; close() wired into SIGTERM shutdown
  alongside fastify.close() + tracer flush.
- Degraded flag (ADR-0002 §3.4) propagated as info.degraded=true ONLY
  when set so steady-state consumers don't have to special-case absent
  vs false.

Status-code translation (matches planning §2.2 Centrifugo v6 transforms):
- success → 200 + result.
- success degraded → 200 + result with info.degraded=true.
- identity Disconnect / gRPC NOT_FOUND/INVALID_ARGUMENT → 401 +
  disconnect{4401, "invalid ticket"} (Centrifugo maps to terminal close).
- identity Error / gRPC UNAVAILABLE/DEADLINE_EXCEEDED → 503 +
  disconnect{4503, "ticket validation temporarily unavailable"}.
- other gRPC errors → 500 + error{100, "internal error"}.

Verification:
- npm install: clean, 8 vulns at install dropped to 7 moderate (vitest
  3.x-only fixes — dev-only, not shipped); high-sev fastify CVE patched
  to 5.8.5.
- npx tsc --noEmit: 0 errors, 0 warnings.
- npm test: 36/36 passing across 5 files (config / metrics / handler /
  server / projectResponse). Covers happy path + degraded propagation +
  single-use rejection + UNAVAILABLE + DEADLINE_EXCEEDED + identity
  Error branch + null body + empty token + UNKNOWN gRPC status + 501
  stubs + 413 oversized body + /health + /metrics.
- npm run build: produces dist/ with all 7 source files compiled.

Env-var contract (.env.example + README.md):
- LISTEN_PORT=9730 (aim2be 9000-range allocation — next free slot).
- IDENTITY_VALIDATE_GRPC_ADDRESS / NEGOTIATION_TYPE / DEADLINE_MS.
- OTEL_EXPORTER_OTLP_ENDPOINT / OTEL_SERVICE_NAME / OTEL_TRACES_SAMPLER_ARG.

Out of scope (subsequent PRs):
- subscribe_proxy + publish_proxy + rpc_proxy handler bodies.
- Static fallback ticket pool (ADR-0002 §3.4 — PR-OPAQUE-7).
- SPIFFE workload-API client (planning §1.2).
- Kafka consumer + centrifuge-go outbound client.

Refs: ADR-0002, .planning/22-stage-b-centrifugo-topology.md §3+§5,
protobuf/realtime/v1/connect.proto, identity-service/.../
ConnectProxyServiceImpl.java.

Superseded by round 2.

Show previous round

hib-pr-reviewer review — PR #1 (affinity-intelligence-rework/im2be-realtime-service)

Round 1 — head 7190de7d4840, base main, trigger opened

TL;DR: NEEDS_WORK — kept 4 findings (1 agreed major, 3 verified minors); no prior run history found in Memora.

Summary

Arbitration — Round 1

Memora context: No prior runs; no submodule pattern memories. First arbitration for this PR.

Verification actions:

  • src/types.ts lines 34-35 — confirmed: JSDoc comment /** Client remote IP (X-Forwarded-For-aware on the Centrifugo side). */ directly annotates protocol_version?: number (A cited lines 35-36; actuals are 34-35 — corrected).
  • src/server.ts line 57 — confirmed: no preHandler or any caller-auth check on /centrifugo/connect; route immediately delegates to handleConnect. Both A and B independently flagged this; A rated minor, B rated major. Keeping major — the Centrifugo proxy_secret / HMAC-SHA256 signature mechanism is a documented, first-class security control and its absence is a concrete DoS vector against individual sessions, not merely a defence-in-depth gap.
  • package.json line 28 (B cited 22; B was reading the package-lock.json excerpt) — confirmed: "tsx": "4.19.2" is inside dependencies, not devDependencies; the Dockerfile builder stage runs npm prune --omit=dev which retains tsx in the runtime image.
  • src/handlers/connect.ts line 111 — confirmed: only ticket.length === 0 guard; no upper-bound; proto comment at connect.proto:31 documents ≈40-char expected length.
  • src/handlers/connect.ts ticket forwarding path — B's major finding cited B-unique no-auth issue (same as A/B agreed finding 2); all remaining B findings are independent.

Outcome: 1 agreed finding (kept, severity elevated to major), 3 unique findings (all verified and kept). Total kept = 4.

Blast Radius

This is a wholly-new service (24 files, all additions). The HTTP surface is cluster-internal (port 9730, Centrifugo proxy callbacks only), but it sits on the critical path for every WebSocket connection attempt. The blast radius is bounded to WebSocket connect flows today; subscribe/publish/rpc stubs are not yet wired.

BLAST_SCORE: 4/10

Risk Indicators

Indicator Value
Sensitive functions handleConnect, identityValidateClient.validate, projectResponse
Migration touched
Test delta +670 / -0 lines in test files
Dependency changes package-lock.json, package.json

CI status (head 7190de7d4840)

No CI checks reported for this commit.

Findings (4)

[MAJOR] No Centrifugo proxy-secret/signature header validation on /centrifugo/connect

src/server.ts:57

The route at line 57 accepts any POST from any mesh-authenticated caller without verifying the X-Centrifugo-Proxy-Signature (or equivalent X-Centrifugo-Proxy-Token) header that Centrifugo emits when proxy_secret is configured.

Envoy mTLS confirms the caller is some service in the mesh — it does not restrict to Centrifugo specifically. Any compromised or misconfigured pod with mesh access can POST to this endpoint and race-consume a legitimate user's single-use GETDEL ticket before the real WebSocket connect arrives, causing that user's connection attempt to return NOT_FOUND (targeted DoS per user session).

Recommend one of:

  1. Preferred (in-code): Add CENTRIFUGO_PROXY_SECRET to src/config.ts + an .env.example entry, then wire a Fastify preHandler that HMAC-SHA256-validates the signature header, rejecting with 403 on mismatch. Must land in the same PR as the Flux overlay that supplies the secret.
  2. Fallback (infra): Enforce a Kubernetes NetworkPolicy restricting ingress on port 9730 to Centrifugo pods only, and document this constraint explicitly in README §Dev-quick-start or the deployment overlay so it is enforceable in CI.

Agreed by both reviewers A and B.

[MINOR] Misattributed JSDoc comment on protocol_version field

src/types.ts:35

Line 34 reads /** Client remote IP (X-Forwarded-For-aware on the Centrifugo side). */ but the field it directly annotates (line 35) is protocol_version?: number, not an IP address. The remote_ip field at line 39 already carries its own correct JSDoc.

Fix: replace the comment on line 34 with an accurate description, e.g.:

/** Centrifugo protocol version number (numeric). */
protocol_version?: number;

[MINOR] tsx belongs in devDependencies, not dependencies

package.json:28

"tsx": "4.19.2" (line 28) is in dependencies, so the Dockerfile builder stage's RUN npm prune --omit=dev (Dockerfile line 29) retains tsx — along with esbuild and its platform-native binaries (~30 MB) — in the production runtime image. The production entrypoint node dist/main.js never invokes tsx; only the dev script (tsx watch src/main.ts) uses it.

Fix: move tsx to devDependencies:

"devDependencies": {
  "tsx": "4.19.2",
  ...
}

The dev script continues to work unchanged; the runtime image is meaningfully slimmer.

[MINOR] No upper-bound length guard on ticket string before gRPC forwarding

src/handlers/connect.ts:111

The guard at line 111 rejects only empty strings:

if (typeof ticket !== "string" || ticket.length === 0) {

The proto comment at connect.proto:31 documents the expected format as "192-bit entropy + crc32, base64url" ≈ 40 characters. A caller that submits a ~15 KB token (within the 16 KB body limit) passes validation and is forwarded verbatim in the gRPC Validate request, incurring a needless gRPC round-trip that identity-service will reject on format grounds.

Add a max-length guard before the gRPC call (256 is generous; 64 is tight but safe for the documented format):

if (typeof ticket !== "string" || ticket.length === 0 || ticket.length > 256) {
  span.setAttribute("validate.reject_reason", "invalid_ticket_format");
  outcome = "invalid";
  return invalidTicketResponse(span, deps.logger, "invalid_ticket_format", body.client);
}

Verdict

NEEDS_WORK


hib-pr-reviewer • round 1 • 4 findings (1M/3m) • 2026-05-24T22:58:47.959Z → 2026-05-24T23:01:22.136Z • posted-as: pr-reviewer-bot

<!-- im2be-pr-reviewer collapsed --> > _Superseded by round 2._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #1 (affinity-intelligence-rework/im2be-realtime-service) **Round 1** — head `7190de7d4840`, base `main`, trigger `opened` **TL;DR:** NEEDS_WORK — kept 4 findings (1 agreed major, 3 verified minors); no prior run history found in Memora. ### Summary ## Arbitration — Round 1 **Memora context:** No prior runs; no submodule pattern memories. First arbitration for this PR. **Verification actions:** - `src/types.ts` lines 34-35 — confirmed: JSDoc comment `/** Client remote IP (X-Forwarded-For-aware on the Centrifugo side). */` directly annotates `protocol_version?: number` (A cited lines 35-36; actuals are 34-35 — corrected). - `src/server.ts` line 57 — confirmed: no `preHandler` or any caller-auth check on `/centrifugo/connect`; route immediately delegates to `handleConnect`. Both A and B independently flagged this; A rated minor, B rated major. Keeping **major** — the Centrifugo `proxy_secret` / HMAC-SHA256 signature mechanism is a documented, first-class security control and its absence is a concrete DoS vector against individual sessions, not merely a defence-in-depth gap. - `package.json` line 28 (B cited 22; B was reading the `package-lock.json` excerpt) — confirmed: `"tsx": "4.19.2"` is inside `dependencies`, not `devDependencies`; the Dockerfile builder stage runs `npm prune --omit=dev` which retains tsx in the runtime image. - `src/handlers/connect.ts` line 111 — confirmed: only `ticket.length === 0` guard; no upper-bound; proto comment at `connect.proto:31` documents ≈40-char expected length. - `src/handlers/connect.ts` ticket forwarding path — B's major finding cited B-unique no-auth issue (same as A/B agreed finding 2); all remaining B findings are independent. **Outcome:** 1 agreed finding (kept, severity elevated to major), 3 unique findings (all verified and kept). Total kept = 4. ### Blast Radius This is a wholly-new service (24 files, all additions). The HTTP surface is cluster-internal (port 9730, Centrifugo proxy callbacks only), but it sits on the critical path for every WebSocket connection attempt. The blast radius is bounded to WebSocket connect flows today; subscribe/publish/rpc stubs are not yet wired. **BLAST_SCORE: 4/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `handleConnect`, `identityValidateClient.validate`, `projectResponse` | | Migration touched | — | | Test delta | +670 / -0 lines in test files | | Dependency changes | `package-lock.json`, `package.json` | ### CI status (head `7190de7d4840`) _No CI checks reported for this commit._ ### Findings (4) #### **[MAJOR]** No Centrifugo proxy-secret/signature header validation on `/centrifugo/connect` _src/server.ts:57_ The route at line 57 accepts any POST from any mesh-authenticated caller without verifying the `X-Centrifugo-Proxy-Signature` (or equivalent `X-Centrifugo-Proxy-Token`) header that Centrifugo emits when `proxy_secret` is configured. Envoy mTLS confirms the caller is *some* service in the mesh — it does **not** restrict to Centrifugo specifically. Any compromised or misconfigured pod with mesh access can POST to this endpoint and race-consume a legitimate user's single-use GETDEL ticket before the real WebSocket connect arrives, causing that user's connection attempt to return NOT_FOUND (targeted DoS per user session). Recommend one of: 1. **Preferred (in-code):** Add `CENTRIFUGO_PROXY_SECRET` to `src/config.ts` + an `.env.example` entry, then wire a Fastify `preHandler` that HMAC-SHA256-validates the signature header, rejecting with 403 on mismatch. Must land in the same PR as the Flux overlay that supplies the secret. 2. **Fallback (infra):** Enforce a Kubernetes `NetworkPolicy` restricting ingress on port 9730 to Centrifugo pods only, and document this constraint explicitly in README §Dev-quick-start or the deployment overlay so it is enforceable in CI. Agreed by both reviewers A and B. #### **[MINOR]** Misattributed JSDoc comment on `protocol_version` field _src/types.ts:35_ Line 34 reads `/** Client remote IP (X-Forwarded-For-aware on the Centrifugo side). */` but the field it directly annotates (line 35) is `protocol_version?: number`, not an IP address. The `remote_ip` field at line 39 already carries its own correct JSDoc. Fix: replace the comment on line 34 with an accurate description, e.g.: ```typescript /** Centrifugo protocol version number (numeric). */ protocol_version?: number; ``` #### **[MINOR]** `tsx` belongs in `devDependencies`, not `dependencies` _package.json:28_ `"tsx": "4.19.2"` (line 28) is in `dependencies`, so the Dockerfile builder stage's `RUN npm prune --omit=dev` (Dockerfile line 29) retains tsx — along with esbuild and its platform-native binaries (~30 MB) — in the production runtime image. The production entrypoint `node dist/main.js` never invokes tsx; only the `dev` script (`tsx watch src/main.ts`) uses it. Fix: move tsx to `devDependencies`: ```json "devDependencies": { "tsx": "4.19.2", ... } ``` The `dev` script continues to work unchanged; the runtime image is meaningfully slimmer. #### **[MINOR]** No upper-bound length guard on ticket string before gRPC forwarding _src/handlers/connect.ts:111_ The guard at line 111 rejects only empty strings: ```typescript if (typeof ticket !== "string" || ticket.length === 0) { ``` The proto comment at `connect.proto:31` documents the expected format as "192-bit entropy + crc32, base64url" ≈ 40 characters. A caller that submits a ~15 KB token (within the 16 KB body limit) passes validation and is forwarded verbatim in the gRPC `Validate` request, incurring a needless gRPC round-trip that identity-service will reject on format grounds. Add a max-length guard before the gRPC call (256 is generous; 64 is tight but safe for the documented format): ```typescript if (typeof ticket !== "string" || ticket.length === 0 || ticket.length > 256) { span.setAttribute("validate.reject_reason", "invalid_ticket_format"); outcome = "invalid"; return invalidTicketResponse(span, deps.logger, "invalid_ticket_format", body.client); } ``` ### Verdict **NEEDS_WORK** --- <sub>hib-pr-reviewer • round 1 • 4 findings (1M/3m) • 2026-05-24T22:58:47.959Z → 2026-05-24T23:01:22.136Z • posted-as: pr-reviewer-bot</sub> </details>
R1 verdict findings (kept=4):

(1) MAJOR src/server.ts:57 — No Centrifugo proxy-secret/signature
    header validation on /centrifugo/connect. Envoy mTLS only confirms
    the caller is SOME mesh service, not specifically Centrifugo; any
    compromised mesh pod could race-consume single-use tickets (targeted
    DoS per user session).

(2) MINOR src/types.ts:34 — Misattributed JSDoc comment on
    protocol_version field (claimed "Client remote IP" — remote_ip has
    its own correct JSDoc at line 39).

(3) MINOR package.json:28 — `tsx` in `dependencies` instead of
    `devDependencies`. Production runtime image retained tsx + esbuild
    native binaries (~30 MB) needlessly.

(4) MINOR src/handlers/connect.ts:111 — No upper-bound length guard
    on ticket string. A 15 KB token within bodyLimit would pass
    validation + waste a gRPC round-trip to identity-service for a
    format-rejection.

Fixes:

(1) Added CENTRIFUGO_PROXY_SECRET env var to src/config.ts (default
    "" = disabled with loud WARN log on startup). ServerDeps grew an
    optional centrifugoProxySecret. When non-empty, a custom JSON
    content-parser captures the raw body alongside the parsed object,
    and a `preHandler` hook scoped to /centrifugo/* paths
    timingSafeEquals the X-Centrifugo-Proxy-Signature header against
    HMAC-SHA256(secret, rawBody). Mismatches return 403 with the
    generic "forbidden" error (CWE-209 — never disclose whether the
    secret is set). Wired into main.ts. /health remains unguarded
    (orchestrator probe should not require Centrifugo's secret).

(2) Replaced comment with "Centrifugo protocol version number (numeric)".

(3) Moved `tsx` from dependencies → devDependencies; runtime image
    npm-pruned to dev-free.

(4) Added MAX_TICKET_LENGTH = 256 guard before gRPC forwarding; over-
    sized tokens short-circuit to 401 with reject_reason
    "invalid_ticket_format". 256 chars is generous (ADR-0002 §3
    documents ~40-char format) while preventing multi-KB token DoS.

Tests added (5 new in tests/server.test.ts):
- Upper-bound ticket-length rejection (token > 256 chars → 401, gRPC
  never invoked).
- Signature-enabled accept path (valid HMAC → 200).
- Missing-signature reject (no header → 403, gRPC never invoked).
- Mismatched-signature reject (wrong HMAC → 403, gRPC never invoked).
- /health remains unguarded (signature-enforcement scoped to /centrifugo/*).

Verification:
- npm install: clean, lockfile updated (tsx now devDeps).
- npx tsc --noEmit: 0 errors, 0 warnings.
- npm test: 41/41 passing (was 36 → +5 for the new findings).
- Behaviour unchanged for the local-dev path (CENTRIFUGO_PROXY_SECRET
  unset → loud WARN + bypass).

Superseded by round 3.

Show previous round

hib-pr-reviewer review — PR #1 (affinity-intelligence-rework/im2be-realtime-service)

Round 2 — head 17ebad0e599f, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — kept 1 unique-to-A minor finding (verified); B agreed in prose but JSON was truncated; all 4 R1 blocking/major findings confirmed fixed.

Summary

Reconciliation — Round 2

Context: No prior Memora entries found for this PR (despite round=2). Memora persistence attempted but failed due to server-side tag allowlist constraint; the arbitration verdict is not affected.

Prior-round findings (R1): All 4 were confirmed fixed by both reviewers — server.ts HMAC-SHA256 guard, types.ts JSDoc, package.json tsx→devDeps, connect.ts ticket length guard. These are not re-raised.

A=1, B=0 (truncated parse): Reviewer B's JSON was truncated to [] due to a mid-stream parse failure, but B's prose summary describes the same issue as A's single finding. Since B's finding is prose-only (no grounded JSON entry), I treated A's finding as unique-to-one and applied Rule 2 (verify before keeping).

Verification performed:

  • Read /var/lib/pr-reviewer/.../640/.env.example → File is 20 lines, ends at OTEL_TRACES_SAMPLER_ARG=1.0. CENTRIFUGO_PROXY_SECRET is absent. confirmed.
  • Grep CENTRIFUGO_PROXY_SECRET src/config.ts:125 reads the variable; server.ts:79 emits a warn if unset. confirmed — the variable is real and used.
  • Grep CENTRIFUGO README.mdZero matches. The README env-var table introduced in this PR omits the secret entirely. confirmed.

Outcome: Finding kept. 1 minor finding retained.

Blast Radius

The gap is documentation-only: .env.example and the README env-var table omit CENTRIFUGO_PROXY_SECRET. The implementation in config.ts, server.ts, and main.ts is correct. No runtime code is changed by the fix; blast radius is confined to two documentation files.

BLAST_SCORE: 2/10

Risk Indicators

Indicator Value
Sensitive functions verifyProxySecret (server.ts), readString(env, "CENTRIFUGO_PROXY_SECRET") (config.ts:125)
Migration touched
Test delta +772 / -0 lines in test files
Dependency changes package-lock.json, package.json

CI status (head 17ebad0e599f)

No CI checks reported for this commit.

Findings (1)

[MINOR] CENTRIFUGO_PROXY_SECRET absent from .env.example and README env-var table

.env.example:20

.env.example ends at line 20 (OTEL_TRACES_SAMPLER_ARG=1.0) with no entry for CENTRIFUGO_PROXY_SECRET. config.ts:125 reads this variable and server.ts:79 emits a warn log if it is unset (signature validation silently disabled). The README env-var table introduced in this PR also has zero mentions of the variable (confirmed by grep). An operator following the .env.example runbook will deploy without the secret and only discover the gap from a runtime warning — exactly the operator ergonomics gap the R1 blocking finding was meant to close. The implementation is correct; the documentation is incomplete.

Fix — add to .env.example after line 20:

# --- Centrifugo proxy signature ------------------------------------------
# Required in production — must match Centrifugo's `proxy_secret` config.
# Leave unset only for local dev (Envoy mTLS alone cannot pin the caller to
# Centrifugo; any compromised mesh pod could consume single-use tickets).
# CENTRIFUGO_PROXY_SECRET=

Fix — add to README env-var table:

| `CENTRIFUGO_PROXY_SECRET` | _(empty — disabled)_ | HMAC-SHA256 key shared with Centrifugo `proxy_secret`. **Must be set in production.** |

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 2 • 1 finding (1m) • 2026-05-24T23:09:20.651Z → 2026-05-24T23:11:00.928Z • posted-as: pr-reviewer-bot

<!-- im2be-pr-reviewer collapsed --> > _Superseded by round 3._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #1 (affinity-intelligence-rework/im2be-realtime-service) **Round 2** — head `17ebad0e599f`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — kept 1 unique-to-A minor finding (verified); B agreed in prose but JSON was truncated; all 4 R1 blocking/major findings confirmed fixed. ### Summary ## Reconciliation — Round 2 **Context:** No prior Memora entries found for this PR (despite round=2). Memora persistence attempted but failed due to server-side tag allowlist constraint; the arbitration verdict is not affected. **Prior-round findings (R1):** All 4 were confirmed fixed by both reviewers — server.ts HMAC-SHA256 guard, types.ts JSDoc, package.json tsx→devDeps, connect.ts ticket length guard. These are not re-raised. **A=1, B=0 (truncated parse):** Reviewer B's JSON was truncated to `[]` due to a mid-stream parse failure, but B's prose summary describes **the same issue** as A's single finding. Since B's finding is prose-only (no grounded JSON entry), I treated A's finding as unique-to-one and applied Rule 2 (verify before keeping). **Verification performed:** - `Read /var/lib/pr-reviewer/.../640/.env.example` → File is 20 lines, ends at `OTEL_TRACES_SAMPLER_ARG=1.0`. `CENTRIFUGO_PROXY_SECRET` is **absent**. ✅ confirmed. - `Grep CENTRIFUGO_PROXY_SECRET src/` → `config.ts:125` reads the variable; `server.ts:79` emits a `warn` if unset. ✅ confirmed — the variable is real and used. - `Grep CENTRIFUGO README.md` → **Zero matches**. The README env-var table introduced in this PR omits the secret entirely. ✅ confirmed. **Outcome:** Finding kept. 1 minor finding retained. ### Blast Radius The gap is documentation-only: `.env.example` and the README env-var table omit `CENTRIFUGO_PROXY_SECRET`. The implementation in `config.ts`, `server.ts`, and `main.ts` is correct. No runtime code is changed by the fix; blast radius is confined to two documentation files. **BLAST_SCORE: 2/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `verifyProxySecret (server.ts)`, `readString(env, "CENTRIFUGO_PROXY_SECRET") (config.ts:125)` | | Migration touched | — | | Test delta | +772 / -0 lines in test files | | Dependency changes | `package-lock.json`, `package.json` | ### CI status (head `17ebad0e599f`) _No CI checks reported for this commit._ ### Findings (1) #### **[MINOR]** `CENTRIFUGO_PROXY_SECRET` absent from `.env.example` and README env-var table _.env.example:20_ `.env.example` ends at line 20 (`OTEL_TRACES_SAMPLER_ARG=1.0`) with no entry for `CENTRIFUGO_PROXY_SECRET`. `config.ts:125` reads this variable and `server.ts:79` emits a `warn` log if it is unset (signature validation silently disabled). The README env-var table introduced in this PR also has zero mentions of the variable (confirmed by grep). An operator following the `.env.example` runbook will deploy without the secret and only discover the gap from a runtime warning — exactly the operator ergonomics gap the R1 blocking finding was meant to close. The implementation is correct; the documentation is incomplete. **Fix — add to `.env.example` after line 20:** ``` # --- Centrifugo proxy signature ------------------------------------------ # Required in production — must match Centrifugo's `proxy_secret` config. # Leave unset only for local dev (Envoy mTLS alone cannot pin the caller to # Centrifugo; any compromised mesh pod could consume single-use tickets). # CENTRIFUGO_PROXY_SECRET= ``` **Fix — add to README env-var table:** ``` | `CENTRIFUGO_PROXY_SECRET` | _(empty — disabled)_ | HMAC-SHA256 key shared with Centrifugo `proxy_secret`. **Must be set in production.** | ``` ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 2 • 1 finding (1m) • 2026-05-24T23:09:20.651Z → 2026-05-24T23:11:00.928Z • posted-as: pr-reviewer-bot</sub> </details>
R2 verdict finding (kept=1):

(1) MINOR .env.example:20 + README env-var table — CENTRIFUGO_PROXY_SECRET
    was implemented in R2 (HMAC preHandler) but absent from both
    .env.example and the README env-var table. An operator following the
    .env.example runbook would deploy without the secret + only discover
    the gap from the runtime warning, exactly the operator-ergonomics gap
    the R1 MAJOR was meant to close.

Fix (docs-only):

- Appended CENTRIFUGO_PROXY_SECRET block to .env.example with the
  "required in production / unset ONLY for local dev" warning + the
  reasoning (Envoy mTLS alone cannot pin caller to Centrifugo).
- Added the row to README's env-var table with the full default,
  semantic, and WARN-on-unset behaviour documented.

Verification:
- Docs-only delta; no code paths touched.
- `npx tsc --noEmit`: clean.
- `npm test`: 41/41 passing.

Superseded by round 4.

Show previous round

hib-pr-reviewer review — PR #1 (affinity-intelligence-rework/im2be-realtime-service)

Round 3 — head 777c7fec8990, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — kept 1 unique-to-B finding (Dockerfile floating base image tags, verified at lines 12 + 31); all five prior round findings confirmed resolved; no blocking issues.

Summary

Round 3 Arbitration

Memora recall: No prior run history found for this PR — treating as first persisted round despite being arbitration round 3. Memora persistence attempted but failed due to tag allowlist constraints (system configuration issue, not a reviewer fault).

Findings reconciliation:

  • Reviewer A: 0 findings — confirms all five prior tracked issues (rounds 1–2) are resolved.
  • Reviewer B: 1 finding — Dockerfile floating base image tags (minor severity).

Unique-to-B verification: Read /var/lib/pr-reviewer/repos/affinity-intelligence-rework/im2be-realtime-service/641/Dockerfile. Confirmed: line 12 = FROM node:22-bookworm-slim AS builder and line 31 = FROM node:22-bookworm-slim — both use a mutable floating tag with no @sha256:<digest> suffix. Finding is grounded; kept at original minor severity.

Outcome: Kept 1 finding (unique-to-B, verified). No agreed findings. No prior findings re-raised.

Blast Radius

The finding touches only the newly added Dockerfile (single build artefact, no exported API surface). The risk is confined to the image build pipeline and does not affect any TypeScript source, proto contracts, or runtime behaviour.

BLAST_SCORE: 2/10

Risk Indicators

Indicator Value
Sensitive functions
Migration touched
Test delta +772 / -0 lines in test files
Dependency changes package-lock.json, package.json

CI status (head 777c7fec8990)

No CI checks reported for this commit.

Findings (1)

[MINOR] Both FROM stages use a floating mutable tag — pin to SHA-256 digest for supply-chain safety

Dockerfile:12

Lines 12 and 31 both reference node:22-bookworm-slim without a digest pin:

FROM node:22-bookworm-slim AS builder   # line 12

FROM node:22-bookworm-slim              # line 31

A mutable tag means any future docker build --pull could silently receive a different OS layer — including a patched or (in a compromised-registry scenario) substituted image — with no diff-visible change in this repository.

Fix: pin both stages to the same immutable digest:

FROM node:22-bookworm-slim@sha256:<current-digest> AS builder

FROM node:22-bookworm-slim@sha256:<current-digest>

Obtain the current digest with:

docker pull node:22-bookworm-slim
docker inspect node:22-bookworm-slim --format '{{index .RepoDigests 0}}'

Both stages should reference the same digest to guarantee the runtime OS layer is provably identical to the builder's. If automated updates are desired, a FluxCD ImagePolicy or Renovate docker manager can keep the pinned digest current.

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 3 • 1 finding (1m) • 2026-05-24T23:15:59.439Z → 2026-05-24T23:17:03.027Z • posted-as: pr-reviewer-bot

<!-- im2be-pr-reviewer collapsed --> > _Superseded by round 4._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #1 (affinity-intelligence-rework/im2be-realtime-service) **Round 3** — head `777c7fec8990`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — kept 1 unique-to-B finding (Dockerfile floating base image tags, verified at lines 12 + 31); all five prior round findings confirmed resolved; no blocking issues. ### Summary ## Round 3 Arbitration **Memora recall:** No prior run history found for this PR — treating as first persisted round despite being arbitration round 3. Memora persistence attempted but failed due to tag allowlist constraints (system configuration issue, not a reviewer fault). **Findings reconciliation:** - **Reviewer A:** 0 findings — confirms all five prior tracked issues (rounds 1–2) are resolved. - **Reviewer B:** 1 finding — `Dockerfile` floating base image tags (minor severity). **Unique-to-B verification:** Read `/var/lib/pr-reviewer/repos/affinity-intelligence-rework/im2be-realtime-service/641/Dockerfile`. Confirmed: line 12 = `FROM node:22-bookworm-slim AS builder` and line 31 = `FROM node:22-bookworm-slim` — both use a mutable floating tag with no `@sha256:<digest>` suffix. Finding is grounded; kept at original minor severity. **Outcome:** Kept 1 finding (unique-to-B, verified). No agreed findings. No prior findings re-raised. ### Blast Radius The finding touches only the newly added Dockerfile (single build artefact, no exported API surface). The risk is confined to the image build pipeline and does not affect any TypeScript source, proto contracts, or runtime behaviour. **BLAST_SCORE: 2/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | — | | Migration touched | — | | Test delta | +772 / -0 lines in test files | | Dependency changes | `package-lock.json`, `package.json` | ### CI status (head `777c7fec8990`) _No CI checks reported for this commit._ ### Findings (1) #### **[MINOR]** Both FROM stages use a floating mutable tag — pin to SHA-256 digest for supply-chain safety _Dockerfile:12_ Lines 12 and 31 both reference `node:22-bookworm-slim` without a digest pin: ```dockerfile FROM node:22-bookworm-slim AS builder # line 12 … FROM node:22-bookworm-slim # line 31 ``` A mutable tag means any future `docker build --pull` could silently receive a different OS layer — including a patched or (in a compromised-registry scenario) substituted image — with no diff-visible change in this repository. **Fix:** pin both stages to the same immutable digest: ```dockerfile FROM node:22-bookworm-slim@sha256:<current-digest> AS builder … FROM node:22-bookworm-slim@sha256:<current-digest> ``` Obtain the current digest with: ```bash docker pull node:22-bookworm-slim docker inspect node:22-bookworm-slim --format '{{index .RepoDigests 0}}' ``` Both stages should reference the **same** digest to guarantee the runtime OS layer is provably identical to the builder's. If automated updates are desired, a FluxCD `ImagePolicy` or Renovate `docker` manager can keep the pinned digest current. ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 3 • 1 finding (1m) • 2026-05-24T23:15:59.439Z → 2026-05-24T23:17:03.027Z • posted-as: pr-reviewer-bot</sub> </details>
R3 verdict finding (kept=1):

(1) MINOR Dockerfile:12 + :31 — Both FROM stages used the mutable tag
    `node:22-bookworm-slim`. A future `docker build --pull` could
    silently receive a different OS layer (patched OR substituted)
    with no diff-visible change in this repo. Supply-chain hardening.

Fix:

- Both stages now pin to
  `node:22-bookworm-slim@sha256:7af03b14a13c8cdd38e45058fd957bf00a72bbe17feac43b1c15a689c029c732`.
- Same digest in BOTH stages so the runtime OS layer is provably
  identical to the builder's.
- Header comment documents (a) the rationale, (b) the manual-bump
  recipe (`docker inspect ... --format '{{index .RepoDigests 0}}'`),
  and (c) automation options (Renovate `docker` manager / FluxCD
  ImagePolicy) for keeping the pin current.

Verification:
- `docker build -t aim2be-realtime-service:r4-verify .`: clean build
  (DONE; image `c5813a98ccef`).
- No application-code delta; tests + tsc unchanged.

Superseded by round 5.

Show previous round

hib-pr-reviewer review — PR #1 (affinity-intelligence-rework/im2be-realtime-service)

Round 4 — head 7e6141853fc3, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — kept 1 verified minor finding (LOG_LEVEL silently ignored); all R1–R3 blocking findings resolved; Reviewer B agrees on zero remaining blockers.

Summary

Arbitration — Round 4

Memora recall: No prior run history found (store returned empty for both tags_any and tags_all queries); treated as first-time store. Memora write attempts failed due to permission/tag-allowlist constraints in this environment.

Reconciliation:

  • Reviewer B: 0 findings — all prior-round conditions confirmed resolved.
  • Reviewer A: 1 minor finding (unique-to-A) — verified via Read on three files.

Verification of unique-to-A finding (src/main.ts:38):

  1. src/config.ts:113logLevel: readString(env, "LOG_LEVEL", "info") — confirmed LOG_LEVEL is parsed into config.logLevel.
  2. src/main.ts:38-41createServer({ grpcClient, centrifugoProxySecret: config.centrifugoProxySecret })logger field is absent; config.logLevel is never passed.
  3. src/server.ts:61logger: deps.logger ?? { level: "info" } — when logger is not injected, Fastify hard-codes "info" regardless of the env var.

Finding is confirmed accurate. Kept at minor severity (operator-visible behaviour gap: LOG_LEVEL=debug in a production ConfigMap has zero effect; runbook step "enable debug logging" is silently broken, but no data-loss or security consequence).

Prior-round findings: All six R1–R3 findings confirmed resolved per both reviewers' sign-offs.

Kept 1, dropped 0.

Blast Radius

The gap is isolated to the Fastify logger initialisation path in src/main.ts. It has no effect on request handling, security enforcement, or the gRPC client. The fix is a single-line addition to one call site.

BLAST_SCORE: 1/10

Risk Indicators

Indicator Value
Sensitive functions createHmac, timingSafeEqual
Migration touched
Test delta +772 / -0 lines in test files
Dependency changes package-lock.json, package.json

CI status (head 7e6141853fc3)

No CI checks reported for this commit.

Findings (1)

[MINOR] config.logLevel loaded but never forwarded — LOG_LEVEL env var silently ignored

src/main.ts:38

loadConfig() parses LOG_LEVEL into config.logLevel (src/config.ts:113), but the createServer call at lines 38–41 passes only grpcClient and centrifugoProxySecret — the logger field is omitted:

// main.ts:38-41 (actual)
const server = createServer({
  grpcClient,
  centrifugoProxySecret: config.centrifugoProxySecret,
  // logger: ← missing
});

createServer therefore falls through to deps.logger ?? { level: "info" } (server.ts:61), which hard-codes "info" regardless of the operator-supplied LOG_LEVEL. Setting LOG_LEVEL=debug in a production ConfigMap has zero effect, silently breaking the standard runbook step of enabling debug logging to diagnose incidents.

Fix:

const server = createServer({
  grpcClient,
  centrifugoProxySecret: config.centrifugoProxySecret,
  logger: { level: config.logLevel }, // ← wire the env var
});

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 4 • 1 finding (1m) • 2026-05-24T23:21:56.450Z → 2026-05-24T23:23:49.593Z • posted-as: pr-reviewer-bot

<!-- im2be-pr-reviewer collapsed --> > _Superseded by round 5._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #1 (affinity-intelligence-rework/im2be-realtime-service) **Round 4** — head `7e6141853fc3`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — kept 1 verified minor finding (LOG_LEVEL silently ignored); all R1–R3 blocking findings resolved; Reviewer B agrees on zero remaining blockers. ### Summary ## Arbitration — Round 4 **Memora recall:** No prior run history found (store returned empty for both `tags_any` and `tags_all` queries); treated as first-time store. Memora write attempts failed due to permission/tag-allowlist constraints in this environment. **Reconciliation:** - Reviewer B: 0 findings — all prior-round conditions confirmed resolved. - Reviewer A: 1 minor finding (unique-to-A) — verified via `Read` on three files. **Verification of unique-to-A finding (`src/main.ts:38`):** 1. `src/config.ts:113` — `logLevel: readString(env, "LOG_LEVEL", "info")` — confirmed `LOG_LEVEL` is parsed into `config.logLevel`. 2. `src/main.ts:38-41` — `createServer({ grpcClient, centrifugoProxySecret: config.centrifugoProxySecret })` — `logger` field is **absent**; `config.logLevel` is never passed. 3. `src/server.ts:61` — `logger: deps.logger ?? { level: "info" }` — when `logger` is not injected, Fastify hard-codes `"info"` regardless of the env var. Finding is **confirmed accurate**. Kept at `minor` severity (operator-visible behaviour gap: `LOG_LEVEL=debug` in a production ConfigMap has zero effect; runbook step "enable debug logging" is silently broken, but no data-loss or security consequence). **Prior-round findings:** All six R1–R3 findings confirmed resolved per both reviewers' sign-offs. **Kept 1, dropped 0.** ### Blast Radius The gap is isolated to the Fastify logger initialisation path in `src/main.ts`. It has no effect on request handling, security enforcement, or the gRPC client. The fix is a single-line addition to one call site. **BLAST_SCORE: 1/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `createHmac`, `timingSafeEqual` | | Migration touched | — | | Test delta | +772 / -0 lines in test files | | Dependency changes | `package-lock.json`, `package.json` | ### CI status (head `7e6141853fc3`) _No CI checks reported for this commit._ ### Findings (1) #### **[MINOR]** `config.logLevel` loaded but never forwarded — `LOG_LEVEL` env var silently ignored _src/main.ts:38_ `loadConfig()` parses `LOG_LEVEL` into `config.logLevel` (`src/config.ts:113`), but the `createServer` call at lines 38–41 passes only `grpcClient` and `centrifugoProxySecret` — the `logger` field is omitted: ```typescript // main.ts:38-41 (actual) const server = createServer({ grpcClient, centrifugoProxySecret: config.centrifugoProxySecret, // logger: ← missing }); ``` `createServer` therefore falls through to `deps.logger ?? { level: "info" }` (`server.ts:61`), which hard-codes `"info"` regardless of the operator-supplied `LOG_LEVEL`. Setting `LOG_LEVEL=debug` in a production ConfigMap has zero effect, silently breaking the standard runbook step of enabling debug logging to diagnose incidents. **Fix:** ```typescript const server = createServer({ grpcClient, centrifugoProxySecret: config.centrifugoProxySecret, logger: { level: config.logLevel }, // ← wire the env var }); ``` ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 4 • 1 finding (1m) • 2026-05-24T23:21:56.450Z → 2026-05-24T23:23:49.593Z • posted-as: pr-reviewer-bot</sub> </details>
R4 verdict finding (kept=1):

(1) MINOR src/main.ts:38 — loadConfig() parses LOG_LEVEL into
    config.logLevel but main() never forwarded it to createServer.
    createServer's `deps.logger ?? { level: "info" }` fallback then
    hard-coded "info" regardless of the operator-supplied env var.
    Setting LOG_LEVEL=debug in a production ConfigMap had zero
    effect — silently breaking the standard incident-diagnosis runbook
    step.

Fix: pass `logger: { level: config.logLevel }` into the createServer
call alongside grpcClient + centrifugoProxySecret. Inline comment
explains the rationale + why the fallback is silent (so future
maintainers don't quietly drop it).

Verification:
- `npx tsc --noEmit`: clean.
- `npm test`: 41/41 passing.
- main.ts-only delta; no test-config impact (tests pass an explicit
  logger override via { level: "warn" }).

Superseded by round 6.

Show previous round

hib-pr-reviewer review — PR #1 (affinity-intelligence-rework/im2be-realtime-service)

Round 5 — head d758adb369d1, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — kept 1 verified unique-to-B minor finding (Dockerfile tini unversioned apt install); A had 0 findings, all 7 prior-round issues confirmed resolved.

Summary

Round 5 arbitration — kept 1 unique-to-B finding after verification; dropped 0.

Reviewer A produced 0 findings (all 7 prior-round issues confirmed resolved). Reviewer B raised 1 new minor finding: Dockerfile:47tini installed via apt-get install without a version pin.

Verification (unique-to-B): Read /var/lib/pr-reviewer/repos/affinity-intelligence-rework/im2be-realtime-service/643/Dockerfile lines 46–48. Confirmed: RUN apt-get update && apt-get install -y --no-install-recommends tini ca-certificates && rm -rf /var/lib/apt/lists/* — no version specifier present. The finding is accurate. Even though the base-image FROM stages are both pinned to sha256:7af03b14…, apt-get update at line 46 still reaches the live Debian bookworm mirror at build time; a future rebuild after a tini package bump would silently embed a different binary with no diff-visible evidence.

Memora persistence: Attempted create/absorb; failed due to tag-allowlist restrictions in this environment. The run summary could not be persisted.

Prior findings (rounds 1–4): All 7 confirmed resolved by both reviewers. Not re-raised.

Blast Radius

The finding is confined to the Dockerfile runtime stage. It affects only the tini binary embedded in the container image and has no bearing on application logic, API surface, or other modules. Impact is limited to supply-chain reproducibility of the built image.

BLAST_SCORE: 2/10

Risk Indicators

Indicator Value
Sensitive functions
Migration touched
Test delta +772 / -0 lines in test files
Dependency changes package-lock.json, package.json

CI status (head d758adb369d1)

No CI checks reported for this commit.

Findings (1)

[MINOR] tini installed without version pinning — supply-chain gap remains after base-image digest fix

Dockerfile:47

Line 46 runs apt-get update and line 47 installs tini ca-certificates without a version specifier. Although both FROM stages are correctly pinned to sha256:7af03b14a13c8cdd38e45058fd957bf00a72bbe17feac43b1c15a689c029c732 (fixing the R3 finding), apt-get update still contacts the live Debian bookworm mirror at build time. A future docker build after a tini package bump — or a mirror-level substitution — would silently land a different binary with no change visible in the Dockerfile diff.

Recommended fix — pin tini to its current bookworm version:

RUN apt-get update \
    && apt-get install -y --no-install-recommends tini=0.19.0-1 ca-certificates \
    && rm -rf /var/lib/apt/lists/*

Confirm the exact epoch+version with apt-cache policy tini inside node:22-bookworm-slim@sha256:7af03b14…. ca-certificates is deliberately excluded from pinning (standard practice — locking it would block CA-bundle updates). An alternative is a checksummed binary download from the tini GitHub releases page using sha256sum --check.

This is a deferred tech-debt item acceptable to ship with the current advisory comment; track for the next infrastructure maintenance window.

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 5 • 1 finding (1m) • 2026-05-24T23:29:49.921Z → 2026-05-24T23:31:12.757Z • posted-as: pr-reviewer-bot

<!-- im2be-pr-reviewer collapsed --> > _Superseded by round 6._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #1 (affinity-intelligence-rework/im2be-realtime-service) **Round 5** — head `d758adb369d1`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — kept 1 verified unique-to-B minor finding (Dockerfile tini unversioned apt install); A had 0 findings, all 7 prior-round issues confirmed resolved. ### Summary **Round 5 arbitration — kept 1 unique-to-B finding after verification; dropped 0.** Reviewer A produced 0 findings (all 7 prior-round issues confirmed resolved). Reviewer B raised 1 new minor finding: `Dockerfile:47` — `tini` installed via `apt-get install` without a version pin. **Verification (unique-to-B):** Read `/var/lib/pr-reviewer/repos/affinity-intelligence-rework/im2be-realtime-service/643/Dockerfile` lines 46–48. Confirmed: `RUN apt-get update && apt-get install -y --no-install-recommends tini ca-certificates && rm -rf /var/lib/apt/lists/*` — no version specifier present. The finding is accurate. Even though the base-image `FROM` stages are both pinned to `sha256:7af03b14…`, `apt-get update` at line 46 still reaches the live Debian bookworm mirror at build time; a future rebuild after a `tini` package bump would silently embed a different binary with no diff-visible evidence. **Memora persistence:** Attempted create/absorb; failed due to tag-allowlist restrictions in this environment. The run summary could not be persisted. **Prior findings (rounds 1–4):** All 7 confirmed resolved by both reviewers. Not re-raised. ### Blast Radius The finding is confined to the Dockerfile runtime stage. It affects only the `tini` binary embedded in the container image and has no bearing on application logic, API surface, or other modules. Impact is limited to supply-chain reproducibility of the built image. **BLAST_SCORE: 2/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | — | | Migration touched | — | | Test delta | +772 / -0 lines in test files | | Dependency changes | `package-lock.json`, `package.json` | ### CI status (head `d758adb369d1`) _No CI checks reported for this commit._ ### Findings (1) #### **[MINOR]** `tini` installed without version pinning — supply-chain gap remains after base-image digest fix _Dockerfile:47_ Line 46 runs `apt-get update` and line 47 installs `tini ca-certificates` without a version specifier. Although both `FROM` stages are correctly pinned to `sha256:7af03b14a13c8cdd38e45058fd957bf00a72bbe17feac43b1c15a689c029c732` (fixing the R3 finding), `apt-get update` still contacts the live Debian bookworm mirror at build time. A future `docker build` after a `tini` package bump — or a mirror-level substitution — would silently land a different binary with no change visible in the Dockerfile diff. **Recommended fix — pin `tini` to its current bookworm version:** ```dockerfile RUN apt-get update \ && apt-get install -y --no-install-recommends tini=0.19.0-1 ca-certificates \ && rm -rf /var/lib/apt/lists/* ``` Confirm the exact epoch+version with `apt-cache policy tini` inside `node:22-bookworm-slim@sha256:7af03b14…`. `ca-certificates` is deliberately excluded from pinning (standard practice — locking it would block CA-bundle updates). An alternative is a checksummed binary download from the [tini GitHub releases page](https://github.com/krallin/tini/releases) using `sha256sum --check`. This is a deferred tech-debt item acceptable to ship with the current advisory comment; track for the next infrastructure maintenance window. ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 5 • 1 finding (1m) • 2026-05-24T23:29:49.921Z → 2026-05-24T23:31:12.757Z • posted-as: pr-reviewer-bot</sub> </details>
R5 verdict finding (kept=1):

(1) MINOR Dockerfile:47 — apt-get install tini ca-certificates was
    unversioned. Although both FROM stages are SHA-pinned (R4 fix),
    `apt-get update` still contacts the live Debian bookworm mirror at
    build time. A tini package bump OR mirror-level substitution would
    silently land a different binary with no Dockerfile-diff signal.

Fix:

- Pinned tini to `0.19.0-1+b3` — the exact version that ships in the
  SHA-pinned base image (confirmed via `apt-cache policy tini` run
  inside the digest-pinned node:22-bookworm-slim).
- ca-certificates LEFT UNPINNED (standard practice — locking it would
  block CA-bundle updates which are security-relevant flow-through).
- Comment block documents the pin source + the next-maintenance-window
  re-check recipe (re-run `apt-cache policy tini` against the
  then-current SHA pin and bump in lockstep).

Verification:
- `docker build -t aim2be-realtime-service:r6-verify .`: clean
  (tini 0.19.0-1+b3 unpacked + installed; DONE).
- Dockerfile-only delta; no application-code changes.

hib-pr-reviewer review — PR #1 (affinity-intelligence-rework/im2be-realtime-service)

Round 6 — head 275934b9c06e, base main, trigger synchronize

TL;DR: NO_NEW_FINDINGS — No new findings this round.

Summary

[quorum-converged] A=0 = B=0. ## Round 6 — All prior findings resolved; no new issues found

This round verified the complete current state of all 8 carried findings:

Round Finding Status
R1 src/server.ts:57 — No proxy-secret HMAC validation Fixed — preHandler hook with timingSafeEqual at lines 99–131
R1 src/types.ts:35 — Misattributed JSDoc on protocol_version Fixed — comment correctly describes field on line 35
R1 package.json:28tsx in dependencies Fixed — moved to devDependencies
R1 src/handlers/connect.ts:111 — No upper-bound ticket length guard Fixed — MAX_TICKET_LENGTH = 256 guard at lines 121–127
R2 .env.example:20CENTRIFUGO_PROXY_SECRET absent Fixed — present at .env.example:26 and in README env-var table
R3 Dockerfile:12 — Floating mutable base image tag Fixed — both FROM stages use sha256:7af03b14… digest
R4 src/main.ts:38config.logLevel never forwarded Fixed — logger: { level: config.logLevel } passed at line 45
R5 Dockerfile:47tini version unpinned Fixed — tini=0.19.0-1+b3

No new actionable issues were found in the updated diff or the files it touches. The HMAC implementation uses timingSafeEqual, the proto-loader path resolution is correct for both src/ and dist/ layouts, the --ignore-scripts flag is safe for all production dependencies (pure-JS @grpc/grpc-js), and the fail-closed behaviour in projectResponse (empty user_id → internal error) is correct.

CI status (head 275934b9c06e)

No CI checks reported for this commit.

Findings

No new findings this round.

Quorum converged on empty findings (A + B both returned 0).

Verdict

NO_NEW_FINDINGS


hib-pr-reviewer • round 6 • 0 findings • 2026-05-24T23:32:25.906Z → 2026-05-24T23:35:14.511Z • posted-as: pr-reviewer-bot • [bookkeeping fallback]

## hib-pr-reviewer review — PR #1 (affinity-intelligence-rework/im2be-realtime-service) **Round 6** — head `275934b9c06e`, base `main`, trigger `synchronize` **TL;DR:** NO_NEW_FINDINGS — No new findings this round. ### Summary [quorum-converged] A=0 = B=0. ## Round 6 — All prior findings resolved; no new issues found This round verified the complete current state of all 8 carried findings: | Round | Finding | Status | |---|---|---| | R1 | `src/server.ts:57` — No proxy-secret HMAC validation | ✅ Fixed — `preHandler` hook with `timingSafeEqual` at lines 99–131 | | R1 | `src/types.ts:35` — Misattributed JSDoc on `protocol_version` | ✅ Fixed — comment correctly describes field on line 35 | | R1 | `package.json:28` — `tsx` in `dependencies` | ✅ Fixed — moved to `devDependencies` | | R1 | `src/handlers/connect.ts:111` — No upper-bound ticket length guard | ✅ Fixed — `MAX_TICKET_LENGTH = 256` guard at lines 121–127 | | R2 | `.env.example:20` — `CENTRIFUGO_PROXY_SECRET` absent | ✅ Fixed — present at `.env.example:26` and in README env-var table | | R3 | `Dockerfile:12` — Floating mutable base image tag | ✅ Fixed — both `FROM` stages use `sha256:7af03b14…` digest | | R4 | `src/main.ts:38` — `config.logLevel` never forwarded | ✅ Fixed — `logger: { level: config.logLevel }` passed at line 45 | | R5 | `Dockerfile:47` — `tini` version unpinned | ✅ Fixed — `tini=0.19.0-1+b3` | No new actionable issues were found in the updated diff or the files it touches. The HMAC implementation uses `timingSafeEqual`, the proto-loader path resolution is correct for both `src/` and `dist/` layouts, the `--ignore-scripts` flag is safe for all production dependencies (pure-JS `@grpc/grpc-js`), and the fail-closed behaviour in `projectResponse` (empty `user_id` → internal error) is correct. ### CI status (head `275934b9c06e`) _No CI checks reported for this commit._ ### Findings **No new findings this round.** _Quorum converged on empty findings (A + B both returned 0)._ ### Verdict **NO_NEW_FINDINGS** --- <sub>hib-pr-reviewer • round 6 • 0 findings • 2026-05-24T23:32:25.906Z → 2026-05-24T23:35:14.511Z • posted-as: pr-reviewer-bot • [bookkeeping fallback]</sub>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
affinity-intelligence-rework/im2be-realtime-service!1
No description provided.