@alvera-ai/platform-sdk 0.21.2 → 0.21.3-next.g393d024

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/.agent/mdm.md CHANGED
@@ -41,8 +41,8 @@ vocabulary — `given_name`, `family_name`, `birth_date` and
41
41
  `gender` are gone, not renamed variants to pick between.
42
42
 
43
43
  ```
44
- MDMVerifyRequest every field the verifier matches
45
- ──────────────── ────────────────────────────────
44
+ MDMVerifyRequest every field the request takes
45
+ ──────────────── ─────────────────────────────
46
46
  required legal_entity_id
47
47
  individual first_name middle_name last_name
48
48
  preferred_name date_of_birth
@@ -53,6 +53,18 @@ vocabulary — `given_name`, `family_name`, `birth_date` and
53
53
  identifiers addresses
54
54
  ```
55
55
 
56
+ Not every field is evidence on its own (§2):
57
+
58
+ ```
59
+ can stand alone first_name last_name preferred_name
60
+ date_of_birth business_name date_formed
61
+ phone identifiers addresses
62
+ only beside one legal_entity_type middle_name
63
+ of those citizenship_country nationality
64
+ doing_business_as_names
65
+ jurisdiction_country email
66
+ ```
67
+
56
68
  An individual example:
57
69
 
58
70
  ```typescript
@@ -88,24 +100,56 @@ const { data: result } = await api.mdm.verify(
88
100
  ],
89
101
  },
90
102
  )
91
- // result.status === 'verified' | 'not_verified'
103
+ // result.status === 'verified' — the only success body
92
104
  // result.legal_entity_id === body.legal_entity_id
93
- // result.verified_at — ISO 8601 timestamp
94
- // when status === 'not_verified':
95
- // result.errors — { <field_name>: [reason, ...] } per-field failure detail
105
+ // result.verified_at — ISO 8601 timestamp
96
106
  ```
97
107
 
98
- The response is **polymorphic on `status`**:
108
+ `verified` is the **only** success body. A subject that does not
109
+ verify is not a body you read — the call throws, like every other
110
+ route that answers 4xx:
99
111
 
100
112
  ```
101
- status === 'verified' → 200 { legal_entity_id, verified_at }
102
- status === 'not_verified' → 422 { legal_entity_id, verified_at, errors }
113
+ verified → 200 { status: 'verified', legal_entity_id, verified_at }
114
+ did not verify → throws AlveraApiError, _httpStatus 422
115
+ { errors: [ { title: 'Invalid value',
116
+ source: { pointer: '/<field>' },
117
+ detail: '<why it failed>' } ] }
118
+ one entry per field that did not match
119
+ subject not found → throws AlveraApiError, _httpStatus 404
120
+ ```
121
+
122
+ So `data` is only ever the verified answer, and every failed check
123
+ reaches you in `catch`, in the same envelope every other 422 uses
124
+ (see `errors.md`). Read which fields failed from each entry's
125
+ `source.pointer`:
126
+
127
+ ```typescript
128
+ import type { AlveraApiError } from '@alvera-ai/platform-sdk'
129
+
130
+ try {
131
+ const { data } = await api.mdm.verify(tenantSlug, datalakeSlug, {
132
+ legal_entity_id: '00000000-0000-0000-0000-000000000000',
133
+ last_name: 'Garcia',
134
+ addresses: [{ postal_code: '94110', country: 'US' }],
135
+ })
136
+ // data.status === 'verified' — act on this person
137
+ } catch (err) {
138
+ const status = (err as { _httpStatus?: number })._httpStatus
139
+ if (status === 422) {
140
+ for (const e of (err as AlveraApiError).errors) {
141
+ // e.source.pointer — '/last_name', '/addresses', '/base', …
142
+ // e.detail — 'does not match any known name', …
143
+ }
144
+ } else if (status === 404) {
145
+ // no legal entity has this id
146
+ } else {
147
+ throw err
148
+ }
149
+ }
103
150
  ```
104
151
 
105
- A `not_verified` response is a successful identity check that
106
- returned a negative result — it arrives as a 422 whose body
107
- still carries the full envelope. Distinguish it from the other
108
- rejections (404 for missing subject, 401 for missing auth).
152
+ A 401 (missing or wrong key) throws too.
109
153
 
110
154
  ### One subject: the legal entity
111
155
 
@@ -128,14 +172,23 @@ declare the column; every generic-table row struct already has it.
128
172
 
129
173
  ## 2. Rules the type cannot encode
