Skip to content

Replace all z.any() output schemas with typed Zod schemas#402

Open
ng wants to merge 4 commits intodevfrom
fix/329-typed-output-schemas
Open

Replace all z.any() output schemas with typed Zod schemas#402
ng wants to merge 4 commits intodevfrom
fix/329-typed-output-schemas

Conversation

@ng
Copy link
Copy Markdown
Contributor

@ng ng commented Apr 10, 2026

Summary

  • Replaced all 47 z.any() output schemas across 11 tRPC routers with proper typed Zod schemas
  • Fixed client-side type errors uncovered by the new schemas:
    • WaterLevelCard/WaterModal: corrected field name mismatches (directiontrend, levelPercentlevel, alertTypetype) — these were bugs hidden by z.any()
    • BedTempMatrix: removed incorrect string cast on Date timestamp
    • CurveEditor: fixed Promise array type inference
    • useSchedules: aligned local interfaces with API types
    • health: JobType is string enum, not number
    • device: added optional isAlarmVibrating to SideStatus for WS/HTTP union compatibility

Closes #329

Test plan

  • TypeScript compilation passes (full project)
  • ESLint passes
  • Drizzle schema validation passes
  • All 16 router tests pass
  • Manual: verify OpenAPI spec now shows typed schemas instead of {}
  • Manual: verify mobile client decode still works

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved water level status display, now showing clear categorical indicators (OK/Low) instead of percentages
    • Refined water level trend reporting with more intuitive stability, declining, and rising states
  • Chores

    • Strengthened data validation and API response schemas for improved reliability across all endpoints

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

📝 Walkthrough

Walkthrough

This PR systematically replaces unconstrained z.any() output schemas across TRPC routers with explicit, typed Zod schemas, enabling runtime validation and proper TypeScript typing. Additionally, minor type refinements are applied to component files and schedule interfaces.

Changes

