@crvouga/mockingbird-service-google-maps 0.2.0 → 1.1.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/CHANGELOG.md CHANGED
@@ -1,6 +1,18 @@
1
1
  # Changelog — @crvouga/mockingbird-service-google-maps
2
2
 
3
- ## 0.2.0 (2026-09-24)
3
+ ## 1.1.0 (2026-09-25)
4
+
5
+ ### Features
6
+
7
+ - add the Address Validation API with USPS CASS/DPV verdicts ([7cec81e](https://github.com/crvouga/mockingbird/commit/7cec81e1d8271e4d55d6310f36324e47e135d510))
8
+
9
+ ## 1.0.0 (2026-09-24)
10
+
11
+ ### ⚠️ Breaking changes
12
+
13
+ - vendor branding for every service; purge internal mocks and consumer names ([f689534](https://github.com/crvouga/mockingbird/commit/f6895343543550dafcb7d76b426cae7635e03226))
14
+
15
+ ## 0.2.0 (2026-09-23)
4
16
 
5
17
  ### Features
6
18
 
package/README.md CHANGED
@@ -2,14 +2,18 @@
2
2
 
3
3
  Mock of the **Google Maps Platform** surface our member app uses for addresses: Places
4
4
  Autocomplete, Place Details and Find Place From Text (the JSON web services), the Geocoding API,
5
- and a **Maps JavaScript API shim** (`/maps/api/js?libraries=places`) exposing
6
- `google.maps.places.*` and `google.maps.Geocoder` over the same data. Answers come from a corpus
5
+ a **Maps JavaScript API shim** (`/maps/api/js?libraries=places`) exposing
6
+ `google.maps.places.*` and `google.maps.Geocoder` over the same data, and the **Address
7
+ Validation API** (`POST /v1:validateAddress`, with USPS CASS/DPV data) for server-side ship-to
8
+ checks. Answers come from a corpus
7
9
  matching our QA fixtures, so the address step that waits 5 s for Google predictions (and then
8
10
  falls back to manual entry) resolves instantly and deterministically.
9
11
 
10
12
  - Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/google-maps/SUPPORT.md)
11
13
  - Google publishes no OpenAPI document for these endpoints: `openapi.yaml` is hand-authored
12
- from Google's documented shapes and the fields our consumer reads.
14
+ from Google's documented shapes and the fields our consumer reads. Address Validation follows
15
+ the [REST reference](https://developers.google.com/maps/documentation/address-validation/reference/rest/v1/TopLevel/validateAddress)
16
+ and [`ValidationResult`](https://developers.google.com/maps/documentation/address-validation/reference/rest/v1/ValidationResult).
13
17
 
14
18
  ## Install
15
19
 
@@ -53,6 +57,39 @@ const details = await get(
53
57
  maps.applyPreset("autocomplete_over_query_limit", "default", { count: 2 })
54
58
  ```
55
59
 
60
+ ### Address Validation
61
+
62
+ Google serves it from a different host, `https://addressvalidation.googleapis.com`; the paths do
63
+ not collide, so one mock serves both. Point the server-side client's Address Validation base URL
64
+ at the mock (or at `<mock>/ns/<namespace>`), with the key as `?key=` (or `X-Goog-Api-Key`).
65
+
66
+ ```ts
67
+ import { createRuntime } from "@crvouga/mockingbird-service-google-maps"
68
+
69
+ const maps = createRuntime()
70
+ const response = await maps.fetch(
71
+ new Request("http://maps.test/v1:validateAddress?key=k", {
72
+ method: "POST",
73
+ headers: { "content-type": "application/json" },
74
+ body: JSON.stringify({
75
+ address: {
76
+ regionCode: "US",
77
+ addressLines: ["1625 N Central Ave"],
78
+ locality: "Phoenix",
79
+ administrativeArea: "AZ",
80
+ postalCode: "85004",
81
+ },
82
+ enableUspsCass: true,
83
+ }),
84
+ }),
85
+ )
86
+ const { result } = await response.json()
87
+ // result.verdict → PREMISE / addressComplete / ACCEPT; result.uspsData.dpvConfirmation → "Y"
88
+
89
+ // Force the USPS "not deliverable" answer on the next call.
90
+ maps.applyPreset("address_validation_dpv_n", "default", { count: 1 })
91
+ ```
92
+
56
93
  On web, load `<mock>/maps/api/js?key=…&libraries=places` exactly as the app loads Google's
57
94
  script; the shim calls back into the mock's REST endpoints on the same origin and namespace.
58
95
 
@@ -69,8 +106,36 @@ works from a browser.
69
106
  | `GET /maps/api/place/details/json` | `place_id` (required), `fields` (honoured: `address_component` returns only `address_components`; unknown field → `INVALID_REQUEST`), `sessiontoken`. `result` with `address_components` (`street_number`, `route`, `locality`, `administrative_area_level_2`, `administrative_area_level_1` long/short, `country`, `postal_code`), `formatted_address`, `geometry{location, viewport}`, `place_id`, `types`, `name`, `url`, `vicinity`. Unknown id → `NOT_FOUND`. |
70
107
  | `GET /maps/api/geocode/json` | `address`, `place_id` or `components=postal_code:…` (none → `INVALID_REQUEST`). `results[0]` with `address_components`, `formatted_address`, `geometry{location, location_type, viewport}`, `place_id`, `types`. A full address (`"line1, city, ST zip"`) resolves to its row or a synthesized address; a bare ZIP or `"City, ST"` to the ZIP centroid; a street without a locatable city is `ZERO_RESULTS`. |
71
108
  | `GET /maps/api/place/findplacefromtext/json` | `input` + `inputtype=textquery` (else `INVALID_REQUEST`), `fields` (default: `place_id` only, as Google). Geocoding first, then the looser autocomplete match, so it finds what our geocode fallback needs. |
109
+ | `POST /v1:validateAddress` | Address Validation (see below). A Google Cloud API: errors are HTTP 4xx/5xx with `{"error": {code, message, status}}`, not a 200 with `status`. |
72
110
  | `GET /maps/api/js` | The shim (`text/javascript`): `google.maps.places.AutocompleteService#getPlacePredictions`, `PlacesService#getDetails` / `#findPlaceFromQuery`, `AutocompleteSessionToken`, `PlacesServiceStatus`, `google.maps.Geocoder#geocode`, `GeocoderStatus`, `LatLng` (`lat()`/`lng()`), `LatLngBounds`, `importLibrary`. Callbacks and promises (rejecting with `MapsRequestError` on errors when no callback is given). Calls `window[callback]` for `&callback=`, and `window.gm_authFailure()` when the key is refused. |
73
111
 
112
+ ### Address Validation verdicts
113
+
114
+ Answers are proto3 JSON, as Google sends them: `false` booleans and empty lists are omitted
115
+ (no `unconfirmedComponentTypes: []`). `result` carries `verdict` (`inputGranularity`,
116
+ `validationGranularity`, `geocodeGranularity`, `addressComplete`, `has*Components`,
117
+ `possibleNextAction`), `address` (`formattedAddress`, `postalAddress`, `addressComponents` with
118
+ `confirmationLevel` / `inferred` / `replaced`, `missingComponentTypes`,
119
+ `unconfirmedComponentTypes`), `geocode` (`location`, `placeId`, `placeTypes`) and, for US
120
+ addresses, `uspsData` (`standardizedAddress`, `dpvConfirmation`, `dpvFootnote`,
121
+ `postOfficeCity`/`State`, `errorMessage`, `cassProcessed` when `enableUspsCass`). Every answer
122
+ has a `responseId`. Send a componentized address (line 1 + optional unit line 2, `locality`,
123
+ `administrativeArea`, `postalCode`) or everything in `addressLines`.
124
+
125
+ | Input | Verdict |
126
+ | --- | --- |
127
+ | A corpus row (`1625 N Central Ave`, Phoenix AZ 85004); spelled-out words (`North`, `Avenue`) match | `PREMISE`, complete, `ACCEPT`, DPV `Y`; `postalAddress` echoes the corpus spelling with a deterministic ZIP+4 (`85004-NNNN`) |
128
+ | `<number> <street>` in a corpus city | Synthesized, as for Autocomplete: `PREMISE`, `ACCEPT`, DPV `Y`, the street USPS-abbreviated (`Maple Street` → `Maple St`) |
129
+ | A unit on a single-delivery-point row (`… Building A`, `Apt 4`) | `PREMISE`, complete, `CONFIRM`, `subpremise` unconfirmed, DPV `Y` |
130
+ | No house number (`N Central Ave`) in a corpus city | `ROUTE`, `FIX`, `missingComponentTypes: ["street_number"]`, no DPV code |
131
+ | A city/ZIP the corpus does not hold | `OTHER`, `FIX`, the unplaceable components unconfirmed, DPV `N` |
132
+ | A ZIP that is wrong for the city (or a city that is wrong for the ZIP) | Replaced: `hasReplacedComponents`, `CONFIRM`, the corrected value in `postalAddress` |
133
+ | A row with `validation.multiUnit` | No unit: `CONFIRM_ADD_SUBPREMISES`, `missingComponentTypes: ["subpremise"]`, DPV `D`. A unit in `validation.units` (or any, when `units` is absent): `SUB_PREMISE`, `ACCEPT`, `Y`. Another unit: `PREMISE`, `CONFIRM`, `subpremise` unconfirmed, DPV `S` |
134
+ | A row with `validation.uspsErrorMessage` | `uspsData: {errorMessage}` and no DPV code |
135
+ | No `address`, empty `addressLines`, an unknown field, non-JSON, over 280 characters, or `enableUspsCass` outside `US`/`PR` | 400 `INVALID_ARGUMENT` |
136
+ | No key | 403 `PERMISSION_DENIED` "The request is missing a valid API key." |
137
+ | A key outside `settings.keys` | 400 `INVALID_ARGUMENT` "API key not valid. Please pass a valid API key." (`reason: API_KEY_INVALID`), as Google answers an unknown key; `address_validation_denied` gives the 403 of a key without the API enabled |
138
+
74
139
  ### Corpus
75
140
 
76
141
  59 addresses: every row of QA's `ROUTING_ZIP_CORPUS` (`packages/qa/src/world/gen/addresses.ts`,
@@ -86,7 +151,7 @@ Details needs no stored state). `GET /health` reports the corpus.
86
151
  | Route | Effect |
87
152
  | --- | --- |
88
153
  | `GET /__admin/corpus` | The namespace's addresses (custom first) and the custom count. |
89
- | `PUT /__admin/corpus` | `{addresses: [{line1, city, state, zip, id?, county?, lat?, lng?}]}` replaces the namespace's custom addresses (on top of the built-in corpus). |
154
+ | `PUT /__admin/corpus` | `{addresses: [{line1, city, state, zip, id?, county?, lat?, lng?, validation?}]}` replaces the namespace's custom addresses (on top of the built-in corpus). `validation` pins Address Validation for that row: `{granularity?, addressComplete?, possibleNextAction?, dpvConfirmation?: "Y"\|"N"\|"S"\|"D", unconfirmedComponentTypes?, uspsErrorMessage?, multiUnit?, units?}`. |
90
155
  | `DELETE /__admin/corpus` | Drop the custom addresses. |
91
156
  | `GET/PUT /__admin/settings` | `{keys?: string[], publicUrl?: string \| null}`. `keys` restricts accepted API keys (others get `REQUEST_DENIED` "The provided API key is invalid."; a missing key always does). `publicUrl` is the origin the JS shim calls back to when it differs from the request's (a rewriting proxy). |
92
157
 
@@ -95,15 +160,22 @@ Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n}`; `GET /_
95
160
  with that status), `geocode_zero_results` (exercises our Find Place fallback),
96
161
  `autocomplete_over_query_limit` (two of these flip our sheet to manual entry), `server_error`
97
162
  (HTTP 500), `slow` (6 s, past QA's 5 s wait), `script_unavailable` (the JS loader answers 503, so
98
- the script's `onerror` fires).
163
+ the script's `onerror` fires). For Address Validation only: `address_validation_denied` (403
164
+ `PERMISSION_DENIED`, a key without the API enabled), `address_validation_unavailable` (503
165
+ `UNAVAILABLE`), `address_validation_slow` (3 s, past a 2.5 s checkout timeout),
166
+ `address_validation_no_verdict` (200 `{"result": {}}`: not something Google sends, for a
167
+ client's defensive branch) and `address_validation_dpv_n` (DPV `N`). `GET /__admin/metrics`
168
+ counts `ValidateAddress` on its own.
99
169
 
100
170
  ### Namespaces
101
171
 
102
172
  `x-mockingbird-namespace`, a `/ns/<name>` prefix on the base URL (the JS shim served under a
103
173
  prefix calls back through it), or by API key:
104
174
  `PUT /__admin/credentials {"credentials": {"<PLACES_KEY>": "<namespace>"}}` (the `key` query
105
- parameter is the credential). The request journal records operation, status, the resolved
106
- `placeId` and the `sessionToken`; never the typed address.
175
+ parameter, or `X-Goog-Api-Key` for Address Validation, is the credential). The request journal
176
+ records operation, status, the resolved `placeId` and the `sessionToken`, and for
177
+ `ValidateAddress` the resolved corpus row id (`addressRowId`) and the verdict class
178
+ (`ACCEPT/Y`, `FIX/N`, …); never the typed address.
107
179
 
108
180
  ### Tests
109
181
 
@@ -115,10 +187,17 @@ parameter is the credential). The request journal records operation, status, the
115
187
  manual-entry switch), `parse-place-details.ts`, `address-autocomplete-web.tsx` and
116
188
  `use-geocoded-address.ts` (native REST and web through the shim evaluated in a fake window).
117
189
  Also served over HTTP.
190
+ - `google-maps.address-validation.acceptance.test.ts`: every Address Validation verdict class,
191
+ the 400/403 envelopes, namespaces, the journal and each preset, through `test/consumer.ts`'s
192
+ port of a checkout's fail-open ship-to evaluation (non-2xx, no verdict → unavailable; USPS
193
+ error, granularity, incomplete, `FIX`/`CONFIRM_ADD_SUBPREMISES`, critical unconfirmed
194
+ components, DPV other than `Y`/`S` → reject). Self-parity walks include `ValidateAddress`.
118
195
  - There is no SDK drop-in test: native uses plain `fetch`, and the web SDK is Google's hosted
