Skip to content

api.enableSearch and metadata.maskObjectFields are live REST config keys that no schema in packages/spec declares — read through as any, and stripped by any parse of their own config object #11983

Description

@os-zhuang

Found while implementing #11637 (making the REST server parse config.api instead of casting to it). Filed, not fixed — the repair is a packages/spec declaration, which is domain:spec's single-owner surface and outside #11637's declared scope.

This is the mirror image of #11637: that card is a key the spec declares and the runtime never enforces; this is a key the runtime honours and the spec never declares.

What was measured

On origin/main @ 7899f5745.

RestServer.normalizeConfig reads two keys through as any because they have no declared seat:

// packages/rest/src/rest-server.ts (normalizeConfig)enableOpenApi: (apiasany).enableOpenApi??true,enableSearch: (apiasany).enableSearch??true,
...
maskObjectFields: isObjectSchemaMaskingEnabled((metadataasany).maskObjectFields),
  • enableOpenApiis declared (packages/spec/src/api/rest-server.zod.ts), so its as any is stale residue — harmless, but it is what makes the line beside it look equally safe.
  • enableSearch is not. git grep -c enableSearch origin/main -- packages/spec/src → zero hits (reverse-checked with enableDiscovery, 1 hit in the same file, and projectResolution, 3 hits across the directory). It is nonetheless live: NormalizedRestServerConfig.api.enableSearch is typed, and registerRoutes / the discovery capability block both read it, so enableSearch: false really does turn the search surface off today.
  • metadata.maskObjectFields is the same shape, and the code says so in place: "MetadataEndpointsConfigSchema lives in packages/spec and giving this key a declared seat there is a separate change" (ADR-0106 D8).

Why it is not merely cosmetic

RestApiConfigSchema is not .strict(), and a non-strict z.object()strips what it does not declare. Measured directly:

RestApiConfigSchema.parse({ version: 'v1', enableSearch: false })
→ the returned object has no `enableSearch` property at all

So the moment anything parses this config and consumes the result, a deployment that turned search off silently gets it back on — no error, no warning, nothing to grep. That is the ADR-0104 silent-strip class the tombstones in packages/spec/src/shared/retired-key.ts exist to prevent, and it is a live trap rather than a hypothetical: #11637 had to make its parse validation-only, discarding the parsed value and normalizing from the raw input, specifically to avoid tripping it. That workaround is a cost this undeclared key imposes on every future parse at that seam.

There is also the authoring face: RestApiConfig is z.input<typeof RestApiConfigSchema>, so a TypeScript author writing api: { enableSearch: false } against the declared type gets an excess-property error today and has to cast — which is why every in-repo caller passes as any.

Not prejudged

  • Declare both keys in RestApiConfigSchema / MetadataEndpointsConfigSchema with the defaults the runtime already applies (enableSearch: z.boolean().default(true), and the ADR-0106 D8 default for maskObjectFields), then delete the three as any reads. Declared = enforced, and the seam's parse can consume its own output.
  • Retire enableSearch under ADR-0049 enforce-or-remove if the deployment-wide search opt-out is not a capability worth keeping — it would need a tombstone, since deleting an honoured key silently is the failure above.

Either way enableOpenApi's stale as any should go with it: it is declared, and the cast is what makes its undeclared neighbour look normal.


Generated by Claude Code

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