@zap-proto/web 0.1.1 → 0.2.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/dist/http.d.ts +75 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +216 -0
- package/dist/http.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -0
- package/dist/server.js.map +1 -1
- package/package.json +4 -3
- package/src/http.ts +315 -0
- package/src/server.ts +11 -0
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* http.ts — httpServe(httpServer, opts): mount a JSON-over-HTTP face on the
|
|
3
|
+
* SAME ZAP service that serve() exposes over WebSocket.
|
|
4
|
+
*
|
|
5
|
+
* Motivation: external consumers (and the OpenAPI surface emitted by
|
|
6
|
+
* `zapgen --emit=openapi`) speak POST /<service>/<method> with a JSON body.
|
|
7
|
+
* httpServe terminates that HTTP shape and dispatches each request through the
|
|
8
|
+
* identical {@link CallHandler} the WebSocket transport uses — so there is ONE
|
|
9
|
+
* service implementation, reached two ways.
|
|
10
|
+
*
|
|
11
|
+
* The JSON⇄ZAP codec is schema-specific and therefore supplied by the caller as
|
|
12
|
+
* a {@link HttpRoute} per method (the generated zapgen bindings know each
|
|
13
|
+
* struct's shape; the runtime stays schema-agnostic — orthogonal separation).
|
|
14
|
+
* Each route declares:
|
|
15
|
+
* - path: POST path, matching the OpenAPI doc (`/document-service/...`).
|
|
16
|
+
* - method: the .zap method ordinal (the same ordinal the WS client sends).
|
|
17
|
+
* - decode: JSON request body → ZAP payload bytes.
|
|
18
|
+
* - encode: ZAP response body bytes → JSON value (omit for void methods).
|
|
19
|
+
*
|
|
20
|
+
* Auth runs per-request via the same {@link MintCap} slot serve() uses: a null
|
|
21
|
+
* mint yields HTTP 401; a non-null mint becomes the `ctx` passed to rootCap,
|
|
22
|
+
* whose {@link CallHandler} dispatches the decoded Call.
|
|
23
|
+
*
|
|
24
|
+
* Node `http` only — no framework. The HTTP server is supplied by the caller
|
|
25
|
+
* (the same http.Server that serve() attaches its WebSocket upgrade to), so one
|
|
26
|
+
* port serves WS + HTTP.
|
|
27
|
+
*/
|
|
28
|
+
import type { Server as HttpServer } from "node:http";
|
|
29
|
+
import type { MintCap } from "./auth.js";
|
|
30
|
+
import type { RootCap } from "./server.js";
|
|
31
|
+
/**
|
|
32
|
+
* HttpRoute binds one POST path to one ZAP method, with the JSON⇄ZAP codec for
|
|
33
|
+
* that method. `decode` turns the parsed JSON request body into the method's
|
|
34
|
+
* ZAP payload bytes; `encode` turns the ZAP response body bytes into the JSON
|
|
35
|
+
* value to serialize. A void method omits `encode` (returns HTTP 204).
|
|
36
|
+
*/
|
|
37
|
+
export interface HttpRoute {
|
|
38
|
+
/** POST path, e.g. "/document-service/create-document". */
|
|
39
|
+
path: string;
|
|
40
|
+
/** The .zap method ordinal this path dispatches. */
|
|
41
|
+
method: number;
|
|
42
|
+
/** Parse a JSON request body into ZAP payload bytes. */
|
|
43
|
+
decode: (json: unknown) => Uint8Array;
|
|
44
|
+
/** Encode ZAP response body bytes into a JSON value. Omit for void methods. */
|
|
45
|
+
encode?: (body: Uint8Array) => unknown;
|
|
46
|
+
}
|
|
47
|
+
/** Options for {@link httpServe}. */
|
|
48
|
+
export interface HttpServeOptions<Ctx> {
|
|
49
|
+
/** The method routes (one per OpenAPI operation). */
|
|
50
|
+
routes: HttpRoute[];
|
|
51
|
+
/** Bearer→ctx boundary; return null to reject with HTTP 401. */
|
|
52
|
+
mintCap: MintCap<Ctx>;
|
|
53
|
+
/** Produce the dispatch handler for a minted ctx (same shape as serve()). */
|
|
54
|
+
rootCap: RootCap<Ctx>;
|
|
55
|
+
/**
|
|
56
|
+
* Optional path prefix mounted before each route path, e.g. "/v1" makes the
|
|
57
|
+
* route "/echo/echo" serve at "/v1/echo/echo". Default "" (no prefix).
|
|
58
|
+
*/
|
|
59
|
+
prefix?: string;
|
|
60
|
+
/** Optional sink for handler errors. */
|
|
61
|
+
onError?: (err: unknown) => void;
|
|
62
|
+
}
|
|
63
|
+
/** A live HTTP face attached to an http.Server. */
|
|
64
|
+
export interface HttpServeHandle {
|
|
65
|
+
/** Detach the request listener. */
|
|
66
|
+
close(): void;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Attach a JSON-over-HTTP face to `httpServer`. Returns a handle whose close()
|
|
70
|
+
* detaches the listener. Non-matching requests are passed through untouched so
|
|
71
|
+
* httpServe can coexist with the app's own request handler and with serve()'s
|
|
72
|
+
* WebSocket upgrade on the same server.
|
|
73
|
+
*/
|
|
74
|
+
export declare function httpServe<Ctx>(httpServer: HttpServer, opts: HttpServeOptions<Ctx>): HttpServeHandle;
|
|
75
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAGV,MAAM,IAAI,UAAU,EACrB,MAAM,WAAW,CAAC;AAGnB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAK3C;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,UAAU,CAAC;IACtC,+EAA+E;IAC/E,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC;CACxC;AAED,qCAAqC;AACrC,MAAM,WAAW,gBAAgB,CAAC,GAAG;IACnC,qDAAqD;IACrD,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,gEAAgE;IAChE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IACtB,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IACtB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAClC;AAED,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAC3B,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,gBAAgB,CAAC,GAAG,CAAC,GAC1B,eAAe,CA8DjB"}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
2
|
+
// See the file LICENSE for licensing terms.
|
|
3
|
+
import { NO_TARGET, Status } from "@zap-proto/zap";
|
|
4
|
+
const EMPTY = new Uint8Array(0);
|
|
5
|
+
const MAX_BODY_BYTES = 8 * 1024 * 1024; // 8 MiB request-body ceiling.
|
|
6
|
+
/**
|
|
7
|
+
* Attach a JSON-over-HTTP face to `httpServer`. Returns a handle whose close()
|
|
8
|
+
* detaches the listener. Non-matching requests are passed through untouched so
|
|
9
|
+
* httpServe can coexist with the app's own request handler and with serve()'s
|
|
10
|
+
* WebSocket upgrade on the same server.
|
|
11
|
+
*/
|
|
12
|
+
export function httpServe(httpServer, opts) {
|
|
13
|
+
const onError = opts.onError ?? (() => { });
|
|
14
|
+
const prefix = normalizePrefix(opts.prefix ?? "");
|
|
15
|
+
// path → route, built once. Promise ordinal collisions are the caller's
|
|
16
|
+
// concern (each method has a distinct path by construction).
|
|
17
|
+
const table = new Map();
|
|
18
|
+
for (const r of opts.routes)
|
|
19
|
+
table.set(prefix + r.path, r);
|
|
20
|
+
const appListeners = httpServer.listeners("request");
|
|
21
|
+
httpServer.removeAllListeners("request");
|
|
22
|
+
const delegate = (req, res) => {
|
|
23
|
+
if (appListeners.length === 0) {
|
|
24
|
+
if (!res.writableEnded) {
|
|
25
|
+
writeJson(res, 404, { error: "not found" });
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
for (const l of appListeners)
|
|
30
|
+
l(req, res);
|
|
31
|
+
};
|
|
32
|
+
const onRequest = (req, res) => {
|
|
33
|
+
let url;
|
|
34
|
+
try {
|
|
35
|
+
url = new URL(req.url ?? "/", "http://localhost");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
delegate(req, res);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const route = table.get(url.pathname);
|
|
42
|
+
if (!route) {
|
|
43
|
+
delegate(req, res); // not ours — hand to the app's own handler.
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (req.method !== "POST") {
|
|
47
|
+
writeJson(res, 405, { error: "method not allowed; use POST" });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
void handle(req, res, route, opts, onError).catch((err) => {
|
|
51
|
+
onError(err);
|
|
52
|
+
if (!res.headersSent)
|
|
53
|
+
writeJson(res, 500, { error: "internal error" });
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
httpServer.on("request", onRequest);
|
|
57
|
+
return {
|
|
58
|
+
close() {
|
|
59
|
+
httpServer.removeListener("request", onRequest);
|
|
60
|
+
// Restore the app's original listeners.
|
|
61
|
+
for (const l of appListeners)
|
|
62
|
+
httpServer.on("request", l);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** handle runs one matched POST through mint → decode → dispatch → encode. */
|
|
67
|
+
async function handle(req, res, route, opts, onError) {
|
|
68
|
+
// Auth boundary — identical slot to serve()'s upgrade mint.
|
|
69
|
+
let ctx;
|
|
70
|
+
try {
|
|
71
|
+
ctx = await opts.mintCap(req);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
onError(err);
|
|
75
|
+
writeJson(res, 500, { error: "auth error" });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (ctx === null) {
|
|
79
|
+
writeJson(res, 401, { error: "unauthorized" });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Read + parse the JSON request body.
|
|
83
|
+
let json;
|
|
84
|
+
try {
|
|
85
|
+
const raw = await readBody(req);
|
|
86
|
+
json = raw.length === 0 ? {} : JSON.parse(raw);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
writeJson(res, 400, { error: "invalid JSON request body" });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// JSON → ZAP payload bytes (schema-specific codec from the route).
|
|
93
|
+
let payload;
|
|
94
|
+
try {
|
|
95
|
+
payload = route.decode(json);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
writeJson(res, 400, {
|
|
99
|
+
error: err instanceof Error ? err.message : "request decode failed",
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Build the Call and dispatch it through the SAME handler the WS path uses.
|
|
104
|
+
let handler;
|
|
105
|
+
try {
|
|
106
|
+
handler = opts.rootCap(ctx);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
onError(err);
|
|
110
|
+
writeJson(res, 500, { error: "rootCap failed" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const call = {
|
|
114
|
+
method: route.method,
|
|
115
|
+
promiseID: 1,
|
|
116
|
+
target: NO_TARGET,
|
|
117
|
+
cap: EMPTY,
|
|
118
|
+
payload,
|
|
119
|
+
};
|
|
120
|
+
let resp;
|
|
121
|
+
try {
|
|
122
|
+
resp = await handler(call);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
onError(err);
|
|
126
|
+
writeJson(res, 500, { error: "handler error" });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (resp.status !== Status.OK) {
|
|
130
|
+
writeJson(res, httpStatusFor(resp.status), {
|
|
131
|
+
error: errorBodyText(resp.body) ?? `status ${resp.status}`,
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Void method: 204, no body.
|
|
136
|
+
if (!route.encode) {
|
|
137
|
+
res.writeHead(204);
|
|
138
|
+
res.end();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// ZAP response body → JSON value.
|
|
142
|
+
let out;
|
|
143
|
+
try {
|
|
144
|
+
out = route.encode(resp.body);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
onError(err);
|
|
148
|
+
writeJson(res, 500, { error: "response encode failed" });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
writeJson(res, 200, out);
|
|
152
|
+
}
|
|
153
|
+
/** readBody collects the request body as a UTF-8 string, capped at the ceiling. */
|
|
154
|
+
function readBody(req) {
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
const chunks = [];
|
|
157
|
+
let total = 0;
|
|
158
|
+
req.on("data", (chunk) => {
|
|
159
|
+
total += chunk.length;
|
|
160
|
+
if (total > MAX_BODY_BYTES) {
|
|
161
|
+
reject(new Error("request body too large"));
|
|
162
|
+
req.destroy();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
chunks.push(chunk);
|
|
166
|
+
});
|
|
167
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
168
|
+
req.on("error", reject);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** writeJson sends a JSON response with the given HTTP status. */
|
|
172
|
+
function writeJson(res, status, value) {
|
|
173
|
+
const body = Buffer.from(JSON.stringify(value), "utf8");
|
|
174
|
+
res.writeHead(status, {
|
|
175
|
+
"content-type": "application/json; charset=utf-8",
|
|
176
|
+
"content-length": String(body.byteLength),
|
|
177
|
+
});
|
|
178
|
+
res.end(body);
|
|
179
|
+
}
|
|
180
|
+
/** Map a ZAP Status to the closest HTTP status (they share the 4xx/5xx space). */
|
|
181
|
+
function httpStatusFor(zapStatus) {
|
|
182
|
+
switch (zapStatus) {
|
|
183
|
+
case Status.BadRequest:
|
|
184
|
+
case Status.Unauthorized:
|
|
185
|
+
case Status.Forbidden:
|
|
186
|
+
case Status.NotFound:
|
|
187
|
+
case Status.Internal:
|
|
188
|
+
return zapStatus;
|
|
189
|
+
default:
|
|
190
|
+
return zapStatus >= 400 && zapStatus <= 599 ? zapStatus : 500;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** errorBodyText extracts {"error": "..."} text from a ZAP error body, if any. */
|
|
194
|
+
function errorBodyText(body) {
|
|
195
|
+
if (body.byteLength === 0)
|
|
196
|
+
return null;
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(new TextDecoder().decode(body));
|
|
199
|
+
if (parsed && typeof parsed.error === "string")
|
|
200
|
+
return parsed.error;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// not JSON — fall through.
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
/** normalizePrefix trims a trailing slash and ensures a leading slash (or ""). */
|
|
208
|
+
function normalizePrefix(prefix) {
|
|
209
|
+
if (prefix === "" || prefix === "/")
|
|
210
|
+
return "";
|
|
211
|
+
let p = prefix.startsWith("/") ? prefix : "/" + prefix;
|
|
212
|
+
if (p.endsWith("/"))
|
|
213
|
+
p = p.slice(0, -1);
|
|
214
|
+
return p;
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { CallHandler } from "./conn.js";
|
|
|
3
3
|
import type { MintCap } from "./auth.js";
|
|
4
4
|
export { nodeWsTransport } from "./transport.js";
|
|
5
5
|
export type { MintCap } from "./auth.js";
|
|
6
|
+
export { httpServe } from "./http.js";
|
|
7
|
+
export type { HttpRoute, HttpServeOptions, HttpServeHandle, } from "./http.js";
|
|
6
8
|
/**
|
|
7
9
|
* rootCap produces the per-connection dispatch root for a minted ctx.
|
|
8
10
|
*
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAmB,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAG7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKzC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,YAAY,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAmB,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAG7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKzC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,YAAY,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMzC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,YAAY,EACV,SAAS,EACT,gBAAgB,EAChB,eAAe,GAChB,MAAM,WAAW,CAAC;AAEnB;;;;;;;;GAQG;AACH,MAAM,MAAM,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,WAAW,CAAC;AAErD,iCAAiC;AACjC,MAAM,WAAW,YAAY,CAAC,GAAG;IAC/B,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IACtB,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IACtB,oDAAoD;IACpD,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAClC;AAED,qEAAqE;AACrE,MAAM,WAAW,WAAW;IAC1B,oDAAoD;IACpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EACvB,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC,GACtB,WAAW,CAmEb"}
|
package/dist/server.js
CHANGED
|
@@ -26,6 +26,11 @@ import { nodeWsTransport } from "./transport.js";
|
|
|
26
26
|
// server-side transport factory and the auth slot so consumers import the whole
|
|
27
27
|
// server API from one sub-path (the root entry is browser-safe and omits these).
|
|
28
28
|
export { nodeWsTransport } from "./transport.js";
|
|
29
|
+
// httpServe mounts a JSON-over-HTTP face on the SAME ZAP service, dispatching
|
|
30
|
+
// each POST /<service>/<method> through the identical CallHandler serve() uses
|
|
31
|
+
// over WebSocket. The OpenAPI doc emitted by `zapgen --emit=openapi` describes
|
|
32
|
+
// exactly these routes.
|
|
33
|
+
export { httpServe } from "./http.js";
|
|
29
34
|
/**
|
|
30
35
|
* Attach a ZAP RPC endpoint to `httpServer`. Returns a handle whose close()
|
|
31
36
|
* tears down the WebSocket server and all open connections.
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,4CAA4C;AAE5C;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAI5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAGjD,sEAAsE;AACtE,gFAAgF;AAChF,iFAAiF;AACjF,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-proto/web",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Browser frontend RPC over ZAP — a drop-in tRPC replacement for Next.js / Remix / SvelteKit. Native ZAP envelopes over WebSocket binary frames
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Browser frontend RPC over ZAP — a drop-in tRPC replacement for Next.js / Remix / SvelteKit. Native ZAP envelopes over WebSocket binary frames, plus an optional JSON-over-HTTP face (httpServe) for the OpenAPI 3.1 surface emitted by zapgen. Layered on @zap-proto/zap. No Cap'n Proto.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"src"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@zap-proto/zap": "^1.
|
|
43
|
+
"@zap-proto/zap": "^1.3.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"ws": ">=8"
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "^22.0.0",
|
|
55
55
|
"@types/ws": "^8.5.12",
|
|
56
|
+
"ajv": "^8.20.0",
|
|
56
57
|
"eslint": "^9.0.0",
|
|
57
58
|
"happy-dom": "^20.10.4",
|
|
58
59
|
"prettier": "^3.3.0",
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
2
|
+
// See the file LICENSE for licensing terms.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* http.ts — httpServe(httpServer, opts): mount a JSON-over-HTTP face on the
|
|
6
|
+
* SAME ZAP service that serve() exposes over WebSocket.
|
|
7
|
+
*
|
|
8
|
+
* Motivation: external consumers (and the OpenAPI surface emitted by
|
|
9
|
+
* `zapgen --emit=openapi`) speak POST /<service>/<method> with a JSON body.
|
|
10
|
+
* httpServe terminates that HTTP shape and dispatches each request through the
|
|
11
|
+
* identical {@link CallHandler} the WebSocket transport uses — so there is ONE
|
|
12
|
+
* service implementation, reached two ways.
|
|
13
|
+
*
|
|
14
|
+
* The JSON⇄ZAP codec is schema-specific and therefore supplied by the caller as
|
|
15
|
+
* a {@link HttpRoute} per method (the generated zapgen bindings know each
|
|
16
|
+
* struct's shape; the runtime stays schema-agnostic — orthogonal separation).
|
|
17
|
+
* Each route declares:
|
|
18
|
+
* - path: POST path, matching the OpenAPI doc (`/document-service/...`).
|
|
19
|
+
* - method: the .zap method ordinal (the same ordinal the WS client sends).
|
|
20
|
+
* - decode: JSON request body → ZAP payload bytes.
|
|
21
|
+
* - encode: ZAP response body bytes → JSON value (omit for void methods).
|
|
22
|
+
*
|
|
23
|
+
* Auth runs per-request via the same {@link MintCap} slot serve() uses: a null
|
|
24
|
+
* mint yields HTTP 401; a non-null mint becomes the `ctx` passed to rootCap,
|
|
25
|
+
* whose {@link CallHandler} dispatches the decoded Call.
|
|
26
|
+
*
|
|
27
|
+
* Node `http` only — no framework. The HTTP server is supplied by the caller
|
|
28
|
+
* (the same http.Server that serve() attaches its WebSocket upgrade to), so one
|
|
29
|
+
* port serves WS + HTTP.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type {
|
|
33
|
+
IncomingMessage,
|
|
34
|
+
ServerResponse,
|
|
35
|
+
Server as HttpServer,
|
|
36
|
+
} from "node:http";
|
|
37
|
+
import { NO_TARGET, Status, type Call, type Response } from "@zap-proto/zap";
|
|
38
|
+
import type { CallHandler } from "./conn.js";
|
|
39
|
+
import type { MintCap } from "./auth.js";
|
|
40
|
+
import type { RootCap } from "./server.js";
|
|
41
|
+
|
|
42
|
+
const EMPTY = new Uint8Array(0);
|
|
43
|
+
const MAX_BODY_BYTES = 8 * 1024 * 1024; // 8 MiB request-body ceiling.
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* HttpRoute binds one POST path to one ZAP method, with the JSON⇄ZAP codec for
|
|
47
|
+
* that method. `decode` turns the parsed JSON request body into the method's
|
|
48
|
+
* ZAP payload bytes; `encode` turns the ZAP response body bytes into the JSON
|
|
49
|
+
* value to serialize. A void method omits `encode` (returns HTTP 204).
|
|
50
|
+
*/
|
|
51
|
+
export interface HttpRoute {
|
|
52
|
+
/** POST path, e.g. "/document-service/create-document". */
|
|
53
|
+
path: string;
|
|
54
|
+
/** The .zap method ordinal this path dispatches. */
|
|
55
|
+
method: number;
|
|
56
|
+
/** Parse a JSON request body into ZAP payload bytes. */
|
|
57
|
+
decode: (json: unknown) => Uint8Array;
|
|
58
|
+
/** Encode ZAP response body bytes into a JSON value. Omit for void methods. */
|
|
59
|
+
encode?: (body: Uint8Array) => unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Options for {@link httpServe}. */
|
|
63
|
+
export interface HttpServeOptions<Ctx> {
|
|
64
|
+
/** The method routes (one per OpenAPI operation). */
|
|
65
|
+
routes: HttpRoute[];
|
|
66
|
+
/** Bearer→ctx boundary; return null to reject with HTTP 401. */
|
|
67
|
+
mintCap: MintCap<Ctx>;
|
|
68
|
+
/** Produce the dispatch handler for a minted ctx (same shape as serve()). */
|
|
69
|
+
rootCap: RootCap<Ctx>;
|
|
70
|
+
/**
|
|
71
|
+
* Optional path prefix mounted before each route path, e.g. "/v1" makes the
|
|
72
|
+
* route "/echo/echo" serve at "/v1/echo/echo". Default "" (no prefix).
|
|
73
|
+
*/
|
|
74
|
+
prefix?: string;
|
|
75
|
+
/** Optional sink for handler errors. */
|
|
76
|
+
onError?: (err: unknown) => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A live HTTP face attached to an http.Server. */
|
|
80
|
+
export interface HttpServeHandle {
|
|
81
|
+
/** Detach the request listener. */
|
|
82
|
+
close(): void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Attach a JSON-over-HTTP face to `httpServer`. Returns a handle whose close()
|
|
87
|
+
* detaches the listener. Non-matching requests are passed through untouched so
|
|
88
|
+
* httpServe can coexist with the app's own request handler and with serve()'s
|
|
89
|
+
* WebSocket upgrade on the same server.
|
|
90
|
+
*/
|
|
91
|
+
export function httpServe<Ctx>(
|
|
92
|
+
httpServer: HttpServer,
|
|
93
|
+
opts: HttpServeOptions<Ctx>,
|
|
94
|
+
): HttpServeHandle {
|
|
95
|
+
const onError = opts.onError ?? (() => {});
|
|
96
|
+
const prefix = normalizePrefix(opts.prefix ?? "");
|
|
97
|
+
|
|
98
|
+
// path → route, built once. Promise ordinal collisions are the caller's
|
|
99
|
+
// concern (each method has a distinct path by construction).
|
|
100
|
+
const table = new Map<string, HttpRoute>();
|
|
101
|
+
for (const r of opts.routes) table.set(prefix + r.path, r);
|
|
102
|
+
|
|
103
|
+
// Take over `request` dispatch: capture the app's existing listeners, detach
|
|
104
|
+
// them, and install a single dispatcher that either handles a matched route
|
|
105
|
+
// fully (so the response is written exactly once) or delegates to the
|
|
106
|
+
// captured listeners. Because route handling is async (mintCap, body read),
|
|
107
|
+
// a plain extra listener would race the app's synchronous catch-all; owning
|
|
108
|
+
// dispatch is the only way to guarantee a single writer per request.
|
|
109
|
+
type RequestListener = (req: IncomingMessage, res: ServerResponse) => void;
|
|
110
|
+
const appListeners = httpServer.listeners("request") as RequestListener[];
|
|
111
|
+
httpServer.removeAllListeners("request");
|
|
112
|
+
|
|
113
|
+
const delegate = (req: IncomingMessage, res: ServerResponse): void => {
|
|
114
|
+
if (appListeners.length === 0) {
|
|
115
|
+
if (!res.writableEnded) {
|
|
116
|
+
writeJson(res, 404, { error: "not found" });
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
for (const l of appListeners) l(req, res);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const onRequest = (req: IncomingMessage, res: ServerResponse): void => {
|
|
124
|
+
let url: URL;
|
|
125
|
+
try {
|
|
126
|
+
url = new URL(req.url ?? "/", "http://localhost");
|
|
127
|
+
} catch {
|
|
128
|
+
delegate(req, res);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const route = table.get(url.pathname);
|
|
132
|
+
if (!route) {
|
|
133
|
+
delegate(req, res); // not ours — hand to the app's own handler.
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (req.method !== "POST") {
|
|
138
|
+
writeJson(res, 405, { error: "method not allowed; use POST" });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
void handle(req, res, route, opts, onError).catch((err) => {
|
|
143
|
+
onError(err);
|
|
144
|
+
if (!res.headersSent) writeJson(res, 500, { error: "internal error" });
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
httpServer.on("request", onRequest);
|
|
149
|
+
return {
|
|
150
|
+
close(): void {
|
|
151
|
+
httpServer.removeListener("request", onRequest);
|
|
152
|
+
// Restore the app's original listeners.
|
|
153
|
+
for (const l of appListeners) httpServer.on("request", l);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** handle runs one matched POST through mint → decode → dispatch → encode. */
|
|
159
|
+
async function handle<Ctx>(
|
|
160
|
+
req: IncomingMessage,
|
|
161
|
+
res: ServerResponse,
|
|
162
|
+
route: HttpRoute,
|
|
163
|
+
opts: HttpServeOptions<Ctx>,
|
|
164
|
+
onError: (err: unknown) => void,
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
// Auth boundary — identical slot to serve()'s upgrade mint.
|
|
167
|
+
let ctx: Ctx | null;
|
|
168
|
+
try {
|
|
169
|
+
ctx = await opts.mintCap(req);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
onError(err);
|
|
172
|
+
writeJson(res, 500, { error: "auth error" });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (ctx === null) {
|
|
176
|
+
writeJson(res, 401, { error: "unauthorized" });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Read + parse the JSON request body.
|
|
181
|
+
let json: unknown;
|
|
182
|
+
try {
|
|
183
|
+
const raw = await readBody(req);
|
|
184
|
+
json = raw.length === 0 ? {} : JSON.parse(raw);
|
|
185
|
+
} catch {
|
|
186
|
+
writeJson(res, 400, { error: "invalid JSON request body" });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// JSON → ZAP payload bytes (schema-specific codec from the route).
|
|
191
|
+
let payload: Uint8Array;
|
|
192
|
+
try {
|
|
193
|
+
payload = route.decode(json);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
writeJson(res, 400, {
|
|
196
|
+
error: err instanceof Error ? err.message : "request decode failed",
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Build the Call and dispatch it through the SAME handler the WS path uses.
|
|
202
|
+
let handler: CallHandler;
|
|
203
|
+
try {
|
|
204
|
+
handler = opts.rootCap(ctx);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
onError(err);
|
|
207
|
+
writeJson(res, 500, { error: "rootCap failed" });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const call: Call = {
|
|
212
|
+
method: route.method,
|
|
213
|
+
promiseID: 1,
|
|
214
|
+
target: NO_TARGET,
|
|
215
|
+
cap: EMPTY,
|
|
216
|
+
payload,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
let resp: Response;
|
|
220
|
+
try {
|
|
221
|
+
resp = await handler(call);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
onError(err);
|
|
224
|
+
writeJson(res, 500, { error: "handler error" });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (resp.status !== Status.OK) {
|
|
229
|
+
writeJson(res, httpStatusFor(resp.status), {
|
|
230
|
+
error: errorBodyText(resp.body) ?? `status ${resp.status}`,
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Void method: 204, no body.
|
|
236
|
+
if (!route.encode) {
|
|
237
|
+
res.writeHead(204);
|
|
238
|
+
res.end();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ZAP response body → JSON value.
|
|
243
|
+
let out: unknown;
|
|
244
|
+
try {
|
|
245
|
+
out = route.encode(resp.body);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
onError(err);
|
|
248
|
+
writeJson(res, 500, { error: "response encode failed" });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
writeJson(res, 200, out);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** readBody collects the request body as a UTF-8 string, capped at the ceiling. */
|
|
255
|
+
function readBody(req: IncomingMessage): Promise<string> {
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
const chunks: Buffer[] = [];
|
|
258
|
+
let total = 0;
|
|
259
|
+
req.on("data", (chunk: Buffer) => {
|
|
260
|
+
total += chunk.length;
|
|
261
|
+
if (total > MAX_BODY_BYTES) {
|
|
262
|
+
reject(new Error("request body too large"));
|
|
263
|
+
req.destroy();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
chunks.push(chunk);
|
|
267
|
+
});
|
|
268
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
269
|
+
req.on("error", reject);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** writeJson sends a JSON response with the given HTTP status. */
|
|
274
|
+
function writeJson(res: ServerResponse, status: number, value: unknown): void {
|
|
275
|
+
const body = Buffer.from(JSON.stringify(value), "utf8");
|
|
276
|
+
res.writeHead(status, {
|
|
277
|
+
"content-type": "application/json; charset=utf-8",
|
|
278
|
+
"content-length": String(body.byteLength),
|
|
279
|
+
});
|
|
280
|
+
res.end(body);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Map a ZAP Status to the closest HTTP status (they share the 4xx/5xx space). */
|
|
284
|
+
function httpStatusFor(zapStatus: number): number {
|
|
285
|
+
switch (zapStatus) {
|
|
286
|
+
case Status.BadRequest:
|
|
287
|
+
case Status.Unauthorized:
|
|
288
|
+
case Status.Forbidden:
|
|
289
|
+
case Status.NotFound:
|
|
290
|
+
case Status.Internal:
|
|
291
|
+
return zapStatus;
|
|
292
|
+
default:
|
|
293
|
+
return zapStatus >= 400 && zapStatus <= 599 ? zapStatus : 500;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** errorBodyText extracts {"error": "..."} text from a ZAP error body, if any. */
|
|
298
|
+
function errorBodyText(body: Uint8Array): string | null {
|
|
299
|
+
if (body.byteLength === 0) return null;
|
|
300
|
+
try {
|
|
301
|
+
const parsed = JSON.parse(new TextDecoder().decode(body));
|
|
302
|
+
if (parsed && typeof parsed.error === "string") return parsed.error;
|
|
303
|
+
} catch {
|
|
304
|
+
// not JSON — fall through.
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** normalizePrefix trims a trailing slash and ensures a leading slash (or ""). */
|
|
310
|
+
function normalizePrefix(prefix: string): string {
|
|
311
|
+
if (prefix === "" || prefix === "/") return "";
|
|
312
|
+
let p = prefix.startsWith("/") ? prefix : "/" + prefix;
|
|
313
|
+
if (p.endsWith("/")) p = p.slice(0, -1);
|
|
314
|
+
return p;
|
|
315
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -35,6 +35,17 @@ import type { MintCap } from "./auth.js";
|
|
|
35
35
|
export { nodeWsTransport } from "./transport.js";
|
|
36
36
|
export type { MintCap } from "./auth.js";
|
|
37
37
|
|
|
38
|
+
// httpServe mounts a JSON-over-HTTP face on the SAME ZAP service, dispatching
|
|
39
|
+
// each POST /<service>/<method> through the identical CallHandler serve() uses
|
|
40
|
+
// over WebSocket. The OpenAPI doc emitted by `zapgen --emit=openapi` describes
|
|
41
|
+
// exactly these routes.
|
|
42
|
+
export { httpServe } from "./http.js";
|
|
43
|
+
export type {
|
|
44
|
+
HttpRoute,
|
|
45
|
+
HttpServeOptions,
|
|
46
|
+
HttpServeHandle,
|
|
47
|
+
} from "./http.js";
|
|
48
|
+
|
|
38
49
|
/**
|
|
39
50
|
* rootCap produces the per-connection dispatch root for a minted ctx.
|
|
40
51
|
*
|