130
174
 
131
- ### `legal_entity_id` is required; at least one identity field with it
175
+ ### `legal_entity_id` is required; at least one field that stands alone with it
132
176
 
133
177
  `legal_entity_id` (the master record to verify against) is the
134
178
  only required field. Every other property is optional, and the
135
- verifier requires **at least one** of them at runtime:
136
-
137
- - a body with only `legal_entity_id` → 422 with
138
- `errors.base: ["at least one verification field is required"]`
179
+ verifier requires **at least one field that can stand alone as
180
+ evidence** at runtime: `first_name`, `last_name`, `preferred_name`,
181
+ `date_of_birth`, `business_name`, `date_formed`, `phone`,
182
+ `identifiers` or `addresses`. The other seven (`legal_entity_type`,
183
+ `middle_name`, `citizenship_country`, `nationality`,
184
+ `doing_business_as_names`, `jurisdiction_country`, `email`) are
185
+ accepted, but count only beside one of those nine.
186
+
187
+ - a body with only `legal_entity_id`, or with only fields from the
188
+ seven → 422, one entry with `source.pointer: '/base'` and
189
+ `detail: 'at least one verification field is required'`
190
+ - an address alone is enough evidence; if it does not match, the
191
+ 422 carries the `/addresses` entry and nothing else
139
192
 
140
193
  A body carrying the retired vocabulary — `given_name`,
141
194
  `family_name`, `birth_date`, `gender` — falls into that same 422,
@@ -160,9 +213,33 @@ addresses per-component exact, case- and whitespace-
160
213
  insensitive; at least one address must match
161
214
  ```
162
215
 
163
- All supplied fields must pass for the overall result to be
164
- `'verified'`. A single failing field flips the result to
165
- `'not_verified'` and surfaces the field name in `errors`.
216
+ All supplied fields must pass for the call to return
217
+ `'verified'`. Each field that fails adds one entry to the thrown
218
+ 422, pointing at the field you sent:
219
+
220
+ ```
221
+ source.pointer detail
222
+ /last_name does not match any known name
223
+ /first_name does not match any known name (see below)
224
+ /business_name does not match any known name
225
+ /date_of_birth does not match
226
+ /date_formed does not match
227
+ /identifiers no matching identifier found
228
+ /phone does not match any known phone number
229
+ /addresses no matching address found
230
+ /addresses/country can't be blank (an address sent without a country)
231
+ /base at least one verification field is required
232
+ ```
233
+
234
+ First, middle and preferred names are checked together as one
235
+ bag of given names, so they fail as one entry. It points at a
236
+ given-name field you actually sent: `/first_name` if you sent one,
237
+ else `/middle_name`, else `/preferred_name`.
238
+
239
+ Two fields failing is two entries; nothing is summarised into a
240
+ single verdict. The mock answers its wrong-details case with one
241
+ `/base` entry, `detail: 'Verification match failed'`; the platform
242
+ itself names the field.
166
243
 
167
244
  ### `legal_entity_id` is the legal entity's own id
168
245
 
@@ -176,11 +253,12 @@ redacted lakes — rather than a second row with its own id. The
176
253
  old failure mode where passing the regulated twin's id returned a
177
254
  404 cannot arise.
178
255
 
179
- If the supplied `legal_entity_id` doesn't resolve, the response
180
- is a 404, not a `'not_verified'`. The distinction matters:
256
+ If the supplied `legal_entity_id` doesn't resolve, the call throws
257
+ a 404 envelope (`title: 'Not Found'`, `detail: 'Subject not found'`,
258
+ empty `source.pointer`), not a 422. The distinction matters:
181
259
 
182
- - `'not_verified'` -> "entity exists, identity fields didn't match"
183
- - 404 -> "entity doesn't exist at all"
260
+ - 422 -> "entity exists, identity fields didn't match"
261
+ - 404 -> "entity doesn't exist at all"
184
262
 
185
263
  A consumer that conflates the two will mis-route an identity
186
264
  challenge as a missing-record error.
@@ -245,13 +323,14 @@ Two limits that follow from the same rule:
245
323
  **Server-derived (Response-only).**
246
324
 
247
325
  ```
