Skip to content

Commit 18f5bcd

Browse files
os-zhuangclaude
andcommitted
fix(spec,core,cloud-connection,metadata): one HTTP contract, one canonical slot name (#4251)
packages/core/src/contracts/ was a dead near-copy of the real contracts -- zero importers (no relative import, no subpath export, not a tsup entry; core's barrel has re-exported the spec versions all along) -- and it had already DIVERGED (spec's IHttpResponse grew write?/end?, IHttpRequest grew rawBody?; the copy never did). Anyone who grepped into it read a stale contract nothing enforces -- the both-humans-and-AI failure mode behind the false http.server exemption (#4382). Deleted; zero-risk by construction. `http.server` is the canonical slot name and the ledger now says so: ServiceSlotContracts gains 'http.server': IHttpServer plus the deprecated 'http-server' alias entry (same instance -- hono-plugin, qa node-plugin and cloud's two server entrypoints all register both, alias commented "backward compatibility"). Canonical is the only name on EVERY provider path -- runtime's config.server path registers no alias, so the three cloud-connection plugins reading the alias alone found an empty slot there. All readers now go canonical-first with the alias as a fallback that dies with the alias registrations; registrations untouched this release, both sites carry the deprecation note. getRawApp?(): any joins IHttpServer -- the deliberate framework-handle escape, declared once with the rationale; four consumers declared it locally before (cloud-connection x2, metadata HMR, cloud node-server), and the local RawAppHost / HttpServerWithRawApp types are deleted. IMetadataService.bulkRegister/bulkUnregister declare the write options their implementation always accepted (bulkRegister's contract dropped the MetadataWriteOptions half it intersects in; bulkUnregister declared no options at all). Same shape as B2's IDataEngine read-methods gap. Baseline 168 -> 167 (marketplace-install-local's lookup typed while touched). Ledger test pins both slot entries and the alias equality. Verified: spec build (dts) + 7192 tests / 281 files; core, metadata, cloud-connection, plugin-hono-server dts builds; runtime 1001/69, rest 539/36, plugin-auth 579/26, plugin-sharing 226/11, service-settings 196/14, metadata 281/13, hono-server 135/12, http-conformance 46/2; ratchet holds 167/36 none new; eslint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c39d713 commit 18f5bcd

15 files changed

Lines changed: 159 additions & 337 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/core": patch
4+
"@objectstack/cloud-connection": patch
5+
"@objectstack/metadata": patch
6+
"@objectstack/plugin-hono-server": patch
7+
---
8+
9+
fix(spec,core,cloud-connection,metadata): one HTTP contract, one canonical slot name — and the dead shadow copy that helped cause the false exemption is deleted (#4251)
10+
11+
**`packages/core/src/contracts/` was a dead near-copy of the real contracts,
12+
and it is gone.** The directory (http-server.ts, data-engine.ts, logger.ts) had
13+
ZERO importers — no relative import, no subpath export, not a tsup entry;
14+
core's barrel has re-exported the `@objectstack/spec/contracts` versions all
15+
along ("Re-export contracts from @objectstack/spec for backward
16+
compatibility"). But the shadow had already **diverged** from the live
17+
contract (spec's `IHttpResponse` grew `write?`/`end?` and `IHttpRequest` grew
18+
`rawBody?`; the copy never did), so anyone who grepped their way into it read a
19+
stale contract that nothing enforces — the exact both-humans-and-AI failure
20+
mode behind the false `http.server` exemption (#4382). Deleting it is
21+
zero-risk by construction: nothing could reach it.
22+
23+
**`http.server` is the canonical slot name, and the ledger now says so.**
24+
`ServiceSlotContracts` gains `'http.server': IHttpServer` plus the deprecated
25+
`'http-server'` alias entry (same instance — hono-plugin and qa's node-plugin
26+
register both two lines apart; cloud's two server entrypoints do the same).
27+
Canonical is the only name present on EVERY provider path: runtime's
28+
`config.server` path registers no alias, so the three cloud-connection plugins
29+
that read the alias alone (marketplace-proxy, runtime-config,
30+
marketplace-install-local) found an empty slot there — a live miss, now fixed:
31+
all readers go canonical-first with the alias as a fallback that dies with the
32+
alias registrations. The registrations themselves are untouched this release;
33+
both sites now carry the deprecation note.
34+
35+
**`getRawApp?(): any` joins `IHttpServer`** — the deliberate framework-handle
36+
escape, declared once. Four consumers were each declaring it locally
37+
(cloud-connection ×2, metadata's HMR routes, cloud's serverless node-server);
38+
those local `RawAppHost`/`HttpServerWithRawApp` types are deleted. The `any`
39+
return is deliberate and documented at the single declaration: the handle's
40+
real type belongs to the framework, and naming it would give the contract a
41+
framework dependency. Adapters are not required to expose it; consumers
42+
feature-detect.
43+
44+
**`IMetadataService.bulkRegister`/`bulkUnregister` declare the write options
45+
their implementation has always accepted.** `bulkRegister`'s contract options
46+
dropped the `MetadataWriteOptions` half its implementation intersects in
47+
(`notify` is destructured on the method's first line); `bulkUnregister`
48+
declared no options at all while the manager takes them. Same shape as the
49+
`IDataEngine` read-methods gap from B2: a caller typed to the contract could
50+
not reach the channel without erasing the lookup. Both additive; no implementor
51+
or caller breaks.
52+
53+
Slot-lookup baseline ratchets 168 → 167 (marketplace-install-local's lookup
54+
typed while touched).

packages/cloud-connection/src/marketplace-install-local-plugin.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { resolveMarketplacePublicBaseUrl } from './marketplace-public-url.js';
4848
import { LocalManifestSource, type InstalledManifestEntry } from './local-manifest-source.js';
4949
import { ConnectionCredentialStore } from './connection-credential-store.js';
5050
import { MARKETPLACE_INSTALLED_UI_BUNDLE } from './marketplace-ui.js';
51+
import type { IHttpServer } from '@objectstack/spec/contracts';
5152

5253
const ROUTE_BASE = '/api/v1/marketplace/install-local';
5354

@@ -120,9 +121,11 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
120121
await this.rehydrate(ctx);
121122

122123
// 2. Mount HTTP endpoints.
123-
let httpServer: any;
124+
// [#4251] Canonical name first — see marketplace-proxy-plugin.
125+
let httpServer: IHttpServer | undefined;
124126
try {
125-
httpServer = ctx.getService('http-server');
127+
httpServer = ctx.getService<IHttpServer>('http.server')
128+
?? ctx.getService<IHttpServer>('http-server');
126129
} catch {
127130
ctx.logger?.warn?.('[MarketplaceInstallLocal] http-server not available — install endpoints not mounted');
128131
return;

packages/cloud-connection/src/marketplace-proxy-plugin.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,6 @@ import {
3535

3636
import type { IHttpServer } from '@objectstack/spec/contracts';
3737

38-
/**
39-
* The `http-server` slot plus the one member this plugin needs from it.
40-
*
41-
* [#4251] `IHttpServer` is a real contract (`@objectstack/spec/contracts`) and
42-
* this slot is bound to it — but `getRawApp()` is NOT on it, deliberately: the
43-
* contract is framework-agnostic and the raw app is the framework's own handle
44-
* (Hono here). So the escape is one named member returning `any`, scoped to the
45-
* one thing that genuinely cannot be typed framework-agnostically, instead of
46-
* the whole lookup collapsing to `any`. The probe below is what runs when a
47-
* host serves the slot with an adapter that exposes no raw app.
48-
*/
49-
type RawAppHost = IHttpServer & { getRawApp?(): any };
50-
5138
const MARKETPLACE_PREFIX = '/api/v1/marketplace';
5239

5340
/**
@@ -196,9 +183,14 @@ export class MarketplaceProxyPlugin implements Plugin {
196183
manifest?.register?.(MARKETPLACE_BROWSE_UI_BUNDLE);
197184
} catch { /* no manifest service */ }
198185

199-
let httpServer: RawAppHost | undefined;
186+
// [#4251] `http.server` is the canonical name (the only one every
187+
// provider registers — runtime's `config.server` path registers no
188+
// alias, which made this alias-only read an empty-slot miss there).
189+
// The alias fallback dies with the alias registrations.
190+
let httpServer: IHttpServer | undefined;
200191
try {
201-
httpServer = ctx.getService<RawAppHost>('http-server');
192+
httpServer = ctx.getService<IHttpServer>('http.server')
193+
?? ctx.getService<IHttpServer>('http-server');
202194
} catch {
203195
ctx.logger?.warn?.('[MarketplaceProxyPlugin] http-server not available — marketplace routes not mounted');
204196
return;

packages/cloud-connection/src/runtime-config-plugin.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,6 @@ import type { Plugin, PluginContext } from '@objectstack/core';
4343
import { resolveCloudUrl } from './cloud-url.js';
4444
import type { IHttpServer } from '@objectstack/spec/contracts';
4545

46-
/**
47-
* The `http-server` slot plus the one member this plugin needs from it.
48-
*
49-
* [#4251] `IHttpServer` is a real contract (`@objectstack/spec/contracts`) and
50-
* this slot is bound to it — but `getRawApp()` is NOT on it, deliberately: the
51-
* contract is framework-agnostic and the raw app is the framework's own handle
52-
* (Hono here). So the escape is one named member returning `any`, scoped to the
53-
* one thing that genuinely cannot be typed framework-agnostically, instead of
54-
* the whole lookup collapsing to `any`. The probe below is what runs when a
55-
* host serves the slot with an adapter that exposes no raw app.
56-
*/
57-
type RawAppHost = IHttpServer & { getRawApp?(): any };
58-
5946
/**
6047
* The `env-registry` slot's hostname resolvers — see the call site for both
6148
* spellings. Async by contract: the consumer awaits the result, and declaring
@@ -196,9 +183,12 @@ export class RuntimeConfigPlugin implements Plugin {
196183

197184
start = async (ctx: PluginContext): Promise<void> => {
198185
ctx.hook('kernel:ready', async () => {
199-
let httpServer: RawAppHost | undefined;
186+
// [#4251] Canonical name first — see marketplace-proxy-plugin for
187+
// the alias-only-miss this fixes; the fallback dies with the alias.
188+
let httpServer: IHttpServer | undefined;
200189
try {
201-
httpServer = ctx.getService<RawAppHost>('http-server');
190+
httpServer = ctx.getService<IHttpServer>('http.server')
191+
?? ctx.getService<IHttpServer>('http-server');
202192
} catch {
203193
ctx.logger?.warn?.('[RuntimeConfigPlugin] http-server not available — runtime/config not mounted');
204194
return;

packages/core/src/contracts/data-engine.ts

Lines changed: 0 additions & 62 deletions
This file was deleted.

packages/core/src/contracts/http-server.ts

Lines changed: 0 additions & 151 deletions
This file was deleted.

0 commit comments

Comments
 (0)