Conversation
Add workspace mark-read, push notification, and analytics routes. Update heartbeat route to accept directConnect and flexible status.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR introduces three new mobile API route handlers (analytics event ingestion, push notification management, and workspace read tracking) with OpenAPI-validated request/response schemas and token-based authentication. These routers are registered in the main Hono app under the Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds three new mobile sync API route files (
Confidence Score: 4/5Safe to merge after adding error handling around verifyTeam in the workspace-read route. One P1 finding: unhandled verifyTeam exceptions in the mark-read handler produce 500s for invalid/unauthorized team access. The remaining findings are P2 (stub type annotation drift, enum relaxation). The push and analytics routes are clean stubs with good test coverage. apps/www/lib/routes/mobile-workspace-read.route.ts — needs error handling around verifyTeam Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Mobile App] -->|POST /mobile/workspaces/mark-read| B[Hono API]
B --> C{Auth check}
C -->|invalid| D[401 Unauthorized]
C -->|valid| E[verifyTeam]
E -->|throws| F[500 - BUG: unhandled]
E -->|resolves| G[Call Convex mark-read]
G --> H[200 ok=true]
A -->|POST /mobile/push/*| B
B --> I{Auth check}
I -->|invalid| D
I -->|valid| J[Stub handler - log only]
J --> K[200 ok=true]
A -->|POST /mobile/analytics| B
B --> L{Auth check}
L -->|invalid| D
L -->|valid| M[Stub ingest - log only]
M --> N[200 accepted=true]
A -->|POST /mobile/heartbeat| B
B --> O{Verify machine JWT}
O -->|invalid| D
O -->|valid| P[Publish to Convex with directConnect]
P --> Q[202 accepted=true]
Reviews (1): Last reviewed commit: "Add mobile sync API routes for iOS app a..." | Re-trigger Greptile |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4548e9b829
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| .replace(/\/$/, ""); | ||
| // Calls Convex HTTP endpoint to mark the workspace as read. | ||
| const response = await fetch( | ||
| `${endpoint}/api/mobile/workspaces/mark-read`, |
There was a problem hiding this comment.
Register Convex handler for mark-read endpoint
The default implementation now POSTs to .../api/mobile/workspaces/mark-read, but the Convex HTTP router only registers /api/mobile/heartbeat (see packages/convex/convex/http.ts), so this request path will return 404 in production. Because this branch throws on non-OK responses, every real call to POST /api/mobile/workspaces/mark-read will fail with a server error instead of marking the workspace as read.
Useful? React with 👍 / 👎.
| tailscaleHostname: z.string().optional(), | ||
| tailscaleIPs: z.array(z.string()), | ||
| status: z.enum(["online", "offline", "unknown"]), | ||
| status: z.string(), |
There was a problem hiding this comment.
Enforce heartbeat status enum at request boundary
Changing status validation to z.string() allows arbitrary values through this API, but the downstream Convex mutation still validates status as online|offline|unknown (mobileMachines.upsertHeartbeatInternal). Any other value now passes initial validation and fails later during mutation args validation, turning a client input issue into a runtime 5xx path.
Useful? React with 👍 / 👎.
| const team = await verifyTeam({ | ||
| req: c.req.raw, | ||
| accessToken, | ||
| teamSlugOrId: body.teamSlugOrId, | ||
| }); | ||
|
|
There was a problem hiding this comment.
Unhandled
verifyTeam exception causes 500
If verifyTeam throws (e.g., the team slug doesn't exist or the user doesn't have access), the error propagates uncaught and Hono returns a 500. The OpenAPI definition declares no 403/404, so clients receive an opaque server error instead of an actionable auth/not-found response. A try/catch around the call (or a null-returning signature with a guard) is needed here.
| const ingest = | ||
| options?.ingest ?? | ||
| (async (args: { | ||
| event: string; | ||
| properties?: Record<string, unknown>; | ||
| }) => { | ||
| // Stub: log and accept. Forward to PostHog later. | ||
| console.log("mobile-analytics:", args.event, args.properties ?? {}); | ||
| }); |
There was a problem hiding this comment.
Default
ingest stub type omits accessToken
The default stub's inner type annotation is { event: string; properties?: ... }, dropping accessToken. The actual call (line 77) passes accessToken — it's silently ignored by the stub. When PostHog forwarding is added later, whoever wires it in via the default path will need to remember to add accessToken to the stub signature, or will find the parameter missing. The stub should mirror the full option type to avoid this drift.
| const ingest = | |
| options?.ingest ?? | |
| (async (args: { | |
| event: string; | |
| properties?: Record<string, unknown>; | |
| }) => { | |
| // Stub: log and accept. Forward to PostHog later. | |
| console.log("mobile-analytics:", args.event, args.properties ?? {}); | |
| }); | |
| const ingest = | |
| options?.ingest ?? | |
| (async (args: { | |
| accessToken: string; | |
| event: string; | |
| properties?: Record<string, unknown>; | |
| }) => { | |
| // Stub: log and accept. Forward to PostHog later. | |
| console.log("mobile-analytics:", args.event, args.properties ?? {}); | |
| }); |
| tailscaleHostname: z.string().optional(), | ||
| tailscaleIPs: z.array(z.string()), | ||
| status: z.enum(["online", "offline", "unknown"]), | ||
| status: z.string(), |
There was a problem hiding this comment.
status widened to unbounded z.string()
Changing from z.enum(["online", "offline", "unknown"]) to z.string() removes all schema-level validation and will generate a less descriptive OpenAPI spec. If new values are needed (e.g., "direct-connect"), consider extending the enum rather than accepting any string, so invalid values are rejected at the API boundary rather than silently forwarded to Convex.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
apps/www/lib/routes/mobile-workspace-read.route.ts (2)
41-53: UnusedaccessTokenparameter in defaultverifyTeam.The function signature accepts
accessTokenbut the default implementation doesn't use it—verifyTeamAccessextracts the token from the request instead. This is fine functionally but slightly misleading.Optional: simplify to match actual usage
const verifyTeam = options?.verifyTeam ?? (async ({ req, teamSlugOrId, }: { req: Request; - accessToken: string; teamSlugOrId: string; }) => {Note: This would require updating the type signature at lines 19-23 as well if you want full consistency.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/www/lib/routes/mobile-workspace-read.route.ts` around lines 41 - 53, The default verifyTeam wrapper currently declares an unused accessToken parameter; update the async function signature to remove accessToken from both the parameter list and its type annotation so it matches actual usage (the implementation uses req and teamSlugOrId only). Locate the wrapper that imports verifyTeamAccess (the async block referencing verifyTeamAccess) and also update the related type definition around the earlier signature (the types at the lines that currently list req, accessToken, teamSlugOrId) to drop accessToken for consistency.
81-85: Misleading field name:teamSlugOrIdalways receives a UUID.
args.teamIdcontainsteam.uuid(from line 136), but it's sent asteamSlugOrId. While this works (the Convex mutation handles both), the naming is confusing. Consider either:
- Renaming to just
teamIdif the Convex endpoint accepts UUIDs directly, or- Adding a comment clarifying the intent.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/www/lib/routes/mobile-workspace-read.route.ts` around lines 81 - 85, The request body field name is misleading: sending args.teamId (which is a UUID) as teamSlugOrId creates confusion; update the payload to use a clear name by replacing teamSlugOrId with teamId in the JSON body (the JSON.stringify call that builds the body) so the Convex mutation receives teamId, or alternatively add a one-line comment next to the body construction explaining that args.teamId is a UUID and is intentionally mapped to teamSlugOrId for compatibility with the Convex endpoint; reference the args.teamId symbol and the JSON.stringify(...) body in mobile-workspace-read.route.ts to locate the change.apps/www/lib/routes/mobile-analytics.route.ts (1)
6-6: Consider usingz.unknown()instead ofz.any()for stricter type safety.While
z.any()works for flexible analytics properties,z.unknown()provides better type safety by requiring explicit type narrowing when accessing values.Optional improvement
- properties: z.record(z.string(), z.any()).optional(), + properties: z.record(z.string(), z.unknown()).optional(),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/www/lib/routes/mobile-analytics.route.ts` at line 6, Replace the liberal use of z.any() in the route schema with z.unknown() to enforce stricter type safety: change the schema entry "properties: z.record(z.string(), z.any()).optional()" to use z.unknown() for value types and then update any downstream code that reads from the "properties" record (where it's consumed/parsed in mobile-analytics.route.ts) to explicitly narrow/validate values (e.g., with Zod parsing, type guards, or specific schemas) before accessing them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/www/lib/routes/mobile-heartbeat.route.ts`:
- Line 29: The route schema currently relaxes status to z.string() (see status
in mobile-heartbeat.route.ts) which can pass invalid values into the Convex
handler; change status to the strict literal union ("online" | "offline" |
"unknown") so it matches the Convex HTTP handler expectation in
packages/convex/convex/mobile_http.ts, and either remove the unused
directConnect field from the route schema or update the Convex handler
(mobile_http.ts) to extract and forward directConnect alongside other payload
fields so it is persisted rather than silently dropped.
- Around line 17-21: The Convex HTTP handler is not extracting or forwarding the
directConnect payload (HeartbeatDirectConnect/directPort, directTlsPins,
ticketSecret), so those fields are dropped; update the handler in
packages/convex/convex/mobile_http.ts to accept and validate a directConnect
object (using HeartbeatDirectConnect shape), include it in the request-body
type/validation and pass it through to the existing Convex mutation(s) that
persist heartbeats (the mutation you call to store heartbeat data), or
alternatively remove directConnect from the client schema if you choose to not
persist it yet; ensure you reference HeartbeatDirectConnect and the
directConnect property names when modifying the handler and mutation call so the
fields are forwarded and stored.
---
Nitpick comments:
In `@apps/www/lib/routes/mobile-analytics.route.ts`:
- Line 6: Replace the liberal use of z.any() in the route schema with
z.unknown() to enforce stricter type safety: change the schema entry
"properties: z.record(z.string(), z.any()).optional()" to use z.unknown() for
value types and then update any downstream code that reads from the "properties"
record (where it's consumed/parsed in mobile-analytics.route.ts) to explicitly
narrow/validate values (e.g., with Zod parsing, type guards, or specific
schemas) before accessing them.
In `@apps/www/lib/routes/mobile-workspace-read.route.ts`:
- Around line 41-53: The default verifyTeam wrapper currently declares an unused
accessToken parameter; update the async function signature to remove accessToken
from both the parameter list and its type annotation so it matches actual usage
(the implementation uses req and teamSlugOrId only). Locate the wrapper that
imports verifyTeamAccess (the async block referencing verifyTeamAccess) and also
update the related type definition around the earlier signature (the types at
the lines that currently list req, accessToken, teamSlugOrId) to drop
accessToken for consistency.
- Around line 81-85: The request body field name is misleading: sending
args.teamId (which is a UUID) as teamSlugOrId creates confusion; update the
payload to use a clear name by replacing teamSlugOrId with teamId in the JSON
body (the JSON.stringify call that builds the body) so the Convex mutation
receives teamId, or alternatively add a one-line comment next to the body
construction explaining that args.teamId is a UUID and is intentionally mapped
to teamSlugOrId for compatibility with the Convex endpoint; reference the
args.teamId symbol and the JSON.stringify(...) body in
mobile-workspace-read.route.ts to locate the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 656b4d75-c10c-4edf-9c20-907b1a1b1c73
📒 Files selected for processing (9)
apps/www/lib/hono-app.tsapps/www/lib/routes/index.tsapps/www/lib/routes/mobile-analytics.route.test.tsapps/www/lib/routes/mobile-analytics.route.tsapps/www/lib/routes/mobile-heartbeat.route.tsapps/www/lib/routes/mobile-push.route.test.tsapps/www/lib/routes/mobile-push.route.tsapps/www/lib/routes/mobile-workspace-read.route.test.tsapps/www/lib/routes/mobile-workspace-read.route.ts
| const HeartbeatDirectConnect = z.object({ | ||
| directPort: z.number(), | ||
| directTlsPins: z.array(z.string()), | ||
| ticketSecret: z.string(), | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether directConnect is handled in Convex HTTP handler
rg -n "directConnect|directPort|directTlsPins|ticketSecret" packages/convex/Repository: manaflow-ai/manaflow
Length of output: 46
🏁 Script executed:
#!/bin/bash
# Examine the schema file at apps/www/lib/routes/mobile-heartbeat.route.ts
echo "=== Checking schema file ==="
cat -n apps/www/lib/routes/mobile-heartbeat.route.ts | head -70Repository: manaflow-ai/manaflow
Length of output: 2709
🏁 Script executed:
#!/bin/bash
# Check if the Convex mobile_http.ts file exists and examine it
echo "=== Checking Convex handler ==="
if [ -f "packages/convex/convex/mobile_http.ts" ]; then
cat -n "packages/convex/convex/mobile_http.ts" | head -80
else
# Try to find mobile_http file
find packages/convex -name "*mobile*" -type f 2>/dev/null
fiRepository: manaflow-ai/manaflow
Length of output: 2138
🏁 Script executed:
#!/bin/bash
# Search for where the heartbeat data is being used in the codebase
echo "=== Searching for heartbeat references ==="
rg -n "HeartbeatDirectConnect" --type ts --type tsx -iRepository: manaflow-ai/manaflow
Length of output: 151
🏁 Script executed:
#!/bin/bash
# Check the specific lines mentioned in "Also applies to"
echo "=== Checking line 32 (directConnect in HeartbeatBody) ==="
sed -n '30,35p' apps/www/lib/routes/mobile-heartbeat.route.ts
echo "=== Checking line 61 (directConnect in function signature) ==="
sed -n '59,63p' apps/www/lib/routes/mobile-heartbeat.route.ts
echo "=== Checking line 149 ==="
sed -n '147,151p' apps/www/lib/routes/mobile-heartbeat.route.tsRepository: manaflow-ai/manaflow
Length of output: 810
🏁 Script executed:
#!/bin/bash
# Check if directConnect is being sent to Convex in the publishMobileHeartbeatToConvex function
echo "=== Checking how directConnect is passed to Convex ==="
sed -n '69,90p' apps/www/lib/routes/mobile-heartbeat.route.tsRepository: manaflow-ai/manaflow
Length of output: 716
🏁 Script executed:
#!/bin/bash
# Check full context around line 149
echo "=== Context around line 149 ==="
sed -n '140,160p' apps/www/lib/routes/mobile-heartbeat.route.tsRepository: manaflow-ai/manaflow
Length of output: 700
🏁 Script executed:
#!/bin/bash
# Check if the Convex handler actually handles directConnect despite my initial grep
echo "=== Full Convex handler file ==="
wc -l packages/convex/convex/mobile_http.tsRepository: manaflow-ai/manaflow
Length of output: 137
🏁 Script executed:
#!/bin/bash
# Get more context to understand what function is being called around line 149
echo "=== Context for line 149 (function being called) ==="
sed -n '130,160p' apps/www/lib/routes/mobile-heartbeat.route.tsRepository: manaflow-ai/manaflow
Length of output: 1008
🏁 Script executed:
#!/bin/bash
# Check if there's any handling of directConnect in the mutations
echo "=== Check mobileMachines.upsertHeartbeatInternal signature ==="
rg -n "upsertHeartbeatInternal" packages/convex/ -A 10Repository: manaflow-ai/manaflow
Length of output: 1788
🏁 Script executed:
#!/bin/bash
# Check the actual mutation definition to see if it accepts directConnect
echo "=== Check full upsertHeartbeatInternal mutation ==="
sed -n '54,90p' packages/convex/convex/mobileMachines.tsRepository: manaflow-ai/manaflow
Length of output: 1190
🏁 Script executed:
#!/bin/bash
# Check the schema of mobileMachine to see if it has directConnect fields
echo "=== Check mobileMachine schema ==="
rg -n "directConnect|directPort|directTlsPins|ticketSecret" packages/convex/convex/mobileMachines.tsRepository: manaflow-ai/manaflow
Length of output: 98
🏁 Script executed:
#!/bin/bash
# Verify the complete data flow - check what the Convex HTTP handler actually receives vs what it uses
echo "=== Verify Convex handler receives directConnect ==="
sed -n '4,45p' packages/convex/convex/mobile_http.tsRepository: manaflow-ai/manaflow
Length of output: 1298
🏁 Script executed:
#!/bin/bash
# Check if there are any other handlers or recent updates
echo "=== Check all HTTP handlers in Convex ==="
find packages/convex -name "*http*" -type fRepository: manaflow-ai/manaflow
Length of output: 768
directConnect fields are silently dropped by the Convex backend.
The client sends directConnect (with directPort, directTlsPins, ticketSecret) in the request body to the Convex HTTP endpoint, but the handler at packages/convex/convex/mobile_http.ts does not extract these fields from the request. The type annotation for the request body (lines 9-31) excludes directConnect, and the subsequent mutations (lines 33-43, 45-52) never receive or persist these fields. The data is lost.
Either:
- Update the Convex HTTP handler to extract and forward
directConnectfields to a mutation, or - Remove these fields from the schema until the backend is ready.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/www/lib/routes/mobile-heartbeat.route.ts` around lines 17 - 21, The
Convex HTTP handler is not extracting or forwarding the directConnect payload
(HeartbeatDirectConnect/directPort, directTlsPins, ticketSecret), so those
fields are dropped; update the handler in packages/convex/convex/mobile_http.ts
to accept and validate a directConnect object (using HeartbeatDirectConnect
shape), include it in the request-body type/validation and pass it through to
the existing Convex mutation(s) that persist heartbeats (the mutation you call
to store heartbeat data), or alternatively remove directConnect from the client
schema if you choose to not persist it yet; ensure you reference
HeartbeatDirectConnect and the directConnect property names when modifying the
handler and mutation call so the fields are forwarded and stored.
| tailscaleHostname: z.string().optional(), | ||
| tailscaleIPs: z.array(z.string()), | ||
| status: z.enum(["online", "offline", "unknown"]), | ||
| status: z.string(), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify the Convex mutation's status validator
rg -n "status:" packages/convex/convex/mobileMachines.ts -A5Repository: manaflow-ai/manaflow
Length of output: 599
🏁 Script executed:
#!/bin/bash
# Check the mobile-heartbeat.route.ts file around line 29 and relevant schema definitions
rg -n "status.*z\." apps/www/lib/routes/mobile-heartbeat.route.ts -B2 -A2Repository: manaflow-ai/manaflow
Length of output: 274
🏁 Script executed:
#!/bin/bash
# Check if directConnect is in the HeartbeatBody schema and how it's used
rg -n "directConnect\|HeartbeatBody" apps/www/lib/routes/mobile-heartbeat.route.ts -B1 -A1Repository: manaflow-ai/manaflow
Length of output: 46
🏁 Script executed:
#!/bin/bash
# Check the Convex HTTP handler to see if directConnect fields are extracted
rg -n "directConnect\|mobile_http" packages/convex/convex/mobile_http.ts -B2 -A2Repository: manaflow-ai/manaflow
Length of output: 46
🏁 Script executed:
#!/bin/bash
# Get the full schema definition around line 29
sed -n '15,50p' apps/www/lib/routes/mobile-heartbeat.route.tsRepository: manaflow-ai/manaflow
Length of output: 1065
🏁 Script executed:
#!/bin/bash
# Search for HeartbeatDirectConnect or directConnect more broadly
rg -i "directconnect|directport|directtls" apps/www/lib/routes/mobile-heartbeat.route.tsRepository: manaflow-ai/manaflow
Length of output: 324
🏁 Script executed:
#!/bin/bash
# Check what Convex expects in the mobile_http handler
cat packages/convex/convex/mobile_http.tsRepository: manaflow-ai/manaflow
Length of output: 1693
Relaxing status to z.string() will cause runtime errors downstream.
The Convex HTTP handler (packages/convex/convex/mobile_http.ts) expects a strict literal union: status: "online" | "offline" | "unknown". Your route will accept any string value, but non-enum statuses will fail at the Convex layer.
Additionally, the directConnect field accepted by your route schema is never extracted by the HTTP handler and will be silently dropped without being persisted.
Suggested fix
- status: z.string(),
+ status: z.enum(["online", "offline", "unknown"]),To preserve the directConnect data, add extraction and forwarding in the Convex HTTP handler, or remove it from the route schema if it's not needed.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| status: z.string(), | |
| status: z.enum(["online", "offline", "unknown"]), |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/www/lib/routes/mobile-heartbeat.route.ts` at line 29, The route schema
currently relaxes status to z.string() (see status in mobile-heartbeat.route.ts)
which can pass invalid values into the Convex handler; change status to the
strict literal union ("online" | "offline" | "unknown") so it matches the Convex
HTTP handler expectation in packages/convex/convex/mobile_http.ts, and either
remove the unused directConnect field from the route schema or update the Convex
handler (mobile_http.ts) to extract and forward directConnect alongside other
payload fields so it is persisted rather than silently dropped.
There was a problem hiding this comment.
6 issues found across 9 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/www/lib/routes/mobile-push.route.ts">
<violation number="1" location="apps/www/lib/routes/mobile-push.route.ts:80">
P1: Do not log push token material (even partial prefixes); this leaks sensitive device credentials into logs.</violation>
<violation number="2" location="apps/www/lib/routes/mobile-push.route.ts:95">
P2: Avoid logging full push message content from request payloads; log metadata only.</violation>
</file>
<file name="apps/www/lib/routes/mobile-workspace-read.route.ts">
<violation number="1" location="apps/www/lib/routes/mobile-workspace-read.route.ts:52">
P2: Pass `accessToken` to `verifyTeamAccess` in the default `verifyTeam` implementation; currently the token is resolved and passed into `verifyTeam` but then dropped.</violation>
<violation number="2" location="apps/www/lib/routes/mobile-workspace-read.route.ts:74">
P1: The default `markRead` implementation POSTs to `/api/mobile/workspaces/mark-read` on the Convex site, but the Convex HTTP router (`packages/convex/convex/http.ts`) only registers `/api/mobile/heartbeat`. This will return 404 in production, and since the response is checked with `if (!response.ok)` and throws, every real call to this endpoint will result in a 500. Either register this route in the Convex HTTP router or update the path to match an existing handler.</violation>
</file>
<file name="apps/www/lib/routes/mobile-analytics.route.ts">
<violation number="1" location="apps/www/lib/routes/mobile-analytics.route.ts:39">
P2: Avoid logging raw analytics `properties`; they can contain sensitive user data and should not be emitted verbatim to server logs.</violation>
</file>
<file name="apps/www/lib/routes/mobile-heartbeat.route.ts">
<violation number="1" location="apps/www/lib/routes/mobile-heartbeat.route.ts:29">
P1: Relaxing `status` to `z.string()` removes API-boundary validation while the downstream Convex mutation (`upsertHeartbeatInternal`) still enforces `"online" | "offline" | "unknown"`. Invalid values now pass Zod validation here but fail at the mutation layer, turning a client input error into a runtime 500. Restore the enum (extending it if new values are needed) so invalid statuses are rejected early.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| platform: args.platform, | ||
| bundleId: args.bundleId, | ||
| deviceId: args.deviceId, | ||
| tokenPrefix: args.token.slice(0, 8), |
There was a problem hiding this comment.
P1: Do not log push token material (even partial prefixes); this leaks sensitive device credentials into logs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/www/lib/routes/mobile-push.route.ts, line 80:
<comment>Do not log push token material (even partial prefixes); this leaks sensitive device credentials into logs.</comment>
<file context>
@@ -0,0 +1,243 @@
+ platform: args.platform,
+ bundleId: args.bundleId,
+ deviceId: args.deviceId,
+ tokenPrefix: args.token.slice(0, 8),
+ });
+ });
</file context>
| tokenPrefix: args.token.slice(0, 8), | |
| tokenLength: args.token.length, |
| .replace(/\/$/, ""); | ||
| // Calls Convex HTTP endpoint to mark the workspace as read. | ||
| const response = await fetch( | ||
| `${endpoint}/api/mobile/workspaces/mark-read`, |
There was a problem hiding this comment.
P1: The default markRead implementation POSTs to /api/mobile/workspaces/mark-read on the Convex site, but the Convex HTTP router (packages/convex/convex/http.ts) only registers /api/mobile/heartbeat. This will return 404 in production, and since the response is checked with if (!response.ok) and throws, every real call to this endpoint will result in a 500. Either register this route in the Convex HTTP router or update the path to match an existing handler.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/www/lib/routes/mobile-workspace-read.route.ts, line 74:
<comment>The default `markRead` implementation POSTs to `/api/mobile/workspaces/mark-read` on the Convex site, but the Convex HTTP router (`packages/convex/convex/http.ts`) only registers `/api/mobile/heartbeat`. This will return 404 in production, and since the response is checked with `if (!response.ok)` and throws, every real call to this endpoint will result in a 500. Either register this route in the Convex HTTP router or update the path to match an existing handler.</comment>
<file context>
@@ -0,0 +1,148 @@
+ .replace(/\/$/, "");
+ // Calls Convex HTTP endpoint to mark the workspace as read.
+ const response = await fetch(
+ `${endpoint}/api/mobile/workspaces/mark-read`,
+ {
+ method: "POST",
</file context>
| tailscaleHostname: z.string().optional(), | ||
| tailscaleIPs: z.array(z.string()), | ||
| status: z.enum(["online", "offline", "unknown"]), | ||
| status: z.string(), |
There was a problem hiding this comment.
P1: Relaxing status to z.string() removes API-boundary validation while the downstream Convex mutation (upsertHeartbeatInternal) still enforces "online" | "offline" | "unknown". Invalid values now pass Zod validation here but fail at the mutation layer, turning a client input error into a runtime 500. Restore the enum (extending it if new values are needed) so invalid statuses are rejected early.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/www/lib/routes/mobile-heartbeat.route.ts, line 29:
<comment>Relaxing `status` to `z.string()` removes API-boundary validation while the downstream Convex mutation (`upsertHeartbeatInternal`) still enforces `"online" | "offline" | "unknown"`. Invalid values now pass Zod validation here but fail at the mutation layer, turning a client input error into a runtime 500. Restore the enum (extending it if new values are needed) so invalid statuses are rejected early.</comment>
<file context>
@@ -14,15 +14,22 @@ const HeartbeatWorkspace = z.object({
tailscaleHostname: z.string().optional(),
tailscaleIPs: z.array(z.string()),
- status: z.enum(["online", "offline", "unknown"]),
+ status: z.string(),
lastSeenAt: z.number().optional(),
lastWorkspaceSyncAt: z.number().optional(),
</file context>
| status: z.string(), | |
| status: z.enum(["online", "offline", "unknown"]), |
| (async (args: { title: string; body: string }) => { | ||
| console.log("mobile-push: test push requested", { | ||
| title: args.title, | ||
| body: args.body, |
There was a problem hiding this comment.
P2: Avoid logging full push message content from request payloads; log metadata only.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/www/lib/routes/mobile-push.route.ts, line 95:
<comment>Avoid logging full push message content from request payloads; log metadata only.</comment>
<file context>
@@ -0,0 +1,243 @@
+ (async (args: { title: string; body: string }) => {
+ console.log("mobile-push: test push requested", {
+ title: args.title,
+ body: args.body,
+ });
+ return 0;
</file context>
| body: args.body, | |
| bodyLength: args.body.length, |
| const { verifyTeamAccess } = await import( | ||
| "@/lib/utils/team-verification" | ||
| ); | ||
| return await verifyTeamAccess({ req, teamSlugOrId }); |
There was a problem hiding this comment.
P2: Pass accessToken to verifyTeamAccess in the default verifyTeam implementation; currently the token is resolved and passed into verifyTeam but then dropped.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/www/lib/routes/mobile-workspace-read.route.ts, line 52:
<comment>Pass `accessToken` to `verifyTeamAccess` in the default `verifyTeam` implementation; currently the token is resolved and passed into `verifyTeam` but then dropped.</comment>
<file context>
@@ -0,0 +1,148 @@
+ const { verifyTeamAccess } = await import(
+ "@/lib/utils/team-verification"
+ );
+ return await verifyTeamAccess({ req, teamSlugOrId });
+ });
+ const markRead =
</file context>
| properties?: Record<string, unknown>; | ||
| }) => { | ||
| // Stub: log and accept. Forward to PostHog later. | ||
| console.log("mobile-analytics:", args.event, args.properties ?? {}); |
There was a problem hiding this comment.
P2: Avoid logging raw analytics properties; they can contain sensitive user data and should not be emitted verbatim to server logs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/www/lib/routes/mobile-analytics.route.ts, line 39:
<comment>Avoid logging raw analytics `properties`; they can contain sensitive user data and should not be emitted verbatim to server logs.</comment>
<file context>
@@ -0,0 +1,90 @@
+ properties?: Record<string, unknown>;
+ }) => {
+ // Stub: log and accept. Forward to PostHog later.
+ console.log("mobile-analytics:", args.event, args.properties ?? {});
+ });
+
</file context>
Summary
directConnect(for direct-connect TLS info from macOS publisher) and relaxstatusfrom enum to stringNew endpoints:
POST /api/mobile/workspaces/mark-read(Stack Auth bearer, calls Convex HTTP)POST /api/mobile/push/register(stub, logs token)POST /api/mobile/push/remove(stub, logs removal)POST /api/mobile/push/test(stub, returnsscheduledCount: 0)POST /api/mobile/analytics(stub, logs event)Push and analytics routes are stubs that accept and log. Full implementation (APNs, PostHog forwarding) will come later.
Test plan
bunx vitest run apps/www/lib/routes/mobile-*.test.ts)bun checkpasses (lint + typecheck)Summary by cubic
Add mobile sync API routes for workspace read-state, push notifications, and analytics. Also update the heartbeat API to accept direct-connect TLS info and a flexible status string.
/api/mobile/workspaces/mark-read(Stack Auth bearer, forwards to Convex HTTP)./api/mobile/push/{register|remove|test}(stubs; register/remove log; test returnsscheduledCount: 0)./api/mobile/analytics(stub; logs event).directConnect(port, TLS pins, ticket) andstatusis now a string.Written for commit 4548e9b. Summary will update on new commits.
Summary by CodeRabbit
New Features
Tests