@nwire/wires 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex Gefter / 200apps Ltd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @nwire/wires
2
+
3
+ > Transport contract — the abstract base every Nwire transport extends, and the shape foreign hosts implement to plug in.
4
+
5
+ ## What it is
6
+
7
+ `NwireInterface` is the structural shape every transport satisfies (`http`, `queue`, `mcp`, `graphql`, future `ws`/`grpc`). `InterfaceBuilder` is the abstract base each transport extends.
8
+
9
+ The verb surface is uniform across transports:
10
+
11
+ ```
12
+ .use(...plugins) augment the chain
13
+ .wire(binding, handler?, o?) bind an outbound surface
14
+ .from(source) declare an inbound stream
15
+ .mount(target) attach this iface to a host
16
+ .run(opts?) → Lifecycle serve standalone
17
+ .boot(opts?) → Booted<T> build without listening
18
+ ```
19
+
20
+ Plus two internal seams (`.manifest()` for scanner/Studio; `.attach(host)` for host lifecycle) and one DI sugar (`.provide(container)`).
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pnpm add @nwire/wires
26
+ ```
27
+
28
+ ## Within nwire-app
29
+
30
+ You usually never import this package directly — `@nwire/http` / `@nwire/queue` / `@nwire/mcp` each ship their own builder that already extends `InterfaceBuilder`. Touch this package only when writing a new transport or a foreign-host adapter.
31
+
32
+ ```ts
33
+ import { endpoint } from "@nwire/endpoint";
34
+ import { http } from "@nwire/http"; // extends InterfaceBuilder
35
+ import { queue } from "@nwire/queue"; // extends InterfaceBuilder
36
+
37
+ const api = http().wire(getUsers);
38
+ const worker = queue().wire(sendEmail);
39
+
40
+ await endpoint("app").serve(api).serve(worker).run();
41
+ ```
42
+
43
+ ## API
44
+
45
+ - `NwireInterface` — structural shape `endpoint().serve()` consumes.
46
+ - `InterfaceBuilder<TBinding, TPlugin, TFromSource, TMountTarget, TArtifact>` — abstract base with the six verbs + manifest + attach.
47
+ - `Lifecycle` / `RunOptions` / `Booted` / `BootOptions` / `InterfaceManifest` — return + option types shared across transports.
48
+ - `makeContainerPlugin` / `isContainerPlugin` — sentinel for transports that need to lift containers out of the `.use()` chain.
49
+ - `isNwireInterface` — structural type narrow.
50
+ - Re-exports `HandlerLike` + `HandlerDefinition` from `@nwire/handler` so transport authors have one import.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `@nwire/wires/cron` — cron binding factory.
3
+ *
4
+ * import { cron } from "@nwire/wires/cron";
5
+ *
6
+ * createInterface().wire(cron("0 *\/15 * * * *"), refreshFeed);
7
+ *
8
+ * Adopter (`@nwire/cron`) filters wires by `binding.$adapter === "cron"`
9
+ * and schedules each handler against the cron expression.
10
+ */
11
+ import type { Binding } from "../index.js";
12
+ export interface CronBindingOptions {
13
+ /** Timezone (IANA) the cron expression is evaluated in. Adopter default applies otherwise. */
14
+ readonly timezone?: string;
15
+ /** Free-form source tag — Studio and observability group by it. */
16
+ readonly source?: string;
17
+ /**
18
+ * Set true to opt the binding out of the adopter's overlap guard. Default
19
+ * false — adopters skip overlapping invocations by default.
20
+ */
21
+ readonly overlap?: boolean;
22
+ }
23
+ export interface CronBinding extends Binding {
24
+ readonly $kind: "binding";
25
+ readonly $adapter: "cron";
26
+ readonly kind: "cron";
27
+ readonly schedule: string;
28
+ readonly timezone?: string;
29
+ readonly source?: string;
30
+ readonly overlap?: boolean;
31
+ /** Chainable source tag — returns a fresh binding. */
32
+ from(source: string): CronBinding;
33
+ }
34
+ export declare function cron(schedule: string, options?: CronBindingOptions): CronBinding;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `@nwire/wires/cron` — cron binding factory.
3
+ *
4
+ * import { cron } from "@nwire/wires/cron";
5
+ *
6
+ * createInterface().wire(cron("0 *\/15 * * * *"), refreshFeed);
7
+ *
8
+ * Adopter (`@nwire/cron`) filters wires by `binding.$adapter === "cron"`
9
+ * and schedules each handler against the cron expression.
10
+ */
11
+ function buildBinding(schedule, options, source) {
12
+ const binding = {
13
+ $kind: "binding",
14
+ $adapter: "cron",
15
+ kind: "cron",
16
+ schedule,
17
+ ...(options ?? {}),
18
+ ...(source !== undefined ? { source } : {}),
19
+ };
20
+ Object.defineProperty(binding, "from", {
21
+ value: (next) => buildBinding(schedule, options, next),
22
+ enumerable: false,
23
+ writable: false,
24
+ configurable: true,
25
+ });
26
+ return binding;
27
+ }
28
+ export function cron(schedule, options) {
29
+ return buildBinding(schedule, options, undefined);
30
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `@nwire/wires/graphql` — GraphQL field binding factories.
3
+ *
4
+ * import { query, mutation, subscription } from "@nwire/wires/graphql";
5
+ *
6
+ * createInterface()
7
+ * .wire(query("orders"), listOrders)
8
+ * .wire(mutation("createOrder"), createOrder);
9
+ *
10
+ * Thin universal-routing shape — the wire just names which GraphQL
11
+ * field this handler implements. The adopter (Apollo / Yoga / raw)
12
+ * decides how to map: build a resolver tree, federate a schema, stitch
13
+ * a remote endpoint, etc. Schemas live with the typedefs the adopter
14
+ * loads, not on the binding.
15
+ *
16
+ * Bindings carry `$adapter: "graphql"` so the GraphQL adopter filters
17
+ * via `interface.forAdapter("graphql")`.
18
+ */
19
+ import type { Binding } from "../index.js";
20
+ export type GraphqlFieldKind = "query" | "mutation" | "subscription";
21
+ export interface GraphqlBindingOptions {
22
+ /** Optional parent type for fields under custom roots (federated graphs). */
23
+ readonly typeName?: string;
24
+ readonly source?: string;
25
+ }
26
+ export interface GraphqlBinding extends Binding {
27
+ readonly $kind: "binding";
28
+ readonly $adapter: "graphql";
29
+ readonly kind: GraphqlFieldKind;
30
+ readonly field: string;
31
+ readonly typeName?: string;
32
+ readonly source?: string;
33
+ from(source: string): GraphqlBinding;
34
+ }
35
+ export declare function query(field: string, options?: GraphqlBindingOptions): GraphqlBinding;
36
+ export declare function mutation(field: string, options?: GraphqlBindingOptions): GraphqlBinding;
37
+ export declare function subscription(field: string, options?: GraphqlBindingOptions): GraphqlBinding;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `@nwire/wires/graphql` — GraphQL field binding factories.
3
+ *
4
+ * import { query, mutation, subscription } from "@nwire/wires/graphql";
5
+ *
6
+ * createInterface()
7
+ * .wire(query("orders"), listOrders)
8
+ * .wire(mutation("createOrder"), createOrder);
9
+ *
10
+ * Thin universal-routing shape — the wire just names which GraphQL
11
+ * field this handler implements. The adopter (Apollo / Yoga / raw)
12
+ * decides how to map: build a resolver tree, federate a schema, stitch
13
+ * a remote endpoint, etc. Schemas live with the typedefs the adopter
14
+ * loads, not on the binding.
15
+ *
16
+ * Bindings carry `$adapter: "graphql"` so the GraphQL adopter filters
17
+ * via `interface.forAdapter("graphql")`.
18
+ */
19
+ function buildBinding(kind, field, options, source) {
20
+ const binding = {
21
+ $kind: "binding",
22
+ $adapter: "graphql",
23
+ kind,
24
+ field,
25
+ ...(options ?? {}),
26
+ ...(source !== undefined ? { source } : {}),
27
+ };
28
+ Object.defineProperty(binding, "from", {
29
+ value: (next) => buildBinding(kind, field, options, next),
30
+ enumerable: false,
31
+ writable: false,
32
+ configurable: true,
33
+ });
34
+ return binding;
35
+ }
36
+ export function query(field, options) {
37
+ return buildBinding("query", field, options, undefined);
38
+ }
39
+ export function mutation(field, options) {
40
+ return buildBinding("mutation", field, options, undefined);
41
+ }
42
+ export function subscription(field, options) {
43
+ return buildBinding("subscription", field, options, undefined);
44
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * `@nwire/wires/http` — HTTP binding factories.
3
+ *
4
+ * import { post, get, del, put, patch } from "@nwire/wires/http";
5
+ *
6
+ * createInterface()
7
+ * .wire(post("/orders", { body: CreateOrder }), placeOrder)
8
+ * .wire(get("/orders/:id", { params: OrderParams }), getOrder);
9
+ *
10
+ * Bindings carry `$adapter: "http"` so an HTTP adopter filters them via
11
+ * `interface.forAdapter("http")`. Schemas are zod-shaped; the adopter
12
+ * parses request data at dispatch time.
13
+ *
14
+ * Foreign-import factories (`fromExpress` / `fromKoa` / `fromFastify`)
15
+ * live in the adopter packages, not here.
16
+ */
17
+ import type { ZodTypeAny, z } from "zod";
18
+ import type { Binding } from "../index.js";
19
+ export type HttpVerb = "get" | "post" | "put" | "patch" | "delete";
20
+ /** Lifecycle of an exposed operation — surfaces on the OpenAPI doc. */
21
+ export type RouteStatus = "draft" | "active" | "deprecated" | "sunset";
22
+ /**
23
+ * OpenAPI / contract metadata for one route. The HTTP adopter reads it at
24
+ * boot to emit the operation id, summary, tags, response shapes, error
25
+ * shapes, and lifecycle status.
26
+ */
27
+ export interface RouteOpenApi {
28
+ readonly operation: string;
29
+ readonly version?: number;
30
+ readonly status?: RouteStatus;
31
+ readonly summary?: string;
32
+ readonly description?: string;
33
+ readonly tags?: ReadonlyArray<string>;
34
+ readonly returns?: ReadonlyArray<any>;
35
+ readonly errors?: ReadonlyArray<any>;
36
+ }
37
+ /**
38
+ * Per-route middleware shape — `unknown` at this layer to keep
39
+ * `@nwire/wires/http` free of a Koa dep. Adopters narrow at consume
40
+ * time.
41
+ */
42
+ export type RouteMiddleware = unknown;
43
+ /** Schemas attached to a route binding. */
44
+ export interface RouteSchemas {
45
+ readonly params?: ZodTypeAny;
46
+ readonly body?: ZodTypeAny;
47
+ readonly query?: ZodTypeAny;
48
+ readonly middleware?: readonly RouteMiddleware[];
49
+ readonly openapi?: RouteOpenApi;
50
+ }
51
+ type ZodOutput<T> = T extends ZodTypeAny ? z.output<T> : unknown;
52
+ /**
53
+ * Infer the handler's flat input type from a `RouteSchemas` shape. The
54
+ * adopter merges params + body + query into a single object the handler
55
+ * sees — this type mirrors that merge.
56
+ */
57
+ export type InferRouteInput<S> = (S extends {
58
+ params?: infer P;
59
+ } ? P extends ZodTypeAny ? ZodOutput<P> : unknown : unknown) & (S extends {
60
+ query?: infer Q;
61
+ } ? (Q extends ZodTypeAny ? ZodOutput<Q> : unknown) : unknown) & (S extends {
62
+ body?: infer B;
63
+ } ? (B extends ZodTypeAny ? ZodOutput<B> : unknown) : unknown);
64
+ /**
65
+ * `RouteBinding` — the binding shape HTTP wires carry. Extends the base
66
+ * `Binding` contract with HTTP specifics (verb, path, schemas, OpenAPI).
67
+ * `$adapter` is fixed to `"http"`; an HTTP adopter consumes wires by
68
+ * that tag.
69
+ */
70
+ export interface RouteBinding<TInput = unknown> extends Binding {
71
+ readonly $kind: "binding";
72
+ readonly $adapter: "http";
73
+ readonly kind: "route";
74
+ readonly verb: HttpVerb;
75
+ readonly path: string;
76
+ readonly params?: ZodTypeAny;
77
+ readonly body?: ZodTypeAny;
78
+ readonly query?: ZodTypeAny;
79
+ readonly middleware?: readonly RouteMiddleware[];
80
+ readonly openapi?: RouteOpenApi;
81
+ readonly source?: string;
82
+ /**
83
+ * Chainable source tag — Studio and observability group by it.
84
+ * Returns a fresh binding; original is unchanged.
85
+ */
86
+ from(source: string): RouteBinding<TInput>;
87
+ /** Phantom — TS inference of the handler's input shape. */
88
+ readonly __inputType?: TInput;
89
+ }
90
+ export declare function get<S extends RouteSchemas = RouteSchemas>(path: string, schemas?: S): RouteBinding<InferRouteInput<S>>;
91
+ export declare function post<S extends RouteSchemas = RouteSchemas>(path: string, schemas?: S): RouteBinding<InferRouteInput<S>>;
92
+ export declare function put<S extends RouteSchemas = RouteSchemas>(path: string, schemas?: S): RouteBinding<InferRouteInput<S>>;
93
+ export declare function patch<S extends RouteSchemas = RouteSchemas>(path: string, schemas?: S): RouteBinding<InferRouteInput<S>>;
94
+ export declare function del<S extends RouteSchemas = RouteSchemas>(path: string, schemas?: S): RouteBinding<InferRouteInput<S>>;
95
+ export {};
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `@nwire/wires/http` — HTTP binding factories.
3
+ *
4
+ * import { post, get, del, put, patch } from "@nwire/wires/http";
5
+ *
6
+ * createInterface()
7
+ * .wire(post("/orders", { body: CreateOrder }), placeOrder)
8
+ * .wire(get("/orders/:id", { params: OrderParams }), getOrder);
9
+ *
10
+ * Bindings carry `$adapter: "http"` so an HTTP adopter filters them via
11
+ * `interface.forAdapter("http")`. Schemas are zod-shaped; the adopter
12
+ * parses request data at dispatch time.
13
+ *
14
+ * Foreign-import factories (`fromExpress` / `fromKoa` / `fromFastify`)
15
+ * live in the adopter packages, not here.
16
+ */
17
+ function buildBinding(verb, path, schemas, source) {
18
+ const binding = {
19
+ $kind: "binding",
20
+ $adapter: "http",
21
+ kind: "route",
22
+ verb,
23
+ path,
24
+ ...(schemas ?? {}),
25
+ ...(source !== undefined ? { source } : {}),
26
+ };
27
+ Object.defineProperty(binding, "from", {
28
+ value: (next) => buildBinding(verb, path, schemas, next),
29
+ enumerable: false,
30
+ writable: false,
31
+ configurable: true,
32
+ });
33
+ return binding;
34
+ }
35
+ export function get(path, schemas) {
36
+ return buildBinding("get", path, schemas, undefined);
37
+ }
38
+ export function post(path, schemas) {
39
+ return buildBinding("post", path, schemas, undefined);
40
+ }
41
+ export function put(path, schemas) {
42
+ return buildBinding("put", path, schemas, undefined);
43
+ }
44
+ export function patch(path, schemas) {
45
+ return buildBinding("patch", path, schemas, undefined);
46
+ }
47
+ export function del(path, schemas) {
48
+ return buildBinding("delete", path, schemas, undefined);
49
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `@nwire/wires` — standalone wire collection + binding factories.
3
+ *
4
+ * The Interface owns a list of `{ binding, handler }` pairs (wires) plus
5
+ * any `provide()`'d ctx builders. Adopters read this list, filter by
6
+ * `binding.$adapter`, and build their transport runtimes from the matched
7
+ * wires. The Interface itself has no transport awareness, no lifecycle,
8
+ * no DI — pure data + context.
9
+ *
10
+ * import { createInterface } from "@nwire/wires";
11
+ * import { post, get } from "@nwire/wires/http";
12
+ * import { queue } from "@nwire/wires/queue";
13
+ *
14
+ * const api = createInterface()
15
+ * .wire(post("/orders"), placeOrder)
16
+ * .wire(get("/orders/:id"), getOrder)
17
+ * .wire(queue("orders.process"), processOrder);
18
+ *
19
+ * api.forAdapter("http").length; // 2
20
+ * api.forAdapter("queue").length; // 1
21
+ *
22
+ * Multiple interfaces compose via `.merge()`; adopters never see the
23
+ * boundary.
24
+ */
25
+ export type { HandlerLike, HandlerDefinition } from "@nwire/handler";
26
+ /**
27
+ * `Binding` — the descriptor every wire pairs with a handler. Adopters
28
+ * filter wires by `binding.$adapter` to find what's theirs. Per-adopter
29
+ * binding shapes (RouteBinding, QueueBinding, etc.) extend this.
30
+ */
31
+ export interface Binding {
32
+ /** Discriminant — `"binding"`. */
33
+ readonly $kind: "binding";
34
+ /** Adopter tag — every adopter consumes wires whose tag matches. */
35
+ readonly $adapter: string;
36
+ /** Free-form subtype within the adopter (`"route"`, `"job"`, `"tool"`, …). */
37
+ readonly kind?: string;
38
+ }
39
+ /**
40
+ * `HandlerDef` — the structural shape the wire-collection holds. Kept
41
+ * deliberately loose at this layer (any function or `HandlerLike` from
42
+ * `@nwire/handler`); concrete validation happens at the adopter when
43
+ * dispatch fires.
44
+ */
45
+ export type HandlerDef = ((input: any, ctx: any) => unknown | Promise<unknown>) | {
46
+ run: (input: any, ctx: any) => unknown | Promise<unknown>;
47
+ } | {
48
+ $kind: string;
49
+ };
50
+ /**
51
+ * `Wire` — `{ binding, handler }`. Optional `app` field tags which App
52
+ * the wire came from (set by `app.interface.wire()` / `appCompose`),
53
+ * which adopters use via `containerOf(wire)` to route to the right
54
+ * container.
55
+ */
56
+ export interface Wire {
57
+ readonly binding: Binding;
58
+ readonly handler: HandlerDef;
59
+ /** Source app reference (opaque at this layer — endpoint resolves it). */
60
+ readonly app?: any;
61
+ }
62
+ /**
63
+ * Per-request ctx builder. Same shape as the legacy `CtxBuilder` from
64
+ * interface-builder.ts — re-declared here under the new surface for
65
+ * standalone use without pulling the legacy types.
66
+ */
67
+ export type WireCtxBuilder<TExtras extends object = object, TRequest = any> = TExtras | ((request: TRequest) => TExtras | Promise<TExtras>);
68
+ /**
69
+ * `Interface` — the wire collection contract.
70
+ *
71
+ * wires — every registered wire (read-only snapshot)
72
+ * wire(b,h) — register one wire; returns this for chaining
73
+ * merge(other)— append another Interface's wires
74
+ * forAdapter — slice by binding.$adapter; adopters consume their slice
75
+ * provide — accumulate ctx builders adopters apply per request
76
+ */
77
+ export interface Interface {
78
+ readonly $kind: "interface";
79
+ readonly wires: readonly Wire[];
80
+ wire(binding: Binding, handler: HandlerDef): this;
81
+ merge(other: Interface): this;
82
+ forAdapter(kind: string): readonly Wire[];
83
+ provide<TExtras extends object>(builder: WireCtxBuilder<TExtras>): this;
84
+ /**
85
+ * Compose accumulated ctx extras for an incoming request. Adopters
86
+ * call this per request; the result is merged onto the handler ctx.
87
+ * Last-write wins on key collision.
88
+ */
89
+ composeCtxExtras(request: unknown): Promise<Record<string, unknown>>;
90
+ }
91
+ /**
92
+ * Construct a new, empty `Interface`. Apps construct one internally and
93
+ * expose it as `app.interface`; consumers can also build standalone wire
94
+ * collections that get merged into apps or attached to endpoints
95
+ * directly.
96
+ */
97
+ export declare function createInterface(): Interface;
98
+ /** Type narrow — does this value look like a new-shape Interface? */
99
+ export declare function isInterface(x: unknown): x is Interface;
package/dist/index.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `@nwire/wires` — standalone wire collection + binding factories.
3
+ *
4
+ * The Interface owns a list of `{ binding, handler }` pairs (wires) plus
5
+ * any `provide()`'d ctx builders. Adopters read this list, filter by
6
+ * `binding.$adapter`, and build their transport runtimes from the matched
7
+ * wires. The Interface itself has no transport awareness, no lifecycle,
8
+ * no DI — pure data + context.
9
+ *
10
+ * import { createInterface } from "@nwire/wires";
11
+ * import { post, get } from "@nwire/wires/http";
12
+ * import { queue } from "@nwire/wires/queue";
13
+ *
14
+ * const api = createInterface()
15
+ * .wire(post("/orders"), placeOrder)
16
+ * .wire(get("/orders/:id"), getOrder)
17
+ * .wire(queue("orders.process"), processOrder);
18
+ *
19
+ * api.forAdapter("http").length; // 2
20
+ * api.forAdapter("queue").length; // 1
21
+ *
22
+ * Multiple interfaces compose via `.merge()`; adopters never see the
23
+ * boundary.
24
+ */
25
+ /**
26
+ * Construct a new, empty `Interface`. Apps construct one internally and
27
+ * expose it as `app.interface`; consumers can also build standalone wire
28
+ * collections that get merged into apps or attached to endpoints
29
+ * directly.
30
+ */
31
+ export function createInterface() {
32
+ const wires = [];
33
+ const ctxBuilders = [];
34
+ const iface = {
35
+ $kind: "interface",
36
+ get wires() {
37
+ return wires;
38
+ },
39
+ wire(binding, handler) {
40
+ wires.push({ binding, handler });
41
+ return iface;
42
+ },
43
+ merge(other) {
44
+ for (const w of other.wires)
45
+ wires.push(w);
46
+ return iface;
47
+ },
48
+ forAdapter(kind) {
49
+ return wires.filter((w) => w.binding.$adapter === kind);
50
+ },
51
+ provide(builder) {
52
+ ctxBuilders.push(builder);
53
+ return iface;
54
+ },
55
+ async composeCtxExtras(request) {
56
+ const out = {};
57
+ for (const b of ctxBuilders) {
58
+ const extras = typeof b === "function" ? await b(request) : b;
59
+ Object.assign(out, extras);
60
+ }
61
+ return out;
62
+ },
63
+ };
64
+ return iface;
65
+ }
66
+ /** Type narrow — does this value look like a new-shape Interface? */
67
+ export function isInterface(x) {
68
+ return (typeof x === "object" &&
69
+ x !== null &&
70
+ x.$kind === "interface" &&
71
+ Array.isArray(x.wires));
72
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * `InterfaceBuilder` — the abstract base every Nwire transport extends.
3
+ *
4
+ * Six verbs are universal and form the public surface every transport
5
+ * supports (HTTP, queue, MCP, GraphQL, WebSocket, CLI, …):
6
+ *
7
+ * .use(...plugins) augment the chain (middleware/plugins)
8
+ * .wire(binding, handler?, o?) bind an outbound surface to a handler
9
+ * .from(source) declare an inbound stream the iface consumes
10
+ * .mount(target) attach this iface to a host (express/koa/…)
11
+ * .run(opts?) serve standalone — returns a Lifecycle
12
+ * .boot(opts?) build internals without listening
13
+ *
14
+ * Two internal seams that aren't verbs:
15
+ *
16
+ * .manifest() pure data describing the wired surface
17
+ * (scanner / Studio / cache consume this)
18
+ * .attach(host) lifecycle hook called by the host endpoint
19
+ * when this interface is mounted on another
20
+ *
21
+ * And one DI shortcut kept for ergonomics:
22
+ *
23
+ * .provide(container) sugar that wires a Container into the chain
24
+ * via the same channel `.use()` uses.
25
+ *
26
+ * Each transport refines the generics:
27
+ * - `TBinding` — outbound binding shape (RouteBinding / JobBinding / …)
28
+ * - `TPlugin` — what `.use()` accepts (Koa middleware / plain fn / …)
29
+ * - `TFromSource` — what `.from()` accepts (eventDef / remote ref / …)
30
+ * - `TMountTarget` — what `.mount()` accepts (Koa app / express / iface)
31
+ * - `TArtifact` — what `.boot()` produces (an opaque handle the host owns)
32
+ *
33
+ * The base owns the type signatures + manifest plumbing. It implements one
34
+ * verb concretely — `.provide()` — as a default that delegates to `.use()`.
35
+ * Transports may override if they need a different DI dispatch shape.
36
+ */
37
+ import type { HandlerLike } from "@nwire/handler";
38
+ /**
39
+ * What `.provide()` accepts — either a static extras object, or a builder
40
+ * function the transport calls per request/job/invocation. The interface
41
+ * stores it opaquely and applies it during dispatch; how it composes is
42
+ * the wire's concern, not the transport's.
43
+ *
44
+ * api.provide({ logger: console });
45
+ * api.provide((req) => ({ user: req.user, requestId: crypto.randomUUID() }));
46
+ * api.provide((req) => ({ ...root.cradle, user: req.user })); // wire reads container
47
+ *
48
+ * The transport never imports `@nwire/container` — what the wire passes
49
+ * is opaque.
50
+ */
51
+ export type CtxBuilder<TExtras extends object, TRequest = unknown> = TExtras | ((request: TRequest) => TExtras | Promise<TExtras>);
52
+ /**
53
+ * Health probe registration — aligned with `@nwire/endpoint`'s `HealthCheck`
54
+ * so every transport's `attach()` accepts the same shape the endpoint passes.
55
+ */
56
+ export interface InterfaceHealthCheck {
57
+ readonly name: string;
58
+ readonly check: () => Promise<void> | void;
59
+ readonly timeout?: number;
60
+ readonly kind?: "readiness" | "liveness";
61
+ }
62
+ /**
63
+ * Passed by the host (typically `@nwire/endpoint`) to `attach()` when this
64
+ * interface is being mounted into a larger composition. The host gives
65
+ * the interface a place to register transport-specific health checks.
66
+ *
67
+ * The host does NOT pass a container — DI composition is a wire-layer
68
+ * concern, threaded through `.provide(builder)` before `.serve()` runs.
69
+ * Interface contract stays container-agnostic.
70
+ */
71
+ export interface AttachBindings {
72
+ /** Register a health check the host reports on `/live` / `/ready`. */
73
+ addCheck(check: InterfaceHealthCheck): void;
74
+ /**
75
+ * The host's primary DI container, when one is available (i.e. the
76
+ * endpoint was given `.serve(app)` before this interface). Interfaces
77
+ * that need a container — e.g. for `ctx.resolve(...)` inside handlers
78
+ * — should fall back to this when no `.provide(container)` was set
79
+ * on the interface itself. Typed as `unknown` so the interface contract
80
+ * stays free of a `@nwire/container` dependency.
81
+ */
82
+ readonly container?: unknown;
83
+ }
84
+ /**
85
+ * `NwireInterface` — the structural shape `@nwire/endpoint` looks for when
86
+ * `.serve(interfaceValue)` is called. Any object exposing `$nwireServable`,
87
+ * `transport`, and `attach()` satisfies it. `InterfaceBuilder` implements
88
+ * the shape; foreign hosts can implement it directly to plug in.
89
+ */
90
+ export interface NwireInterface {
91
+ readonly $nwireServable: true;
92
+ readonly transport: string;
93
+ attach(host: AttachBindings): void;
94
+ }
95
+ /**
96
+ * `InterfaceManifest` — the data shape `.manifest()` returns. Scanner,
97
+ * Studio, and the cache all read this; they never reach into the live
98
+ * builder. Each transport extends with its own concrete shape (RouteEntry
99
+ * for HTTP, JobEntry for queue, etc.); the base only nails down the
100
+ * uniform fields.
101
+ */
102
+ export interface InterfaceManifest {
103
+ /** Transport id — matches the builder's `transport` field. */
104
+ readonly transport: string;
105
+ /** Optional prefix or namespace applied to every wired binding. */
106
+ readonly prefix?: string;
107
+ /** Outbound bindings — opaque at this level; transports type their own. */
108
+ readonly wired: readonly unknown[];
109
+ /** Inbound sources declared via `.from()` — same opacity. */
110
+ readonly from?: readonly unknown[];
111
+ /** Plugins applied via `.use()`. Stored by name when possible. */
112
+ readonly plugins?: readonly string[];
113
+ }
114
+ /**
115
+ * Options accepted by `.run()`. Transports refine via their own union.
116
+ * The base only knows about the universal lifecycle knobs the endpoint
117
+ * also exposes; transports use the same names so `.run({port: 3000})`
118
+ * means the same thing on an http interface as on an endpoint.
119
+ */
120
+ export interface RunOptions {
121
+ /** Bind port (HTTP/WS/queue admin). Transports may ignore. */
122
+ readonly port?: number;
123
+ /** Bind host. Default `"0.0.0.0"` where applicable. */
124
+ readonly host?: string;
125
+ /** Drain timeout in ms before the lifecycle force-exits. */
126
+ readonly shutdownTimeoutMs?: number;
127
+ /** Override the default signal handlers. Default `true` (install handlers). */
128
+ readonly installSignalHandlers?: boolean;
129
+ }
130
+ /**
131
+ * The handle `.run()` returns. Owners call `close()` to drain gracefully.
132
+ * Mirrors `@nwire/endpoint`'s `Lifecycle` so swapping the runner is
133
+ * transparent for callers that already use the endpoint pattern.
134
+ */
135
+ export interface Lifecycle {
136
+ /** Drain in-flight work + release resources. Idempotent. */
137
+ close(): Promise<void>;
138
+ /** Resolved when the lifecycle has finished closing (manual or signal). */
139
+ readonly closed: Promise<void>;
140
+ }
141
+ /**
142
+ * Options accepted by `.boot()`. Boot builds internals (validates the
143
+ * routing table, freezes the middleware chain, opens DI containers) but
144
+ * does NOT bind a port or start serving traffic. The endpoint uses this
145
+ * to take ownership of the artifact before deciding when to listen.
146
+ */
147
+ export interface BootOptions {
148
+ }
149
+ /**
150
+ * The opaque handle `.boot()` returns. Transports type `value` to their
151
+ * concrete artifact (a Koa app, a queue worker, an MCP server, …). The
152
+ * host calls `.serve()` on the result when it's ready to bind.
153
+ */
154
+ export interface Booted<TArtifact = unknown> {
155
+ /** The concrete artifact — opaque at the base level. */
156
+ readonly value: TArtifact;
157
+ /** Manifest snapshot taken at boot. */
158
+ readonly manifest: InterfaceManifest;
159
+ /** Shutdown handle for the booted artifact. */
160
+ shutdown(): Promise<void>;
161
+ }
162
+ /**
163
+ * Base class every transport extends.
164
+ *
165
+ * Implementing the contract:
166
+ *
167
+ * class HttpInterfaceImpl extends InterfaceBuilder<
168
+ * RouteBinding,
169
+ * Koa.Middleware,
170
+ * RouteBinding, // `from` source shape (forwarded route)
171
+ * Koa | NwireInterface, // `mount` target shape
172
+ * RawHttpHandler // boot artifact
173
+ * > {
174
+ * readonly transport = "http" as const;
175
+ * use(...mw) { … return this; }
176
+ * wire(binding, handler) { … return this; }
177
+ * from(src) { … return this; }
178
+ * mount(target) { … return this; }
179
+ * async run(opts) { … return lifecycle; }
180
+ * async boot(opts) { … return booted; }
181
+ * manifest() { … return data; }
182
+ * attach(host) { … }
183
+ * }
184
+ */
185
+ export declare abstract class InterfaceBuilder<TBinding = unknown, TPlugin = unknown, TFromSource = unknown, TMountTarget = unknown, TArtifact = unknown> implements NwireInterface {
186
+ readonly $nwireServable: true;
187
+ /** Transport id — `"http"` / `"queue"` / `"mcp"` / `"graphql"` / … */
188
+ abstract readonly transport: string;
189
+ /** Augment the chain with one or more plugins/middleware values. */
190
+ abstract use(...plugins: TPlugin[]): this;
191
+ /**
192
+ * Bind an outbound surface (route / topic / tool / channel) to a handler.
193
+ * `handler` may be a `HandlerDefinition` (typed + validated) or a bare
194
+ * async function. Transports may overload to accept per-binding options.
195
+ */
196
+ abstract wire(binding: TBinding, handler?: HandlerLike, options?: unknown): this;
197
+ /**
198
+ * Declare an inbound stream the interface consumes. Semantically the
199
+ * opposite of `.wire()` — `.wire()` exposes; `.from()` ingests. Each
200
+ * transport defines what counts as a source:
201
+ *
202
+ * - HTTP: a remote route or upstream API to proxy/forward
203
+ * - queue: an event definition or another queue to consume from
204
+ * - MCP: a remote tool list to re-export
205
+ * - GraphQL: a remote schema to stitch
206
+ *
207
+ * Transports that have no meaningful inbound concept omit `.from()` from
208
+ * their concrete class signature (the abstract declaration here exists
209
+ * so the universal verb table is uniform on the type level).
210
+ */
211
+ abstract from(source: TFromSource): this;
212
+ /**
213
+ * Mount this interface onto an external host. Returns the host (so the
214
+ * caller can use it), not `this`. Transports type the target shape:
215
+ *
216
+ * - HTTP: Koa | express.Application | NwireInterface
217
+ * - queue: QueueWorker | NwireInterface
218
+ * - MCP: existing MCP server | NwireInterface
219
+ *
220
+ * Use this for interop (mounting on Express / Fastify) or for composing
221
+ * one interface inside another (HTTP gateway → queue producer).
222
+ */
223
+ abstract mount(target: TMountTarget, options?: unknown): unknown;
224
+ /**
225
+ * Boot the interface (validate, freeze, build internals) and serve it
226
+ * standalone. Installs default signal handlers unless opts say
227
+ * otherwise. Returns a `Lifecycle` the caller can close to drain.
228
+ *
229
+ * Use this when the interface is the entire app. For multi-interface
230
+ * apps, prefer `@nwire/endpoint` which orchestrates several `.boot()`d
231
+ * interfaces together with full lightship + http-terminator semantics.
232
+ */
233
+ abstract run(options?: RunOptions): Promise<Lifecycle>;
234
+ /**
235
+ * Build the interface's runnable artifact WITHOUT binding a port. The
236
+ * host (typically `@nwire/endpoint`) calls this, takes ownership of the
237
+ * returned `Booted` handle, and decides when to start serving.
238
+ *
239
+ * Boot is the "compile + freeze" moment — after `.boot()` returns,
240
+ * additional `.use()` / `.wire()` calls are not honored on the booted
241
+ * artifact (the builder still mutates, but a re-boot is required to
242
+ * pick up changes).
243
+ */
244
+ abstract boot(options?: BootOptions): Promise<Booted<TArtifact>>;
245
+ /**
246
+ * Pure-data snapshot of the wired surface. Scanner, Studio, and the
247
+ * cache read this. Calling `.manifest()` does NOT freeze the builder —
248
+ * the manifest is a copy taken at the call site.
249
+ */
250
+ abstract manifest(): InterfaceManifest;
251
+ /**
252
+ * Lifecycle hook called by a host that's composing this interface into
253
+ * a larger app. The host passes its container + a way to register
254
+ * health checks. Default no-op subclasses override as needed.
255
+ */
256
+ attach(_host: AttachBindings): void;
257
+ /**
258
+ * Builders accumulated via `.provide()`. Transports apply each per
259
+ * request/job/invocation, spreading the merged result onto handler ctx.
260
+ * Last-write wins on key collision.
261
+ */
262
+ protected readonly ctxBuilders: CtxBuilder<object>[];
263
+ /**
264
+ * Compose ctx extras for an incoming request. Transports call this in
265
+ * their dispatch loop to build the per-invocation extras object that
266
+ * gets spread onto handler ctx.
267
+ *
268
+ * The interface is agnostic to where the values come from. A wire
269
+ * builder may close over a container, read req headers, mint a request
270
+ * id, fetch a tenant — all opaque to the transport.
271
+ */
272
+ protected composeCtxExtras(request: unknown): Promise<Record<string, unknown>>;
273
+ /**
274
+ * Register a ctx builder applied per request/job/invocation.
275
+ *
276
+ * - Static extras: every request gets the same object merged in.
277
+ * - Builder fn: receives the transport's native request value (`req`,
278
+ * `job`, `evt`), returns extras to merge onto ctx.
279
+ *
280
+ * Multiple `.provide()` calls compose — each builder runs in
281
+ * registration order; later writes win on key collision.
282
+ *
283
+ * api.provide({ logger: console }); // static
284
+ * api.provide((req) => ({ user: req.user })); // dynamic
285
+ * api.provide((req) => ({ ...root.cradle, tenant: req.tenant })); // closes over container
286
+ *
287
+ * The interface NEVER imports `@nwire/container`. Whatever the wire
288
+ * passes is opaque here.
289
+ */
290
+ provide<TExtras extends object>(builder: CtxBuilder<TExtras>): this;
291
+ }
292
+ /** Type narrow — does this value look like a Nwire interface? */
293
+ export declare function isNwireInterface(x: unknown): x is NwireInterface;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * `InterfaceBuilder` — the abstract base every Nwire transport extends.
3
+ *
4
+ * Six verbs are universal and form the public surface every transport
5
+ * supports (HTTP, queue, MCP, GraphQL, WebSocket, CLI, …):
6
+ *
7
+ * .use(...plugins) augment the chain (middleware/plugins)
8
+ * .wire(binding, handler?, o?) bind an outbound surface to a handler
9
+ * .from(source) declare an inbound stream the iface consumes
10
+ * .mount(target) attach this iface to a host (express/koa/…)
11
+ * .run(opts?) serve standalone — returns a Lifecycle
12
+ * .boot(opts?) build internals without listening
13
+ *
14
+ * Two internal seams that aren't verbs:
15
+ *
16
+ * .manifest() pure data describing the wired surface
17
+ * (scanner / Studio / cache consume this)
18
+ * .attach(host) lifecycle hook called by the host endpoint
19
+ * when this interface is mounted on another
20
+ *
21
+ * And one DI shortcut kept for ergonomics:
22
+ *
23
+ * .provide(container) sugar that wires a Container into the chain
24
+ * via the same channel `.use()` uses.
25
+ *
26
+ * Each transport refines the generics:
27
+ * - `TBinding` — outbound binding shape (RouteBinding / JobBinding / …)
28
+ * - `TPlugin` — what `.use()` accepts (Koa middleware / plain fn / …)
29
+ * - `TFromSource` — what `.from()` accepts (eventDef / remote ref / …)
30
+ * - `TMountTarget` — what `.mount()` accepts (Koa app / express / iface)
31
+ * - `TArtifact` — what `.boot()` produces (an opaque handle the host owns)
32
+ *
33
+ * The base owns the type signatures + manifest plumbing. It implements one
34
+ * verb concretely — `.provide()` — as a default that delegates to `.use()`.
35
+ * Transports may override if they need a different DI dispatch shape.
36
+ */
37
+ /* ─── The abstract base ─────────────────────────────────────────────────── */
38
+ /**
39
+ * Base class every transport extends.
40
+ *
41
+ * Implementing the contract:
42
+ *
43
+ * class HttpInterfaceImpl extends InterfaceBuilder<
44
+ * RouteBinding,
45
+ * Koa.Middleware,
46
+ * RouteBinding, // `from` source shape (forwarded route)
47
+ * Koa | NwireInterface, // `mount` target shape
48
+ * RawHttpHandler // boot artifact
49
+ * > {
50
+ * readonly transport = "http" as const;
51
+ * use(...mw) { … return this; }
52
+ * wire(binding, handler) { … return this; }
53
+ * from(src) { … return this; }
54
+ * mount(target) { … return this; }
55
+ * async run(opts) { … return lifecycle; }
56
+ * async boot(opts) { … return booted; }
57
+ * manifest() { … return data; }
58
+ * attach(host) { … }
59
+ * }
60
+ */
61
+ export class InterfaceBuilder {
62
+ $nwireServable = true;
63
+ /**
64
+ * Lifecycle hook called by a host that's composing this interface into
65
+ * a larger app. The host passes its container + a way to register
66
+ * health checks. Default no-op subclasses override as needed.
67
+ */
68
+ attach(_host) {
69
+ // No-op by default. Transports override when they need to adopt the
70
+ // host's container or register transport-specific health checks.
71
+ }
72
+ /* ─── Per-request ctx provider ─────────────────────────────────────── */
73
+ /**
74
+ * Builders accumulated via `.provide()`. Transports apply each per
75
+ * request/job/invocation, spreading the merged result onto handler ctx.
76
+ * Last-write wins on key collision.
77
+ */
78
+ ctxBuilders = [];
79
+ /**
80
+ * Compose ctx extras for an incoming request. Transports call this in
81
+ * their dispatch loop to build the per-invocation extras object that
82
+ * gets spread onto handler ctx.
83
+ *
84
+ * The interface is agnostic to where the values come from. A wire
85
+ * builder may close over a container, read req headers, mint a request
86
+ * id, fetch a tenant — all opaque to the transport.
87
+ */
88
+ async composeCtxExtras(request) {
89
+ const result = {};
90
+ for (const builder of this.ctxBuilders) {
91
+ const extras = typeof builder === "function" ? await builder(request) : builder;
92
+ Object.assign(result, extras);
93
+ }
94
+ return result;
95
+ }
96
+ /**
97
+ * Register a ctx builder applied per request/job/invocation.
98
+ *
99
+ * - Static extras: every request gets the same object merged in.
100
+ * - Builder fn: receives the transport's native request value (`req`,
101
+ * `job`, `evt`), returns extras to merge onto ctx.
102
+ *
103
+ * Multiple `.provide()` calls compose — each builder runs in
104
+ * registration order; later writes win on key collision.
105
+ *
106
+ * api.provide({ logger: console }); // static
107
+ * api.provide((req) => ({ user: req.user })); // dynamic
108
+ * api.provide((req) => ({ ...root.cradle, tenant: req.tenant })); // closes over container
109
+ *
110
+ * The interface NEVER imports `@nwire/container`. Whatever the wire
111
+ * passes is opaque here.
112
+ */
113
+ provide(builder) {
114
+ this.ctxBuilders.push(builder);
115
+ return this;
116
+ }
117
+ }
118
+ /* ─── Structural type narrows ────────────────────────────────────────── */
119
+ /** Type narrow — does this value look like a Nwire interface? */
120
+ export function isNwireInterface(x) {
121
+ return (typeof x === "object" &&
122
+ x !== null &&
123
+ x.$nwireServable === true &&
124
+ typeof x.transport === "string");
125
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `@nwire/wires/mcp` — MCP binding factories.
3
+ *
4
+ * import { tool, resource } from "@nwire/wires/mcp";
5
+ *
6
+ * createInterface()
7
+ * .wire(tool("create-order"), createOrder)
8
+ * .wire(resource("orders://{id}"), getOrderResource);
9
+ *
10
+ * The `@nwire/mcp` adopter consumes wires by `$adapter === "mcp"` and
11
+ * registers each binding with the MCP server (`tool` becomes an MCP tool,
12
+ * `resource` becomes a resource template).
13
+ */
14
+ import type { ZodTypeAny, z } from "zod";
15
+ import type { Binding } from "../index.js";
16
+ type ZodOutput<T> = T extends ZodTypeAny ? z.output<T> : unknown;
17
+ export interface ToolBindingOptions {
18
+ /** Schema for the tool's input arguments. */
19
+ readonly input?: ZodTypeAny;
20
+ /** Human-readable summary surfaced on tool discovery. */
21
+ readonly description?: string;
22
+ readonly source?: string;
23
+ }
24
+ export interface ToolBinding<TInput = unknown> extends Binding {
25
+ readonly $kind: "binding";
26
+ readonly $adapter: "mcp";
27
+ readonly kind: "tool";
28
+ readonly tool: string;
29
+ readonly input?: ZodTypeAny;
30
+ readonly description?: string;
31
+ readonly source?: string;
32
+ from(source: string): ToolBinding<TInput>;
33
+ readonly __inputType?: TInput;
34
+ }
35
+ export declare function tool<O extends ToolBindingOptions = ToolBindingOptions>(toolName: string, options?: O): ToolBinding<O extends {
36
+ input: infer I;
37
+ } ? (I extends ZodTypeAny ? ZodOutput<I> : unknown) : unknown>;
38
+ export interface ResourceBindingOptions {
39
+ readonly description?: string;
40
+ readonly mimeType?: string;
41
+ readonly source?: string;
42
+ }
43
+ export interface ResourceBinding extends Binding {
44
+ readonly $kind: "binding";
45
+ readonly $adapter: "mcp";
46
+ readonly kind: "resource";
47
+ readonly uriTemplate: string;
48
+ readonly description?: string;
49
+ readonly mimeType?: string;
50
+ readonly source?: string;
51
+ from(source: string): ResourceBinding;
52
+ }
53
+ export declare function resource(uriTemplate: string, options?: ResourceBindingOptions): ResourceBinding;
54
+ export {};
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `@nwire/wires/mcp` — MCP binding factories.
3
+ *
4
+ * import { tool, resource } from "@nwire/wires/mcp";
5
+ *
6
+ * createInterface()
7
+ * .wire(tool("create-order"), createOrder)
8
+ * .wire(resource("orders://{id}"), getOrderResource);
9
+ *
10
+ * The `@nwire/mcp` adopter consumes wires by `$adapter === "mcp"` and
11
+ * registers each binding with the MCP server (`tool` becomes an MCP tool,
12
+ * `resource` becomes a resource template).
13
+ */
14
+ function buildToolBinding(toolName, options, source) {
15
+ const binding = {
16
+ $kind: "binding",
17
+ $adapter: "mcp",
18
+ kind: "tool",
19
+ tool: toolName,
20
+ ...(options ?? {}),
21
+ ...(source !== undefined ? { source } : {}),
22
+ };
23
+ Object.defineProperty(binding, "from", {
24
+ value: (next) => buildToolBinding(toolName, options, next),
25
+ enumerable: false,
26
+ writable: false,
27
+ configurable: true,
28
+ });
29
+ return binding;
30
+ }
31
+ export function tool(toolName, options) {
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ return buildToolBinding(toolName, options, undefined);
34
+ }
35
+ function buildResourceBinding(uriTemplate, options, source) {
36
+ const binding = {
37
+ $kind: "binding",
38
+ $adapter: "mcp",
39
+ kind: "resource",
40
+ uriTemplate,
41
+ ...(options ?? {}),
42
+ ...(source !== undefined ? { source } : {}),
43
+ };
44
+ Object.defineProperty(binding, "from", {
45
+ value: (next) => buildResourceBinding(uriTemplate, options, next),
46
+ enumerable: false,
47
+ writable: false,
48
+ configurable: true,
49
+ });
50
+ return binding;
51
+ }
52
+ export function resource(uriTemplate, options) {
53
+ return buildResourceBinding(uriTemplate, options, undefined);
54
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `@nwire/wires/queue` — queue binding factory.
3
+ *
4
+ * import { queue } from "@nwire/wires/queue";
5
+ *
6
+ * createInterface().wire(queue("orders.process"), processOrder);
7
+ *
8
+ * The binding carries `$adapter: "queue"` so a queue adopter
9
+ * (`@nwire/bullmq`, `@nwire/queue-redis`, …) filters wires via
10
+ * `interface.forAdapter("queue")`. Adopters interpret the queue-name
11
+ * field as a topic / channel / queue identifier per their impl.
12
+ */
13
+ import type { ZodTypeAny, z } from "zod";
14
+ import type { Binding } from "../index.js";
15
+ /** Options accepted by `queue()`. */
16
+ export interface QueueBindingOptions {
17
+ /** Schema for the message body. The adopter validates at dispatch. */
18
+ readonly input?: ZodTypeAny;
19
+ /** Free-form trigger source tag — Studio and observability group by it. */
20
+ readonly source?: string;
21
+ /**
22
+ * Per-binding concurrency hint — the adopter may honor or ignore.
23
+ * Default left to the adopter.
24
+ */
25
+ readonly concurrency?: number;
26
+ }
27
+ export interface QueueBinding<TInput = unknown> extends Binding {
28
+ readonly $kind: "binding";
29
+ readonly $adapter: "queue";
30
+ readonly kind: "queue";
31
+ readonly queue: string;
32
+ readonly input?: ZodTypeAny;
33
+ readonly source?: string;
34
+ readonly concurrency?: number;
35
+ /** Chainable source tag — returns a fresh binding. */
36
+ from(source: string): QueueBinding<TInput>;
37
+ readonly __inputType?: TInput;
38
+ }
39
+ type ZodOutput<T> = T extends ZodTypeAny ? z.output<T> : unknown;
40
+ export declare function queue<O extends QueueBindingOptions = QueueBindingOptions>(queueName: string, options?: O): QueueBinding<O extends {
41
+ input: infer I;
42
+ } ? (I extends ZodTypeAny ? ZodOutput<I> : unknown) : unknown>;
43
+ export {};
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `@nwire/wires/queue` — queue binding factory.
3
+ *
4
+ * import { queue } from "@nwire/wires/queue";
5
+ *
6
+ * createInterface().wire(queue("orders.process"), processOrder);
7
+ *
8
+ * The binding carries `$adapter: "queue"` so a queue adopter
9
+ * (`@nwire/bullmq`, `@nwire/queue-redis`, …) filters wires via
10
+ * `interface.forAdapter("queue")`. Adopters interpret the queue-name
11
+ * field as a topic / channel / queue identifier per their impl.
12
+ */
13
+ function buildBinding(queueName, options, source) {
14
+ const binding = {
15
+ $kind: "binding",
16
+ $adapter: "queue",
17
+ kind: "queue",
18
+ queue: queueName,
19
+ ...(options ?? {}),
20
+ ...(source !== undefined ? { source } : {}),
21
+ };
22
+ Object.defineProperty(binding, "from", {
23
+ value: (next) => buildBinding(queueName, options, next),
24
+ enumerable: false,
25
+ writable: false,
26
+ configurable: true,
27
+ });
28
+ return binding;
29
+ }
30
+ export function queue(queueName, options) {
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ return buildBinding(queueName, options, undefined);
33
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@nwire/wires",
3
+ "version": "0.10.0",
4
+ "description": "Nwire — transport-interface contract. `InterfaceBuilder` is the abstract base every transport (HTTP, queue, MCP, GraphQL, …) extends. Foreign hosts implement it. Six universal verbs (.use / .wire / .from / .mount / .run / .boot) plus manifest + attach seams are typed at this layer so transports stay consistent.",
5
+ "keywords": [
6
+ "interface",
7
+ "nwire",
8
+ "transport"
9
+ ],
10
+ "license": "MIT",
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "type": "module",
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "import": "./dist/index.js",
22
+ "types": "./dist/index.d.ts"
23
+ },
24
+ "./http": {
25
+ "import": "./dist/http/index.js",
26
+ "types": "./dist/http/index.d.ts"
27
+ },
28
+ "./queue": {
29
+ "import": "./dist/queue/index.js",
30
+ "types": "./dist/queue/index.d.ts"
31
+ },
32
+ "./cron": {
33
+ "import": "./dist/cron/index.js",
34
+ "types": "./dist/cron/index.d.ts"
35
+ },
36
+ "./mcp": {
37
+ "import": "./dist/mcp/index.js",
38
+ "types": "./dist/mcp/index.d.ts"
39
+ },
40
+ "./graphql": {
41
+ "import": "./dist/graphql/index.js",
42
+ "types": "./dist/graphql/index.d.ts"
43
+ }
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "zod": "^4.0.0",
50
+ "@nwire/handler": "0.10.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^22.19.9",
54
+ "typescript": "^5.9.3",
55
+ "vitest": "^4.0.18"
56
+ },
57
+ "scripts": {
58
+ "build": "tsc && node ../../scripts/fix-dist-extensions.mjs dist",
59
+ "dev": "tsc --watch",
60
+ "typecheck": "tsc --noEmit"
61
+ }
62
+ }