119
196
  script, which the shim replaces.
120
- - `bun scripts/parity.ts`: live parity against `maps.googleapis.com` with
121
- `MOCKINGBIRD_GOOGLE_MAPS_API_KEY` (env / `.env.local`); exits 2 without it.
197
+ - `bun scripts/parity.ts`: live parity against `maps.googleapis.com` (and
198
+ `addressvalidation.googleapis.com` for `ValidateAddress`) with
199
+ `MOCKINGBIRD_GOOGLE_MAPS_API_KEY` (env / `.env.local`; Address Validation must be enabled on
200
+ the key); exits 2 without it.
122
201
 
123
202
  ### Deliberately not modelled
124
203
 
@@ -130,6 +209,13 @@ parameter is the credential). The request journal records operation, status, the
130
209
  (`places.googleapis.com`, `Place` class, `AutocompleteSuggestion`), reverse geocoding
131
210
  (`latlng=`), `locationbias`/`radius`, and billing/quotas beyond the presets.
132
211
  - Session-token billing semantics: tokens are accepted and journaled, nothing more.
212
+ - Address Validation beyond `validateAddress`: `provideValidationFeedback`, `previousResponseId`
213
+ chaining, `languageOptions` and `sessionToken` (accepted, ignored), `metadata`
214
+ (business/residential/PO box), `englishLatinAddress`, and non-US regions (without
215
+ `enableUspsCass` they get a `FIX` answer and no `uspsData`; Google validates many countries).
216
+ ZIP+4 digits, `dpvFootnote` codes and the rest of the USPS record (carrier route, delivery
217
+ point, county FIPS…) are synthesized or omitted: compare verdict classes and DPV codes, not
218
+ those.
133
219
 