Cohort / File(s) Summary
Component Type Fixes
src/components/Schedule/CurveEditor.tsx, src/components/Sensors/BedTempMatrix.tsx
Removed explicit double type-casting on mutation promises; simplified timestamp conversion to use getTime() directly instead of string-casting first.
Water Status Component Updates
src/components/status/WaterLevelCard.tsx, src/components/status/WaterModal.tsx
Updated trendIcon helper to interpret trend field values (declining/rising/stable/unknown) instead of direction values. Replaced percent-based level display with categorical status (OK or Low). Updated trend summary UI to render distinct messages per trend state using trend.trend, trend.lowPercent fields.
Schedule Type Refinements
src/hooks/useSchedules.ts
Narrowed TemperatureSchedule createdAt/updatedAt from `string
TRPC Biometrics Router Schema
src/server/routers/biometrics.ts
Replaced z.any() with explicit typed outputs: getSleepRecords returns array of sleep-record objects; getVitals/getMovement return arrays of measurement objects; getLatestSleep returns nullable sleep-record object; getVitalsSummary returns nullable aggregated metrics; updateSleepRecord returns typed sleep-record object.
TRPC Calibration Router Schema
src/server/routers/calibration.ts
Replaced z.any() outputs: getHistory returns array with constrained enums for side/sensorType/status/triggeredBy and typed timing/scoring fields; getVitalsQuality returns array with typed fields including constrained side and nullable hrRaw.
TRPC Device Router Schema
src/server/routers/device.ts
Updated getStatus response from z.any() to object with leftSide/rightSide temperature/level, waterLevel, isPriming, podVersion, sensorLabel, optional gestures/primeCompletedNotification, and snooze state. Updated execute response to typed object with command, nullable args, response (z.unknown()), and disclaimer.
TRPC Environment Router Schema
src/server/routers/environment.ts
Replaced z.any() with explicit typed outputs across 9 procedures: getBedTemp/getLatestBedTemp with bed-sensor fields; getFreezerTemp/getLatestFreezerTemp with freezer-sensor fields; getSummary with nested bedTemp/freezerTemp sub-objects; getAmbientLight/getLatestAmbientLight with lux fields; getAmbientLightSummary with summary metrics.
TRPC Health Router Schema
src/server/routers/health.ts
Replaced z.any() with explicit typed outputs: scheduler returns enabled, typed jobCounts, upcomingJobs, healthy; system returns status, timestamp, typed database/scheduler/iptables objects; dacMonitor returns status, nullable podVersion, gesturesSupported; hardware returns status, socketPath, latencyMs, optional error.
TRPC Raw Router Schema
src/server/routers/raw.ts
Updated files output to typed array of { name, sizeBytes, modifiedAt }; updated diskUsage output to typed object with totalBytes, usedBytes, availableBytes, rawFileCount, rawBytes (all numbers).
TRPC RunOnce Router Schema
src/server/routers/runOnce.ts
Updated getActive output from z.any() to typed object with id, side, setPoints, wakeTime, startedAt (number), expiresAt (number), status (enum: active/completed/cancelled), nullable.
TRPC ScheduleGroups Router Schema
src/server/routers/scheduleGroups.ts
Replaced z.any() outputs with typed schedule-group objects: getAll returns array; create/update return single object; getByDay returns nullable object. All include id, side, name, days, createdAt, updatedAt.
TRPC Schedules Router Schema
src/server/routers/schedules.ts
Replaced z.any() with new Zod object definitions: getAll/getByDay return schedulesCollectionOutput (array); create/update procedures return respective single-item outputs (temperatureScheduleOutput/powerScheduleOutput/alarmScheduleOutput).
TRPC Settings Router Schema
src/server/routers/settings.ts
Replaced z.any() with explicit typed outputs: getAll returns object with device, sides (left/right), and gestures; updateDevice/updateSide/setAlwaysOn return their respective fully-typed objects with timestamps and settings fields.
TRPC WaterLevel Router Schema
src/server/routers/waterLevel.ts
Replaced z.any() with explicit typed outputs: getHistory/getLatest with { id, timestamp, level: 'low'|'ok' }; getTrend with { totalReadings, okPercent, lowPercent, trend enum }; getAlerts array with type enum; getFlowReadings/getLatestFlowReading with nullable flow/pump fields.
Test Updates
src/server/routers/tests/scheduleGroups.test.ts, src/server/routers/tests/schedules.test.ts
Added explicit runtime null checks in assertions; removed inline type annotations in test callbacks to rely on type inference.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • #193 — Direct overlap on src/server/routers/waterLevel.ts and water-level UI components (WaterLevelCard, WaterModal); both modify water feature data shapes and schemas.
  • #271 — Introduces device.execute endpoint and flow-reading APIs whose response shapes are being formalized with explicit schemas in this PR.
  • #400 — Modifies the same schedules.ts procedures (getAll/getByDay) that are having their output schemas tightened here.

Suggested labels

released

Poem

🐰 Hops with glee
No more any, hooray! At last we see,
Zod schemas typed, validation free,
Types now dance with clarity divine—
Each response shape, no longer undefined! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Replace all z.any() output schemas with typed Zod schemas' clearly and concisely summarizes the main objective of the changeset, directly reflecting the primary refactoring effort across 11 tRPC routers.
Linked Issues check ✅ Passed The pull request fully addresses issue #329 objectives: replaced 47 z.any() occurrences across 11 routers with typed Zod schemas, fixed client-side type errors, and improved OpenAPI specifications with concrete response types.
Out of Scope Changes check ✅ Passed All changes directly support the stated objective of replacing z.any() schemas with typed schemas; fixes to affected client code (WaterLevel, BedTempMatrix, CurveEditor, useSchedules, tests) are necessary consequences of the type-safety improvements and remain in scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/329-typed-output-schemas

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

ng and others added 4 commits April 10, 2026 15:22
47 tRPC procedures across 11 routers now have proper typed output
schemas instead of z.any(). This enables runtime response validation,
typed OpenAPI specs, and TypeScript client inference.

Closes #329

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- health.ts: JobType is string enum, not number
- settings.ts: use sideSchema instead of z.literal for db select results
- BedTempMatrix: timestamp is Date, remove string cast
- CurveEditor: type createPromises as Promise<unknown>[]
- WaterLevelCard/WaterModal: fix field name mismatches (direction→trend,
  levelPercent→level, changePercent→lowPercent, alertType→type)
- Tests: add null guards for nullable getByDay/find results

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- device.ts: add optional isAlarmVibrating to SideStatus output
  (present in WS stream, absent in HTTP — union type needs it)
- useSchedules.ts: align local interfaces with typed API output
  (createdAt/updatedAt as Date, add missing fields)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ng ng force-pushed the fix/329-typed-output-schemas branch from 6c4aee8 to 825ba64 Compare April 10, 2026 22:24
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/server/routers/settings.ts (1)

53-124: Consider extracting shared settings payload schemas.

The device and side response shapes are now duplicated across several procedures, so the next settings-field change will be easy to miss in one endpoint. Pulling them into named shared payload schemas would keep these contracts in sync.

Also applies to: 189-204, 300-311, 424-435

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/routers/settings.ts` around lines 53 - 124, The inline Zod objects
for device, sides (left/right), and gesture items are duplicated; extract them
into shared named schemas (e.g., deviceSettingsSchema, sideSettingsSchema,
gestureItemSchema or gesturesSchema) and reuse them instead of repeating the
same object literal in the .output call and the other repeated blocks referenced
(the other occurrences around 189-204, 300-311, 424-435). Ensure the extracted
schemas reference existing primitives like temperatureUnitSchema, sideSchema,
and tapTypeSchema, then replace the inline z.object(...) definitions for device,
sides.left/right, and gestures.left/right with the new named schemas so all
endpoints share a single source of truth.
src/server/routers/environment.ts (1)