248
- status enum — 'verified' | 'not_verified'
249
- verified_at string — ISO 8601 timestamp of the verification attempt
250
- errors object — present only when status === 'not_verified';
251
- maps field name to an array of failure reasons
326
+ status enum — 'verified', its only value
327
+ verified_at string — ISO 8601 timestamp of the verification
252
328
  ```
253
329
 
254
- **Caller-supplied (round-trip on the verified branch).**
330
+ There is no `errors` field on the response: a failed check is a
331
+ thrown 422 (§4), never a body.
332
+
333
+ **Caller-supplied (round-trip in the verified answer).**
255
334
 
256
335
  ```
257
336
  legal_entity_id required UUID — the master record to verify against
@@ -270,35 +349,35 @@ legal_entity_id required UUID — the master record to verify against
270
349
 
271
350
  ## 4. Error envelopes
272
351
 
273
- Standard JSON:API envelopes per `errors.md`. Common rejections:
274
-
275
- | `source.pointer` | Cause |
276
- |-------------------------------|--------------------------------------------------------|
277
- | `/legal_entity_id` | missing, or no legal entity has this id |
278
- | `errors.base` | no identity field was supplied, or only retired ones (`given_name`, `family_name`, `birth_date`, `gender`) which are ignored — 422 with `status: "not_verified"` in the body |
279
- | `/identifiers/0/system` | missing system on an identifier entry |
280
- | `/identifiers/0/value` | missing value on an identifier entry |
281
-
282
- The `not_verified` outcome is always a **422 response** carrying
283
- the full body envelope; the two shapes differ only in `errors`:
284
-
285
- - **Identity mismatch (most common)** — `errors` keyed by the
286
- failing field(s), e.g. `errors.last_name`, or `errors.base`
287
- with "Verification match failed". The entity exists; the
288
- supplied identity didn't match.
289
- - **No usable identity fields** — `errors.base` set to
290
- `["at least one verification field is required"]`. Verification
291
- was structurally impossible: nothing supplied, or only retired
292
- fields, which are ignored.
293
-
294
- Malformed identifier entries surface the embed failure flattened
295
- into the field's error list — `errors.identifiers: ["value can't
296
- be blank"]` — still a 422, never a 500. `system` and `value` are
297
- the required pair; `id_type` and `country` are optional.
298
-
299
- Pattern-match on `status` first, then on the HTTP status code.
300
- The 404 case (subject doesn't exist) is a separate envelope —
301
- see §6 gotcha 2.
352
+ Every error `.verify` returns is the standard JSON:API envelope
353
+ per `errors.md`, thrown as `AlveraApiError`. There is no second
354
+ shape. Common rejections:
355
+
356
+ | HTTP | `source.pointer` | Cause |
357
+ |------|---------------------------|-------|
358
+ | 422 | `/<field>` | a field you sent did not match the master record — one entry per field (§2 lists the pointers and details) |
359
+ | 422 | `/base` | no field that can stand alone as evidence was supplied (§2) — including a body of only `email`, `middle_name` or the other five that count only beside one, or only retired fields (`given_name`, `family_name`, `birth_date`, `gender`), which are ignored |
360
+ | 422 | `/legal_entity_id` | `legal_entity_id` missing from the body |
361
+ | 422 | `/identifiers/0/system` | missing system on an identifier entry |
362
+ | 422 | `/identifiers/0/value` | missing value on an identifier entry |
363
+ | 404 | `""` (empty) | no legal entity has this id — `title: 'Not Found'`, `detail: 'Subject not found'` |
364
+
365
+ A subject that does not verify and a body the platform refuses
366
+ arrive the same way: a 422 whose entries all carry
367
+ `title: 'Invalid value'`. Read the field from `source.pointer`.
368
+ There is no `errors` map keyed by field name, and a failure body
369
+ carries no `status`, `legal_entity_id` or `verified_at`.
370
+
371
+ `system` and `value` on an identifier are required by the request
372
+ schema, so a missing one is refused before verification runs, and
373
+ its pointer carries the entry's index. An embed the verifier
374
+ itself rejects points at the embed's field without an index: an
375
+ address sent without a country is `/addresses/country`, not
376
+ `/addresses/0/country`. `id_type` and `country` on an identifier
377
+ are optional. Either way it is a 422, never a 500.
378
+
379
+ Branch on `_httpStatus`: 422 for a failed check or a refused body,
380
+ 404 for a subject that does not exist (§6 gotcha 2).
302
381
 
303
382
  ## 5. Lifecycle
304
383
 
@@ -344,26 +423,30 @@ through `api.mdm`.
344
423
 
345
424
  ## 6. Gotchas
346
425
 
347
- 1. **Every `not_verified` outcome is a 422 whose body still
348
- carries the envelope.** Read the body's `status` — don't let
349
- generic 4xx handling swallow the `errors` payload that
350
- explains which fields failed. Gate the downstream "act on
351
- this person" path on `result.status === 'verified'`.
352
-
353
- 2. **`legal_entity_id` missing → 404, identity mismatch → 422
354
- with a body.** A verify call against a deleted or never-existed
355
- entity id returns a 404 envelope, NOT a `'not_verified'`
356
- response. Consumers that handle only the polymorphic
357
- verify body will surface the 404 as an unexpected exception.
358
- Wrap the verify call in standard 4xx handling per `errors.md`.
359
-
360
- 3. **At least one identity field is required.** A body with only
361
- `legal_entity_id` — or only retired fields like `given_name`
362
- or `birth_date`, which are ignored — returns a 422 with
363
- `errors.base` set to `["at least one verification field is
364
- required"]` and `status: "not_verified"` in the body. This is
365
- enforced at runtime rather than in the request schema, so the
366
- TypeScript type permits the body but the wire rejects it.
426
+ 1. **A subject that does not verify throws; there is no
427
+ `not_verified` body.** `data` is only ever the verified answer.
428
+ Every failed check is a thrown `AlveraApiError` with
429
+ `_httpStatus === 422`, one entry per mismatched field. Code
430
+ written for the old `{ status: 'not_verified', errors: {…} }`
431
+ body never runs: its branch is unreachable and the 422 lands in
432
+ `catch`. Don't let generic 4xx handling swallow it — read each
433
+ entry's `source.pointer` to learn which fields failed.
434
+
435
+ 2. **Subject missing → 404, identity mismatch → 422.** Both
436
+ throw, in the same envelope. A verify call against a deleted
437
+ or never-existed entity id throws a 404 (`title: 'Not Found'`,
438
+ empty pointer), not a 422. Branch on `_httpStatus` before
439
+ reading pointers, per `errors.md`.
440
+
441
+ 3. **At least one field that can stand alone is required.** A body
442
+ with only `legal_entity_id` — or only fields that count just
443
+ beside another (`email`, `middle_name`, `nationality`, …), or
444
+ only retired fields like `given_name` or `birth_date`, which are
445
+ ignored — throws a 422 with one
446
+ entry: `source.pointer: '/base'`, `detail: 'at least one
447
+ verification field is required'`. This is enforced at runtime
448
+ rather than in the request schema, so the TypeScript type
449
+ permits the body but the wire rejects it.
367
450
 
368
451
  4. **Unknown fields are ignored, not rejected.** There is one
369
452
  field set (§1) and no domain scoping, but anything outside it
@@ -385,8 +468,9 @@ through `api.mdm`.
385
468
  case, and trailing slashes in `system` URIs all matter.
386
469
  Compare with the master record's identifier system strings
387
470
  character-for-character; a `'http://hospital.org/mrn'` vs
388
- `'http://hospital.org/mrn/'` mismatch will flip the verifier
389
- to `'not_verified'` even when the value is correct.
471
+ `'http://hospital.org/mrn/'` mismatch fails the check with an
472
+ `/identifiers` entry (`no matching identifier found`) even when
473
+ the value is correct.
390
474
  `system` and `value` are the required pair. `id_type`
391
475
  (`'digital_identifier'`, `'us_ssn'`, `'lei'`) and `country`
392
476
  are optional — the per-domain rule that once made `id_type`
package/.agent/tools.md CHANGED
@@ -792,12 +792,16 @@ the ASU's AI metadata. A failed poll lands its structured reason in
792
792
  You do **not** have to wait for the cron. `actionStatusUpdaters.refresh`
793
793
  fires one poll cycle on demand (CLI: `alvera refresh-action-status-updater
794
794
  --id <uuid>`), and reading `last_run_*` correctly is what makes it
795
- usable as a proof — three things to get right:
795
+ usable as a proof — four things to get right:
796
796
 
797
797
  - **`refresh` answers `202` with the updater row AS-IS.** The poll runs
798
798
  asynchronously, so the response carries the PREVIOUS run's stamps.
799
799
  Capture `last_run_at` as a baseline BEFORE calling it, then poll the
800
800
  row until `last_run_at` advances past that baseline.
801
+ - **`refresh` can answer `409` and queue nothing.** A halted updater
802
+ (`status` not `'active'`) refuses until you set `status` back to
803
+ `'active'`. An updater with a poll already queued or running refuses
804
+ until that poll finishes; retry then. The error's `detail` says which.
801
805
  - **`last_run_status` has two values.** `'ok'` means the run read its
802
806
  whole time window; `'error'` means it stopped, and `last_run_error`
803
807
  says why. There is no partial run: every page of the window is read,
package/dist/index.d.mts CHANGED
@@ -690,6 +690,10 @@ type ActionStatusUpdaterResponse = {
690
690
  * Outcome of the last poll run: `ok` when the run read its whole window, `error` when it stopped — read `last_run_error` for why. A run is never partial: every page of the window is read, one job per page.
691
691
  */
692
692
  readonly last_run_status?: 'ok' | 'error';
693
+ /**
694
+ * When a cron tick last skipped because an earlier run of this updater was still going (null if none has). A skip is not a run and leaves the last-run fields alone.
695
+ */
696
+ readonly last_skipped_at?: string | null;
693
697
  message_config: SimpleTemplateConfigResponse;
694
698
  /**
695
699
  * Updater name
@@ -999,21 +1003,20 @@ declare enum ActionType {
999
1003
  *
1000
1004
  * Every legal entity is ultimately a **person** or a **company**, so the identity
1001
1005
  * fields are organized along that ontology rather than per industry. All identity
1002
- * fields are optional on the wire; the datalake's own data-domain validator casts
1003
- * only the fields its domain knows and requires **at least one** of them — a body
1004
- * that carries only fields foreign to the datalake's domain (or none at all)
1005
- * returns 422 with `errors.base: ["at least one verification field is required"]`.
1006
+ * fields are optional on the wire, and every datalake reads the same set.
1006
1007
  *
1007
- * Field vocabulary per data domain:
1008
+ * At least one field that can stand alone as evidence is required: `first_name`,
1009
+ * `last_name`, `preferred_name`, `date_of_birth`, `business_name`, `date_formed`,
1010
+ * `phone`, `identifiers` or `addresses`. The rest (`legal_entity_type`,
1011
+ * `middle_name`, `citizenship_country`, `nationality`, `doing_business_as_names`,
1012
+ * `jurisdiction_country`, `email`) are accepted but cannot be the only evidence.
1013
+ * A body with none returns 422 in the house error envelope, pointer `/base`.
1008
1014
  *
1009
- * | identity fields |
1010
- * |---|---|
1011
- * | `legal_entity_type`, `first_name`, `middle_name`, `last_name`, `preferred_name`, `date_of_birth`, `citizenship_country`, `nationality`, `business_name`, `doing_business_as_names`, `date_formed`, `jurisdiction_country`, `phone`, `email`, `identifiers` |
1012
- *
1013
- * Every property below is cast by `Platform.MDMInput`'s
1014
- * `@verification_fields`. Keep the two in step — a property advertised here
1015
- * but absent there is accepted by the router and then silently dropped by
1016
- * `cast/3`, which reads to the caller as "my attribute did not match".
1015
+ * Every property below is cast by `Platform.MDMInput.verification_changeset/2`:
1016
+ * `@verification_fields` plus the `identifiers` and `addresses` embeds. Keep the
1017
+ * two in step — a property advertised here but absent there is accepted by the
1018
+ * router and then silently dropped by `cast/3`, which reads to the caller as "my
1019
+ * attribute did not match".
1017
1020
  *
1018
1021
  */
1019
1022
  type MdmVerifyRequest = {
@@ -3324,7 +3327,7 @@ type SqlDatabaseResponse = {
3324
3327
  /**
3325
3328
  * MDMVerifyResponse
3326
3329
  *
3327
- * Result of MDM identity verification
3330
+ * A legal entity that verified against its master record
3328
3331
  */
3329
3332
  type MdmVerifyResponse = {
3330
3333
  /**
@@ -3336,22 +3339,6 @@ type MdmVerifyResponse = {
3336
3339
  * Timestamp of verification
3337
3340
  */
3338
3341
  verified_at: string;
3339
- } | {
3340
- /**
3341
- * Field-level verification failure details
3342
- */
3343
- errors: {
3344
- [key: string]: Array<string>;
3345
- };
3346
- /**
3347
- * The legal entity id that failed verification
3348
- */
3349
- legal_entity_id: string;
3350
- status: 'not_verified';
3351
- /**
3352
- * Timestamp of verification attempt
3353
- */
3354
- verified_at: string;
3355
3342
  };
3356
3343
  /**
3357
3344
  * S3CloudStorageAwsResponse