toga-ai 1.0.210 → 1.0.212

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,6 +3,7 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Library (1.0 Framework) Architecture](architecture.md) | `library` is the shared library repository for **all 1.0 (legacy) applications** — the `App_` framework. | library/_.php, library/app/, library/browser/ |
6
+ | [App_Sso — Reusable 1.0 SSO Initiation (SP-initiated SAML via saml.togahub.com)](features/app-sso-initiation.md) | `App_Sso` (`library/app/sso.php`) is the **1.0 port of the 2.0 SAML gateway's SP-initiated SSO initiation**, packaged as a reusable, framework-level capability | library/app/sso.php, library/sso/togahub_private_key.key |
6
7
  | [Diagnostic Dialog — View Recommended Services Routing](features/diagnostic-dialog-view-recommended-services.md) | `App_Model_Toga_Diagnostic::initializeDiagnosticDialog()` renders the device modal used across all TOGa service request views. | library/app/model/toga/diagnostic.php |
7
8
  | [Elite Freshservice Sync (library)](features/elite-freshservice-sync.md) | `App_Api_Toga2` in `library/app/api/toga2.php` orchestrates bidirectional sync between TOGA 2 and TOGaDesk. | library/app/api/toga2.php |
8
9
  | [Branded HTML Email Templates (App_Email_Template)](features/email-templates.md) | `App_Email_Template` (`app/email/template.php`) is the base class for branded HTML emails in the 1.0 (`App_`) framework. | library/app/email/template.php, library/app/email/agilant.php |
@@ -0,0 +1,106 @@
1
+ ---
2
+ title: App_Sso — Reusable 1.0 SSO Initiation (SP-initiated SAML via saml.togahub.com)
3
+ framework: "1.0"
4
+ repo: library
5
+ project: Library
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-26
10
+ owners: [jcardinal]
11
+ files:
12
+ - library/app/sso.php
13
+ - library/sso/togahub_private_key.key
14
+ related:
15
+ - ../architecture.md
16
+ - ./mvc-page-pattern-and-app-skeleton.md
17
+ - ../../../2.0/apps/saml/features/downstream-integration-contract.md
18
+ - ../../apps/tools/features/saml-sso-auth.md
19
+ - ../../apps/tools/workflows/deploy-to-elastic-beanstalk-al2023.md
20
+ ---
21
+
22
+ ## Summary
23
+
24
+ `App_Sso` (`library/app/sso.php`) is the **1.0 port of the 2.0 SAML gateway's SP-initiated SSO
25
+ initiation**, packaged as a reusable, framework-level capability so **any 1.0 `App_` app** can
26
+ adopt SSO through `saml.togahub.com` without touching 2.0. It is the 1.0 counterpart to the 2.0
27
+ `_Model_Core_ClientAuthentication::singleSignOnServiceUrl()` builder described in the
28
+ [SAML downstream integration contract](../../../2.0/apps/saml/features/downstream-integration-contract.md).
29
+
30
+ Design intent (read before adopting): the class is **config-driven and has NO Core DB
31
+ dependency** — a consuming 1.0 app never queries Core. The team's standing preference is **not to
32
+ modify 2.0**; 1.0 SSO initiation logic lives here in `App_Sso`. This is the canonical doc to
33
+ follow for any future "add SSO to `<1.0 app>`" request.
34
+
35
+ ## How it works
36
+
37
+ `App_Sso::initiate(array $params): string` builds a signed SAML 2.0 `AuthnRequest` and returns the
38
+ full IdP redirect URL (`idpSsoUrl?QUERY&Signature=...`). It uses the **HTTP-Redirect binding**:
39
+
40
+ 1. Build the `AuthnRequest` XML, then encode it for the redirect binding:
41
+ `XML → gzdeflate → base64 → urlencode`.
42
+ 2. Build the **RelayState** payload `{v:1, time, domain, urlParameters}` and encrypt it with
43
+ `App_String::encryptWithKey` (AES-256-CBC, output `base64(iv . ciphertext)`). This is
44
+ **byte-for-byte compatible** with the 2.0 `_String::encryptWithKey`, so the gateway `/acs` can
45
+ decrypt the RelayState the 1.0 app produced.
46
+ 3. Sign the assembled query string with `openssl_sign` + `OPENSSL_ALGO_SHA256` using the bundled
47
+ private key, and append `&Signature=<base64>`.
48
+ - `SIGNATURE_ALGORITHM = http://www.w3.org/2001/04/xmldsig-more#rsa-sha256`
49
+ - `PRIVATE_KEY_PATH = __DIR__/../sso/togahub_private_key.key`
50
+ (`library/sso/togahub_private_key.key`, the togahub signing key, copied from the 2.0
51
+ `_underscore` `Assets/ssl`).
52
+
53
+ ### Parameters
54
+ **Required:** `idpSingleSignOnServiceUrl`, `idpEntityIdentifier`, `assertionConsumerServiceUrl`,
55
+ `domainUuid`, `apiSecretAccessToken`. **Optional:** `urlParameters`.
56
+ Throws `InvalidArgumentException` on a missing required param; `RuntimeException` on signing failure.
57
+
58
+ ### Deliberate deviation from 2.0
59
+ `App_Sso` emits a **valid** closing tag `</samlp:AuthnRequest>`, whereas the 2.0 builder emits a
60
+ malformed `<\samlp:AuthnRequest>`. `App_Sso` is the corrected version — this is intentional, not a
61
+ bug to "fix back."
62
+
63
+ ## Adopting SSO in another 1.0 app (step-by-step)
64
+
65
+ (a) **Config — add a `[saml]` section** with:
66
+ `idp_single_sign_on_service_url`, `idp_entity_identifier` (= `https://saml.togahub.com/meta`),
67
+ `assertion_consumer_service_url` (= `https://saml.togahub.com/acs`), `domain_uuid`,
68
+ `api_secret_access_token` (+ `..._previous` for rotation), and the client uuid(s) the consumer
69
+ validates against. Document **where** these secrets live; never paste the values.
70
+
71
+ (b) **Initiation route** (e.g. `mvc/sso/initiate/get.php`): guard `isLoggedIn`, read `config[saml]`,
72
+ call `App_Sso::initiate([...])`, then `App_MVC::routeTo($url); exit;`.
73
+
74
+ (c) **Handoff consumer route** `mvc/sso/get.php`: base64-decode the `?saml=` payload, JSON-parse,
75
+ decrypt the client/user values with the shared API secret (**current then previous** key, for a
76
+ rotation grace window), validate the UUIDs, look up the user, and establish the session. See the
77
+ Tools implementation in [Tools SAML SSO Consumer & Persona-Gated Auth](../../apps/tools/features/saml-sso-auth.md).
78
+
79
+ (d) **Register a `Core.Domains` row** (`uuid, clientId, appId, environmentId, domain =
80
+ https://<app-host>/sso`). The `uuid` **MUST equal** the config `domain_uuid` (use a fixed uuid, not
81
+ `UUID()`). The gateway resolves client + environment + return URL from this row.
82
+
83
+ (e) **AWS — the app host MUST have a working HTTPS (443) listener + an ACM cert covering that exact
84
+ hostname**, or the gateway's return redirect to `https://<host>/sso` fails with a connection
85
+ timeout. See the gotcha below.
86
+
87
+ ## Gotchas / known issues
88
+
89
+ - **Return-leg "connection timeout" is usually a missing HTTPS listener, not code.** When a SAML
90
+ SSO return leg times out in the browser, **verify the EB/ALB HTTPS (443) listener + ACM cert
91
+ cover the EXACT return hostname before debugging PHP/SAML.** Diagnosis signature: `curl` to the
92
+ public `https://` URL **from the instance itself** returns `http=000` after the full timeout,
93
+ while `curl http://127.0.0.1` with a `Host:` header returns `200` fast. Reusable for any TOGA app
94
+ behind Elastic Beanstalk; see the
95
+ [Tools EB deploy workflow](../../apps/tools/workflows/deploy-to-elastic-beanstalk-al2023.md).
96
+ - **Consumer route runs mid-render in 1.0.** The 1.0 framework runs `mvc/sso/get.php` **inside** the
97
+ page render (`frameworkindex → body → loadFile`) after `common/header.php` has already emitted
98
+ output, so headers are sent. `session_regenerate_id(true)` (and any `http_response_code` on the
99
+ failure path) fatals with "headers already sent." Guard `session_regenerate_id` with
100
+ `if (!headers_sent())`. The proper long-term fix is to route `/sso` **before any output** and
101
+ `exit`; until then this is a known architectural limitation any 1.0 consumer inherits.
102
+
103
+ ## Change history
104
+ - 2026-06-26 — Built `App_Sso::initiate()`: 1.0 port of the 2.0 SP-initiated SSO builder, config-driven with no Core DB dependency, RelayState encrypted via `App_String::encryptWithKey` (2.0-compatible), rsa-sha256 query signing with the bundled togahub key, and a corrected `</samlp:AuthnRequest>` closing tag. Captured the 5-step adoption guide and the HTTPS-listener / mid-render headers-sent gotchas for any 1.0 app adopting SSO (jcardinal)
105
+ </content>
106
+ </invoke>
@@ -5,5 +5,5 @@
5
5
  | [Tools (1.0 Internal-Tools App) Architecture](architecture.md) | **Tools** is a standalone 1.0 (`App_`) application that houses many small internal tools behind simple interfaces, gated by Client_True staff persona. | tools/index.php, tools/_/app/framework.php, tools/_/app/frameworkindex.php, tools/_/app/auth.php, tools/_/app/nav.php, tools/common/header.php, tools/common/footer.php, tools/mvc/get.php, tools/mvc/_TEMPLATE/get.php, tools/docs/ADDING_A_TOOL.md |
