import { mobileMediaAssetInputSchema, pingMobileDeviceSchema, registerMobileDeviceSchema, syncMobileSMSSchema, } from "@pkg/logic/domains/mobile/data"; import { getFileController } from "@pkg/logic/domains/files/controller"; import { getMobileController } from "@pkg/logic/domains/mobile/controller"; import type { FlowExecCtx } from "@pkg/logic/core/flow.execution.context"; import { errorStatusMap, type Err } from "@pkg/result"; import { logDomainEvent } from "@pkg/logger"; import type { Context } from "hono"; import * as v from "valibot"; import { Hono } from "hono"; import { createHash } from "node:crypto"; const mobileController = getMobileController(); const fileController = getFileController(); const DEVICE_ID_HEADER = "x-device-id"; const FLOW_ID_HEADER = "x-flow-id"; const MEDIA_EXTERNAL_ID_HEADER = "x-media-external-id"; const MEDIA_MIME_TYPE_HEADER = "x-media-mime-type"; const MEDIA_FILENAME_HEADER = "x-media-filename"; const MEDIA_CAPTURED_AT_HEADER = "x-media-captured-at"; const MEDIA_SIZE_BYTES_HEADER = "x-media-size-bytes"; const MEDIA_HASH_HEADER = "x-media-hash"; const MEDIA_METADATA_HEADER = "x-media-metadata"; function buildFlowExecCtx(c: Context): FlowExecCtx { return { flowId: c.req.header(FLOW_ID_HEADER) || crypto.randomUUID(), }; } function respondError(c: Context, fctx: FlowExecCtx, err: Err) { const status = errorStatusMap[err.code] || 500; c.status(status as any); return c.json({ data: null, error: { ...err, flowId: fctx.flowId } }); } function requireExternalDeviceId(c: Context): string | null { const externalDeviceId = c.req.header(DEVICE_ID_HEADER); if (!externalDeviceId || externalDeviceId.trim().length === 0) { return null; } return externalDeviceId.trim(); } async function parseJson(c: Context) { try { return await c.req.json(); } catch { return null; } } function toOptionalString( value: FormDataEntryValue | string | null | undefined, ): string | undefined { if (typeof value !== "string") { return undefined; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } function parseOptionalJsonRecord(value: string | undefined): { value?: Record; error?: string; } { if (!value) { return {}; } try { const parsed = JSON.parse(value); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { return { value: parsed as Record }; } return { error: "metadata must be a JSON object" }; } catch { return { error: "metadata must be valid JSON" }; } } async function makeFileHash(file: File): Promise { try { const buffer = Buffer.from(await file.arrayBuffer()); return createHash("sha256").update(buffer).digest("hex"); } catch { return null; } } export const mobileRouter = new Hono() .post("/register", async (c) => { const fctx = buildFlowExecCtx(c); const startedAt = Date.now(); logDomainEvent({ event: "processor.mobile.register.started", fctx, }); const payload = await parseJson(c); const parsed = v.safeParse(registerMobileDeviceSchema, payload); if (!parsed.success) { const error = { flowId: fctx.flowId, code: "VALIDATION_ERROR", message: "Invalid register payload", description: "Please validate the payload and retry", detail: parsed.issues.map((issue) => issue.message).join(", "), } as Err; return respondError(c, fctx, error); } const result = await mobileController.registerDevice( fctx, parsed.output, ); if (result.isErr()) { logDomainEvent({ level: "error", event: "processor.mobile.register.failed", fctx, durationMs: Date.now() - startedAt, error: result.error, }); return respondError(c, fctx, result.error); } logDomainEvent({ event: "processor.mobile.register.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { deviceId: result.value.id }, }); return c.json({ data: result.value, error: null }); }) .put("/ping", async (c) => { const fctx = buildFlowExecCtx(c); const startedAt = Date.now(); const externalDeviceId = requireExternalDeviceId(c); if (!externalDeviceId) { const error = { flowId: fctx.flowId, code: "AUTH_ERROR", message: "Missing device id header", description: `Request must include ${DEVICE_ID_HEADER}`, detail: `${DEVICE_ID_HEADER} header is required`, } as Err; return respondError(c, fctx, error); } const payload = await parseJson(c); const parsed = v.safeParse(pingMobileDeviceSchema, payload ?? {}); if (!parsed.success) { const error = { flowId: fctx.flowId, code: "VALIDATION_ERROR", message: "Invalid ping payload", description: "Please validate the payload and retry", detail: parsed.issues.map((issue) => issue.message).join(", "), } as Err; return respondError(c, fctx, error); } const result = await mobileController.pingByExternalDeviceId( fctx, externalDeviceId, parsed.output, ); if (result.isErr()) { logDomainEvent({ level: "error", event: "processor.mobile.ping.failed", fctx, durationMs: Date.now() - startedAt, error: result.error, meta: { externalDeviceId }, }); return respondError(c, fctx, result.error); } logDomainEvent({ event: "processor.mobile.ping.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { externalDeviceId }, }); return c.json({ data: { ok: result.value }, error: null }); }) .put("/sms/sync", async (c) => { const fctx = buildFlowExecCtx(c); const startedAt = Date.now(); const externalDeviceId = requireExternalDeviceId(c); if (!externalDeviceId) { const error = { flowId: fctx.flowId, code: "AUTH_ERROR", message: "Missing device id header", description: `Request must include ${DEVICE_ID_HEADER}`, detail: `${DEVICE_ID_HEADER} header is required`, } as Err; return respondError(c, fctx, error); } const payload = await parseJson(c); const parsed = v.safeParse(syncMobileSMSSchema, payload); if (!parsed.success) { const error = { flowId: fctx.flowId, code: "VALIDATION_ERROR", message: "Invalid SMS sync payload", description: "Please validate the payload and retry", detail: parsed.issues.map((issue) => issue.message).join(", "), } as Err; return respondError(c, fctx, error); } const result = await mobileController.syncSMSByExternalDeviceId( fctx, externalDeviceId, parsed.output.messages, ); if (result.isErr()) { logDomainEvent({ level: "error", event: "processor.mobile.sms_sync.failed", fctx, durationMs: Date.now() - startedAt, error: result.error, meta: { externalDeviceId, received: parsed.output.messages.length, }, }); return respondError(c, fctx, result.error); } logDomainEvent({ event: "processor.mobile.sms_sync.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { externalDeviceId, ...result.value, }, }); return c.json({ data: result.value, error: null }); }) .put("/media/sync", async (c) => { const fctx = buildFlowExecCtx(c); const startedAt = Date.now(); const externalDeviceId = requireExternalDeviceId(c); if (!externalDeviceId) { const error = { flowId: fctx.flowId, code: "AUTH_ERROR", message: "Missing device id header", description: `Request must include ${DEVICE_ID_HEADER}`, detail: `${DEVICE_ID_HEADER} header is required`, } as Err; return respondError(c, fctx, error); } const form = await c.req.formData().catch(() => null); if (!form) { const error = { flowId: fctx.flowId, code: "VALIDATION_ERROR", message: "Invalid media upload request", description: "Expected multipart/form-data with a file field", detail: "Unable to parse multipart form data", } as Err; return respondError(c, fctx, error); } const fileEntry = form.get("file"); if (!(fileEntry instanceof File)) { const error = { flowId: fctx.flowId, code: "VALIDATION_ERROR", message: "Missing media file", description: "Expected a file part named 'file'", detail: "multipart/form-data requires file field", } as Err; return respondError(c, fctx, error); } const sizeBytesRaw = toOptionalString(form.get("sizeBytes")) || toOptionalString(c.req.header(MEDIA_SIZE_BYTES_HEADER)); const sizeBytesParsed = sizeBytesRaw !== undefined ? Number(sizeBytesRaw) : undefined; const metadataRaw = toOptionalString(form.get("metadata")) || toOptionalString(c.req.header(MEDIA_METADATA_HEADER)); const metadataResult = parseOptionalJsonRecord(metadataRaw); if (metadataResult.error) { const error = { flowId: fctx.flowId, code: "VALIDATION_ERROR", message: "Invalid media metadata", description: "Please provide metadata as a JSON object", detail: metadataResult.error, } as Err; return respondError(c, fctx, error); } const mediaPayload = { externalMediaId: toOptionalString(form.get("externalMediaId")) || toOptionalString(c.req.header(MEDIA_EXTERNAL_ID_HEADER)), fileId: crypto.randomUUID(), mimeType: toOptionalString(form.get("mimeType")) || toOptionalString(c.req.header(MEDIA_MIME_TYPE_HEADER)) || fileEntry.type || "application/octet-stream", filename: toOptionalString(form.get("filename")) || toOptionalString(c.req.header(MEDIA_FILENAME_HEADER)) || fileEntry.name, capturedAt: toOptionalString(form.get("capturedAt")) || toOptionalString(c.req.header(MEDIA_CAPTURED_AT_HEADER)), sizeBytes: Number.isFinite(sizeBytesParsed) ? sizeBytesParsed : fileEntry.size, hash: toOptionalString(form.get("hash")) || toOptionalString(c.req.header(MEDIA_HASH_HEADER)), metadata: metadataResult.value, }; const parsed = v.safeParse(mobileMediaAssetInputSchema, mediaPayload); if (!parsed.success) { const error = { flowId: fctx.flowId, code: "VALIDATION_ERROR", message: "Invalid media upload metadata", description: "Please validate multipart metadata and retry", detail: parsed.issues.map((issue) => issue.message).join(", "), } as Err; return respondError(c, fctx, error); } const deviceResult = await mobileController.resolveDeviceByExternalId( fctx, externalDeviceId, ); if (deviceResult.isErr()) { return respondError(c, fctx, deviceResult.error); } const externalMediaId = parsed.output.externalMediaId ?? null; const mediaHash = parsed.output.hash ?? (await makeFileHash(fileEntry)); if (externalMediaId) { const existingResult = await mobileController.findMediaAssetByExternalMediaId( fctx, deviceResult.value.id, externalMediaId, ); if (existingResult.isErr()) { return respondError(c, fctx, existingResult.error); } if (existingResult.value) { logDomainEvent({ event: "processor.mobile.media_sync.duplicate_skipped", fctx, durationMs: Date.now() - startedAt, meta: { externalDeviceId, externalMediaId, deviceId: deviceResult.value.id, mediaAssetId: existingResult.value.id, fileId: existingResult.value.fileId, }, }); return c.json({ data: { received: 1, inserted: 0, deviceId: deviceResult.value.id, fileId: existingResult.value.fileId, deduplicated: true, }, error: null, }); } } if (!externalMediaId && mediaHash) { const existingByHash = await mobileController.findMediaAssetByHash( fctx, deviceResult.value.id, mediaHash, ); if (existingByHash.isErr()) { return respondError(c, fctx, existingByHash.error); } if (existingByHash.value) { logDomainEvent({ event: "processor.mobile.media_sync.duplicate_skipped", fctx, durationMs: Date.now() - startedAt, meta: { externalDeviceId, dedupKey: "hash", mediaHash, deviceId: deviceResult.value.id, mediaAssetId: existingByHash.value.id, fileId: existingByHash.value.fileId, }, }); return c.json({ data: { received: 1, inserted: 0, deviceId: deviceResult.value.id, fileId: existingByHash.value.fileId, deduplicated: true, }, error: null, }); } } const uploadResult = await fileController.uploadFile( fctx, deviceResult.value.ownerUserId, fileEntry, { visibility: "private", metadata: { source: "mobile.media.sync", externalDeviceId, externalMediaId: parsed.output.externalMediaId || null, ...(parsed.output.metadata || {}), }, tags: ["mobile", "media-sync"], processImage: parsed.output.mimeType.startsWith("image/"), processVideo: parsed.output.mimeType.startsWith("video/"), }, ); if (uploadResult.isErr()) { logDomainEvent({ level: "error", event: "processor.mobile.media_upload.failed", fctx, durationMs: Date.now() - startedAt, error: uploadResult.error, meta: { externalDeviceId, filename: fileEntry.name }, }); return respondError(c, fctx, uploadResult.error); } const uploadedFileId = uploadResult.value.file?.id || uploadResult.value.uploadId || null; if (!uploadedFileId) { const error = { flowId: fctx.flowId, code: "INTERNAL_ERROR", message: "Failed to resolve uploaded file id", description: "Please retry media upload", detail: "File upload succeeded but returned no file id", } as Err; return respondError(c, fctx, error); } const result = await mobileController.syncMediaByExternalDeviceId( fctx, externalDeviceId, [{ ...parsed.output, hash: mediaHash ?? undefined, fileId: uploadedFileId }], ); if (result.isErr()) { await fileController.deleteFiles( fctx, [uploadedFileId], deviceResult.value.ownerUserId, ); logDomainEvent({ level: "error", event: "processor.mobile.media_sync.failed", fctx, durationMs: Date.now() - startedAt, error: result.error, meta: { externalDeviceId, received: 1, }, }); return respondError(c, fctx, result.error); } if (result.value.inserted === 0) { const rollback = await fileController.deleteFiles( fctx, [uploadedFileId], deviceResult.value.ownerUserId, ); if (rollback.isErr()) { logDomainEvent({ level: "error", event: "processor.mobile.media_sync.duplicate_cleanup_failed", fctx, durationMs: Date.now() - startedAt, error: rollback.error, meta: { externalDeviceId, fileId: uploadedFileId, externalMediaId, }, }); return respondError(c, fctx, rollback.error); } let dedupedFileId = uploadedFileId; if (externalMediaId) { const existingAfterInsert = await mobileController.findMediaAssetByExternalMediaId( fctx, deviceResult.value.id, externalMediaId, ); if (existingAfterInsert.isOk() && existingAfterInsert.value) { dedupedFileId = existingAfterInsert.value.fileId; } } logDomainEvent({ event: "processor.mobile.media_sync.duplicate_cleaned", fctx, durationMs: Date.now() - startedAt, meta: { externalDeviceId, externalMediaId, deviceId: deviceResult.value.id, uploadedFileId, dedupedFileId, }, }); return c.json({ data: { ...result.value, fileId: dedupedFileId, deduplicated: true, }, error: null, }); } logDomainEvent({ event: "processor.mobile.media_sync.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { externalDeviceId, fileId: uploadedFileId, ...result.value, }, }); return c.json({ data: { ...result.value, fileId: uploadedFileId, deduplicated: false, }, error: null, }); });