toga-ai 1.0.873 → 1.0.875

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.
@@ -3,7 +3,7 @@
3
3
  | Doc | Summary |
4
4
  |-----|---------|
5
5
  | [Test (test) Architecture](architecture.md) | Repo of ad-hoc developer scripts (not a deployed app); `team/` is the shared, documented surface — open to find/run a one-off script or add one. |
6
- | [2.0 Deployment — Per-Client SQL Generator](features/2-0-deployment-client-sql.md) | Standalone script that fans one SQL change-set across all 2.0 client DBs (prints to stdout, executes nothing); open when a schema/data change must run identical |
6
+ | [2.0 Deployment — Per-Client SQL Generator (superseded)](features/2-0-deployment-client-sql.md) | SUPERSEDED by the Tools Multi-Client SQL tool — old offline script that fans one SQL change-set across a static list of 2.0 client DBs; open only when working o |
7
7
  | [Active Directory Authentication Test](features/active-directory-auth-test.md) | Interactive CLI REPL to test Active Directory authentication via `App_ActiveDirectory::isAuthenticated()`; open to verify AD login from a terminal. |
8
8
  | [Compass retrofix2 — Retroactive Data-Fix SQL Generator](features/compass-retrofix2-sql-generator.md) | Standalone 1.0 script emitting review-first SQL to retroactively repair historical Compass USA (`Client_Compass`, 2.0) order data broken by 50+ past bugs; open |
