toga-ai 1.0.526 → 1.0.528

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.
Files changed (23) hide show
  1. package/knowledge/1.0/apps/library/features/error-capture-1-0.md +17 -1
  2. package/knowledge/1.0/apps/togadesk/features/ticket-lifecycle.md +15 -1
  3. package/knowledge/1.0/apps/togaview/INDEX.md +3 -2
  4. package/knowledge/1.0/apps/togaview/architecture.md +24 -3
  5. package/knowledge/1.0/apps/togaview/features/login-flows.md +54 -1
  6. package/knowledge/1.0/apps/togaview/features/ticket-detail-page.md +116 -8
  7. package/knowledge/1.0/apps/togaview/workflows/git-and-php-tooling-under-the-underscore-app-root.md +58 -0
  8. package/knowledge/1.0/standards/backend-php.md +6 -3
  9. package/knowledge/2.0/apps/_underscore/INDEX.md +3 -1
  10. package/knowledge/2.0/apps/_underscore/features/apirequest-json-content-type.md +20 -2
  11. package/knowledge/2.0/apps/_underscore/features/email-send-pipeline.md +25 -4
  12. package/knowledge/2.0/apps/_underscore/features/string-html-entity-helpers.md +82 -0
  13. package/knowledge/2.0/apps/_underscore/features/togaiq-gateway-client.md +155 -0
  14. package/knowledge/2.0/apps/api2/INDEX.md +1 -0
  15. package/knowledge/2.0/apps/api2/features/environment-variable-drives-underscore-branch.md +100 -0
  16. package/knowledge/INDEX.md +3 -3
  17. package/knowledge/clients/compass-canada/INDEX.md +1 -1
  18. package/knowledge/clients/compass-canada/features/french-order-email-localization.md +130 -2
  19. package/knowledge/clients/compass-usa/features/approval-decision-flow.md +20 -1
  20. package/knowledge/clients/compass-usa/features/mr-ma-order-approval-and-status.md +16 -2
  21. package/knowledge/clients/rumcsi/profile.md +12 -1
  22. package/knowledge/clients/tow-foundation/profile.md +31 -3
  23. package/package.json +1 -1
@@ -7,7 +7,7 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-08-04
10
- owners: ["jcardinal"]
10
+ owners: ["jcardinal", "mhammontree"]
11
11
  files:
12
12
  - library/app/error/capture.php
13
13
  - library/app/error.php
@@ -200,6 +200,18 @@ exists in `api2`/`worker2`. So 1.0 reads the AL1 container config at
200
200
  the class+entryPoint fallback changes hashes, so open issues re-fingerprint on first occurrence
201
201
  after deploy. Deliberate; merge old hashes onto the curated Issue with the Tools
202
202
  merge-fingerprint box.
203
+ - **⚠ The legacy pre-Sentry handler dumps the entire config on any error it renders.**
204
+ `library/app/DELETE_DOA_PRE-SENTRY_error.php` is a dead-named but still-reachable handler whose
205
+ `htmlBacktrace()` does `print_r($entry['args'])` for **every** frame. On 2026-08-05 a real
206
+ error page on **https://towfoundation.togaview.com** rendered ~26 pages containing the whole
207
+ config array — production DB host/user/password, SES SMTP credentials, NetSuite credentials,
208
+ Payeezy API key/secret/merchant token, SSL Store token — plus ticket PII. The current
209
+ `library/app/error.php` is safe (it dumps only under `App_Registry::inDevMode()`), but **both**
210
+ handlers gate on `inDevMode()`, so the affected host was **evaluating as dev mode in
211
+ production**. Two actions: (1) verify `inDevMode()` per host in production before trusting any
212
+ error page not to leak, and (2) delete the legacy handler. Rotation of every exposed credential
213
+ **was escalated to Jeff on 2026-08-05; confirmation that it was completed is still owed** —
214
+ treat those credentials as compromised until confirmed. (No values are recorded here or anywhere in the KB.)
203
215
  - **Pre-existing committed secrets (location only — not fixed, out of scope).**
204
216
  `library/app/cloud.php` holds a hardcoded **AWS access key id and secret access key** as
205
217
  class constants/statics (~lines 23–24), used by `App_Cloud::$preCommands` for every 1.0 AWS CLI
@@ -208,6 +220,10 @@ exists in `api2`/`worker2`. So 1.0 reads the AL1 container config at
208
220
 
209
221
  ## Change history
210
222
 
223
+ - 2026-08-05 — Recorded the legacy `DELETE_DOA_PRE-SENTRY_error.php` handler leaking the full
224
+ config array (prod DB / SES / NetSuite / Payeezy / SSL Store credentials + ticket PII) on a
225
+ live towfoundation.togaview.com error page, meaning that host evaluates as dev mode in
226
+ production; rotation escalated to Jeff, confirmation still owed. (mhammontree)
211
227
  - 2026-08-04 (latest, **uncommitted/undeployed** at time of writing) — **Mirrored 2.0's reversal:
212
228
  `$GLOBALS` is now captured** into a top-level `GLOBALS` group in `capture.php`
