toga-ai 1.0.437 → 1.0.439
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/1.0/apps/tools/INDEX.md +1 -0
- package/knowledge/1.0/apps/tools/architecture.md +19 -7
- package/knowledge/1.0/apps/tools/features/cloudfront-client-setup.md +181 -0
- package/knowledge/1.0/standards/backend-php.md +40 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/sessions/2026-07-27-true-79533-pin-depth-mhammontree.md +73 -0
- package/package.json +1 -1
|
@@ -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-
|
|
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
|
|
48
|
-
|
|
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
|
|
90
|
-
plaintext production secrets (RDS master
|
|
91
|
-
|
|
92
|
-
|
|
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-
|
|
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.
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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) —
|
|
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
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: session
|
|
3
|
+
slug: true-79533-pin-depth
|
|
4
|
+
title: TRUE-79533 WH service-address pin blocked by depth=-1 starving postPost
|
|
5
|
+
author: mhammontree
|
|
6
|
+
repos: [_underscore, dbchanges2, toga2-view]
|
|
7
|
+
framework: "2.0"
|
|
8
|
+
client: rate
|
|
9
|
+
status: active
|
|
10
|
+
created: 2026-07-27
|
|
11
|
+
updated: 2026-07-27
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Session: true-79533-pin-depth
|
|
15
|
+
**Date:** 2026-07-27
|
|
16
|
+
**Project/Repo:** _underscore / dbchanges2 / toga2-view (2.0, client Rate)
|
|
17
|
+
**Task:** Get the validated Whole Home Warranty service address to pin (`Entitlements.serviceAddressId`) and render on beta. Backend is done and correct; the live blocker is that the beta purchase POST sends `depth=-1`, which starves the `postPost` interceptor of the just-created address id.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What WORKED
|
|
22
|
+
<!-- Include specific file paths and evidence -->
|
|
23
|
+
- **Backend pin implemented** in `_underscore/Model/Rate/Entitlement.php` `postPost` → `persistWarrantyServiceAddress`: reads the created address id from the in-memory hydrated payload (`$payload->contact->primaryContactAddress->address->id`), then `UPDATE Addresses SET isValidated=1` + `UPDATE Entitlements SET serviceAddressId=… WHERE uuid=…`. Committed `dff1fb5e`. **Proven working**: a no-`depth` (deep-payload) purchase — ET100057, made from the local FE against the beta backend — pinned `serviceAddressId=56` with `Addresses.id 56` (STE 400) `isValidated=1`.
|
|
24
|
+
- **`serviceAddressId` is a STANDARD field** on base `_underscore/Model/Client/Entitlement.php:20` (`FIELD_FOREIGNKEY` → `_Model_Client_Address`), inherited by the Rate model. Verified in the model file. So V2 persists/reads it; the postPost UPDATE and the FE read both rely on this.
|
|
25
|
+
- **Display EZ-2 fixed**: `dbchanges2/Client_Rate/2026-07-24a - AddressIdFieldPermission.sql` grants READ on `Addresses.id` for roles 1/2/3 (isWritable 0). Committed `ea1e0fb`. Applied on beta manually and verified (`AclFieldPermissions` field 41 → roles 1,2,3). This fixes the FE `GET /v2/addresses?fields=id,…` 403.
|
|
26
|
+
- **Hard-blocks now return HTTP 400** via `_Exception_Validation` (the 3 prePost throws: missing address, unverifiable address, duplicate WH). Committed `41a5dc1a`. Verified against `api2/Controller/Index.php:204-222` — it catches `_Exception_Validation` → `DEFINED_MESSAGE_ERROR_BAD_REQUEST` (400 + clean message); all other exceptions → 500 + stack trace. This is what makes the FE blocked-purchase toaster work.
|
|
27
|
+
- **Dedup** `hasActiveWarrantyAtAddress` forces the WRITE host + disables the query cache for a fresh read (committed). `COALESCE(serviceAddressId, ContactAddresses.addressId)` fallback so unpinned WHs are still deduped.
|
|
28
|
+
- **Root cause proven via `Logs_Rate.Api`** (beta, MCP env `dev-sandbox`): the local-FE and beta-FE purchases hit the SAME beta backend instance (`i-0ce5624547cfecc93`, `api.beta.togahub.com`). The ONLY difference is the request query string — local: `transactionId=…` (no depth) → 64,650-byte deep response → **pinned**; beta: `depth=-1&transactionId=…` → 644-byte shallow response → **starved** (no address node in the payload postPost receives).
|
|
29
|
+
- **`-1` source confirmed = front-end**: `toga2-view/src/pages/CheckOut/api/checkoutApi.ts:21` (`createService` passes `{ 'depth': -1 }`), serialized to `?depth=-1`. Confirmed in the logged query string.
|
|
30
|
+
- **Beta interceptor config read** (`ApiPayloadInterceptors` join `Core.Records`): entitlements `postPost` = id **2**, active, **minDepth 5**; entitlements `prePost` = id 4, active, minDepth **null → default 3**; sales-orders `postPost` = id 3, null→3.
|
|
31
|
+
|
|
32
|
+
## What did NOT work — DO NOT RETRY THESE
|
|
33
|
+
<!-- Exact failure reasons — do not vague-ify -->
|
|
34
|
+
- **Forcing the write host inside `persistWarrantyServiceAddress`** (commit `7b63955d`, later removed in `dff1fb5e`): NO EFFECT. V2 already globally disables the read host for the entire create block (`api2/.../V2.php:5024`), so the lookup was ALREADY on the write connection — forcing it again was a no-op. Confirmed by a genuinely post-deploy purchase (ET100051/52/53) still returning null.
|
|
35
|
+
- **The original 3-table JOIN lookup** in postPost (`Entitlement → Contacts.primaryContactAddressId → ContactAddresses → addressId`): returned 0 rows *in-request* on beta even on the write connection. The identical query resolves correctly *post-commit*. Superseded by the payload approach.
|
|
36
|
+
- **The payload-based pin (`$address->id`) still returned null on beta** — NOT because the code is wrong, but because with `depth=-1` the payload built for `postPost` is shallow (`meta.calcDepth: 1`) and does **not contain the `contact.primaryContactAddress.address` node at all**, so `$address->id` (and the `uuid` fallback) find nothing. Root cause is the depth, not the pin code.
|
|
37
|
+
- **Merging `_underscore` to `_beta` did NOT deploy to beta.** The beta EB env **API-Sandbox-Dev** pulls `_underscore` from branch **`_sandbox-dev`** via `api2/.platform/hooks/prebuild/git.sh` + `api2/.ebextensions/git.sandbox-dev.json` (the prebuild hook `rm -Rf`s and re-clones that branch, overwriting the pipeline copy). Deploying anything not on `_sandbox-dev` runs old code.
|
|
38
|
+
- **"Removing `depth=-1` from the beta curl" (Fri/earlier)**: the edited curl STILL had `depth=-1` in the query string (it wasn't actually removed) — confirmed via the logged `queryString`. So that test never proved anything.
|
|
39
|
+
- **The single-host-vs-multi-host connection theory** (an earlier hypothesis): DISPROVEN. Both local-FE and beta-FE requests run on the SAME beta backend instance. The difference is purely the request's `depth` param, not connection topology.
|
|
40
|
+
|
|
41
|
+
## Not tried yet (candidates for next session)
|
|
42
|
+
- **THE decisive test (do this first):** on beta `Client_Rate`, `UPDATE ApiPayloadInterceptors SET minDepth = 10 WHERE id = 2;` then fire one fresh WH purchase (FE still sends `depth=-1`) and check `meta.calcDepth` + whether the new entitlement pins (`serviceAddressId` set, its address `isValidated=1`). Open question for Rohan: does the `minDepth` clamp engage for the `-1` sentinel at all? It's already 5 yet the build came through at `calcDepth 1` — so either it needs > 5 or `-1` bypasses the clamp.
|
|
43
|
+
- **FE fix (alternative/parallel):** drop `{ 'depth': -1 }` at `toga2-view/src/pages/CheckOut/api/checkoutApi.ts:21` (or set ≥ minDepth), deploy `toga2-view`. This is the FE-side lever; it's owned by Tanner/Alex and the repo is currently on `_beta`.
|
|
44
|
+
- Merge `_underscore` `TRUE-79533` → `_sandbox-dev` + deploy (ships the pin + 400 fix; backend is NOT yet on beta).
|
|
45
|
+
- Merge `dbchanges2` `TRUE-79533` to its deploy branch for prod parity (beta already has the migrations, applied manually).
|
|
46
|
+
- Run `/capture`.
|
|
47
|
+
- File side tickets: (1) framework `minDepth`-clamp appears to be a no-op for `depth=-1` (starves ALL payload interceptors); (2) AIG contract block (`postPost`) has the SAME `depth` starvation — silently skipped for beta-FE purchases; (3) SECURITY: plaintext GitHub PAT committed in `api2/.ebextensions/git.*.json` — rotate + move to env config.
|
|
48
|
+
|
|
49
|
+
## Current file state
|
|
50
|
+
| File | Status | Notes |
|
|
51
|
+
|------|--------|-------|
|
|
52
|
+
| `_underscore/Model/Rate/Entitlement.php` | Committed on `TRUE-79533` (HEAD `41a5dc1a`) | payload-based pin (`dff1fb5e`), dedup write-host, `_Exception_Validation` hard-blocks (`41a5dc1a`). NOT merged to `_sandbox-dev`. |
|
|
53
|
+
| `_underscore/Model/Client/Entitlement.php` | Committed | `serviceAddressId` standard FK field (line 20). |
|
|
54
|
+
| `dbchanges2/Client_Rate/2026-07-24a - AddressIdFieldPermission.sql` | Committed (`ea1e0fb`) | `Addresses.id` read ACL. Applied on beta manually. |
|
|
55
|
+
| `dbchanges2` serviceAddressId col + `Core` field + ACL migrations | Committed on `TRUE-79533` | Applied on beta manually; branch needs merge for prod. |
|
|
56
|
+
| `toga2-view/src/pages/CheckOut/api/checkoutApi.ts` | UNCHANGED (on `_beta`) | Line 21 still `'depth': -1` — the FE fix is pending. |
|
|
57
|
+
| `test/@Mark/Rate/verify_wholehome_per_address_guard.php` | Updated (personal, not in a repo) | `c_serviceAddressId` → `serviceAddressId`; documents the multi-host/depth caveat. |
|
|
58
|
+
|
|
59
|
+
## Decisions made
|
|
60
|
+
- **Pin from the in-memory hydrated payload, not a DB read-back.** Rationale: in-request DB SELECTs can't reliably see the transaction's own uncommitted nested writes; V2's `getFullModelData` hydrates the payload before `postPost`. Rejected: the JOIN lookup (0 rows in-request) and an async post-commit worker (more surface; unnecessary once depth is fixed).
|
|
61
|
+
- **`serviceAddressId` = STANDARD field** (per Jeff), not the old Rate-custom `c_serviceAddressId`.
|
|
62
|
+
- **Hard-blocks throw `_Exception_Validation`** so they map to HTTP 400 with the message surfaced (the FE checkout expects 400). Rejected: plain `_Exception` → 500 + stack trace.
|
|
63
|
+
- **Preferred depth fix = raise the `postPost` interceptor `minDepth`** (Rohan's lever) over the FE change: config-only, FE-independent, and also un-breaks the AIG block. Pending the beta test to confirm the clamp actually deepens the build for a `depth=-1` request.
|
|
64
|
+
|
|
65
|
+
## Blockers
|
|
66
|
+
- Pin cannot fire on beta until the `depth` collision is resolved: FE sends `depth=-1`, and the `postPost` interceptor `minDepth 5` floor is NOT producing a deep-enough payload (`calcDepth 1`). Open question to Rohan: does the `minDepth` clamp engage for `depth=-1`, and what target value? Needs the beta `UPDATE` test to settle.
|
|
67
|
+
- Backend (`TRUE-79533`) is not yet on beta — it lives on `TRUE-79533`, but beta deploys `_underscore` from `_sandbox-dev`.
|
|
68
|
+
|
|
69
|
+
## Exact next step
|
|
70
|
+
> On beta (`dev-sandbox`, `Client_Rate`) run `UPDATE ApiPayloadInterceptors SET minDepth = 10 WHERE id = 2;`, then make one fresh WH purchase (FE unchanged, still sends `depth=-1`) and check the create response `meta.calcDepth` plus the new entitlement's `serviceAddressId` and its `Addresses.isValidated`. If it pins → the clamp works and 5 was too low; dial in the minimal value and ship it as a `dbchanges2 Client_Rate` migration (no FE change). If still null / `calcDepth` stays 1 → the clamp doesn't engage for `-1`; fix the FE (`checkoutApi.ts:21` drop `depth:-1`) instead. Confirm the intended target value with Rohan first.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
_Saved by /session-save on 2026-07-27_
|
package/package.json
CHANGED