6
6
  | [Tools — Developers Folder (UUID & Password Generators)](features/developer-tools.md) | The first two tools shipped in the Tools app, both under the **Developers** folder and gated to personas **Development Team** / **TOGa Technology**. | tools/mvc/developers/uuid/get.php, tools/mvc/developers/password/get.php |
7
7
  | [Tools Persona-Gated Navigation (App_Nav)](features/persona-gated-navigation.md) | `App_Nav` is the Tools app's two-level, **persona-gated** navigation. | tools/_/app/nav.php |
8
- | [Tools SAML SSO Consumer & Persona-Gated Auth (App_Auth)](features/saml-sso-auth.md) | `App_Auth` is the Tools app's authentication layer: it consumes the SAML gateway `?saml=` handoff (see the 2.0 SAML downstream integration contract), establishe | tools/_/app/auth.php, tools/mvc/sso/get.php, tools/mvc/login/get.php, tools/mvc/login/post.php, tools/mvc/logout/get.php |
8
+ | [Tools SAML SSO Consumer & Persona-Gated Auth (App_Auth)](features/saml-sso-auth.md) | `App_Auth` is the Tools app's authentication layer: it consumes the SAML gateway `?saml=` handoff (see the 2.0 SAML downstream integration contract), establishe | tools/_/app/auth.php, tools/mvc/sso/initiate/get.php, tools/mvc/sso/get.php, tools/mvc/login/get.php, tools/mvc/login/post.php, tools/mvc/logout/get.php, tools/mvc/get.php, tools/config.production.ini |
9
9
  | [Deploying Tools to Elastic Beanstalk (PHP 8.5 / Amazon Linux 2023)](workflows/deploy-to-elastic-beanstalk-al2023.md) | How the **Tools** 1.0 app boots on Elastic Beanstalk running `PHP 8.5 on 64bit Amazon Linux 2023/4.13.1 (aarch64)`. | tools/.ebextensions/004_http_to_https.config, tools/.ebextensions/006_mount-s3fs.config, tools/.ebextensions/007_setup_export_cache_folders.config, tools/.ebextensions/008_setup_ldap.config, tools/.ebextensions/009_setup_phpini.config, tools/.ebextensions/020_setup_git_libraries.config, tools/.ebextensions/050_register_instance_to_shared_application_load_balancer.config, tools/ebs/git.json |
@@ -6,16 +6,20 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-25
9
+ updated: 2026-06-26
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - tools/_/app/auth.php
13
+ - tools/mvc/sso/initiate/get.php
13
14
  - tools/mvc/sso/get.php
14
15
  - tools/mvc/login/get.php
15
16
  - tools/mvc/login/post.php
16
17
  - tools/mvc/logout/get.php
18
+ - tools/mvc/get.php
19
+ - tools/config.production.ini
17
20
  related:
18
21
  - ../architecture.md
22
+ - ../../library/features/app-sso-initiation.md
19
23
  - ../../../2.0/apps/saml/features/downstream-integration-contract.md
20
24
  - ../../../clients/true/features/users-personas-data-model.md
21
25
  ---
@@ -30,6 +34,18 @@ auto-created.
30
34
 
31
35
  ## How it works
32
36
 
37
+ ### Wiring overview
38
+ - **`/sso/initiate`** (`mvc/sso/initiate/get.php`) — guards `isLoggedIn`, reads `config[saml]`,
39
+ calls `App_Sso::initiate([...])` and `App_MVC::routeTo($url); exit;`. Initiation now uses the
40
+ reusable 1.0 [`App_Sso`](../../library/features/app-sso-initiation.md) library class (no longer a
41
+ placeholder).
42
+ - **`/sso`** (`mvc/sso/get.php`) — the handoff consumer (below).
43
+ - **Core.Domains** — a fixed-uuid row registers this app's return domain so the gateway resolves
44
+ client/environment/return URL: `uuid 2927bc15-e347-4358-a430-fb28f9446d27`, `clientId 1` (True),
45
+ `appId 30`, `environmentId 1` (production), `domain https://tools.togatech.com/sso`. The uuid
46
+ **equals** config `domain_uuid` (created in `dbchanges2/Core/2026-06-26a - ToolsSsoDomain.sql`,
47
+ applied to live Core).
48
+
33
49
  ### Consuming the `?saml=` handoff (`mvc/sso/get.php` → `App_Auth`)