213
229
  (`buildGlobalsContext()`, `sanitizeForContext()`, `CONTEXT_GLOBALS_TO_EXCLUDE`,
@@ -6,7 +6,7 @@ project: TOGa Desk
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-09
9
+ updated: 2026-08-05
10
10
  owners: ["mhammontree", "dfranks"]
11
11
  files:
12
12
  - desk/includes/classes/class.ticket.php
@@ -109,6 +109,17 @@ backfill (e.g. craftex:
109
109
  `UPDATE TOGaDeskSupport.tickets SET customerid = 932297 WHERE clientid = 169 AND customerid IS NULL;`).
110
110
 
111
111
  ## Gotchas / known issues
112
+ - **`tickets.escalateToStartech` has NO migration anywhere — schema drift.** The column is
113
+ referenced by `App_Model_TogaDesk_Ticket` (`FIELDTYPE_INT`), `quickactions.php` (sets `1`) and
114
+ `manage.php` (checks `=== null`), and **exists in production**, but the `dbchanges` repo has no
115
+ migration for it; the only `escalateToStartech` SQL in any repo is the 2.0 sibling
116
+ `dbchanges2/Client_Pcmaticb2b/2026-06-22-pcmaticb2b-enhancements.sql`
117
+ (`c_escalateToStartech TINYINT(1) UNSIGNED NULL DEFAULT NULL`). A local dev DB missing the
118
+ column fails **every** ticket INSERT with MySQL error **1054**. Apply it locally; no migration
119
+ file is needed (prod already has it).
120
+ **Diagnosis technique worth reusing:** diff the model's `public $field` list against
121
+ `information_schema.COLUMNS` to find *every* gap at once — MySQL reports only the first unknown
122
+ column, so fixing them one error at a time is slow and misleading.
112
123
  - `'Resolved'` is STILL missing from the `addReply()` switch — a client reply to a Resolved
113
124
  ticket changes nothing. Known gap, awaiting product decision.
114
125
  - **There is NO 72-hour reopen window in code**, despite SME belief. `Closed` → `Reopened` is
@@ -135,6 +146,9 @@ backfill (e.g. craftex:
135
146
  via `emailToTicket()`.
136
147
 
137
148
  ## Change history
149
+ - 2026-08-05 — recorded `tickets.escalateToStartech` schema drift (in prod, no `dbchanges`
150
+ migration; missing column fails every ticket INSERT with error 1054 locally) and the
151
+ model-fields-vs-`information_schema` diffing technique, from TRUE-80671 (mhammontree)
138
152
  - 2026-07-09 — mapped the four reply entry points (togaview portal bypasses `addReply()` so
139
153
  never transitions status); documented `emailToTicket()` adminid-overwrite reply
140
154
  misclassification, the `origin`-absence agent-reply signal, and togadesk↔togaview shared-DB
@@ -3,8 +3,9 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [TOGa View Architecture](architecture.md) | TOGa View is the client-facing support portal, served per client at `<client>.togaview.com`. | index.php, reset.php, _/app/framework.php, _/app/frameworkindex.php, _/browser/pagination.php, mvc/login/post.php, mvc/ticket/get.php |
6
- | [TOGa View Login Flows & Session Variables](features/login-flows.md) | `mvc/login/post.php` tries login flows in order; the first match wins. | mvc/login/post.php, _/app/framework.php |
6
+ | [TOGa View Login Flows & Session Variables](features/login-flows.md) | `mvc/login/post.php` tries login flows in order; the first match wins. | mvc/login/post.php, _/app/framework.php, _/app/frameworkindex.php, mvc/msp_client_dashboard/post.php |
7
7
  | [MSP Dashboard & Ticket Visibility Rules](features/msp-dashboard.md) | Why tickets "disappear" in the TOGa View client portal: different pages scope tickets **differently**, and the MSP pages depend on `tickets.customerid` and `SMB | common/togaview/msp_dashboard.php, mvc/msp_client_dashboard, mvc/enterprise_dashboard, mvc/support/support.php |
8
8
  | [Retail Commerce Lifecycle (winback, purchase, claims, activation)](features/retail-commerce-lifecycle.md) | Beyond support, TOGa View hosts the **retail tech-support commerce flows**: subscription purchase/renewal (with payment), service activation, and insurance-styl | mvc/winback_landing/get.php, mvc/winback_payment/get.php, mvc/winback_payment/post.php, mvc/purchase/get.php, mvc/service_activation/get.php, mvc/claims/get.php, mvc/techsupport_app/get.php |
9
- | [Ticket Detail Page Security (common/togaview/ticket.php)](features/ticket-detail-page.md) | `common/togaview/ticket.php` is the ticket detail page for nearly ALL hosts — only towfoundation/newcenturyholdingsllc have their own variants; every other clie | common/togaview/ticket.php |
9
+ | [Ticket Detail Page Security & Its Three Forks (common/*/ticket.php)](features/ticket-detail-page.md) | `common/togaview/ticket.php` is the ticket detail page for nearly ALL hosts — only towfoundation/newcenturyholdingsllc have their own variants; every other clie | common/togaview/ticket.php, common/towfoundation/ticket.php, common/newcenturyholdingsllc/ticket.php, mvc/ticket/get.php, mvc/ticket/post.php |
10
+ | [Git & php -l Tooling Against the `_/` App Root (MSYS path mangling)](workflows/git-and-php-tooling-under-the-underscore-app-root.md) | In **Git Bash on Windows**, any command argument beginning with `_/` is rewritten by MSYS path conversion before the tool sees it. | _/app/framework.php, _/app/frameworkindex.php |
10
11
  | [Route 53 Hosted Zone Migration Between AWS Accounts](workflows/route53-cross-account-zone-migration.md) | How to move a domain's DNS records (a Route 53 hosted zone) from one AWS account to another. | |
@@ -6,7 +6,7 @@ project: TOGa View
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-06-15
9
+ updated: 2026-08-05
10
10
  owners: ["mhammontree", "jcardinal"]
11
11
  files:
12
12
  - index.php
@@ -46,7 +46,9 @@ generic `common/togaview/*.php` templates, while a handful have full per-client
46
46
  superglobals, starts the session (`HttpOnly`/`Secure`), inits Sentry, and registers the
47
47
  NetSuite toolkit + SAML.
48
48
  - **`frameworkindex.php`** is the page-shell renderer (CSS/JS registration, page title).
49
- **`framework_DOA.php`** is a near-duplicate variant of `framework.php`.
49
+ A near-duplicate `framework_DOA.php` was **deleted 2026-08-05** (440 dead lines carrying the
50
+ same unguarded name-split bug as the live file); if one reappears, delete it rather than
51
+ maintaining it.
50
52
 
51
53
  ## Page routing (stylePath model)
52
54
  `index.php → App_Framework_TogaView::renderIndex()` (in `_/app/framework.php`):
@@ -67,6 +69,17 @@ generic `common/togaview/*.php` templates, while a handful have full per-client
67
69
  **Implication:** a change to `common/togaview/<page>.php` affects nearly every client at
68
70
  once; a per-client behavior change belongs in a host/stylePath variant or an inline branch.
69
71
 
72
+ **Fork parity is a security requirement.** `common/{togaview,towfoundation,newcenturyholdingsllc}/ticket.php`
73
+ are three near-duplicates; TRUE-78835's ownership checks and int-cast landed in only one, leaving
74
+ two hosts with a live cross-tenant IDOR for two months (found and fixed in TRUE-80671). Any fix to
75
+ a forked page must be applied to every fork, and any review of one must check the others.
76
+
77
+ **Open question — is `newcenturyholdingsllc` a real tenant?** It exists only as a togaview host
78
+ with its own `common/newcenturyholdingsllc/` page fork and a SAML branch that auto-creates
79
+ `people` rows, but it has **no client profile** in the knowledge base and its tenant status is
80
+ unconfirmed. The TRUE-80671 fixes shipped to its fork and are recorded on the shared togaview
81
+ feature docs. Deliberately not onboarded — treat as an open question, not an oversight.
82
+
70
83
  ## MVC request lifecycle
71
84
  - `App_MVC::parseRoute()` (in `library`) turns the request URI into route parts and resolves
72
85
  `mvc/<route…>/<method>.php`, where `<method>` is `get` or `post` (from the HTTP verb,
@@ -95,7 +108,9 @@ divergent shapes (read `emailAddress` with `email` fallback).
95
108
 
96
109
  **SAML** (rumcsi, towfoundation, newcenturyholdingsllc) is handled in `framework.php`
97
110
  (`meta`/`acs`/`sls`); newcenturyholdingsllc and towfoundation **auto-create `people` rows** for
98
- unknown SAML users. rumcsi additionally accepts a legacy `?hash=<base64 email>` auto-login.
111
+ unknown SAML users. rumcsi additionally accepts a legacy `?hash=<base64 email>` auto-login — **an unauthenticated
112
+ account-takeover path that appears to be the client's actual production login**; do not remove it
113
+ before completing the `acs` handler (see [login flows](features/login-flows.md)).
99
114
 
100
115
  ## Dashboards
101
116
  Five dashboards, one per audience/business model, each delegating to a client override or the
@@ -154,6 +169,12 @@ retail-commerce-lifecycle feature doc.
154
169
  - **Hardcoded Braintree credentials (including a private key) are committed under
155
170
  `mvc/winback_payment/`** — these should be rotated and moved to `config.<env>.ini`
156
171
  secret management. Flagged 2026-06-15; not yet remediated.
172
+ - **Production can render dev-mode error pages.** A live towfoundation error page dumped the
173
+ entire config array (prod DB, SES, NetSuite, Payeezy, SSL Store credentials) because both error
174
+ handlers gate on `App_Registry::inDevMode()` and that host evaluated as dev in production.
175
+ Verify `inDevMode()` per host. Rotation of every exposed credential was escalated to Jeff on
176
+ 2026-08-05; **confirmation that it was completed is still owed** — treat those credentials as
177
+ compromised until confirmed.
157
178
  - `ENVIRONMENT` defaults to `worker` when unset → wrong config file.
158
179
 
159
180
  ## Related docs
@@ -6,11 +6,13 @@ project: TOGa View
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-12
9
+ updated: 2026-08-05
10
10
  owners: ["mhammontree"]
11
11
  files:
12
12
  - mvc/login/post.php
13
13
  - _/app/framework.php
14
+ - _/app/frameworkindex.php
15
+ - mvc/msp_client_dashboard/post.php
14
16
  related:
15
17
  - 1.0/apps/togaview/features/msp-dashboard.md
16
18
  - 1.0/apps/togaview/features/ticket-detail-page.md
@@ -51,13 +53,64 @@ end-user flows.
51
53
  - staplesprotection: inline branch, end-user style, divergent session keys (see table).
52
54
  - rumcsi / newcenturyholdingsllc: SAML handled in `_/app/framework.php`, not login/post.php.
53
55
 
56
+ ## Session-name building — never index `[1]` unguarded
57
+ Several flows built `$_SESSION['clientName'] = $user_name[0] . ' ' . $user_name[1]` from
58
+ `explode(' ', $name)`. A **mononym or shared mailbox** (`operations@`, `kindred@`) — or a name
59
+ stored with a leading space — makes `[1]` undefined, and `App_Error::handleError()` promotes
60
+ every notice to an `ErrorException` and `exit()`s. The result was **total lockout**: those users
61
+ simply could not log in, with no page error anyone would report — a silent outage class. Fixed at
62
+ 4 sites (`mvc/login/post.php` towfoundation/newcenturyholdingsllc/rumcsi blocks, plus the rumcsi
63
+ bootstrap in `_/app/framework.php`) to `trim($user_name[0] . ' ' . ($user_name[1] ?? ''))`.
64
+
65
+ ## `sqlProtect(sqlEscape())` removed from the login path
66
+ `mvc/login/post.php`, `mvc/msp_client_dashboard/post.php`, `_/app/framework.php` and
67
+ `_/app/frameworkindex.php` wrapped already-escaped emails in `App_Database::sqlProtect()`, which
68
+ **strips** `% \ / * " ' ` and ` or` — so `o'brien@x.com` became `obrien@x.com` and the login
69
+ lookup silently found nothing. Removed, and `email LIKE '…'` changed to `=` in the same change
70
+ (see the coupling note in the
71
+ [ticket detail page doc](ticket-detail-page.md#no-sqlprotect-on-escaped-values-email-lookups-use--not-like)).
72
+
54
73
  ## Gotchas / known issues
74
+
75
+ ### OPEN SECURITY ITEM — rumcsi `?hash=` unauthenticated account takeover (deliberately left in place)
76
+ `_/app/framework.php` authenticates a rumcsi user on nothing but
77
+ `base64_decode(urldecode($_GET['hash']))` matching an active `people` row for the rumcsi
78
+ clientid — **no password, signature, nonce, or expiry** — then populates ~18 session keys and
79
+ redirects to `/enterprise_dashboard`. Anyone who can guess a RUMC user's email can log in as
80
+ them.
81
+
82
+ It was removed during TRUE-80671 and then **deliberately reverted**, because it may BE the
83
+ production login mechanism: the rumcsi SAML `acs` handler does `App_MVC::routeTo('/')` on success
84
+ and **never populates `$_SESSION`** (unchanged since commit 9e7d306, "phase one … make sure
85
+ redirect works"); repo-wide only two places ever build a rumcsi session — this hash block and
86
+ `mvc/login/post.php:462`, and the latter is unreachable because the SAML gate redirects to the
87
+ IdP before a password POST can be processed; and RUMC login is confirmed **working in
88
+ production**. Nothing in togaview/togadesk/library generates such a link, so links are presumably
89
+ generated outside our codebase.
90
+
91
+ - **Next step:** obtain the post-SSO redirect URL from Jonathan to confirm whether it carries
92
+ `hash=`.
93
+ - **Proper fix:** complete the rumcsi `acs` handler (mirror newcenturyholdingsllc:
94
+ `$auth->getNameId()` → `people` lookup → session build) — **not** restoring the bypass.
95
+ - Note the surviving SAML branch is gated on `!inTestMode()`, so **test mode leaves rumcsi with
96
+ no login path at all**.
97
+
98
+ ### Other
99
+ - **`people.password` is a bare SHA-1** (`mvc/login/post.php:21/96/471`) and the hash is also
100
+ carried in `$_SESSION['Contact']['password']`. Known and accepted for now — togaview is
101
+ migrating to 2.0, so no remediation is planned here; do not "fix" it as a side effect of
102
+ another ticket.
55
103
  - Code consuming the session must read `emailAddress` with an `email` fallback (and tolerate
56
104
  missing `togadeskClientId`) or it rejects all staplesprotection users — this broke the
57
105
  ticket.php ownership check until the fallback was added (June 2026).
58
106
  - The flows are order-dependent: a contact matching an earlier flow never reaches later ones.
59
107
 
60
108
  ## Change history
109
+ - 2026-08-05 (TRUE-80671, uncommitted) — Fixed the unguarded `$user_name[1]` session-name build
110
+ that **locked mononym / shared-mailbox users out entirely** (4 sites); removed
111
+ `sqlProtect(sqlEscape())` and switched email lookups to `=`; recorded the rumcsi `?hash=`
112
+ auto-login as an open takeover risk that was intentionally left in place pending confirmation
113
+ of the real SSO redirect. (mhammontree)
61
114
  - 2026-06-12 — documented from craftex MSP portal debugging session (mhammontree)
62
115
 
63
116
  ## Related docs
@@ -1,15 +1,19 @@
1
1
  ---
2
- title: Ticket Detail Page Security (common/togaview/ticket.php)
2
+ title: Ticket Detail Page Security & Its Three Forks (common/*/ticket.php)
3
3
  framework: "1.0"
4
4
  repo: togaview
5
5
  project: TOGa View
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-12
9
+ updated: 2026-08-05
10
10
  owners: ["mhammontree"]
11
11
  files:
12
12
  - common/togaview/ticket.php
13
+ - common/towfoundation/ticket.php
14
+ - common/newcenturyholdingsllc/ticket.php
15
+ - mvc/ticket/get.php
16
+ - mvc/ticket/post.php
13
17
  related:
14
18
  - 1.0/apps/togaview/features/login-flows.md
15
19
  - 1.0/apps/togaview/features/msp-dashboard.md
@@ -21,6 +25,24 @@ towfoundation/newcenturyholdingsllc have their own variants; every other client'
21
25
  back to it. Hardened June 2026 against IDOR (ticket view + file attachment download) and a
22
26
  "Deactivated Seat" false positive that blocked MSP admins.
23
27
 
28
+ ⚠ **THREE near-duplicate forks exist, and a fix applied to one is NOT applied to the others.**
29
+ `mvc/ticket/get.php` selects: host in
30
+ `App_Framework_TogaView::HOSTS_USING_TOGA_TECH_STYLE` (= `['towfoundation',
31
+ 'newcenturyholdingsllc']`) → `common/<host>/ticket.php`; otherwise `common/togaview/ticket.php`.
32
+ **Every security or bug fix here must be applied to all three forks.** TRUE-80671 found
33
+ **three separate hardenings** (ticket-view ownership, attachment-download ownership, the
34
+ `ticketId` int-cast) that had only ever landed in `common/togaview/ticket.php` — commit
35
+ 9052672/8cfc80e of TRUE-78835 patched one of three files, leaving towfoundation and
36
+ newcenturyholdingsllc fully exploitable for two months. Treat fork parity as a checklist item,
37
+ not a nicety.
38
+
39
+ **Open question — `newcenturyholdingsllc` has a fork but no client profile.** It exists only as a
40
+ togaview host with its own `common/newcenturyholdingsllc/` pages and a SAML branch that
41
+ auto-creates `people` rows; whether it is a real tenant is **unconfirmed**, so it was deliberately
42
+ not onboarded as a client (2026-08-05). Its TRUE-80671 fixes are recorded here on the shared docs.
43
+ If you learn its formal name and TOGa Desk `clients.id`, onboard it — this is an open question,
44
+ not an oversight.
45
+
24
46
  ## How it works
25
47
 
26
48
  ### Ownership check (IDOR fix)
@@ -44,8 +66,52 @@ requester email isn't a provisioned SMB seat user (the SMBContractItems JOIN ret
44
66
  which is NORMAL for email-intake tickets, so it must never apply to MSP/CUSTOMER viewers.
45
67
 
46
68
  ### Input handling
47
- `$_GET['ticketId']` is int-cast once at the top — it is interpolated into several queries in
48
- this file (was a live SQL injection before June 2026).
69
+ `$_GET['ticketId']` is int-cast once at the top of **each** fork — it is interpolated into
70
+ several queries (was a live SQL injection in `common/togaview/` before June 2026 and in
71
+ `common/towfoundation/` until 2026-08-05).
72
+
73
+ **Why an uncast id is genuinely injectable:** `App_Model` interpolates the primary key **raw**
74
+ into its SELECT (`library/app/model.php:218` and `:294`; the `(int)` at `:404` covers only the
75
+ save path). So `new App_Model_TogaDesk_Ticket($_GET['ticketId'])` or
76
+ `new App_Model_TogaDesk_File($_GET['fileId'])` with an uncast value is an injection point —
77
+ constructing a model is not a safe boundary.
78
+
79
+ ### Requester-name splitting (must be guarded)
80
+ Each fork renders the requester's first/last name via `explode(' ', $name)`. A **mononym or
81
+ shared mailbox** (`operations@`, `kindred@`) — or a name stored with a **leading space** —
82
+ yields a 1-element array, and reading `[1]` raises `Undefined offset: 1`. Because
83
+ `App_Error::handleError()` promotes **every** notice to an `ErrorException` and `exit()`s, that
84
+ notice is a hard 500, not cosmetic. Guard with
85
+ `if (!empty($ticketUserInformation) && isset($ticketUserInformation['name']))` and use
86
+ `$parts[1] ?? ''`.
87
+
88
+ ⚠ **Do not assign firstName/lastName unconditionally to "simplify" the guard.** Doing so makes
89
+ an empty no-match result **non-empty**, which silently bypasses the `empty($ticketUserInformation)`
90
+ deactivated-seat fallback further down the same file. `App_Database::fetchRow()` returns `[]`
91
+ (not `false`) on no match, so emptiness *is* the no-match signal.
92
+
93
+ ### Custom fields are keyed off the ticket's own client
94
+ The towfoundation fork was a copy-paste of the rumcsi branch and queried
95
+ `tickets_customfields WHERE clientid = (SELECT id FROM clients WHERE name = "Richmond
96
+ University Medical Center")` — serving TOW Foundation **RUMC's** field definitions
97
+ (cross-tenant leak). It now uses `(int) $ticketDetails->clientid`
98
+ (`App_Model_TogaDesk_Ticket::$clientid` is `FIELDTYPE_INT`) with no hardcoded client name.
99
+ Never resolve a tenant by literal client *name* in these files.
100
+
101
+ ### No `sqlProtect()` on escaped values; email lookups use `=`, not `LIKE`
102
+ `App_Database::sqlProtect(App_Database::sqlEscape($x))` was removed at **39 sites across 7
103
+ files**. `sqlProtect()` is `str_ireplace(['%','\\','/','*',' or','"',"'"], '', $input)` — it
104
+ **deletes** those characters, stripping the backslashes `sqlEscape()` just added, so
105
+ `o'brien@x.com` silently becomes `obrien@x.com` and the lookup finds nothing. It also mangles
106
+ ordinary strings ("Victor Ortiz" → "Victt Ortiz", because ` or` is stripped case-insensitively).
107
+ It adds **no** injection protection — the escape already made the value safe.
108
+
109
+ **The `LIKE` → `=` change must ship with the removal.** `sqlProtect` stripping `%` was the only
110
+ thing preventing a `%` in an address from acting as a LIKE wildcard; removing it while keeping
111
+ `LIKE` would introduce over-matching. Verified behaviorally identical against the real schema:
112
+ `people.email` / `contacts.email` are `utf8mb3_general_ci`, `Customers.email` and per-client
113
+ `Contacts.emailAddress` are `latin1_swedish_ci` — all case-insensitive VARCHARs, `people.email`
114
+ is BTREE-indexed, and no site relied on wildcards.
49
115
 
50
116
  ## Data model
51
117
  `TOGaDeskSupport.tickets`, `tickets_replies`, `files`; TOGA-side `SMBContractItems` →
@@ -58,14 +124,56 @@ this file (was a live SQL injection before June 2026).
58
124
  `emailAddress`/`togadeskClientId`) — the ownership check's email fallback exists for them.