31-43: Extract shared bed/freezer output schemas to avoid drift.

The same object shapes are repeated across list/latest procedures. Centralizing them will reduce maintenance risk.

♻️ Refactor sketch
+const bedTempOutputSchema = z.object({
+  id: z.number(),
+  timestamp: z.date(),
+  ambientTemp: z.number().nullable(),
+  mcuTemp: z.number().nullable(),
+  humidity: z.number().nullable(),
+  leftOuterTemp: z.number().nullable(),
+  leftCenterTemp: z.number().nullable(),
+  leftInnerTemp: z.number().nullable(),
+  rightOuterTemp: z.number().nullable(),
+  rightCenterTemp: z.number().nullable(),
+  rightInnerTemp: z.number().nullable(),
+})
+
+const freezerTempOutputSchema = z.object({
+  id: z.number(),
+  timestamp: z.date(),
+  ambientTemp: z.number().nullable(),
+  heatsinkTemp: z.number().nullable(),
+  leftWaterTemp: z.number().nullable(),
+  rightWaterTemp: z.number().nullable(),
+})
...
-.output(z.array(z.object({ ... })))
+.output(z.array(bedTempOutputSchema))
...
-.output(z.object({ ... }).nullable())
+.output(bedTempOutputSchema.nullable())
...
-.output(z.array(z.object({ ... })))
+.output(z.array(freezerTempOutputSchema))
...
-.output(z.object({ ... }).nullable())
+.output(freezerTempOutputSchema.nullable())

Also applies to: 87-94, 133-145, 182-189

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/routers/environment.ts` around lines 31 - 43, Extract the repeated
object shape into a shared Zod schema (e.g., const EnvironmentReadingSchema =
z.object({...})) and replace the inline object literal in all .output(...) calls
with EnvironmentReadingSchema (use z.array(EnvironmentReadingSchema) where
arrays are expected and EnvironmentReadingSchema for single-object outputs).
Update the usages in the environment router procedures (the list/latest outputs
shown and the other occurrences around the file) to reference this constant so
the schema is centralized and reused.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/server/routers/biometrics.ts`:
- Around line 58-68: The sleep-record output schemas in
src/server/routers/biometrics.ts declare leftBedAt as z.date() but active
sessions allow null; update the schema(s) that define leftBedAt (the
array/object outputs around the .output(...) that include id, side,
enteredBedAt, leftBedAt, sleepDurationSeconds, etc.) to make leftBedAt nullable
(use z.date().nullable()) so runtime validation accepts active records; apply
the same change to the other occurrences of the sleep-record output schema
patterns (the similar blocks around the other two occurrences referenced).

