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,7 @@
export {
createUnsafeGate,
getUnsafeGateInstruments,
resetUnsafeGateInstruments,
type UnsafeGateOptions,
type UnsafeQueryApi,
} from "./unsafe-query-gate.js";

View File

@@ -0,0 +1,84 @@
# `unsafe/unsafe-query-gate` — capability-gated escape hatch
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createUnsafeGate` builds the `ctx.db.unsafe` API exposed on every tenant-
scoped surface. The gate is the only sanctioned path for a plugin to issue
raw SQL that bypasses the tenant-scope rewriter — and the only path that is
required to be auditable. The capability declaration is read from the
plugin manifest at construction time; calls without the declaration throw
synchronously, before the engine sees the SQL.
Each permitted call increments `plugin.unsafe_query_total{plugin}` so operators
can graph which plugins are exercising the escape hatch and at what rate. A
high count is an audit signal, not an error.
## Features
- Capability check is a literal array membership on `db.unsafe-queries`.
- `plugin.unsafe_query_total` counter, attribute `plugin` carrying the
plugin name from `UnsafeGateOptions`.
- Lazy `getUnsafeGateInstruments()` so test harnesses can install a custom
`MeterProvider` BEFORE the gate is constructed.
- `resetUnsafeGateInstruments()` for tests that swap meter providers
between cases.
- Both `query` and `execute` are gated; the gate is symmetric across
statement shapes.
## Usage: Basic
```ts
import { createUnsafeGate } from "@suaveplan/api-db-adapter/unsafe";
const unsafe = createUnsafeGate({
plugin: "audit",
capabilities: ["db.unsafe-queries"],
admin,
});
const rows = await unsafe.query("SELECT * FROM audit_log");
```
## Usage: Advanced
```ts
import { metrics } from "@opentelemetry/api";
import {
createUnsafeGate,
resetUnsafeGateInstruments,
} from "@suaveplan/api-db-adapter/unsafe";
// Install a test MeterProvider FIRST.
metrics.setGlobalMeterProvider(testProvider);
resetUnsafeGateInstruments();
const denied = createUnsafeGate({
plugin: "user",
capabilities: [], // no db.unsafe-queries
admin,
});
await denied
.execute("DELETE FROM secrets")
.catch((e) => e.code === "DB_ADAPTER.UNSAFE_NOT_PERMITTED");
```
## API Reference
- `createUnsafeGate(opts: UnsafeGateOptions): UnsafeQueryApi`.
- `UnsafeQueryApi.query<R>(sql, params?): Promise<R>`.
- `UnsafeQueryApi.execute(sql, params?): Promise<{ rowsAffected }>`.
- `getUnsafeGateInstruments(): { meter, unsafeQueryTotal }`.
- `resetUnsafeGateInstruments(): void` — test only.
## Implementation Notes
The capability check happens at gate construction and again on each call (so a
permissions change applied between requests does not require the host to swap
the gate). Counter attributes use `plugin` rather than the prefixed
`db.plugin` to match the metric naming in the design doc. The gate never
inspects the SQL — that is the host's audit-sink job; the gate's contract is
"capability or `UNSAFE_NOT_PERMITTED`".

View File

@@ -0,0 +1,91 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { createMemoryAdmin } from "../../tests/_helpers/memory-admin.js";
import { DB_ADAPTER_CODES } from "../error-codes/index.js";
import {
createUnsafeGate,
getUnsafeGateInstruments,
resetUnsafeGateInstruments,
} from "./unsafe-query-gate.js";
describe("createUnsafeGate", () => {
it("throws when capability not declared (query)", {
timeout: 5000,
}, async () => {
const admin = createMemoryAdmin();
const gate = createUnsafeGate({
plugin: "audit",
capabilities: [],
admin,
});
await expect(gate.query("SELECT 1")).rejects.toMatchObject({
code: DB_ADAPTER_CODES.UNSAFE_NOT_PERMITTED,
});
});
it("throws when capability not declared (execute)", {
timeout: 5000,
}, async () => {
const admin = createMemoryAdmin();
const gate = createUnsafeGate({
plugin: "audit",
capabilities: [],
admin,
});
await expect(gate.execute("DELETE FROM x")).rejects.toMatchObject({
code: DB_ADAPTER_CODES.UNSAFE_NOT_PERMITTED,
});
});
it("emits plugin.unsafe_query_total when allowed (query)", {
timeout: 5000,
}, async () => {
resetUnsafeGateInstruments();
const admin = createMemoryAdmin();
await admin.execute("INSERT INTO t (id, tenant_id) VALUES (?, ?)", [
"a",
"x",
]);
const gate = createUnsafeGate({
plugin: "audit",
capabilities: ["db.unsafe-queries"],
admin,
});
const rows = await gate.query<unknown[]>("SELECT * FROM t");
expect(Array.isArray(rows)).toBe(true);
// counter exists & is wired
const i = getUnsafeGateInstruments();
expect(i.unsafeQueryTotal).toBeDefined();
});
it("execute path also counts and delegates", {
timeout: 5000,
}, async () => {
const admin = createMemoryAdmin();
const gate = createUnsafeGate({
plugin: "audit",
capabilities: ["db.unsafe-queries"],
admin,
});
const r = await gate.execute(
"INSERT INTO t (id, tenant_id) VALUES (?, ?)",
["a", "x"]
);
expect(r.rowsAffected).toBe(1);
});
it("getUnsafeGateInstruments is cached across calls", {
timeout: 5000,
}, () => {
resetUnsafeGateInstruments();
const a = getUnsafeGateInstruments();
const b = getUnsafeGateInstruments();
expect(a).toBe(b);
});
it("reset clears the cache", { timeout: 5000 }, () => {
const a = getUnsafeGateInstruments();
resetUnsafeGateInstruments();
const b = getUnsafeGateInstruments();
expect(a).not.toBe(b);
});
});

View File

@@ -0,0 +1,90 @@
import type { Counter, Meter } from "@opentelemetry/api";
import { metrics } from "@opentelemetry/api";
import type { AdminDatabaseLike, DbParams } from "@suaveplan/api-types/db";
import { DB_ADAPTER_CODES, DbAdapterError } from "../error-codes/index.js";
/**
* Public shape of `ctx.db.unsafe`. Always present on the tenant-scoped surface
* created by the host; calls are gated at runtime by the
* `db.unsafe-queries` capability.
*/
export interface UnsafeQueryApi {
query<R = unknown>(sql: string, params?: DbParams): Promise<R>;
execute(
sql: string,
params?: DbParams
): Promise<{ readonly rowsAffected: number }>;
}
/** Construction options for {@link createUnsafeGate}. */
export interface UnsafeGateOptions {
/** Plugin name — surfaced on the OTel counter label. */
readonly plugin: string;
/** Capability declarations from the plugin manifest. */
readonly capabilities: ReadonlyArray<string>;
/** Underlying admin adapter to delegate to when permitted. */
readonly admin: AdminDatabaseLike;
}
const UNSAFE_CAPABILITY = "db.unsafe-queries";
interface CachedInstruments {
readonly meter: Meter;
readonly unsafeQueryTotal: Counter;
}
let cached: CachedInstruments | undefined;
/** Lazily resolve OTel instruments — host may install a `MeterProvider` first. */
export function getUnsafeGateInstruments(): CachedInstruments {
if (cached !== undefined) return cached;
const meter = metrics.getMeter("@suaveplan/api-db-adapter");
cached = {
meter,
unsafeQueryTotal: meter.createCounter("plugin.unsafe_query_total", {
description:
"Number of db.unsafe.query / db.unsafe.execute calls made by a plugin.",
}),
};
return cached;
}
/** Test helper: reset the cached instruments so a new MeterProvider is picked up. */
export function resetUnsafeGateInstruments(): void {
cached = undefined;
}
/**
* Build the `db.unsafe` capability-gated escape hatch.
*
* - If `capabilities` does NOT include `db.unsafe-queries`, every call throws
* `DB_ADAPTER.UNSAFE_NOT_PERMITTED` without touching the engine.
* - If the capability IS declared, the call is delegated to `admin` and the
* `plugin.unsafe_query_total{plugin}` counter increments by 1.
*/
export function createUnsafeGate(opts: UnsafeGateOptions): UnsafeQueryApi {
const allowed = opts.capabilities.includes(UNSAFE_CAPABILITY);
const reject = (): never => {
throw new DbAdapterError(
DB_ADAPTER_CODES.UNSAFE_NOT_PERMITTED,
`plugin '${opts.plugin}' did not declare '${UNSAFE_CAPABILITY}' capability`,
{ context: { plugin: opts.plugin } }
);
};
return {
query: async <R>(sql: string, params?: DbParams): Promise<R> => {
if (!allowed) reject();
getUnsafeGateInstruments().unsafeQueryTotal.add(1, {
plugin: opts.plugin,
});
return await opts.admin.query<R>(sql, params);
},
execute: async (sql: string, params?: DbParams) => {
if (!allowed) reject();
getUnsafeGateInstruments().unsafeQueryTotal.add(1, {
plugin: opts.plugin,
});
return await opts.admin.execute(sql, params);
},
};
}