diff --git a/.changeset/ponytail-trims.md b/.changeset/ponytail-trims.md new file mode 100644 index 0000000..1ef3899 --- /dev/null +++ b/.changeset/ponytail-trims.md @@ -0,0 +1,26 @@ +--- +"@btravstack/contract": minor +"@btravstack/di": minor +"@btravstack/config": minor +"@btravstack/core": minor +"@btravstack/testing": minor +"@btravstack/observability": minor +"@btravstack/http": minor +"@btravstack/temporal": minor +"@btravstack/amqp": minor +--- + +The `Unmet` type is gone from `@btravstack/di` + +Its documented purpose — a shaped module re-declaring the gates with it — was +impossible to serve: declaration emit keeps the alias unreduced, and the +unreduced form names imported modules' internal ports (TS2883 on the first +consumer that exports a composition root), which is why every in-repo sugar +already inlined the computation instead. Inline it; `NeedsGate` is unchanged +and still exported. + +Internal trims alongside, none of them surface: `@btravstack/http` no longer +memoises scheme ports (di resolves by id, so a fresh class per call is the same +lookup — measured), and `HasMark`, `authenticatorPort` and `Http.authenticators` +now carry TSDoc naming the external consumer each exists for, so their lack of +an in-repo caller stops reading as dead surface. diff --git a/packages/di/CLAUDE.md b/packages/di/CLAUDE.md index f54f2e7..8084eae 100644 --- a/packages/di/CLAUDE.md +++ b/packages/di/CLAUDE.md @@ -115,7 +115,7 @@ missing: N]`. What that **prints** is the arity line alone — private). `Provider(port)({ name: Dep }, arm)`'s return type is `Provider
&
{ readonly port: typeof port }` — the provider carries its port class typed,
so `provider.port` is what a dependent lists in its deps; purely additive. `AnyModule`, `AnyProvider`,
- `Exportable`, **`NeedsGate` and `Unmet`** are exported so a package offering a **shaped module** (a
+ `Exportable` and **`NeedsGate`** are exported so a package offering a **shaped module** (a
starter's `HttpModule(name)({ router, imports, provides, exports })` sugar,
which appends its own import and export to what the application wrote) can
constrain its `imports`/`provides`/`exports` the way `Module(name)` does and
@@ -240,8 +240,10 @@ broke something:
reason: declaration emit keeps a named alias unreduced, and the unreduced
form names the imported modules' internal ports — TS2883/TS4023 on the first
consumer that exports a composition root (`OrderApi` "cannot be named
- without a reference to 'OrderDatabase'"). `Unmet` is exported for the
- starters' sugars and used only inside parameter types.
+ without a reference to 'OrderDatabase'"). The same wall is why there is no
+ exported `Unmet` helper: a shaped module could not name it in a return type
+ either, so the computation is inlined wherever it appears — there was such an
+ export once, and its documented purpose was impossible to serve.
The channel itself is unchanged: `Needs` is still what the module genuinely
owes, computed, not what it declared. Declaring a port nothing owes is inert —
@@ -263,7 +265,7 @@ of this gate noisy.
### The gate cannot be computed generically — and that is why the casts exist
-`Unmet` over a generic tuple `I` is a deferred conditional, and no object
+The unmet-needs computation over a generic tuple `I` is a deferred conditional, and no object
literal satisfies one. So a **generic wrapper around `Module(name)`** — the
three starter sugars, `start`'s `Env` wrapper, `@btravstack/testing`'s
`tapped`, a factory like `makeAppModule` — cannot satisfy the gate at its own
diff --git a/packages/di/src/index.ts b/packages/di/src/index.ts
index c331767..4eb4ccc 100644
--- a/packages/di/src/index.ts
+++ b/packages/di/src/index.ts
@@ -51,5 +51,5 @@ export { Module } from "./module.js";
// through a named generic alias was tried and removed: declaration emit keeps
// such an alias unreduced and cannot name imported modules' internal ports —
// TS2883.)
-export type { AnyModule, AnyProvider, Exportable, NeedsGate, Unmet } from "./module.js";
+export type { AnyModule, AnyProvider, Exportable, NeedsGate } from "./module.js";
export type { ScopedOptions } from "./build.js";
diff --git a/packages/di/src/module.ts b/packages/di/src/module.ts
index b4a15da..579f316 100644
--- a/packages/di/src/module.ts
+++ b/packages/di/src/module.ts
@@ -160,20 +160,6 @@ type ResolvedExports | NeedsOfModule,
- Available
->;
-
/**
* The declaration gate. A port **this module's own providers** read, and that
* nothing here satisfies, is an error unless it is named in `needs` — so a
@@ -254,12 +240,12 @@ function ModuleDeclaration | ErrOfModule,
diff --git a/packages/http/src/auth.spec.ts b/packages/http/src/auth.spec.ts
index da005d8..1157eed 100644
--- a/packages/http/src/auth.spec.ts
+++ b/packages/http/src/auth.spec.ts
@@ -65,12 +65,27 @@ describe("an authenticated procedure", () => {
});
});
+describe("substituting one scheme's authenticator", () => {
+ it("serves a caller the real table would refuse, without building the verifier", async ({
+ rpcSubstituted,
+ }) => {
+ // GIVEN a hand-rolled composition providing a stub on the scheme's own
+ // port — recomposition, not a second registry: the TokenTable-backed
+ // authenticator is not in this graph at all
+ const client = rpcSubstituted("not-in-any-table");
+
+ // WHEN a marked procedure is called with a token only the stub accepts
+ // THEN the stub named the caller
+ await expect(client.orders.whoami({ id: "o-1" })).resolves.toEqual({ userId: "u-stub" });
+ });
+});
+
describe("an authenticator with dependencies of its own", () => {
it("is built from the services it declared, and names the caller with them", async ({
rpcVerified,
}) => {
// GIVEN a client presenting a token only the injected table knows
- const client = await rpcVerified("keyed");
+ const client = rpcVerified("keyed");
// WHEN a marked procedure is called
// THEN the authenticator resolved it through the dependency di gave it —
diff --git a/packages/http/src/auth.ts b/packages/http/src/auth.ts
index 6e76a11..cf56491 100644
--- a/packages/http/src/auth.ts
+++ b/packages/http/src/auth.ts
@@ -80,14 +80,26 @@ const ports = new Map = S extends S
- ? { readonly scheme: S; readonly identity: S extends keyof Schemes ? Schemes[S] : never }
+export type Tagged = S extends S
+ ? { readonly scheme: S; readonly identity: Schemes[S] }
: never;
/**
@@ -29,8 +29,8 @@ export type Tagged = S extends S
*/
export type Principal = [S] extends [never]
? never
- : IsUnion extends true
- ? Tagged
- : S extends keyof Schemes
- ? Schemes[S]
- : never;
+ : [S] extends [keyof Schemes]
+ ? IsUnion extends true
+ ? Tagged
+ : Schemes[S & keyof Schemes]
+ : never;
diff --git a/packages/http/src/test-fixtures.ts b/packages/http/src/test-fixtures.ts
index 144f4fb..5a0391f 100644
--- a/packages/http/src/test-fixtures.ts
+++ b/packages/http/src/test-fixtures.ts
@@ -37,13 +37,14 @@ import { CORSHandlerPlugin } from "@orpc/server/plugins";
import { ErrAsync, OkAsync, fromSafePromise } from "unthrown";
import { test } from "vitest";
-import { HttpAuthenticator, Unauthenticated } from "./auth.js";
+import { HttpAuthenticator, Unauthenticated, authenticatorPort } from "./auth.js";
import { defineHttp } from "./define-http.js";
import { HttpHandler } from "./handler.js";
import { HttpModule } from "./http-module.js";
import {
HttpConfig,
HttpRuntime,
+ http,
httpModule,
type HttpInfo,
type HttpOptions,
@@ -329,6 +330,27 @@ const rpcVerifiedAppOf = () =>
],
});
+/**
+ * The substitution seam `authenticatorPort` exists for: a hand-rolled
+ * composition provides its OWN authenticator on the scheme's port and never
+ * spreads `router.authenticators` — recomposition, this repo's stated way to
+ * swap an adapter, not a second `defineHttp` registry and not a provider
+ * layered over one (di refuses two providers for one port). The real,
+ * `TokenTable`-backed authenticator is not in this graph at all, which is the
+ * point: the stub composition never builds the verifier.
+ */
+const rpcSubstitutedAppOf = () =>
+ Module("RpcSubstitutedApp")({
+ imports: [http({ port: 0, hostname: "127.0.0.1" })],
+ provides: [
+ verifiedRouter,
+ Provider(authenticatorPort("user"))({
+ value: () => OkAsync({ userId: "u-stub" }),
+ }),
+ ],
+ exports: [HttpRuntime],
+ });
+
/** `Bearer ${token}`, or no credentials at all when `token` is `undefined`. */
const linkOf = (origin: string, token: string | undefined) =>
new RPCLink({
@@ -588,7 +610,9 @@ export type HttpFixtures = {
* dependency, resolved by an imported module — the form `defineHttp` binds
* through `Provider(port)(deps, arm)`. Shut down by the fixture.
*/
- readonly rpcVerified: (token: string) => Promise