In `@src/server/routers/health.ts`:
- Around line 36-54: The health "healthy" field is computed incorrectly because
jobCounts.total is derived from jobs.length, making the existing condition
always true; update the logic that sets the healthy field (the "healthy"
property generation around jobs and jobCounts.total) to treat the
scheduler-enabled-but-empty case as unhealthy by computing healthy as: when
enabled is true, healthy is true only if jobCounts.total > 0 (or jobs.length >
0); when enabled is false, healthy should remain true — i.e., replace the
current jobs.length > 0 || jobCounts.total === 0 expression with an expression
equivalent to enabled ? jobCounts.total > 0 : true so the
scheduler-enabled-but-empty scenario yields healthy = false.

In `@src/server/routers/runOnce.ts`:
- Around line 128-136: The output schema currently exposes setPoints as
z.unknown(), allowing arbitrary JSON; change the output definition to validate
setPoints against the existing setPointSchema array (use z.array(setPointSchema)
instead of z.unknown()) and, in the retrieval logic that returns this route's
payload (the code that constructs the object matching this output), run a safe
parse (e.g., setPointsSchemaArray.safeParse or parse with try/catch) and return
a typed fallback (an empty array or a well-defined default) when parsing fails
so malformed persisted payloads cannot leak through; update references to
setPointSchema and the output z.object(...) to enforce this contract.

---

Nitpick comments:
In `@src/server/routers/environment.ts`:
- Around line 31-43: Extract the repeated object shape into a shared Zod schema
(e.g., const EnvironmentReadingSchema = z.object({...})) and replace the inline
object literal in all .output(...) calls with EnvironmentReadingSchema (use
z.array(EnvironmentReadingSchema) where arrays are expected and
EnvironmentReadingSchema for single-object outputs). Update the usages in the
environment router procedures (the list/latest outputs shown and the other
occurrences around the file) to reference this constant so the schema is
centralized and reused.

In `@src/server/routers/settings.ts`:
- Around line 53-124: The inline Zod objects for device, sides (left/right), and
gesture items are duplicated; extract them into shared named schemas (e.g.,
deviceSettingsSchema, sideSettingsSchema, gestureItemSchema or gesturesSchema)
and reuse them instead of repeating the same object literal in the .output call
and the other repeated blocks referenced (the other occurrences around 189-204,
300-311, 424-435). Ensure the extracted schemas reference existing primitives
like temperatureUnitSchema, sideSchema, and tapTypeSchema, then replace the
inline z.object(...) definitions for device, sides.left/right, and
gestures.left/right with the new named schemas so all endpoints share a single
source of truth.
🪄 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: 269f9ec4-7983-43ff-ae3e-607617211024

📥 Commits

Reviewing files that changed from the base of the PR and between c2b13f9 and 825ba64.

📒 Files selected for processing (18)
  • src/components/Schedule/CurveEditor.tsx
  • src/components/Sensors/BedTempMatrix.tsx
  • src/components/status/WaterLevelCard.tsx
  • src/components/status/WaterModal.tsx
  • src/hooks/useSchedules.ts
  • src/server/routers/biometrics.ts
  • src/server/routers/calibration.ts
  • src/server/routers/device.ts
  • src/server/routers/environment.ts
  • src/server/routers/health.ts
  • src/server/routers/raw.ts
  • src/server/routers/runOnce.ts
  • src/server/routers/scheduleGroups.ts
  • src/server/routers/schedules.ts
  • src/server/routers/settings.ts
  • src/server/routers/tests/scheduleGroups.test.ts
  • src/server/routers/tests/schedules.test.ts
  • src/server/routers/waterLevel.ts

Comment on lines +58 to +68
.output(z.array(z.object({
id: z.number(),
side: sideSchema,
enteredBedAt: z.date(),
leftBedAt: z.date(),
sleepDurationSeconds: z.number(),
timesExitedBed: z.number(),
presentIntervals: z.unknown(),
notPresentIntervals: z.unknown(),
createdAt: z.date(),
})))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Allow leftBedAt to be nullable in sleep-record output schemas.

