toga-ai 1.0.122 → 1.0.124
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/2.0/apps/toga2-commerce/INDEX.md +5 -0
- package/knowledge/2.0/apps/toga2-commerce/features/cart-notification-emails.md +79 -0
- package/knowledge/INDEX.md +2 -0
- package/knowledge/clients/compass-canada/profile.md +3 -2
- package/knowledge/clients/compass-usa/profile.md +3 -2
- package/knowledge/registry.json +161 -19
- package/knowledge/standalone/apps/forward/INDEX.md +6 -0
- package/knowledge/standalone/apps/forward/architecture.md +121 -0
- package/knowledge/standalone/apps/forward/features/encrypted-link-handler.md +70 -0
- package/package.json +1 -1
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# toga2-commerce (TOGa Commerce) — 2.0 knowledge
|
|
2
|
+
|
|
3
|
+
| Doc | Summary | Files |
|
|
4
|
+
|-----|---------|-------|
|
|
5
|
+
| [Cart Notification Emails — duplicate prevention](features/cart-notification-emails.md) | On the cart "Notifications" section a user can add CC email addresses to an order. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/stores/useEmailOptionsStore.ts, src/stores/useCartSalesQuoteZu.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts |
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Cart Notification Emails — duplicate prevention
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga2-commerce
|
|
5
|
+
project: TOGa Commerce
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-18
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- src/pages/Cart/CartPage.tsx
|
|
13
|
+
- src/pages/Cart/view/cartForm/CartForm.tsx
|
|
14
|
+
- src/stores/useEmailOptionsStore.ts
|
|
15
|
+
- src/stores/useCartSalesQuoteZu.ts
|
|
16
|
+
- src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts
|
|
17
|
+
related: []
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
On the cart "Notifications" section a user can add CC email addresses to an order. Adding the
|
|
22
|
+
same address twice (often with different casing) used to slip through to the API and blow up on
|
|
23
|
+
submit with a MySQL 1062 duplicate-key error on the `SalesOrderEmailAddresses.salesOrderId_emailAddress`
|
|
24
|
+
unique index (whose collation `utf8mb4_0900_ai_ci` is case-insensitive). This feature blocks
|
|
25
|
+
duplicates case-insensitively in the frontend and shows a sapphire info message instead of letting
|
|
26
|
+
the error reach the user.
|
|
27
|
+
|
|
28
|
+
## Key files / entry points
|
|
29
|
+
- `CartPage.tsx` — `handleAddEmail`: validates the email regex, then on success calls
|
|
30
|
+
`addEmailOption` (UI options list) and `addEmail` (the cart sales-quote store that builds the
|
|
31
|
+
`salesOrderEmailAddresses` payload). Normalizes the input once with `.trim()`.
|
|
32
|
+
- `CartForm.tsx` — owns the duplicate UX. `handleAddEmailWithDuplicateCheck` wraps the passed-in
|
|
33
|
+
`handleAddEmail`: it case-insensitively checks the existing `emails` list and, on a match, shows
|
|
34
|
+
the message instead of adding. The **Add Email** button calls this wrapper.
|
|
35
|
+
- `useEmailOptionsStore.ts` — `addEmailOption` de-dupes the checkbox list.
|
|
36
|
+
- `useCartSalesQuoteZu.ts` — `addEmail` de-dupes the actual `salesOrderEmailAddresses` payload.
|
|
37
|
+
|
|
38
|
+
## How it works
|
|
39
|
+
1. **Before (manual add):** clicking Add Email runs `handleAddEmailWithDuplicateCheck` in
|
|
40
|
+
`CartForm`. It compares `(e.email ?? "").trim().toLowerCase()` against the normalized input. On a
|
|
41
|
+
match it sets `duplicateEmailMessage` and returns (does not add).
|
|
42
|
+
2. **After (auto-add on user select):** when the order user is chosen, their email (and manager's)
|
|
43
|
+
is auto-added via `addEmailOption`. Both stores de-dupe case-insensitively, so a case-variant of
|
|
44
|
+
an already-present address is silently dropped rather than duplicated.
|
|
45
|
+
3. **Display:** the message renders via the shared `InfoBanner` component (the same one used for the
|
|
46
|
+
"view as" / "edit order" banners), styled `text-sapphire-700 text-xs`, no icon. It is NOT a
|
|
47
|
+
toaster and NOT a red `setError` validation — it is a sapphire info message.
|
|
48
|
+
4. The message text comes from the `notifications.duplicateEmailAddressError.label` field config,
|
|
49
|
+
present in every `CARTPAGE.ts` (all client/role/language variants, English + French).
|
|
50
|
+
5. A `useEffect` clears the message when the input, selected user (`orderForUser?.uuid`), or email
|
|
51
|
+
list (`emails.length`) changes.
|
|
52
|
+
|
|
53
|
+
## Data model
|
|
54
|
+
Frontend only. The payload list maps to the `SalesOrderEmailAddresses` table (api2/backend), which
|
|
55
|
+
has a case-insensitive unique index on `(salesOrderId, emailAddress)`.
|
|
56
|
+
|
|
57
|
+
## Client variations
|
|
58
|
+
The de-dupe mechanism is uniform across clients. Only the message label text differs per client
|
|
59
|
+
config: English `"This email has already been added"`; French (Compass Canada)
|
|
60
|
+
`"Cette adresse e-mail a déjà été ajoutée"`. The label must exist in every `CARTPAGE.ts` variant or
|
|
61
|
+
the message resolves to `undefined` and renders blank.
|
|
62
|
+
|
|
63
|
+
## Gotchas / known issues
|
|
64
|
+
- The real source of the 1062 was `useCartSalesQuoteZu.addEmail` comparing with `===`
|
|
65
|
+
(case-sensitive). Both stores must compare case-insensitively; fixing only the UI list is not
|
|
66
|
+
enough because `addEmail` builds the payload.
|
|
67
|
+
- Normalize with `(x ?? "").trim().toLowerCase()`, not `x?.trim().toLowerCase()` — both are
|
|
68
|
+
nullish-safe (optional chaining short-circuits the whole chain, it does not throw), but `(x ?? "")`
|
|
69
|
+
guarantees a string and avoids an `undefined === undefined` match edge.
|
|
70
|
+
- Adding the label to only some `CARTPAGE.ts` files leaves other client/role/language users with a
|
|
71
|
+
blank message.
|
|
72
|
+
- Do not use a toaster or `setError` (red) for this — product wants the sapphire info style.
|
|
73
|
+
|
|
74
|
+
## Change history
|
|
75
|
+
- 2026-06-18 — Initial: case-insensitive de-dupe in both cart stores, sapphire `InfoBanner` message in
|
|
76
|
+
CartForm, `duplicateEmailAddressError` label added to all CARTPAGE.ts variants (bala)
|
|
77
|
+
|
|
78
|
+
## Related docs
|
|
79
|
+
None yet.
|
package/knowledge/INDEX.md
CHANGED
|
@@ -25,10 +25,12 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
25
25
|
- **talos** (TOGa IQ) — 6 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
|
|
26
26
|
- **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
|
|
27
27
|
- **ai-bdr** (AI-BDR) — 4 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
|
|
28
|
+
- **toga2-commerce** (TOGa Commerce) — 1 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
|
|
28
29
|
|
|
29
30
|
## standalone framework
|
|
30
31
|
|
|
31
32
|
- **togatech** (TOGA Technology Website) — 1 doc(s) → [standalone/apps/togatech/INDEX.md](standalone/apps/togatech/INDEX.md)
|
|
33
|
+
- **forward** (Forwarder) — 2 doc(s) → [standalone/apps/forward/INDEX.md](standalone/apps/forward/INDEX.md)
|
|
32
34
|
|
|
33
35
|
## Clients
|
|
34
36
|
|
|
@@ -5,14 +5,15 @@ apps:
|
|
|
5
5
|
- _underscore
|
|
6
6
|
- api2
|
|
7
7
|
- toga2-supply
|
|
8
|
+
- toga2-commerce
|
|
8
9
|
- worker
|
|
9
10
|
- dbchanges2
|
|
10
11
|
project: _Underscore
|
|
11
12
|
client: compass-canada
|
|
12
13
|
type: profile
|
|
13
14
|
status: active
|
|
14
|
-
updated: 2026-06-
|
|
15
|
-
owners: [jcardinal]
|
|
15
|
+
updated: 2026-06-18
|
|
16
|
+
owners: [jcardinal, bala]
|
|
16
17
|
files: []
|
|
17
18
|
related:
|
|
18
19
|
- ../compass-usa/profile.md
|
|
@@ -5,14 +5,15 @@ apps:
|
|
|
5
5
|
- _underscore
|
|
6
6
|
- api2
|
|
7
7
|
- toga2-supply
|
|
8
|
+
- toga2-commerce
|
|
8
9
|
- worker
|
|
9
10
|
- dbchanges2
|
|
10
11
|
project: _Underscore
|
|
11
12
|
client: compass-usa
|
|
12
13
|
type: profile
|
|
13
14
|
status: active
|
|
14
|
-
updated: 2026-06-
|
|
15
|
-
owners: [jcardinal]
|
|
15
|
+
updated: 2026-06-18
|
|
16
|
+
owners: [jcardinal, bala]
|
|
16
17
|
files: []
|
|
17
18
|
related:
|
|
18
19
|
- features/asn-to-item-fulfillment.md
|
package/knowledge/registry.json
CHANGED
|
@@ -1,21 +1,163 @@
|
|
|
1
1
|
[
|
|
2
|
-
{
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
2
|
+
{
|
|
3
|
+
"repo": "_underscore",
|
|
4
|
+
"project": "_Underscore",
|
|
5
|
+
"framework": "2.0",
|
|
6
|
+
"role": "core",
|
|
7
|
+
"dependsOn": []
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"repo": "worker2",
|
|
11
|
+
"project": "Worker",
|
|
12
|
+
"framework": "2.0",
|
|
13
|
+
"role": "app",
|
|
14
|
+
"dependsOn": []
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"repo": "api2",
|
|
18
|
+
"project": "API",
|
|
19
|
+
"framework": "2.0",
|
|
20
|
+
"role": "app",
|
|
21
|
+
"dependsOn": []
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"repo": "dbchanges2",
|
|
25
|
+
"project": "Database Changes",
|
|
26
|
+
"framework": "2.0",
|
|
27
|
+
"role": "core",
|
|
28
|
+
"dependsOn": []
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"repo": "library",
|
|
32
|
+
"project": "Library",
|
|
33
|
+
"framework": "1.0",
|
|
34
|
+
"role": "core",
|
|
35
|
+
"dependsOn": []
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"repo": "worker",
|
|
39
|
+
"project": "Worker",
|
|
40
|
+
"framework": "1.0",
|
|
41
|
+
"role": "app",
|
|
42
|
+
"dependsOn": []
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"repo": "toga2-supply",
|
|
46
|
+
"project": "TOGa Supply",
|
|
47
|
+
"framework": "2.0",
|
|
48
|
+
"role": "app",
|
|
49
|
+
"dependsOn": [
|
|
50
|
+
"api2"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"repo": "saml",
|
|
55
|
+
"project": "SAML SSO Gateway",
|
|
56
|
+
"framework": "2.0",
|
|
57
|
+
"role": "app",
|
|
58
|
+
"dependsOn": []
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"repo": "toga2-view",
|
|
62
|
+
"project": "TOGa View Frontend",
|
|
63
|
+
"framework": "2.0",
|
|
64
|
+
"role": "app",
|
|
65
|
+
"dependsOn": [
|
|
66
|
+
"api2"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"repo": "togadesk",
|
|
71
|
+
"project": "TOGa Desk",
|
|
72
|
+
"framework": "1.0",
|
|
73
|
+
"role": "app",
|
|
74
|
+
"dependsOn": []
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"repo": "togaview",
|
|
78
|
+
"project": "TOGa View",
|
|
79
|
+
"framework": "1.0",
|
|
80
|
+
"role": "app",
|
|
81
|
+
"dependsOn": []
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"repo": "toga2-hub",
|
|
85
|
+
"project": "TOGa Hub",
|
|
86
|
+
"framework": "2.0",
|
|
87
|
+
"role": "app",
|
|
88
|
+
"dependsOn": [
|
|
89
|
+
"api2"
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"repo": "togatech",
|
|
94
|
+
"project": "TOGA Technology Website",
|
|
95
|
+
"framework": "standalone",
|
|
96
|
+
"role": "app",
|
|
97
|
+
"dependsOn": []
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"repo": "webhook",
|
|
101
|
+
"project": "Webhook",
|
|
102
|
+
"framework": "1.0",
|
|
103
|
+
"role": "app",
|
|
104
|
+
"dependsOn": [
|
|
105
|
+
"library"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"repo": "walmarttechservices",
|
|
110
|
+
"project": "Walmart Tech Services",
|
|
111
|
+
"framework": "1.0",
|
|
112
|
+
"role": "app",
|
|
113
|
+
"dependsOn": [
|
|
114
|
+
"library"
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"repo": "talos",
|
|
119
|
+
"project": "TOGa IQ",
|
|
120
|
+
"framework": "2.0",
|
|
121
|
+
"role": "app",
|
|
122
|
+
"dependsOn": [],
|
|
123
|
+
"language": "python"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"repo": "test",
|
|
127
|
+
"project": "Test",
|
|
128
|
+
"framework": "1.0",
|
|
129
|
+
"role": "app",
|
|
130
|
+
"dependsOn": [
|
|
131
|
+
"library"
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"repo": "voice-to-voice",
|
|
136
|
+
"project": "TOGa Voice",
|
|
137
|
+
"framework": "2.0",
|
|
138
|
+
"role": "app",
|
|
139
|
+
"dependsOn": [],
|
|
140
|
+
"language": "python"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"repo": "ai-bdr",
|
|
144
|
+
"project": "AI-BDR",
|
|
145
|
+
"framework": "2.0",
|
|
146
|
+
"role": "app",
|
|
147
|
+
"dependsOn": []
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"repo": "forward",
|
|
151
|
+
"project": "Forwarder",
|
|
152
|
+
"framework": "standalone",
|
|
153
|
+
"role": "app",
|
|
154
|
+
"dependsOn": []
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"repo": "toga2-commerce",
|
|
158
|
+
"project": "TOGa Commerce",
|
|
159
|
+
"framework": "2.0",
|
|
160
|
+
"role": "app",
|
|
161
|
+
"dependsOn": ["api2"]
|
|
162
|
+
}
|
|
21
163
|
]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# forward (Forwarder) — standalone knowledge
|
|
2
|
+
|
|
3
|
+
| Doc | Summary | Files |
|
|
4
|
+
|-----|---------|-------|
|
|
5
|
+
| [Forwarder Architecture](architecture.md) | Forwarder is a tiny **standalone PHP application** that powers TOGA's short, branded redirect domains. | forward/forward.ini, forward/index.php, forward/.htaccess, forward/.platform/httpd/conf.d/rewritemap.conf, forward/.ebextensions/rewritemap.config, forward/composer.json |
|
|
6
|
+
| [Encrypted-Link Handler](features/encrypted-link-handler.md) | A `index.php` feature in [Forwarder](../architecture.md) for redirect domains whose URL path carries an **encrypted token** that must be decoded before redirect | forward/index.php |
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Forwarder Architecture
|
|
3
|
+
framework: "standalone"
|
|
4
|
+
repo: forward
|
|
5
|
+
project: Forwarder
|
|
6
|
+
client: shared
|
|
7
|
+
type: architecture
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-18
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- forward/forward.ini
|
|
13
|
+
- forward/index.php
|
|
14
|
+
- forward/.htaccess
|
|
15
|
+
- forward/.platform/httpd/conf.d/rewritemap.conf
|
|
16
|
+
- forward/.ebextensions/rewritemap.config
|
|
17
|
+
- forward/composer.json
|
|
18
|
+
related: []
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Forwarder Architecture
|
|
22
|
+
|
|
23
|
+
Forwarder is a tiny **standalone PHP application** that powers TOGA's short, branded
|
|
24
|
+
redirect domains. The pattern: point a short **CNAME** (e.g. `powerbi.togatech.com`,
|
|
25
|
+
`feedback.agilantsolutions.com`) at the Forwarder app running on **AWS Elastic Beanstalk**
|
|
26
|
+
(Amazon Linux 2023 / Apache), and Forwarder issues an HTTP redirect to the real
|
|
27
|
+
destination. It does **not** use either PHP framework (1.0 `App_` or 2.0 `_underscore`),
|
|
28
|
+
which is why it lives under the `standalone/` knowledge partition.
|
|
29
|
+
|
|
30
|
+
Local path on dev machines is tracked in Claude memory (`repo-path-forward`), not here.
|
|
31
|
+
|
|
32
|
+
## Two layers: zero-compute map vs. light PHP compute
|
|
33
|
+
|
|
34
|
+
Forwarder has two distinct redirect mechanisms. The vast majority of traffic never
|
|
35
|
+
touches PHP at all.
|
|
36
|
+
|
|
37
|
+
### 1. Zero-compute redirects (the common case) — Apache `RewriteMap` + `forward.ini`
|
|
38
|
+
|
|
39
|
+
The `.htaccess` at the repo root drives a plain-text lookup table:
|
|
40
|
+
|
|
41
|
+
- `forward.ini` is an Apache **`txt` RewriteMap** of `source destination` lines:
|
|
42
|
+
- `source` = full host or `host/path` (no scheme, no trailing slash)
|
|
43
|
+
- `destination` = full target URL (query strings allowed)
|
|
44
|
+
- Blank lines and `#` comments are ignored.
|
|
45
|
+
- The `RewriteMap forwardmap txt:/var/www/html/forward.ini` directive **cannot** live in
|
|
46
|
+
`.htaccess` (must be server/VirtualHost scope), so it is deployed separately (see
|
|
47
|
+
Deployment below).
|
|
48
|
+
- **Match precedence in `.htaccess`:**
|
|
49
|
+
1. Exact `HTTP_HOST + REQUEST_URI` match → `302` (flags `NE,QSA`).
|
|
50
|
+
2. Else `HTTP_HOST`-only match → `302`, appending the original path + query to the
|
|
51
|
+
destination.
|
|
52
|
+
3. Else, if the request is not a real file/dir, fall through to `index.php`.
|
|
53
|
+
- **Operational win:** editing `forward.ini` takes effect **immediately** — Apache
|
|
54
|
+
re-reads the txt map on the next request after a graceful reload (`sudo systemctl
|
|
55
|
+
reload httpd`); no redeploy or restart needed. Most "add a redirect" requests are a
|
|
56
|
+
one-line edit to `forward.ini`.
|
|
57
|
+
- **Authoring rule:** put the most **specific** `host/path` lines **before** the general
|
|
58
|
+
`host`-only line, or the general line matches first and the path line is never reached
|
|
59
|
+
(documented in the `forward.ini` header).
|
|
60
|
+
- **Health check:** `.htaccess` short-circuits to `200 OK` for `ELB-HealthChecker`
|
|
61
|
+
user-agents and localhost / private-IP hosts (`127.*`, `10.*`, `172.*`, `192.168.*`),
|
|
62
|
+
so AWS ELB health checks pass without a map entry.
|
|
63
|
+
|
|
64
|
+
### 2. Light compute (the exception) — `index.php`
|
|
65
|
+
|
|
66
|
+
`index.php` is reached only when no map entry matched. It handles three cases:
|
|
67
|
+
|
|
68
|
+
- **Encrypted-link handler** — for a hardcoded `$encryptedDomains` allowlist (PCMatic /
|
|
69
|
+
Newegg / `forward.*` agent-download domains), it extracts the encrypted path segment
|
|
70
|
+
(by a per-domain `offset`: `0` = first segment, `1` = second), runs `decrypt()`, then
|
|
71
|
+
`302`s to the domain's configured base URL with the decrypted value `urlencode`d
|
|
72
|
+
appended. Invalid/empty/tampered input returns a styled `400`. See the
|
|
73
|
+
[encrypted-link-handler](features/encrypted-link-handler.md) feature doc.
|
|
74
|
+
- **Local directory handler** — if the first host label matches a directory in the repo
|
|
75
|
+
(e.g. `sos.*` → `/sos/`), it redirects to `/<subdomain>/` and serves that dir's
|
|
76
|
+
`index.html`. Used for the bundled `sos/` Splashtop SOS download page.
|
|
77
|
+
- **404** — otherwise returns a styled `404` echoing the unmatched `host + uri`.
|
|
78
|
+
|
|
79
|
+
## Deployment (Elastic Beanstalk / Apache)
|
|
80
|
+
|
|
81
|
+
- Runs on **Elastic Beanstalk**, Amazon Linux 2023, Apache `httpd`.
|
|
82
|
+
- The `RewriteMap` definition is deployed to `/etc/httpd/conf.d/rewritemap.conf` two ways
|
|
83
|
+
for cross-platform safety:
|
|
84
|
+
- `.platform/httpd/conf.d/rewritemap.conf` — the **Amazon Linux 2+/2023** mechanism
|
|
85
|
+
(current).
|
|
86
|
+
- `.ebextensions/rewritemap.config` — legacy **Amazon Linux 1** `files:` fallback.
|
|
87
|
+
Both can coexist; AL2+ ignores the `.ebextensions` file path managed by `.platform`,
|
|
88
|
+
and AL1 ignores `.platform` entirely.
|
|
89
|
+
- `forward.ini` deploys to `/var/www/html/forward.ini` (the path the map points at).
|
|
90
|
+
- `composer.json` declares a `Togatech\Forward\` PSR-4 autoload over `src/` but has **no
|
|
91
|
+
dependencies** and no `src/` is shipped today — the app is effectively a single
|
|
92
|
+
`index.php` plus the Apache config.
|
|
93
|
+
|
|
94
|
+
## The `decrypt()` scheme
|
|
95
|
+
|
|
96
|
+
`decrypt()` in `index.php` is a **home-rolled** symmetric scheme, used only to obfuscate
|
|
97
|
+
agent-download tokens in URLs — **not** real encryption and not for protecting secrets:
|
|
98
|
+
|
|
99
|
+
1. URL-safe base64 normalize (`-_` → `+/`); the **last char** is a stored integrity hash.
|
|
100
|
+
2. base64-decode the remainder; the **first byte** is the key-start seed.
|
|
101
|
+
3. Regenerate a keystream from the seed (`i += i` per step, `chr(i % 255)`) and XOR it
|
|
102
|
+
against the payload to recover the plaintext.
|
|
103
|
+
4. Verify by recomputing the first char of `base64(hmac_sha256(payload, key))` and
|
|
104
|
+
comparing to the stored hash; mismatch → empty string (treated as failure → `400`).
|
|
105
|
+
|
|
106
|
+
Treat this as obfuscation only. Anything genuinely sensitive must not rely on it.
|
|
107
|
+
|
|
108
|
+
## Notes / gotchas
|
|
109
|
+
|
|
110
|
+
- **No restart needed for new redirects** — edit `forward.ini`, reload httpd. This is the
|
|
111
|
+
single most useful operational fact about this app.
|
|
112
|
+
- Specific-path map lines must precede host-only lines (see `forward.ini` header).
|
|
113
|
+
- New **encrypted** domains require a code change (`$encryptedDomains` in `index.php`),
|
|
114
|
+
not just a `forward.ini` edit.
|
|
115
|
+
- `index.php` only runs on a map miss, so a bad/over-broad `forward.ini` host-only line
|
|
116
|
+
can shadow paths you expected PHP to handle.
|
|
117
|
+
|
|
118
|
+
## Change history
|
|
119
|
+
|
|
120
|
+
- 2026-06-18 (jcardinal) — Initial architecture documentation; registered `forward`
|
|
121
|
+
(Forwarder, standalone) in the knowledge base.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Encrypted-Link Handler
|
|
3
|
+
framework: "standalone"
|
|
4
|
+
repo: forward
|
|
5
|
+
project: Forwarder
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-18
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- forward/index.php
|
|
13
|
+
related:
|
|
14
|
+
- standalone/apps/forward/architecture.md
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Encrypted-Link Handler
|
|
18
|
+
|
|
19
|
+
A `index.php` feature in [Forwarder](../architecture.md) for redirect domains whose URL
|
|
20
|
+
path carries an **encrypted token** that must be decoded before redirecting — used for
|
|
21
|
+
remote-support / agent-download links (PCMatic, Newegg, generic `forward.*` agent
|
|
22
|
+
downloads) where the trailing value (e.g. a user identifier) is obfuscated in the path.
|
|
23
|
+
|
|
24
|
+
## When it runs
|
|
25
|
+
|
|
26
|
+
Only when the Apache `RewriteMap` (`forward.ini`) produced **no match** and the request
|
|
27
|
+
falls through to `index.php`, and the lowercased `HTTP_HOST` is a key in the hardcoded
|
|
28
|
+
`$encryptedDomains` allowlist near the top of `index.php`.
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
`$encryptedDomains` maps a host to `['url' => <base>, 'offset' => <int>]`:
|
|
33
|
+
|
|
34
|
+
- `url` — the destination base URL; the decrypted value is `urlencode`d and appended.
|
|
35
|
+
- `offset` — which **0-based path segment** holds the encrypted token:
|
|
36
|
+
- `0` → `host.com/ENCRYPTED`
|
|
37
|
+
- `1` → `host.com/prefix/ENCRYPTED`
|
|
38
|
+
|
|
39
|
+
Adding a new encrypted domain is a **code change** (edit `$encryptedDomains`), not a
|
|
40
|
+
`forward.ini` edit.
|
|
41
|
+
|
|
42
|
+
## Flow
|
|
43
|
+
|
|
44
|
+
1. Split the request path on `/`; take the segment at `offset` as the token.
|
|
45
|
+
2. Empty token → styled **`400` Invalid Link**.
|
|
46
|
+
3. `decrypt($token)` (see below); failure (empty return) → styled **`400` Decryption
|
|
47
|
+
Failed**.
|
|
48
|
+
4. Success → `header('Location: ' . $base . urlencode($decrypted), 302)`.
|
|
49
|
+
|
|
50
|
+
## The `decrypt()` algorithm
|
|
51
|
+
|
|
52
|
+
Custom symmetric scheme — **obfuscation, not real encryption**:
|
|
53
|
+
|
|
54
|
+
1. Restore URL-safe base64 (`-_` → `+/`). The **final character** is a stored 1-char
|
|
55
|
+
integrity hash; strip and keep it.
|
|
56
|
+
2. base64-decode the rest. The **first byte** is the key-start seed; the remainder is the
|
|
57
|
+
payload.
|
|
58
|
+
3. Regenerate a keystream from the seed — `for (i = seed, j = 0; j < len; i += i, j++)
|
|
59
|
+
key .= chr(i % 255)` — and XOR it byte-for-byte against the payload to recover the
|
|
60
|
+
plaintext.
|
|
61
|
+
4. Integrity check: recompute `substr(base64_encode(hash_hmac('SHA256', payload, key,
|
|
62
|
+
true)), 0, 1)` and compare to the stored hash char. Mismatch → return `''` (→ `400`).
|
|
63
|
+
|
|
64
|
+
Because it is home-rolled and uses only a single hash character for integrity, treat it
|
|
65
|
+
strictly as link obfuscation. Do not use it to protect anything sensitive.
|
|
66
|
+
|
|
67
|
+
## Change history
|
|
68
|
+
|
|
69
|
+
- 2026-06-18 (jcardinal) — Initial documentation of the encrypted-link handler during
|
|
70
|
+
Forwarder onboarding into the knowledge base.
|
package/package.json
CHANGED