toga-ai 1.0.856 → 1.0.857
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.
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: session
|
|
3
|
+
slug: order-for-delegation
|
|
4
|
+
title: Compass Order For — server-side delegation gate, originalUserId audit, delegation-list GET
|
|
5
|
+
author: bala
|
|
6
|
+
repos: [_underscore, api2, dbchanges2, toga2-commerce]
|
|
7
|
+
framework: "2.0"
|
|
8
|
+
client: compass-usa
|
|
9
|
+
status: active
|
|
10
|
+
created: 2026-09-21
|
|
11
|
+
updated: 2026-09-21
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Session: order-for-delegation
|
|
15
|
+
**Date:** 2026-09-21
|
|
16
|
+
**Project/Repo:** _underscore, api2, dbchanges2, toga2-commerce (2.0)
|
|
17
|
+
**Task:** Build the server side of Compass "Order For" (TRUE-81848) so user A can place an order for user B only when a `Users_DelegatorUsers` row allows it, record who really signed in via `Logs_<Client>.Api.originalUserId`, and hand the front end a delegation-list GET call. Covers US Compass and Compass Canada.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What WORKED
|
|
22
|
+
|
|
23
|
+
- **`Users_DelegatorUsers` junction table in every `Client_*` DB.** `dbchanges2/Client/2026-09-18a - UsersDelegatorUsersTable.sql`. Columns `id, uuid, userId, delegatorUserId`, unique on the pair. Read as "userId may order for delegatorUserId". Model `_underscore/Model/Client/Users/DelegatorUser.php`. Applied to all 12 local `Client_*` schemas; confirmed present on dev-sandbox `Client_Compass` and `Client_CompassCanada`.
|
|
24
|
+
- **Core metadata.** `dbchanges2/Core/2026-09-18a - UserDelegatorUserRecords.sql`. Record 362, route `user-delegator-users`, `aclDatabase = CLIENT`, RecordFields 2604 id / 2605 uuid / 2606 userId / 2607 delegatorUserId, plus an `InherentRecordChildren` row under record 2 (Users) so `userDelegatorUsers` reads nested under `/users/{uuid}`.
|
|
25
|
+
- **The gate, checked twice.** `_Model_Client_User::canUserOrderForOtherUser()` and `getRequestingUser()` called from `encryptedUser()`, then re-checked in `api2/Component/Api/V2/V2.php` when `/auth/delegator` issues the token, and again on `/auth/refresh`. Evidence: local suite of 27 cases passed — listed target allowed, self allowed, unlisted refused, cross-list refused, chain escalation blocked (Dev Team acting as Gina cannot reach Kathy who is on Gina's list), replayed pairs refused, forged/garbage/missing bearers refused, refresh honours a deleted row and a deactivated account.
|
|
26
|
+
- **`originalUserId` audit column.** `dbchanges2/Logs_Client/2026-09-18a - ApiOriginalUserId.sql` + `_Model_Client_Logs_Api` + three write sites in `V2.php`. Evidence on local, order **SA136321**: `Client_Compass.SalesOrders` id 118454 `createdByUserId = 124256` (Dawn Anderson); `Logs_Compass.Api` row 3618506 `POST /v2/sales-orders` 201 with `userId 124256 / originalUserId 127046` (Dev Team). The switch itself is visible at 13:10:23 — `GET /v2/users/encryptedUser` 200 as 127046, then `POST /v2/auth/delegator` 201 and every row after that is 124256/127046. Across the last 200 rows: 126 own-token rows with NULL, then 70 delegated rows. `SELECT COUNT(*) FROM Logs_Compass.Api WHERE originalUserId = userId` returns 0.
|
|
27
|
+
- **`fetchDelegationList` + `DelegationOption`** in `toga2-commerce/src/pages/Cart/api/CartApi.ts` and `src/pages/Cart/types.ts`. `npx tsc --noEmit` clean. Tanner has since wired it into `useOrderForDropdownViewModel.ts` on `_feature-OrderForDropDown`.
|
|
28
|
+
- **Caller-scoped ACL for both tenants.** `dbchanges2/Client_Compass/2026-09-18a` and `Client_CompassCanada/2026-09-18a`. Read only. US roles 7 Compass Base / 8 Manager; **Canada roles 8 Compass Base / 9 Manager**; `appId 5` = TOGa Commerce. Expression is `CASE WHEN Users_DelegatorUsers.userId = AclUser.id THEN 1 ELSE 0 END`, slug `user-logged-in`. Evidence: with `AclUser.id = 127046` the three Dev Team rows return `isPermitted 1`; with `124256` the same rows return `0`.
|
|
29
|
+
- **Canada `encryptedUser` grant.** Folded into the Canada file as section 4: `Core.RecordScripts` id 16 granted to Canada roles 8 and 9. Canada had only role 1 (Base), so a normal Canada user got a 403 on every pick. US already had it (prod rows 16/30/13). Applied twice locally, no duplicates.
|
|
30
|
+
- **Dev-sandbox twin files** for the three metadata files, field ids **2767 id / 2768 uuid / 2769 userId / 2770 delegatorUserId**: `Core/2026-09-21a - UserDelegatorUserRecordsDevSandbox.sql`, `Client_Compass/2026-09-21a - …DevSandbox.sql`, `Client_CompassCanada/2026-09-21a - …DevSandbox.sql`. Verified by reproducing the dev-sandbox starting state inside a local transaction, running each file twice, then `ROLLBACK`.
|
|
31
|
+
- **Local dbchanges catch-up.** Diffed every local table and column against prod and closed the gaps: `Logs_*` `Email` + `EmailAttachment`, core `Logs` `Issue`/`Event`/`Issue*`, `Client/2026-09-11a` (`SalesOrderItems.fulfillmentType`), `2026-08-28b`, `2026-08-28f`, `2026-09-01b`, `2026-09-01c`, `2026-09-14a`, `_modules/netsuite/2026-09-11a`. `Client_Compass` and `Client_CompassCanada` now match prod column for column.
|
|
32
|
+
- **`/order-for` team skill**, pushed as `d4751825`. Knowledge captured as `0960de3b` (1 new doc + 8 updates).
|
|
33
|
+
|
|
34
|
+
## What did NOT work — DO NOT RETRY THESE
|
|
35
|
+
|
|
36
|
+
- **`empty()` / `isset()` on a `_Model` field.** `$isLoaded = (is_object($api->user) && !empty($api->user->id))` is **always false**. `_Model::initialize()` (`_underscore/Model.php:151`) runs `unset()` on every declared field and `_Model` has no `__isset`, so PHP short-circuits before `__get` runs. This made the gate 403 **everyone**, including a user acting as themself. `php -l` and both reviewers passed it; only running the endpoint found it. Use `(int)$x->id > 0` or `!== null`.
|
|
37
|
+
- **An ACL permission with no logic group.** `allowRead = 1` alone grants nothing — `buildSqlExpression()` returns `'(0)'`. Symptom was a 403 naming `aclRecordPermissionIds: ["1050"]` with `userDelegatorUsers: [null, null, null]`. You need all three rows: `AclRecordExpressions`, `AclLogicGroups`, `AclLogicGroupExpressions`.
|
|
38
|
+
- **Putting the ACL for record 362 in Core.** Record 362 is `aclDatabase = CLIENT`, and every lookup in `V2.php` (3236, 4057, 6502, 7307) switches on that and reads only the matching bucket. The Core rows are never evaluated. They are kept in the file only so Core matches across environments.
|
|
39
|
+
- **`$isDelegationPermitted = true` as the default** in the `/auth/delegator` path. A forged or expired bearer with no `id`, or an already-used `transactionId`, left `$this->delegator` null with no status set, and the fall-through minted a token. Caught in security review. Fixed with `$isDelegatorAuthRequest = false` set before the switch and `true` as the first statement of `case 'delegator'`.
|
|
40
|
+
- **`sqlExpression = '1'` on record 362.** `user-delegator-users` is a real top-level route, so any Compass user could `GET /v2/user-delegator-users` and list who every colleague can order for. Valid CodeRabbit finding.
|
|
41
|
+
- **Reusing the prod pinned uuids in the dev-sandbox files.** Failed with `ERROR 1062 (23000): Duplicate entry 'afb16e5b-f77c-4fe5-94d1-be8ed683e1f5' for key 'aclfieldpermissions.uuid'`. The `AclFieldPermissions` rows are guarded on `recordFieldId`, which differs per environment, so the guard passes and the unique uuid key fails. Only those 9 rows got new uuids; rows keyed the same across environments kept theirs.
|
|
42
|
+
- **Running `Core/2026-09-18a` on dev-sandbox.** RecordFields 2604-2607 there belong to record 344 (`ItemCategoryFeatureGroupTranslations`), so the file dies on a duplicate key at its first insert and **everything after section 2 silently never runs**. Dev-sandbox was left with record 362 present but zero RecordFields, no `InherentRecordChildren` row and no ACL.
|
|
43
|
+
- **Core referencing an api2 class.** `\api\_Component_Api_V2_Response` from inside `_underscore` broke layering (php-reviewer CRITICAL). Use `$api->response::DEFINED_MESSAGE_ERROR_UNAUTHORIZED`.
|
|
44
|
+
- **Requesting `id` in `fetchDelegationList` fields.** 403s the whole GET for these roles. Use `uuid`.
|
|
45
|
+
- **Fetching the delegation list after a token switch.** The signed-in user's own record reads as a 403 once the token belongs to the other person. Load it before the switch and keep it.
|
|
46
|
+
- **Mapping `fetchDelegationList` to `{name}` for the dropdown.** `toOption()` reads `_name`, so every name rendered blank. TypeScript missed it because all `ApiUser` fields are optional.
|
|
47
|
+
- **`sed`/heredoc with `\\` in Bash for PHP namespaces.** `'\\api\\_Component…'` collapsed to `\a` = BEL (0x07) and produced `Parse error: unexpected character 0x07`. Build backslashes with `chr(92)` in Python instead.
|
|
48
|
+
- **Reading/writing `V2.php` with universal newlines.** Converted the whole CRLF file to LF and showed 9221 changed lines. Always read and write with `newline=''`.
|
|
49
|
+
- **Minting a test JWT by hand** to exercise the endpoint. Blocked by the harness as credential handling. Test through the browser or a real login instead.
|
|
50
|
+
- **Blaming the app for `message.map is not a function`.** It was dependency drift: `package.json` and the lock pin `@agilant/toga-blox` at `1.1.1-production.139`, `node_modules` had `1.0.317`. The old Toaster takes an array; `ToasterList.tsx` passes a string or `{title, content}`. Any toast crashed the page.
|
|
51
|
+
- **`npm install <pkg>@<exact>`** rewrote `package.json` to `^1.1.1-production.139`. Reverted both `package.json` and the lock to keep the exact pin.
|
|
52
|
+
|
|
53
|
+
## Not tried yet (candidates for next session)
|
|
54
|
+
|
|
55
|
+
- **Stamp `c_orderSubmittedById` in `_Model_Compass_SalesOrder::prePost()`** (line 445; `ApiPayloadInterceptor` id 24 is already active as PRE on POST for record 14), reading `$api->delegator->user`. Column, `CustomRecordFields` id 40 and the model field (line 51, `FIELD_INTEGER`) all already exist from Compass 1.0. Leave NULL when there is no delegation. Old data is dirty: of 112,096 Compass orders 8,118 have it set and 7,174 of those equal `createdByUserId`.
|
|
56
|
+
- **Grant role 3 (API) on record 362 in each `Client_*` database.** Only the dead Core grant exists, so no integration can read or write the table through api2. Nothing can seed `Users_DelegatorUsers` that way today.
|
|
57
|
+
- **Close the `/auth/encrypted` hole** — it still accepts a delegation pair with no bearer and records no delegator. Three options were drafted and none chosen: guard `/auth/encrypted`, use a separate key for delegation pairs, or drop pairs for a plain uuid. Narrowed because Commerce no longer calls it; toga2-desk still does for client switching.
|
|
58
|
+
- **Seed `Users_DelegatorUsers`.** No admin screen and no import exist. The table ships empty, so the feature does nothing until rows are added.
|
|
59
|
+
- **Turn the 27-case suite into PHPUnit.** It lives only in a scratch `t.sh` and was never committed.
|
|
60
|
+
- **Replay the Core surface seeds on local.** Local Core data is behind prod: Surfaces 39 vs 56, SurfaceElements 113 vs 214, RecordFields 2171 vs 2186, CronJobs 57 vs 71. Affects UI columns and actions, not ordering.
|
|
61
|
+
- **`Client_Aig`, `Client_Growrk`, `Client_Managelife`, `Client_True`** have no `TransferOrders` tables locally, so those dbchanges skipped them.
|
|
62
|
+
|
|
63
|
+
## Current file state
|
|
64
|
+
|
|
65
|
+
| File | Status | Notes |
|
|
66
|
+
|------|--------|-------|
|
|
67
|
+
| `_underscore/Model/Client/Users/DelegatorUser.php` | new, uncommitted | Model for the junction table |
|
|
68
|
+
| `_underscore/Model/Client/User.php` | modified, uncommitted | `canUserOrderForOtherUser`, `getRequestingUser`, gate inside `encryptedUser` |
|
|
69
|
+
| `_underscore/Model/Client/Logs/Api.php` | modified, uncommitted | `originalUserId` field after `userId` |
|
|
70
|
+
| `api2/Component/Api/V2/V2.php` | modified, uncommitted | `$isDelegatorAuthRequest`, delegator re-check, refresh re-check, `getOriginalUserId()` + 3 log sites |
|
|
71
|
+
| `dbchanges2/Client/2026-09-18a - UsersDelegatorUsersTable.sql` | new, uncommitted | Applied to all 12 local `Client_*` |
|
|
72
|
+
| `dbchanges2/Core/2026-09-18a - UserDelegatorUserRecords.sql` | new, uncommitted | Record 362, fields 2604-2607. Its ACL block is inert (CLIENT record) |
|
|
73
|
+
| `dbchanges2/Client_Compass/2026-09-18a - UserDelegatorUsersReadAcl.sql` | new, uncommitted | Roles 7/8, caller-scoped expression |
|
|
74
|
+
| `dbchanges2/Client_CompassCanada/2026-09-18a - UserDelegatorUsersReadAcl.sql` | new, uncommitted | Roles 8/9, caller-scoped, plus the `encryptedUser` grant as section 4 |
|
|
75
|
+
| `dbchanges2/Logs_Client/2026-09-18a - ApiOriginalUserId.sql` | new, uncommitted | Applied to all 10 local `Logs_*` |
|
|
76
|
+
| `dbchanges2/Core/2026-09-21a - UserDelegatorUserRecordsDevSandbox.sql` | new, uncommitted | Field ids 2767-2770. NOT yet run on dev-sandbox |
|
|
77
|
+
| `dbchanges2/Client_Compass/2026-09-21a - …DevSandbox.sql` | new, uncommitted | Field ids 2768-2770. NOT yet run on dev-sandbox |
|
|
78
|
+
| `dbchanges2/Client_CompassCanada/2026-09-21a - …DevSandbox.sql` | new, uncommitted | Field ids 2768-2770. NOT yet run on dev-sandbox |
|
|
79
|
+
| `toga2-commerce/src/pages/Cart/api/CartApi.ts` | modified, uncommitted | `fetchDelegationList` only |
|
|
80
|
+
| `toga2-commerce/src/pages/Cart/types.ts` | modified, uncommitted | `DelegationOption` only |
|
|
81
|
+
| `toga2-commerce/src/components/OrderForDropdown/useOrderForDropdownViewModel.ts` | changed by Tanner | Now calls `fetchDelegationList`; still no token switch |
|
|
82
|
+
| `~/toga-tech/skills/order-for/SKILL.md` | pushed `d4751825` | Also copied to `C:\WWW\.claude\skills` and `~/.claude/skills` |
|
|
83
|
+
| `~/toga-tech/knowledge/**` | pushed `0960de3b` | 1 new doc + 8 updates |
|
|
84
|
+
|
|
85
|
+
## Decisions made
|
|
86
|
+
|
|
87
|
+
- **The server is the gate, never the UI.** Checked at pair mint and again at token issue, because the encrypted pair has no expiry and no MAC (`_String::encryptWithKey`, AES-256-CBC keyed on `Core.Parameters.API_SECRET_REFRESH_TOKEN`) so a captured pair is replayable forever. Rejected: checking once at mint.
|
|
88
|
+
- **Read-only ACL for the tenant roles.** A role that can write its own row can self-grant the right to order for anyone. Rejected: giving Compass Base create/update.
|
|
89
|
+
- **Scope the row expression to `AclUser.id` rather than `'1'`.** `user-delegator-users` is a routable record. Rejected: `'1'` with the nested-read path as the only protection.
|
|
90
|
+
- **Separate `…DevSandbox.sql` twins instead of editing the originals.** Keeps local and prod stable while dev-sandbox uses its own field ids. Rejected: renumbering the shared files.
|
|
91
|
+
- **Keep the inert Core ACL block in the dev-sandbox Core file.** Core then looks the same in every environment and diffs cleanly. Rejected: deleting three rows that do nothing.
|
|
92
|
+
- **Scoped `toga2-commerce` back to the GET call only.** The token switch is Tanner's work, not this ticket. I had built the full switch (new `useOrderForSessionZu` store, `beginDelegation` on pick, cross-tab handling) and reverted all of it on the developer's instruction. Rejected: shipping both halves from this branch.
|
|
93
|
+
- **Share the feature as a `/order-for` skill, not the raw chat.** The transcript is 9.2 MB of JSONL and contains a bearer token and prod query output.
|
|
94
|
+
|
|
95
|
+
## Blockers
|
|
96
|
+
|
|
97
|
+
- **Nothing seeds `Users_DelegatorUsers`.** The feature is inert in every environment until rows exist. No admin screen, no import, and role 3 has no working grant to write them through api2.
|
|
98
|
+
- **The front end does not switch the token.** Until it calls `/auth/delegator`, `$api->delegator` is never set, so `originalUserId` stays NULL, the `Users_DelegatorUsers` check never fires, and the user browses their own catalog and prices while "ordering for" someone else. This is a Tanner/PM decision, not a code blocker on the server side.
|
|
99
|
+
- Nothing else is blocked. All four repos have working, tested changes sitting uncommitted.
|
|
100
|
+
|
|
101
|
+
## Exact next step
|
|
102
|
+
|
|
103
|
+
> Run `dbchanges2/Core/2026-09-21a - UserDelegatorUserRecordsDevSandbox.sql` against dev-sandbox `Core`, then the two `…DevSandbox.sql` files against dev-sandbox `Client_Compass` and `Client_CompassCanada`. Confirm with `SELECT id, field FROM RecordFields WHERE recordId = 362` returning 2767-2770. Dev-sandbox is currently half-registered: record 362 exists with no fields, no `InherentRecordChildren` row and no ACL, so any `user-delegator-users` or nested `userDelegatorUsers` read there is broken until these run.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
_Saved by /session-save on 2026-09-21_
|
package/package.json
CHANGED