mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(cycle): keep custom-symptom client constants out of the server crypto module
The log sheet's chip grid imported the icon allow-list from custom-symptoms.ts, which pulls node:crypto + the AES helpers into the client bundle and broke the Turbopack build for /cycle. Split the client-safe constants and Zod schemas into custom-symptoms-shared.ts; the server module re-exports them so the API routes are unchanged, and the client imports from the shared module.
This commit is contained in:
1 parent
32552f02ec
commit
2f44654dbe
3 files changed
+106
-80
No files matched your search
@@ -43,7 +43,7 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { CUSTOM_SYMPTOM_ICON_ALLOWLIST } from "@/lib/cycle/custom-symptoms";
|
||||
import { CUSTOM_SYMPTOM_ICON_ALLOWLIST } from "@/lib/cycle/custom-symptoms-shared";
|
||||
import { CYCLE_SYMPTOM_CATALOG } from "./symptom-catalog";
|
||||
import { FLOW_HUE } from "./phase-tokens";
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
/**
|
||||
* v1.15.1 — client-safe constants + Zod schemas for custom cycle symptoms.
|
||||
*
|
||||
* Split out of `custom-symptoms.ts` so a `"use client"` component (the log
|
||||
* sheet's chip grid) can read the icon allow-list and key helpers WITHOUT
|
||||
* pulling the server-only crypto / `node:crypto` the mint + label-encryption
|
||||
* helpers depend on into the browser bundle. The server module re-exports
|
||||
* everything here, so existing server import sites are unaffected.
|
||||
*/
|
||||
|
||||
/** Reserved key prefix for a user-minted custom symptom. */
|
||||
export const CUSTOM_SYMPTOM_KEY_PREFIX = "custom:";
|
||||
|
||||
/** Stable category key + seeded id every custom symptom hangs under. */
|
||||
export const CUSTOM_SYMPTOM_CATEGORY_KEY = "custom";
|
||||
export const CUSTOM_SYMPTOM_CATEGORY_ID = "csc_custom";
|
||||
|
||||
/** Per-user ceiling on custom symptoms (422 over the cap). */
|
||||
export const MAX_CUSTOM_SYMPTOMS_PER_USER = 50;
|
||||
|
||||
/**
|
||||
* Allow-list of Lucide icon names a custom symptom may use. Kept to names the
|
||||
* iOS client maps to an SF Symbol so a custom symptom never falls back to the
|
||||
* generic glyph on one platform. An unknown name → 422. `Tag` is the default.
|
||||
*/
|
||||
export const CUSTOM_SYMPTOM_ICON_ALLOWLIST = [
|
||||
"Tag",
|
||||
"Activity",
|
||||
"Heart",
|
||||
"HeartPulse",
|
||||
"Brain",
|
||||
"Zap",
|
||||
"Flame",
|
||||
"Snowflake",
|
||||
"Droplet",
|
||||
"CircleDot",
|
||||
"BatteryLow",
|
||||
"MoonStar",
|
||||
"PersonStanding",
|
||||
"Drama",
|
||||
"Frown",
|
||||
"Cookie",
|
||||
"Soup",
|
||||
"Pill",
|
||||
"Thermometer",
|
||||
"Stethoscope",
|
||||
] as const;
|
||||
|
||||
const ICON_SET = new Set<string>(CUSTOM_SYMPTOM_ICON_ALLOWLIST);
|
||||
|
||||
/** Is `key` a custom-symptom key (vs a bare catalogue slug)? */
|
||||
export function isCustomSymptomKey(key: string): boolean {
|
||||
return key.startsWith(CUSTOM_SYMPTOM_KEY_PREFIX);
|
||||
}
|
||||
|
||||
const labelSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Label must not be empty")
|
||||
.max(40, "Label must be at most 40 characters");
|
||||
|
||||
const iconSchema = z
|
||||
.string()
|
||||
.refine((v) => ICON_SET.has(v), "Unknown icon")
|
||||
.nullish();
|
||||
|
||||
/** POST body — create a custom symptom. */
|
||||
export const createCustomSymptomSchema = z.object({
|
||||
label: labelSchema,
|
||||
icon: iconSchema,
|
||||
// Reserved for a future per-symptom category choice; v1 pins everything to
|
||||
// the `custom` category, so a supplied value other than `custom` is rejected.
|
||||
categoryKey: z.literal(CUSTOM_SYMPTOM_CATEGORY_KEY).optional(),
|
||||
});
|
||||
|
||||
/** PATCH body — update a custom symptom (every field optional). */
|
||||
export const updateCustomSymptomSchema = z
|
||||
.object({
|
||||
label: labelSchema.optional(),
|
||||
icon: iconSchema,
|
||||
isActive: z.boolean().optional(),
|
||||
})
|
||||
.refine(
|
||||
(v) =>
|
||||
v.label !== undefined || v.icon !== undefined || v.isActive !== undefined,
|
||||
"At least one field is required",
|
||||
);
|
||||
@@ -1,9 +1,9 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { encrypt, decrypt } from "@/lib/crypto";
|
||||
|
||||
import { CUSTOM_SYMPTOM_KEY_PREFIX } from "@/lib/cycle/custom-symptoms-shared";
|
||||
|
||||
/**
|
||||
* v1.15.1 — per-user custom cycle-symptom helpers (the v1.13 custom-mood-tag
|
||||
* precedent, mirrored 1:1).
|
||||
@@ -17,52 +17,22 @@ import { encrypt, decrypt } from "@/lib/crypto";
|
||||
* is held AES-256-GCM-encrypted at rest (`labelEncrypted`): a symptom name is
|
||||
* intent-revealing free text, the highest-sensitivity category in the
|
||||
* post-Dobbs threat model.
|
||||
*
|
||||
* This module carries the SERVER-ONLY helpers (uuid mint + label crypto). The
|
||||
* client-safe constants + Zod schemas live in `custom-symptoms-shared.ts` and
|
||||
* are re-exported here so existing server import sites need no change.
|
||||
*/
|
||||
|
||||
/** Reserved key prefix for a user-minted custom symptom. */
|
||||
export const CUSTOM_SYMPTOM_KEY_PREFIX = "custom:";
|
||||
|
||||
/** Stable category key + seeded id every custom symptom hangs under. */
|
||||
export const CUSTOM_SYMPTOM_CATEGORY_KEY = "custom";
|
||||
export const CUSTOM_SYMPTOM_CATEGORY_ID = "csc_custom";
|
||||
|
||||
/** Per-user ceiling on custom symptoms (422 over the cap). */
|
||||
export const MAX_CUSTOM_SYMPTOMS_PER_USER = 50;
|
||||
|
||||
/**
|
||||
* Allow-list of Lucide icon names a custom symptom may use. Kept to names the
|
||||
* iOS client maps to an SF Symbol so a custom symptom never falls back to the
|
||||
* generic glyph on one platform. An unknown name → 422. `Tag` is the default.
|
||||
*/
|
||||
export const CUSTOM_SYMPTOM_ICON_ALLOWLIST = [
|
||||
"Tag",
|
||||
"Activity",
|
||||
"Heart",
|
||||
"HeartPulse",
|
||||
"Brain",
|
||||
"Zap",
|
||||
"Flame",
|
||||
"Snowflake",
|
||||
"Droplet",
|
||||
"CircleDot",
|
||||
"BatteryLow",
|
||||
"MoonStar",
|
||||
"PersonStanding",
|
||||
"Drama",
|
||||
"Frown",
|
||||
"Cookie",
|
||||
"Soup",
|
||||
"Pill",
|
||||
"Thermometer",
|
||||
"Stethoscope",
|
||||
] as const;
|
||||
|
||||
const ICON_SET = new Set<string>(CUSTOM_SYMPTOM_ICON_ALLOWLIST);
|
||||
|
||||
/** Is `key` a custom-symptom key (vs a bare catalogue slug)? */
|
||||
export function isCustomSymptomKey(key: string): boolean {
|
||||
return key.startsWith(CUSTOM_SYMPTOM_KEY_PREFIX);
|
||||
}
|
||||
export {
|
||||
CUSTOM_SYMPTOM_KEY_PREFIX,
|
||||
CUSTOM_SYMPTOM_CATEGORY_KEY,
|
||||
CUSTOM_SYMPTOM_CATEGORY_ID,
|
||||
MAX_CUSTOM_SYMPTOMS_PER_USER,
|
||||
CUSTOM_SYMPTOM_ICON_ALLOWLIST,
|
||||
isCustomSymptomKey,
|
||||
createCustomSymptomSchema,
|
||||
updateCustomSymptomSchema,
|
||||
} from "@/lib/cycle/custom-symptoms-shared";
|
||||
|
||||
/** Mint a fresh, collision-proof custom-symptom key. */
|
||||
export function mintCustomSymptomKey(): string {
|
||||
@@ -86,36 +56,3 @@ export function decryptCustomLabel(encoded: string | null): string | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const labelSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Label must not be empty")
|
||||
.max(40, "Label must be at most 40 characters");
|
||||
|
||||
const iconSchema = z
|
||||
.string()
|
||||
.refine((v) => ICON_SET.has(v), "Unknown icon")
|
||||
.nullish();
|
||||
|
||||
/** POST body — create a custom symptom. */
|
||||
export const createCustomSymptomSchema = z.object({
|
||||
label: labelSchema,
|
||||
icon: iconSchema,
|
||||
// Reserved for a future per-symptom category choice; v1 pins everything to
|
||||
// the `custom` category, so a supplied value other than `custom` is rejected.
|
||||
categoryKey: z.literal(CUSTOM_SYMPTOM_CATEGORY_KEY).optional(),
|
||||
});
|
||||
|
||||
/** PATCH body — update a custom symptom (every field optional). */
|
||||
export const updateCustomSymptomSchema = z
|
||||
.object({
|
||||
label: labelSchema.optional(),
|
||||
icon: iconSchema,
|
||||
isActive: z.boolean().optional(),
|
||||
})
|
||||
.refine(
|
||||
(v) =>
|
||||
v.label !== undefined || v.icon !== undefined || v.isActive !== undefined,
|
||||
"At least one field is required",
|
||||
);
|
||||
Reference in new issue
Block a user