@mailwoman/api-kit 9.1.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.
- package/README.md +1 -1
- package/lib/engine-stamp.ts +66 -0
- package/{error.ts → lib/error.ts} +16 -1
- package/{index.ts → lib/index.ts} +7 -6
- package/{metrics.ts → lib/metrics.ts} +12 -13
- package/{openapi.ts → lib/openapi.ts} +26 -15
- package/{request.ts → lib/request.ts} +8 -0
- package/lib/serve.ts +61 -0
- package/out/engine-stamp.d.ts +53 -0
- package/out/engine-stamp.d.ts.map +1 -0
- package/out/engine-stamp.js +55 -0
- package/out/engine-stamp.js.map +1 -0
- package/out/error.d.ts +15 -1
- package/out/error.d.ts.map +1 -1
- package/out/error.js +13 -1
- package/out/error.js.map +1 -1
- package/out/geo.d.ts.map +1 -1
- package/out/geo.js.map +1 -1
- package/out/index.d.ts +7 -6
- package/out/index.d.ts.map +1 -1
- package/out/index.js +7 -6
- package/out/index.js.map +1 -1
- package/out/metrics.d.ts.map +1 -1
- package/out/metrics.js +11 -14
- package/out/metrics.js.map +1 -1
- package/out/openapi.d.ts +4 -2
- package/out/openapi.d.ts.map +1 -1
- package/out/openapi.js +19 -12
- package/out/openapi.js.map +1 -1
- package/out/request.d.ts +5 -0
- package/out/request.d.ts.map +1 -1
- package/out/request.js +7 -0
- package/out/request.js.map +1 -1
- package/out/serve.d.ts +16 -10
- package/out/serve.d.ts.map +1 -1
- package/out/serve.js +17 -8
- package/out/serve.js.map +1 -1
- package/package.json +36 -7
- package/serve.ts +0 -46
- /package/{geo.ts → lib/geo.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @mailwoman/api-kit
|
|
2
2
|
|
|
3
|
-
Plumbing for [Mailwoman](https://mailwoman.
|
|
3
|
+
Plumbing for [Mailwoman](https://mailwoman.ai)'s HTTP surfaces — a node `serve` wrapper and
|
|
4
4
|
OpenAPI emit helpers shared by the drop-in packages ([`@mailwoman/libpostal`](../libpostal),
|
|
5
5
|
[`@mailwoman/photon`](../photon), [`@mailwoman/nominatim`](../nominatim)).
|
|
6
6
|
|
|
@@ -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
|
+
}
|
|
@@ -29,6 +29,21 @@ export const APIErrorSchema = z
|
|
|
29
29
|
* handler's return type against that specific route's declared per-status `responses` map. A flat-typed `status` param
|
|
30
30
|
* would widen every branch to "any content-carrying status", which no single declared response branch matches.
|
|
31
31
|
*/
|
|
32
|
-
export function
|
|
32
|
+
export function errorResponse<S extends ContentfulStatusCode>(c: Context, status: S, error: string, detail?: string) {
|
|
33
33
|
return c.json(detail === undefined ? { error } : { error, detail }, status)
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
const GEOCODER_UNAVAILABLE_DETAIL =
|
|
37
|
+
"install @mailwoman/neural + @mailwoman/resolver-wof-sqlite and provide gazetteer data (MAILWOMAN_WOF_DB / MAILWOMAN_CANDIDATE_DB)"
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The "engine method absent" 503, for the engine method the route actually needed.
|
|
41
|
+
*
|
|
42
|
+
* `subject` is a WIRE VALUE, not a label. `<subject> not available` is published verbatim in the HTTP API reference
|
|
43
|
+
* table and in the docker deploy guide, so a caller branching on it is doing what the docs told them to — and
|
|
44
|
+
* `/v1/resolve` answers `resolver`, not `geocoder`, because the method it found missing is `engine.resolveTree`. Rename
|
|
45
|
+
* this function freely; never the string it emits.
|
|
46
|
+
*/
|
|
47
|
+
export function geocoderUnavailableError(c: Context, subject: "geocoder" | "resolver" = "geocoder") {
|
|
48
|
+
return errorResponse(c, 503, `${subject} not available`, GEOCODER_UNAVAILABLE_DETAIL)
|
|
49
|
+
}
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* 2026-07-12 design spec's anti-meta guardrails).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
export * from "
|
|
13
|
-
export * from "
|
|
14
|
-
export * from "
|
|
15
|
-
export * from "
|
|
16
|
-
export * from "
|
|
17
|
-
export * from "
|
|
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
|
-
*
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
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(
|
|
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:
|
|
97
|
-
p90:
|
|
98
|
-
p99:
|
|
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,10 @@
|
|
|
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"
|
|
13
|
+
|
|
14
|
+
type OpenAPISecurityRequirements = Parameters<OpenAPIHono["getOpenAPI31Document"]>[0]["security"]
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* The document config stamped into emitted documents: `title`/`version`/`description`/`summary`/`license`/`contact`
|
|
@@ -32,7 +32,7 @@ export interface OpenAPIDocInfo {
|
|
|
32
32
|
variables?: Record<string, { default: string; description?: string }>
|
|
33
33
|
}>
|
|
34
34
|
tags?: Array<{ name: string; description?: string }>
|
|
35
|
-
security?:
|
|
35
|
+
security?: OpenAPISecurityRequirements
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
@@ -54,18 +54,28 @@ function toDocumentConfig(info: OpenAPIDocInfo) {
|
|
|
54
54
|
* Mount the OpenAPI 3.1 document endpoint on `app` (default `/openapi.json`).
|
|
55
55
|
*/
|
|
56
56
|
export function attachOpenAPIDocs(app: OpenAPIHono, info: OpenAPIDocInfo, path = "/openapi.json"): void {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
app.doc31(path,
|
|
57
|
+
const config: Parameters<OpenAPIHono["doc31"]>[1] = { openapi: "3.1.0", ...toDocumentConfig(info) }
|
|
58
|
+
|
|
59
|
+
app.doc31(path, config)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Emit both document flavors programmatically (build artifacts, parity tests, client generation).
|
|
64
64
|
*/
|
|
65
65
|
export function emitOpenAPIDocuments(app: OpenAPIHono, info: OpenAPIDocInfo): { v31: object; v30: object } {
|
|
66
|
+
const v31Config: Parameters<OpenAPIHono["getOpenAPI31Document"]>[0] = {
|
|
67
|
+
openapi: "3.1.0",
|
|
68
|
+
...toDocumentConfig(info),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const v30Config: Parameters<OpenAPIHono["getOpenAPIDocument"]>[0] = {
|
|
72
|
+
openapi: "3.0.3",
|
|
73
|
+
...toDocumentConfig(info),
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
return {
|
|
67
|
-
v31: app.getOpenAPI31Document(
|
|
68
|
-
v30: app.getOpenAPIDocument(
|
|
77
|
+
v31: app.getOpenAPI31Document(v31Config),
|
|
78
|
+
v30: app.getOpenAPIDocument(v30Config),
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
81
|
|
|
@@ -78,19 +88,20 @@ export function emitOpenAPIDocuments(app: OpenAPIHono, info: OpenAPIDocInfo): {
|
|
|
78
88
|
* gitignored, not-yet-existing `docs/static/openapi/`). One place owns this so the four emitters can't drift out of
|
|
79
89
|
* lockstep with each other.
|
|
80
90
|
*/
|
|
81
|
-
export function printOpenAPIDocument(
|
|
91
|
+
export async function printOpenAPIDocument(
|
|
82
92
|
app: OpenAPIHono,
|
|
83
93
|
info: OpenAPIDocInfo,
|
|
84
94
|
opts: { flavor?: string; out?: string } = {}
|
|
85
|
-
): void {
|
|
95
|
+
): Promise<void> {
|
|
86
96
|
const { v31, v30 } = emitOpenAPIDocuments(app, info)
|
|
87
|
-
|
|
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`
|
|
88
100
|
|
|
89
101
|
if (opts.out) {
|
|
90
|
-
|
|
91
|
-
writeFileSync(opts.out, `${json}\n`)
|
|
102
|
+
await writeLocalTextFile(json, opts.out)
|
|
92
103
|
} else {
|
|
93
|
-
|
|
104
|
+
process.stdout.write(json)
|
|
94
105
|
}
|
|
95
106
|
}
|
|
96
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"}
|
package/out/error.d.ts
CHANGED
|
@@ -24,10 +24,24 @@ export declare const APIErrorSchema: z.ZodObject<{
|
|
|
24
24
|
* handler's return type against that specific route's declared per-status `responses` map. A flat-typed `status` param
|
|
25
25
|
* would widen every branch to "any content-carrying status", which no single declared response branch matches.
|
|
26
26
|
*/
|
|
27
|
-
export declare function
|
|
27
|
+
export declare function errorResponse<S extends ContentfulStatusCode>(c: Context, status: S, error: string, detail?: string): Response & import("hono").TypedResponse<{
|
|
28
28
|
error: string;
|
|
29
29
|
} | {
|
|
30
30
|
error: string;
|
|
31
31
|
detail: string;
|
|
32
32
|
}, S, "json">;
|
|
33
|
+
/**
|
|
34
|
+
* The "engine method absent" 503, for the engine method the route actually needed.
|
|
35
|
+
*
|
|
36
|
+
* `subject` is a WIRE VALUE, not a label. `<subject> not available` is published verbatim in the HTTP API reference
|
|
37
|
+
* table and in the docker deploy guide, so a caller branching on it is doing what the docs told them to — and
|
|
38
|
+
* `/v1/resolve` answers `resolver`, not `geocoder`, because the method it found missing is `engine.resolveTree`. Rename
|
|
39
|
+
* this function freely; never the string it emits.
|
|
40
|
+
*/
|
|
41
|
+
export declare function geocoderUnavailableError(c: Context, subject?: "geocoder" | "resolver"): Response & import("hono").TypedResponse<{
|
|
42
|
+
error: string;
|
|
43
|
+
} | {
|
|
44
|
+
error: string;
|
|
45
|
+
detail: string;
|
|
46
|
+
}, 503, "json">;
|
|
33
47
|
//# sourceMappingURL=error.d.ts.map
|
package/out/error.d.ts.map
CHANGED
|
@@ -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,
|
|
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
CHANGED
|
@@ -24,7 +24,19 @@ export const APIErrorSchema = z
|
|
|
24
24
|
* handler's return type against that specific route's declared per-status `responses` map. A flat-typed `status` param
|
|
25
25
|
* would widen every branch to "any content-carrying status", which no single declared response branch matches.
|
|
26
26
|
*/
|
|
27
|
-
export function
|
|
27
|
+
export function errorResponse(c, status, error, detail) {
|
|
28
28
|
return c.json(detail === undefined ? { error } : { error, detail }, status);
|
|
29
29
|
}
|
|
30
|
+
const GEOCODER_UNAVAILABLE_DETAIL = "install @mailwoman/neural + @mailwoman/resolver-wof-sqlite and provide gazetteer data (MAILWOMAN_WOF_DB / MAILWOMAN_CANDIDATE_DB)";
|
|
31
|
+
/**
|
|
32
|
+
* The "engine method absent" 503, for the engine method the route actually needed.
|
|
33
|
+
*
|
|
34
|
+
* `subject` is a WIRE VALUE, not a label. `<subject> not available` is published verbatim in the HTTP API reference
|
|
35
|
+
* table and in the docker deploy guide, so a caller branching on it is doing what the docs told them to — and
|
|
36
|
+
* `/v1/resolve` answers `resolver`, not `geocoder`, because the method it found missing is `engine.resolveTree`. Rename
|
|
37
|
+
* this function freely; never the string it emits.
|
|
38
|
+
*/
|
|
39
|
+
export function geocoderUnavailableError(c, subject = "geocoder") {
|
|
40
|
+
return errorResponse(c, 503, `${subject} not available`, GEOCODER_UNAVAILABLE_DETAIL);
|
|
41
|
+
}
|
|
30
42
|
//# sourceMappingURL=error.js.map
|
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,
|
|
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 "
|
|
12
|
-
export * from "
|
|
13
|
-
export * from "
|
|
14
|
-
export * from "
|
|
15
|
-
export * from "
|
|
16
|
-
export * from "
|
|
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
|
package/out/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,
|
|
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 "
|
|
12
|
-
export * from "
|
|
13
|
-
export * from "
|
|
14
|
-
export * from "
|
|
15
|
-
export * from "
|
|
16
|
-
export * from "
|
|
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,
|
|
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"}
|
package/out/metrics.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
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
|
-
*
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return Math.round(
|
|
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:
|
|
75
|
-
p90:
|
|
76
|
-
p99:
|
|
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,
|
package/out/metrics.js.map
CHANGED
|
@@ -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
|
|
1
|
+
{"version":3,"file":"metrics.js","sourceRoot":"","sources":["../lib/metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAExD;;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;;;GAGG;AACH,SAAS,iBAAiB,CAAC,MAAyB,EAAE,CAAS;IAC9D,MAAM,KAAK,GAAG,gBAAgB,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;IAEzC,IAAI,KAAK,KAAK,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;IAEpF,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,CAAA;AACrC,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,iBAAiB,CAAC,MAAM,EAAE,EAAE,CAAC;oBAClC,GAAG,EAAE,iBAAiB,CAAC,MAAM,EAAE,EAAE,CAAC;oBAClC,GAAG,EAAE,iBAAiB,CAAC,MAAM,EAAE,EAAE,CAAC;oBAClC,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"}
|
package/out/openapi.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* (progenitor), replacing the old hand-downgrade step.
|
|
9
9
|
*/
|
|
10
10
|
import type { OpenAPIHono } from "@hono/zod-openapi";
|
|
11
|
+
type OpenAPISecurityRequirements = Parameters<OpenAPIHono["getOpenAPI31Document"]>[0]["security"];
|
|
11
12
|
/**
|
|
12
13
|
* The document config stamped into emitted documents: `title`/`version`/`description`/`summary`/`license`/`contact`
|
|
13
14
|
* land under the document's `info` block; `externalDocs`/`servers`/`tags`/`security` are top-level document fields. All
|
|
@@ -42,7 +43,7 @@ export interface OpenAPIDocInfo {
|
|
|
42
43
|
name: string;
|
|
43
44
|
description?: string;
|
|
44
45
|
}>;
|
|
45
|
-
security?:
|
|
46
|
+
security?: OpenAPISecurityRequirements;
|
|
46
47
|
}
|
|
47
48
|
/**
|
|
48
49
|
* Mount the OpenAPI 3.1 document endpoint on `app` (default `/openapi.json`).
|
|
@@ -67,7 +68,7 @@ export declare function emitOpenAPIDocuments(app: OpenAPIHono, info: OpenAPIDocI
|
|
|
67
68
|
export declare function printOpenAPIDocument(app: OpenAPIHono, info: OpenAPIDocInfo, opts?: {
|
|
68
69
|
flavor?: string;
|
|
69
70
|
out?: string;
|
|
70
|
-
}): void
|
|
71
|
+
}): Promise<void>;
|
|
71
72
|
/**
|
|
72
73
|
* An OpenAPI error-response descriptor: a description plus a JSON body of `schema`.
|
|
73
74
|
*
|
|
@@ -83,4 +84,5 @@ export declare function errorContent<S>(description: string, schema: S): {
|
|
|
83
84
|
};
|
|
84
85
|
};
|
|
85
86
|
};
|
|
87
|
+
export {};
|
|
86
88
|
//# sourceMappingURL=openapi.d.ts.map
|
package/out/openapi.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
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 {
|
|
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
|
*/
|
|
@@ -26,17 +25,24 @@ function toDocumentConfig(info) {
|
|
|
26
25
|
* Mount the OpenAPI 3.1 document endpoint on `app` (default `/openapi.json`).
|
|
27
26
|
*/
|
|
28
27
|
export function attachOpenAPIDocs(app, info, path = "/openapi.json") {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
app.doc31(path, { openapi: "3.1.0", ...toDocumentConfig(info) });
|
|
28
|
+
const config = { openapi: "3.1.0", ...toDocumentConfig(info) };
|
|
29
|
+
app.doc31(path, config);
|
|
32
30
|
}
|
|
33
31
|
/**
|
|
34
32
|
* Emit both document flavors programmatically (build artifacts, parity tests, client generation).
|
|
35
33
|
*/
|
|
36
34
|
export function emitOpenAPIDocuments(app, info) {
|
|
35
|
+
const v31Config = {
|
|
36
|
+
openapi: "3.1.0",
|
|
37
|
+
...toDocumentConfig(info),
|
|
38
|
+
};
|
|
39
|
+
const v30Config = {
|
|
40
|
+
openapi: "3.0.3",
|
|
41
|
+
...toDocumentConfig(info),
|
|
42
|
+
};
|
|
37
43
|
return {
|
|
38
|
-
v31: app.getOpenAPI31Document(
|
|
39
|
-
v30: app.getOpenAPIDocument(
|
|
44
|
+
v31: app.getOpenAPI31Document(v31Config),
|
|
45
|
+
v30: app.getOpenAPIDocument(v30Config),
|
|
40
46
|
};
|
|
41
47
|
}
|
|
42
48
|
/**
|
|
@@ -48,15 +54,16 @@ export function emitOpenAPIDocuments(app, info) {
|
|
|
48
54
|
* gitignored, not-yet-existing `docs/static/openapi/`). One place owns this so the four emitters can't drift out of
|
|
49
55
|
* lockstep with each other.
|
|
50
56
|
*/
|
|
51
|
-
export function printOpenAPIDocument(app, info, opts = {}) {
|
|
57
|
+
export async function printOpenAPIDocument(app, info, opts = {}) {
|
|
52
58
|
const { v31, v30 } = emitOpenAPIDocuments(app, info);
|
|
53
|
-
|
|
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`;
|
|
54
62
|
if (opts.out) {
|
|
55
|
-
|
|
56
|
-
writeFileSync(opts.out, `${json}\n`);
|
|
63
|
+
await writeLocalTextFile(json, opts.out);
|
|
57
64
|
}
|
|
58
65
|
else {
|
|
59
|
-
|
|
66
|
+
process.stdout.write(json);
|
|
60
67
|
}
|
|
61
68
|
}
|
|
62
69
|
/**
|
package/out/openapi.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.js","sourceRoot":"","sources":["../openapi.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
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
|
package/out/request.d.ts.map
CHANGED
|
@@ -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())) {
|
package/out/request.js.map
CHANGED
|
@@ -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,14 +7,15 @@
|
|
|
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, type ServerType } from "@hono/node-server";
|
|
10
11
|
/**
|
|
11
12
|
* A `fetch`-shaped request handler (what `OpenAPIHono.fetch` provides).
|
|
12
13
|
*/
|
|
13
14
|
export type FetchLike = (request: Request, ...args: never[]) => Response | Promise<Response>;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Options for `serveNode()`. Extracted since Hono doesn't seem to export them.
|
|
17
|
+
*/
|
|
18
|
+
export type ServeNodeOptions = Parameters<typeof serve>[0] & {
|
|
18
19
|
/**
|
|
19
20
|
* Called once the listener is bound — receives the actual port (useful with `port: 0`).
|
|
20
21
|
*/
|
|
@@ -22,12 +23,17 @@ export interface ServeNodeOptions {
|
|
|
22
23
|
port: number;
|
|
23
24
|
address: string;
|
|
24
25
|
}) => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
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.
|
|
31
|
+
*/
|
|
32
|
+
export type ServerHandle = ServerType & AsyncDisposable & {
|
|
33
|
+
readonly port: number;
|
|
34
|
+
};
|
|
29
35
|
/**
|
|
30
|
-
* Boot a node HTTP listener for a Hono app. Returns
|
|
36
|
+
* Boot a node HTTP listener for a Hono app. Returns an async-disposable handle once the listener is ready.
|
|
31
37
|
*/
|
|
32
|
-
export declare function serveNode(options: ServeNodeOptions): ServerHandle
|
|
38
|
+
export declare function serveNode({ onListen, ...options }: ServeNodeOptions): Promise<ServerHandle>;
|
|
33
39
|
//# sourceMappingURL=serve.d.ts.map
|
package/out/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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,
|
|
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,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mailwoman/api-kit",
|
|
3
|
-
"version": "9.
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/sister-software/mailwoman.git",
|
|
9
|
-
"directory": "api-kit"
|
|
9
|
+
"directory": "packages/api-kit"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"out/**/*.js",
|
|
@@ -21,14 +21,29 @@
|
|
|
21
21
|
"!*.test.ts",
|
|
22
22
|
"!*.test.tsx",
|
|
23
23
|
"!**/*.test.ts",
|
|
24
|
-
"!**/*.test.tsx"
|
|
24
|
+
"!**/*.test.tsx",
|
|
25
|
+
"!test/**"
|
|
25
26
|
],
|
|
26
27
|
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"imports": {
|
|
30
|
+
"#*": {
|
|
31
|
+
"types": "./out/*.d.ts",
|
|
32
|
+
"node": "./out/*.js",
|
|
33
|
+
"default": "./out/*.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
27
36
|
"exports": {
|
|
28
37
|
"./package.json": "./package.json",
|
|
29
38
|
".": {
|
|
30
39
|
"types": "./out/index.d.ts",
|
|
40
|
+
"node": "./out/index.js",
|
|
31
41
|
"default": "./out/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./metrics": {
|
|
44
|
+
"types": "./out/metrics.d.ts",
|
|
45
|
+
"node": "./out/metrics.js",
|
|
46
|
+
"default": "./out/metrics.js"
|
|
32
47
|
}
|
|
33
48
|
},
|
|
34
49
|
"publishConfig": {
|
|
@@ -37,14 +52,28 @@
|
|
|
37
52
|
"./package.json": "./package.json",
|
|
38
53
|
".": {
|
|
39
54
|
"types": "./out/index.d.ts",
|
|
55
|
+
"node": "./out/index.js",
|
|
40
56
|
"default": "./out/index.js"
|
|
57
|
+
},
|
|
58
|
+
"./metrics": {
|
|
59
|
+
"types": "./out/metrics.d.ts",
|
|
60
|
+
"node": "./out/metrics.js",
|
|
61
|
+
"default": "./out/metrics.js"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"imports": {
|
|
65
|
+
"#*": {
|
|
66
|
+
"types": "./out/*.d.ts",
|
|
67
|
+
"node": "./out/*.js",
|
|
68
|
+
"default": "./out/*.js"
|
|
41
69
|
}
|
|
42
70
|
}
|
|
43
71
|
},
|
|
44
72
|
"dependencies": {
|
|
45
|
-
"@hono/node-server": "^2.
|
|
46
|
-
"@hono/zod-openapi": "^1.
|
|
47
|
-
"
|
|
48
|
-
"
|
|
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"
|
|
49
78
|
}
|
|
50
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
|
-
export interface ServeNodeOptions {
|
|
19
|
-
fetch: FetchLike
|
|
20
|
-
port: number
|
|
21
|
-
hostname: string
|
|
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 as never, 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
|
-
}
|
/package/{geo.ts → lib/geo.ts}
RENAMED
|
File without changes
|