From f9cf50e695502df3999e710c77bbc2ccb7655112 Mon Sep 17 00:00:00 2001 From: Natalie Date: Mon, 29 Jun 2026 19:47:18 -0400 Subject: [PATCH] fix(server): unbuffered logging + reject operator token on contact sync - logger: emit straight to fd 1/2 (unbuffered). The buffered process.std* streams block-buffer to a pipe under systemd, so low-volume logs never flushed and were invisible. - /client/imessage/contacts: return 401 (like /sync/batch) when the caller presents the operator/service token instead of a device token, instead of 500ing on a null deviceId downstream. - systemd unit: reflect the working deploy (root + /root/.bun, Redis dependency, file logging since the droplet journald is volatile). Co-Authored-By: Claude Opus 4.8 --- deploy/systemd/mac-sync-server.service | 12 +++++++++--- src/server/src/shared/logger.ts | 9 +++++++-- src/server/src/surfaces/client/imessage.ts | 3 +++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/deploy/systemd/mac-sync-server.service b/deploy/systemd/mac-sync-server.service index 60ac66f..c44b648 100644 --- a/deploy/systemd/mac-sync-server.service +++ b/deploy/systemd/mac-sync-server.service @@ -1,16 +1,22 @@ [Unit] Description=Mac Sync Server -After=network.target +After=network.target redis-server.service +Wants=redis-server.service [Service] Type=simple -User=lilith +# Runs as root on the backend droplet using the root-owned bun install — matches +# the other services on that host (no dedicated service user is provisioned). +User=root WorkingDirectory=/opt/mac-sync-server -ExecStart=/home/lilith/.bun/bin/bun run src/main.ts +ExecStart=/root/.bun/bin/bun run src/main.ts Restart=on-failure RestartSec=5 Environment=NODE_ENV=production EnvironmentFile=/etc/mac-sync-server/env +# journald on this droplet is volatile and captures nothing, so log to a file. +StandardOutput=append:/var/log/mac-sync-server.log +StandardError=append:/var/log/mac-sync-server.log [Install] WantedBy=multi-user.target diff --git a/src/server/src/shared/logger.ts b/src/server/src/shared/logger.ts index 2306d6b..bf1051e 100644 --- a/src/server/src/shared/logger.ts +++ b/src/server/src/shared/logger.ts @@ -1,9 +1,14 @@ +import { writeSync } from 'node:fs'; + type Level = 'debug' | 'info' | 'warn' | 'error'; function emit(level: Level, msg: string, meta?: Record): void { const line = { ts: new Date().toISOString(), level, msg, ...(meta ?? {}) }; - const stream = level === 'error' || level === 'warn' ? process.stderr : process.stdout; - stream.write(`${JSON.stringify(line)}\n`); + // Write straight to the fd (1=stdout, 2=stderr). The buffered process.std* + // streams block-buffer to a pipe (systemd journal), so low-volume logs never + // flush and stay invisible; writeSync emits each line immediately. + const fd = level === 'error' || level === 'warn' ? 2 : 1; + writeSync(fd, `${JSON.stringify(line)}\n`); } export const logger = { diff --git a/src/server/src/surfaces/client/imessage.ts b/src/server/src/surfaces/client/imessage.ts index c6228d7..9bdb62e 100644 --- a/src/server/src/surfaces/client/imessage.ts +++ b/src/server/src/surfaces/client/imessage.ts @@ -99,6 +99,9 @@ export const imessageClientRouter = new Hono() }) .post('/contacts', async (c) => { const deviceId = c.get('deviceId'); + if (!deviceId || deviceId === 'operator') { + return c.json({ error: 'device token required for contact sync', code: 'bad_token' }, 401); + } const payload = syncContactsSchema.parse(await c.req.json()); const result = await withSyncHistory(deviceId, 'contacts', () => ingestContacts(deviceId, payload)); return c.json({ success: true, data: result });