Skip to content

zen generate --lite does not strip attributes from typeDefs, shipping access policies to the browser #2764

Description

@ErikDakoda

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions