@mailwoman/api-kit 9.2.0 → 9.3.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.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The engine stamp on the HTTP side: the zod schema every app documents it with, the two headers every response
7
+ * carries, and the helper that attaches the body field. The stamp itself is built by the `mailwoman` package and
8
+ * arrives as an option value: an app factory (`lib/app.ts`, `lib/routes.ts`, `lib/schema.ts`) is engine-agnostic and
9
+ * must not import `mailwoman`; the bin (`lib/cli.ts`) is the wiring layer that resolves the stamp and passes it in.
10
+ */
11
+
12
+ import { z } from "@hono/zod-openapi"
13
+ import type { EngineStamp } from "@mailwoman/core/license"
14
+ import type { MiddlewareHandler } from "hono"
15
+
16
+ /**
17
+ * Strict on purpose: the stamp carries no licensee and no key id, and a strict object makes a field that leaks one a
18
+ * schema failure rather than a documented extension.
19
+ */
20
+ export const EngineStampSchema = z
21
+ .strictObject({
22
+ name: z.literal("mailwoman"),
23
+ version: z.string(),
24
+ license: z.string(),
25
+ license_url: z.string(),
26
+ notice: z.string().optional(),
27
+ })
28
+ .openapi("EngineStamp") satisfies z.ZodType<EngineStamp>
29
+
30
+ /**
31
+ * A route's response schema once the route attaches the stamp: the body schema intersected with the optional `engine`
32
+ * field. Applied at the ROUTE, never on an outcome schema, so an outcome schema keeps describing what the engine
33
+ * produces (the schema drift pin in `mailwoman` depends on that) and the OpenAPI document references the outcome
34
+ * component through `allOf` instead of cloning it.
35
+ */
36
+ export function stampedResponseSchema<S extends z.ZodTypeAny>(schema: S) {
37
+ return z.intersection(schema, z.object({ engine: EngineStampSchema.optional() }))
38
+ }
39
+
40
+ /**
41
+ * `Server` names the engine and its license branch; `Link: rel="license"` is the registered relation (RFC 8288) that
42
+ * lets a proxy, a browser, or `curl -I` find the terms without a body change. Set before the handler runs, so the
43
+ * headers are on the context when any `c.json` — the route's or the error net's — builds its response.
44
+ */
45
+ export function engineHeaders(stamp: EngineStamp): MiddlewareHandler {
46
+ const server = `mailwoman/${stamp.version} (${stamp.license})`
47
+ const link = `<${stamp.license_url}>; rel="license"`
48
+
49
+ return async (c, next) => {
50
+ c.header("Server", server)
51
+ c.header("Link", link)
52
+
53
+ await next()
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Attach the `engine` field when a stamp is configured. The field goes LAST, so a body that already spells a key of the
59
+ * same name keeps the stamp's value.
60
+ */
61
+ export function withEngineStamp<T extends object>(
62
+ body: T,
63
+ stamp: EngineStamp | undefined
64
+ ): T & { engine?: EngineStamp } {
65
+ return stamp ? { ...body, engine: stamp } : body
66
+ }
@@ -9,9 +9,10 @@
9
9
  * 2026-07-12 design spec's anti-meta guardrails).
10
10
  */
11
11
 
12
- export * from "./error.ts"
13
- export * from "./geo.ts"
14
- export * from "./metrics.ts"
15
- export * from "./openapi.ts"
16
- export * from "./request.ts"
17
- export * from "./serve.ts"
12
+ export * from "#error"
13
+ export * from "#geo"
14
+ export * from "#metrics"
15
+ export * from "#openapi"
16
+ export * from "#request"
17
+ export * from "#serve"
18
+ export * from "#engine-stamp"
@@ -12,6 +12,8 @@
12
12
  * state: under `node:cluster` each worker reports its own snapshot — aggregate at the scraper.
13
13
  */
14
14
 
15
+ import { percentileSorted } from "@mailwoman/core/stats"
16
+
15
17
  /**
16
18
  * Recent-latency reservoir size. ~2k samples gives stable p99 without unbounded memory.
17
19
  */
@@ -50,18 +52,15 @@ export function recordTimed(latencyMs: number, tier: string): void {
50
52
  }
51
53
 
52
54
  /**
53
- * Percentile for the metrics snapshot deliberately NOT `percentile` from `@mailwoman/core/utils`.
54
- *
55
- * Three differences, each on purpose for a hot request-path counter: it takes an ALREADY-sorted array (the caller sorts
56
- * once per snapshot, not once per percentile), returns `0` rather than `null` on empty so the snapshot stays a plain
57
- * number map, and rounds to two decimals because these are milliseconds on a wire format. Core's returns `null` and
58
- * does not round.
55
+ * A latency percentile as milliseconds on a wire format: two decimals. The caller sorts once per snapshot and
56
+ * guarantees a non-empty sample, so `percentileSorted`'s `null` is a caller error here, never a reading.
59
57
  */
60
- function percentile(sorted: number[], p: number): number {
61
- if (!sorted.length) return 0
62
- const idx = Math.min(sorted.length - 1, Math.floor(p * sorted.length))
58
+ function latencyPercentile(sorted: readonly number[], p: number): number {
59
+ const value = percentileSorted(sorted, p)
60
+
61
+ if (value === null) throw new Error("latencyPercentile requires a non-empty sample")
63
62
 
64
- return Math.round(sorted[idx]! * 100) / 100
63
+ return Math.round(value * 100) / 100
65
64
  }
66
65
 
67
66
  export interface MetricsSnapshot {
@@ -93,9 +92,9 @@ export function metricsSnapshot(): MetricsSnapshot {
93
92
  tiers: { ...tierCounts },
94
93
  latency_ms: sorted.length
95
94
  ? {
96
- p50: percentile(sorted, 0.5),
97
- p90: percentile(sorted, 0.9),
98
- p99: percentile(sorted, 0.99),
95
+ p50: latencyPercentile(sorted, 50),
96
+ p90: latencyPercentile(sorted, 90),
97
+ p99: latencyPercentile(sorted, 99),
99
98
  max: Math.round(sorted.at(-1)! * 100) / 100,
100
99
  }
101
100
  : null,
@@ -8,10 +8,8 @@
8
8
  * (progenitor), replacing the old hand-downgrade step.
9
9
  */
10
10
 
11
- import { mkdirSync, writeFileSync } from "node:fs"
12
- import { dirname } from "node:path"
13
-
14
11
  import type { OpenAPIHono } from "@hono/zod-openapi"
12
+ import { writeLocalTextFile } from "@mailwoman/core/fs/writers"
15
13
 
16
14
  type OpenAPISecurityRequirements = Parameters<OpenAPIHono["getOpenAPI31Document"]>[0]["security"]
17
15
 
@@ -90,19 +88,20 @@ export function emitOpenAPIDocuments(app: OpenAPIHono, info: OpenAPIDocInfo): {
90
88
  * gitignored, not-yet-existing `docs/static/openapi/`). One place owns this so the four emitters can't drift out of
91
89
  * lockstep with each other.
92
90
  */
93
- export function printOpenAPIDocument(
91
+ export async function printOpenAPIDocument(
94
92
  app: OpenAPIHono,
95
93
  info: OpenAPIDocInfo,
96
94
  opts: { flavor?: string; out?: string } = {}
97
- ): void {
95
+ ): Promise<void> {
98
96
  const { v31, v30 } = emitOpenAPIDocuments(app, info)
99
- const json = JSON.stringify(opts.flavor === "3.0" ? v30 : v31)
97
+ // Compact JSON, one line, the same bytes to a file and to stdout — a consumer piping either into a diff or a
98
+ // generator sees one form.
99
+ const json = `${JSON.stringify(opts.flavor === "3.0" ? v30 : v31)}\n`
100
100
 
101
101
  if (opts.out) {
102
- mkdirSync(dirname(opts.out), { recursive: true })
103
- writeFileSync(opts.out, `${json}\n`)
102
+ await writeLocalTextFile(json, opts.out)
104
103
  } else {
105
- console.log(json)
104
+ process.stdout.write(json)
106
105
  }
107
106
  }
108
107
 
@@ -20,6 +20,14 @@ import type { Context } from "hono"
20
20
  * this shape rather than Hono's uniformly-array `queries()`. Built on a null-prototype object so a query key of
21
21
  * `__proto__` or `constructor` cannot reach `Object`'s prototype — these handlers take arbitrary internet input.
22
22
  */
23
+ /**
24
+ * A non-empty string query value, else `undefined`. An empty `?q=` is treated as absent, since every drop-in reads it
25
+ * that way.
26
+ */
27
+ export function asString(raw: unknown): string | undefined {
28
+ return typeof raw === "string" && raw.length ? raw : undefined
29
+ }
30
+
23
31
  export function legacyQuery(c: Context): Record<string, string | string[]> {
24
32
  const out: Record<string, string | string[]> = Object.create(null)
25
33
 
package/lib/serve.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Node serve wrapper over `@hono/node-server`. The one place the node listener is created —
7
+ * surface packages stay web-standard (they only export `fetch`-shaped apps) so an edge
8
+ * deployment needs no changes to them.
9
+ */
10
+
11
+ import { serve, type ServerType } from "@hono/node-server"
12
+
13
+ /**
14
+ * A `fetch`-shaped request handler (what `OpenAPIHono.fetch` provides).
15
+ */
16
+ export type FetchLike = (request: Request, ...args: never[]) => Response | Promise<Response>
17
+
18
+ /**
19
+ * Options for `serveNode()`. Extracted since Hono doesn't seem to export them.
20
+ */
21
+ export type ServeNodeOptions = Parameters<typeof serve>[0] & {
22
+ /**
23
+ * Called once the listener is bound — receives the actual port (useful with `port: 0`).
24
+ */
25
+ onListen?: (info: { port: number; address: string }) => void
26
+ }
27
+
28
+ /**
29
+ * The listener plus the port it bound, which `port: 0` callers need. Only `port` is added: `net.Server` already owns an
30
+ * `address()` method, and Node's cluster child calls it inside its own `listening` handler, so a value property of that
31
+ * name on the handle breaks every cluster worker at listen.
32
+ */
33
+ export type ServerHandle = ServerType &
34
+ AsyncDisposable & {
35
+ readonly port: number
36
+ }
37
+
38
+ const defaultOnListen = ({ port, address }: { port: number; address: string }) =>
39
+ console.error(`[mailwoman] native /v1 API listening on http://${address}:${port}`)
40
+
41
+ /**
42
+ * Boot a node HTTP listener for a Hono app. Returns an async-disposable handle once the listener is ready.
43
+ */
44
+ export function serveNode({ onListen = defaultOnListen, ...options }: ServeNodeOptions): Promise<ServerHandle> {
45
+ return new Promise<ServerHandle>((resolve, reject) => {
46
+ const server = serve(options, (info) => {
47
+ server.off("error", reject)
48
+
49
+ Object.defineProperty(server, "port", { value: info.port, writable: false })
50
+
51
+ try {
52
+ onListen(info)
53
+ resolve(server as ServerHandle)
54
+ } catch (error) {
55
+ void server[Symbol.asyncDispose]().then(() => reject(error), reject)
56
+ }
57
+ })
58
+
59
+ server.once("error", reject)
60
+ })
61
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The engine stamp on the HTTP side: the zod schema every app documents it with, the two headers every response
7
+ * carries, and the helper that attaches the body field. The stamp itself is built by the `mailwoman` package and
8
+ * arrives as an option value: an app factory (`lib/app.ts`, `lib/routes.ts`, `lib/schema.ts`) is engine-agnostic and
9
+ * must not import `mailwoman`; the bin (`lib/cli.ts`) is the wiring layer that resolves the stamp and passes it in.
10
+ */
11
+ import { z } from "@hono/zod-openapi";
12
+ import type { EngineStamp } from "@mailwoman/core/license";
13
+ import type { MiddlewareHandler } from "hono";
14
+ /**
15
+ * Strict on purpose: the stamp carries no licensee and no key id, and a strict object makes a field that leaks one a
16
+ * schema failure rather than a documented extension.
17
+ */
18
+ export declare const EngineStampSchema: z.ZodObject<{
19
+ name: z.ZodLiteral<"mailwoman">;
20
+ version: z.ZodString;
21
+ license: z.ZodString;
22
+ license_url: z.ZodString;
23
+ notice: z.ZodOptional<z.ZodString>;
24
+ }, z.core.$strict>;
25
+ /**
26
+ * A route's response schema once the route attaches the stamp: the body schema intersected with the optional `engine`
27
+ * field. Applied at the ROUTE, never on an outcome schema, so an outcome schema keeps describing what the engine
28
+ * produces (the schema drift pin in `mailwoman` depends on that) and the OpenAPI document references the outcome
29
+ * component through `allOf` instead of cloning it.
30
+ */
31
+ export declare function stampedResponseSchema<S extends z.ZodTypeAny>(schema: S): z.ZodIntersection<S, z.ZodObject<{
32
+ engine: z.ZodOptional<z.ZodObject<{
33
+ name: z.ZodLiteral<"mailwoman">;
34
+ version: z.ZodString;
35
+ license: z.ZodString;
36
+ license_url: z.ZodString;
37
+ notice: z.ZodOptional<z.ZodString>;
38
+ }, z.core.$strict>>;
39
+ }, z.core.$strip>>;
40
+ /**
41
+ * `Server` names the engine and its license branch; `Link: rel="license"` is the registered relation (RFC 8288) that
42
+ * lets a proxy, a browser, or `curl -I` find the terms without a body change. Set before the handler runs, so the
43
+ * headers are on the context when any `c.json` — the route's or the error net's — builds its response.
44
+ */
45
+ export declare function engineHeaders(stamp: EngineStamp): MiddlewareHandler;
46
+ /**
47
+ * Attach the `engine` field when a stamp is configured. The field goes LAST, so a body that already spells a key of the
48
+ * same name keeps the stamp's value.
49
+ */
50
+ export declare function withEngineStamp<T extends object>(body: T, stamp: EngineStamp | undefined): T & {
51
+ engine?: EngineStamp;
52
+ };
53
+ //# sourceMappingURL=engine-stamp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-stamp.d.ts","sourceRoot":"","sources":["../lib/engine-stamp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAA;AACrC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAE7C;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;;;;kBAQ2B,CAAA;AAEzD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC;;;;;;;;mBAEtE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,iBAAiB,CAUnE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAC/C,IAAI,EAAE,CAAC,EACP,KAAK,EAAE,WAAW,GAAG,SAAS,GAC5B,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,CAE9B"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The engine stamp on the HTTP side: the zod schema every app documents it with, the two headers every response
7
+ * carries, and the helper that attaches the body field. The stamp itself is built by the `mailwoman` package and
8
+ * arrives as an option value: an app factory (`lib/app.ts`, `lib/routes.ts`, `lib/schema.ts`) is engine-agnostic and
9
+ * must not import `mailwoman`; the bin (`lib/cli.ts`) is the wiring layer that resolves the stamp and passes it in.
10
+ */
11
+ import { z } from "@hono/zod-openapi";
12
+ /**
13
+ * Strict on purpose: the stamp carries no licensee and no key id, and a strict object makes a field that leaks one a
14
+ * schema failure rather than a documented extension.
15
+ */
16
+ export const EngineStampSchema = z
17
+ .strictObject({
18
+ name: z.literal("mailwoman"),
19
+ version: z.string(),
20
+ license: z.string(),
21
+ license_url: z.string(),
22
+ notice: z.string().optional(),
23
+ })
24
+ .openapi("EngineStamp");
25
+ /**
26
+ * A route's response schema once the route attaches the stamp: the body schema intersected with the optional `engine`
27
+ * field. Applied at the ROUTE, never on an outcome schema, so an outcome schema keeps describing what the engine
28
+ * produces (the schema drift pin in `mailwoman` depends on that) and the OpenAPI document references the outcome
29
+ * component through `allOf` instead of cloning it.
30
+ */
31
+ export function stampedResponseSchema(schema) {
32
+ return z.intersection(schema, z.object({ engine: EngineStampSchema.optional() }));
33
+ }
34
+ /**
35
+ * `Server` names the engine and its license branch; `Link: rel="license"` is the registered relation (RFC 8288) that
36
+ * lets a proxy, a browser, or `curl -I` find the terms without a body change. Set before the handler runs, so the
37
+ * headers are on the context when any `c.json` — the route's or the error net's — builds its response.
38
+ */
39
+ export function engineHeaders(stamp) {
40
+ const server = `mailwoman/${stamp.version} (${stamp.license})`;
41
+ const link = `<${stamp.license_url}>; rel="license"`;
42
+ return async (c, next) => {
43
+ c.header("Server", server);
44
+ c.header("Link", link);
45
+ await next();
46
+ };
47
+ }
48
+ /**
49
+ * Attach the `engine` field when a stamp is configured. The field goes LAST, so a body that already spells a key of the
50
+ * same name keeps the stamp's value.
51
+ */
52
+ export function withEngineStamp(body, stamp) {
53
+ return stamp ? { ...body, engine: stamp } : body;
54
+ }
55
+ //# sourceMappingURL=engine-stamp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-stamp.js","sourceRoot":"","sources":["../lib/engine-stamp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAA;AAIrC;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAChC,YAAY,CAAC;IACb,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC;IAC5B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;IACvB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC7B,CAAC;KACD,OAAO,CAAC,aAAa,CAAkC,CAAA;AAEzD;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAyB,MAAS;IACtE,OAAO,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,iBAAiB,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAA;AAClF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAkB;IAC/C,MAAM,MAAM,GAAG,aAAa,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC,OAAO,GAAG,CAAA;IAC9D,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW,kBAAkB,CAAA;IAEpD,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACxB,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;QAC1B,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;QAEtB,MAAM,IAAI,EAAE,CAAA;IACb,CAAC,CAAA;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC9B,IAAO,EACP,KAA8B;IAE9B,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AACjD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../error.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAA;AACrC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAElE;;GAEG;AACH,eAAO,MAAM,cAAc;;;iBAKN,CAAA;AAErB;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,oBAAoB,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;;;;;cAElH;AAKD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,UAAU,GAAG,UAAuB;;;;;gBAEjG"}
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../lib/error.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAA;AACrC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAElE;;GAEG;AACH,eAAO,MAAM,cAAc;;;iBAKN,CAAA;AAErB;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,oBAAoB,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;;;;;cAElH;AAKD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,UAAU,GAAG,UAAuB;;;;;gBAEjG"}
package/out/error.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"error.js","sourceRoot":"","sources":["../error.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAA;AAIrC;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC;KAC7B,MAAM,CAAC;IACP,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC7B,CAAC;KACD,OAAO,CAAC,UAAU,CAAC,CAAA;AAErB;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAiC,CAAU,EAAE,MAAS,EAAE,KAAa,EAAE,MAAe;IAClH,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,CAAA;AAC5E,CAAC;AAED,MAAM,2BAA2B,GAChC,mIAAmI,CAAA;AAEpI;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CAAC,CAAU,EAAE,UAAmC,UAAU;IACjG,OAAO,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,gBAAgB,EAAE,2BAA2B,CAAC,CAAA;AACtF,CAAC"}
1
+ {"version":3,"file":"error.js","sourceRoot":"","sources":["../lib/error.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAA;AAIrC;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC;KAC7B,MAAM,CAAC;IACP,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC7B,CAAC;KACD,OAAO,CAAC,UAAU,CAAC,CAAA;AAErB;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAiC,CAAU,EAAE,MAAS,EAAE,KAAa,EAAE,MAAe;IAClH,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,CAAA;AAC5E,CAAC;AAED,MAAM,2BAA2B,GAChC,mIAAmI,CAAA;AAEpI;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CAAC,CAAU,EAAE,UAAmC,UAAU;IACjG,OAAO,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,gBAAgB,EAAE,2BAA2B,CAAC,CAAA;AACtF,CAAC"}
package/out/geo.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"geo.d.ts","sourceRoot":"","sources":["../geo.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAA;AAErC;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;iBAKN,CAAA;AAE1B;;GAEG;AACH,eAAO,MAAM,UAAU,wEAA4D,CAAA;AAEnF;;GAEG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,UAAU,EAAE,CAAC;;;;;;;kBAMlE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,OAAO,EAAE,CAAC;;;kBAKzE"}
1
+ {"version":3,"file":"geo.d.ts","sourceRoot":"","sources":["../lib/geo.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAA;AAErC;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;iBAKN,CAAA;AAE1B;;GAEG;AACH,eAAO,MAAM,UAAU,wEAA4D,CAAA;AAEnF;;GAEG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,UAAU,EAAE,CAAC;;;;;;;kBAMlE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,OAAO,EAAE,CAAC;;;kBAKzE"}
package/out/geo.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"geo.js","sourceRoot":"","sources":["../geo.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAA;AAErC;;GAEG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC;KAClC,MAAM,CAAC;IACP,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;IACxB,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;CAC9C,CAAC;KACD,OAAO,CAAC,eAAe,CAAC,CAAA;AAE1B;;GAEG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;AAEnF;;GAEG;AACH,MAAM,UAAU,aAAa,CAAyB,UAAa;IAClE,OAAO,CAAC,CAAC,MAAM,CAAC;QACf,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC;QAC1B,QAAQ,EAAE,mBAAmB;QAC7B,UAAU;KACV,CAAC,CAAA;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB,CAAyB,OAAU;IACzE,OAAO,CAAC,CAAC,MAAM,CAAC;QACf,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,mBAAmB,CAAC;QACpC,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;KAC1B,CAAC,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"geo.js","sourceRoot":"","sources":["../lib/geo.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAA;AAErC;;GAEG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC;KAClC,MAAM,CAAC;IACP,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;IACxB,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;CAC9C,CAAC;KACD,OAAO,CAAC,eAAe,CAAC,CAAA;AAE1B;;GAEG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;AAEnF;;GAEG;AACH,MAAM,UAAU,aAAa,CAAyB,UAAa;IAClE,OAAO,CAAC,CAAC,MAAM,CAAC;QACf,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC;QAC1B,QAAQ,EAAE,mBAAmB;QAC7B,UAAU;KACV,CAAC,CAAA;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB,CAAyB,OAAU;IACzE,OAAO,CAAC,CAAC,MAAM,CAAC;QACf,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,mBAAmB,CAAC;QACpC,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;KAC1B,CAAC,CAAA;AACH,CAAC"}
package/out/index.d.ts CHANGED
@@ -8,10 +8,11 @@
8
8
  * domain schemas live next to their routes in the package that owns the wire contract (see the
9
9
  * 2026-07-12 design spec's anti-meta guardrails).
10
10
  */
11
- export * from "./error.ts";
12
- export * from "./geo.ts";
13
- export * from "./metrics.ts";
14
- export * from "./openapi.ts";
15
- export * from "./request.ts";
16
- export * from "./serve.ts";
11
+ export * from "#error";
12
+ export * from "#geo";
13
+ export * from "#metrics";
14
+ export * from "#openapi";
15
+ export * from "#request";
16
+ export * from "#serve";
17
+ export * from "#engine-stamp";
17
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,YAAY,CAAA;AAC1B,cAAc,UAAU,CAAA;AACxB,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,QAAQ,CAAA;AACtB,cAAc,MAAM,CAAA;AACpB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,QAAQ,CAAA;AACtB,cAAc,eAAe,CAAA"}
package/out/index.js CHANGED
@@ -8,10 +8,11 @@
8
8
  * domain schemas live next to their routes in the package that owns the wire contract (see the
9
9
  * 2026-07-12 design spec's anti-meta guardrails).
10
10
  */
11
- export * from "./error.js";
12
- export * from "./geo.js";
13
- export * from "./metrics.js";
14
- export * from "./openapi.js";
15
- export * from "./request.js";
16
- export * from "./serve.js";
11
+ export * from "#error";
12
+ export * from "#geo";
13
+ export * from "#metrics";
14
+ export * from "#openapi";
15
+ export * from "#request";
16
+ export * from "#serve";
17
+ export * from "#engine-stamp";
17
18
  //# sourceMappingURL=index.js.map
package/out/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,YAAY,CAAA;AAC1B,cAAc,UAAU,CAAA;AACxB,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,QAAQ,CAAA;AACtB,cAAc,MAAM,CAAA;AACpB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,QAAQ,CAAA;AACtB,cAAc,eAAe,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAkBH;;;GAGG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAejE;AAiBD,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE;QACR,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;QACd;;;WAGG;QACH,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC7B,UAAU,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;QACzE,eAAe,EAAE,MAAM,CAAA;KACvB,CAAA;CACD;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,eAAe,CAoBjD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAW1C"}
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../lib/metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoBH;;;GAGG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAejE;AAcD,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE;QACR,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;QACd;;;WAGG;QACH,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC7B,UAAU,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;QACzE,eAAe,EAAE,MAAM,CAAA;KACvB,CAAA;CACD;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,eAAe,CAoBjD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAW1C"}
package/out/metrics.js CHANGED
@@ -11,6 +11,7 @@
11
11
  * Surfaced by `GET /metrics`; reset on process restart (no persistence — scrape it). Per-process
12
12
  * state: under `node:cluster` each worker reports its own snapshot — aggregate at the scraper.
13
13
  */
14
+ import { percentileSorted } from "@mailwoman/core/stats";
14
15
  /**
15
16
  * Recent-latency reservoir size. ~2k samples gives stable p99 without unbounded memory.
16
17
  */
@@ -45,18 +46,14 @@ export function recordTimed(latencyMs, tier) {
45
46
  }
46
47
  }
47
48
  /**
48
- * Percentile for the metrics snapshot deliberately NOT `percentile` from `@mailwoman/core/utils`.
49
- *
50
- * Three differences, each on purpose for a hot request-path counter: it takes an ALREADY-sorted array (the caller sorts
51
- * once per snapshot, not once per percentile), returns `0` rather than `null` on empty so the snapshot stays a plain
52
- * number map, and rounds to two decimals because these are milliseconds on a wire format. Core's returns `null` and
53
- * does not round.
49
+ * A latency percentile as milliseconds on a wire format: two decimals. The caller sorts once per snapshot and
50
+ * guarantees a non-empty sample, so `percentileSorted`'s `null` is a caller error here, never a reading.
54
51
  */
55
- function percentile(sorted, p) {
56
- if (!sorted.length)
57
- return 0;
58
- const idx = Math.min(sorted.length - 1, Math.floor(p * sorted.length));
59
- return Math.round(sorted[idx] * 100) / 100;
52
+ function latencyPercentile(sorted, p) {
53
+ const value = percentileSorted(sorted, p);
54
+ if (value === null)
55
+ throw new Error("latencyPercentile requires a non-empty sample");
56
+ return Math.round(value * 100) / 100;
60
57
  }
61
58
  /**
62
59
  * Current metrics snapshot — sorted-reservoir percentiles + counters.
@@ -71,9 +68,9 @@ export function metricsSnapshot() {
71
68
  tiers: { ...tierCounts },
72
69
  latency_ms: sorted.length
73
70
  ? {
74
- p50: percentile(sorted, 0.5),
75
- p90: percentile(sorted, 0.9),
76
- p99: percentile(sorted, 0.99),
71
+ p50: latencyPercentile(sorted, 50),
72
+ p90: latencyPercentile(sorted, 90),
73
+ p99: latencyPercentile(sorted, 99),
77
74
  max: Math.round(sorted.at(-1) * 100) / 100,
78
75
  }
79
76
  : null,
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;GAEG;AACH,MAAM,WAAW,GAAG,IAAI,CAAA;AAExB,MAAM,SAAS,GAAa,EAAE,CAAA;AAC9B,IAAI,QAAQ,GAAG,CAAC,CAAA;AAEhB;;GAEG;AACH,MAAM,UAAU,GAA2B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;AAC9D,IAAI,KAAK,GAAG,CAAC,CAAA;AACb,IAAI,MAAM,GAAG,CAAC,CAAA;AACd,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;AAE5B;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,SAAiB,EAAE,IAAY;IAC1D,KAAK,EAAE,CAAA;IAEP,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACtB,MAAM,EAAE,CAAA;IACT,CAAC;SAAM,CAAC;QACP,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;IAC/C,CAAC;IAED,IAAI,SAAS,CAAC,MAAM,GAAG,WAAW,EAAE,CAAC;QACpC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAC1B,CAAC;SAAM,CAAC;QACP,SAAS,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAA;QAC/B,QAAQ,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,GAAG,WAAW,CAAA;IACxC,CAAC;AACF,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,UAAU,CAAC,MAAgB,EAAE,CAAS;IAC9C,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO,CAAC,CAAA;IAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;IAEtE,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAE,GAAG,GAAG,CAAC,GAAG,GAAG,CAAA;AAC5C,CAAC;AAiBD;;GAEG;AACH,MAAM,UAAU,eAAe;IAC9B,MAAM,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IAEvD,OAAO;QACN,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;QACrD,OAAO,EAAE;YACR,KAAK;YACL,MAAM;YACN,KAAK,EAAE,EAAE,GAAG,UAAU,EAAE;YACxB,UAAU,EAAE,MAAM,CAAC,MAAM;gBACxB,CAAC,CAAC;oBACA,GAAG,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC;oBAC5B,GAAG,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC;oBAC5B,GAAG,EAAE,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC;oBAC7B,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,GAAG,GAAG,CAAC,GAAG,GAAG;iBAC3C;gBACF,CAAC,CAAC,IAAI;YACP,eAAe,EAAE,MAAM,CAAC,MAAM;SAC9B;KACD,CAAA;AACF,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB;IAClC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAA;IACpB,QAAQ,GAAG,CAAC,CAAA;IAEZ,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3C,iIAAiI;QACjI,OAAO,UAAU,CAAC,GAAG,CAAC,CAAA;IACvB,CAAC;IAED,KAAK,GAAG,CAAC,CAAA;IACT,MAAM,GAAG,CAAC,CAAA;AACX,CAAC"}
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../lib/metrics.ts"],"names":[],"mappings":"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"}
package/out/openapi.d.ts CHANGED
@@ -68,7 +68,7 @@ export declare function emitOpenAPIDocuments(app: OpenAPIHono, info: OpenAPIDocI
68
68
  export declare function printOpenAPIDocument(app: OpenAPIHono, info: OpenAPIDocInfo, opts?: {
69
69
  flavor?: string;
70
70
  out?: string;
71
- }): void;
71
+ }): Promise<void>;
72
72
  /**
73
73
  * An OpenAPI error-response descriptor: a description plus a JSON body of `schema`.
74
74
  *
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEpD,KAAK,2BAA2B,GAAG,UAAU,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAA;AAEjG;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC/C,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACzC,YAAY,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;IACpD,OAAO,CAAC,EAAE,KAAK,CAAC;QACf,GAAG,EAAE,MAAM,CAAA;QACX,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KACrE,CAAC,CAAA;IACF,IAAI,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACpD,QAAQ,CAAC,EAAE,2BAA2B,CAAA;CACtC;AAiBD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,SAAkB,GAAG,IAAI,CAItG;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,cAAc,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAezG;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CACnC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,cAAc,EACpB,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1C,IAAI,CAUN;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;;;;;;;EAK7D"}
1
+ {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../lib/openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAGpD,KAAK,2BAA2B,GAAG,UAAU,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAA;AAEjG;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC/C,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACzC,YAAY,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;IACpD,OAAO,CAAC,EAAE,KAAK,CAAC;QACf,GAAG,EAAE,MAAM,CAAA;QACX,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KACrE,CAAC,CAAA;IACF,IAAI,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACpD,QAAQ,CAAC,EAAE,2BAA2B,CAAA;CACtC;AAiBD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,SAAkB,GAAG,IAAI,CAItG;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,cAAc,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAezG;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACzC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,cAAc,EACpB,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1C,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;;;;;;;EAK7D"}
package/out/openapi.js CHANGED
@@ -7,8 +7,7 @@
7
7
  * handwritten. 3.1 is the published flavor; 3.0 exists solely for client generators that lag
8
8
  * (progenitor), replacing the old hand-downgrade step.
9
9
  */
10
- import { mkdirSync, writeFileSync } from "node:fs";
11
- import { dirname } from "node:path";
10
+ import { writeLocalTextFile } from "@mailwoman/core/fs/writers";
12
11
  /**
13
12
  * Split an `OpenAPIDocInfo` into the document's `info` block and its top-level sibling fields.
14
13
  */
@@ -55,15 +54,16 @@ export function emitOpenAPIDocuments(app, info) {
55
54
  * gitignored, not-yet-existing `docs/static/openapi/`). One place owns this so the four emitters can't drift out of
56
55
  * lockstep with each other.
57
56
  */
58
- export function printOpenAPIDocument(app, info, opts = {}) {
57
+ export async function printOpenAPIDocument(app, info, opts = {}) {
59
58
  const { v31, v30 } = emitOpenAPIDocuments(app, info);
60
- const json = JSON.stringify(opts.flavor === "3.0" ? v30 : v31);
59
+ // Compact JSON, one line, the same bytes to a file and to stdout — a consumer piping either into a diff or a
60
+ // generator sees one form.
61
+ const json = `${JSON.stringify(opts.flavor === "3.0" ? v30 : v31)}\n`;
61
62
  if (opts.out) {
62
- mkdirSync(dirname(opts.out), { recursive: true });
63
- writeFileSync(opts.out, `${json}\n`);
63
+ await writeLocalTextFile(json, opts.out);
64
64
  }
65
65
  else {
66
- console.log(json);
66
+ process.stdout.write(json);
67
67
  }
68
68
  }
69
69
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.js","sourceRoot":"","sources":["../openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AA4BnC;;GAEG;AACH,SAAS,gBAAgB,CAAC,IAAoB;IAC7C,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAA;IAE9G,OAAO;QACN,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE;QAChE,YAAY;QACZ,OAAO;QACP,IAAI;QACJ,QAAQ;KACR,CAAA;AACF,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAgB,EAAE,IAAoB,EAAE,IAAI,GAAG,eAAe;IAC/F,MAAM,MAAM,GAAwC,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAA;IAEnG,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;AACxB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAgB,EAAE,IAAoB;IAC1E,MAAM,SAAS,GAAuD;QACrE,OAAO,EAAE,OAAO;QAChB,GAAG,gBAAgB,CAAC,IAAI,CAAC;KACzB,CAAA;IAED,MAAM,SAAS,GAAqD;QACnE,OAAO,EAAE,OAAO;QAChB,GAAG,gBAAgB,CAAC,IAAI,CAAC;KACzB,CAAA;IAED,OAAO;QACN,GAAG,EAAE,GAAG,CAAC,oBAAoB,CAAC,SAAS,CAAC;QACxC,GAAG,EAAE,GAAG,CAAC,kBAAkB,CAAC,SAAS,CAAC;KACtC,CAAA;AACF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CACnC,GAAgB,EAChB,IAAoB,EACpB,OAA0C,EAAE;IAE5C,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,oBAAoB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;IACpD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IAE9D,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;QACd,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACjD,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,IAAI,CAAC,CAAA;IACrC,CAAC;SAAM,CAAC;QACP,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAClB,CAAC;AACF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAI,WAAmB,EAAE,MAAS;IAC7D,OAAO;QACN,WAAW;QACX,OAAO,EAAE,EAAE,kBAAkB,EAAE,EAAE,MAAM,EAAE,EAAE;KAC3C,CAAA;AACF,CAAC"}
1
+ {"version":3,"file":"openapi.js","sourceRoot":"","sources":["../lib/openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AA0B/D;;GAEG;AACH,SAAS,gBAAgB,CAAC,IAAoB;IAC7C,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAA;IAE9G,OAAO;QACN,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE;QAChE,YAAY;QACZ,OAAO;QACP,IAAI;QACJ,QAAQ;KACR,CAAA;AACF,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAgB,EAAE,IAAoB,EAAE,IAAI,GAAG,eAAe;IAC/F,MAAM,MAAM,GAAwC,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAA;IAEnG,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;AACxB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAgB,EAAE,IAAoB;IAC1E,MAAM,SAAS,GAAuD;QACrE,OAAO,EAAE,OAAO;QAChB,GAAG,gBAAgB,CAAC,IAAI,CAAC;KACzB,CAAA;IAED,MAAM,SAAS,GAAqD;QACnE,OAAO,EAAE,OAAO;QAChB,GAAG,gBAAgB,CAAC,IAAI,CAAC;KACzB,CAAA;IAED,OAAO;QACN,GAAG,EAAE,GAAG,CAAC,oBAAoB,CAAC,SAAS,CAAC;QACxC,GAAG,EAAE,GAAG,CAAC,kBAAkB,CAAC,SAAS,CAAC;KACtC,CAAA;AACF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACzC,GAAgB,EAChB,IAAoB,EACpB,OAA0C,EAAE;IAE5C,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,oBAAoB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;IACpD,6GAA6G;IAC7G,2BAA2B;IAC3B,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAA;IAErE,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;QACd,MAAM,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;IACzC,CAAC;SAAM,CAAC;QACP,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC3B,CAAC;AACF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAI,WAAmB,EAAE,MAAS;IAC7D,OAAO;QACN,WAAW;QACX,OAAO,EAAE,EAAE,kBAAkB,EAAE,EAAE,MAAM,EAAE,EAAE;KAC3C,CAAA;AACF,CAAC"}
package/out/request.d.ts CHANGED
@@ -18,5 +18,10 @@ import type { Context } from "hono";
18
18
  * this shape rather than Hono's uniformly-array `queries()`. Built on a null-prototype object so a query key of
19
19
  * `__proto__` or `constructor` cannot reach `Object`'s prototype — these handlers take arbitrary internet input.
20
20
  */
21
+ /**
22
+ * A non-empty string query value, else `undefined`. An empty `?q=` is treated as absent, since every drop-in reads it
23
+ * that way.
24
+ */
25
+ export declare function asString(raw: unknown): string | undefined;
21
26
  export declare function legacyQuery(c: Context): Record<string, string | string[]>;
22
27
  //# sourceMappingURL=request.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAQzE"}
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../lib/request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC;;;;;;GAMG;AACH;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAEzD;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAQzE"}
package/out/request.js CHANGED
@@ -17,6 +17,13 @@
17
17
  * this shape rather than Hono's uniformly-array `queries()`. Built on a null-prototype object so a query key of
18
18
  * `__proto__` or `constructor` cannot reach `Object`'s prototype — these handlers take arbitrary internet input.
19
19
  */
20
+ /**
21
+ * A non-empty string query value, else `undefined`. An empty `?q=` is treated as absent, since every drop-in reads it
22
+ * that way.
23
+ */
24
+ export function asString(raw) {
25
+ return typeof raw === "string" && raw.length ? raw : undefined;
26
+ }
20
27
  export function legacyQuery(c) {
21
28
  const out = Object.create(null);
22
29
  for (const [key, values] of Object.entries(c.req.queries())) {
@@ -1 +1 @@
1
- {"version":3,"file":"request.js","sourceRoot":"","sources":["../request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,CAAU;IACrC,MAAM,GAAG,GAAsC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAElE,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;QAC7D,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,MAAM,CAAA;IACrD,CAAC;IAED,OAAO,GAAG,CAAA;AACX,CAAC"}
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../lib/request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH;;;;;;GAMG;AACH;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,GAAY;IACpC,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;AAC/D,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,CAAU;IACrC,MAAM,GAAG,GAAsC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAElE,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;QAC7D,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,MAAM,CAAA;IACrD,CAAC;IAED,OAAO,GAAG,CAAA;AACX,CAAC"}
package/out/serve.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * surface packages stay web-standard (they only export `fetch`-shaped apps) so an edge
8
8
  * deployment needs no changes to them.
9
9
  */
10
- import { serve } from "@hono/node-server";
10
+ import { serve, type ServerType } from "@hono/node-server";
11
11
  /**
12
12
  * A `fetch`-shaped request handler (what `OpenAPIHono.fetch` provides).
13
13
  */
@@ -24,11 +24,16 @@ export type ServeNodeOptions = Parameters<typeof serve>[0] & {
24
24
  address: string;
25
25
  }) => void;
26
26
  };
27
- export interface ServerHandle {
28
- close(): Promise<void>;
29
- }
30
27
  /**
31
- * Boot a node HTTP listener for a Hono app. Returns a handle whose `close()` resolves when the listener is down.
28
+ * The listener plus the port it bound, which `port: 0` callers need. Only `port` is added: `net.Server` already owns an
29
+ * `address()` method, and Node's cluster child calls it inside its own `listening` handler, so a value property of that
30
+ * name on the handle breaks every cluster worker at listen.
32
31
  */
33
- export declare function serveNode(options: ServeNodeOptions): ServerHandle;
32
+ export type ServerHandle = ServerType & AsyncDisposable & {
33
+ readonly port: number;
34
+ };
35
+ /**
36
+ * Boot a node HTTP listener for a Hono app. Returns an async-disposable handle once the listener is ready.
37
+ */
38
+ export declare function serveNode({ onListen, ...options }: ServeNodeOptions): Promise<ServerHandle>;
34
39
  //# sourceMappingURL=serve.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAEzC;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAE5F;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG;IAC5D;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CAC5D,CAAA;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACtB;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,CAWjE"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../lib/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAE1D;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAE5F;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG;IAC5D;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CAC5D,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GACpC,eAAe,GAAG;IACjB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACrB,CAAA;AAKF;;GAEG;AACH,wBAAgB,SAAS,CAAC,EAAE,QAA0B,EAAE,GAAG,OAAO,EAAE,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAiB7G"}
package/out/serve.js CHANGED
@@ -8,15 +8,24 @@
8
8
  * deployment needs no changes to them.
9
9
  */
10
10
  import { serve } from "@hono/node-server";
11
+ const defaultOnListen = ({ port, address }) => console.error(`[mailwoman] native /v1 API listening on http://${address}:${port}`);
11
12
  /**
12
- * Boot a node HTTP listener for a Hono app. Returns a handle whose `close()` resolves when the listener is down.
13
+ * Boot a node HTTP listener for a Hono app. Returns an async-disposable handle once the listener is ready.
13
14
  */
14
- export function serveNode(options) {
15
- const server = serve({ fetch: options.fetch, port: options.port, hostname: options.hostname }, (info) => options.onListen?.({ port: info.port, address: info.address }));
16
- return {
17
- close: () => new Promise((resolve, reject) => {
18
- server.close((error) => (error ? reject(error) : resolve()));
19
- }),
20
- };
15
+ export function serveNode({ onListen = defaultOnListen, ...options }) {
16
+ return new Promise((resolve, reject) => {
17
+ const server = serve(options, (info) => {
18
+ server.off("error", reject);
19
+ Object.defineProperty(server, "port", { value: info.port, writable: false });
20
+ try {
21
+ onListen(info);
22
+ resolve(server);
23
+ }
24
+ catch (error) {
25
+ void server[Symbol.asyncDispose]().then(() => reject(error), reject);
26
+ }
27
+ });
28
+ server.once("error", reject);
29
+ });
21
30
  }
22
31
  //# sourceMappingURL=serve.js.map
package/out/serve.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"serve.js","sourceRoot":"","sources":["../serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAqBzC;;GAEG;AACH,MAAM,UAAU,SAAS,CAAC,OAAyB;IAClD,MAAM,MAAM,GAAG,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CACvG,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAC9D,CAAA;IAED,OAAO;QACN,KAAK,EAAE,GAAG,EAAE,CACX,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAa,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;QACrE,CAAC,CAAC;KACH,CAAA;AACF,CAAC"}
1
+ {"version":3,"file":"serve.js","sourceRoot":"","sources":["../lib/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,EAAmB,MAAM,mBAAmB,CAAA;AA2B1D,MAAM,eAAe,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,EAAqC,EAAE,EAAE,CAChF,OAAO,CAAC,KAAK,CAAC,kDAAkD,OAAO,IAAI,IAAI,EAAE,CAAC,CAAA;AAEnF;;GAEG;AACH,MAAM,UAAU,SAAS,CAAC,EAAE,QAAQ,GAAG,eAAe,EAAE,GAAG,OAAO,EAAoB;IACrF,OAAO,IAAI,OAAO,CAAe,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACpD,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YACtC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;YAE3B,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAA;YAE5E,IAAI,CAAC;gBACJ,QAAQ,CAAC,IAAI,CAAC,CAAA;gBACd,OAAO,CAAC,MAAsB,CAAC,CAAA;YAChC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,KAAK,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAA;YACrE,CAAC;QACF,CAAC,CAAC,CAAA;QAEF,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IAC7B,CAAC,CAAC,CAAA;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mailwoman/api-kit",
3
- "version": "9.2.0",
3
+ "version": "9.3.0",
4
4
  "description": "API plumbing for Mailwoman's HTTP surfaces — Hono node serve wrapper, OpenAPI emit helpers, shared wire atoms. Plumbing only: domain schemas live with their routes.",
5
5
  "license": "AGPL-3.0-only OR LicenseRef-Commercial",
6
6
  "repository": {
@@ -25,14 +25,24 @@
25
25
  "!test/**"
26
26
  ],
27
27
  "type": "module",
28
+ "sideEffects": false,
29
+ "imports": {
30
+ "#*": {
31
+ "types": "./out/*.d.ts",
32
+ "node": "./out/*.js",
33
+ "default": "./out/*.js"
34
+ }
35
+ },
28
36
  "exports": {
29
37
  "./package.json": "./package.json",
30
38
  ".": {
31
39
  "types": "./out/index.d.ts",
40
+ "node": "./out/index.js",
32
41
  "default": "./out/index.js"
33
42
  },
34
43
  "./metrics": {
35
44
  "types": "./out/metrics.d.ts",
45
+ "node": "./out/metrics.js",
36
46
  "default": "./out/metrics.js"
37
47
  }
38
48
  },
@@ -42,18 +52,28 @@
42
52
  "./package.json": "./package.json",
43
53
  ".": {
44
54
  "types": "./out/index.d.ts",
55
+ "node": "./out/index.js",
45
56
  "default": "./out/index.js"
46
57
  },
47
58
  "./metrics": {
48
59
  "types": "./out/metrics.d.ts",
60
+ "node": "./out/metrics.js",
49
61
  "default": "./out/metrics.js"
50
62
  }
63
+ },
64
+ "imports": {
65
+ "#*": {
66
+ "types": "./out/*.d.ts",
67
+ "node": "./out/*.js",
68
+ "default": "./out/*.js"
69
+ }
51
70
  }
52
71
  },
53
72
  "dependencies": {
54
- "@hono/node-server": "^2.0.12",
55
- "@hono/zod-openapi": "^1.5.1",
56
- "hono": "^4.13.0",
57
- "zod": "^4.4.3"
73
+ "@hono/node-server": "^2.1.1",
74
+ "@hono/zod-openapi": "^1.6.3",
75
+ "@mailwoman/core": "9.3.0",
76
+ "hono": "^4.13.7",
77
+ "zod": "^4.5.4"
58
78
  }
59
79
  }
package/serve.ts DELETED
@@ -1,46 +0,0 @@
1
- /**
2
- * @copyright Sister Software
3
- * @license AGPL-3.0
4
- * @author Teffen Ellis, et al.
5
- *
6
- * Node serve wrapper over `@hono/node-server`. The one place the node listener is created —
7
- * surface packages stay web-standard (they only export `fetch`-shaped apps) so an edge
8
- * deployment needs no changes to them.
9
- */
10
-
11
- import { serve } from "@hono/node-server"
12
-
13
- /**
14
- * A `fetch`-shaped request handler (what `OpenAPIHono.fetch` provides).
15
- */
16
- export type FetchLike = (request: Request, ...args: never[]) => Response | Promise<Response>
17
-
18
- /**
19
- * Options for `serveNode()`. Extracted since Hono doesn't seem to export them.
20
- */
21
- export type ServeNodeOptions = Parameters<typeof serve>[0] & {
22
- /**
23
- * Called once the listener is bound — receives the actual port (useful with `port: 0`).
24
- */
25
- onListen?: (info: { port: number; address: string }) => void
26
- }
27
-
28
- export interface ServerHandle {
29
- close(): Promise<void>
30
- }
31
-
32
- /**
33
- * Boot a node HTTP listener for a Hono app. Returns a handle whose `close()` resolves when the listener is down.
34
- */
35
- export function serveNode(options: ServeNodeOptions): ServerHandle {
36
- const server = serve({ fetch: options.fetch, port: options.port, hostname: options.hostname }, (info) =>
37
- options.onListen?.({ port: info.port, address: info.address })
38
- )
39
-
40
- return {
41
- close: () =>
42
- new Promise<void>((resolve, reject) => {
43
- server.close((error?: Error) => (error ? reject(error) : resolve()))
44
- }),
45
- }
46
- }
File without changes
File without changes