toga-ai 1.0.1071 → 1.0.1072
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/_underscore/features/note-record-authorship.md +3 -1
- package/knowledge/2.0/apps/_underscore/features/record-activity-timeline.md +8 -2
- package/knowledge/2.0/apps/_underscore/features/user-email-as-identity-and-password-reset.md +10 -2
- package/knowledge/2.0/apps/api2/features/cross-client-data-retrieval.md +2 -1
- package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +5 -1
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ files:
|
|
|
16
16
|
- _underscore/Model/Client/UnitNote.php
|
|
17
17
|
- _underscore/Model/Client/ServiceRequestNote.php
|
|
18
18
|
- _underscore/Model/Client/PartRequestNote.php
|
|
19
|
+
- _underscore/Model/Client/UserNote.php
|
|
19
20
|
related:
|
|
20
21
|
- ../../toga-blox/features/add-notes-template.md
|
|
21
22
|
- ./case-status-and-stage-gate.md
|
|
@@ -63,10 +64,11 @@ Before building anything that shows or filters by note author:
|
|
|
63
64
|
|
|
64
65
|
- **`contactId` on a `*Notes` model is an unwritten FK — treat it as always NULL.** Seven models, zero writers.
|
|
65
66
|
- **A notes list UI has no author to show.** Design for it, or the field silently renders blank for every row.
|
|
66
|
-
- **Exception — `CaseNotes
|
|
67
|
+
- **Exception — server-stamped author:** `CaseNotes`, `CustomerNotes`, `ContactNotes`, `IssuerNotes`, `CertificationNotes`, `AttributeNotes`, `UserNotes` carry `authorUserId`, stamped in `::save()` from `_Model::getRequestUserId()` (write-once). Copy that pattern for the real fix. The `save()` body is now copied 7 times — **consolidation debt:** move it to a shared trait/base before adding another. See [Case status + stage gate](./case-status-and-stage-gate.md), [Customer related records](./customer-locations-files-notes.md), [Contact related records](./contact-notes-files-types.md).
|
|
67
68
|
- **`dtStamp` is `FIELD_DATETIME_CREATED`** — stamped by the ORM on insert, the only provenance a note actually carries.
|
|
68
69
|
|
|
69
70
|
## Change history
|
|
71
|
+
- 2026-10-17 — UserNotes added; seven author-stamped note models now copy one save() body (TRUE-82872). (jcardinal)
|
|
70
72
|
- 2026-10-17 — ContactNotes added as third server-stamped-author note model; save() body is a consolidation candidate (TRUE-82871). (jcardinal)
|
|
71
73
|
- 2026-10-17 — CustomerNotes added as a second server-stamped-author note model (TRUE-82870). (jcardinal)
|
|
72
74
|
- 2026-10-15 — Noted CaseNotes as the first note model with a server-stamped author. (jcardinal)
|
|
@@ -32,7 +32,8 @@ client `AclRecordScripts`. Data source: [record change audit log](record-change-
|
|
|
32
32
|
|
|
33
33
|
- **Roots** = `ROOT_ACTIVITY_SURFACE_SLUGS` (route → activity Surface slug): `tickets` →
|
|
34
34
|
`ticket-activity`, `units` → `asset-activity`, `cases` → `case-activity`, `service-requests` →
|
|
35
|
-
`service-request-activity`, `customers` → `customer-activity`, `contacts` → `contact-activity
|
|
35
|
+
`service-request-activity`, `customers` → `customer-activity`, `contacts` → `contact-activity`,
|
|
36
|
+
`issuers`, `certifications`, `attributes`, `users` → `<singular>-activity`. Any other route → 400. **A new root = one map entry + its Surface
|
|
36
37
|
(`dbchanges2` `Core/`).** Engine (`Source` reads Logs, `Reader` checks as the reader through api2)
|
|
37
38
|
is generic; no new client ACL needed for a new root.
|
|
38
39
|
- **Children:** `Logs.RecordParent`, level by level, max `Source::MAX_DEPTH` (3) levels and
|
|
@@ -45,7 +46,8 @@ client `AclRecordScripts`. Data source: [record change audit log](record-change-
|
|
|
45
46
|
`isVisible = 0`). An UPDATE with no visible field is dropped. Secrets → `{redacted:true}`.
|
|
46
47
|
4. FK values → `{uuid, label[, slug]}`, `{missing:true}`, or `{value:null}` — never a raw id.
|
|
47
48
|
- **CREATE rows of `CREATE_HIDDEN_ROUTES`** (`ticket-notes`, `ticket-note-files`, `unit-notes`,
|
|
48
|
-
`case-notes`, `service-request-notes`, `customer-notes`, `contact-notes`
|
|
49
|
+
`case-notes`, `service-request-notes`, `customer-notes`, `contact-notes`, `issuer-notes`,
|
|
50
|
+
`certification-notes`, `attribute-notes`, `user-notes`) are dropped — the host screen merges the notes feed.
|
|
49
51
|
Note edits/deletes still show.
|
|
50
52
|
- **Paging:** cursor = `Logs.Record` id; `nextCursor` null at the end. Scan bounded
|
|
51
53
|
(`MAX_SCAN_BATCHES` × `SCAN_BATCH_SIZE`); a page cut by the bound still returns a `nextCursor`.
|
|
@@ -68,7 +70,11 @@ client `AclRecordScripts`. Data source: [record change audit log](record-change-
|
|
|
68
70
|
best-effort (same user + short time window). Exact grouping needs a new request-id column
|
|
69
71
|
(schema + api2 change) — not built.
|
|
70
72
|
|
|
73
|
+
- **`users` root is safe for secrets:** `password` / `ldapDn` are redacted on write (api2
|
|
74
|
+
`LOG_REDACTED_FIELD_NAMES`) and on read (`isRedactedLogField`).
|
|
75
|
+
|
|
71
76
|
## Change history
|
|
77
|
+
- 2026-10-17 — Added root `users` + `user-notes`; listed `issuers` / `certifications` / `attributes` roots already in code (TRUE-82872). (jcardinal)
|
|
72
78
|
- 2026-10-17 — Added root `contacts` + `contact-notes` to `CREATE_HIDDEN_ROUTES` (TRUE-82871). (jcardinal)
|
|
73
79
|
- 2026-10-17 — Added root `customers` + `customer-notes` to `CREATE_HIDDEN_ROUTES` (TRUE-82870). (jcardinal)
|
|
74
80
|
- 2026-10-17 — One-FK rule for parent links; no request id for grouping (TRUE-82889). (jcardinal)
|
package/knowledge/2.0/apps/_underscore/features/user-email-as-identity-and-password-reset.md
CHANGED
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
10
|
-
owners: [ajean, rgirish]
|
|
9
|
+
updated: 2026-10-17
|
|
10
|
+
owners: [ajean, rgirish, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/User.php
|
|
13
13
|
- api2/Component/Api/V2/V2.php
|
|
@@ -78,6 +78,13 @@ So whoever controls an address that appears on a `Users` row can request, receiv
|
|
|
78
78
|
- **⚠ The reset lookup has no `isActive` filter and no duplicate handling.** On a tenant that allows duplicate emails, "the user with this address" is whichever row MySQL happens to return first. Do not assume the reset went to the account you were looking at.
|
|
79
79
|
- **⚠ Do not assume the database enforces one email per user.** The 2.0 blank-client template declares `UNIQUE KEY email (email)`, but **live tenants have diverged** — both Compass tenants have no unique key and no index on `email` (prod-verified 2026-09-04). Any code that writes an email must check for a cross-row collision itself. Evidence and the full index list: [PEOPLE-file User Lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md).
|
|
80
80
|
- **⚠ Treat an email write as a security change, not a data change.** Minimum rails for any UI or job that sets `Users.email`: confirm the value twice, validate the domain against `Core.ClientEmailDomains`, refuse when another row in the tenant already holds the address, and write an audit row **before** the update. That is the shape the [Tools Compass admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) shipped.
|
|
81
|
+
- **⚠ AD sync overwrites `email`, `title`, `phoneNumber` on every run.**
|
|
82
|
+
`_Model_Client_User::syncFromActiveDirectoryUser()` (~L314) writes them from AD; an app edit to
|
|
83
|
+
those fields on an AD-synced user is silently reverted (Client_True: 2,788 of 2,791 users have
|
|
84
|
+
`ldapDn` set, 2026-10-17). Lock those fields in the UI when calc field `_isDirectorySynced` = 1
|
|
85
|
+
(`ldapDn` non-empty) — use it instead of exposing `ldapDn`.
|
|
86
|
+
- **Client role `Base` cannot write `Users.email`** (RecordField 115 `isWritable` 0 in Client_True /
|
|
87
|
+
Client_Elite, 2026-10-17). An email edit needs a stronger role — keep it that way.
|
|
81
88
|
- **An email is also a worker matching key.** worker2's `ensureCrossRegionUsersActive()` matches Compass users on **email**, so changing an address can silently remove a user from a hardcoded protected set — a second reason email edits need an explicit block list.
|
|
82
89
|
- **⚠ `_Http::validateJsonWebToken()` checks the signature only, never `exp`.** Any code that trusts
|
|
83
90
|
a JWT must check `exp` itself. Before 2026-10-13, `updatePassword()` did not, so a reset link
|
|
@@ -92,6 +99,7 @@ So whoever controls an address that appears on a `Users` row can request, receiv
|
|
|
92
99
|
- **Never paste real addresses into tickets or the KB.** Reference the mechanism and the column, not the person.
|
|
93
100
|
|
|
94
101
|
## Change history
|
|
102
|
+
- 2026-10-17 — AD sync overwrites email/title/phoneNumber (`_isDirectorySynced` lock); Base role cannot write email (TRUE-82872). (jcardinal)
|
|
95
103
|
- 2026-10-14 — Reset token now 30 min and `updatePassword()` rejects expired tokens; documented the two scripted APIs, Public ACL, `Core.Domains` link host, and the BCC / query-string exposure (rgirish)
|
|
96
104
|
- 2026-09-16 — Created: email as login identity and tenant-routing key (ajean)
|
|
97
105
|
|
|
@@ -45,7 +45,7 @@ Because client DBs live on **different clusters**, a cross-DB JOIN/UNION is impo
|
|
|
45
45
|
|
|
46
46
|
Entry point is `_Component_Api_CrossClient` (`api2/Component/Api/CrossClient/CrossClient.php`), delegated from a guard at the **top** of the `ACTION__LIST` case in `_Component_Api_V2::processRoutePairs()`: when `isCrossClientRequest($httpOptions)` is true (a non-empty `client` option), it calls `handleListing()`, merges result rows into `$outData` and meta into `$this->response->meta`, and breaks. The guard is **inert** on ordinary single-client GETs.
|
|
47
47
|
|
|
48
|
-
1. **resolveScope** — the caller's entitled clients resolve via the `Users_Clients` bridge (`
|
|
48
|
+
1. **resolveScope** — the caller's entitled clients resolve via the `Users_Clients` bridge (Records 199 `user-clients`); `mintIdentity()` then picks each target's linked user. Identity model: a **home** row has `homeClientUserId` NULL; a **linked** row in another client DB has `clientId` = home Core client id + `homeClientUserId` = home `Users.id` (`UNIQUE(clientId, homeClientUserId)`). **"Which clients can this user reach" = `Users_Clients`**, not the linked rows — they drift (local data has a linked row with no `Users_Clients` row). This is **serial** — one DB context switch + query per permitted client, on every request including cache hits.
|
|
49
49
|
2. **queryHash** — SHA-256 of the normalized query plus `QUERY_HASH_VERSION`; the cache key. Bump `QUERY_HASH_VERSION` whenever how cached rows are built or ordered changes — it orphans every older result table (v2 = dotted-field sort keys).
|
|
50
50
|
3. **Build lock** — a named `GET_LOCK` over a **fixed-width digest** of client/user/hash.
|
|
51
51
|
4. **Fan out** — a two-phase concurrent HTTP handshake per client (see [encrypted-user-uuid-auth-handoff](./encrypted-user-uuid-auth-handoff.md)): Phase A mints an access token, Phase B issues the authenticated GET.
|
|
@@ -145,5 +145,6 @@ Page 1 of 25 still fetches exactly 25; page 254 fetches 500 per pass — ~13 rou
|
|
|
145
145
|
- [_Model magic-field access](../../_underscore/features/model-magic-field-access.md)
|
|
146
146
|
|
|
147
147
|
## Change history
|
|
148
|
+
- 2026-10-17 — Home vs linked user rows; `Users_Clients` is the reach list, linked rows drift (TRUE-82872). (jcardinal)
|
|
148
149
|
- 2026-10-17 — Fetch scope is `client=` OR own token, never both; key multi-client rows by client + record uuid (TRUE-82903). (jcardinal)
|
|
149
150
|
- 2026-10-17 — Internal-request writes must add their own `cacheInvalidations` (TRUE-82889). (jcardinal)
|
|
@@ -368,7 +368,10 @@ where=(field:op:value,LOGIC,field:op:value,(nested,OR,nested))
|
|
|
368
368
|
- **A dotted FK path (`case.uuid`) in `where` fails `EV-8` on a list GET.** Filter by a parent FK
|
|
369
369
|
through a join alias: `ojoin=Cases:Cases.id=Tickets.caseId&where=(Cases.uuid:eq:<uuid>)` (or a
|
|
370
370
|
`TableViewJoins` row that gives the alias). A path through an **inherent child**
|
|
371
|
-
(`ticketUsers.user.uuid`) does work
|
|
371
|
+
(`ticketUsers.user.uuid`) does work — but only when the **first** segment is an inherent child
|
|
372
|
+
of the route (`buildChildPathWhereSql`). A path that starts with a forward FK hop fails
|
|
373
|
+
(`service-requests` → `ticket.ticketUsers.user.uuid`). No-api2-change fix: a TableView that
|
|
374
|
+
outer-joins the FK table and the bridge, then `where` on the join alias.
|
|
372
375
|
|
|
373
376
|
### ✅ You CAN `where` on a calculated (`FIELD_SQL`) field — but ONLY with the BARE name
|
|
374
377
|
|
|
@@ -625,6 +628,7 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
|
|
|
625
628
|
[V2 request logging](request-logging.md).
|
|
626
629
|
|
|
627
630
|
## Change history
|
|
631
|
+
- 2026-10-17 — Child-path `where` must start at an inherent child; FK-first path → TableView join alias (TRUE-82872). (jcardinal)
|
|
628
632
|
- 2026-10-17 — Multi-sort comma list + `uuid` tie-breaker for stable paging; sort on calc works; refreshed where-path line refs. (jcardinal)
|
|
629
633
|
- 2026-10-17 — POST create = 201, PUT/DELETE = 200 (TRUE-82889). (jcardinal)
|
|
630
634
|
- 2026-10-15 — Security: `where` on an unreadable field still filters under default `identifier_acl_mode=log` (hash guessing); dotted FK path in `where` = `EV-8`, use a join alias (TRUE-82836). Folded older history. (jcardinal)
|
package/package.json
CHANGED