Skip to content

fix(validate): support decorated classes extending decorated classes#194

Merged
teamchong merged 3 commits into
cloudflare:mainfrom
teamchong:fix/validate-inheritance-wrap
Jun 12, 2026
Merged

fix(validate): support decorated classes extending decorated classes#194
teamchong merged 3 commits into
cloudflare:mainfrom
teamchong:fix/validate-inheritance-wrap

Conversation

@teamchong

@teamchong teamchong commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Problem

A downstream repo hit:

Uncaught TypeError: capnweb-validate: Class1.field1 is not in the generated validator

and calls to subclass-only methods failed with the same misleading error.

Root cause: @validateRpc() returned a new Proxy(...) from the decorated constructor. When a decorated class extends another decorated class (Class2 extends Class1), super() returns the base class's validator proxy, so every subclass-only method (method2, method3, ...) is checked against the base validator and refused. The generated validator itself is correct; only the runtime wrapping was wrong.

The constructor-returns-Proxy approach had a second cost: instances were Proxies rather than real branded RpcTargets, a serialization hazard over native workerd RPC.

Fix

  • __validateRpcClass now wraps the declared methods in place on the class's prototype instead of returning a Proxy from the constructor.
  • Wrapping is idempotent (a WRAPPED_METHOD symbol marks wrapped functions), so decorated-extends-decorated and double-decoration compose: each class's methods are validated against its own validator, and inheritance behaves like ordinary JS.
  • Instances stay real branded RpcTargets with intact instanceof identity and #-field privacy.
  • Incoming argument stubs (callbacks declared with v.stubOf(...)) now pass through as native stubs instead of being wrapped in validation proxies. The wrapper proxy could not be forwarded over native workerd RPC (for example, handing a received callback to a Durable Object). The trade-off: return values from those callbacks are no longer validated by default. This follows the review direction on Add capnweb-validate RPC validators #169: incoming wrappers validate only arguments, and stub validation is explicit opt-in via validateStub<T>(stub) rather than automatic.
  • Refusal errors now distinguish instance-property access from methods genuinely missing from the RPC interface, and suggest rebuilding if the validator may be stale.

Verification

  • Full capnweb-validate suite: 179/179 pass. New regression file decorated-class-runtime.test.ts covers subclass-only methods, inherited methods, double-decoration, prototype/identity preservation, and refusal semantics.
  • workerd suite: 146/146 pass, including a new integration test that receives a native RpcStub callback through a validated service and forwards it to a Durable Object, both as a top-level argument and nested inside an object. The test fails against the previous wrapping behavior.
  • Return direction over native workerd RPC, verified with the same wrangler repro: a decorated method whose declared return type resolves to a stub-shaped validator (a class type) returns the instance across the boundary correctly, and declared methods on the returned stub still validate because the prototype methods are wrapped in place. Interface-typed returns emit object shapes and pass through unchanged. Over capnweb sessions the returned stub keeps the validation wrapper, preserving nested capability validation per the Add capnweb-validate RPC validators #169 direction (receiver validates, sender wraps nested capabilities).
  • Minimal repro (wrangler dev over native Workers RPC + a pure capnweb session), before vs after:
Probe Before After
Subclass-only method on decorated-extends-decorated fails: method2 is not in the generated validator passes
Top-level return of decorated instance Proxy real branded RpcTarget
Forward a received callback stub to a Durable Object fails in serialization passes
Instance-property probe (field1) refused refused (clearer message)
Inherited method via subclass passes passes
Decorated method returns a decorated instance over native RPC Proxy instance, serialization hazard passes, declared methods still validated

Note: the downstream Class1.field1 console error is independently legitimate. protected fields are runtime-public properties and should be #-private in the downstream code; this PR makes the refusal message say so explicitly.

@changeset-bot

changeset-bot Bot commented Jun 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 17d5408

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
capnweb-validate Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jun 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/capnweb@194

commit: 17d5408

@teamchong teamchong force-pushed the fix/validate-inheritance-wrap branch 4 times, most recently from 9130edc to 7bb21cc Compare June 11, 2026 00:57
@teamchong teamchong marked this pull request as ready for review June 11, 2026 03:48
@teamchong teamchong force-pushed the fix/validate-inheritance-wrap branch from 866c993 to 3ce9472 Compare June 11, 2026 04:15
@teamchong teamchong requested a review from kentonv June 11, 2026 13:30
- Replace constructor-returns-Proxy in __validateRpcClass with in-place
  prototype method wrapping, idempotent via a WRAPPED_METHOD symbol
- Decorated classes extending decorated classes now resolve subclass-only
  methods against the subclass validator instead of refusing them
- Instances stay real branded RpcTargets, removing the Proxy-serialization
  hazard over native workerd RPC
- Reword refusal errors to distinguish instance-property access from
  methods not declared on the RPC interface
- Cover subclass-only and inherited methods on decorated-extends-decorated
- Verify double-decoration is a no-op and instanceof identity is preserved
- Assert instance-property refusal semantics and #-field privacy intact
@teamchong teamchong force-pushed the fix/validate-inheritance-wrap branch from 3ce9472 to 4986418 Compare June 11, 2026 13:34
@kentonv

kentonv commented Jun 12, 2026

Copy link
Copy Markdown
Member

Let's plan on @dimitropoulos reviewing changes to this package, I probably don't have bandwidth to really learn the code myself.

@dimitropoulos dimitropoulos self-assigned this Jun 12, 2026
Comment thread packages/capnweb-validate/__tests__/decorated-class-runtime.test.ts
Comment thread packages/capnweb-validate/src/internal/core.ts Outdated
Comment thread packages/capnweb-validate/src/internal/core.ts
Comment thread packages/capnweb-validate/src/internal/core.ts
Comment thread packages/capnweb-validate/src/internal/core.ts

@dimitropoulos dimitropoulos left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some very very minor things I noticed - none blocking

@teamchong teamchong merged commit 4093556 into cloudflare:main Jun 12, 2026
8 checks passed
@teamchong teamchong deleted the fix/validate-inheritance-wrap branch June 12, 2026 20:00
@github-actions github-actions Bot mentioned this pull request Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants