toga-ai 1.0.994 → 1.0.996
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/knowledge/2.0/apps/api2/features/api-payload-interceptors.md +25 -2
- package/knowledge/2.0/apps/api2/features/language-translation-layer.md +13 -9
- package/knowledge/clients/compass-canada/profile.md +4 -1
- package/knowledge/standalone/apps/forward/architecture.md +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-09
|
|
10
10
|
owners: ["mhammontree", "dfranks", "bala", "snaredla", "jcardinal", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -25,6 +25,8 @@ files:
|
|
|
25
25
|
- dbchanges2/Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql
|
|
26
26
|
- dbchanges2/Client_Compass/2026-08-14a - AddSalesOrderInactiveStandaloneItemPreInterceptors.sql
|
|
27
27
|
- dbchanges2/Client_CompassCanada/2026-08-14a - AddSalesOrderInactiveStandaloneItemPreInterceptors.sql
|
|
28
|
+
- _underscore/Model/Client/UserGlobalSetting.php
|
|
29
|
+
- dbchanges2/Core/2026-10-09a - UserGlobalSettingsInterceptors.sql
|
|
28
30
|
related:
|
|
29
31
|
- ../../worker2/features/netsuite-transferorder-outbound-push.md
|
|
30
32
|
- ../../../clients/rate/features/aig-contract-creation.md
|
|
@@ -40,6 +42,8 @@ related:
|
|
|
40
42
|
- ../../_underscore/features/email-template-sending.md
|
|
41
43
|
- ../../../clients/elite/features/salesorder-netsuite-push.md
|
|
42
44
|
- ../../../clients/compass-usa/features/sales-order-line-renumbering.md
|
|
45
|
+
- ./language-translation-layer.md
|
|
46
|
+
- ../../_underscore/features/persona-nav-visibility-gate.md
|
|
43
47
|
---
|
|
44
48
|
|
|
45
49
|
How `_Model` prePost/postPost/prePut/postPut hooks are dispatched by the api2 /v2 engine only when a metadata row exists — the missing-row silent no-op, the wrong-row hard fatal, and the persistence/transaction traps; open before writing, activating, or debugging any interceptor.
|
|
@@ -73,7 +77,7 @@ The lookup predicate is **`apiId IS NULL OR apiId = <the caller's api>`**, so a
|
|
|
73
77
|
### The interceptor sees payload keys verbatim (no `c_` stripping, no rename)
|
|
74
78
|
|
|
75
79
|
**Verified empirically** by a live HTTP probe against local dev (`test/@dave/Junk Drawer/probe_c_prefix_payload_mapping.php`):
|
|
76
|
-
- `$payload` carries the keys **exactly as the client sent them**. Sending `mitsSalesOrder` yields `$payload->mitsSalesOrder`; sending `c_mitsSalesOrder` yields `$payload->c_mitsSalesOrder`. **Column mapping happens AFTER the interceptor runs
|
|
80
|
+
- `$payload` carries the keys **exactly as the client sent them**. Sending `mitsSalesOrder` yields `$payload->mitsSalesOrder`; sending `c_mitsSalesOrder` yields `$payload->c_mitsSalesOrder`. **Column mapping happens AFTER the interceptor runs** — including alias keys: a PRE hook sees `user` / `setting`, and api2 maps them onto `userId` / `settingId` only afterwards (security impact: [own-row guards](#own-row-guards-in-a-pre-interceptor)).
|
|
77
81
|
- The rename in **`Client_<X>.Apis_CustomRecordFields.overrideFieldName`** governs only which field names the API **accepts on input**. Without a rename row the renamed name is rejected with *"a field specified in your request does not exist"*; the **raw model field name is always accepted**. Rename rows are themselves `apiId`-scoped, so two APIs writing the same record can legitimately use different names for one field.
|
|
78
82
|
|
|
79
83
|
> **⚠ Do not re-derive this from the source.** Static reading of `api2/Component/Api/V2/V2.php` around **L7384** — where `$modelVarsToSet` is cast to an object after a `$flippedRenamedFields` flip — *looks* like the interceptor receives MODEL-level field names. It does not. The probe result above is authoritative; the code path is misleading.
|
|
@@ -243,6 +247,23 @@ The reverse direction: not "queue a worker from an interceptor" but **"run the p
|
|
|
243
247
|
- **Do not use the interceptor path just to send an email.** For a plain send there is a documented non-API entry point: `_Model_Client_EmailTemplate::send(string $clientIdentifier, ...)`, versus the `$api`-based `sendEmail()`. See [Email Template Sending](../../_underscore/features/email-template-sending.md). Faking `$api` is for reusing *business logic*, not for reaching the mailer.
|
|
244
248
|
- **Notes will not be logged.** `_Model_Client_Logs_Record::addNote()` is flushed only by `Component/Api/V2/V2.php`, so notes staged by the interceptor you just ran are **dropped** in a worker. See [Record Change Audit Log](../../_underscore/features/record-change-audit-log.md).
|
|
245
249
|
|
|
250
|
+
### Own-row guards in a PRE interceptor
|
|
251
|
+
|
|
252
|
+
A PRE hook that limits a user to their own rows must close four bypasses — each found by review on the `user-global-settings` guard (TRUE-82613):
|
|
253
|
+
1. **Alias keys.** api2 maps `user` / `setting` (FK objects) onto `userId` / `settingId` **after** PRE runs, so a hook that only checks `$payload->userId` is skipped by sending `user: {uuid}`. **Allowlist the payload keys** and reject anything else.
|
|
254
|
+
2. **Parent routes.** On `POST /v2/users/<uuid>/user-global-settings` api2 overwrites the FK the hook stamped with the parent's id **after** the hook. Refuse any POST with a parent segment.
|
|
255
|
+
3. **PRE runs BEFORE the field-level write ACL check** — the hook cannot count on the ACL to have rejected a field yet.
|
|
256
|
+
4. **PUT/PATCH carry no owner in the body.** Load the row by `$api->recordUuid`, refuse not-own rows, refuse changes to the owner/type columns, then `unset` them before the save.
|
|
257
|
+
|
|
258
|
+
Refuse API-key callers when the rule is "the signed-in user" (no user on the token). Throw `_Exception_Validation` (400); use **one** message for "not yours" and "not allowed" so the response does not reveal whether a row exists.
|
|
259
|
+
|
|
260
|
+
**Reference implementation:** `_Model_Client_UserGlobalSetting::prePost/prePut/prePatch` (Core.Records **279**), rows in `dbchanges2/Core/2026-10-09a - UserGlobalSettingsInterceptors.sql` (`clientId` + `apiId` NULL → **every tenant, every API**). Keys: POST `settingId, value`; PUT/PATCH `userId, settingId, value`. Only the language setting (Core.Settings id 2) is writable; `value` must be a code in the **client** `Languages` table (`_Model_Client_Language` — there is no Core Languages table and no active flag). POST stamps `userId` to the caller and refuses a second language row. No ACL grant ships with it yet.
|
|
261
|
+
|
|
262
|
+
**Known own-row holes (open, not fixed — 2026-10-09):**
|
|
263
|
+
- `Client_CompassCanada/2026-01-15 - UserGlobalSettingsAcl.sql` grants UserGlobalSettings writes with no own-row check.
|
|
264
|
+
- `UserNavPreferences` checks the user on create, not on PUT/PATCH.
|
|
265
|
+
- `TableViewPresets` has no PATCH check.
|
|
266
|
+
|
|
246
267
|
### Worked example — the EV-10 that was not a code bug
|
|
247
268
|
|
|
248
269
|
`_Model_Client_ItemFulfillment::prePost()` defaults `itemFulfillmentStageId` to the shipped stage on a `POST /v2/item-fulfillments`. In **dev-sandbox** the `(recordId 28, PRE, POST)` row was missing, so `prePost` never ran, `itemFulfillmentStageId` stayed `NULL`, and the write failed with **`EV-10` "Column 'itemFulfillmentStageId' cannot be null"** — while the identical code worked in production. Fix was purely metadata: apply the existing `dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql`. **Diagnostic that works:** do a **full-table compare of `ApiPayloadInterceptors` between production and the failing environment**. It is a small table, and the drift is usually exactly one row.
|
|
@@ -262,6 +283,7 @@ The reverse direction: not "queue a worker from an interceptor" but **"run the p
|
|
|
262
283
|
- **⚠ Validate that a payload ARRAY's elements are objects before touching them — `property_exists()` THROWS.** `property_exists()` raises a `TypeError` when argument 1 is an `array`, `int`, `null` or `bool` (a *string* merely returns `false`). So a `foreach` over e.g. `$payload->purchaseOrderItems` that calls `property_exists($item, …)` turns a malformed payload into an **HTTP 500** where the interceptor was supposed to answer **400**. Reject non-array containers and non-object elements **first**, with a plain `_Exception_Validation` (a sender coding fault is not a business condition — it should not raise an Issue or page anyone). Worked example: [Compass MITS PO zero-line-item rejection](../../../clients/compass-usa/features/mits-po-zero-line-item-rejection.md).
|
|
263
284
|
- **⚠ A throw from a PRE interceptor leaves NO `Logs_<Client>.Api` row.** The transaction log is written near the END of `_Component_Api_V2::execute()` (~L2225-2271), *after* route processing (~L1742-2043), so an exception raised from `prePost` never reaches it and the rejected request looks like it never arrived. See [V2 request logging](./request-logging.md#-a-throw-from-a-pre-interceptor-leaves-no-api-log-row-at-all).
|
|
264
285
|
- **⚠ Client behaviour never goes on `_Model_Client_X`.** Every tenant with a row for that record inherits it through `parent::` calls. Put it on `_Model_<Client>_X`.
|
|
286
|
+
- **⚠ An own-row PRE guard that checks only `userId` is bypassable** — alias keys (`user`) map after PRE, and a parent-route POST overwrites the stamped FK. Allowlist keys + refuse parent routes. See [own-row guards](#own-row-guards-in-a-pre-interceptor).
|
|
265
287
|
- **A client's rows may be in `Core` (with `clientId`), not in `Client_<X>`** — check both before concluding a client has no interceptors.
|
|
266
288
|
- **⚠ The same hook in BOTH `Core` (with `clientId`) and `Client_<X>` fires TWICE** — both tables are read, nothing dedupes. Adyen (2026-10-08): Core ids 15–17 duplicated client rows 1, 2, 5 → double worker jobs and double NetSuite queueing; fixed by deactivating the Core rows. Keep each `(record, phase, method)` in one table only. See [Adyen reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md#gotchas).
|
|
267
289
|
- **The method name is derived, so a typo'd enum silently misses.** A row with `prePostProcessing = 'PRE'`, `httpMethod = 'PUT'` resolves to `prePut`, not `prePost`; the engine finds no method and moves on.
|
|
@@ -291,3 +313,4 @@ The reverse direction: not "queue a worker from an interceptor" but **"run the p
|
|
|
291
313
|
|
|
292
314
|
## Change history
|
|
293
315
|
- 2026-10-08 — `internalApiRequest` "[]" throw also fires on the OUTER request's warnings (e.g. WO-1); throw site now ~L2436; Core + client duplicate rows fire twice. (rgirish)
|
|
316
|
+
- 2026-10-09 — Own-row guards in PRE hooks (alias keys + parent-route bypasses, PRE before field ACL); user-global-settings reference; known open holes. (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09
|
|
9
|
+
updated: 2026-10-09
|
|
10
10
|
owners: ["jcardinal", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -18,9 +18,12 @@ files:
|
|
|
18
18
|
- _underscore/Model/Client/FeatureTranslation.php
|
|
19
19
|
- _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php
|
|
20
20
|
- _underscore/Model/Client/ItemFeatureTranslation.php
|
|
21
|
+
- _underscore/Model/Client/UserGlobalSetting.php
|
|
22
|
+
- dbchanges2/Client/2026-10-08l - LanguageOverridable.sql
|
|
21
23
|
related:
|
|
22
24
|
- ./acl-permission-chain.md
|
|
23
25
|
- ../../_underscore/features/model-magic-field-access.md
|
|
26
|
+
- ./api-payload-interceptors.md
|
|
24
27
|
---
|
|
25
28
|
|
|
26
29
|
How api2 serves per-language item/feature text via `audience.language` + per-table sidecar tables resolved in the API layer (ORM untouched); open when adding a translatable field, debugging a missing translation, or a collation-mismatch SQL error.
|
|
@@ -38,20 +41,18 @@ Uniform across all clients (no client variations). The sidecar table + ACL ship
|
|
|
38
41
|
- `api2/Component/Api/V2/V2.php`: `resolveSettingValue($slug, $context)` (the 8-layer Settings-cascade resolver, private); `resolveValidLanguageCode()` (validates against `Languages.code`, default `en`); `getTranslatedFieldValue()` / `loadTranslationSidecar()` / `shouldTranslate()` / `addTranslationFallbackWarning()` (read path); `extractTranslationWrites()` / `saveTranslationWrites()` (write path); `lookupTranslatableFieldByRecordFieldId` built in the RecordFields load (~line 200).
|
|
39
42
|
- `_underscore/Model/Client/ItemTranslation.php` — the first sidecar model.
|
|
40
43
|
|
|
41
|
-
**Language resolution (auth time, user auth only).** `resolveSettingValue('language', …)` walks the Settings Matrix in order — DefaultGlobal, DefaultApp, ClientGlobal, ClientApp, PersonaGlobal, PersonaApp, UserGlobal, UserApp — each overriding the previous when set. A Client/Persona layer with `isOverridable = 0` **locks** the value against all later layers. For the persona layers, the **first persona** (in the user's persona order) that sets it wins. The resolved code is validated against `Languages.code` (
|
|
44
|
+
**Language resolution (auth time, user auth only).** `resolveSettingValue('language', …)` walks the Settings Matrix in order — DefaultGlobal, DefaultApp, ClientGlobal, ClientApp, PersonaGlobal, PersonaApp, UserGlobal, UserApp — each overriding the previous when set. A Client/Persona layer with `isOverridable = 0` **locks** the value against all later layers. For the persona layers, the **first persona** (in the user's persona order) that sets it wins. The resolved code is validated against the client `Languages.code` (see *Invalid saved language* below), embedded in the JWT `id.language` claim, and echoed on every response as `audience.language`. API-credential auth gets no language.
|
|
42
45
|
|
|
43
46
|
**Language re-resolution on token refresh (user tokens only).** `/auth/refresh` re-resolves the user's language and embeds it in the newly minted tokens, so a language switch takes effect on the next hourly refresh **without a full re-login** (previously the refresh path copied the decoded JWT payload verbatim and only reset `exp`/`iat`, so a language change was invisible until re-auth). Runs **only** for user tokens — reads `id.client.user.uuid` off the token and skips entirely for API-credential and public tokens (no language claim). Rebuilds the resolution context from the existing token — loads `_Model_Client_User` by `uuid` (→ `userId`), `_Model_Core_App` by `id.app.uuid` (→ `appId`), takes `personaIds` from `id.client.personas` — then calls the **same** private `resolveSettingValue(slug: _Model_Core_Setting::SLUG_LANGUAGE, context: {appId, personaIds, userId})` helper the login path uses (same 8-layer cascade; the client DB is already registered). The resolved code is validated against `_Model_Client_Language` (load by `code`):
|
|
44
47
|
- **Valid** → sets `$this->jwtPayload->id->language` to the resolved code.
|
|
45
48
|
- **Unset/empty** → defaults to `'en'`.
|
|
46
|
-
- **Invalid** (non-empty but absent from `Languages`) → **keeps the token's current language** and adds a `WZ-4` warning.
|
|
49
|
+
- **Invalid** (non-empty but absent from `Languages`) → **keeps the token's current language** and adds a `WZ-4` warning. Never fails the refresh.
|
|
47
50
|
|
|
48
51
|
Placement is load-bearing: the `id.language` mutation happens **after** client-DB registration and **before** both the access and refresh tokens are signed, so both new tokens carry the updated language.
|
|
49
52
|
|
|
50
|
-
**Invalid
|
|
53
|
+
**Invalid saved language — warn, never block (login + refresh).** Since 2026-10-09 (TRUE-82613) `/v2/auth/login` no longer fails with EV-16 when the user's resolved code is missing from the client `Languages` table. It falls back via private `resolveClientFallbackLanguageCode($appId)` — the same cascade with **no persona/user layers** (default + client layers only), validated again — else `_Model_Client_Language::CODE_BASE` (`en`), and adds `WZ-4 DEFINED_MESSAGE_WARNING_INVALID_LANGUAGE_SETTING` (WARNING, `{language}` identifier, `Response.php`). Status stays `201` with tokens. OAuth sign-in unsets `messages`, so OAuth callers never see WZ-4. Refresh keeps the token's current language instead (above). `EV-16` is still defined in `Response.php` but nothing raises it. Unset/empty → `en`.
|
|
51
54
|
|
|
52
|
-
**
|
|
53
|
-
|
|
54
|
-
**Auth-mint status finalization (subtle).** The auth success path sets `status = 201` and attaches the tokens. EV-16 is signalled by a local flag `$invalidLanguageSetting` set at the resolution site; finalization branches `if (empty($invalidLanguageSetting))` → `201` + tokens, else `addDefinedMessage(EV-16)` + `data = null`. See Gotchas — do **not** convert this to an `is_null($this->response->status)` guard.
|
|
55
|
+
**Writing a user's language.** `user-global-settings` writes (Core.Records 279) are guarded by PRE interceptors on `_Model_Client_UserGlobalSetting` — own row, language setting only, value must exist in client `Languages`. Rules + the bypasses they close: [API Payload Interceptors → own-row guards](./api-payload-interceptors.md#own-row-guards-in-a-pre-interceptor). Clients get the new language on the next refresh as `audience.language`.
|
|
55
56
|
|
|
56
57
|
**Which fields are translatable (metadata-driven).** `Core.RecordFields.translationRecordFieldId` (new column, positioned after `recordId`) points a source field's RecordField at the sidecar field's RecordField. `buildLookups`/RecordFields-load resolves this into `lookupTranslatableFieldByRecordFieldId[sourceRecordFieldId] => {sidecarRecordId, sidecarField}`.
|
|
57
58
|
|
|
@@ -88,9 +89,9 @@ Placement is load-bearing: the `id.language` mutation happens **after** client-D
|
|
|
88
89
|
- The resolver/read/write deliberately use `_Model::load()` per row, not a SQL JOIN/COALESCE — less SQL-efficient but the team's chosen approach; the sidecar is cached per row to avoid N-per-field loads.
|
|
89
90
|
- `audience.language` reflects the *requested* language even if a given field lacks a translation; the per-field fallback warning signals the English fallback.
|
|
90
91
|
- The base language is `en`; when the resolved language is `en` (or API auth) all translation logic is skipped and responses are byte-identical to pre-feature.
|
|
91
|
-
- **"Why isn't my language setting applying?"** A `ClientGlobalSettings` row with `isOverridable = 0` **locks** the value against all
|
|
92
|
+
- **"Why isn't my language setting applying?"** A `ClientGlobalSettings` row with `isOverridable = 0` **locks** the value against all persona/user layers — a `UserGlobalSettings` override never wins. `dbchanges2/Client/2026-10-08l - LanguageOverridable.sql` (fan-out) set the language row (`settingId 2`) to `isOverridable = 1` on every existing client. **New-client onboarding seeds still create it locked** (left as-is by decision) — a new tenant whose users cannot change language needs the same `UPDATE`.
|
|
92
93
|
- **Namespace gotcha in `V2.php` (`namespace api;`).** The api-namespaced classes (`_Component_Api_V2_Response`, `_Component_Api_V2_Response_Message`) must be referenced **unqualified** (resolves to `api\…`) or with the full `\api\_Component_Api_V2_Response_Message`. A bare leading backslash (`\_Component_Api_V2_Response_Message`) forces the global namespace and throws `EO-1 "Could not find required file"` via the autoloader. The translation fallback-warning code hit this.
|
|
93
|
-
- **Do NOT guard the auth-mint finalization with `is_null($this->response->status)`.** The auth response
|
|
94
|
+
- **Do NOT guard the auth-mint finalization with `is_null($this->response->status)`.** The auth response runs `processRoutePairs()` (client `name`, user `firstName`/`lastName`), which sets `status = 200` **before** finalization, so an `is_null` guard yields a broken `200`/`data:null` login. Set `201` unconditionally.
|
|
94
95
|
- **The all-fields (expand-everything) serializer branch must also translate.** `V2.php` has two nested-child serialization paths: when the caller requests **specific** fields it already ran each value through `getTranslatedFieldValue()`, but when the caller requested **no** specific fields (empty `$varOptions`, i.e. "return everything I have access to") the code copied raw child values straight into `$outData` and never translated — so any field returned via the all-fields expansion stayed in base English even for a non-base-language user. Both branches must route through `getTranslatedFieldValue($record, $field, $sourceModel, $defaultValue)` (preserving the `renamedFields` alias). Symptom: on the Compass Canada French storefront item-detail page (loaded via `/bundles`), feature-spec **values** were French but the feature-group **headers** (`itemCategoryFeatureGroup.name`, e.g. "Processeur"/"Mémoire") stayed English, because the group object serialized through the all-fields branch. **Not a query-depth problem** — a commerce-side `depth:6` theory was wrong and reverted; the fix returns French headers at the default `meta.depth 3`.
|
|
95
96
|
- **Sidecar/source collation mismatch — and the collations DIFFER PER CLIENT DATABASE.** The sidecar tables use `utf8mb4_unicode_ci` (matching `ItemTranslations`) while the source field's collation varies by tenant: verified in prod **and** dev-sandbox, `Client_CompassCanada.Items.title` is `utf8mb4_0900_ai_ci` but `Client_Compass.Items.title` is `utf8mb4_unicode_ci`. So a hardcoded `COLLATE utf8mb4_bin` "fix" is brittle — works in one client DB, unnecessary or wrong in another. Consequences:
|
|
96
97
|
- **Never build a mixed-collation fallback expression in SQL.** `COALESCE(NULLIF(ItemTranslations.title, ''), Items.title)` produces a result whose collation is forced to `utf8mb4_bin` (MySQL cannot reconcile the two inputs). Plain `SELECT`ing it works, so it looks fine — but **any** comparison, `ORDER BY`, `GROUP BY` or `DISTINCT` on that expression throws **MySQL error 1267 "Illegal mix of collations"**. Reproduced against prod. A latent landmine: the query ships green and fails the first time someone sorts or dedupes on it.
|
|
@@ -110,3 +111,6 @@ Placement is load-bearing: the `id.language` mutation happens **after** client-D
|
|
|
110
111
|
- [api2 Architecture](../architecture.md)
|
|
111
112
|
- [Persona Name Translation](../../_underscore/features/persona-name-translation.md) — metadata-only worked example (no api2 change).
|
|
112
113
|
- [_Model magic-field access](../../_underscore/features/model-magic-field-access.md)
|
|
114
|
+
|
|
115
|
+
## Change history
|
|
116
|
+
- 2026-10-09 — Login no longer fails on an unknown saved language (EV-16 retired): client-language fallback + WZ-4; language row made overridable on all clients; user-global-settings write guard linked. (jcardinal)
|
|
@@ -19,7 +19,7 @@ project: _Underscore
|
|
|
19
19
|
client: compass-canada
|
|
20
20
|
type: profile
|
|
21
21
|
status: active
|
|
22
|
-
updated: 2026-10-
|
|
22
|
+
updated: 2026-10-09
|
|
23
23
|
owners: [jcardinal, bala, tcox, apeterson, ajean, mhammontree, rgirish]
|
|
24
24
|
files: []
|
|
25
25
|
related:
|
|
@@ -175,6 +175,9 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
175
175
|
block, and entity-escaped because the send is iso-8859-1. See
|
|
176
176
|
[French Order Email Localization](features/french-order-email-localization.md). French
|
|
177
177
|
`EmailTemplates` bodies must be pure ASCII.
|
|
178
|
+
- **⚠ Open security hole (2026-10-09, not fixed):** `Client_CompassCanada/2026-01-15 - UserGlobalSettingsAcl.sql`
|
|
179
|
+
grants UserGlobalSettings writes with **no own-row check** — a user could write another user's row. See
|
|
180
|
+
[own-row guards](../../2.0/apps/api2/features/api-payload-interceptors.md#own-row-guards-in-a-pre-interceptor).
|
|
178
181
|
- **Approval decisions use the shared Compass parent.** `_Model_Compass_Canada_ApprovalDecision`
|
|
179
182
|
is an empty subclass of `_Model_Compass_ApprovalDecision`; the entire approval/notification/
|
|
180
183
|
manager-reassignment/VIP-auto-approve flow lives in the parent and branches at runtime on
|
|
@@ -86,5 +86,5 @@ Treat this as obfuscation only. Anything genuinely sensitive must not rely on it
|
|
|
86
86
|
- New **encrypted** domains require a code change (`$encryptedDomains` in `index.php`), not just a `forward.ini` edit.
|
|
87
87
|
- `index.php` only runs on a map miss, so a bad/over-broad `forward.ini` host-only line can shadow paths you expected PHP to handle.
|
|
88
88
|
- **EB rejects bundles over 500 MB** ("The artifact is empty or exceeds the maximum size supported by AWS Elastic Beanstalk"), and the CodePipeline EB deploy ignores `.ebignore`. Never commit large static assets here — that broke every deploy from 2026-10-08 until demos moved to S3.
|
|
89
|
-
- **Git history
|
|
89
|
+
- **Git history was rewritten on 2026-10-09** (`git filter-repo`) to drop ~500 MB of old demo blobs; a clone is now ~39 MB. Any clone made before that date must be deleted and re-cloned — pulling into it brings the old history back. GitHub's `refs/pull/1/head` and `refs/pull/2/head` still point at old commits (GitHub blocks pushes to them).
|
|
90
90
|
- **Removing a top-level folder:** first check no Route 53 name pointing at forward EB has a first label matching it (local directory handler). Checked for the 2026-10-09 removal; `togadesk.beta.agilantsolutions.com` is redirected by `forward.ini` first.
|
package/package.json
CHANGED