134
220
  ## API
135
221
 
@@ -139,12 +225,13 @@ parameter is the credential). The request journal records operation, status, the
139
225
  | `createRuntime` | function | The mock with the full service contract (health, admin, namespaces, credentials, presets, journal). Options: `corpus`, `settings`, `clock`, `seed`, `adminKey`, `onLog`. |
140
226
  | `GOOGLE_MAPS_PRESETS` | object | Every named fault preset. |
141
227
  | `GOOGLE_MAPS_NAMESPACE` | string | The service name, `"google-maps"`. |
142
- | `keyCredential` | function | The `key` query parameter of a request (how API keys map to namespaces). |
228
+ | `keyCredential` | function | The `key` query parameter (or `X-Goog-Api-Key` header) of a request (how API keys map to namespaces). |
143
229
  | `DEFAULT_CORPUS`, `PHOENIX_DEMO_ADDRESS`, `STATE_NAMES` | values | The address corpus, the Phoenix demo member row, and state names for `administrative_area_level_1`. |
144
230
  | `corpusPlaceId` | function | The stable place id of a corpus row. |
145
231
  | `mapsJavaScript` | function | Render the Maps JavaScript shim for `{base, key, authFailed, callback}`. |
146
232
  | `normalize` | function | The address normalization used for matching. |
147
233
  | `MISSING_KEY_MESSAGE`, `INVALID_KEY_MESSAGE` | strings | Google's `error_message` for a missing / refused key. |
234
+ | `MISSING_API_KEY_MESSAGE`, `INVALID_API_KEY_MESSAGE` | strings | Address Validation's `error.message` for a missing (403) / invalid (400) key. |
148
235
  | `document`, `operationIds`, `supportedOperationIds` | values | The OpenAPI contract and its operation ids. |
149
236
  | `createServer`, `serveTarget`, `DEFAULT_PORT` (`./server`) | Node | Serve over `node:http`; the `serve` CLI target (`--api-key`, `--public-url`); port 8814. |
150
237