9
9
  | [Create Elastic Beanstalk Environment (script)](features/create-elastic-beanstalk.md) | Standalone constants-driven generator that PRINTS a ready-to-paste AWS CloudShell `create-environment` command for a new EB env in an existing app (never calls |
@@ -61,7 +61,7 @@ Run directly from a local dev environment (CLI or browser, depending on the scri
61
61
  | Script | Runs on | Targets | Doc |
62
62
  |---|---|---|---|
63
63
  | `aws/create_elastic_beanstalk.php` | standalone | AWS (EB) | [create-elastic-beanstalk](./features/create-elastic-beanstalk.md) |
64
- | `2.0 deployment/generate_client_sql.php` | standalone | 2.0 client DBs | [2-0-deployment-client-sql](./features/2-0-deployment-client-sql.md) |
64
+ | `2.0 deployment/generate_client_sql.php` | standalone | 2.0 client DBs (superseded by Tools Multi-Client SQL) | [2-0-deployment-client-sql](./features/2-0-deployment-client-sql.md) |
65
65
  | `forecast-netsuite/discrepancy_analysis.php` | 1.0 | Forecast DB vs NetSuite | [forecast-netsuite-discrepancy-analysis](./features/forecast-netsuite-discrepancy-analysis.md) |
66
66
  | `tableViewBuilder/TableViewGenerator.php` | standalone | 2.0 TableViews | [tableview-builder](./features/tableview-builder.md) |
67
67
  | `active_directory_authentication_test.php` | 1.0 | Active Directory | [active-directory-auth-test](./features/active-directory-auth-test.md) |
@@ -1,41 +1,45 @@
1
1
  ---
2
- title: 2.0 Deployment — Per-Client SQL Generator
2
+ title: 2.0 Deployment — Per-Client SQL Generator (superseded)
3
3
  framework: "1.0"
4
4
  repo: test
5
5
  project: Test
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-09-22
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - test/team/2.0 deployment/generate_client_sql.php
13
+ - test/team/2.0 deployment/INPUT.sql
13
14
  - test/team/2.0 deployment/Clients_Db.txt
14
- - test/team/2.0 deployment/Client.sql
15
+ - test/team/2.0 deployment/Logs_Clients_Db.txt
16
+ - test/team/2.0 deployment/NetSuite_Clients_Db.txt
15
17
  related:
16
18
  - ../architecture.md
19
+ - ../../tools/features/multi-client-sql.md
17
20
  ---
18
21
 
19
- Standalone script that fans one SQL change-set across all 2.0 client DBs (prints to stdout, executes nothing); open when a schema/data change must run identically per 2.0 client.
22
+ SUPERSEDED by the Tools Multi-Client SQL tool — old offline script that fans one SQL change-set across a static list of 2.0 client DBs; open only when working offline.
20
23
 
21
24
  ## Summary
22
25
 
23
- `team/2.0 deployment/generate_client_sql.php` fans a single SQL change-set out across **all 2.0 client databases**. **Standalone** (no `App_` framework) — pure file processing. Use it when a schema/data change must run identically against every 2.0 client DB and you want one script to paste into a SQL client.
26
+ Use [Tools Multi-Client SQL](../../tools/features/multi-client-sql.md) instead (live DB lists, module lists). This standalone script (no `App_` framework) stays as the offline fallback. Prints to stdout; executes nothing.
24
27
 
25
28
  ## How it works
26
29
 
27
- 1. Reads `Clients_Db.txt` — newline-separated list of client database names (blank lines ignored).
28
- 2. Reads `Client.sql` — the SQL template (e.g. `CREATE TABLE` / `ALTER` statements) to apply per client.
29
- 3. For each client, emits a banner comment, `` USE `<client>`; ``, then the template SQL.
30
- 4. Prints the combined SQL to **stdout** (with a generated-on header and client count). Does **not** execute anything — the developer reviews the output and runs it manually.
31
-
32
- Edit `Client.sql` to hold the change, keep `Clients_Db.txt` current, then run `php generate_client_sql.php` (or via browser) and run the output against the target cluster.
30
+ 1. Reads `INPUT.sql` — the SQL to apply per DB.
31
+ 2. Reads one static list file; switch lists by commenting between `Clients_Db.txt` / `Logs_Clients_Db.txt` / `NetSuite_Clients_Db.txt` in the script.
32
+ 3. Per DB: banner comment, `` USE `<db>`; ``, then the SQL. Prints the combined script with a header and DB count.
33
33
 
34
34
  ## Gotchas
35
35
 
36
- - Output is only as correct as `Clients_Db.txt` — a stale/missing client is silently skipped. Refresh the list before a deployment.
37
- - The script wraps each client block in `` USE `db`; `` — make sure `Client.sql` does **not** also switch databases or it will fight the `USE`.
36
+ - Lists are static — a stale/missing DB is silently skipped. The Tools tool reads them live.
37
+ - Do not put your own `USE` in `INPUT.sql`; it fights the generated `USE`.
38
38
 
39
39
  ## Related
40
40
 
41
41
  - [Test architecture](../architecture.md)
42
+
43
+ ## Change history
44
+
45
+ - 2026-09-22 — Marked superseded by Tools Multi-Client SQL; corrected input file (`INPUT.sql`) and list-switching. (jcardinal)
@@ -12,6 +12,7 @@
12
12
  | [/errors Curation Console (Tools → shared Core Logs DB)](features/errors-curation-console.md) | Internal-only 1.0 Tools triage/curation screen (`/errors`) for the 2.0 Issue/Event error pipeline, reading the shared Core Logs DB via `db_toga2logs`; curates, |
13
13
  | [GitHub Audit (monthly code-churn report)](features/github-audit.md) | Internal monthly code-churn report at `/developers/github-audit` (lines added/removed across every `agilantsolutions` repo for one month); open for the two-phas |
14
14
  | [Legacy Email Notifier (tools /email-migration/notify)](features/legacy-email-notifier.md) | SSO-gated admin tool at `/email-migration/notify` that emails employees a digest of mail still hitting their legacy domains (asks senders to switch to `@togatec |
15
+ | [Tools — Multi-Client SQL (per-client-DB SQL fan-out)](features/multi-client-sql.md) | Tools `/developers/multi-client-sql` — fans one SQL change-set across a live list of 2.0 client/log/archive/module DBs into a copy/download script (never execut |
15
16
  | [Tools MVC — Routing, CSRF & App_Database Access Patterns](features/mvc-data-access-patterns.md) | Load-bearing 1.0 (App_) conventions for adding a Tools page — routing (get/post), Origin-based CSRF, and App_Database access — plus the footguns (global transac |
16
17
  | [OneUptime Monitor Status Panel & Outage Alerting (tools /developer)](features/oneuptime-monitor-status-panel.md) | Live operational monitor column on the `/developer` wall-display dashboard (state-adaptive: calm green when healthy, loud amber/red when broken) fed by a same-o |
17
18
  | [Tools Persona-Gated Navigation (App_Nav)](features/persona-gated-navigation.md) | Tools two-level persona-gated navigation (`App_Nav`), a hard-coded `definition()` array driving left rail + dashboard and the single source of route→persona gat |
@@ -31,6 +31,7 @@ related:
31
31
  - ./features/saml-sso-auth.md
32
32
  - ./features/persona-gated-navigation.md
33
33
  - ./features/developer-tools.md
34
+ - ./features/multi-client-sql.md
34
35
  - ./features/oneuptime-monitor-status-panel.md
35
36
  - ./features/github-audit.md
36
37
  - ../library/architecture.md
@@ -69,6 +70,7 @@ Tools is no longer only a click-through internal-tool shell: `/developer` (renam
69
70
 
70
71
  - **No cross-origin browser dependency any more — it is server-proxied.** Every fetch in this app is now same-origin and authenticated by the SSO session cookie, including the monitor column: the browser GETs `/v2/monitors/status/` (`App_Clickup_Monitors`), and the server does the cross-origin POST to the OneUptime **public** status-page API, reduces ~250 KB → ~1 KB, caches ~5s (APCu) and returns the api2 envelope. This removed the app's last cross-origin browser dependency and the CORS / edge-cache / autoplay coupling it carried; the status-page ID moved from a hardcoded HTML constant into config (`[oneuptime] status_page_id`, default `DEFAULT_STATUS_PAGE_ID`).
71
72
  - **Iframed pages carry their own permission policy.** `mvc/developer/get.php` embeds its asset in an iframe, and browser autoplay policy is scoped **per-iframe** — an iframed page that must play audio needs `allow="… autoplay"` on the frame or it is blocked silently. Unattended displays additionally never receive the user gesture browsers require before audio plays.
73
+ - **External reads:** `/developers/multi-client-sql` reads Core via `[database_toga2core]` and GitHub (dbchanges2 `_main`) via the `GITHUB_TOKEN` env var only (EB environment property in prod) — see `features/multi-client-sql.md`.
72
74
  - **`crypto.randomUUID()` is undefined on a non-secure origin.** Browser code served over plain HTTP by hostname/LAN IP (not `localhost`) has no secure context, so `crypto.randomUUID()` and `crypto.subtle` throw. The dashboard now guards its per-request `transactionId` behind a helper (randomUUID → `getRandomValues` v4 → non-crypto id); any browser code in this app that must run over a non-secure origin must do the same rather than call `crypto.randomUUID` unguarded.
73
75
 
74
76
  ## Adding a tool
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: Tools — Multi-Client SQL (per-client-DB SQL fan-out)
3
+ framework: "1.0"
4
+ repo: tools
5
+ project: Tools
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-09-22
10
+ owners: [jcardinal]
11
+ files:
12
+ - tools/_/app/developers/multiclientsql.php
13
+ - tools/mvc/developers/multi-client-sql/get.php
14
+ - tools/mvc/developers/multi-client-sql/post.php
15
+ - tools/_/app/nav.php
16
+ related:
17
+ - ../architecture.md
18
+ - ./developer-tools.md
19
+ - ./persona-gated-navigation.md
20
+ - ../../test/features/2-0-deployment-client-sql.md
21
+ ---
22
+
23
+ Tools `/developers/multi-client-sql` — fans one SQL change-set across a live list of 2.0 client/log/archive/module DBs into a copy/download script (never executes); open to use/extend it or for the `Databases` reserved-word gotcha.
24
+
25
+ ## Summary
26
+
27
+ Replaces `test/team/2.0 deployment/generate_client_sql.php`. Development Team persona **only** (narrowed in `nav.php` + hardcoded `requireAuth` in `get.php` and `post.php`). Class `App_Developers_MultiClientSql`. Output is reviewed and run by the developer — the tool never executes SQL.
28
+
29
+ ## How it works
30
+
31
+ 1. Paste SQL, pick a list, untick/filter DBs. Generate builds the script **in the browser**: header + per-DB banner + `` USE `db`; `` + SQL. Copy / Download `.sql`. Warns if the SQL has its own `USE`.
32
+ 2. `post.php` action `lists` only returns JSON lists.
33
+ 3. Lists are dynamic — no tools commit when clients change:
34
+ - **All Clients / Client Logs / Client Archives** — live read of `Core.Clients` joined to `Core.Databases` on `clientDatabaseId` / `logDatabaseId` / `archiveDatabaseId`, via config `[database_toga2core]`. Every row (no active flag).
35
+ - **Module lists** — one per folder under `dbchanges2/_modules` (branch `_main`). A client is in the list when `Client_<Name>/_modules.txt` names the module (case-insensitive); DB name = client folder name. GitHub GraphQL, 2 calls (folder list, then all `_modules.txt` as aliased blobs). APCu cache 5 min + 7-day "last good" fallback shown with a warning. Warns when a module client is not in `Core.Clients`. One retry on a dropped connection; log names the failed step (`folder list` / `_modules.txt read`).
36
+ 4. GitHub token: **only** the `GITHUB_TOKEN` env var (read via `getenv` + `$_SERVER` + `$_ENV`, because php-fpm `clear_env`). No config fallback (developer decision). Own curl, not `App_Design_Github`. Needs read on `agilantsolutions/dbchanges2`; set as an EB environment property in prod.
37
+ 5. Every DB/folder name is checked against `^[A-Za-z0-9_]{1,64}$` before it goes into GraphQL or a `USE` line. Page writes via `textContent`/`.value` only.
38
+
39
+ ## Gotchas
40
+
41
+ - `Databases` is a MySQL reserved word: `FROM Databases` / `JOIN Databases AS X` fails with error 1064. Backtick it (`` `Databases` ``) or qualify it (`Core.Databases`).
42
+ - dbchanges2 has **no** `_production` branch; its deployed branch is `_main`.
43
+ - A missing `[database_...]` config section is not catchable in 1.0 — `App_Error` renders its HTML error page and exits. The tool pre-checks `config['database_toga2core']` and throws a user-safe message. Local `config.dev-*.ini` files may lack that section.
44
+ - CSS: a class rule with `display:flex` overrides the HTML `hidden` attribute — add `.scope [hidden]{display:none !important}`.
45
+
46
+ ## Related
47
+
48
+ - [Developers folder tools](./developer-tools.md)
49
+ - [Old offline script (test)](../../test/features/2-0-deployment-client-sql.md)
50
+
51
+ ## Change history
52
+
53
+ - 2026-09-22 — Created; shipped and verified in production. (jcardinal)
@@ -15,7 +15,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
15
15
  - **test** (Test) — 16 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
16
16
  - **toga** (TOGa) — 3 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
17
17
  - **store** (Store) — 3 doc(s) → [1.0/apps/store/INDEX.md](1.0/apps/store/INDEX.md)
18
- - **tools** (Tools) — 18 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
18
+ - **tools** (Tools) — 19 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
19
19
 
20
20
  ## 2.0 framework
21
21
 
@@ -0,0 +1,212 @@
1
+ ---
2
+ type: session
3
+ slug: commerce-sso-and-cache
4
+ title: Commerce persisted-cache, session-gating and SSO sign-in screen
5
+ author: apeterson
6
+ repos: [toga2-commerce]
7
+ framework: "2.0"
8
+ client: shared
9
+ status: active
10
+ created: 2026-09-23
11
+ updated: 2026-09-23
12
+ ---
13
+
14
+ # Session: commerce-sso-and-cache
15
+ **Date:** 2026-09-23
16
+ **Project/Repo:** toga2-commerce (2.0), branch `TRUE-81991`
17
+ **Task:** Implement the 2026-09-03 cache-diagnosis plan in toga2-commerce, then build an SSO sign-in screen for SSO tenants and consolidate how the app decides someone is signed in.
18
+
19
+ ---
20
+
21
+ ## What WORKED
22
+
23
+ **The 2026-09-03 plan, implemented and verified.**
24
+ - App default `staleTime` 24h → `0` plus `refetchOnWindowFocus: false` (`src/App.tsx`); heavy hooks
25
+ got `staleTime: 30_000` + `gcTime: 1h` (`src/pages/BundleView/hooks/useBundleQueryData.ts:26,42`,
26
+ `src/pages/Home/viewModel/useHomeViewModel.ts:150,175,201`). Evidence: `npx tsc --noEmit -p
27
+ tsconfig.app.json` exit 0 and `npx vite build` succeeded after each change.
28
+ - `src/lib/queryCacheRestoration.ts` (new) — every cache clear now waits for restoration, cancels
29
+ in-flight queries, then clears; capped at 1.5s; an epoch counter makes a superseded clear no-op.
30
+ Persister set to `throttleTime: 0` (the 1000ms default let a queued write land AFTER the clear).
31
+ - `src/lib/authStorage.ts` (new) — one `AUTH_STORAGE_KEYS` list used by BOTH logout paths, and by
32
+ `resetClientState()` which now runs on login as well as logout.
33
+ - Build-id `buster` via `__BUILD_ID__` (`vite.config.ts` `define`, declared in `src/vite-env.d.ts`).
34
+ Verified in the bundle: 0 unresolved `__BUILD_ID__` occurrences, the `"dev"` literal present once.
35
+
36
+ **Real bugs found and fixed (each confirmed in source before changing anything).**
37
+ - `AUTH_STORAGE_KEYS` contained `"zu-cart-store"`, which is not a real key — the cart persists under
38
+ `"zu-cart"` (`src/stores/useCartStoreZu.ts:956`). The previous user's cart survived a 401 logout.
39
+ - Clearing a persisted key is not enough: the zustand store must ALSO be reset in memory or the next
40
+ `set()` re-persists the old user's state. Added a `clearSettingsModal` action (that store had none)
41
+ and wired `useBundleBuilderZu` / `useUserStore` resets into `resetClientState()`.
42
+ - `fetchPublicToken()` wrote the ANONYMOUS token to `localStorage` under `"accessToken"` — the same
43
+ key a real login writes — so every "is there an accessToken?" check was true for a logged-out
44
+ visitor. Root-caused: the public token is now a module variable in `src/api/axiosInstance.ts`,
45
+ never stored, and its refresh token is not stored at all.
46
+ - The 401 handler gated logout on `"zu-user"`, a zustand store that clears itself independently. With
47
+ that key gone but the session live, logout was skipped and the app sat looping on 401.
48
+
49
+ **Security fixes (all found by the `cso` reviewer, all verified in source).**
50
+ - Open redirect on the post-auth return path (`src/utils/postAuthRedirect.ts`). Visiting
51
+ `https://<tenant>//evil.com` gives `pathname === "//evil.com"`, which the browser treats as
52
+ protocol-relative — so after a SUCCESSFUL login the user went to another origin with a live
53
+ session. Now only a single-slash same-origin path is stored or restored.
54
+ - `window.location.href = ssoUrl` with no scheme check. Now must be `https:`, checked where fetched
55
+ AND at the click; a bad value means "not SSO" so the tenant falls back to the credential form.
56
+ - Token refresh could run under the wrong identity. Made single-flight, then identity-scoped with an
57
+ epoch in `src/lib/session.ts` bumped on sign-in, sign-out and view-as, plus a second guard
58
+ comparing the stored refresh token.
59
+ - View-as swap stored `response?.data.tokens.access` with only `response` optional-chained — a
60
+ malformed reply wrote the literal string `"undefined"` (truthy), so the app sent `Bearer undefined`
61
+ on every call. Now both tokens are validated and the session is left untouched on a bad reply.
62
+
63
+ **SSO sign-in screen built.** Detection already existed inside `useAuthenticationFlow` as a mutation
64
+ that jumped straight to the IdP — the user never saw a page. Extracted to `src/hooks/useClientSsoUrl.ts`
65
+ (React Query, so the auth flow and the login page share one cached answer). `/login` now renders
66
+ `src/pages/Login/view/SsoSignIn.tsx` for SSO tenants, else the credential form. Design came from the
67
+ Claude Design project "TOGa Supply - SSO Sign In", rebuilt with commerce's own logo, theme tokens and
68
+ per-tenant FIELDS copy. Every logout path already lands on `/login`, so SSO users get the SSO screen
69
+ on sign-out with no extra code — verified by reading `src/routes.tsx`, NOT by running it.
70
+
71
+ **First unit-test harness in the repo.** vitest + jsdom, `npm test` / `npm test:watch`,
72
+ `vitest.config.ts`. 19 tests pass. Each was proven to fail on the pre-fix code first: reverting
73
+ `hasSession` turned **5 red**; removing the single-flight and identity guards turned **3 red**
74
+ (including both race tests). Race tests use a held-open promise, not real timing, so no flake.
75
+
76
+ ---
77
+
78
+ ## What did NOT work — DO NOT RETRY THESE
79
+
80
+ - **Falling back to the anonymous token when a session exists but `accessToken` is missing.** I wrote
81
+ this thinking it fixed "a 401 with no explanation". It is backwards — the 401 IS the point. The
82
+ `cso` agent confirmed against api2 (`Component/Api/V2/V2.php:578-598`): `/auth/public` mints a
83
+ FULLY VALID JWT carrying `KEY_PUBLIC_ROLE`, and its client comes from the **Domains row, not the
84
+ user**. So the request would NOT 401 — it would return 200 with public-scoped rows (or `EZ-1`),
85
+ the refresh/logout path would never fire, and the tab would stay silently anonymous under a real
86
+ session. On a domain whose `clientId` differs from the signed-in user's, a write could land in the
87
+ wrong tenant's database. **Reverted.** A live session with no `accessToken` must send NO
88
+ `Authorization` header so it 401s.
89
+ - **`hasSession()` as `Boolean(localStorage.getItem("user"))`.** Too loose — `"{}"`, `"null"` or a
90
+ half-written value booted the app into the signed-in state with no identity behind it. It must
91
+ parse and require a `uuid`. (This is one of the 5 tests that goes red on the loose version.)
92
+ - **Anchoring the SSO footer inside the form column.** It only worked by accident of the nearest
93
+ positioned ancestor, and escaped the `xl:w-96` width. The footer belongs to the left PANEL — it is
94
+ now `SsoSignInFooter`, rendered as a sibling of the form column.
95
+ - **Prefixing every per-session localStorage key so logout can clear by prefix.** My idea; the `cto`
96
+ agent rejected it with a concrete reason: renaming every zustand `persist` name silently orphans
97
+ every existing user's persisted state on deploy, and touches every `localStorage.getItem("zu-user")`
98
+ call site scattered through `src/api/`. Cheaper answer, which supply already uses: plain
99
+ `localStorage.clear()` on logout, since every key commerce writes is session state.
100
+ - **`grep --include=*.ts` under zsh** fails with "no matches found" when the glob is unquoted —
101
+ quote it (`--include="*.ts"`). Cost two wasted tool calls.
102
+ - **Python string-replace edits against this repo assuming space indentation.** Source is TABS, and
103
+ one line in `axiosInstance.ts` had a trailing-space blank line. Two `AssertionError`s before I
104
+ matched the real bytes. Check with `sed 's/\t/<TAB>/g'` first.
105
+
106
+ ---
107
+
108
+ ## Not tried yet (candidates for next session)
109
+
110
+ 1. **Compass Canada production SSO** — `https://compasscanada.togacommerce.com/home` sends users to
111
+ the credential form though the client uses SSO. Diagnostic SQL was handed over, NOT run. Candidate
112
+ causes in order: no `Domains` row matching the exact `https://compasscanada.togacommerce.com`
113
+ string; no `ClientAuthentications` row for that `clientId`+`environmentId`; `type` not the literal
114
+ `'SSO'`; or the PUBLIC role lacking `AclFieldPermissions` to read `ClientAuthentications.type`
115
+ (the call runs pre-login on the public token, so a null field fails silently). Note the production
116
+ build is plain `vite build` with NO `--mode`, so `MODE` is `production` and the query asks for
117
+ `Environments.slug LIKE 'PRODUCTION'`.
118
+ 2. **blox `fetchPublicToken` defect** (`toga-blox-npm/src/api/auth.ts:48-60`) — same bug commerce just
119
+ fixed locally; supply works around it in three places. Needs its own ticket; do NOT bundle it.
120
+ 3. **CTO-ranked, not done:** replace the `window.history.pushState`/`replaceState` monkey-patch in
121
+ `AuthContext.tsx` with supply's poll-on-interval + focus (Sentry patches history too, so the
122
+ cleanup can clobber it); make `login()` fail loudly instead of swallowing errors after tokens are
123
+ already written.
124
+ 4. **Narrow the core-parameters forced logout** to `META_LAST_REFRESH_DATETIME` only (it currently
125
+ compares the WHOLE `/core-parameters` blob, so any parameter edit logs out every user) and add
126
+ supply's notice toast.
127
+ 5. **Constrain the SSO URL's HOST**, not just its scheme, to the tenant's configured IdP domain. No
128
+ allowlist source exists today.
129
+ 6. **Unanswered since 2026-09-03:** do commerce bundle/item payloads carry prices or stock counts? If
130
+ they do, those queries need their spinner on `isFetching`, not `isLoading`.
131
+ 7. Point `performLogout` and the core-parameters logout at `/login` directly instead of `/`, to remove
132
+ the brief blank screen while `AuthRedirect` renders nothing.
133
+
134
+ ---
135
+
136
+ ## Current file state
137
+
138
+ All work is on branch `TRUE-81991` in `toga2-commerce`, as three commits.
139
+ `e546db3d` is pushed; **`93616f20 "Save"` and `fd92de42 "Save"` are LOCAL ONLY** and were made by the
140
+ developer, not by Claude. Working tree otherwise clean (only `.env.development` modified, unrelated).
141
+
142
+ | File | Status | Notes |
143
+ |------|--------|-------|
144
+ | `src/lib/session.ts` | new | `hasSession()` (parses `user`, requires a uuid, storage read inside the try) + the identity epoch |
145
+ | `src/lib/session.test.ts` | new | 11 tests; 5 go red if `hasSession` is loosened |
146
+ | `src/lib/authStorage.ts` | new | `AUTH_STORAGE_KEYS` + `clearAuthStorage()`, shared by both logout paths |
147
+ | `src/lib/queryCacheRestoration.ts` | new | restoration gate, `clearPersistedQueryCache()`, epoch guard, 1.5s cap |
148
+ | `src/hooks/useClientSsoUrl.ts` | new | `/domains` + `singleSignOnServiceUrl` lookup, `isSafeSsoUrl` https check |
149
+ | `src/pages/Login/view/SsoSignIn.tsx` | new | SSO panel + `SsoSignInFooter` (footer is a sibling of the form column) |
150
+ | `src/api/refreshAuthTokens.test.ts` | new | 8 tests; 3 go red without the single-flight + identity guards |
151
+ | `vitest.config.ts` | new | jsdom, `src/**/*.test.ts` |
152
+ | `src/api/axiosInstance.ts` | modified | in-memory public token, single-flight + identity-scoped refresh, `hasSession()` gates, `clearPublicToken()` |
153
+ | `src/contexts/AuthContext.tsx` | modified | `resetClientState()` on login AND logout, async `logout()`, storage listener uses `hasSession()` only |
154
+ | `src/hooks/useAuthenticationFlow.ts` | modified | SSO lookup removed; navigates to `/login`; gates on `hasSession()` |
155
+ | `src/pages/Login/LoginPage.tsx` | modified | branches to the SSO panel; left panel is now `relative` |
156
+ | `src/pages/Cart/api/CartApi.ts` | modified | view-as token validation + epoch bump + `clearPublicToken()` |
157
+ | `src/utils/postAuthRedirect.ts` | modified | `isSafeDestination()` blocks the protocol-relative open redirect |
158
+ | `src/stores/useSettingsModalStore.ts` | modified | added `clearSettingsModal` |
159
+ | `src/App.tsx` | modified | `staleTime: 0`, `refetchOnWindowFocus: false`, `buster`, `throttleTime: 0` |
160
+ | `vite.config.ts` / `src/vite-env.d.ts` | modified | `__BUILD_ID__` define + declaration |
161
+ | `src/pages/Login/viewModel/FIELDS/LOGINDUMMYFIELDS.json` | modified | `SSOSECTIONFIELDS` (button, loading, copyright) |
162
+ | `package.json` / `package-lock.json` | modified | vitest + jsdom devDeps, `test` scripts. Blox pin untouched at `1.1.1-production.139` |
163
+
164
+ ---
165
+
166
+ ## Decisions made
167
+
168
+ - **`gcTime`, not `staleTime`, is the speed knob.** React Query paints cached data instantly whatever
169
+ `staleTime` says, and commerce gates every spinner on `isLoading` (false when cached data exists) —
170
+ so lowering `staleTime` costs no spinner. Rejected: keeping the 24h `staleTime` "for speed".
171
+ - **One `hasSession()` predicate.** Four sites decided this off three different keys with three
172
+ different predicates, and the disagreements WERE the bugs. Rejected (CTO advice): collapsing `user`
173
+ and `zu-user` into one store — ~15 files, no user-visible gain. Unify the predicate, not the storage.
174
+ - **`hasSession()` is a routing/UI signal only, never an authorization check.** The value is
175
+ user-writable; api2 enforces every permission server-side. Noted in the file so it does not drift.
176
+ - **Keep tokens in `localStorage`.** api2 is Bearer-only with no cookie session; switching means
177
+ backend work plus CSRF protection that does not exist. Matches `2.0/standards/frontend.md` §12.
178
+ - **SSO tenants land on `/login` and click through, rather than auto-redirecting.** Developer's call.
179
+ `/login` IS the SSO page — the same URL decides which screen to show.
180
+ - **Commerce keeps its own FIELDS copy and theme**, not the Supply wording/art from the design.
181
+ Developer's call. Deviations from the mockup were listed rather than silently absorbed.
182
+ - **Fix the public-token bug app-local, not in blox.** Per the app-level-over-core rule; the blox fix
183
+ changes behavior for every consumer and needs its own review.
184
+ - **Dropped the `?ssoPreview=1` dev switch** at the developer's request after using it to verify the
185
+ screen. The `isSafeSsoUrl` https check stayed — that is a security fix, not test code.
186
+
187
+ ---
188
+
189
+ ## Blockers
190
+
191
+ - **Compass Canada production SSO is broken and the cause is unconfirmed.** The diagnostic SQL has
192
+ not been run. Until the `/domains` lookup returns SSO for that tenant, the new screen cannot be
193
+ seen there — and it cannot be seen locally either, since the dev preview switch was removed.
194
+ - **`93616f20` and `fd92de42` are unpushed local commits** with generic "Save" messages. They need
195
+ real commit messages (or a squash) before a PR.
196
+ - The mid-session checkout moved to `_production` and back; the work was never lost, but confirm
197
+ `git branch --show-current` is `TRUE-81991` before doing anything.
198
+
199
+ ---
200
+
201
+ ## Exact next step
202
+
203
+ > Run the Compass Canada diagnostic against the **Core production** database — the `SELECT` joining
204
+ > `Core.Domains` → `Core.Environments` → `Core.Clients` → `Core.ClientAuthentications` filtered on
205
+ > `d.domain LIKE '%compasscanada%'`. Read the result against four causes in order: no row / wrong
206
+ > `domain` string, `authType` NULL (no `ClientAuthentications` row for that clientId+environmentId),
207
+ > `authType` not the literal `'SSO'`, or — if the data is right — the PUBLIC role missing
208
+ > `AclFieldPermissions` on `ClientAuthentications.type`, which you check by opening the site in a
209
+ > private window and looking at the `/v2/domains` response in the Network tab.
210
+
211
+ ---
212
+ _Saved by /session-save on 2026-09-23_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.873",
3
+ "version": "1.0.875",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",