fix(rpc): deep-freeze procedures in defineService, not just the map
This commit is contained in:
@@ -62,5 +62,16 @@ export function defineService<Procedures extends AnyProcedures>(def: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Object.freeze({ name: def.name, procedures: Object.freeze({ ...def.procedures }) });
|
// Freeze each procedure, not just the map. AnyProcedures accepts any object
|
||||||
|
// of ProcedureDef shape, so a hand-built def that never went through
|
||||||
|
// procedure.build() would otherwise stay mutable and the "single source of
|
||||||
|
// truth" guarantee would rest on every call site remembering the builder.
|
||||||
|
const frozen: Record<string, ProcedureDef> = {};
|
||||||
|
for (const [name, value] of Object.entries(def.procedures)) {
|
||||||
|
frozen[name] = Object.freeze({ ...value });
|
||||||
|
}
|
||||||
|
return Object.freeze({
|
||||||
|
name: def.name,
|
||||||
|
procedures: Object.freeze(frozen) as Procedures,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,20 @@ describe("defineService", () => {
|
|||||||
).toThrow(/procedure name/i);
|
).toThrow(/procedure name/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a hand-built procedure is frozen too, not just builder output", () => {
|
||||||
|
// AnyProcedures accepts any ProcedureDef shape; the guarantee must not
|
||||||
|
// depend on the caller having used procedure.build().
|
||||||
|
const contract = defineService({
|
||||||
|
name: "demo",
|
||||||
|
procedures: { ping: { permission: "demo:read" } },
|
||||||
|
});
|
||||||
|
expect(Object.isFrozen(contract.procedures.ping)).toBe(true);
|
||||||
|
expect(() => {
|
||||||
|
(contract.procedures.ping as { permission?: string }).permission = "hacked";
|
||||||
|
}).toThrow();
|
||||||
|
expect(contract.procedures.ping.permission).toBe("demo:read");
|
||||||
|
});
|
||||||
|
|
||||||
test("the builder is immutable — reusing a base does not cross-contaminate", () => {
|
test("the builder is immutable — reusing a base does not cross-contaminate", () => {
|
||||||
const base = procedure.permission("a:read");
|
const base = procedure.permission("a:read");
|
||||||
const one = base.idempotent().build();
|
const one = base.idempotent().build();
|
||||||
|
|||||||
Reference in New Issue
Block a user