Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
/**
* Foundation-fixture plugin module + the route registration block the E2E
* server installs onto the `HonoRouter` pipeline facet.
*
* Since the `foundation-fix-gap-1-pipeline-router` change, the
* `HonoRouter` from `@suaveplan/api-router-hono` exposes a
* `registerPipelineRoute(...)` + `matchPipeline(...)` facet that
* satisfies `@suaveplan/api-host`'s `PipelineRouter` contract. The
* hand-rolled `createFixtureRouter()` shim that existed before has been
* removed; this module now just owns plugin module + route registration.
*/
import { definePlugin } from "@suaveplan/api-host/define-plugin";
import type { PipelineRouter } from "@suaveplan/api-host/request-pipeline";
import { HonoRouter } from "@suaveplan/api-router-hono";
import type { RequestContext } from "@suaveplan/api-types/plugin-context";
import { SuaveplanError, defineCodes } from "@suaveplan/error";
/** Plugin identifier — `<plugin>.<route>` per the route registration spec. */
const PLUGIN = "foundation-fixture";
/** Custom domain code emitted by `POST /api/throw-domain`. */
export const FIXTURE_CODES = defineCodes("FIXTURE", ["TEAPOT"] as const);
/** String-literal union of all FIXTURE.* codes, suitable for `SuaveplanError<TCode>`. */
export type FixtureCode =
(typeof FIXTURE_CODES)[keyof typeof FIXTURE_CODES];
/**
* Public-constructor subclass over the protected `SuaveplanError` base.
* Authors throw this directly; the host's error boundary serialises it
* to the canonical wire envelope.
*/
export class FixtureError extends SuaveplanError<FixtureCode> {
public constructor(
code: FixtureCode,
messageKey: string,
options?: {
readonly cause?: unknown;
readonly context?: Readonly<Record<string, unknown>>;
readonly traceId?: string;
}
) {
super(code, messageKey, options);
}
}
/** Secret name the plugin's `/api/secret-key` route reads. */
const SECRET_KEY = "FOUNDATION_SECRET";
/** Strip the leading `Uint8Array(...)`-style PII from a secret before returning. */
function redact(value: string): string {
if (value.length <= 4) return "****";
return `${value.slice(0, 2)}****${value.slice(-2)}`;
}
/**
* The plugin module — the host registers + starts this through
* `definePlugin`. The lifecycle hooks are no-ops because the route table
* is supplied separately via the in-memory `PipelineRouter` shim (the
* host pipeline does not expose a `ctx.routes(...)` surface in Stage 4).
*/
export const foundationFixturePlugin = definePlugin({
name: PLUGIN,
version: "0.1.0",
requires: [] as const,
isolation: "trusted",
trustLevel: "first-party",
start: () => undefined,
stop: () => undefined,
});
/**
* Bridge type — the host's `PipelineRouter` surface backed by a
* `HonoRouter`'s pipeline facet, with detach/attach hooks for the
* lifecycle tests. We wrap the underlying `HonoRouter` via `matchPipeline`
* so the host pipeline sees a plain `PipelineRouter`.
*/
export interface FixtureRouter extends PipelineRouter {
/** Remove the foundation-fixture plugin's routes (lifecycle test). */
detachFixture(): void;
/** Reinstate the foundation-fixture plugin's routes. */
attachFixture(): void;
}
/** Register the foundation-fixture routes onto a fresh `HonoRouter`. */
function installFixtureRoutes(hr: HonoRouter): void {
hr.registerPipelineRoute({
plugin: PLUGIN,
method: "GET",
path: "/api/hello",
handler: () => ({ ok: true, plugin: PLUGIN }),
});
hr.registerPipelineRoute({
plugin: PLUGIN,
method: "GET",
path: "/api/echo/:msg",
handler: (raw) => {
const ctx = raw as RequestContext & {
params: { msg: string };
};
return {
msg: ctx.params.msg,
tenantId: ctx.tenant?.id ?? null,
principal: ctx.principal ?? null,
};
},
});
hr.registerPipelineRoute({
plugin: PLUGIN,
method: "POST",
path: "/api/throw-domain",
handler: () => {
throw new FixtureError(
FIXTURE_CODES.TEAPOT,
"deliberately throwing teapot for E2E coverage"
);
},
statusMap: { "FIXTURE.TEAPOT": 418 },
});
hr.registerPipelineRoute({
plugin: PLUGIN,
method: "GET",
path: "/api/secret-key",
handler: async (raw) => {
const ctx = raw as RequestContext;
const value = await ctx.secrets.require(SECRET_KEY);
return { name: SECRET_KEY, redacted: redact(value) };
},
});
// foundation-fix-gap-4 — proves `ctx.db` is wired through the
// request pipeline to the tenant-scoped wrapper provisioned in
// server.ts. Inserts then reads back a row through the scoped DB.
hr.registerPipelineRoute({
plugin: PLUGIN,
method: "GET",
path: "/api/db-ping",
handler: async (raw) => {
const ctx = raw as RequestContext;
if (ctx.db === null) {
return { ok: false, reason: "no-db" };
}
await ctx.db.execute(
"INSERT INTO pings (note) VALUES (?)",
["hello"]
);
const rows = (await ctx.db.query(
"SELECT note FROM pings WHERE note = ?",
["hello"]
)) as ReadonlyArray<{ note: string }>;
return {
ok: true,
tenant: ctx.tenant?.id ?? null,
count: rows.length,
first: rows[0]?.note ?? null,
};
},
});
}
export function createFixtureRouter(): FixtureRouter {
const hr = new HonoRouter();
installFixtureRoutes(hr);
let attached = true;
return {
match(method: string, url: string) {
if (!attached) return null;
// HonoPipelineRoute is structurally compatible with the host's
// PipelineRoute (every required field is present); the body
// schema field differs only in nominal type (Zod schema vs
// unknown). Cast through unknown to bridge the two declared
// shapes — see HonoPipelineRouteOptions in the api-router-hono
// pipeline types module.
return hr.matchPipeline(method, url) as unknown as ReturnType<
PipelineRouter["match"]
>;
},
detachFixture(): void {
attached = false;
},
attachFixture(): void {
attached = true;
},
};
}