leftBedAt is treated as nullable for active sessions in this router (see Line 745 and Line 755), but these outputs require a non-null z.date(). That can fail runtime output validation for active records.

💡 Suggested fix
 .output(z.array(z.object({
   id: z.number(),
   side: sideSchema,
   enteredBedAt: z.date(),
-  leftBedAt: z.date(),
+  leftBedAt: z.date().nullable(),
   sleepDurationSeconds: z.number(),
   timesExitedBed: z.number(),
   presentIntervals: z.unknown(),
   notPresentIntervals: z.unknown(),
   createdAt: z.date(),
 })))
 .output(z.object({
   id: z.number(),
   side: sideSchema,
   enteredBedAt: z.date(),
-  leftBedAt: z.date(),
+  leftBedAt: z.date().nullable(),
   sleepDurationSeconds: z.number(),
   timesExitedBed: z.number(),
   presentIntervals: z.unknown(),
   notPresentIntervals: z.unknown(),
   createdAt: z.date(),
 }).nullable())
 .output(z.object({
   id: z.number(),
   side: sideSchema,
   enteredBedAt: z.date(),
-  leftBedAt: z.date(),
+  leftBedAt: z.date().nullable(),
   sleepDurationSeconds: z.number(),
   timesExitedBed: z.number(),
   presentIntervals: z.unknown(),
   notPresentIntervals: z.unknown(),
   createdAt: z.date(),
 }))

Also applies to: 310-320, 572-582

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/routers/biometrics.ts` around lines 58 - 68, The sleep-record
output schemas in src/server/routers/biometrics.ts declare leftBedAt as z.date()
but active sessions allow null; update the schema(s) that define leftBedAt (the
array/object outputs around the .output(...) that include id, side,
enteredBedAt, leftBedAt, sleepDurationSeconds, etc.) to make leftBedAt nullable
(use z.date().nullable()) so runtime validation accepts active records; apply
the same change to the other occurrences of the sleep-record output schema
patterns (the similar blocks around the other two occurrences referenced).

Comment on lines +36 to +54
.output(z.object({
enabled: z.boolean(),
jobCounts: z.object({
temperature: z.number(),
powerOn: z.number(),
powerOff: z.number(),
alarm: z.number(),
prime: z.number(),
reboot: z.number(),
total: z.number(),
}),
upcomingJobs: z.array(z.object({
id: z.string(),
type: z.string(),
side: z.string().optional(),
nextRun: z.string().nullable(),
})),
healthy: z.boolean(),
}))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

healthy is currently always true.

The implementation for this field uses jobs.length > 0 || jobCounts.total === 0, but jobCounts.total is initialized from jobs.length, so one side of the || is always true. This endpoint will miss the exact “scheduler enabled but empty” failure case the comment describes.

Suggested fix
         return {
           enabled: scheduler.isEnabled(),
           jobCounts,
           upcomingJobs,
-          healthy: jobs.length > 0 || jobCounts.total === 0, // Healthy if has jobs or expected to have none
+          healthy: !scheduler.isEnabled() || jobs.length > 0,
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/routers/health.ts` around lines 36 - 54, The health "healthy"
field is computed incorrectly because jobCounts.total is derived from
jobs.length, making the existing condition always true; update the logic that
sets the healthy field (the "healthy" property generation around jobs and
jobCounts.total) to treat the scheduler-enabled-but-empty case as unhealthy by
computing healthy as: when enabled is true, healthy is true only if
jobCounts.total > 0 (or jobs.length > 0); when enabled is false, healthy should
remain true — i.e., replace the current jobs.length > 0 || jobCounts.total === 0
expression with an expression equivalent to enabled ? jobCounts.total > 0 : true
so the scheduler-enabled-but-empty scenario yields healthy = false.

Comment on lines +128 to +136
.output(z.object({
id: z.number(),
side: z.enum(['left', 'right']),
setPoints: z.unknown(),
wakeTime: z.string(),
startedAt: z.number(),
expiresAt: z.number(),
status: z.enum(['active', 'completed', 'cancelled']),
}).nullable())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "getActive|setPoints" src/server/routers/runOnce.ts -C3
rg -n "runOnce\\.getActive|setPoints\\." --type=ts --type=tsx src -C2

