Version
@zenstackhq/sdk / @zenstackhq/cli 3.8.3 (current latest).
Summary
zen generate --lite strips attributes from models and from fields, but not from typeDefs. Any access-control policy declared on a type rather than a model therefore still ships to the browser in schema-lite.ts.
This matters because the docs present schema-lite.ts as the client-side artifact and justify it partly on the grounds that the full schema embeds access policies. For schemas that put shared policies on type declarations, that guarantee does not hold.
To be clear about severity: this is information disclosure, not privilege escalation — the server still enforces policies. But the client bundle ends up describing the authorization model.
Reproduction
In our generated schema-lite.ts, --lite removed every field-level attribute (487 @default and 55 @updatedAt occurrences → 0) and every model-level attribute (0 remaining), but left 18 type-level attributes intact:
| typeDef |
remaining attributes |
UserBase |
5 × @@allow |
Base |
2 × @@allow, 2 × @@deny, 3 × @@index |
TaxonomyBase |
3 × @@allow, 3 × @@index |
The retained expressions are fully readable in the emitted AST — for example an @@allow('all', auth().role == 'ADMIN') rule, and a space-membership read rule, both reconstructable from the shipped ExpressionUtils calls.
Cause
packages/sdk/src/ts-schema-generator.ts. The model path honours lite; the typeDef path receives the flag and uses it only for fields, not for the type's own attributes.
// createDataModelObject (line ~374)
const allAttributes = lite
? [] // in lite mode, skip all model-level attributes
: getAllAttributes(dm).filter(...);
// createTypeDefObject (line ~503)
const allAttributes = getAllAttributes(td); // `lite` is in scope but unused here
createTypeDefObject does pass lite down to createDataFieldObject(field, undefined, lite), so field attributes inside typeDefs are stripped correctly — it is only the type-level attributes that are missed.
Suggested fix
Mirror the model path:
const allAttributes = lite ? [] : getAllAttributes(td);
Related, and possibly a separate decision
Because lite strips all field attributes, @default and @updatedAt are absent from schema-lite.ts. packages/clients/client-helpers/src/mutator.ts:209 reads exactly those two attributes to synthesise values for optimistic updates:
const defaultAttr = field.attributes?.find((attr) => attr.name === '@default');
...
if (defaultAttr || field.attributes?.some((attr) => attr.name === '@updatedAt')) { ... }
So on a lite schema that logic silently no-ops. That may well be an accepted trade-off, but it is not documented, and it is invisible at runtime — worth confirming it is intentional. Happy to split this into its own issue if you'd prefer.
Offer
Happy to contribute a PR with the one-line fix (plus a regression test asserting typeDefs attributes are absent under --lite) if you agree with the approach.
Version
@zenstackhq/sdk/@zenstackhq/cli3.8.3 (current latest).Summary
zen generate --litestrips attributes from models and from fields, but not fromtypeDefs. Any access-control policy declared on atyperather than amodeltherefore still ships to the browser inschema-lite.ts.This matters because the docs present
schema-lite.tsas the client-side artifact and justify it partly on the grounds that the full schema embeds access policies. For schemas that put shared policies ontypedeclarations, that guarantee does not hold.To be clear about severity: this is information disclosure, not privilege escalation — the server still enforces policies. But the client bundle ends up describing the authorization model.
Reproduction
In our generated
schema-lite.ts,--literemoved every field-level attribute (487@defaultand 55@updatedAtoccurrences → 0) and every model-level attribute (0 remaining), but left 18 type-level attributes intact:UserBase@@allowBase@@allow, 2 ×@@deny, 3 ×@@indexTaxonomyBase@@allow, 3 ×@@indexThe retained expressions are fully readable in the emitted AST — for example an
@@allow('all', auth().role == 'ADMIN')rule, and a space-membership read rule, both reconstructable from the shippedExpressionUtilscalls.Cause
packages/sdk/src/ts-schema-generator.ts. The model path honourslite; the typeDef path receives the flag and uses it only for fields, not for the type's own attributes.createTypeDefObjectdoes passlitedown tocreateDataFieldObject(field, undefined, lite), so field attributes inside typeDefs are stripped correctly — it is only the type-level attributes that are missed.Suggested fix
Mirror the model path:
Related, and possibly a separate decision
Because
litestrips all field attributes,@defaultand@updatedAtare absent fromschema-lite.ts.packages/clients/client-helpers/src/mutator.ts:209reads exactly those two attributes to synthesise values for optimistic updates:So on a lite schema that logic silently no-ops. That may well be an accepted trade-off, but it is not documented, and it is invisible at runtime — worth confirming it is intentional. Happy to split this into its own issue if you'd prefer.
Offer
Happy to contribute a PR with the one-line fix (plus a regression test asserting
typeDefsattributes are absent under--lite) if you agree with the approach.