View File

@@ -0,0 +1,268 @@
/**
* E2E boot — composes the Stage-1..4 foundation packages into a real
* `createHost(...)`-driven Bun.serve server.
*
* Wiring deviations + foundation gaps (see report):
*
* 1. (CLOSED via `foundation-fix-gap-1-pipeline-router`) The host's
* `PipelineRouter` is now satisfied directly by the `HonoRouter`'s
* pipeline facet — `registerPipelineRoute` + `matchPipeline`. The
* E2E fixture wraps that via `createFixtureRouter()` for
* detach/attach lifecycle hooks only.
*
* 2. (CLOSED — foundation-fix-gap-2) `api-tenant-provider` now ships
* `tenantResolverChainFromProvider(...)` so callers no longer
* hand-roll the header -> provider.lookup chain. `TenantRecord`
* now carries `slug` directly.
*
* 3. (CLOSED — foundation-fix-gap-3) `HostAdapters` now exposes a
* `secrets` slot. The pipeline + lifecycle drivers thread the
* adapter (or an `EmptySecrets` default) into `ctx.secrets`, so
* route handlers + lifecycle hooks resolve secrets through the
* typed `ctx` instead of a hand-rolled closure binding.
*
* 4. (CLOSED — foundation-fix-gap-4) `HostAdapters` now splits the
* narrow `bun:sqlite`-shaped ledger handle (`ledgerDb`, host-private)
* from the multi-engine `AdminDatabaseLike` façade (`db`, plugin-
* facing). The E2E wires `createDbAdapter({ engine: "sqlite", url:
* ":memory:" }).admin` into the `db` slot and keeps `bun:sqlite` on
* `ledgerDb`.
*/
import { Database } from "bun:sqlite";
import {
createDbAdapter,
type DbAdapter,
registerEngine,
} from "@suaveplan/api-db-adapter";
import type {
AdminDatabaseLike,
DbParams,
} from "@suaveplan/api-types/db";
import { createHost } from "@suaveplan/api-host/host";
import type {
Host,
HostAdapters,
} from "@suaveplan/api-host/host";
import type { PipelineRoute } from "@suaveplan/api-host/request-pipeline";
import { SessionResolverChain } from "@suaveplan/api-auth";
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous";
import { createEnvSecrets } from "@suaveplan/api-secrets-env";
import { tenantResolverChainFromProvider } from "@suaveplan/api-tenant-provider/chain";
import { createMemoryTenantProvider } from "@suaveplan/api-tenant-provider-memory";
import type { TenantId } from "@suaveplan/api-types/tenant";
import type { TenantResolverChain } from "@suaveplan/api-types/tenant-resolver";
import {
createFixtureRouter,
foundationFixturePlugin,
type FixtureRouter,
} from "./fixture-plugin.js";
/**
* Register a `bun:sqlite`-backed engine factory under the canonical
* `"sqlite"` engine name. The factory normalises positional params and
* returns an `AdminDatabaseLike` over the in-memory database. The
* `api-db-adapter` registry is empty by default — no engine ships
* pre-installed — so this wiring lives in the consumer (the E2E here,
* production hosts elsewhere).
*/
function ensureSqliteEngine(): void {
registerEngine("sqlite", async () => {
const handle = new Database(":memory:");
const normalize = (params: DbParams | undefined): unknown[] => {
if (params === undefined) return [];
if (Array.isArray(params)) return [...params];
return [];
};
const admin: AdminDatabaseLike = {
engine: "sqlite",
async query<R = unknown>(
sql: string,
params?: DbParams
): Promise<R> {
const stmt = handle.query(sql);
const rows = stmt.all(...(normalize(params) as never[]));
return rows as unknown as R;
},
async execute(sql, params) {
const stmt = handle.query(sql);
const result = stmt.run(...(normalize(params) as never[]));
return { rowsAffected: Number(result.changes ?? 0) };
},
async close(): Promise<void> {
handle.close();
},
};
return admin;
});
}
/** Build the in-memory tenant chain backed by the provider package. */
function buildTenantChain(): TenantResolverChain {
const now = new Date().toISOString();
const provider = createMemoryTenantProvider({
tenants: [
{
id: "t1" as TenantId,
slug: "tenant-1",
status: "active",
createdAt: now,
},
{
id: "t2" as TenantId,
slug: "tenant-2",
status: "active",
createdAt: now,
},
],
});
return tenantResolverChainFromProvider(provider, {
headerName: "X-Tenant-Id",
});
}
/** Compose the full `HostAdapters` bag. */
export async function buildAdapters(
router: FixtureRouter
): Promise<{ adapters: HostAdapters; dbAdapter: DbAdapter }> {
const auth = new SessionResolverChain([createAnonymousResolver()]);
const tenant = buildTenantChain();
// Per-IP token-bucket-ish counter — 5 requests / minute on
// `/api/hello` triggers the 6th rate-limit response.
const counts = new Map<string, { used: number; resetAt: number }>();
const RATE_LIMIT = 5;
const RATE_WINDOW_MS = 60_000;
const rateLimit = {
consume(key: string, _weight: number): boolean {
const now = Date.now();
const cur = counts.get(key);
if (cur === undefined || cur.resetAt <= now) {
counts.set(key, { used: 1, resetAt: now + RATE_WINDOW_MS });
return true;
}
if (cur.used >= RATE_LIMIT) return false;
cur.used += 1;
return true;
},
};
// Idempotency replay — second call with the same key returns true so
// the pipeline short-circuits. We do NOT cache the response body —
// that's a Stage 5 concern.
const seen = new Set<string>();
const idempotency = {
has(key: string): boolean {
return seen.has(key);
},
set(key: string, _expiresAt: number): void {
seen.add(key);
},
};
const audit = { emit(): void {} };
// Build the consumer-facing admin DB via api-db-adapter. The in-memory
// SQLite engine is the simplest path; the E2E suite uses it to
// exercise the `ctx.db?.query(...)` end-to-end wiring.
ensureSqliteEngine();
const dbAdapter = await createDbAdapter({
engine: "sqlite",
url: ":memory:",
});
// Provision the `pings` table the /api/db-ping route reads/writes
// through the tenant-scoped wrapper. Admin DDL bypasses scoping.
await dbAdapter.admin.execute(
"CREATE TABLE IF NOT EXISTS pings (id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT NOT NULL, note TEXT NOT NULL)"
);
const secrets = createEnvSecrets({
allowedKeys: ["FOUNDATION_SECRET"],
source: () => ({ FOUNDATION_SECRET: "shh-very-secret" }),
});
const adapters: HostAdapters = {
router,
auth,
tenant,
capabilities: { forRequest: () => ({}) },
rateLimit,
idempotency,
csrf: { validate: () => true },
audit,
secrets,
adminPrincipal: {
resolve: () => ({ id: "admin", scopes: ["host:admin"] }),
},
span: {
start: () =>
({
setAttribute: () => undefined,
setStatus: () => undefined,
recordException: () => undefined,
end: () => undefined,
}) as never,
},
ledgerDb: new Database(
":memory:"
) as unknown as HostAdapters["ledgerDb"],
db: dbAdapter.admin,
};
return { adapters, dbAdapter };
}
/** Manifest shape the host expects — keep it minimal but spec-compliant. */
function fixtureManifest(): unknown {
return {
name: "foundation-fixture",
version: "0.1.0",
apiHost: "^0.1.0",
isolation: "trusted" as const,
trustLevel: "first-party" as const,
errors: { statusMap: { "FIXTURE.TEAPOT": 418 } },
};
}
export async function startServer(port: number): Promise<{
host: Host;
router: FixtureRouter;
stop: () => Promise<void>;
}> {
const router = createFixtureRouter();
const { adapters, dbAdapter } = await buildAdapters(router);
const host = await createHost({
adapters,
plugins: [
{
module: foundationFixturePlugin,
manifest: fixtureManifest() as never,
manifestHash: `sha256:${"0".repeat(64)}`,
specifier: "file:///foundation-fixture.ts",
},
],
adminPrefix: "/admin",
mode: "development",
// Foundation gap 5: opt into the legacy "public" tenant fallback
// so the existing E2E suite's `tenantId === "public"` assertion
// still holds. New deployments may leave this unset to receive
// the new default (`ctx.tenant === null` on resolver miss).
defaultTenant: { id: "public" as TenantId, slug: "public" },
});
await host.start();
const server = Bun.serve({
port,
async fetch(request: Request): Promise<Response> {
return host.fetch(request);
},
});
return {
host,
router,
async stop() {
await host.stop().catch(() => undefined);
await dbAdapter.close().catch(() => undefined);
server.stop(true);
},
};
}
if (import.meta.main) {
const port = Number.parseInt(process.env["PORT"] ?? "3500", 10);
const { host: _h } = await startServer(port);
// eslint-disable-next-line no-console
console.log(`[e2e-foundation-stack] listening on :${port}`);
}