Repository: sleepypod/core

Length of output: 2135


🏁 Script executed:

#!/bin/bash
# Check for setPointSchema definition/import in runOnce.ts
head -50 src/server/routers/runOnce.ts | cat -n

# Search for setPointSchema definition anywhere in the file
rg -n "setPointSchema|export.*setPointSchema" src/server/routers/runOnce.ts

# Check for imports at the top of the file
rg -n "^import" src/server/routers/runOnce.ts | head -20

Repository: sleepypod/core

Length of output: 3047


🏁 Script executed:

#!/bin/bash
# Search for usages of getActive in the codebase
rg -n "\.getActive\(" --type=ts src -C2

# Also check for getActive in tests or other reference patterns
rg -n "getActive" --type=ts src -C1

Repository: sleepypod/core

Length of output: 297


🏁 Script executed:

#!/bin/bash
# Check the complete context around the getActive procedure and its return handling
sed -n '125,170p' src/server/routers/runOnce.ts | cat -n

Repository: sleepypod/core

Length of output: 1744


🏁 Script executed:

#!/bin/bash
# Look for any client or type imports from runOnce router
rg -n "runOnce|from.*runOnce" --type=ts src -C1 | head -40

# Check if there are any test files
fd "runOnce" src --type=ts | grep -i test

Repository: sleepypod/core

Length of output: 2494


🏁 Script executed:

#!/bin/bash
# Look for any trpc client generation or type inference that might reference getActive
rg -n "getActive|inferProcedureOutput" --type=ts src -C2

Repository: sleepypod/core

Length of output: 475


🏁 Script executed:

#!/bin/bash
# Final check: search for any TRPC client calls to runOnce.getActive
rg -n "runOnce\.getActive|\.getActive\(" src --type-list | head -20

Repository: sleepypod/core

Length of output: 447


🏁 Script executed:

#!/bin/bash
# Check the exact return statement one more time to ensure fix applies correctly
sed -n '158,162p' src/server/routers/runOnce.ts | cat -n

Repository: sleepypod/core

Length of output: 214


Validate setPoints shape instead of exposing arbitrary JSON.

Line 131 still allows any structure with z.unknown(), so malformed persisted payloads can bypass the runtime contract. The input validates setPoints as z.array(setPointSchema), but the output doesn't. Parse and validate against setPointSchema[] on retrieval, returning a typed fallback on parse failure.

Proposed fix
    .output(z.object({
      id: z.number(),
      side: z.enum(['left', 'right']),
-     setPoints: z.unknown(),
+     setPoints: z.array(setPointSchema),
      wakeTime: z.string(),
      startedAt: z.number(),
      expiresAt: z.number(),
      status: z.enum(['active', 'completed', 'cancelled']),
    }).nullable())
    .query(async ({ input }) => {
      // ...
-     let setPoints: unknown = []
+     let setPoints: z.infer<typeof setPointSchema>[] = []
      try {
-       setPoints = JSON.parse(session.setPoints)
+       const parsed = JSON.parse(session.setPoints)
+       const validated = z.array(setPointSchema).safeParse(parsed)
+       if (validated.success) setPoints = validated.data
      }
      catch {
        // malformed — return empty
      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/routers/runOnce.ts` around lines 128 - 136, The output schema
currently exposes setPoints as z.unknown(), allowing arbitrary JSON; change the
output definition to validate setPoints against the existing setPointSchema
array (use z.array(setPointSchema) instead of z.unknown()) and, in the retrieval
logic that returns this route's payload (the code that constructs the object
matching this output), run a safe parse (e.g., setPointsSchemaArray.safeParse or
parse with try/catch) and return a typed fallback (an empty array or a
well-defined default) when parsing fails so malformed persisted payloads cannot
leak through; update references to setPointSchema and the output z.object(...)
to enforce this contract.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API: Add typed output schemas (remove z.any())

1 participant