34
50
  1. Length-guard `$_GET['saml']`, then base64-decode → `json_decode` → read `payload.client`
35
51
  and `payload.user`.
@@ -42,7 +58,8 @@ auto-created.
42
58
  4. Load the active Client_True user: `WHERE uuid = ? AND isActive = 1`. No match → fail closed.
43
59
 
44
60
  ### Session establishment (`establishSession()`)
45
- - Calls `session_regenerate_id(true)`, then caches the user and **persona names** in `$_SESSION`.
61
+ - Calls `session_regenerate_id(true)` **guarded by `if (!headers_sent())`** (see gotcha), then
62
+ caches the user and **persona names** in `$_SESSION`.
46
63
  - Persona lookup (read-only `db_true`):
47
64
  `SELECT p.name FROM Users u JOIN Users_Personas up ON up.userId=u.id JOIN Personas p ON p.id=up.personaId WHERE u.uuid = <escaped>`.
48
65
  - The session cookie is set **HttpOnly + SameSite=Lax** in `App_Framework_Tools::initialize()`
@@ -57,20 +74,33 @@ auto-created.
57
74
  is truthy **and** `App_Registry::inDevMode()` — both must hold. When used it writes a `SECURITY`
58
75
  line to `error_log`.
59
76
 
77
+ ### One-step sign-in
78
+ Home route `/` (`mvc/get.php`), when not logged in, does `App_MVC::routeTo('/login'); return;` so the
79
+ SSO button is the only click — the previous welcome-card "Sign in" → `/login` intermediate step was
80
+ removed.
81
+
60
82
  ## Gotchas / known issues
61
83
 
62
- - **SSO initiation is not implemented in this 1.0 app.** Building the signed AuthnRequest lives
63
- in 2.0 `_underscore` (`singleSignOnServiceUrl()`); login here points at a configurable
64
- `[saml] initiation_url` **placeholder**. Open item: needs a gateway initiation entry point or
65
- a ported builder.
84
+ - **Consumer route runs mid-render → headers already sent.** The 1.0 framework runs `mvc/sso/get.php`
85
+ **inside** the page render (`frameworkindex → body → loadFile`) after `common/header.php` has
86
+ emitted output, so `session_regenerate_id(true)` fatals ("cannot be regenerated after headers
87
+ already sent"). Fixed by guarding with `if (!headers_sent())`. **Known architectural limitation:**
88
+ the failure path (`tools_ssoFail → http_response_code` at `mvc/sso/get.php:23`) *also* fatals on
89
+ headers-sent; the proper long-term fix is to route `/sso` **before any output** and `exit`. Any
90
+ 1.0 app adopting this consumer pattern inherits this — see
91
+ [App_Sso](../../library/features/app-sso-initiation.md).
66
92
  - **No app-side replay defense.** The handoff token carries no nonce/timestamp the app verifies.
67
93
  Recommend the gateway embed `iat` + `jti`.
68
94
 
69
95
  ## Config keys
70
96
 
71
97
  `[database_true]` (read-only Client_True); `[saml]` `api_secret_access_token` /
72
- `api_secret_access_token_previous`, `initiation_url`, `client_authentication_uuid`,
73
- `domain_uuid`, `true_client_uuid`; `[internal]` `dev_mode`.
98
+ `api_secret_access_token_previous`, `client_authentication_uuid`, `domain_uuid`,
99
+ `true_client_uuid` (plus the IdP/ACS urls used by initiation); `[internal]` `dev_mode`. **Secret
100
+ location:** `config.production.ini` holds the plaintext production secrets — including the shared
101
+ Core `API_SECRET_ACCESS_TOKEN` — in its `[saml]` section (committed; developer explicitly accepted
102
+ this). Document **where** they live, never the values.
74
103
 
75
104
  ## Change history
105
+ - 2026-06-26 — Wired up real SSO initiation via the new 1.0 `App_Sso` library class (`/sso/initiate`), replacing the `initiation_url` placeholder; registered the fixed-uuid Core.Domains return row (dbchanges2 `2026-06-26a`); fixed the `session_regenerate_id` headers-already-sent fatal (guarded with `!headers_sent()`) and documented the mid-render failure-path limitation; collapsed home → `/login` to one-step sign-in; noted prod secrets live in `config.production.ini [saml]` (jcardinal)
76
106
  - 2026-06-25 — Built App_Auth: SAML `?saml=` handoff consumer with dual-key decrypt, hash_equals client-uuid check, fail-closed 401, persona-cached session (HttpOnly+SameSite=Lax), and a double-gated dev bypass. Initiation + replay defense left as open items (jcardinal)
@@ -117,8 +117,17 @@ setup — the git clone (`020`) deliberately does **not** use it (see gotchas).
117
117
  e.g. `GIT=$(which git || echo /usr/bin/git)`.
118
118
  - **cfn-init `files:` runs before `container_commands`** — the correct pattern for writing a
119
119
  script and then executing it in the same config.
120
+ - **A SAML SSO return-leg "connection timeout" is usually a missing HTTPS listener, not code.**
121
+ When the gateway's return redirect to `https://<host>/sso` times out in the browser, verify the
122
+ **EB/ALB HTTPS (443) listener + ACM cert cover the EXACT return hostname before debugging
123
+ PHP/SAML.** This bit Tools after the domain was switched from `togahub.com` to `togatech.com` and
124
+ no 443 listener existed for the new hostname. Diagnosis signature: `curl` to the public `https://`
125
+ URL **from the instance itself** returns `http=000` after the full timeout, while
126
+ `curl http://127.0.0.1` with a `Host:` header returns `200` fast. Reusable for any TOGA app behind
127
+ Elastic Beanstalk.
120
128
 
121
129
  ## Change history
130
+ - 2026-06-26 — Added the SAML SSO return-leg gotcha: a "connection timeout" on `https://<host>/sso` was a missing EB/ALB HTTPS (443) listener for the new `togatech.com` hostname (not PHP/SAML); documented the curl `http=000` vs `127.0.0.1` 200 diagnosis (jcardinal)
122
131
  - 2026-06-26 — Documented the AL2 → AL2023 (PHP 8.5) EB migration for Tools: package renames
123
132
  (`libstdc++48`→`libstdc++`, `php73-ldap`→`php-ldap`, drop `libcurl`), php ini via cfn-init
124
133
  `files:` writing `/etc/php.d/application.ini`, bash-based library clone replacing the
@@ -4,7 +4,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
4
4
 
5
5
  ## 1.0 framework
6
6
 
7
- - **library** (Library) _(framework core)_ — 9 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
7
+ - **library** (Library) _(framework core)_ — 10 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
8
  - **worker** (Worker) — 10 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **togadesk** (TOGa Desk) — 8 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
10
10
  - **togaview** (TOGa View) — 6 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
@@ -33,7 +33,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
33
33
 
34
34
  ## standalone framework
35
35
 
36
- - **togatech** (TOGA Technology Website) — 2 doc(s) → [standalone/apps/togatech/INDEX.md](standalone/apps/togatech/INDEX.md)
36
+ - **togatech** (TOGA Technology Website) — 4 doc(s) → [standalone/apps/togatech/INDEX.md](standalone/apps/togatech/INDEX.md)
37
37
  - **forward** (Forwarder) — 4 doc(s) → [standalone/apps/forward/INDEX.md](standalone/apps/forward/INDEX.md)
38
38
 
39
39
  ## Clients
@@ -8,7 +8,7 @@ project: _Underscore
8
8
  client: true
9
9
  type: profile
10
10
  status: active
11
- updated: 2026-06-25
11
+ updated: 2026-06-26
12
12
  owners: [jcardinal]
13
13
  files: []
14
14
  related:
@@ -26,5 +26,8 @@ by staff role (e.g. the planned Toolbox app) reads its `Users` / `Personas` mode
26
26
  - **Client identifier:** `True`
27
27
  - **SSO mapper:** `_Model_True_ClientAuthentication` (base; matches by email from NameID)
28
28
 
29
- The new **Tools** app (1.0; repo `tools`) reads the `Client_True` DB **read-only** to gate its
30
- internal tooling by staff persona (see `1.0/apps/tools/`).
29
+ The **Tools** app (1.0; repo `tools`) authenticates True users via **SSO** through
30
+ `saml.togahub.com` and reads the `Client_True` DB **read-only** to gate its internal tooling by
31
+ staff persona (see `1.0/apps/tools/`). Its gateway return domain is registered in `Core.Domains`
32
+ (`uuid 2927bc15-e347-4358-a430-fb28f9446d27`, `clientId 1`, `appId 30`, env 1,
33
+ `https://tools.togatech.com/sso`).
@@ -3,4 +3,5 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [TOGA Technology Website Architecture](architecture.md) | The public-facing TOGA Technology corporate/marketing website. | togatech/src/main.tsx, togatech/src/App.tsx, togatech/src/routes.tsx, togatech/src/lib/api.ts, togatech/src/lib/contentful.ts, togatech/src/themeConfig/ThemeContext.tsx, togatech/vite.config.ts, togatech/package.json |
6
- | [SEO / AEO / GEO — Prerendering, Single-Source Meta & Structured Data](features/seo-aeo-geo-prerender.md) | Makes togatech.com visible and citable to search engines **and** AI answer engines (ChatGPT/Perplexity/Claude search, Google AI Overviews). | togatech/vite.config.ts, togatech/scripts/prerender.mjs, togatech/src/routes.config.json, togatech/src/main.tsx, togatech/src/App.tsx, togatech/src/components/SEO/Seo.tsx, togatech/src/components/SEO/JsonLd.tsx, togatech/src/components/SEO/schema.ts, togatech/public/robots.prod.txt, togatech/public/sitemap.xml, togatech/public/llms.txt |
6
+ | [SEO / AEO / GEO — Prerendering, Single-Source Meta & Structured Data](features/seo-aeo-geo-prerender.md) | Makes togatech.com visible and citable to search engines **and** AI answer engines (ChatGPT/Perplexity/Claude search, Google AI Overviews). | togatech/vite.config.ts, togatech/scripts/prerender.mjs, togatech/src/routes.config.json, togatech/src/main.tsx, togatech/src/App.tsx, togatech/src/components/SEO/Seo.tsx, togatech/src/components/SEO/JsonLd.tsx, togatech/src/components/SEO/schema.ts, togatech/src/components/templates/AppLayout.tsx, togatech/src/pages/WhatWeDo/WhatWeDoPage.tsx, togatech/src/pages/WhatWeDo/viewModel/FIELDS/WHATWEDOPAGEFIELDS.json, togatech/src/pages/About/AboutPage.tsx, togatech/src/pages/Contact/ContactPage.tsx, togatech/src/pages/OurPlatform/OurPlatformPage.tsx, togatech/public/robots.prod.txt, togatech/public/sitemap.xml, togatech/public/llms.txt |
7
+ | [Creating Pull Requests on togatech](workflows/creating-pull-requests.md) | How to open a PR against `agilantsolutions/togatech`. | togatech/.git/config |
@@ -6,7 +6,7 @@ project: TOGA Technology Website
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-19
9
+ updated: 2026-06-26
10
10
  owners: ["ajean", "jcardinal"]
11
11
  files:
12
12
  - togatech/vite.config.ts
@@ -17,6 +17,12 @@ files:
17
17
  - togatech/src/components/SEO/Seo.tsx
18
18
  - togatech/src/components/SEO/JsonLd.tsx
19
19
  - togatech/src/components/SEO/schema.ts
20
+ - togatech/src/components/templates/AppLayout.tsx
21
+ - togatech/src/pages/WhatWeDo/WhatWeDoPage.tsx
22
+ - togatech/src/pages/WhatWeDo/viewModel/FIELDS/WHATWEDOPAGEFIELDS.json
23
+ - togatech/src/pages/About/AboutPage.tsx
24
+ - togatech/src/pages/Contact/ContactPage.tsx
25
+ - togatech/src/pages/OurPlatform/OurPlatformPage.tsx
20
26
  - togatech/public/robots.prod.txt
21
27
  - togatech/public/sitemap.xml
22
28
  - togatech/public/llms.txt
@@ -37,7 +43,15 @@ Makes togatech.com visible and citable to search engines **and** AI answer engin
37
43
  1. **robots:** `robotsPlugin` copies `robots.prod.txt` only when `mode === "production"`; beta/gamma/dev get `robots.dev.txt` (`Disallow: /`). `robots.prod.txt` carries an explicit AI-bot allow-list (GPTBot, ClaudeBot, PerplexityBot, …) + `https://` sitemap.
38
44
  2. **prerender:** `npm run build` = `tsc && vite build && npx puppeteer browsers install chrome && node scripts/prerender.mjs` (the Chrome-install step was added 2026-06-19 — see gotchas). The script serves `dist/` (reading the original shell once, serving it for all nav routes), snapshots each route in headless Chrome, and writes `dist/<route>/index.html`. Runs in a real browser, so **no SSR/window guards needed**.
39
45
  3. **meta:** each page renders `<Seo {...FIELDS.meta} />` — title/description/keywords/canonical/OG/Twitter from one place per page; canonical/OG URLs derived from one `SITE_URL`. `<JsonLd>` (Helmet `<script type="application/ld+json">`) is baked into the snapshot.
40
- 4. **structured data:** Organization + WebSite site-wide (AppLayout), per-page BreadcrumbList (Seo), platform SoftwareApplication (OurPlatform), LocalBusiness×5 (from Contact locations), Person (from About team) — all **derived from existing FIELDS** (single source).
46
+ 4. **structured data:** all JSON-LD now lives in **one module — `src/components/SEO/schema.ts`** (the single source of truth), derived from `FOOTERFIELDS.json` (socials/email/phone) and each page's FIELDS JSON, so editing footer/content updates schema automatically. Exports and their render sites (emitted via `<JsonLd>`/Helmet):
47
+ - `organizationSchema` + `websiteSchema` → **AppLayout** (all pages).
48
+ - `serviceSchemas` (4 `Service` nodes) → **WhatWeDoPage**.
49
+ - `platformSchema` (`SoftwareApplication` for the TOGa Platform) → **OurPlatformPage**.
50
+ - `aboutPageSchema` (`AboutPage`) + `personSchemas` (per leader) → **AboutPage**.
51
+ - `contactPageSchema` (`ContactPage`) + `localBusinessSchemas` (per office, from `CONTACTPAGEFIELDS.map.locations`) → **ContactPage**.
52
+ - **Entity graph linked by `@id`:** Organization is `${SITE_URL}/#organization`, WebSite `/#website`, logo `/#logo`. Service/Person/LocalBusiness reference the org via `provider`/`worksFor`/`parentOrganization` `@id`; AboutPage/ContactPage set `isPartOf` the website and `about` the org. Use `@id` cross-references — do not inline duplicate org nodes.
53
+ - `organizationSchema.logo` is an `ImageObject` (`@id` `/#logo`, `url`, `contentUrl`, `caption "TOGA Technology"`); width/height are intentionally omitted because the logo is an SVG.
54
+ - `personSchemas` de-dupes a leader who appears in both `boardMembers` and `members` in `ABOUTPAGEFIELDS`, keyed on lowercased trimmed name (a `Set`) — naive emission produced duplicate `Person` nodes.
41
55
 
42
56
  ## Data model
43
57
  None (static marketing site). All copy/meta/NAP/leaders come from `*FIELDS*.json`; structured data is derived from `FOOTERFIELDS.json`, `CONTACTPAGEFIELDS.json` (`map.locations`), and `ABOUTPAGEFIELDS.json` (`teamMembers`).
@@ -57,10 +71,15 @@ None — uniform.
57
71
  - **og:image dedupe:** static OG/Twitter tags were removed from `index.html` because `<Seo>` emits per-page ones (Helmet appends rather than replacing pre-existing static tags → duplicates otherwise).
58
72
  - **Verified data corrections:** real positioning is *integrated IT services + the ERA TOGa Platform*; real socials are LinkedIn `/toga-tech` + YouTube + Instagram (no Twitter/GitHub/Crunchbase); contact is `website@togatech.com` / `+1 (212) 736-0111`; **5** offices. Prior planning docs had several wrong specifics — verify schema/llms.txt against FIELDS before shipping.
59
73
  - **Build size:** JS bundle is ~14.8 MB (4.9 MB gzip) — a real CWV/LCP risk; code-splitting is a separate follow-up.
74
+ - **FIELDS JSON is component-coupled — no free-prose body slot:** each page's `*FIELDS*.json` is shaped to specific components (hero, cardScroll, businessProblems, kpi, carousel), so there is **no place to drop a long prose block**. An audit's "rewrite to 900 words" is not structurally possible without building new components — you can only sharpen titles/descriptions/keywords/card copy that already have slots. (`/our-platform` was left unchanged: it is already content-dense — 7 app modules + 4 stacks + testimonials + KPIs — and the original audit under-counted because that content is JS-rendered.)
75
+ - **`personSchemas` duplicate guard:** a leader can be in both `boardMembers` and `members` in `ABOUTPAGEFIELDS` — `personSchemas` must de-dup by lowercased trimmed name, or it emits duplicate `Person` nodes.
76
+ - **🔴 Brand name is "TOGA Technology" only — never "Agilant":** an earlier draft adding `alternateName: "Agilant"` / "(formerly Agilant)" to the org schema was explicitly reverted. Do not reintroduce "Agilant" in schema, copy, or meta. (Captured as a team brand standard — see Related docs.)
60
77
 
61
78
  ## Change history
79
+ - 2026-06-26 — SEO improvement effort (PRs #33–#36 → `_production`; SEO health ~61→78 est.). Centralized all JSON-LD into `src/components/SEO/schema.ts` (org/website/platform/service×4/about/contact/localBusiness/person, `@id`-linked graph), de-duped Person nodes, logo→`ImageObject`, sharpened `/what-we-do` meta+copy, added `<lastmod>` to all 7 sitemap URLs. Recorded brand constraint ("TOGA Technology" only, never "Agilant") and the component-coupled-FIELDS limit. (ajean)
62
80
  - 2026-06-19 — Merged SEO/AEO into `_production` (resolved ContactPage conflict: reCAPTCHA + Seo/JsonLd, dropped Helmet). Fixed Amplify build: committed `package-lock.json` (npm ci EUSAGE) and added `npx puppeteer browsers install chrome` to the build before prerender. (jcardinal)
63
81
  - 2026-06-18 — Initial SEO/AEO/GEO implementation: robots mode-gate + AI allow-list + sitemap/llms; Puppeteer prerender (fail-fast); shared `<Seo>` single-source meta + canonical; JSON-LD (Organization/WebSite/Breadcrumb/SoftwareApplication/LocalBusiness×5/Person×12). PRs #29 + #30, pending merge. (ajean)
64
82
 
65
83
  ## Related docs
66
84
  - standalone/apps/togatech/architecture.md (update its Build/deploy section once PR #30 merges — robotsPlugin gating + prerender step).
85
+ - standalone/apps/togatech/workflows/creating-pull-requests.md — how PRs get opened on this repo (gh CLI is unauthenticated; uses curl + osxkeychain token; base `_production`).
@@ -0,0 +1,47 @@
1
+ ---
2
+ title: Creating Pull Requests on togatech
3
+ framework: "standalone"
4
+ repo: togatech
5
+ project: TOGA Technology Website
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-06-26
10
+ owners: ["ajean"]
11
+ files:
12
+ - togatech/.git/config
13
+ related:
14
+ - standalone/apps/togatech/features/seo-aeo-geo-prerender.md
15
+ ---
16
+
17
+ ## Summary
18
+ How to open a PR against `agilantsolutions/togatech`. The local `gh` CLI is **not usable**
19
+ for this repo (it lacks the `read:org` scope), so PRs are created with a raw GitHub REST
20
+ call authenticated by the token already sitting in the macOS keychain. **Base branch is
21
+ `_production`, not `_main`** (`_main` is stale) — feature branches are cut from `_production`.
22
+
23
+ ## How it works
24
+ 1. Branch off `_production` (e.g. `feature/seo-schema`), commit, and push the branch.
25
+ 2. Pull the GitHub token from the osxkeychain credential helper rather than relying on `gh`:
26
+ ```
27
+ TOKEN=$(printf 'protocol=https\nhost=github.com\n\n' | git credential fill | sed -n 's/^password=//p')
28
+ ```
29
+ 3. POST to the PR API with that token:
30
+ ```
31
+ curl -s -X POST \
32
+ -H "Authorization: token $TOKEN" \
33
+ -H "Accept: application/vnd.github+json" \
34
+ https://api.github.com/repos/agilantsolutions/togatech/pulls \
35
+ -d '{"title":"...","head":"<branch>","base":"_production","body":"..."}'
36
+ ```
37
+ 4. The response JSON contains `html_url` for the new PR. Merge follows the normal review flow.
38
+
39
+ ## Gotchas / known issues
40
+ - **`gh pr create` fails** here — the authenticated `gh` session lacks `read:org`. Do not
41
+ burn time re-authing `gh`; use the curl path above.
42
+ - **Base is `_production`.** Targeting `_main` opens a PR against a stale branch. (This matches
43
+ the `worker2` mainline convention — mainline is `_production`, not `_main`.)
44
+ - **Never echo `$TOKEN`** into logs, commit messages, or PR bodies — it is a live credential.
45
+
46
+ ## Change history
47
+ - 2026-06-26 — Documented the curl-based PR flow (gh unauthenticated; base `_production`), captured during the SEO improvement effort (PRs #33–#36). (ajean)
@@ -0,0 +1,35 @@
1
+ ---
2
+ title: Brand Naming — "TOGA Technology" only
3
+ framework: "standalone"
4
+ repo: togatech
5
+ project: TOGA Technology Website
6
+ client: shared
7
+ type: standard
8
+ status: active
9
+ updated: 2026-06-26
10
+ owners: ["ajean"]
11
+ files:
12
+ - togatech/src/components/SEO/schema.ts
13
+ related:
14
+ - standalone/apps/togatech/features/seo-aeo-geo-prerender.md
15
+ ---
16
+
17
+ # Brand Naming — "TOGA Technology" only
18
+
19
+ The company is referenced **only** as "TOGA Technology" in all code, schema, meta, and
20
+ copy. **Never** use "Agilant" — not as a brand name, not as `alternateName`, and not in
21
+ any "(formerly Agilant)" framing.
22
+
23
+ ## Why
24
+ During the 2026-06 SEO effort an earlier draft added `alternateName: "Agilant"` and a
25
+ "(formerly Agilant)" note to the Organization JSON-LD and org description. This was
26
+ **explicitly reverted by the owner**. Surfacing the legacy name in structured data and
27
+ copy is off-brand and creates the wrong entity association for search/AI answer engines.
28
+
29
+ ## Applies to
30
+ - Schema.org JSON-LD (`src/components/SEO/schema.ts`) — no `alternateName: "Agilant"`.
31
+ - Page meta (titles, descriptions), on-page copy, `llms.txt`, and any FIELDS JSON.
32
+ - Any future SEO/content/AEO work on togatech.
33
+
34
+ ## Change history
35
+ - 2026-06-26 — Standard recorded after reverting an "Agilant" alternateName/"formerly" draft in the org schema. (ajean)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.210",
3
+ "version": "1.0.212",
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",