toga-ai 1.0.294 → 1.0.296
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.
|
@@ -6,8 +6,8 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["jcardinal"]
|
|
9
|
+
updated: 2026-07-09
|
|
10
|
+
owners: ["jcardinal", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
- api2/Component/Api/V2/Response/Response.php
|
|
@@ -54,8 +54,27 @@ PersonaApp, UserGlobal, UserApp — each overriding the previous when set. A Cli
|
|
|
54
54
|
`isOverridable = 0` **locks** the value against all later layers. For the persona layers, the **first
|
|
55
55
|
persona** (in the user's persona order) that sets it wins. The resolved code is validated against
|
|
56
56
|
`Languages.code` (fallback `en`), embedded in the JWT `id.language` claim, and echoed on every
|
|
57
|
-
response as `audience.language`. API-credential auth gets no language.
|
|
58
|
-
|
|
57
|
+
response as `audience.language`. API-credential auth gets no language.
|
|
58
|
+
|
|
59
|
+
**Language re-resolution on token refresh (user tokens only).** `/auth/refresh` now re-resolves the
|
|
60
|
+
user's language and embeds it in the newly minted tokens, so a language switch takes effect on the
|
|
61
|
+
next hourly refresh **without a full re-login** (previously the refresh path copied the decoded JWT
|
|
62
|
+
payload verbatim and only reset `exp`/`iat`, so a language change was invisible until re-auth). The
|
|
63
|
+
re-resolution runs **only** for user tokens — it reads `id.client.user.uuid` off the token and skips
|
|
64
|
+
entirely for API-credential and public tokens (no language claim). It rebuilds the resolution context
|
|
65
|
+
from the existing token — loads `_Model_Client_User` by `uuid` (→ `userId`), `_Model_Core_App` by
|
|
66
|
+
`id.app.uuid` (→ `appId`), and takes `personaIds` from `id.client.personas` — then calls the **same**
|
|
67
|
+
private `resolveSettingValue(slug: _Model_Core_Setting::SLUG_LANGUAGE, context: {appId, personaIds,
|
|
68
|
+
userId})` helper the login path uses (same 8-layer cascade; the client DB is already registered at this
|
|
69
|
+
point). The resolved code is validated against `_Model_Client_Language` (load by `code`):
|
|
70
|
+
- **Valid** → sets `$this->jwtPayload->id->language` to the resolved code.
|
|
71
|
+
- **Unset/empty** → defaults to `'en'`.
|
|
72
|
+
- **Invalid** (non-empty but absent from `Languages`) → **keeps the token's current language** and adds
|
|
73
|
+
a `WZ-4` warning (see below). It deliberately does **not** fail the refresh — a bad setting must not
|
|
74
|
+
log an active user out on their hourly refresh (contrast the login path, which hard-fails with EV-16).
|
|
75
|
+
|
|
76
|
+
Placement is load-bearing: the `id.language` mutation happens **after** client-DB registration and
|
|
77
|
+
**before** both the access and refresh tokens are signed, so both new tokens carry the updated language.
|
|
59
78
|
|
|
60
79
|
**Invalid-language hard failure (auth time).** The resolved code is validated against the client's
|
|
61
80
|
`Languages` table. An **unset/empty** setting defaults to `en` (no error). But a **non-empty** code
|
|
@@ -64,6 +83,14 @@ that does **not** exist in `Languages` now **fails authentication** with HTTP 40
|
|
|
64
83
|
with the offending `language` code in the message `identifiers` — instead of silently falling back to
|
|
65
84
|
`en`. A misconfigured language must surface loudly, not silently degrade.
|
|
66
85
|
|
|
86
|
+
**Invalid-language soft warning (refresh time).** On the refresh path an invalid code is handled with a
|
|
87
|
+
distinct, non-fatal WARNING instead of EV-16: `WZ-4 DEFINED_MESSAGE_WARNING_INVALID_LANGUAGE_SETTING`
|
|
88
|
+
(type WARNING, group VALIDATION, `{language}` identifier placeholder; defined in `Response.php`, added
|
|
89
|
+
via `addDefinedMessage(...)` so the warning has a stable catalogued code). Because it is a WARNING it
|
|
90
|
+
carries no `status` key and does not change the response status — the refresh succeeds and the token
|
|
91
|
+
keeps its current language. This is deliberately softer than EV-16 (status 400), which would fail the
|
|
92
|
+
refresh and log the user out.
|
|
93
|
+
|
|
67
94
|
**Auth-mint status finalization (subtle).** The auth success path sets `status = 201` and attaches the
|
|
68
95
|
tokens. EV-16 is signalled by a local flag `$invalidLanguageSetting` set at the resolution site; the
|
|
69
96
|
finalization branches `if (empty($invalidLanguageSetting))` → `201` + tokens, else
|
|
@@ -134,6 +161,9 @@ None — uniform across all clients. The sidecar table + ACL ship via `dbchanges
|
|
|
134
161
|
`__get` — `isset()`/`?? null` always read false/null. See
|
|
135
162
|
[_Model magic-field access](../../_underscore/features/model-magic-field-access.md); this was the
|
|
136
163
|
cause of the uuid not appearing in the fallback warning.
|
|
164
|
+
- **Refresh re-resolution is user-token-only.** If you extend the JWT with new audience-shaped claims,
|
|
165
|
+
remember the refresh path rebuilds context from the *existing* token (uuid/app-uuid/personas) — it
|
|
166
|
+
does not re-read anything the token doesn't carry.
|
|
137
167
|
- Migrations not yet executed at time of writing; needs live verification.
|
|
138
168
|
|
|
139
169
|
## Change history
|
|
@@ -149,6 +179,14 @@ None — uniform across all clients. The sidecar table + ACL ship via `dbchanges
|
|
|
149
179
|
record `uuid` and dedupes per-record (`recordFieldId:uuid`). Documented the `isOverridable = 0`
|
|
150
180
|
client-global lock as a config gotcha. Discovered the `_Model` `__isset` gap behind the missing-uuid
|
|
151
181
|
bug (see linked _underscore doc). Live-tested on the local Compass DB. (jcardinal)
|
|
182
|
+
- 2026-07-09 — `/auth/refresh` now re-resolves the user's language (same `resolveSettingValue` cascade as
|
|
183
|
+
login) and embeds it in the newly minted access + refresh tokens, so a language switch applies on the
|
|
184
|
+
next refresh with no re-login (previously refresh copied the JWT payload verbatim, resetting only
|
|
185
|
+
`exp`/`iat`). User-tokens only (skips API-credential/public — no language claim); mutates `id.language`
|
|
186
|
+
after client-DB registration and before signing both tokens. Invalid code on refresh is a **soft**
|
|
187
|
+
non-fatal `WZ-4 WARNING_INVALID_LANGUAGE_SETTING` (new constant in `Response.php`) that keeps the
|
|
188
|
+
token's current language — vs. login's hard EV-16 400 — so a bad setting can't log an active user out.
|
|
189
|
+
(TRUE-80064, bala)
|
|
152
190
|
|
|
153
191
|
## Related docs
|
|
154
192
|
|
|
@@ -6,12 +6,13 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
10
|
-
owners: ["bala"]
|
|
9
|
+
updated: 2026-07-09
|
|
10
|
+
owners: ["bala", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
related:
|
|
14
14
|
- ../architecture.md
|
|
15
|
+
- ../../../clients/aig/features/entitlement-intake.md
|
|
15
16
|
---
|
|
16
17
|
|
|
17
18
|
## Summary
|
|
@@ -57,6 +58,37 @@ rows on every write.
|
|
|
57
58
|
If you need to link by a business key (like an employee XID), **resolve that key to a uuid in your
|
|
58
59
|
caller first** (e.g. build a `key → uuid` map), then send `{uuid}`.
|
|
59
60
|
|
|
61
|
+
## Per-API overrides (`Apis_RecordFields`)
|
|
62
|
+
|
|
63
|
+
The base identifier flags above (`Core.RecordFields.isIdentifier`) can be **overridden per API**
|
|
64
|
+
via rows in a tenant's `Apis_RecordFields` table, read by the V2 resolver's override block
|
|
65
|
+
(`V2.php` ~6926–6936; child policy in `getChildPolicy()` ~8019–8041; loader ~8526–8538). Two
|
|
66
|
+
columns matter:
|
|
67
|
+
|
|
68
|
+
- **`overrideIsIdentifier`** — overrides whether the field is a searchable identifier for that API.
|
|
69
|
+
- **`overrideChildPolicy`** — overrides link-vs-create for the nested record (e.g. `MATCH_CREATE`
|
|
70
|
+
= match an existing child by identifier, else create it).
|
|
71
|
+
|
|
72
|
+
> **Trap: a `NULL` `overrideIsIdentifier` is read as "not an identifier", not "no opinion".**
|
|
73
|
+
> The override block evaluates `!$row->overrideIsIdentifier`, so a `NULL` **unsets** the field
|
|
74
|
+
> from the API's searchable identifiers even when the base `Core.RecordFields` row has
|
|
75
|
+
> `isIdentifier=1`. Any `Apis_RecordFields` row created **only** to set a child policy
|
|
76
|
+
> (leaving `overrideIsIdentifier` NULL) will silently strip that field as an identifier. The
|
|
77
|
+
> nested object then has no searchable identifier and the write fails **EV-12 VALIDATION**
|
|
78
|
+
> (`V2.php:7094`) — the `MATCH_CREATE` that should have saved it never runs on that path.
|
|
79
|
+
> Correct the data with an explicit `overrideIsIdentifier = 1`, or (systemic fix, not yet done)
|
|
80
|
+
> change the code so only an explicit `0` removes an identifier and `NULL` means "no override".
|
|
81
|
+
|
|
82
|
+
**Build divergence warning.** The `Apis_RecordFields` override feature (reading
|
|
83
|
+
`overrideIsIdentifier`/`overrideChildPolicy`) was introduced 2026-03-20 (api2 commit `624146d`
|
|
84
|
+
"Cache custom fields and API override lookups"). Environments on **older** builds ignore the
|
|
85
|
+
override and keep the base identifier, so a tenant with identical Core metadata + override rows +
|
|
86
|
+
data can behave differently across environments purely by build version. A NULL-`overrideIsIdentifier`
|
|
87
|
+
override row that works on prod (older build) will start throwing EV-12 once prod gets the newer
|
|
88
|
+
build — audit such rows before/with the deploy. First seen in AIG (TRUE-79978): prod resolved,
|
|
89
|
+
beta/QA threw EV-12 on the injected `entitlementFulfillmentType` — see
|
|
90
|
+
[AIG entitlement intake](../../../clients/aig/features/entitlement-intake.md).
|
|
91
|
+
|
|
60
92
|
## Gotcha
|
|
61
93
|
|
|
62
94
|
- **Nested write with only a non-identifier field silently creates duplicates.** This is a
|
|
@@ -68,6 +100,11 @@ caller first** (e.g. build a `key → uuid` map), then send `{uuid}`.
|
|
|
68
100
|
|
|
69
101
|
## Change history
|
|
70
102
|
|
|
103
|
+
- 2026-07-09 — Documented the per-API `Apis_RecordFields` override layer (`overrideIsIdentifier`,
|
|
104
|
+
`overrideChildPolicy`) and the trap that a `NULL` `overrideIsIdentifier` is read as "not an
|
|
105
|
+
identifier" (`!$row->overrideIsIdentifier`) — an override row set only for a child policy
|
|
106
|
+
silently strips the field as an identifier → EV-12 at `V2.php:7094`. Added the build-divergence
|
|
107
|
+
warning (override feature added 2026-03-20, commit `624146d`). Surfaced by AIG TRUE-79978. (mhammontree)
|
|
71
108
|
- 2026-07-07 — Documented that api2 nested-relationship writes match an existing child **only** by
|
|
72
109
|
identifier (`uuid`); a non-unique custom field is not a match key and forces a new-record insert.
|
|
73
110
|
Correct pattern is `contact: {uuid}` (resolve business keys to uuid in the caller). Surfaced by
|
|
@@ -78,3 +115,5 @@ caller first** (e.g. build a `key → uuid` map), then send `{uuid}`.
|
|
|
78
115
|
- [api2 architecture](../architecture.md) — the V2 CRUD engine (`processRoutePairs`).
|
|
79
116
|
- [Prudential device import + contact linking](../../../clients/prudential/features/device-information-import-and-contact-linking.md)
|
|
80
117
|
— the production incident that exposed this behavior.
|
|
118
|
+
- [AIG entitlement intake](../../../clients/aig/features/entitlement-intake.md) — the TRUE-79978
|
|
119
|
+
EV-12 that exposed the `Apis_RecordFields` NULL-`overrideIsIdentifier` trap + build divergence.
|
|
@@ -5,12 +5,13 @@ project: API
|
|
|
5
5
|
client: aig
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
8
|
+
updated: 2026-07-09
|
|
9
9
|
owners: ["mhammontree"]
|
|
10
10
|
files:
|
|
11
11
|
- _underscore/Model/Aig/Entitlement.php
|
|
12
12
|
- dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql
|
|
13
13
|
related:
|
|
14
|
+
- 2.0/apps/api2/features/nested-relationship-writes.md
|
|
14
15
|
- 2.0/apps/api2/architecture.md
|
|
15
16
|
- 2.0/apps/dbchanges2/architecture.md
|
|
16
17
|
- 2.0/apps/_underscore/architecture.md
|
|
@@ -42,7 +43,9 @@ the FK can't resolve and intake fails with **"Missing AIG item ID."** Keeping
|
|
|
42
43
|
- reads `partNumber = payload->saleItem->partNumber` and runs
|
|
43
44
|
`SELECT id FROM Items WHERE partNumber = '{partNumber}' LIMIT 1` to get `$itemId`;
|
|
44
45
|
- uses `$itemId` to load fulfillment **types** (`Items_EntitlementFulfillmentTypes`) and
|
|
45
|
-
**methods** (`Items_EntitlementFulfillmentMethods`) and injects them into the payload
|
|
46
|
+
**methods** (`Items_EntitlementFulfillmentMethods`) and injects them into the payload as
|
|
47
|
+
**name-only** related objects (e.g. `entitlementFulfillmentType: {name: "Repair"}`). AIG
|
|
48
|
+
never sends `entitlementFulfillmentType`; it is populated entirely by this interceptor.
|
|
46
49
|
3. The V2 engine resolves `Entitlement.saleItemId` (FK → `_Model_Client_Item`) from that same
|
|
47
50
|
`saleItem.partNumber`. **No matching `Items` row → unresolvable FK → "Missing AIG item ID".**
|
|
48
51
|
4. `postPost` then emails the contact a Staples Protection Plan registration link
|
|
@@ -118,9 +121,28 @@ this interceptor or use this dual-purpose Items pattern.
|
|
|
118
121
|
codes are **not** in the SaleItemID spreadsheet and must be sourced separately if missing.
|
|
119
122
|
- **No unique key on `Items.partNumber`** — use an anti-join for idempotent loads, not
|
|
120
123
|
`INSERT IGNORE`.
|
|
124
|
+
- **An `entitlementFulfillmentType` EV-12 VALIDATION error is NOT a bad client payload —
|
|
125
|
+
it's this interceptor's injected field failing to resolve.** AIG never sends that field;
|
|
126
|
+
the interceptor injects it as `{name: "Repair"}`. The error means the V2 resolver could not
|
|
127
|
+
find a searchable identifier for the injected object. Root cause (TRUE-79978) was an
|
|
128
|
+
`Apis_RecordFields` override row that stripped `name` as an identifier — see
|
|
129
|
+
[api2 nested-relationship writes → Per-API overrides](../../../2.0/apps/api2/features/nested-relationship-writes.md).
|
|
130
|
+
- **Empty `saleItem.partNumber` silently resolves to the wrong item.** The lookup is
|
|
131
|
+
`WHERE partNumber = '{$partNumber}' LIMIT 1`; when `partNumber` is `""`, it matches item
|
|
132
|
+
`id=10` ("2YR Tablet") — the first row with an empty part number — which carries a "Repair"
|
|
133
|
+
fulfillment type. So a blank part number does not fail loudly; it injects a spurious
|
|
134
|
+
name-only fulfillment type. The interceptor should guard an empty/failed lookup before
|
|
135
|
+
injecting (not yet fixed).
|
|
121
136
|
|
|
122
137
|
## Change history
|
|
123
138
|
|
|
139
|
+
- 2026-07-09 — TRUE-79978: root-caused an `entitlementFulfillmentType` EV-12 on beta/QA. The
|
|
140
|
+
field is injected (name-only) by `prePost`; the beta `Client_Aig.Apis_RecordFields` override
|
|
141
|
+
row (apiId=2, recordFieldId=1202) had `overrideIsIdentifier=NULL` + `overrideChildPolicy=MATCH_CREATE`,
|
|
142
|
+
which the newer V2 build reads as "not an identifier", stripping `name` → no searchable id →
|
|
143
|
+
EV-12. Unblocked with a manual one-row beta data fix (`SET overrideIsIdentifier=1`); prod runs
|
|
144
|
+
an older build without the override feature so it still resolves. See the api2 feature doc for
|
|
145
|
+
the shared resolver behavior. (mhammontree)
|
|
124
146
|
- 2026-06-19 — Documented intake flow; loaded 2,200 new SaleItem codes (1,700 STS `ASI-*`/`SM-*`
|
|
125
147
|
+ 500 unique numeric `SA`) into `Client_Aig.Items` via TRUE-79534, fixing "Missing AIG item ID"
|
|
126
148
|
on the new `ASI-*` scheme. Follow-up to TRUE-79441. (mhammontree)
|
package/package.json
CHANGED