@mailwoman/api 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/{app.ts → lib/app.ts} +39 -11
- package/{engine.ts → lib/engine.ts} +24 -12
- package/{index.ts → lib/index.ts} +4 -4
- package/{routes.ts → lib/routes.ts} +78 -54
- package/lib/schema.ts +502 -0
- package/out/app.d.ts +10 -2
- package/out/app.d.ts.map +1 -1
- package/out/app.js +21 -9
- package/out/app.js.map +1 -1
- package/out/engine.d.ts +19 -23
- package/out/engine.d.ts.map +1 -1
- package/out/engine.js.map +1 -1
- package/out/index.d.ts +4 -4
- package/out/index.d.ts.map +1 -1
- package/out/index.js +4 -4
- package/out/index.js.map +1 -1
- package/out/routes.d.ts +9 -3
- package/out/routes.d.ts.map +1 -1
- package/out/routes.js +47 -50
- package/out/routes.js.map +1 -1
- package/out/schema.d.ts +616 -9
- package/out/schema.d.ts.map +1 -1
- package/out/schema.js +177 -21
- package/out/schema.js.map +1 -1
- package/package.json +38 -9
- package/schema.ts +0 -328
package/lib/schema.ts
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Zod wire schemas for the native `/v1` surface. Unlike the drop-ins (photon, nominatim,
|
|
7
|
+
* libpostal), nothing here is a vendor contract — this surface is ours to design, so request
|
|
8
|
+
* bodies are REQUIRED and validator-enforced (no legacy tolerance to preserve). A `defaultHook`
|
|
9
|
+
* on the app maps validation failures through the shared `APIErrorSchema` envelope
|
|
10
|
+
* (`errorResponse(c, 400, "invalid request body", <zod summary>)`) — the pattern boundary every
|
|
11
|
+
* surface holds to: where no legacy contract exists, the validator MAY speak, but only in
|
|
12
|
+
* our envelope.
|
|
13
|
+
*
|
|
14
|
+
* `APIErrorSchema` itself is owned by `@mailwoman/api-kit` (plumbing shared by every native
|
|
15
|
+
* surface) — it's re-exported here so route modules can import every schema they need, request
|
|
16
|
+
* and error alike, from this one file.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { z } from "@hono/zod-openapi"
|
|
20
|
+
import type { AddressNode } from "@mailwoman/core/decoder"
|
|
21
|
+
import type { DerivationProjection, Evidence } from "@mailwoman/evidence"
|
|
22
|
+
|
|
23
|
+
export { APIErrorSchema } from "@mailwoman/api-kit"
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `POST /v1/parse` request body.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* One node of the decoded address tree. The decoder's `AddressNode` is a recursive union the OpenAPI generator cannot
|
|
30
|
+
* derive a schema for on its own, so it is registered as an open object; the shape is documented by the type.
|
|
31
|
+
*/
|
|
32
|
+
export const AddressNodeSchema = z.custom<AddressNode>().openapi("AddressNode", {
|
|
33
|
+
type: "object",
|
|
34
|
+
additionalProperties: true,
|
|
35
|
+
description:
|
|
36
|
+
"A decoded address-tree node: a tag, its span, and its children. See `AddressNode` in `@mailwoman/core/decoder`.",
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The input register (Decision A / GTM B10): `fragmented` = the map-search register (evidence-bundle channels feed);
|
|
41
|
+
* `formatted` = the validation/record register (channels off). Unset → the engine derives it from the input's shape.
|
|
42
|
+
* `/v1/batch` defaults to `formatted` (batch rows are the record register by nature).
|
|
43
|
+
*/
|
|
44
|
+
export const InputModeSchema = z.enum(["fragmented", "formatted"]).openapi("InputMode")
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Longest accepted `address`, in characters.
|
|
48
|
+
*
|
|
49
|
+
* Sized against what the model can actually read, not against a guess at abuse. The classifier's window is 128
|
|
50
|
+
* SentencePiece pieces — roughly 330 characters of address text — and everything past it is truncated before inference,
|
|
51
|
+
* so input beyond this bound cannot influence a result. The margin over that window leaves room for scripts that
|
|
52
|
+
* tokenize denser than Latin, and for the department-and-division prefixes web forms concatenate.
|
|
53
|
+
*
|
|
54
|
+
* The bound exists because preprocessing is linear but not free: a 1 MB body costs ~1.7 s across normalize, query-shape
|
|
55
|
+
* and the phrase grouper, and Node runs them on the one thread every other request is waiting on. A cap here is cheaper
|
|
56
|
+
* than fairness plumbing, and rejecting is more honest than accepting a body whose tail the parser will silently
|
|
57
|
+
* discard.
|
|
58
|
+
*/
|
|
59
|
+
export const MAX_ADDRESS_LENGTH = 1024
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `POST /v1/parse` request body.
|
|
63
|
+
*/
|
|
64
|
+
export const ParseRequestSchema = z
|
|
65
|
+
.object({
|
|
66
|
+
address: z.string().max(MAX_ADDRESS_LENGTH),
|
|
67
|
+
debug: z.boolean().optional(),
|
|
68
|
+
input_mode: InputModeSchema.optional(),
|
|
69
|
+
})
|
|
70
|
+
.openapi("ParseRequest")
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* One `ParseOutcome.components` entry — mirrors {@linkcode ParseComponent} (`engine.ts`).
|
|
74
|
+
*/
|
|
75
|
+
export const ParseComponentSchema = z.object({ tag: z.string(), value: z.string() }).openapi("ParseComponent")
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* `POST /v1/parse` response — mirrors {@linkcode ParseOutcome} (`engine.ts`): the ordered components plus the full
|
|
79
|
+
* decoded tree. `tree` is the same loose-tree idiom {@link ResolveResponseSchema} uses (`api/schema.ts:134-146`) — the
|
|
80
|
+
* decoder's `AddressTree` is the engine's contract, not this wire schema's.
|
|
81
|
+
*/
|
|
82
|
+
export const ParseOutcomeSchema = z
|
|
83
|
+
.object({
|
|
84
|
+
input: z.string(),
|
|
85
|
+
components: z.array(ParseComponentSchema),
|
|
86
|
+
tree: z.looseObject({ raw: z.string(), roots: z.array(AddressNodeSchema) }),
|
|
87
|
+
debug: z.string().optional(),
|
|
88
|
+
})
|
|
89
|
+
.openapi("ParseOutcome")
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* `POST /v1/geocode` request body.
|
|
93
|
+
*/
|
|
94
|
+
export const GeocodeRequestSchema = z
|
|
95
|
+
.object({
|
|
96
|
+
address: z.string().max(MAX_ADDRESS_LENGTH),
|
|
97
|
+
input_mode: InputModeSchema.optional(),
|
|
98
|
+
})
|
|
99
|
+
.openapi("GeocodeRequest")
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* One `GeocodeOutcome.hierarchy` entry — locality → country, most specific first. `name` is the resolved gazetteer name
|
|
103
|
+
* (proper-cased canonical); `value` is the raw parsed span. Mirrors `GeocodeResult["hierarchy"]` entries
|
|
104
|
+
* (`mailwoman/geocode-core.ts`), hand-modeled — see {@link GeocodeOutcomeSchema} for the no-import rationale.
|
|
105
|
+
*/
|
|
106
|
+
const GeocodeHierarchyEntrySchema = z
|
|
107
|
+
.object({
|
|
108
|
+
tag: z.string(),
|
|
109
|
+
value: z.string(),
|
|
110
|
+
name: z.string(),
|
|
111
|
+
lat: z.number().optional(),
|
|
112
|
+
lon: z.number().optional(),
|
|
113
|
+
placeID: z.string().optional(),
|
|
114
|
+
// #1731 tri-state lineage provenance: true = the winner's ancestor chain vouches for this entry, false =
|
|
115
|
+
// resolved independently OUTSIDE the winner's lineage, absent = unverifiable. Absence is not false.
|
|
116
|
+
in_winner_lineage: z.boolean().optional(),
|
|
117
|
+
})
|
|
118
|
+
.openapi("GeocodeHierarchyEntry")
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* One `GeocodeOutcome.candidates` entry — a ranked alternative place for the query's primary result (the winning place
|
|
122
|
+
* first, then same-query runner-ups). Mirrors `GeocodeResult["candidates"]` entries.
|
|
123
|
+
*/
|
|
124
|
+
const GeocodeCandidateSchema = z
|
|
125
|
+
.object({
|
|
126
|
+
name: z.string(),
|
|
127
|
+
tag: z.string(),
|
|
128
|
+
lat: z.number(),
|
|
129
|
+
lon: z.number(),
|
|
130
|
+
countryCode: z.string().nullable(),
|
|
131
|
+
placeID: z.string().optional(),
|
|
132
|
+
})
|
|
133
|
+
.openapi("GeocodeCandidate")
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The `ComponentTag` union at this engine-agnostic boundary, named once so every schema that speaks about a tag speaks
|
|
137
|
+
* about the SAME list. Two hand-copied enums would agree on the day they were written and diverge on the day a tag is
|
|
138
|
+
* added — the shape of defect `feedback-parity-needs-shared-function-not-shared-constants` describes.
|
|
139
|
+
*/
|
|
140
|
+
const ComponentTagSchema = z.enum([
|
|
141
|
+
"country",
|
|
142
|
+
"region",
|
|
143
|
+
"locality",
|
|
144
|
+
"dependent_locality",
|
|
145
|
+
"postcode",
|
|
146
|
+
"subregion",
|
|
147
|
+
"house_number",
|
|
148
|
+
"street",
|
|
149
|
+
"street_prefix",
|
|
150
|
+
"street_prefix_particle",
|
|
151
|
+
"street_suffix",
|
|
152
|
+
"intersection_a",
|
|
153
|
+
"intersection_b",
|
|
154
|
+
"unit",
|
|
155
|
+
"venue",
|
|
156
|
+
"attention",
|
|
157
|
+
"po_box",
|
|
158
|
+
"cedex",
|
|
159
|
+
"prefecture",
|
|
160
|
+
"municipality",
|
|
161
|
+
"district",
|
|
162
|
+
"block",
|
|
163
|
+
"sub_block",
|
|
164
|
+
"building_number",
|
|
165
|
+
"building_name",
|
|
166
|
+
"locality_unit",
|
|
167
|
+
])
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Canonical parsed-component map carried by `GeocodeResult.components`. Spelled out at this engine-agnostic API
|
|
171
|
+
* boundary for the same reason the result schema is hand-modeled; the compile-time drift pin in
|
|
172
|
+
* `mailwoman/test/api-schema-drift.test.ts` catches any mismatch with the real `ComponentTag`-keyed result type.
|
|
173
|
+
*/
|
|
174
|
+
const GeocodeComponentsSchema = z.partialRecord(ComponentTagSchema, z.string())
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* One `GeocodeOutcome.intent_markers` entry — an advisory the ROAD_TO_V9 §4 intent vocabulary raised about the QUERY.
|
|
178
|
+
* Mirrors `QueryIntentMarker` (`core/pipeline/types.ts`).
|
|
179
|
+
*
|
|
180
|
+
* `evidence` is deliberately open (`z.record`): each `code` carries its own measurement — a dominance margin, a pair of
|
|
181
|
+
* interpretations, a taxonomy id — and flattening those into one closed shape would either lose the numbers or invent
|
|
182
|
+
* fields that do not apply. `code` is the discriminator a client branches on.
|
|
183
|
+
*/
|
|
184
|
+
const QueryIntentMarkerSchema = z
|
|
185
|
+
.object({
|
|
186
|
+
// Spelled out rather than `z.string()` so `mailwoman/test/api-schema-drift.test.ts`'s schema-too-wide direction
|
|
187
|
+
// keeps biting: a new `QueryKind` that never reaches this list is a documented contract that has quietly stopped
|
|
188
|
+
// describing the real one.
|
|
189
|
+
kind: z.enum([
|
|
190
|
+
"postcode_only",
|
|
191
|
+
"locality_only",
|
|
192
|
+
"structured_address",
|
|
193
|
+
"intersection",
|
|
194
|
+
"po_box",
|
|
195
|
+
"landmark",
|
|
196
|
+
"poi_query",
|
|
197
|
+
"vague",
|
|
198
|
+
"bare_toponym",
|
|
199
|
+
"route_pair",
|
|
200
|
+
"near_me",
|
|
201
|
+
"poi_category",
|
|
202
|
+
]),
|
|
203
|
+
code: z.enum([
|
|
204
|
+
"declared_ambiguity",
|
|
205
|
+
"declared_fork",
|
|
206
|
+
"focus_point_required",
|
|
207
|
+
"poi_category",
|
|
208
|
+
"coverage_qualified_absence",
|
|
209
|
+
"authority_designation",
|
|
210
|
+
]),
|
|
211
|
+
mechanism: z.string(),
|
|
212
|
+
message: z.string(),
|
|
213
|
+
evidence: z.record(z.string(), z.unknown()).optional(),
|
|
214
|
+
})
|
|
215
|
+
.openapi("QueryIntentMarker")
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* One authoritative-provider match on the wire (#1901) — hoisted so the outcome schema below stays inside the
|
|
219
|
+
* call-nesting bound. Field-for-field mirror of `mailwoman/authoritative.ts`'s `AuthoritativeAssertionMatch`.
|
|
220
|
+
*/
|
|
221
|
+
const AuthoritativeMatchSchema = z.object({
|
|
222
|
+
provider_place_id: z.string(),
|
|
223
|
+
object_ids: z.record(z.string(), z.string()).optional(),
|
|
224
|
+
canonical_fields: z.record(z.string(), z.string()).optional(),
|
|
225
|
+
lat: z.number().optional(),
|
|
226
|
+
lon: z.number().optional(),
|
|
227
|
+
precision: z.string().optional(),
|
|
228
|
+
match_status: z.enum(["exact", "approximate"]),
|
|
229
|
+
provider_score: z.number().optional(),
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const EpistemicStatusSchema = z.enum(["designated", "observed", "derived", "inferred", "unresolved"])
|
|
233
|
+
|
|
234
|
+
const CoverageBasisSchema = z.enum(["designated", "surveyed", "source_present"])
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* `@mailwoman/evidence`'s `Evidence` union, spelled for the wire. The `EvidencePin` below fails to compile the moment
|
|
238
|
+
* either side gains, loses or retypes a field.
|
|
239
|
+
*/
|
|
240
|
+
const EvidenceSchema = z.discriminatedUnion("kind", [
|
|
241
|
+
z.object({ kind: z.literal("observation"), source: z.string(), vintage: z.string().nullable(), value: z.unknown() }),
|
|
242
|
+
z.object({
|
|
243
|
+
kind: z.literal("exclusion"),
|
|
244
|
+
source: z.string(),
|
|
245
|
+
vintage: z.string(),
|
|
246
|
+
scope: z.object({ layer: z.string(), h3Cell: z.number(), basis: CoverageBasisSchema, fold: z.string() }),
|
|
247
|
+
}),
|
|
248
|
+
z.object({
|
|
249
|
+
kind: z.literal("relation"),
|
|
250
|
+
source: z.string(),
|
|
251
|
+
vintage: z.string(),
|
|
252
|
+
relationship: z.string(),
|
|
253
|
+
assertion: z.enum(["authoritative", "inferred"]),
|
|
254
|
+
score: z.number().optional(),
|
|
255
|
+
}),
|
|
256
|
+
z.object({ kind: z.literal("prior"), source: z.string(), label: z.string(), weight: z.number() }),
|
|
257
|
+
])
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The derivation behind a geocode answer, present only when the engine was asked to trace — `@mailwoman/evidence`'s
|
|
261
|
+
* `DerivationProjection` on the wire.
|
|
262
|
+
*/
|
|
263
|
+
export const DerivationProjectionSchema = z.object({
|
|
264
|
+
status: EpistemicStatusSchema,
|
|
265
|
+
constraints: z.array(z.object({ label: z.string(), evidence: EvidenceSchema, contribution: z.string() })).readonly(),
|
|
266
|
+
uncertaintyM: z.number().nullable(),
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// Both directions: the schema's inferred type is exactly the evidence package's, or this does not compile.
|
|
270
|
+
type Mutual<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false
|
|
271
|
+
const evidencePin: Mutual<z.infer<typeof EvidenceSchema>, Evidence> = true
|
|
272
|
+
const derivationPin: Mutual<z.infer<typeof DerivationProjectionSchema>, DerivationProjection> = true
|
|
273
|
+
|
|
274
|
+
void evidencePin
|
|
275
|
+
void derivationPin
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* `POST /v1/geocode` response — a hand-modeled mirror of `GeocodeResult`'s wire shape (`mailwoman/geocode-core.ts`),
|
|
279
|
+
* `.loose()` so a field the engine adds that this schema doesn't yet know about still rides through undocumented rather
|
|
280
|
+
* than being stripped or rejected. DOC-ACCURACY ONLY: the route passes `engine.geocode()`'s outcome through verbatim
|
|
281
|
+
* (`GeocodeOutcome = Record<string, unknown>`, `api/engine.ts`) — nothing here validates a real response, so a
|
|
282
|
+
* schema/engine mismatch can never reject or mutate a result at runtime. Deliberately carries NO import from
|
|
283
|
+
* `mailwoman` (the engine-agnosticism boundary — `mailwoman` is the one workspace allowed to depend on
|
|
284
|
+
* `@mailwoman/api`, never the reverse). `mailwoman/test/api-schema-drift.test.ts` is the compile-time regression check
|
|
285
|
+
* that catches this shape drifting from the real `GeocodeResult` interface.
|
|
286
|
+
*/
|
|
287
|
+
export const GeocodeOutcomeLikeSchema = z.object({
|
|
288
|
+
input: z.string(),
|
|
289
|
+
components: GeocodeComponentsSchema,
|
|
290
|
+
lat: z.number().nullable(),
|
|
291
|
+
lon: z.number().nullable(),
|
|
292
|
+
resolution_tier: z.enum(["address_point", "interpolated", "street", "admin", "venue", "plus_code"]),
|
|
293
|
+
// What the evidence permits a consumer to claim about the coordinate, orthogonal to how it was produced; see
|
|
294
|
+
// `@mailwoman/evidence`'s `EpistemicStatus`.
|
|
295
|
+
epistemic_status: z.enum(["designated", "observed", "derived", "inferred", "unresolved"]),
|
|
296
|
+
// The derivation behind the answer, present only when the engine was asked to trace. `DerivationProjectionSchema` is
|
|
297
|
+
// pinned to `@mailwoman/evidence`'s types below, so the wire contract and the evidence union cannot drift apart.
|
|
298
|
+
derivation: DerivationProjectionSchema.optional(),
|
|
299
|
+
// The fork→entity probe's answer (#1585) — present only on the `venue` tier; see geocode-core's
|
|
300
|
+
// GeocodeResult.entity.
|
|
301
|
+
entity: z
|
|
302
|
+
.object({
|
|
303
|
+
name: z.string(),
|
|
304
|
+
categoryID: z.string().nullable(),
|
|
305
|
+
confidence: z.number(),
|
|
306
|
+
country: z.string(),
|
|
307
|
+
})
|
|
308
|
+
.optional(),
|
|
309
|
+
uncertainty_m: z.number().nullable(),
|
|
310
|
+
locality: z.string().nullable(),
|
|
311
|
+
region: z.string().nullable(),
|
|
312
|
+
postcode: z.string().nullable(),
|
|
313
|
+
house_number: z.string().nullable(),
|
|
314
|
+
street: z.string().nullable(),
|
|
315
|
+
// The parsed venue span (#1041 posture; surfaced 2026-08-01 for the hierarchy-evidence campaign R1).
|
|
316
|
+
venue: z.string().nullable(),
|
|
317
|
+
// The parsed dependent-locality span (parse view; `hierarchy` is the resolved view).
|
|
318
|
+
dependent_locality: z.string().nullable(),
|
|
319
|
+
// The parsed unit / sub-venue span (parse view) — "Terminal 5", "Suite 300".
|
|
320
|
+
unit: z.string().nullable(),
|
|
321
|
+
countryCode: z.string().nullable(),
|
|
322
|
+
hierarchy: z.array(GeocodeHierarchyEntrySchema),
|
|
323
|
+
candidates: z.array(GeocodeCandidateSchema),
|
|
324
|
+
// The register row's OWN scope tags when the address_point tier answered and its extract carries
|
|
325
|
+
// them (normalized locality key + postcode of the ROOFTOP) — see geocode-core's GeocodeResult.rooftop.
|
|
326
|
+
rooftop: z
|
|
327
|
+
.object({
|
|
328
|
+
localityNorm: z.string().optional(),
|
|
329
|
+
postcode: z.string().optional(),
|
|
330
|
+
})
|
|
331
|
+
.optional(),
|
|
332
|
+
// #42: the country the postcode-country coherence pass scoped the walk to, or null. Non-null ONLY when it
|
|
333
|
+
// OVERRODE the request's country prior — so a caller who asked for US and got an FR answer can see which
|
|
334
|
+
// evidence bought the change instead of reading it as a bug.
|
|
335
|
+
postcode_country_scope: z.string().nullable(),
|
|
336
|
+
// #1880: the capital promotion's firing receipt — the promoted candidate's country, present only when the
|
|
337
|
+
// promotion changed some node's leading candidate. Advisory, same posture as postcode_country_scope.
|
|
338
|
+
capital_promotion: z.string().optional(),
|
|
339
|
+
// #1893: the variant-alias exemption's firing receipt — present (true) only when the winning candidate reached
|
|
340
|
+
// the top because the exemption spared it the cross-country alias penalty. Advisory, same posture again.
|
|
341
|
+
variant_alias_exemption: z.literal(true).optional(),
|
|
342
|
+
// ROAD_TO_V9 §4: query-intent advisories. Always present; empty means the vocabulary looked and had nothing to
|
|
343
|
+
// say. Advisory ONLY — no marker changed which answer won, and a client is free to ignore the array entirely.
|
|
344
|
+
intent_markers: z.array(QueryIntentMarkerSchema),
|
|
345
|
+
// #1717 stage 1: flag-only admin-coherence verdicts — did the winning candidate's resolved ancestry confirm,
|
|
346
|
+
// contradict, or fail to speak to the PARSED region/country qualifiers? Nothing ranks or filters on these; present
|
|
347
|
+
// whenever a winner resolved (both members always populated — `unstated` is the explicit no-qualifier claim),
|
|
348
|
+
// absent when nothing resolved to check against. See mailwoman's `admin-coherence.ts` for the verdict contract.
|
|
349
|
+
admin_coherence: z
|
|
350
|
+
.object({
|
|
351
|
+
region: z.enum(["confirmed", "contradicted", "unstated", "unverifiable"]),
|
|
352
|
+
country: z.enum(["confirmed", "contradicted", "unstated", "unverifiable"]),
|
|
353
|
+
})
|
|
354
|
+
.optional(),
|
|
355
|
+
// #1901: a configured authoritative provider's answer, carried BESIDE the open result — every value inside is
|
|
356
|
+
// the PROVIDER'S assertion, hand-modeled here to match `mailwoman/authoritative.ts`'s wire shape (the
|
|
357
|
+
// engine-agnosticism boundary forbids importing it). Absent when no provider is configured; `refused` is the
|
|
358
|
+
// provider declining (distinct from a parse failure or a gazetteer miss); `transport_error` is the provider
|
|
359
|
+
// being unreachable, reported rather than silently dropped. An `ambiguous` status carries EVERY candidate.
|
|
360
|
+
authoritative: z
|
|
361
|
+
.object({
|
|
362
|
+
provider: z.string(),
|
|
363
|
+
status: z.enum(["matched", "ambiguous", "refused", "transport_error"]),
|
|
364
|
+
matches: z.array(AuthoritativeMatchSchema).optional(),
|
|
365
|
+
attribution: z.string().optional(),
|
|
366
|
+
license: z.string().optional(),
|
|
367
|
+
retrieved_at: z.string().optional(),
|
|
368
|
+
dataset_version: z.string().optional(),
|
|
369
|
+
error: z.string().optional(),
|
|
370
|
+
})
|
|
371
|
+
.optional(),
|
|
372
|
+
// #1755: spans the flat `components` map could not represent. `components` holds one value per tag, so a second
|
|
373
|
+
// `locality` span ceases to exist there — and without this line `region: null` means both "the input named no
|
|
374
|
+
// region" and "it named one and we deleted it". Absent when nothing was dropped; never an empty array on the wire,
|
|
375
|
+
// because the common case is nothing dropped and a client should not have to read a field to learn that.
|
|
376
|
+
dropped_components: z
|
|
377
|
+
.array(
|
|
378
|
+
z.object({
|
|
379
|
+
tag: ComponentTagSchema,
|
|
380
|
+
value: z.string(),
|
|
381
|
+
// The value that held the slot, so a reader sees which of the two survived without re-deriving it.
|
|
382
|
+
kept: z.string(),
|
|
383
|
+
})
|
|
384
|
+
)
|
|
385
|
+
.optional(),
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
export type GeocodeOutcomeLike = z.infer<typeof GeocodeOutcomeLikeSchema>
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* `POST /v1/geocode` response — a hand-modeled mirror of `GeocodeResult`'s wire shape (`mailwoman/geocode-core.ts`),
|
|
392
|
+
* `.loose()` so a field the engine adds that this schema doesn't yet know about still rides through undocumented rather
|
|
393
|
+
* than being stripped or rejected. DOC-ACCURACY ONLY: the route passes `engine.geocode()`'s outcome through verbatim
|
|
394
|
+
* (`GeocodeOutcome = Record<string, unknown>`, `api/engine.ts`) — nothing here validates a real response, so a
|
|
395
|
+
* schema/engine mismatch can never reject or mutate a result at runtime. Deliberately carries NO import from
|
|
396
|
+
* `mailwoman` (the engine-agnosticism boundary — `mailwoman` is the one workspace allowed to depend on
|
|
397
|
+
* `@mailwoman/api`, never the reverse). `mailwoman/test/api-schema-drift.test.ts` is the compile-time regression check
|
|
398
|
+
* that catches this shape drifting from the real `GeocodeResult` interface.
|
|
399
|
+
*/
|
|
400
|
+
export const GeocodeOutcomeSchema = GeocodeOutcomeLikeSchema.loose().openapi("GeocodeOutcome")
|
|
401
|
+
|
|
402
|
+
export type GeocodeOutcome = z.infer<typeof GeocodeOutcomeSchema>
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* `POST /v1/batch` request body.
|
|
406
|
+
*/
|
|
407
|
+
export const BatchRequestSchema = z
|
|
408
|
+
.object({
|
|
409
|
+
// Per-ROW, not just per-request: the row cap (`batchMax`, default 500) bounds how many addresses arrive,
|
|
410
|
+
// and this bounds how large each may be. Without both, one request is 500 unbounded bodies.
|
|
411
|
+
addresses: z.array(z.string().max(MAX_ADDRESS_LENGTH)),
|
|
412
|
+
/**
|
|
413
|
+
* Register override for every row. DEFAULT `"formatted"` — batch rows are the record register by nature.
|
|
414
|
+
*/
|
|
415
|
+
input_mode: InputModeSchema.optional(),
|
|
416
|
+
})
|
|
417
|
+
.openapi("BatchRequest")
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The failure slot for one batch row. A row that throws does not fail its neighbours.
|
|
421
|
+
*/
|
|
422
|
+
const BatchRowErrorSchema = z.object({ input: z.string(), error: z.string() })
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* One batch row: the geocode outcome, or the failure slot that stands in for it.
|
|
426
|
+
*/
|
|
427
|
+
const BatchRowSchema = z.union([GeocodeOutcomeSchema, BatchRowErrorSchema])
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* `POST /v1/batch` response — one `GeocodeOutcome`, or an `{ input, error }` slot, per row (per-row isolation).
|
|
431
|
+
*/
|
|
432
|
+
export const BatchResponseSchema = z
|
|
433
|
+
.object({
|
|
434
|
+
results: z.array(BatchRowSchema),
|
|
435
|
+
})
|
|
436
|
+
.openapi("BatchResponse")
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* `POST /v1/resolve` request body — an already-decoded `AddressTree` (the parser's output) to resolve against the
|
|
440
|
+
* gazetteer.
|
|
441
|
+
*/
|
|
442
|
+
export const ResolveRequestSchema = z
|
|
443
|
+
.object({
|
|
444
|
+
tree: z.looseObject({ raw: z.string(), roots: z.array(AddressNodeSchema) }),
|
|
445
|
+
opts: z.looseObject({}).optional(),
|
|
446
|
+
})
|
|
447
|
+
.openapi("ResolveRequest")
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* `POST /v1/resolve` response — the same tree, decorated in place with gazetteer coords + attribution.
|
|
451
|
+
*/
|
|
452
|
+
export const ResolveResponseSchema = z
|
|
453
|
+
.object({
|
|
454
|
+
tree: z.looseObject({ raw: z.string(), roots: z.array(AddressNodeSchema) }),
|
|
455
|
+
})
|
|
456
|
+
.openapi("ResolveResponse")
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* One component's value. Repeatable tags (a street with two names, say) arrive as an array; the caller joins them
|
|
460
|
+
* before handing the dict to `formatAddress`, which takes single strings only.
|
|
461
|
+
*/
|
|
462
|
+
const ComponentValueSchema = z.union([z.string(), z.array(z.string())])
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* `POST /v1/format` request body. `components` accepts `string | string[]` per key on the wire — a handler-side
|
|
466
|
+
* concern, not this schema's: `@mailwoman/formatter`'s `ComponentDict` (`format.ts`) is `Partial<Record<ComponentTag,
|
|
467
|
+
* string>>`, single-string only, so a route handler must join array values before calling
|
|
468
|
+
* `formatAddress`/`canonicalKey`.
|
|
469
|
+
*/
|
|
470
|
+
export const FormatRequestSchema = z
|
|
471
|
+
.object({
|
|
472
|
+
components: z.record(z.string(), ComponentValueSchema),
|
|
473
|
+
country: z.string(),
|
|
474
|
+
options: z.looseObject({}).optional(),
|
|
475
|
+
})
|
|
476
|
+
.openapi("FormatRequest")
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* `POST /v1/format` response — the rendered string plus the deterministic canonical match key.
|
|
480
|
+
*/
|
|
481
|
+
export const FormatResponseSchema = z
|
|
482
|
+
.object({
|
|
483
|
+
formatted: z.string(),
|
|
484
|
+
canonicalKey: z.string(),
|
|
485
|
+
})
|
|
486
|
+
.openapi("FormatResponse")
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* `GET /health` response — `status`/`uptime_s` are stamped by the ROUTE itself, unconditionally, regardless of engine
|
|
490
|
+
* (`api/routes.ts`'s `healthRoute` handler: `{ status: "ok", uptime_s, ...engine.health?.() }`), so those two are
|
|
491
|
+
* cheap
|
|
492
|
+
*
|
|
493
|
+
* - Accurate to pin. Everything else is `HealthData` (`api/engine.ts`) — an engine-defined block (model card, data-root
|
|
494
|
+
* inventory for `mailwoman serve`; something else entirely for another engine) — stays loose.
|
|
495
|
+
*/
|
|
496
|
+
export const HealthResponseSchema = z
|
|
497
|
+
.object({
|
|
498
|
+
status: z.literal("ok"),
|
|
499
|
+
uptime_s: z.number(),
|
|
500
|
+
})
|
|
501
|
+
.loose()
|
|
502
|
+
.openapi("HealthResponse")
|
package/out/app.d.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
11
11
|
import { type OpenAPIDocInfo } from "@mailwoman/api-kit";
|
|
12
|
-
import type {
|
|
12
|
+
import type { EngineStamp } from "@mailwoman/core/license";
|
|
13
|
+
import type { MailwomanAPIEngine } from "#engine";
|
|
14
|
+
import type { GeocodeOutcomeLike } from "#schema";
|
|
13
15
|
/**
|
|
14
16
|
* Options for {@link createMailwomanAPI}.
|
|
15
17
|
*/
|
|
@@ -29,6 +31,12 @@ export interface MailwomanAPIOptions {
|
|
|
29
31
|
* Max `addresses` rows accepted by `POST /v1/batch`. Default 500 (see `routes.ts`'s `DEFAULT_BATCH_MAX`).
|
|
30
32
|
*/
|
|
31
33
|
batchMax?: number;
|
|
34
|
+
/**
|
|
35
|
+
* The engine stamp to carry on every response: `engine` in each `/v1` body and the `Server` + `Link: rel="license"`
|
|
36
|
+
* headers everywhere. Absent when an embedding application builds the app without the `mailwoman` package; the
|
|
37
|
+
* `mailwoman serve` command always passes one.
|
|
38
|
+
*/
|
|
39
|
+
engine?: EngineStamp;
|
|
32
40
|
}
|
|
33
41
|
/**
|
|
34
42
|
* The document info stamped into the emitted OpenAPI document. Exported (not inlined) so the `mailwoman openapi`
|
|
@@ -39,5 +47,5 @@ export declare const MAILWOMAN_API_DOC_INFO: OpenAPIDocInfo;
|
|
|
39
47
|
/**
|
|
40
48
|
* Build the native Mailwoman app around an injected {@link MailwomanAPIEngine}.
|
|
41
49
|
*/
|
|
42
|
-
export declare function createMailwomanAPI(engine: MailwomanAPIEngine
|
|
50
|
+
export declare function createMailwomanAPI<T extends Partial<GeocodeOutcomeLike> = GeocodeOutcomeLike>(engine: MailwomanAPIEngine<T>, options?: MailwomanAPIOptions): OpenAPIHono;
|
|
43
51
|
//# sourceMappingURL=app.d.ts.map
|
package/out/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../app.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../lib/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAmD,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAEzG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAK1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAEjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAejD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IAEd;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IAEvB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;CACpB;AAUD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,cAoBpC,CAAA;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,OAAO,CAAC,kBAAkB,CAAC,GAAG,kBAAkB,EAC5F,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC,EAC7B,OAAO,GAAE,mBAAwB,GAC/B,WAAW,CAyDb"}
|
package/out/app.js
CHANGED
|
@@ -8,11 +8,17 @@
|
|
|
8
8
|
* CLI wires the real parse/geocode/resolve stack (phase 4b); tests inject fixtures.
|
|
9
9
|
*/
|
|
10
10
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
11
|
+
import { attachOpenAPIDocs, engineHeaders, errorResponse } from "@mailwoman/api-kit";
|
|
12
|
+
import { readLocalJSONFile } from "@mailwoman/core/fs/readers";
|
|
13
|
+
import { resolvePackagePath } from "@mailwoman/core/module/resolvers";
|
|
13
14
|
import { bodyLimit } from "hono/body-limit";
|
|
14
15
|
import { cors } from "hono/cors";
|
|
15
|
-
import { DEFAULT_BATCH_MAX, registerMailwomanAPIRoutes } from "
|
|
16
|
+
import { DEFAULT_BATCH_MAX, registerMailwomanAPIRoutes } from "#routes";
|
|
17
|
+
/**
|
|
18
|
+
* This package's own manifest, read at load rather than imported as a module: a JSON import makes `tsc` copy the file
|
|
19
|
+
* into `out/`, where it becomes the package scope for the compiled tree and breaks every `#` import in it.
|
|
20
|
+
*/
|
|
21
|
+
const packageJson = await readLocalJSONFile(resolvePackagePath("@mailwoman/api", "package.json"));
|
|
16
22
|
/**
|
|
17
23
|
* 2 MiB — carried from the express server's `express.json({ limit: "2mb" })` (`mailwoman/server/index.ts`).
|
|
18
24
|
*/
|
|
@@ -34,7 +40,7 @@ export const MAILWOMAN_API_DOC_INFO = {
|
|
|
34
40
|
version: packageJson.version,
|
|
35
41
|
description: packageJson.description,
|
|
36
42
|
license: { name: "AGPL-3.0-only OR LicenseRef-Commercial", identifier: "AGPL-3.0-only" },
|
|
37
|
-
contact: { name: "Sister Software", url: "https://mailwoman.
|
|
43
|
+
contact: { name: "Sister Software", url: "https://mailwoman.ai" },
|
|
38
44
|
servers: [
|
|
39
45
|
{
|
|
40
46
|
url: "http://{host}:{port}",
|
|
@@ -62,7 +68,7 @@ export function createMailwomanAPI(engine, options = {}) {
|
|
|
62
68
|
// just `/v1/format`).
|
|
63
69
|
defaultHook: (result, c) => {
|
|
64
70
|
if (!result.success) {
|
|
65
|
-
return
|
|
71
|
+
return errorResponse(c, 400, "invalid request body", summarizeValidationError(result.error));
|
|
66
72
|
}
|
|
67
73
|
return undefined;
|
|
68
74
|
},
|
|
@@ -72,6 +78,9 @@ export function createMailwomanAPI(engine, options = {}) {
|
|
|
72
78
|
if (options.cors !== false) {
|
|
73
79
|
app.use(cors({ origin: "*", allowMethods: ["GET", "POST", "OPTIONS"], allowHeaders: ["*"], maxAge: 86_400 }));
|
|
74
80
|
}
|
|
81
|
+
if (options.engine) {
|
|
82
|
+
app.use(engineHeaders(options.engine));
|
|
83
|
+
}
|
|
75
84
|
// Safety net: an engine fault answers the native envelope, never a crash. `detail` carries the raw message —
|
|
76
85
|
// this surface is ours to design, so (unlike the vendor-constrained drop-in envelopes) we can be helpful.
|
|
77
86
|
app.onError((error, c) => {
|
|
@@ -79,17 +88,20 @@ export function createMailwomanAPI(engine, options = {}) {
|
|
|
79
88
|
// validator throws before a route's own hook ever sees the body, so it lands here instead of the
|
|
80
89
|
// per-route 400s in routes.ts. Answer 400, not the 500 net (which stays reserved for engine faults).
|
|
81
90
|
if (error instanceof Error && error.message.includes("Malformed JSON")) {
|
|
82
|
-
return
|
|
91
|
+
return errorResponse(c, 400, "invalid request body", "malformed JSON");
|
|
83
92
|
}
|
|
84
|
-
return
|
|
93
|
+
return errorResponse(c, 500, "internal error", error instanceof Error ? error.message : String(error));
|
|
85
94
|
});
|
|
86
95
|
// Ahead of the handlers (which buffer the body into memory) so an oversized POST is rejected before that
|
|
87
96
|
// buffering happens, not after — mirrors the libpostal precedent.
|
|
88
97
|
app.use("/v1/*", bodyLimit({
|
|
89
98
|
maxSize: options.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES,
|
|
90
|
-
onError: (c) =>
|
|
99
|
+
onError: (c) => errorResponse(c, 413, "request body too large"),
|
|
91
100
|
}));
|
|
92
|
-
registerMailwomanAPIRoutes(app, engine, {
|
|
101
|
+
registerMailwomanAPIRoutes(app, engine, {
|
|
102
|
+
batchMax: options.batchMax ?? DEFAULT_BATCH_MAX,
|
|
103
|
+
engine: options.engine,
|
|
104
|
+
});
|
|
93
105
|
attachOpenAPIDocs(app, MAILWOMAN_API_DOC_INFO);
|
|
94
106
|
return app;
|
|
95
107
|
}
|
package/out/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","sourceRoot":"","sources":["../app.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../lib/app.ts"],"names":[],"mappings":"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"}
|