fix(server): unbuffered logging + reject operator token on contact sync
Some checks failed
Swift Build & Test / swift build + test (push) Waiting to run
Server Typecheck & Test / bun typecheck + test (push) Failing after 5m9s

- 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 <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-29 19:47:18 -04:00
parent f6ce05d864
commit f9cf50e695
3 changed files with 19 additions and 5 deletions

View file

@ -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

View file

@ -1,9 +1,14 @@
import { writeSync } from 'node:fs';
type Level = 'debug' | 'info' | 'warn' | 'error';
function emit(level: Level, msg: string, meta?: Record<string, unknown>): 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 = {

View file

@ -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 });