toga-ai 1.0.437 → 1.0.438

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
  | [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/assets/img/favicon/favicon.ico, tools/assets/img/favicon/favicon-32x32.png, tools/assets/img/favicon/favicon-16x16.png, tools/assets/img/favicon/apple-touch-icon.png, 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
+ | [CloudFront Client Setup](features/cloudfront-client-setup.md) | An SSO-gated admin tool at **`/devops/cloudfront-clients`** in the Tools 1.0 app that onboards a client onto **CloudFront + Route 53 across multiple AWS account | tools/_/app/devops/cloudfront.php, tools/mvc/devops/cloudfront-clients/get.php, tools/mvc/devops/cloudfront-clients/post.php, tools/assets/js/cloudfront-clients.js, tools/assets/css/cloudfront-clients.css, tools/_/app/nav.php, tools/_/app/frameworkindex.php, tools/config.production.ini |
6
7
  | [Design Demo Admin](features/design-demo-admin.md) | A self-serve admin UI at **`/design`** in the SSO-protected **Tools** app that lets the design team publish self-contained "Claude Design" HTML exports as **ver | tools/_/app/design/github.php, tools/mvc/design/get.php, tools/mvc/design/post.php, tools/assets/css/design.css, tools/assets/js/design.js, tools/_/app/frameworkindex.php, tools/_/app/nav.php, tools/composer.json |
7
8
  | [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 |
8
9
  | [Tools MVC — Routing, CSRF & App_Database Access Patterns](features/mvc-data-access-patterns.md) | The load-bearing 1.0 (`App_`) framework conventions a developer needs when adding a page to the Tools app — URL routing, CSRF, and DB access through `App_Databa | tools/_/app/nav.php, tools/mvc/get.php |
@@ -6,7 +6,7 @@ project: Tools
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-06-26
9
+ updated: 2026-07-24
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - tools/index.php
@@ -44,8 +44,10 @@ dashboard stylesheet's `header{position:fixed;width:100vw}` rule and cause horiz
44
44
  (the top header bar was removed for exactly this reason). A nav action's `route` maps **1:1** to
45
45
  `mvc/<route>/get.php`. Auth fails **closed** and never auto-creates users; the dev bypass is
46
46
  double-gated (`[internal] dev_mode` AND `App_Registry::inDevMode()`). `config.*.ini`
47
- (incl. `config.prod.ini`) is committed with plaintext production secrets — a known, team-accepted
48
- risk (see Known issues); never add more secrets and treat these as compromised if leaked.
47
+ (incl. `config.prod.ini`) is committed with plaintext production secrets that were **publicly
48
+ web-readable** until the front-controller deny blocks added 2026-07-24 (see Known issues);
49
+ rotation of every exposed secret is still owed. Never add more secrets, and keep config files
50
+ denied at the front controller / out of the web root.
49
51
 
50
52
  ## Boot & structure
51
53
 
@@ -86,10 +88,15 @@ Create `mvc/<folder>/<tool>/get.php` (copy `mvc/_TEMPLATE/get.php`), add one ent
86
88
 
87
89
  ## Known issues / security
88
90
 
89
- - **Plaintext secrets committed to git.** `config.*.ini` (incl. `config.prod.ini`) contains
90
- plaintext production secrets (RDS master password, SMTP, Payeezy, NetSuite). The team has
91
- **accepted this for now** (developer decision, 2026-06-25). Future remediation: git-ignore the
92
- config files and rotate all exposed credentials. (Location only — no values recorded here.)
91
+ - **Plaintext secrets were publicly web-readable (fixed 2026-07-24; rotation still owed).**
92
+ `config.*.ini` (incl. `config.prod.ini`) holds plaintext production secrets (RDS master
93
+ password, SMTP, Payeezy, NetSuite). The front controller routed through `index.php` on a
94
+ `RewriteCond !-f` rule **only**, which is not access control — a direct
95
+ `GET /config.production.ini` served the file **verbatim**. Fixed 2026-07-24 by adding
96
+ `<FilesMatch>` `Require all denied` blocks (sensitive extensions incl. `.ini`, and any
97
+ `config.*`) — see the [1.0 back-end security standard](../../standards/backend-php.md).
98
+ **Action still owed:** rotate every secret in `config.production.ini` — the file was
99
+ reachable, so treat all of it as compromised. (Location only — no values recorded here.)
93
100
  - **SSO initiation + replay defense are open items** — see `features/saml-sso-auth.md`.
94
101
  - **Framework-level Sentry error reporting is an open gap.** Prod printed "Sentry is not
95
102
  installed" because `sentry/sentry` was missing from `composer.json` (added `^4.10`; sibling
@@ -99,6 +106,11 @@ Create `mvc/<folder>/<tool>/get.php` (copy `mvc/_TEMPLATE/get.php`), add one ent
99
106
  composer package must `require_once` the autoloader itself.
100
107
 
101
108
  ## Change history
109
+ - 2026-07-24 — Fixed the config-file web-root exposure: `config.*.ini` was publicly readable via
110
+ the `!-f`-only front controller (`GET /config.production.ini` served it verbatim); added
111
+ `<FilesMatch>` deny blocks referencing the new 1.0 back-end security standard. Softened the
112
+ Critical-rules note from "team-accepted risk" to fixed-but-rotation-owed. Every secret in
113
+ `config.production.ini` still owes rotation (jcardinal)
102
114
  - 2026-07-24 — Added the SSO-gated **Design Demo Admin** tool at `/design` (`App_Design_Github`
103
115
  + `mvc/design/*` + namespaced assets + a `design` nav group), which publishes versioned demos
104
116
  into the `forward` repo over the GitHub API (see `features/design-demo-admin.md`). Noted the
@@ -0,0 +1,181 @@
1
+ ---
2
+ title: CloudFront Client Setup
3
+ framework: "1.0"
4
+ repo: tools
5
+ project: Tools
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-24
10
+ owners: [jcardinal]
11
+ files:
12
+ - tools/_/app/devops/cloudfront.php
13
+ - tools/mvc/devops/cloudfront-clients/get.php
14
+ - tools/mvc/devops/cloudfront-clients/post.php
15
+ - tools/assets/js/cloudfront-clients.js
16
+ - tools/assets/css/cloudfront-clients.css
17
+ - tools/_/app/nav.php
18
+ - tools/_/app/frameworkindex.php
19
+ - tools/config.production.ini
20
+ related:
21
+ - ../architecture.md
22
+ - ../features/persona-gated-navigation.md
23
+ - ../features/saml-sso-auth.md
24
+ - ../features/design-demo-admin.md
25
+ - ../../standards/backend-php.md
26
+ ---
27
+
28
+ ## Summary
29
+
30
+ An SSO-gated admin tool at **`/devops/cloudfront-clients`** in the Tools 1.0 app that
31
+ onboards a client onto **CloudFront + Route 53 across multiple AWS accounts** in one run.
32
+ For each client slug × selected distribution it adds `{slug}.{baseDomain}` as a CloudFront
33
+ **Alternate Domain Name** and creates the matching Route 53 **A + AAAA alias** records
34
+ pointing at the distribution. It automates a repetitive, multi-account, error-prone manual
35
+ onboarding step. Gated to the **`Development Team`** persona; has a **Dry-run** preview
36
+ mode, a per-run slug cap, and an audit log line per non-dry-run attempt.
37
+
38
+ ## Key files / entry points
39
+
40
+ - `_/app/devops/cloudfront.php` — class **`App_Devops_Cloudfront`** (all-static): the
41
+ single source of truth for account/distribution topology, AWS SDK client construction,
42
+ and all CloudFront + Route 53 domain logic.
43
+ - `mvc/devops/cloudfront-clients/get.php` — route `/devops/cloudfront-clients` (page shell).
44
+ - `mvc/devops/cloudfront-clients/post.php` — AJAX action endpoint (`action=process`).
45
+ - `assets/js/cloudfront-clients.js`, `assets/css/cloudfront-clients.css` — self-guarded on
46
+ the `.cloudfront-clients-tool` root element, registered globally in
47
+ `_/app/frameworkindex.php`.
48
+ - `_/app/nav.php` — new **`devops`** nav group, persona `['Development Team']`.
49
+ - `config.production.ini` — documented, **commented placeholder** `[cloudfront_<accountId>]`
50
+ credential sections (location only — **no secret values** are stored in the knowledge base
51
+ or authored into tracked config here).
52
+
53
+ Follows the same tool pattern as [Design Demo Admin](../features/design-demo-admin.md):
54
+ `get.php` + `post.php` + a static `App_` class + per-tool namespaced js/css + one nav entry.
55
+ The AWS SDK **v3** is vendored at `tools/vendor/autoload.php`; client construction mirrors
56
+ `App_Talos_S3::client()` (explicit autoload-require guard, `class_exists` check, `try/catch`,
57
+ `['ok' => ...]` array returns).
58
+
59
+ ## How it works
60
+
61
+ ### Topology is a single class constant
62
+
63
+ `App_Devops_Cloudfront::CLOUDFRONTS` is the **only** source of truth for what can be
64
+ onboarded, shaped as:
65
+
66
+ ```
67
+ [ '<awsAccountId>' => [ '<distributionId>' => '<baseDomain>' ] ]
68
+ ```
69
+
70
+ Adding or removing a distribution is done **solely** by editing this constant — no other code
71
+ change is required.
72
+
73
+ ### Per slug × distribution
74
+
75
+ 1. Build the FQDN `{slug}.{baseDomain}`.
76
+ 2. **CloudFront alias:** `GetDistribution` → append the FQDN to `Aliases.Items` and bump
77
+ `Aliases.Quantity` → `UpdateDistribution` with `IfMatch = ETag`.
78
+ 3. **Route 53 records:** create **A** and **AAAA** alias records targeting the distribution's
79
+ DNS name. `AliasTarget.HostedZoneId` is the fixed AWS CloudFront constant
80
+ **`Z2FDTNDATAQYW2`**; `EvaluateTargetHealth = false`.
81
+
82
+ ### Route 53 zone lookup — by name, not list-all
83
+
84
+ Zone resolution uses **`ListHostedZonesByName`** with the most-specific candidate walking down
85
+ to the registrable apex (e.g. `compass.dev.sandbox.togasupply.com` → the `togasupply.com`
86
+ zone). This jumps straight to the zone regardless of how many zones the account holds. The
87
+ earlier `ListHostedZones` (list-all + longest-suffix match) approach **silently found
88
+ nothing** and was replaced.
89
+
90
+ ### Record creation is INSERT-ONLY (never UPSERT)
91
+
92
+ Existing A/AAAA records are read first (`existingAliasTypes` via `ListResourceRecordSets`); the
93
+ tool creates **only the missing types** with the `CREATE` action. An existing record (e.g. a
94
+ live production DNS entry) is **always left untouched**. UPSERT is deliberately never used —
95
+ it would silently repoint a live production record.
96
+
97
+ ### Throttling + consistency gating
98
+
99
+ - **Adaptive retries:** the SDK client is built with
100
+ `'retries' => ['mode' => 'adaptive', 'max_attempts' => 10]` because CloudFront
101
+ `UpdateDistribution` is aggressively rate-limited and a run fires many calls (was hitting
102
+ `Throttling: Rate exceeded`).
103
+ - **Route 53 is gated on the CloudFront alias step succeeding for that item.** If the alias
104
+ fails (throttled, or `CNAMEAlreadyExists`), the Route 53 step is **skipped** so DNS is never
105
+ created pointing at a distribution that is not serving that hostname. Skips are **per item**;
106
+ the overall run continues past failures.
107
+
108
+ ### Safety rails
109
+
110
+ - **Dry-run (preview only)** mode performs no writes.
111
+ - **`MAX_SLUGS_PER_RUN = 100`** caps a single run.
112
+ - Every **non-dry-run** attempt writes an **audit log** line including the acting user's email
113
+ (`App_Auth::currentUser()`).
114
+
115
+ ## Cross-account credential model
116
+
117
+ Every account — **including** the production account `654654170868` that owns all Route 53
118
+ zones — resolves to its **own** `[cloudfront_<accountId>]` config section through the single
119
+ seam method **`credentialsForAccount()`**. Do **not** reuse the existing `[aws]` key for the
120
+ prod account: `[aws]` is a **cross-account** key that only reaches the `654654170868` SQS queue
121
+ from another account — it is **not an identity in `654654170868`**.
122
+
123
+ On the actual EB server the **`ElasticBeanstalk-EC2-Instance-Profile`** role *is* in
124
+ `654654170868`, so the prod account can use the instance role (no config key needed) once that
125
+ role is granted: `route53:ListHostedZonesByName`, `route53:ChangeResourceRecordSets`,
126
+ `route53:ListResourceRecordSets`, `cloudfront:GetDistribution`, `cloudfront:UpdateDistribution`.
127
+
128
+ ## Access control
129
+
130
+ Behind Tools SSO; the nav action and page are gated to the **`Development Team`** persona (see
131
+ [persona-gated-navigation](../features/persona-gated-navigation.md) and
132
+ [saml-sso-auth](../features/saml-sso-auth.md)).
133
+
134
+ ## Client variations
135
+
136
+ None — this is a shared internal DevOps tool. It *operates on* client slugs, but no single
137
+ client owns or overrides it.
138
+
139
+ ## Gotchas / known issues
140
+
141
+ - **`NoSuchDistribution` / zone-not-found means the WRONG ACCOUNT, not a permission problem.**
142
+ Permission failures return `AccessDenied`. A `NoSuchDistribution` or a missing
143
+ `togasupply.com` zone (when both demonstrably exist) means the call **authenticated into the
144
+ wrong account** — here, using the cross-account `[aws]` key against `654654170868`. Fix the
145
+ credential resolution, not the IAM policy. This is the key diagnostic for this tool.
146
+ - **A CloudFront CNAME can live on only one distribution.** Re-adding an FQDN already attached
147
+ to a different distribution returns **`CNAMEAlreadyExists`**; the item is skipped (and Route 53
148
+ is skipped with it).
149
+ - **Alternate domain names require a covering ACM certificate.** A wildcard cert covers only a
150
+ single label — `*.togasupply.com` does not cover `a.b.togasupply.com`. Ensure the distribution
151
+ has a cert covering the FQDN before adding it.
152
+ - **Route 53 CloudFront alias records use the fixed `HostedZoneId` `Z2FDTNDATAQYW2`** (an AWS
153
+ global constant for all CloudFront alias targets — not the zone's own ID).
154
+ - **1.0 promotes undefined-index warnings to a thrown `ErrorException`** that would abort the
155
+ whole "never fatally abort" run — all AWS-response array access is guarded. The SDK
156
+ client-builder `catch` is widened to **`\Throwable`** because the constructor can throw
157
+ `InvalidArgumentException`, not just `AwsException`.
158
+ - **Credential exposure caveat.** This tool wields the prod IAM key, which amplified the impact
159
+ of the separate config-file web-root exposure fixed this session — see the 1.0 back-end
160
+ security standard and the Tools architecture Known issues. Every secret in
161
+ `config.production.ini` still owes rotation because the file was publicly readable.
162
+
163
+ ## Change history
164
+
165
+ - 2026-07-24 — Built the CloudFront Client Setup tool (`App_Devops_Cloudfront` +
166
+ `/devops/cloudfront-clients` MVC route + namespaced assets + a `devops` nav group). Topology
167
+ is the `CLOUDFRONTS` class constant; per slug×distribution it adds a CloudFront alternate
168
+ domain name and Route 53 A+AAAA alias records (fixed `Z2FDTNDATAQYW2`). Cross-account
169
+ credentials resolve per-account via `credentialsForAccount()` (never reuse `[aws]` for the prod
170
+ account 654654170868 — instance role is used there). Route 53 lookup switched to
171
+ `ListHostedZonesByName`; record creation is INSERT-ONLY (never UPSERT); SDK adaptive retries
172
+ for CloudFront throttling; Route 53 gated on the alias step succeeding. Dry-run, per-run slug
173
+ cap (100), and per-attempt audit logging. (jcardinal)
174
+
175
+ ## Related docs
176
+
177
+ - [Tools Architecture](../architecture.md)
178
+ - [Design Demo Admin](../features/design-demo-admin.md)
179
+ - [Back-End Coding Standards (1.0)](../../standards/backend-php.md)
180
+ </content>
181
+ </invoke>
@@ -5,7 +5,7 @@ project: Library
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-07-09
8
+ updated: 2026-07-24
9
9
  owners: [jcardinal, rgirish, mhammontree]
10
10
  files: []
11
11
  related:
@@ -453,6 +453,45 @@ $order->location = App_Page::getVarIfSet('installation');
453
453
 
454
454
  * Always use HTTPS to encrypt data in transit.
455
455
 
456
+ ### Web-root file exposure — deny config/secret files at the front controller
457
+
458
+ A front controller whose only guard is a rewrite that skips existing files —
459
+ `RewriteCond %{REQUEST_FILENAME} !-f` — is **not access control**. `!-f` means "if the
460
+ requested path is a real file on disk, let Apache serve it directly rather than routing it
461
+ through `index.php`." Any file that physically sits under the web root (config, `.env`,
462
+ logs, SQL dumps, backups) is therefore served **verbatim** on a direct request. A plain
463
+ `GET /config.production.ini` returns the raw file, secrets and all. This is a real breach
464
+ class, not a theoretical one — it was found live in a 1.0 app this session.
465
+
466
+ Deny sensitive files explicitly at the front controller, in addition to (not instead of)
467
+ keeping them out of the web root:
468
+
469
+ ```apache
470
+ # Deny by sensitive extension
471
+ <FilesMatch "\.(ini|env|log|sql|sh|bak|dist)$">
472
+ Require all denied
473
+ </FilesMatch>
474
+
475
+ # Deny anything named config.* (config.production.ini, config.prod.ini, …)
476
+ <FilesMatch "^config\.">
477
+ Require all denied
478
+ </FilesMatch>
479
+ ```
480
+
481
+ Rules:
482
+
483
+ * **Keep config/secret files outside the web root where at all possible** — a file the web
484
+ server cannot reach cannot be served. Deny blocks are defense in depth for files that
485
+ must live inside the tree.
486
+ * **The `!-f` rewrite is a router optimization, never a security boundary.** Do not rely on
487
+ it to hide anything.
488
+ * **Anything that was ever web-readable is compromised and must be rotated** — assume it was
489
+ fetched. Rotate every credential in an exposed file; a deny rule added after the fact does
490
+ not un-expose what was already reachable.
491
+ * **Audit every 1.0 app's front controller** for this pattern. If a `.htaccess`/vhost routes
492
+ through `index.php` on `!-f` alone with no `FilesMatch` deny blocks, it is exposed — add
493
+ the deny blocks and treat any secret-bearing file under the root as leaked.
494
+
456
495
  ### Authentication and Authorization
457
496
 
458
497
  * Use the framework's `App_Auth` / `App_Acl` for authentication and role-based access control; do not roll your own session/permission checks.
@@ -13,7 +13,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
13
13
  - **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
14
14
  - **test** (Test) — 13 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
15
15
  - **toga** (TOGa) — 2 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
16
- - **tools** (Tools) — 9 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
16
+ - **tools** (Tools) — 10 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
17
17
 
18
18
  ## 2.0 framework
19
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.437",
3
+ "version": "1.0.438",
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",