59
125
 
60
126
  ## Gotchas / known issues
61
- - Pre-existing warts (untouched): unescaped `$emailAddress` in the walmart branch query;
62
- unguarded `explode(' ', name)` in rumcsi/towfoundation branches.
127
+
128
+ ### OPEN — the ownership check is defeated for retail by a shared client id
129
+ Both RetailServices logins hardcode `$_SESSION['togadeskClientId'] = 11`
130
+ (`mvc/login/post.php:536` and `:560`) and **every** retail ticket is created with that value
131
+ (`mvc/ticket/post.php:220,285`). So the ownership check's client-match arm evaluates `11 == 11`
132
+ for every retail session and **never reaches the email arm**: any retail customer can read any
133
+ other retail customer's ticket and attachments by iterating `ticketId`/`fileId`. Retail routes
134
+ to `common/togaview/ticket.php` (there is no `common/walmart*/ticket.php`).
135
+ **Suggested fix:** treat id 11 as a shared bucket — require the email arm when
136
+ `togadeskClientId == 11` — and replace the magic `11` with a named constant. Not fixed in
137
+ TRUE-80671 by explicit developer decision (one login issue at a time; togaview is migrating to
138
+ 2.0).
139
+
140
+ ### OPEN — portal-uploaded attachments can never be downloaded
141
+ Upload writes to **S3** (`App_Cloud::copyFileToS3($tempPath, 'asifiles',
142
+ '/togadesk-<env>/uploads/<name>')`, `mvc/ticket/post.php:160`; `files.file` stores only the bare
143
+ filename), but the download handler reads the **local filesystem** at
144
+ `$_SERVER['DOCUMENT_ROOT'] . $config['internal']['kb_documents_path'] . '/' . $file->file` — the
145
+ **knowledge-base** document path, evidently copy-pasted from a KB handler and never adapted for
146
+ S3. Fails **silently**: `file_exists()` fails → `routeTo(...&invalidFile=true)` → "Invalid File"
147
+ toaster, not an error, which is why it went unnoticed. Same in all three forks.
148
+ `App_Cloud::getFileFromS3()` (`library/app/cloud.php:540`) exists and is unused here. Verify
149
+ against production (prod `DOCUMENT_ROOT/uploads` may be S3-synced) before sizing.
150
+
151
+ ### Per-developer config must carry the `kb_*` keys
152
+ Attachment download reads `$config['internal']['kb_documents_path']`. A dev config missing the
153
+ five `kb_*` keys (`kb_documents_path`, `kb_articles_path`, `kb_faqs_path`, `kb_tasks_path`,
154
+ `kb_general_path`) — which prod/alpha/beta/stage and most dev configs carry — raises
155
+ "Undefined index", **fatal** under `App_Error`. The active config is chosen by
156
+ `getenv('ENVIRONMENT')`, set via Apache `SetEnv ENVIRONMENT` in `httpd.conf`. Copy the `kb_*`
157
+ block when creating a new `config.dev-*.ini`.
158
+
159
+ ### Other
160
+ - Pre-existing wart (untouched): unescaped `$emailAddress` in the walmart branch query.
63
161
  - Pending as of 2026-06-12: MSP/CUSTOMER bypass for the ownership check (MSP session's
64
162
  `togadeskClientId` is the MSP's own client id — matches their customers' tickets since those
65
- carry the MSP's clientid, but verify when an MSP views a ticket whose clientid differs);
66
- togaview changes were uncommitted at session end.
163
+ carry the MSP's clientid, but verify when an MSP views a ticket whose clientid differs).
164
+ - TRUE-80671 changes were **working-tree only** (branch `TRUE-80671`, nothing committed) at
165
+ session end.
67
166
 
68
167
  ## Change history
168
+ - 2026-08-05 (TRUE-80671, uncommitted) — **Ported both TRUE-78835 ownership checks (ticket view
169
+ + attachment download) into the towfoundation and newcenturyholdingsllc forks**, which had
170
+ neither — a live cross-tenant IDOR on both hosts; verified both directions live. Guarded the
171
+ requester name-split in all three forks (`Undefined offset: 1` → hard 500 for mononyms/shared
172
+ mailboxes); fixed the towfoundation fork serving **RUMC's** custom-field definitions
173
+ (hardcoded client name → `(int) $ticketDetails->clientid`); added the missing `(int)` cast on
174
+ `$_GET['ticketId']` in the towfoundation fork; removed
175
+ `sqlProtect(sqlEscape())` and switched email lookups to `=`. Recorded the retail shared-clientId
176
+ IDOR and the S3-vs-local attachment-download bug as open items. (mhammontree)
69
177
  - 2026-06-12 — file-attachment IDOR fix (ownership check now covers fileId branch);
70
178
  email-key fallback for staplesprotection (mhammontree)
71
179
  - 2026-06 — ownership check added (ticket view), deactivated-seat gate restricted to
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Git & php -l Tooling Against the `_/` App Root (MSYS path mangling)
3
+ framework: "1.0"
4
+ repo: togaview
5
+ project: TOGa View
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-08-05
10
+ owners: ["mhammontree"]
11
+ files:
12
+ - _/app/framework.php
13
+ - _/app/frameworkindex.php
14
+ related:
15
+ - 1.0/apps/togaview/architecture.md
16
+ ---
17
+
18
+ ## Summary
19
+ In **Git Bash on Windows**, any command argument beginning with `_/` is rewritten by MSYS
20
+ path conversion before the tool sees it. So `git diff HEAD -- _/app/framework.php` returns an
21
+ **empty diff** on a file you definitely changed, and `php -l _/app/framework.php` misfires the
22
+ same way. **Quoting does not help** — MSYS converts quoted arguments too.
23
+
24
+ This is not an edge case in togaview: the app root **is** `_/` (`App_Framework` resolves
25
+ `__APPROOT__` by walking up to a folder named `_`), so every framework file in the repo hits it.
26
+ In TRUE-80671 it caused **two separate subagent reviews to report a changed file as unchanged**,
27
+ which nearly sent a security fix out unreviewed.
28
+
29
+ ## How it works — the safe forms
30
+
31
+ Use any one of these (all verified):
32
+
33
+ ```bash
34
+ MSYS_NO_PATHCONV=1 git diff HEAD -- "_/app/framework.php" # disable conversion
35
+ git diff HEAD -- "./_/app/framework.php" # leading ./ defeats it
36
+ git diff HEAD -- "*app/framework.php" # glob pathspec
37
+ ```
38
+
39
+ The same applies to `php -l` and to anything else taking a `_/`-prefixed path.
40
+
41
+ ## Rules
42
+
43
+ 1. **Any review instruction, script, or agent prompt that names a path under `_/` must use one
44
+ of the safe forms above.** Prefer `MSYS_NO_PATHCONV=1`, which fixes the whole command.
45
+ 2. **An empty diff on a file you know you changed is this bug**, not evidence of no change.
46
+ Confirm with `git status --porcelain` or a bare `git diff HEAD` (no pathspec) before
47
+ concluding anything.
48
+ 3. Reviewing "no changes found" is a **failed review**, not a passing one — re-run it.
49
+
50
+ ## Gotchas / known issues
51
+ - PowerShell and cmd are unaffected; the bug appears only under Git Bash/MSYS, so a teammate on
52
+ a different shell may not reproduce it.
53
+ - A bare `git diff HEAD` with **no** pathspec is always safe — path conversion only touches
54
+ arguments, and reviewing the whole diff avoids the trap entirely.
55
+
56
+ ## Change history
57
+ - 2026-08-05 — Documented after two TRUE-80671 subagent reviews silently reported changed
58
+ `_/app/framework.php` as unchanged. (mhammontree)
@@ -5,7 +5,7 @@ project: Library
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-08-04
8
+ updated: 2026-08-05
9
9
  owners: [jcardinal, rgirish, mhammontree]
10
10
  files: []
11
11
  related:
@@ -192,7 +192,10 @@ In code you rarely write raw column types — an `App_Model` declares each colum
192
192
  * Never interpolate raw user input into a query string. In order of preference:
193
193
  1. **Use the `App_Model` layer** — it builds and escapes queries for you.
194
194
  2. **When writing SQL by hand, escape every interpolated value with `App_Database::sqlEscape()`** before placing it in the query string. In `browser/` action files the helper `getVarEscaped('field')` wraps `sqlEscape()` for request values.
195
- * `App_Database::sqlEscape()` escapes a value for safe interpolation (and handles arrays/objects recursively). `App_Database::sqlProtect()` is a **stricter, lossy filter** that *strips* `% \ / * " '` and the literal ` or` — use it only to sanitize free-form search input, never for values you intend to store intact.
195
+ * `App_Database::sqlEscape()` escapes a value for safe interpolation (and handles arrays/objects recursively). **`sqlEscape()` alone is the injection control — nothing needs to be added to it.**
196
+ * **NEVER wrap an escaped value in `sqlProtect()`.** `App_Database::sqlProtect()` is `str_ireplace(['%','\\','/','*',' or','"',"'"], '', $input)` — it **deletes** those characters, including the backslashes `sqlEscape()` just added. `sqlProtect(sqlEscape($email))` turns `o'brien@x.com` into `obrien@x.com`, so the lookup silently finds nothing; it also mangles ordinary strings ("Victor Ortiz" → "Victt Ortiz", since ` or` is stripped case-insensitively). It is **data corruption, not defense in depth**. 39 such sites across 7 togaview files were removed in TRUE-80671.
197
+ * **If you remove `sqlProtect()` from a `LIKE` comparison, change `LIKE` to `=` in the same commit.** `sqlProtect` stripping `%` was the only thing stopping a `%` in user data from acting as a wildcard; removing it while keeping `LIKE` introduces over-matching. (Safe for email columns, which are case-insensitive VARCHARs — verify collation before generalizing.)
198
+ * `sqlProtect()`'s only legitimate use is sanitizing free-form **search** input, never a value you intend to match or store intact.
196
199
  * Legacy uses **mysqli** (via `App_Database`), not PDO/prepared statements; escaping discipline at the call site is the control.
197
200
 
198
201
  ```php
@@ -347,7 +350,7 @@ class App_Model_Client extends App_Model {
347
350
  * **Read results:** `fetchRow(&$res)`, `fetchOne(&$res, $rowIndex, $colIndexOrField)`, `numRows(&$res)`.
348
351
  * **Writes:** `affectedRows($link)`, `getInsertId($link)`.
349
352
  * **Cached lookups:** `lookupField($table, $aryLookup, $returnField, $link, $throwIfNotFound)`, `lookupRecord($table, $idValue, $link)`.
350
- * **Escaping:** `sqlEscape()` (safe interpolation) and `sqlProtect()` (strict, lossy filter) — see SQL Injection Prevention.
353
+ * **Escaping:** `sqlEscape()` (the injection control — safe interpolation) and `sqlProtect()` (**lossy character-stripping filter; never wrap an escaped value in it**) — see SQL Injection Prevention.
351
354
 
352
355
  ## browser/ — Server-Rendered UI
353
356
 
@@ -15,7 +15,7 @@
15
15
  | [_Cloud S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` centralizes AWS SDK S3 usage for the 2.0 stack so the `S3Client` never leaks into workers or app code. | _underscore/Cloud.php |
16
16
  | [_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps](features/component-model-namespace-registration.md) | Every **project-local** `_Component_*` and `_Model_*` class in a 2.0 app **must declare the project namespace** at the top of the file: ```php namespace <NAMESP | _underscore/Loader.php, worker2/_.php, api2/_.php, worker2/Component/Forecast/Db/Db.php, worker2/Component/Forecast/SaleImport/SaleImport.php, api2/Component/Api/Netsuite/Netsuite.php, _underscore/Component/Api/Paypal/Paypal.php |
17
17
  | [Re-pointing a DB alias mid-request (_Database::register park/restore)](features/database-alias-repointing.md) | `_Database` keys **all live per-database runtime state by the connection ALIAS** (`Client` / `_underscore::DB_CLIENT`, `ClientLogs`, `Archive`), **not** by the | _underscore/Database.php, _underscore/Query.php, api2/Component/Api/V2/V2.php, api2/Component/Api/CrossClient/CrossClient.php |
18
- | [2.0 Email Send Pipeline (queue + Send worker)](features/email-send-pipeline.md) | In 2.0, `_Email::send()` **does not transmit** — it queues the message. | _underscore/Email.php, worker2/Worker/Infrastructure/Email/Send.php |
18
+ | [2.0 Email Send Pipeline (queue + Send worker)](features/email-send-pipeline.md) | In 2.0, `_Email::send()` **does not transmit** — it queues the message. | _underscore/Email.php, _underscore/String.php, worker2/Worker/Infrastructure/Email/Send.php |
19
19
  | [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
20
20
  | [Error Reporting — Issue/Event Capture, Fingerprinting & Aggregation](features/error-reporting-issue-event.md) | Platform-wide error reporting for TOGA 2.0, built on an **Issue / Event** aggregation model in the **shared Core Logs DB**. | _underscore/Error.php, _underscore/Database.php, _underscore/Exception/Business.php, api2/Controller/Index.php, worker2/Controller/Index.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, _underscore/Model/Core/Logs/IssueFingerprint.php, _underscore/Model/Core/Logs/IssueClickupTask.php, _underscore/Model/Core/Logs/IssueEmailAddress.php, _underscore/Model/Core/Logs/IssueAreaOwner.php, dbchanges2/Logs/2026-07-30a - Error reporting Issues and Events.sql, dbchanges2/Logs/2026-08-01a - Rename tables to singular.sql, dbchanges2/Logs_Client/2026-08-01a - Drop Error table.sql, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql |
21
21
  | [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
@@ -29,8 +29,10 @@
29
29
  | [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/Query.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, api2/Controller/Index.php |
30
30
  | [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22b - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22c - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22 - PersonaTranslationsFrench.sql, toga2-commerce/src/pages/Account/view/MySettingsView.tsx |
31
31
  | [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
32
+ | [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` is the 2.0 framework's static string utility class. | _underscore/String.php |
32
33
  | [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php |
33
34
  | [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
35
+ | [TogaIQ Gateway Client (_Component_Api_Togaiq) — AI generate/translate from 2.0](features/togaiq-gateway-client.md) | `_Component_Api_Togaiq` is the 2.0 framework's client for the **TogaIQ** (Talos) AI gateway. | _underscore/Component/Api/Togaiq/Togaiq.php, _underscore/ApiRequest.php |
34
36
  | [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Growrk/2026-07-13a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
35
37
  | [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | |
36
38
  | [USPS DPV Deliverability Verdict (is this address actually insurable/shippable?)](features/usps-dpv-deliverability.md) | **USPS returning HTTP 200 with a populated address is NOT evidence that the address is deliverable.** The authoritative signal is USPS's **DPV (Delivery Point V | _underscore/Component/Library/Carriers/Usps/Usps.php, _underscore/Model/Client/Address.php, _underscore/Model/Rate/Entitlement.php |
@@ -14,6 +14,7 @@ related:
14
14
  - ../../worker2/features/oneuptime-worker2-monitoring.md
15
15
  - ../../worker2/features/creating-worker-actions.md
16
16
  - ../../worker2/features/talos-transcript-ingestion.md
17
+ - togaiq-gateway-client.md
17
18
  ---
18
19
 
19
20
  ## Summary
@@ -49,11 +50,21 @@ case-insensitive scan of already-set headers, so a caller-supplied `Content-Type
49
50
  per attempt — that also gives you one api-log row per attempt.
50
51
  - **Transport failures surface as a thrown Exception** from `execute()` (the equivalent of
51
52
  `curl_error()`), not a return value. Catch it if your method has an error-return contract.
53
+ - **The outbound log row is COMMITTED immediately — use it as proof a call happened.** When
54
+ logging is on (the default), `execute()` writes a `_Model_Client_Logs_Api` row with
55
+ `direction = 'OUT'` and calls `transactionCommit(DB_CLIENT_LOGS)` (`ApiRequest.php` ~lines
56
+ 253–270), so the row **survives even if the surrounding request later rolls back**. That makes
57
+ `SELECT … FROM Logs_<Client>.Api WHERE direction = 'OUT'` definitive evidence of whether an
58
+ outbound call was ever *attempted* — the first thing to check when an integration "returns
59
+ nothing" (no row = the code never ran / was never deployed; a row with a null `responseCode` =
60
+ the two-phase logging issue below).
52
61
  - **Api logging depends on `DB_CLIENT_LOGS` being registered.** The logging branch writes via
53
62
  `_Model_Client_Logs_Api`, whose `DATABASE` const is `_underscore::DB_CLIENT_LOGS`. In a context
54
63
  that hasn't registered it (most workers, CLI harnesses) logging is a **silent no-op** — or
55
- throws `Unknown database 'ClientLogs'`. Either register the logs DB under that alias or pass
56
- `setLogging(false)` deliberately. See
64
+ throws `Unknown database 'ClientLogs'`. In a bare CLI script that never called
65
+ `_Database::registerClientDatabases()`, that throw comes out of `execute()` itself and **looks
66
+ like a broken integration** when the API call is fine. Either register the logs DB under that
67
+ alias or pass `setLogging(false)` deliberately. See
57
68
  [Creating Worker Actions](../../worker2/features/creating-worker-actions.md#gotchas).
58
69
  - **OPEN / not fixed: logging happens in two halves around the HTTP call.** `execute()` inserts
59
70
  the request row and **commits before** the call, then `save()`s the response fields **after**.
@@ -64,6 +75,13 @@ case-insensitive scan of already-set headers, so a caller-supplied `Content-Type
64
75
  pending architecture review. Do **not** work around it per-caller.
65
76
 
66
77
  ## Change history
78
+ - 2026-08-05 — Documented (no code change) that the `direction = 'OUT'` api-log row is
79
+ **committed immediately** inside `execute()` (`transactionCommit(DB_CLIENT_LOGS)`), so it
80
+ survives a later rollback and `Logs_<Client>.Api` is definitive evidence of whether an outbound
81
+ call was attempted — the first diagnostic for a silent integration. Also noted that in a bare CLI
82
+ script without `_Database::registerClientDatabases()`, the logging branch throws
83
+ `Unknown database 'ClientLogs'` out of `execute()` and masquerades as a broken integration.
84
+ Surfaced building `_Component_Api_Togaiq`. (bala)
67
85
  - 2026-07-28 — Broadened from the Content-Type fix to the general `_ApiRequest` contract:
68
86
  documented the non-assoc `json_decode` response (returns `stdClass`), the `ENCODE__JSON`
69
87
  re-encode losing `JSON_UNESCAPED_*`, `setAutoRetry()`'s flat-delay/retry-all behavior,