Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .changeset/http-server-exemption-revoked.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
"@objectstack/metadata": patch
---

fix(lint,metadata): revoke the `http.server` lint exemption — its stated reason was false (#4251)

`http.server` was added to `UNCONTRACTED_SLOTS` in #4321 on the ground that
"no IHttpServer contract exists". The contract does exist —
`packages/spec/src/contracts/http-server.ts` — and eight call sites were
already resolving the slot as `getService<IHttpServer>(…)` when the exemption
was written. An exemption is a claim like any other, and this one rested on a
premise nobody checked: the same shape as the gaps the rule exists to find.

Revoked. That surfaced **9 erasures the exemption had been hiding** — 7 in
files never grandfathered, 2 as count growth inside grandfathered ones, none of
which the baseline could legally absorb. All typed to `IHttpServer`;
`packages/metadata/src/plugin.ts` came out clean entirely, so the baseline
ratchets **DOWN to 168 sites in 36 files** and loses a file.

Two things confirmed on the way, reported rather than changed:

**`http.server` and `http-server` are the same instance under two names.**
plugin-hono-server and qa's node-plugin each register it twice, two lines
apart; runtime's `config.server` path registers only `http.server`.
`metadata/src/plugin.ts` reads both with a `??`, which is how it survived. No
registration is removed here — that is a runtime-behaviour change and belongs
with whoever picks the canonical name.

**`IHttpServer` is defined twice and the two have already diverged.**
`packages/spec/src/contracts/http-server.ts` (15 importers) declares `write?()`
and `end?()`; `packages/core/src/contracts/http-server.ts` (8 importers) does
not. Spec's is the superset and the one the ledger points at, so it is the
source; core's is a stale near-copy and should re-export it. Left for its own
change — collapsing a duplicated contract is not a lint fix.

Also worth a note for whoever writes the wider HTTP contract: `getRawApp()` now
has a **third** independent consumer (metadata's HMR routes, joining
cloud-connection's two). It is deliberately absent from `IHttpServer` — the
contract is framework-agnostic and the raw app is the framework's own handle —
so each consumer names it locally. Three is enough evidence to decide whether
that stays the right answer.
15 changes: 10 additions & 5 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,16 @@ const SLOT_LOOKUPS = ['resolveService', 'getService', 'getRequestKernelService']
// place, not sprinkled through the code. Deleting a name from this list is how
// the exemption ends once that contract gets written.
//
// Entries are spliced into a regex, so escape metacharacters (`http\\.server`).
// `http.server` is served by three providers (plugin-hono-server, runtime's
// config.server path, qa's node-plugin) and no IHttpServer contract exists;
// callers read only `getPort()`.
const UNCONTRACTED_SLOTS = ['protocol', 'mcp', 'kernel-resolver', 'scope-manager', 'http\\.server'].join('|');
// Entries are spliced into a regex, so escape metacharacters if one returns.
//
// [#4251] `http.server` was here, on the stated ground that "no IHttpServer
// contract exists". That was FALSE when it was written: the contract is
// `packages/spec/src/contracts/http-server.ts`, and eight call sites were
// already resolving the slot as `getService<IHttpServer>(…)`. An exemption is a
// claim like any other, and this one rested on a premise nobody checked — the
// same shape as the gaps this rule exists to find. Revoked; the slot is
// accounted for like every other contracted slot.
const UNCONTRACTED_SLOTS = ['protocol', 'mcp', 'kernel-resolver', 'scope-manager'].join('|');

// Exported so `scripts/check-slot-lookup-ratchet.mjs` can identify THIS rule's
// reports among the other `no-restricted-syntax` rules, by exact message —
Expand Down
5 changes: 3 additions & 2 deletions packages/client/src/client.batch-transaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
import { createRestApiPlugin } from '@objectstack/runtime';
import { ObjectStackClient } from './index';
import type { IHttpServer } from '@objectstack/spec/contracts';

describe('data.batchTransaction (live Hono, #1604)', () => {
let baseUrl: string;
Expand Down Expand Up @@ -103,8 +104,8 @@ describe('data.batchTransaction (live Hono, #1604)', () => {
await ql.syncObjectSchema('project');
await ql.syncObjectSchema('task');

const httpServer = kernel.getService<any>('http.server');
baseUrl = `http://localhost:${httpServer.getPort()}`;
const httpServer = kernel.getService<IHttpServer>('http.server');
baseUrl = `http://localhost:${httpServer.getPort!()}`;
client = new ObjectStackClient({ baseUrl });
}, 30_000);

Expand Down
5 changes: 3 additions & 2 deletions packages/client/src/client.environment-scoping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
import { createRestApiPlugin } from '@objectstack/runtime';
import { ObjectStackClient } from './index';
import type { IHttpServer } from '@objectstack/spec/contracts';

describe('Project-scoped REST routing (live Hono)', () => {
let baseUrl: string;
Expand Down Expand Up @@ -82,8 +83,8 @@ describe('Project-scoped REST routing (live Hono)', () => {
// write fails with `no such table`.
await ql.syncObjectSchema('task');

const httpServer = kernel.getService<any>('http.server');
const port = httpServer.getPort();
const httpServer = kernel.getService<IHttpServer>('http.server');
const port = httpServer.getPort!();
baseUrl = `http://localhost:${port}`;
}, 30_000);

Expand Down
5 changes: 3 additions & 2 deletions packages/client/src/client.hono.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
import { createRestApiPlugin } from '@objectstack/runtime';
import { ObjectStackClient } from './index';
import type { IHttpServer } from '@objectstack/spec/contracts';

describe('ObjectStackClient (with Hono Server)', () => {
let baseUrl: string;
Expand Down Expand Up @@ -130,8 +131,8 @@ describe('ObjectStackClient (with Hono Server)', () => {
await ql.syncObjectSchema('customer');

// 5. Get Port from Service
const httpServer = kernel.getService<any>('http.server');
const port = httpServer.getPort();
const httpServer = kernel.getService<IHttpServer>('http.server');
const port = httpServer.getPort!();
baseUrl = `http://localhost:${port}`;

console.log(`Test server running at ${baseUrl}`);
Expand Down
14 changes: 12 additions & 2 deletions packages/metadata/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ const ARTIFACT_FIELD_TO_TYPE: Record<string, string> = {
// callers below and for `view-expand.test.ts`.
export { isAggregatedViewContainer, expandViewContainer } from '@objectstack/spec';
import { isAggregatedViewContainer, expandViewContainer } from '@objectstack/spec';
import type { IHttpServer } from '@objectstack/spec/contracts';

/** `IHttpServer` plus the framework-specific raw-app handle this route needs. */
type HttpServerWithRawApp = IHttpServer & { getRawApp?(): any };

export interface MetadataPluginOptions {
rootDir?: string;
Expand Down Expand Up @@ -405,8 +409,14 @@ export class MetadataPlugin implements Plugin {
// Production deployments simply won't have a CLI POSTing to this
// endpoint and won't surface the route to clients.
try {
const httpServer = ctx.getService<any>('http-server')
?? ctx.getService<any>('http.server');
// [#4251] Both names are the SAME instance — plugin-hono-server and
// qa/node-plugin each register it twice, two lines apart. Typed to
// the contract now that the `http.server` exemption is revoked;
// `getRawApp()` is not on `IHttpServer` (framework-agnostic by
// design, the raw app is the framework's own handle), so that one
// member is named here — the third consumer to need it.
const httpServer = ctx.getService<HttpServerWithRawApp>('http-server')
?? ctx.getService<HttpServerWithRawApp>('http.server');
if (httpServer && typeof httpServer.getRawApp === 'function') {
const { registerMetadataHmrRoutes } = await import('./routes/hmr-routes.js');
const hub = registerMetadataHmrRoutes(httpServer.getRawApp(), this.manager);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
import { createDispatcherPlugin, createRestApiPlugin } from '@objectstack/runtime';
import { NodeServerPlugin } from './node-plugin.js';
import type { IHttpServer } from '@objectstack/spec/contracts';

type AdapterCase = {
label: 'node' | 'hono';
Expand Down Expand Up @@ -97,8 +98,8 @@ async function bootStack(makePlugin: () => any, opts: { withAnalytics?: boolean
// a 404 OBJECT_NOT_FOUND — a routing-shaped symptom for a DDL-shaped cause.
await ql.syncObjectSchema('task');

const httpServer = kernel.getService<any>('http.server');
return { kernel, base: `http://127.0.0.1:${httpServer.getPort()}` };
const httpServer = kernel.getService<IHttpServer>('http.server');
return { kernel, base: `http://127.0.0.1:${httpServer.getPort!()}` };
}

describe.each(ADAPTERS)('IHttpServer conformance on $label adapter', ({ makePlugin }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { LiteKernel } from '@objectstack/core';
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';

import { createDispatcherPlugin } from './dispatcher-plugin.js';
import type { IHttpServer } from '@objectstack/spec/contracts';

/**
* Integration regression for framework #2217 seam #2.
Expand All @@ -29,8 +30,8 @@ describe('GET /ready over a real HTTP server (integration)', () => {

await kernel.bootstrap();

const httpServer = kernel.getService<any>('http.server');
baseUrl = `http://127.0.0.1:${httpServer.getPort()}`;
const httpServer = kernel.getService<IHttpServer>('http.server');
baseUrl = `http://127.0.0.1:${httpServer.getPort!()}`;
}, 30_000);

afterAll(async () => {
Expand Down
5 changes: 3 additions & 2 deletions packages/runtime/src/notifications.hono.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { MessagingServicePlugin, MessagingService } from '@objectstack/service-m

import { createDispatcherPlugin } from './dispatcher-plugin.js';
import { DriverPlugin } from './driver-plugin.js';
import type { IHttpServer } from '@objectstack/spec/contracts';

/**
* End-to-end regression for framework #3362 (`#3354 not effective on hono`).
Expand Down Expand Up @@ -89,8 +90,8 @@ describe('in-app notifications over a real hono server (integration, #3362)', ()

await kernel.bootstrap();

const httpServer = kernel.getService<any>('http.server');
baseUrl = `http://127.0.0.1:${httpServer.getPort()}`;
const httpServer = kernel.getService<IHttpServer>('http.server');
baseUrl = `http://127.0.0.1:${httpServer.getPort!()}`;
messaging = kernel.getService<MessagingService>('notification');
}, 30_000);

Expand Down
5 changes: 3 additions & 2 deletions packages/runtime/src/route-parity.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
import { isMcpServerEnabled } from '@objectstack/types';

import { createDispatcherPlugin } from './dispatcher-plugin.js';
import type { IHttpServer } from '@objectstack/spec/contracts';

/**
* The statuses that mean "advertised but not actually served" — 404 (route not
Expand Down Expand Up @@ -126,8 +127,8 @@ async function bootServe(stubOpts: { notification?: boolean; mcp?: boolean } = {
kernel.use(new HonoServerPlugin({ port: 0, cors: false }));
kernel.use(createDispatcherPlugin({ prefix: '/api/v1', securityHeaders: false, requireAuth: true }));
await kernel.bootstrap();
const httpServer = kernel.getService<any>('http.server');
const baseUrl = `http://127.0.0.1:${httpServer.getPort()}`;
const httpServer = kernel.getService<IHttpServer>('http.server');
const baseUrl = `http://127.0.0.1:${httpServer.getPort!()}`;
return { kernel, baseUrl };
}

Expand Down
5 changes: 3 additions & 2 deletions packages/runtime/src/server-timing.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { LiteKernel } from '@objectstack/core';
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
import { SqlDriver } from '@objectstack/driver-sql';
import { allowPerfDisclosure } from '@objectstack/observability';
import type { IHttpServer } from '@objectstack/spec/contracts';

/**
* End-to-end regression for the Server-Timing `db` span (issue #2408).
Expand Down Expand Up @@ -41,7 +42,7 @@ describe('Server-Timing db span over a real HTTP server (integration)', () => {
kernel.use(new HonoServerPlugin({ port: 0, serverTiming: true, cors: false }));
await kernel.bootstrap();

const httpServer = kernel.getService<any>('http.server');
const httpServer = kernel.getService<IHttpServer>('http.server');
// A route whose handler runs two real queries through the driver — the
// same shape as a data-API list that does a find + a follow-up lookup.
httpServer.get('/widgets', async (_req: any, res: any) => {
Expand All @@ -57,7 +58,7 @@ describe('Server-Timing db span over a real HTTP server (integration)', () => {
allowPerfDisclosure();
res.json({ all: all.length, one: one.length });
});
baseUrl = `http://127.0.0.1:${httpServer.getPort()}`;
baseUrl = `http://127.0.0.1:${httpServer.getPort!()}`;
}, 30_000);

afterAll(async () => {
Expand Down
1 change: 0 additions & 1 deletion scripts/slot-lookup-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"packages/cloud-connection/src/marketplace-install-local-plugin.ts": 17,
"packages/core/examples/kernel-features-example.ts": 5,
"packages/metadata-protocol/src/plugin.ts": 1,
"packages/metadata/src/plugin.ts": 1,
"packages/objectql/src/plugin.integration.test.ts": 23,
"packages/objectql/src/plugin.ts": 7,
"packages/plugins/plugin-approvals/src/approvals-plugin.ts": 9,
Expand Down
Loading