Skip to content

finding: the class-name limb of Serve.providesCapability's identity registries never matches the SHIPPED build for a self-referencing plugin class #8645

Description

@os-zhuang

Observation-class finding from #8357. No measured user impact today — filed so it is not rediscovered as a mystery later.

Serve.providesCapability (packages/cli/src/commands/serve.ts) decides whether a host already supplied a capability's provider by comparing, by EQUALITY, both a loaded plugin's name and its constructor.name against a declared identity list. Every identity registry in that file therefore declares two spellings per provider: the registered plugin.name id, and the exported class name.

Measured: for MarketplaceProxyPlugin the class-name spelling never matches the built package.

$ node -e "console.log(require('@objectstack/cloud-connection').MarketplaceProxyPlugin.name)"
_MarketplaceProxyPlugin

The cause is in the emitted bundle, not in the source. packages/cloud-connection/src/marketplace-proxy-plugin.ts references the class by name inside its own body — MarketplaceProxyPlugin.prototype.version, twice, building the outbound proxy User-Agent — and esbuild rewrites a class that self-references into a named class expression so the inner reference binds to the class binding rather than the outer var:

packages/cloud-connection/dist/index.js:151
var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {

The emitted class's .name is therefore _MarketplaceProxyPlugin. The other three plugins in that package (MarketplaceInstallLocalPlugin, RuntimeConfigPlugin, CloudConnectionPlugin) do not reference themselves and keep their names — so this is not a package-wide build setting, it is a per-class consequence of one source idiom.

Why there is no impact today

Every guard that names this plugin also declares its registered id (com.objectstack.runtime.marketplace-proxy), which the instance carries as a plain field and which no bundler touches. The name limb fires, the guard works, and #8357's cloud-arm precedence check is correct as landed. What is dead is the redundancy: the class-name limb is a second, independent way for the guard to recognise a provider, and for this plugin it recognises nothing. A guard running on one limb it does not know it is running on is one rename away from failing OPEN — and failing open here means silently mounting a second instance over a host's own.

Why nothing caught it

packages/cli/test/serve-capability-identity.test.ts is the drift guard for these registries, and it is careful — but its class-name assertion is expect(identities).toContain(exportName), comparing the registry against the string in the registry spec, not against Ctor.name. It then asserts the constructed instance's name is declared. So the class-name limb is never compared to what the class is actually called at runtime, for any entry in CAPABILITY_PROVIDERS or the four marketplace identity lists. #8357's new drift test compares constructor.name modulo one leading underscore, which is what surfaced this — but that is a local accommodation in one test file, not a check.

Suggested direction

Not obvious which end should move; both are defensible and the choice is worth a grading round rather than a guess:

  1. Fix the source idiom — replace the self-reference with this.version / a module-scope constant in marketplace-proxy-plugin.ts, so esbuild has nothing to rename. Narrow, restores the limb, and leaves nothing to remember, but it only fixes the instances someone happens to find.
  2. Make the drift guard read Ctor.name — assert every declared class-name identity equals the runtime class name of the export it names, across all the registries in serve.ts. This turns the whole class of drift into a red test the day it appears, and would have caught this one. It also makes the bundler idiom a build-time contract, which may be a stronger claim than the repo wants to hold.
  3. Both — the guard is what makes (1) stay fixed.

Worth noting for whoever grades it: the same idiom in any other plugin class produces the same silent result, and nothing enumerates it. A one-off grep is not the remedy; whichever option is chosen, the question is whether the repo wants a mechanical check here or accepts the registered-id limb as the only load-bearing one and drops the class-name limb's claim to redundancy.

Backlink: #8357.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions