A Supported Consumer Interface Is a Capability Boundary

api-design capability-safety package-boundaries release-engineering verification Aug 12, 2026

The failure that blocked a usable package was not a missing file export. It was structural: no root contract, no compiled output or declarations in the shipped artifact, and an install from the published tarball that could not run. Opening the existing engine barrel would have made the package “work” in a narrow sense — and would also have leaked mutation capability that external consumers must never hold.

A supported consumer interface is therefore a capability boundary, not a packaging chore. The useful work is deciding what may be imported, what must still ship for runtime, and what must remain unreachable even when someone ignores your types.

The problem

Publishing a library so hosts can depend on it looks like configuration: set exports, fill files, emit declarations, and let consumers import the root. That framing understates the risk.

Two different failures hide under the same word “boundary”:

  • Import reachability — what a consumer is allowed to resolve through the package’s public contract.
  • Physical runtime closure — what code must still exist inside the installed package so the allowed imports actually run.

Treat those as one knob and you get a false choice: either ship everything and rely on documentation, or trim the tarball so aggressively that the supported root crashes at runtime. Neither is a supported interface.

A third failure is subtler. A narrowed declared type can look read-only while the returned objects still expose mutating methods at runtime. TypeScript will not catch that. An external consumer who treats the types as optional will.

What actually happened

The repair stayed at the boundary rather than rewriting the engine.

A curated consumer root replaced “export the engine barrel.” Package exports were made restrictive so only that root was importable. An independent files allowlist controlled what the tarball contained. Declarations were emitted for the public surface. A session wrapper held engine authorities and the mutable store behind ECMAScript-private fields so mutation capability was not a public return value.

That combination required a distinction that is easy to blur in review: restrictive exports and do not ship internals are different concerns. The published artifact still needed the compiled runtime closure the public root depends on. Exports made only the root importable; the pack contents kept the closure available. Collapsing those two rules into one would either break installs or invite deep imports.

Safety proof went past typing. An installed-package fixture exercised the public read methods and asserted that forbidden mutation and raw-SQL names were absent from the returned surfaces — checking runtime capability, not a narrowed interface alone.

Verification governance mattered too. An existing regression scanned every in-tree caller of a privileged transition helper; the new host-boundary session was supposed to call that helper. The first attempted carve-out was rejected as too broad. The accepted change recorded an exact two-path exception and left detection intact for every other caller. A directory-wide suffix match would have quietly licensed future internals.

Acceptance closed against external proof: root import and type-check through an installed consumer, negative imports for unsupported paths, tarball integrity paired to the same build commit as the fixture evidence, regression evidence, and no unplanned diff through the restricted engine surface.

The lesson

A supported consumer interface has to model both the contract consumers may use and the closure the contract needs to execute, while proving that public returns do not hand out authorities the contract claims to withhold.

Types are communication. Capability safety is what remains true when someone ignores them. If a wrapper is meant to be read-only, prove it on every returned public surface at runtime — method inventory included — not only on the declared type.

When an old verification rule assumes “all callers are internal,” introducing a sanctioned host-boundary caller is an authority change. Scope it with exact allowed paths and the invariant you are still preserving. Vague exceptions become permanent holes.

The broader principle

Package publication is a security and authority-boundary task whenever the package can return handles into privileged internals. Metadata fields (exports, files, types) are necessary controls, but none of them alone answers: what can a determined consumer reach, and what can the objects they receive still do?

The same principle applies to evidence. Build commit, tarball integrity, and installed-consumer proof belong together from a clean build — not as a repair after an audit notices the revision drifted. “Tests pass” is not the same as “the boundary you claim is the boundary you shipped.”

How to apply it

For the next package you expose as a supported consumer surface:

  • Start from a boundary template that separately requires restrictive exports, an independent files allowlist, generated declarations, and a packed external fixture.
  • In planning, enumerate every public wrapper return surface so the first capability-proof fixture covers them all.
  • Require positive install proof, negative-import proof, and runtime non-leak proof as distinct checklist items — not one bundled “packaging done” gate.
  • When a verification rule must recognize a new sanctioned boundary caller, write the preserved invariant, the exact assertion, the exact allowed paths, and who authorized the change before you implement.
  • Capture build SHA, tarball integrity, and fixture result from the same clean build commit before anyone audits the result.