toga-ai 1.0.969 → 1.0.971
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/1.0/apps/tools/architecture.md +12 -1
- package/knowledge/1.0/apps/tools/features/central-ids.md +2 -2
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/page-meta-context-field-settings.md +3 -2
- package/knowledge/2.0/apps/_underscore/features/surface-overrides-uuid-rekey.md +5 -4
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +18 -1
- package/knowledge/2.0/apps/saml/features/acs-assertion-trust-gaps.md +38 -1
- package/knowledge/2.0/apps/toga2-commerce/features/view-as-session-tokens.md +3 -1
- package/knowledge/clients/compass-usa/features/cost-centers.md +17 -8
- package/knowledge/clients/compass-usa/features/location-hierarchy-and-sectors.md +5 -2
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: Tools
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-07
|
|
10
10
|
owners: [jcardinal, kyalamarthi, ajean, mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- tools/index.php
|
|
@@ -61,6 +61,16 @@ No per-host `stylePath` subfolder — one `assets/css/style.css`. Uses TOGA Tech
|
|
|
61
61
|
|
|
62
62
|
**All colour comes from a semantic token layer, `assets/css/theme.css`**, registered **first** in `App_FrameworkIndex_Tools::__construct()` so every other sheet resolves through it. The app supports light / dark / auto; the resolved theme is stamped on `<html>` as `data-theme` by an inline pre-paint script and the user's choice lives in `localStorage` (`toga-tools-theme`) — **not** the DB. The left sidebar and public header stay dark in **both** themes, which is why `toga-brandmark-white.png` still works everywhere. Login shows `toga-horizontal-colored.png` in light and `toga-horizontal-white.png` in dark. See `features/theme-light-dark.md`.
|
|
63
63
|
|
|
64
|
+
## UI standard (internal tools)
|
|
65
|
+
|
|
66
|
+
Developer standard (jcardinal, 2026-09): the team works on wide screens, so tool pages use the space.
|
|
67
|
+
|
|
68
|
+
- **Wide-screen first (~1600px+).** Split-pane / master-detail (list left, detail right), one-row toolbars, meta laid out in grids. Never a vertical stack of full-width cards (the first `/emails` page was rejected for this).
|
|
69
|
+
- **Real gaps + padding between cards**, clear hierarchy, easy-to-reach actions. Opt out of the 960px cap with `.tool--wide` (see Gotchas).
|
|
70
|
+
- **Visible progress for anything that takes more than a moment:** a loading state, a real progress bar (N of total, %), and a live count of the work consumed (API calls, rows, budget left). Prefer honest per-step progress over a bare spinner.
|
|
71
|
+
- **Polished controls:** style buttons/inputs with the theme tokens (`assets/css/theme.css`), never default gray buttons.
|
|
72
|
+
- Check it in the browser (light + dark) before showing it.
|
|
73
|
+
|
|
64
74
|
## Home route
|
|
65
75
|
|
|
66
76
|
`/` (`mvc/get.php`) renders a persona-filtered dashboard of available tools (tiles grouped by folder) when logged in, via `App_Nav::visibleGroups()`. When **not** logged in, `/` immediately routes to `/login` (one-step SSO sign-in — no intermediate welcome card). A persona-less but authenticated user gets the dashboard's explicit empty-state (see `features/persona-gated-navigation.md`).
|
|
@@ -93,4 +103,5 @@ Create `mvc/<folder>/<tool>/get.php` (copy `mvc/_TEMPLATE/get.php`), add one ent
|
|
|
93
103
|
- **Framework-level Sentry error reporting is an open gap.** Prod printed "Sentry is not installed" because `sentry/sentry` was missing from `composer.json` (added `^4.10`; sibling apps use `^4.10`/`^4.19`). But even with Sentry in `vendor/`, the 1.0 framework does **not** auto-require `vendor/autoload.php` (the `App_Talos_S3` pattern `require_once`s it explicitly), so framework-level Sentry reporting for Tools still does not load. Any code path that needs a composer package must `require_once` the autoloader itself.
|
|
94
104
|
|
|
95
105
|
## Change history
|
|
106
|
+
- 2026-10-07 — Added "UI standard (internal tools)": wide-screen split-pane layout, real gaps, visible progress + live counts, themed controls. (jcardinal)
|
|
96
107
|
- 2026-09-23 — Added gotchas from the /emails build: input :not chain, 960px cap/.tool--wide, red btn-secondary, Origin/Referer CSRF. (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: Tools
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-08
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- tools/_/app/developers/centralids.php
|
|
@@ -28,7 +28,7 @@ Replaces tracking next central ids in the developer chat. Branches hold hardcode
|
|
|
28
28
|
## How it works
|
|
29
29
|
- Storage: prod `Core.Parameters` (`id`, `uuid` char36 unique, `key` varchar64 unique, `value` varchar255), keys `NEXT_CORE_RECORDS_ID`, `NEXT_CORE_RECORD_FIELDS_ID`. Read via config `[database_toga2core]` (link `db_toga2core`; tools is prod-only).
|
|
30
30
|
- Access: Developers folder, **Development Team** persona only — hardcoded `requireAuth` in `get.php` + `post.php`, narrowed in `nav.php`.
|
|
31
|
-
- UI: one card per key showing stored value + table's current `MAX(id)`; number box + Save; "Take 1" saves shown value + 1.
|
|
31
|
+
- UI: one card per key showing stored value + table's current `MAX(id)`; number box + Save; "Take 1" saves shown value + 1. "Copy for Claude" copies `Next Core.Records.id = N` / `Next Core.RecordFields.id = N` (labels from `KEYS`) from latest loaded/saved state; disabled until load; unset key → "not set"; `navigator.clipboard`, falls back to textarea + `execCommand('copy')` off secure context.
|
|
32
32
|
- `App_Developers_CentralIds`: `all()`, `set($key, $newValue, $expected)`. `post.php` actions `load`, `set`.
|
|
33
33
|
- Save rules: refused if value <= `MAX(id)` of the tracked table or > INT UNSIGNED max. Compare-and-set: `UPDATE ... WHERE value = expected`; missing row → `INSERT IGNORE` with `App_String::generateUuidV4()`. Loser of a race gets "someone changed this value". Table names only from hardcoded `KEYS` const. Each change `error_log`'d with user uuid.
|
|
34
34
|
- First Save creates the Parameters row — no dbchanges2 file needed.
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
| [Shipping Carriers & Carrier Account Numbers (records, matching, duplication, deletion)](features/shipping-carriers-and-accounts.md) | Per-client `ShippingCarriers` / `ShippingCarrierAccountNumbers` model, how the syncs match/create carrier rows, and the duplication + blank-carrier + pagination |
|
|
55
55
|
| [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` static string utilities: use `encodeToAsciiHtmlEntities` for any dynamic text going into a 2.0 email; open when escaping email text or parsing substri |
|
|
56
56
|
| [Surface FIELD ↔ RecordField binding (maxLength / validationType / isWritable fold-in)](features/surface-field-recordfield-binding.md) | Resolver capability that folds `Core.RecordFields` input-contract metadata (maxLength/validationType/isWritable) onto a surface FIELD element; open when a 2.5 s |
|
|
57
|
-
| [
|
|
57
|
+
| [DECLINED 2026-10-08 — re-key Client.SurfaceOverrides from numeric Core ids to uuids](features/surface-overrides-uuid-rekey.md) | **Decision 2026-10-08 (jcardinal): keep numeric ids; no uuid re-key.** The plan below is not scheduled; see the Layer boundary in [surface-resolver](surface-res |
|
|
58
58
|
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | Runtime for the platform Surface UI layer (`_Model_Core_Surface::resolve`/`metaGroup`, replaces `Page::meta`); open for surface resolution, the override cascade |
|
|
59
59
|
| [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | How a 2.0 table-view column renders its value as a clickable link (`hyperlinkRecordFieldId` → ACL field-read → computed URL → render); open when a configured hy |
|
|
60
60
|
| [TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL](features/tableview-joins.md) | How `TableViewJoins` rows become a view's join SQL — aliasing, chained multi-hop joins, ACL gating, and the three ways a view breaks; open before adding/rebasin |
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-08
|
|
10
10
|
owners: [tcox, bala, jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Core/Page.php
|
|
@@ -41,7 +41,7 @@ So a currency/dollar icon is a **type** problem (fix the field's `type` — Core
|
|
|
41
41
|
|
|
42
42
|
**⚠ A CUSTOM record field's label row is UNREACHABLE from the grid header (legacy path only).** `Page.php` emits `ClientCustomRecordFieldSettings` in its own block (~L900–935) and keys it by a record route resolved through `$lookupRecordIdFromCustomRecordFieldId` → `$lookupRecordFromRecordId[...]->route`. For a column pointed at a `CustomRecordFields` row, that resolves to an **empty** record-route key — the row lands at `settings.fields[""][<field>][…]`, a bucket `convertColumnTitles` never reads. **A `ClientCustomRecordFieldSettings` `label-singular` row for a custom column is therefore dead metadata**, and no amount of adding/correcting those rows moves the header. (Measured 2026-08-26: two hours lost adding label rows that could never be read.)
|
|
43
43
|
|
|
44
|
-
> **⚠
|
|
44
|
+
> **⚠ CORRECTION (2026-10-08): `/table-views/meta` does NOT carry labels.** `_Model_Client_TableView::meta()` reads the settings tables only to decide the context columns (`settings.contextTable` / `contextField`); it returns no label values. Grid header text comes from `/pages/meta` `settings.fields[<record route>][<field>]['label-singular']`. An earlier version of this note said `table-views/meta` fetches `ClientCustomRecordFieldSettings` and labels base custom-field columns directly. That is wrong. The Elite / NYCHH custom-column headers (2026-09-10 seeds, e.g. `Client_Nychh/2026-09-10c - TransferLocationColumnLabel.sql`) were seen live, but the label path that served them is not proven; re-check against `Page::meta` before relying on a client custom-field settings row for a header. Boundary for new apps: see "Layer boundary (2026-10-08)" in [surface-resolver](surface-resolver.md).
|
|
45
45
|
|
|
46
46
|
Where the label must come from instead, for such a column:
|
|
47
47
|
- **`Core.DefaultRecordFieldSettings`** on the underlying **Core** `RecordField`, or
|
|
@@ -103,3 +103,4 @@ Pages --< PageRecords >-- Records --< RecordFields
|
|
|
103
103
|
|
|
104
104
|
## Change history
|
|
105
105
|
- 2026-09-24 — Documented the Core-only label chain (`Pages`→`PageRecords`→`RecordFields`→`DefaultRecordFieldSettings` `label-singular`, setting id 5) and that `Sections` are a separate path listing pages never have. (apeterson)
|
|
106
|
+
- 2026-10-08 — Corrected: `/table-views/meta` returns no labels (header text = `/pages/meta` `settings.fields`). Frozen for old apps per the Layer boundary in surface-resolver (jcardinal)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "
|
|
2
|
+
title: "DECLINED 2026-10-08 — re-key Client.SurfaceOverrides from numeric Core ids to uuids"
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
repo: _underscore
|
|
5
5
|
project: _Underscore
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
|
-
status:
|
|
9
|
-
updated: 2026-
|
|
8
|
+
status: deprecated
|
|
9
|
+
updated: 2026-10-08
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Core/Surface.php
|
|
@@ -16,7 +16,7 @@ related:
|
|
|
16
16
|
- surface-resolver.md
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
**Decision 2026-10-08 (jcardinal): keep numeric ids; no uuid re-key.** The plan below is not scheduled; see the Layer boundary in [surface-resolver](surface-resolver.md). Was: `Client.SurfaceOverrides.surfaceId`/`surfaceElementId` still point at Core rows by numeric id; open before a client's first override rows on an env whose Core can be rebuilt.
|
|
20
20
|
|
|
21
21
|
## Summary
|
|
22
22
|
|
|
@@ -35,3 +35,4 @@ Planned fix (do before first client use on such an env):
|
|
|
35
35
|
|
|
36
36
|
## Change history
|
|
37
37
|
- 2026-09-29 — Recorded as deferred after the nav-gate uuid fix exposed the same numeric-id risk (jcardinal)
|
|
38
|
+
- 2026-10-08 — Declined: keep numeric ids, no uuid re-key (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-08
|
|
10
10
|
owners: [jcardinal, apeterson, bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/Language.php
|
|
@@ -313,6 +313,20 @@ Same discipline as the reserved-id blocks: **read the id out of the target envir
|
|
|
313
313
|
- **The meta-group dispatch grant is per-client-DB too.** A 403 EZ-1 on `meta-group` on dev-sandbox was the same mechanism as the original `meta` 403: the `meta-group` RecordScript needs its own per-role `Client.AclRecordScripts` dispatch grant. Verified via toga-db — `meta` script id 25 was granted roles 1/3/4 but `meta-group` (id 27) had **none** until the grant ran. Any new surfaces scripted-API method repeats this: record READ never authorizes dispatch.
|
|
314
314
|
- **The meta-group grant is seeded per-tenant, not globally — new tenants repeat the 403.** When `meta-group` shipped, the `AclRecordScripts` grant was seeded **only** for `Client_Compass` (recordScriptId → roles 1,3,4). `Client_Quad` and `Client_CompassCanada` had **no** grant for the meta-group recordScriptId at all — their `meta` was granted to client role 1 (why `meta` worked but `meta-group` 403'd for the same user/token). Same-user-different-script + same-script-different-tenant is the fingerprint of this gap. Fix (2026-07-01): two `dbchanges2/Client_<tenant>/` migrations granting meta-group to **client role 1** (parity with each tenant's existing `meta` grant), id-agnostic (resolve `Core.RecordScripts` id by route/method/phpMethod, cross-DB read of `Core.*` per the 2026-06-29c ACL pattern) and re-runnable via `NOT EXISTS`. Surface-layer DB changes ship to the **beta** environment only. ⚠ **Root-cause not yet closed:** the meta-group grant should be added to the `dbchanges2/Client/` **baseline (blank) seed** so newly-cut clients get it automatically — otherwise the next migrated tenant hits the identical 403. Only Compass (already correct), Quad, and CompassCanada are addressed. Also note **role breadth**: Compass exposes surfaces to roles 1,3,4; Quad/CC got role 1 only (parity with their `meta`). If Quad/CC should match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
|
|
315
315
|
|
|
316
|
+
## Layer boundary (2026-10-08)
|
|
317
|
+
|
|
318
|
+
Developer decision (jcardinal, 2026-10-08), platform apps (Desk, IQ, template). Which layer owns what:
|
|
319
|
+
|
|
320
|
+
1. **Surface owns what users SEE.** `Core.Surfaces` / `SurfaceElements` / `Messages` + Client `MessageTranslations` + Client `SurfaceOverrides`: copy, labels, order, visibility, required, icons, colours. Client changes = `SurfaceOverrides` (client / persona / role); translations = `MessageTranslations`.
|
|
321
|
+
2. **`*Settings` keeps ONLY preferences:** `language`, `date-format`, `time-format`, `time-zone`, `currency`, `persona`. One shared preferences resolver (to move from api2 `V2::resolveSettingValue` into `_underscore`), honouring `isOverridable`. UI keys are FROZEN, new apps never read them: `label`, `label-singular`, `icon`, `tooltip`, `placeholder`, `sort-order`, `is-required`, `default-value`, `min-required-siblings`, `max-required-siblings`.
|
|
322
|
+
3. **Record + field names move to `Core.Messages`** (translatable) via message-id link columns on `Core.Records` / `Core.RecordFields`, first seeded from `DefaultRecordSettings` / `DefaultRecordFieldSettings` labels; until seeded, fall back to the settings label.
|
|
323
|
+
4. **No per-user text overrides.** User choices = preferences, saved views, column order. Add a Surface lock only when a real need appears.
|
|
324
|
+
5. **`SurfaceOverrides` keep numeric Core ids** in client DBs (no uuid re-key) - see [surface-overrides-uuid-rekey](surface-overrides-uuid-rekey.md).
|
|
325
|
+
6. **Permissions** (record CRUD + field writable) get their own once-per-session call for new apps instead of `GET /pages/meta`. Shape TBD; table-based preferred; a scripted API needs the developer's OK.
|
|
326
|
+
7. **Old apps** (`toga2-supply`, `toga2-commerce`) keep `GET /pages/meta` frozen: bug + security fixes only. `toga25-supply` (uses both) moves with the platform apps.
|
|
327
|
+
|
|
328
|
+
Precedence is per layer and never mixed on one value: Surface = base < client < persona < role (language = filter); Settings = Default < Client < Persona < User (< Language where the reader applies it). Rejected: settings absorbs Surface; keep both + a rule. Detail: Desk ADR 0140 (Settings holds preferences only).
|
|
329
|
+
|
|
316
330
|
## Related
|
|
317
331
|
|
|
318
332
|
- [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md)
|
|
@@ -322,3 +336,6 @@ Same discipline as the reserved-id blocks: **read the id out of the target envir
|
|
|
322
336
|
- [sales-order-status-filter-surface](sales-order-status-filter-surface.md)
|
|
323
337
|
- [surface-field-recordfield-binding](surface-field-recordfield-binding.md)
|
|
324
338
|
- [side-navigation-and-default-route](../../toga25-supply/features/side-navigation-and-default-route.md)
|
|
339
|
+
|
|
340
|
+
## Change history
|
|
341
|
+
- 2026-10-08 — Added "Layer boundary": Surface owns what users see, `*Settings` = preferences only (jcardinal decision; see Desk ADR 0140) (jcardinal)
|
|
@@ -6,12 +6,14 @@ project: SAML SSO Gateway
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-07
|
|
10
10
|
owners: ["rgirish", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- saml/Controller/Index.php
|
|
13
13
|
- _underscore/String.php
|
|
14
14
|
- _underscore/Model/True/ClientAuthentication.php
|
|
15
|
+
- _underscore/Model/Compass/ClientAuthentication.php
|
|
16
|
+
- _underscore/Model/Core/ClientAuthentication.php
|
|
15
17
|
related:
|
|
16
18
|
- ../architecture.md
|
|
17
19
|
- ./gateway-environments-and-redirect-safety.md
|
|
@@ -78,6 +80,40 @@ the window. App-side fix: a used-payload table. Gateway-side fix: nonce + iat +
|
|
|
78
80
|
payload. Changing `encryptWithKey()` affects every caller — it is also
|
|
79
81
|
the `?saml=` handoff and the api2 encrypted-user-uuid auth, so this is not a local change.
|
|
80
82
|
|
|
83
|
+
## Planned fix (deferred 2026-09-25)
|
|
84
|
+
|
|
85
|
+
Proposed, **not approved and not built** — deferred by the developer (jcardinal) as too large for
|
|
86
|
+
that moment. Needs a `cto` pass (schema) and a `cso` review before any code.
|
|
87
|
+
|
|
88
|
+
**Rollout — per client, LOG_ONLY then ENFORCE:**
|
|
89
|
+
|
|
90
|
+
1. Add to `Core.ClientAuthentications_Sso`: `idpEntityIdentifier`, `idpSigningCertificate`,
|
|
91
|
+
`signatureMode` ENUM(`LOG_ONLY`, `ENFORCE`).
|
|
92
|
+
2. Ship every SSO client in `LOG_ONLY` (verify the IdP signature, log the result, still allow).
|
|
93
|
+
3. Flip a client to `ENFORCE` after one real login passes verification. No cert on file = refuse.
|
|
94
|
+
|
|
95
|
+
**Turn on for everyone at once (no per-client stage):** reject unsigned assertions; check the
|
|
96
|
+
assertion time window; Audience = our `/meta`; Destination = our `/acs`; RelayState expiry; bind
|
|
97
|
+
RelayState's `domainUuid` to the SSO client (today `_underscore/Model/Core/ClientAuthentication.php`
|
|
98
|
+
accepts any `domainUuid`).
|
|
99
|
+
|
|
100
|
+
**Remove now-known unsafe code in the same change:**
|
|
101
|
+
|
|
102
|
+
- **Compass backdoor:** `_underscore/Model/Compass/ClientAuthentication.php` (~lines 17-20) maps any
|
|
103
|
+
`@togatech.com` NameID straight to one fixed internal user. Combined with Gap 1, any trusted IdP can
|
|
104
|
+
sign in as that user. Remove it.
|
|
105
|
+
- A comment near line ~235 of `saml/Controller/Index.php` holds a **real customer SAMLResponse**
|
|
106
|
+
(personal data). Delete it; never copy its content anywhere.
|
|
107
|
+
|
|
108
|
+
**Inputs still needed (IdP metadata / signing certs):** Compass (Shibboleth IdP — the old commented
|
|
109
|
+
code carries a cert valid to 2031), Rate (Okta metadata URLs — today stored in `entityIdentifier`),
|
|
110
|
+
True + Compass Sandbox Client (Microsoft Entra — app metadata URL needed from the developer).
|
|
111
|
+
`entityIdentifier` is used two ways today (our SP id for Compass/True, the IdP metadata URL for Rate)
|
|
112
|
+
— settle that before adding the new columns.
|
|
113
|
+
|
|
114
|
+
Prod SSO clients in scope (`Core.ClientAuthentications`, type SSO): Compass Group (Production,
|
|
115
|
+
Sandbox Client), CompassCanada, Rate (Production, Beta), True.
|
|
116
|
+
|
|
81
117
|
## Gotchas
|
|
82
118
|
|
|
83
119
|
- **Do not describe the gateway as "authenticating" anyone to a client or an auditor.** Until Gap 1
|
|
@@ -89,5 +125,6 @@ the `?saml=` handoff and the api2 encrypted-user-uuid auth, so this is not a loc
|
|
|
89
125
|
citing this doc as sign-off.
|
|
90
126
|
|
|
91
127
|
## Change history
|
|
128
|
+
- 2026-10-07 — Added "Planned fix (deferred 2026-09-25)": per-client LOG_ONLY → ENFORCE signature rollout, IdP metadata still needed, Compass `@togatech.com` backdoor + stored SAMLResponse comment to remove (jcardinal)
|
|
92
129
|
- 2026-10-02 — Added Tools + TOGa Desk as affected consumers; recorded handoff replay (Gap 3) (mhammontree)
|
|
93
130
|
- 2026-09-25 — Created; recorded that `/acs` verifies no IdP signature and no Audience/Destination/InResponseTo/NotBefore/NotOnOrAfter, and that RelayState uses unauthenticated AES-256-CBC with an attacker-supplied IV (rgirish)
|
|
@@ -6,7 +6,7 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-07
|
|
10
10
|
owners: [tcox, bala, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- src/api/sessionTokens.ts
|
|
@@ -96,11 +96,13 @@ Seeds a long-lived `ADMIN_ACCESS_JWT`; stubs `/auth/delegator` → `mock-delegat
|
|
|
96
96
|
|
|
97
97
|
- On a 401 logout the in-memory zustand stores are not reset, so a store write in the ≤1.5s before the redirect can write `zu-cart` / `order-for-zu` back. Low risk: login runs `resetClientState` (cso NIT, 2026-09-30).
|
|
98
98
|
- `SENTRY_AUTH_TOKEN` is committed in 25 tracked `.env*` files — treat it as compromised and rotate it. (Never store the value in this KB.)
|
|
99
|
+
- **`GET /users/encryptedUser` permission gap (found 2026-09-25, deferred).** The RecordScript (`Core.RecordScripts` id 16; `_underscore/Model/Client/User.php` ~line 353, `@TODO` with no check) was granted in prod to ordinary roles across many clients (e.g. Compass Base + Manager), likely from `dbchanges2/Core/HISTORIC/2025/2025-01-13 - 55324 - GetEncryptedUser.sql`. Fix = a real permission check in code, not only pulling grants. The `canUserOrderForOtherUser` gate (Gotchas) covers it on beta / client-sandbox — confirm it is live in prod before closing this item.
|
|
99
100
|
- ~~Confirm api2 authorizes `/auth/delegator` **server-side**~~ — **answered 2026-09-21:** it now re-checks `_Model_Client_User::canUserOrderForOtherUser` at mint time (and `/auth/refresh` re-checks it), so a captured encrypted pair cannot be replayed. See [order-for delegation](../../_underscore/features/order-for-delegation.md).
|
|
100
101
|
- ~~The parked-token slot is shared with "Order For" — decide precedence~~ — **settled 2026-09-22:** one slot, no nesting; the mode is told apart by the Order For marker, not the slot.
|
|
101
102
|
|
|
102
103
|
## Change history
|
|
103
104
|
|
|
105
|
+
- 2026-10-07 — Open item: `/users/encryptedUser` permission gap found 2026-09-25 (prod grants to ordinary roles); confirm the delegation gate is live in prod. (jcardinal)
|
|
104
106
|
- 2026-09-30 — Order For merged onto the TRUE-81991 session layer on `_sandbox-client`: parked pair cleared via `AUTH_STORAGE_KEYS`; return and switch use `clearPersistedQueryCache`; a refresh that lost the identity race now returns the current token instead of `null` (stopped a false logout on return); View As `onError` restores the admin's language and logs out if the parked pair is gone. (tcox)
|
|
105
107
|
- 2026-09-22 — Refresh identity binding upgraded to an epoch bumped on the view-as swap; `Bearer undefined` swap bug fixed. TRUE-81991 (`e546db3d`). (apeterson)
|
|
106
108
|
- 2026-09-22 — Return moved to the shared `useReturnToOriginalUser` hook (state cleared before any await); mode now tested with `useIsViewAsActive` because Order For parks the same user; View As start calls `endDelegation()` and its cart-clear effect is gated; recorded the 403 EZ-1 when the target is not on the delegation list. (tcox)
|
|
@@ -5,10 +5,11 @@ project: _Underscore
|
|
|
5
5
|
client: compass-usa
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-10-
|
|
9
|
-
owners: [bala, ajean]
|
|
8
|
+
updated: 2026-10-08
|
|
9
|
+
owners: [bala, ajean, rgirish]
|
|
10
10
|
files:
|
|
11
11
|
- toga2-commerce/src/pages/Cart/api/CartApi.ts
|
|
12
|
+
- toga2-commerce/src/pages/Cart/viewModel/useCartViewModel.ts
|
|
12
13
|
- worker1.5/crons/toga2/compass/import_locations.php
|
|
13
14
|
- _underscore/Model/Compass/SalesOrder.php
|
|
14
15
|
- api2/Component/Api/V2/V2.php
|
|
@@ -33,17 +34,20 @@ US Compass only (`Client_Compass`). Compass Canada is explicitly out of scope. S
|
|
|
33
34
|
|
|
34
35
|
### What a cost center actually is
|
|
35
36
|
- A cost center = a **Unit Location** in `Client_Compass.Locations` (`locationTypeId = 1`), keyed by custom field `c_erpSystemEntityId`.
|
|
36
|
-
- `c_erpEntityId` is a **different** field
|
|
37
|
+
- `Locations.c_erpEntityId` is a **different** field: the full `"<entity>-<CC>"` unit id (e.g. `1001-20180`). At checkout only the **CC number** (e.g. `20180`) is written to `SalesOrders.c_erpEntityId` as a **plain string, not a foreign key** — the entity (`1001` / `1019`) is not stored. `_underscore/Model/Compass/SalesOrder.php` exposes a `_costCenter` computed field.
|
|
38
|
+
- Entity `1001` = US cost centers. Entity `1019` rows look like old / reused CCs (feed active flag `U`, not `Y`).
|
|
37
39
|
- The `CostCenters` table is empty for Compass; nothing reads `Users_CostCenters` / `Locations_CostCenters` / `SalesOrders.costCenterId` / `Users.defaultCostCenterId`.
|
|
38
40
|
|
|
39
41
|
### The commerce dropdown
|
|
40
|
-
- Built by `fetchCostCenter()` in `toga2-commerce/src/pages/Cart/api/CartApi.ts`, which calls `GET /locations` filtered on `c_erpSystemEntityId LIKE %search% AND isActive = 1`,
|
|
42
|
+
- Built by `fetchCostCenter()` in `toga2-commerce/src/pages/Cart/api/CartApi.ts`, which calls `GET /locations` filtered on `c_erpSystemEntityId LIKE %search% AND isActive = 1`, fields `c_erpSystemEntityId,name`, distinct (exact `=` match on the display path).
|
|
43
|
+
- `useCartViewModel.ts` (~426-460) labels options `"<CC> - <name>"` and dedupes by `c_erpSystemEntityId` — **first row wins**. So when one CC number exists under several entities, the shown name can belong to the wrong entity.
|
|
41
44
|
- Funnels both the cart checkout and the New-User form, so a single filter in `fetchCostCenter` covers both entry points.
|
|
42
45
|
- **api2 does no cost-center validation** — the FE is the only selection gate.
|
|
43
46
|
|
|
44
47
|
### Where the values come from (import)
|
|
45
|
-
- `worker1.5/crons/toga2/compass/import_locations.php` reads
|
|
46
|
-
-
|
|
48
|
+
- `worker1.5/crons/toga2/compass/import_locations.php` (1.0, daily) reads pipe-delimited `UNIT*.txt` from SFTP. Col 0 = `<entity>-<CC>` → `c_erpEntityId`; col 16 = CC → `c_erpSystemEntityId`; cols 17-20 = address; col 23 = active flag (`Y` → 1, else 0).
|
|
49
|
+
- Units are looked up by `c_erpEntityId` with an **INNER JOIN** to `Locations_Addresses` / `Addresses` on `primaryLocationAddressId` (~line 210-220). A unit with no address is never found, so a **new `Locations` row is inserted every run** — the source of the duplicates (see gotcha).
|
|
50
|
+
- The update branch sets a new `uuid` on every run, so Location uuids are not stable.
|
|
47
51
|
|
|
48
52
|
### Numeric-only enforcement (2026-07-06) — three layers
|
|
49
53
|
1. **Front-end selection block** — in `fetchCostCenter`, on the **search path** (`useSearchOptions === true`), returned locations are filtered to numeric-only with `/^[0-9]+$/` on `c_erpSystemEntityId.trim()`. The non-search / display path is left untouched (an already-saved value still renders). Done client-side because api2 rejects a server-side numeric filter (see gotcha) and api2 was out of scope.
|
|
@@ -61,12 +65,15 @@ Deactivation proven safe against prod data:
|
|
|
61
65
|
- Proven end-to-end in dev-sandbox: after the migration, active non-numeric rows = **0** (all 1,185 codes incl. the 725 number+alphabet combos) while pure-numeric stayed untouched at **440,005** active rows.
|
|
62
66
|
|
|
63
67
|
## Gotchas
|
|
68
|
+
- **⚠ "TechHub shows the wrong location name for CC X" is display-only.** The CC number is shared by more than one entity (176 numeric CCs among active units, prod 2026-10-08); the dropdown keeps the first row. The order stores only the CC number, so the order data is correct. Example: `20180` → `1001-20180` "General Overhead" (id 58192, RA B&I sector) vs `1019-20180` "Williamsburg Pottery Main" (548 active rows). Quick fix per CC: set `isActive = 0` on the stale entity's rows — check `Users.locationId` first (`1019-20180` had 0 users).
|
|
69
|
+
- **⚠ BUG (found 2026-10-08, NOT fixed) — the import duplicates units with no address, forever.** Cause: the INNER JOIN unit lookup above. Prod 2026-10-08: **874,887** active unit rows with no address; many units have ~1,200 rows (e.g. `1019-20180`: 1,038 rows, 859 active). Duplicates made before the active-flag logic stay active forever — the cron can never find them to deactivate. Fix: LEFT JOIN the unit lookup, then clean up the duplicates (soft delete, `isActive = 0`).
|
|
70
|
+
- **⚠ SECURITY — `import_locations.php` has the `cgsftp` SFTP password hardcoded in source.** Move it to config and rotate it. Never copy the value anywhere.
|
|
64
71
|
- **⚠ A cost-center number is NOT unique across sectors — never match on the number alone.** Scope
|
|
65
72
|
every match to a sector / hierarchy path. Measured 2026-10-05: 10 of the client's 634 "corporate"
|
|
66
73
|
cost centers also exist under other sectors — `4018` / `45781` / `62937` under B&I Overhead
|
|
67
74
|
(`M00000`), `22772` / `26963` / `28184` / `54188` under Corporate Income (`H00000`), `47258` /
|
|
68
75
|
`62230` under CCL Hospitality (`Y00000`), `31335` under ESFM (`W00000`). Three carry real users
|
|
69
|
-
(`4018` → 10, `31335` → 9, `47258` → 9), so the wrong match hits live people. See
|
|
76
|
+
(`4018` → 10, `31335` → 9, `47258` → 9), so the wrong match hits live people. Same number can also repeat across entities (`1001` / `1019`) — see the display gotcha above. See
|
|
70
77
|
[Location Hierarchy & Sectors](./location-hierarchy-and-sectors.md) for the walk and the
|
|
71
78
|
dedupe-before-counting recipe.
|
|
72
79
|
- **⚠ `Users.c_erpSystemEntityId` has a DOUBLE meaning — never overwrite it to persist a cost-center change.** In `worker2/Worker/Client/Compass/PeopleFile.php` it is (a) copied straight into `c_hrEmpDirectoryKey` and treated as **unique per employee** (`:544-546`, e.g. `"1001-1003"`), and (b) used as the **primary key for resolving `Users.locationId`** (`:566-571`). Writing a location code into it corrupts employee identity to buy one night of persistence. This is why the [Tools Compass admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) writes only `Users.locationId` + `Users.c_erpEntityId` and accepts that a cost-center edit is **undone that night** for anyone in the PEOPLE file.
|
|
@@ -74,7 +81,6 @@ Deactivation proven safe against prod data:
|
|
|
74
81
|
- **⚠ There is no usable index for a cost-center search, so never wire one to a type-ahead.** `Client_Compass` has ~909,700 active `locationTypeId = 1` (Unit) rows carrying only ~36,352 distinct `c_erpSystemEntityId` values (~730 duplicate rows per code); **442,549** of the active rows have a numeric short code. Per the `dbchanges2` schema the only key on `Locations` is `locationTypeId`, which covers nearly the whole table — so there is no index on `c_erpSystemEntityId`, on `isActive`, or any composite. Every cost-center search reads the table then groups. **Recommended follow-up (not done): index `Locations (locationTypeId, isActive, c_erpSystemEntityId)` in BOTH Compass tenants** (dbchanges2).
|
|
75
82
|
- **⚠ BUG (found 2026-09-04, NOT fixed) — `_Model_Compass_SalesOrder::_costCenter()` joins the wrong column.** `_underscore/Model/Compass/SalesOrder.php:287-305` does `INNER JOIN Contacts ON Contacts.id = Users.id`; it should be `Users.contactId`. The user-level cost-center fallback therefore resolves an **unrelated user's** contact. Needs an `_underscore` change.
|
|
76
83
|
- **api2 cannot do a server-side numeric filter.** The V2 query-string where-parser (`api2/Component/Api/V2/V2.php` → `parseOptionsWhere`) whitelists operators and **rejects `regexp` with a 500**. Any numeric/regex filtering must happen client-side (or via a new operator in api2, which was out of scope here).
|
|
77
|
-
- **Two fields, easy to swap.** `c_erpSystemEntityId` is the cost center (what users pick); `c_erpEntityId` is the unit id and the value orders record on `SalesOrders.c_erpEntityId`.
|
|
78
84
|
- **Second, possibly-live copy of the import cron not yet guarded.** A second copy exists at `worker/crons/toga2/compass/DOA_Scripts/import_locations.php` (appears disabled / "DOA"). It did **not** receive the numeric guard — confirm whether it is live before trusting the guard to be complete.
|
|
79
85
|
|
|
80
86
|
## Related
|
|
@@ -83,3 +89,6 @@ Deactivation proven safe against prod data:
|
|
|
83
89
|
- [Cart page config architecture](../../../2.0/apps/toga2-commerce/features/cart-page-config-architecture.md)
|
|
84
90
|
- [Tools Compass user/persona admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md)
|
|
85
91
|
- [PEOPLE-file user lifecycle](./people-file-user-lifecycle.md)
|
|
92
|
+
|
|
93
|
+
## Change history
|
|
94
|
+
- 2026-10-08 — Documented dropdown first-row-wins dedupe (wrong name = display only), entity 1001 vs 1019, the INNER JOIN import bug that inserts duplicate address-less units every run, uuid churn, and the hardcoded SFTP credential. (rgirish)
|
|
@@ -5,8 +5,8 @@ project: _Underscore
|
|
|
5
5
|
client: compass-usa
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-10-
|
|
9
|
-
owners: [bala]
|
|
8
|
+
updated: 2026-10-08
|
|
9
|
+
owners: [bala, rgirish]
|
|
10
10
|
files:
|
|
11
11
|
- worker/crons/toga2/compass/DOA_Scripts/import_locations.php
|
|
12
12
|
- worker1.5/crons/toga2/compass/import_locations.php
|
|
@@ -69,6 +69,8 @@ not re-derive it. Ids are set in `import_locations.php:37-45`.
|
|
|
69
69
|
| 16 | cost center entity id → `Locations.c_erpSystemEntityId` |
|
|
70
70
|
| 17-20 | street / city / state / zip |
|
|
71
71
|
|
|
72
|
+
The live daily copy, `worker1.5/crons/toga2/compass/import_locations.php`, also reads **col 23 = unit active flag** (`Y` → active). Its duplicate-unit bug is in [Cost Centers](./cost-centers.md).
|
|
73
|
+
|
|
72
74
|
Because new corporate cost centers arrive on this feed on their own, **anything scoped to "corporate"
|
|
73
75
|
must key off the hierarchy, never a client-maintained spreadsheet.**
|
|
74
76
|
|
|
@@ -120,6 +122,7 @@ All of them are flagged Active and all have **zero users**. Drop them before any
|
|
|
120
122
|
- [Effective persona resolution](../../../2.0/apps/_underscore/features/effective-persona-resolution.md)
|
|
121
123
|
|
|
122
124
|
## Change history
|
|
125
|
+
- 2026-10-08 — Noted the live worker1.5 import copy reads col 23 as the active flag. (rgirish)
|
|
123
126
|
- 2026-10-05 — Created from the field-laptop research. Recorded that corporate is identified only by
|
|
124
127
|
sector `J00000` (`Locations.id = 52`) and the ERP-id letter prefix (no flag, no column in the
|
|
125
128
|
21-column feed), that Levy's corporate office is the **district** `K9902` not a sector, that the
|
package/package.json
CHANGED