pinbox-mongo-proxy — Operations

Service: pinbox-mongo-proxy · Host: bms-4 (54.36.123.110) · Runtime: Node.js 20 (Hono) in Docker Compose · Ingress: Traefik → https://pinbox-mongo-proxy.bms-4.infra.zintegrowana.online Source: infra-src/pinbox-mongo-proxy/ · Design: docs/designs/5408-pinbox-mongo-proxy.md · Issues: #5408, #5400

Purpose

Authenticated proxy that lets et-operational-platform (Vercel, Pro plan, no static egress IP) read w4_db.regRecords (and edit one record at a time, #5597) from the Pinbox24 rs0 replica set without exposing port 27017 to Vercel’s rotating serverless IPs. bms-4 is already on the rs0 27017 ufw allowlist and already runs Traefik on 80/443, so the proxy adds no new open port and no new ufw rule. The et_oper MongoDB credential stays on bms-4; Vercel holds only the shared proxy secret. Mirror-image sibling of infra-src/redis-bridge.

Endpoints

MethodPathAuthResponse
GET/healthnone200 ok (liveness — never touches Mongo)
POST/recordsX-Proxy-Key200 {records, count} — bounded w4_db.regRecords.find(filter, {projection, sort}).skip().limit()
POST/filesX-Proxy-Key200 {records, count} — same, bound to w4_db.files (#5572)
POST/registriesX-Proxy-Key200 {records, count} — same, bound to w4_db.registries (#5572)
POST/records/updateX-Proxy-Key200 {record} / 404 not_foundw4_db.regRecords.findOneAndUpdate({_id}, {$set}, {returnDocument:"after"}) (#5597)

The three read POST routes share one request/response contract; each is bound to a single fixed collection (the caller cannot select one). /files and /registries were added in #5572 to unblock et-op’s /zarzad/pinbox preview/download/sync. /records/update (#5597) is the one narrow write: body {filter:{_id:{$oid:...}}, set:{...}}, filter pinned to exactly one _id (never update-many), set used verbatim as the $set document. et-op whitelists writable fields upstream (against pinbox_column_defs.is_editable), so the proxy carries no field whitelist of its own.

Errors: 401 unauthorized, 400 invalid_json / invalid_filter / invalid_set (write route) / forbidden_operator / invalid_oid / invalid_pagination, 404 not_found (write route matched no record), 413 body_too_large, 502 mongo_query_failed. Full contract in the service README.

ObjectId values — {"$oid": "..."} request contract (#5480)

regId / officeId and other _id-style fields in w4_db.regRecords are BSON ObjectIds. The caller must send them in MongoDB Extended-JSON form — {"$oid": "<24-hex>"} — not as a bare hex string. JSON.stringify(ObjectId) emits a plain hex string, and Mongo will not auto-cast it for equality, so a bare-string filter matches zero documents (200 {records:[], count:0}) — a silent failure worse than an error. The proxy reconstructs {"$oid":...} wrappers into real ObjectIds recursively across filter/projection/sort (and, on /records/update, across filter and set) including inside $in arrays before querying; a malformed $oid (not a 24-char hex string) is rejected 400 invalid_oid. The forbidden-operator guard runs on the raw filter (and write-route set) before reconstruction and is unaffected.

Configuration

Env is injected from bms-4/.env (populated by secrets-sync.yml). See bms-4/docker-compose.yml service pinbox-mongo-proxy.

EnvSourceNotes
PINBOX24_MONGODB_URIsecrets/et-operational-platform.env.sopsMust be distributed to bms-4/.env — currently synced to Vercel only. secret-manager action.
PINBOX_PROXY_SHARED_SECRETnew SOPS keyCreated + distributed (bms-4 and Vercel) by secret-manager.
MONGO_DB_NAME / MONGO_COLLECTIONcompose (w4_db / regRecords)Fixed; caller cannot change.
MAX_LIMITcompose (1000)Hard result cap.

The container fails closed — a missing PINBOX24_MONGODB_URI or PINBOX_PROXY_SHARED_SECRET exits on boot. Both keys must be in bms-4/.env before docker compose up.

Deploy / rebuild (sys-admin on bms-4)

Prerequisite: secret-manager has placed both env keys in bms-4/.env.

cd /opt/p24-infra/bms-4
docker compose build pinbox-mongo-proxy
docker compose up -d pinbox-mongo-proxy
docker compose logs --tail=20 pinbox-mongo-proxy   # expect "connected to w4_db [regRecords, files, registries]"

Verify

# 1. Health (public) — expect: ok, with a valid Let's Encrypt cert
curl https://pinbox-mongo-proxy.bms-4.infra.zintegrowana.online/health
 
# 2. Auth gate — expect: 401
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
  https://pinbox-mongo-proxy.bms-4.infra.zintegrowana.online/records -d '{}'
 
# 3. Injection guard — expect: 400 forbidden_operator (key sourced silently from bms-4/.env)
KEY=$(grep '^PINBOX_PROXY_SHARED_SECRET=' /opt/p24-infra/bms-4/.env | cut -d= -f2-)
curl -s -X POST -H "X-Proxy-Key: $KEY" -H 'Content-Type: application/json' \
  https://pinbox-mongo-proxy.bms-4.infra.zintegrowana.online/records \
  -d '{"filter":{"$where":"1"}}' ; unset KEY
 
# 4. Authed read — expect: 200 {records, count}
 
# 5. New routes reachable (#5572) — expect: 200 for each (key sourced silently from bms-4/.env)
KEY=$(grep '^PINBOX_PROXY_SHARED_SECRET=' /opt/p24-infra/bms-4/.env | cut -d= -f2-)
for route in files registries; do
  echo -n "$route: "
  curl -s -o /dev/null -w '%{http_code}\n' -X POST -H "X-Proxy-Key: $KEY" \
    -H 'Content-Type: application/json' \
    "https://pinbox-mongo-proxy.bms-4.infra.zintegrowana.online/$route" -d '{"limit":1}'
done ; unset KEY
 
# 6. Write route live + guards (#5597) — NON-DESTRUCTIVE checks only (key sourced silently):
KEY=$(grep '^PINBOX_PROXY_SHARED_SECRET=' /opt/p24-infra/bms-4/.env | cut -d= -f2-)
# 6a. Filter-shape guard — a non-_id filter is rejected before any Mongo call. Expect: 400 invalid_filter
curl -s -X POST -H "X-Proxy-Key: $KEY" -H 'Content-Type: application/json' \
  https://pinbox-mongo-proxy.bms-4.infra.zintegrowana.online/records/update \
  -d '{"filter":{"regId":{"$oid":"000000000000000000000000"}},"set":{"x":1}}'
# 6b. Route live — a well-formed update against a non-existent _id matches nothing → NO write occurs.
#     Expect: 404 {"error":"not_found"}  (all-zeros _id will never exist in regRecords)
curl -s -X POST -H "X-Proxy-Key: $KEY" -H 'Content-Type: application/json' \
  https://pinbox-mongo-proxy.bms-4.infra.zintegrowana.online/records/update \
  -d '{"filter":{"_id":{"$oid":"000000000000000000000000"}},"set":{"__probe":1}}'
unset KEY

Monitoring

Liveness is served by GET /health. A blackbox HTTP probe of /health can be added to the monitoring stack in a follow-up (not required for launch). Container logs never contain the URI, password, or proxy-key value — only Mongo error messages.

Compliance — dev_r_services registration

Register the new service in Supabase dev_r_services:

ColumnValue
namepinbox-mongo-proxy
categoryservice (bms-4 Docker)
hostbms-4
compliance_workbookyes
workbook_urldocs/pinbox-mongo-proxy-operations.md

dev_r_services is a VIEW in this project (see CLAUDE.md §Do NOT). The worker attempts a best-effort registration against the underlying relation; if the write is rejected, this is a secret-manager / admin follow-up — the row must exist before the service is treated as compliant.

Ownership

  • Code / config: dev-coder (this repo).
  • Secrets: secret-manager (PINBOX_PROXY_SHARED_SECRET creation + both keys → bms-4/.env).
  • Deploy / server ops: sys-admin / infra-task on bms-4.
  • et-op route swap: cross-repo follow-up (see #5400) — not owned here.