{"record":{"id":"b2184fe566abd388","repo":"thedotmack/claude-mem","slug":"user-id-must-be-non-empty","errorCode":null,"errorMessage":"user_id must be non-empty","messagePattern":"user_id must be non-empty","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"workers/sync-hub/src/do/SyncHub.ts","lineNumber":576,"sourceCode":"\n\tgetStatus(deviceId: string | null = null, deviceName: string | null = null): StatusOutcome {\n\t\t// Status is an authenticated read/probe, not device admission. A known\n\t\t// device may refresh its display metadata, but an arbitrary X-Device-Id\n\t\t// must not consume one of the account's 64 durable device slots.\n\t\tif (deviceId !== null) this.touchExistingDevice(deviceId, normalizeDeviceName(deviceName));\n\t\tconst sql = this.ctx.storage.sql;\n\t\treturn {\n\t\t\tprotocol_version: 2,\n\t\t\tepoch: this.meta(\"epoch\"),\n\t\t\thead_seq: this.headSeq(),\n\t\t\tprojected_seq: this.projectedSeq(),\n\t\t\top_count: sql.exec<{ n: number }>(\"SELECT COUNT(*) AS n FROM canonical_ops\").one().n,\n\t\t\tdevice_count: sql.exec<{ n: number }>(\"SELECT COUNT(*) AS n FROM devices\").one().n,\n\t\t};\n\t}\n\n\tgetMetadata(userId: string): HubMetadata {\n\t\tif (typeof userId !== \"string\" || userId.length === 0) throw invalid(\"user_id must be non-empty\");\n\t\tconst head = this.headSeq();\n\t\tconst projected = this.projectedSeq();\n\t\tconst connected = new Set<string>();\n\t\tfor (const socket of this.ctx.getWebSockets()) {\n\t\t\ttry {\n\t\t\t\tconst attachment = socket.deserializeAttachment() as { device_id?: unknown } | null;\n\t\t\t\tif (typeof attachment?.device_id === \"string\") connected.add(attachment.device_id);\n\t\t\t} catch {}\n\t\t}\n\t\tconst devices = this.ctx.storage.sql.exec<{\n\t\t\tdevice_id: string;\n\t\t\tname: string | null;\n\t\t\tlast_ack_seq: string;\n\t\t\tlast_seen: number | null;\n\t\t}>(\n\t\t\t`SELECT device_id, name, last_ack_seq, last_seen\n\t\t\t FROM devices\n\t\t\t ORDER BY last_seen IS NULL, last_seen DESC, device_id","sourceCodeStart":558,"sourceCodeEnd":594,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/workers/sync-hub/src/do/SyncHub.ts#L558-L594","documentation":"Validation guard in SyncHubDO.getMetadata: userId must be a non-empty string. getMetadata reports hub stats (op count, device count, head seq) for a user's DO, and the userId selects the DO instance, so it cannot be blank.","triggerScenarios":"Calling getMetadata('') or with null/undefined; admin/diagnostic endpoint receiving a request without a user identifier in path or query.","commonSituations":"Monitoring dashboards constructed before the user session is known, id extraction from a malformed JWT/URL returning empty string.","solutions":["Pass the authenticated user id (from JWT sub or session) to getMetadata","Validate the id is non-empty before constructing the DO stub","Fix id extraction (trim, correct claim name) so it never yields ''"],"exampleFix":"// before\nconst meta = await stub.getMetadata(token.sub);\n// after\nconst uid = (token.sub ?? '').trim();\nif (!uid) throw new Error('no user id in token');\nconst meta = await stub.getMetadata(uid);","handlingStrategy":"validation","validationCode":"const uid = (user?.id ?? '').trim();\nif (!uid) throw new Error('userId required for getMetadata');","typeGuard":"const hasUserId = (u: unknown): u is { id: string } => typeof u === 'object' && u !== null && typeof (u as any).id === 'string' && (u as any).id.length > 0;","tryCatchPattern":"try {\n  return await stub.getMetadata(userId);\n} catch (e) {\n  if (String(e).includes('user_id must be non-empty')) return null;\n  throw e;\n}","preventionTips":["Resolve the user id from the verified auth token, not raw client input","Trim and assert non-empty before constructing DO stubs","Have monitoring endpoints require an explicit user identifier"],"tags":["validation","sync","metadata"],"backgroundTag":"empty-required-field","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}