fix(modifiers): close client schema/server schema drift + add parity test (Q4.2)
The T3 audit flagged the Zod v3 / v4 hand-mirrored schemas as a silent-drift risk: a bug where the client accepts what the server rejects (or vice versa) would silently degrade gameplay rather than fail loudly. New cross-package parity test at packages/server/src/custom-modifier-wire-parity.test.ts imports BOTH schemas and asserts they agree on an accept/reject matrix of 17 cases (valid minimal, valid rich, valid optional-field combinations + 12 rejection cases covering type/version/name/description/uiForm/source literals, bounds, empty id, oversized primitives/description, and primitive-node empty-kind). A final round-trip case parses on the client, JSON-serializes, and parses on the server — catching stringification edge cases too. First run surfaced a real drift: the client schema was missing the primitives.max(50) cap that the server schema enforces. A malicious or buggy client could construct an oversized descriptor, get past local validation, then hit the server's rejection. Fixed by adding matching caps (primitives.max(50) + targetAttrs.max(32) + author.max(80)) to the client schema. Barrel export: chess/src/index.ts now re-exports CustomModifierDescriptorSchema + EffectPrimitiveNodeSchema + the parse/serialize helpers so the server parity test can import them without reaching into subpaths. 1400 → 1417 unit tests.
This commit is contained in:
parent
c8d7480a26
commit
abc5c863fd
3 changed files with 193 additions and 3 deletions
|
|
@ -87,3 +87,15 @@ export type {
|
|||
// new ModifierProfile to a live session at a turn boundary without
|
||||
// reaching into engine internals.
|
||||
export { reconcileProfileSwap } from "./modifiers/reconcile.js";
|
||||
|
||||
// T3 custom modifier schema + parser. Exported so the server
|
||||
// package can run the cross-package wire-shape parity test (Q4.2)
|
||||
// — it imports both the v4 client schema and the v3 server schema
|
||||
// and asserts they agree on the accept/reject matrix.
|
||||
export {
|
||||
CustomModifierDescriptorSchema,
|
||||
EffectPrimitiveNodeSchema,
|
||||
parseCustomModifierDescriptor,
|
||||
safeParseCustomModifierDescriptor,
|
||||
serializeCustomModifierDescriptor,
|
||||
} from "./modifiers/custom/schema.js";
|
||||
|
|
|
|||
|
|
@ -53,11 +53,15 @@ export const CustomModifierDescriptorSchema = z.object({
|
|||
name: z.string().min(1).max(40),
|
||||
description: z.string().max(200),
|
||||
version: z.literal(1),
|
||||
primitives: z.array(EffectPrimitiveNodeSchema),
|
||||
targetAttrs: z.array(z.string()),
|
||||
// Cap matches the server-side wire schema's .max(50) and the
|
||||
// validator's MAX_PRIMITIVE_COUNT. Drift between the three caps
|
||||
// is caught by the cross-package parity test (Q4.2).
|
||||
primitives: z.array(EffectPrimitiveNodeSchema).max(50),
|
||||
// Also bounded for parity with the server schema.
|
||||
targetAttrs: z.array(z.string()).max(32),
|
||||
uiForm: z.literal("primitive-composer"),
|
||||
source: z.literal("custom"),
|
||||
author: z.string().optional(),
|
||||
author: z.string().max(80).optional(),
|
||||
createdAt: z.number().int().nonnegative().optional(),
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue