toga-ai 1.0.359 → 1.0.360
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.
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
| [BDR Web Funnel — Full Implementation Plan](features/bdr-web-funnel-plan.md) | > **Execution status (2026-07-16):** PLAN **Phases 0–5 are BUILT and QA'd**, Phase 6 > was **mostly already built** (the `info` contract was implemented at comm | bdr/PLAN.md, bdr/mockup/app.jsx, bdr/mockup/screens.jsx, bdr/mockup/components.jsx |
|
|
7
7
|
| [Call Orchestration — PHP Worker ↔ Vapi (the integration seam)](features/call-orchestration.md) | The **PHP worker** is the orchestrator; **Vapi** is the actor. | ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-inbound-callback-assistant-request.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md, ai-bdr/scripts/update_structured_output.py |
|
|
8
8
|
| [Vapi Integration — Assistants, Tools, Structured Output](features/vapi-integration.md) | Everything inside Vapi: the three assistants, the three shared tools, the shared structured-output schema, the Liquid-templated system prompt, and the Python sc | ai-bdr/vapi/templates/assistant.template.json, ai-bdr/vapi/templates/system-prompt.template.md, ai-bdr/prompts/archive/prompt-may-15.txt, ai-bdr/prompts/campaigns/healthcare-2026-03-18.md, ai-bdr/prompts/campaigns/healthcare-v2-2026-03-25.md, ai-bdr/prompts/templates/campaign-template.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py, ai-bdr/scripts/update_structured_output.py, ai-bdr/scripts/update_call_summary_context.py, ai-bdr/scripts/fix_booking_guardrails.py, ai-bdr/scripts/get_assistant.py, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md |
|
|
9
|
-
| [Web Funnel — Built Next.js App (structure, stack, how to run)](features/web-funnel-app.md) | The **built** state of the BDR web funnel — the Next.js UI / lead-capture front door of the existing AI-BDR product (see `../architecture.md`). | bdr/PLAN.md, bdr/.gitignore, bdr/eslint.config.mjs, bdr/amplify.yml, bdr/src/app, bdr/src/app/layout.tsx, bdr/src/app/page.tsx, bdr/src/app/studio.css, bdr/src/lib/resolveCampaignId.ts, bdr/src/lib/scheduleSlots.ts, bdr/src/lib/formatPhone.ts, bdr/src/lib/analytics.ts, bdr/src/lib/rateLimit.ts, bdr/src/proxy.ts, bdr/.env.example, bdr/src/components/GoogleAnalytics.tsx, bdr/src/components/RichText.tsx, bdr/src/content, bdr/src/components, bdr/src/flow, bdr/src/server, bdr/src/app/api, bdr/test, bdr/public, bdr/mockup/styles.css |
|
|
9
|
+
| [Web Funnel — Built Next.js App (structure, stack, how to run)](features/web-funnel-app.md) | The **built** state of the BDR web funnel — the Next.js UI / lead-capture front door of the existing AI-BDR product (see `../architecture.md`). | bdr/PLAN.md, bdr/.gitignore, bdr/eslint.config.mjs, bdr/amplify.yml, bdr/src/app, bdr/src/app/layout.tsx, bdr/src/app/page.tsx, bdr/src/app/studio.css, bdr/src/lib/resolveCampaignId.ts, bdr/src/lib/scheduleSlots.ts, bdr/src/lib/formatPhone.ts, bdr/src/lib/analytics.ts, bdr/src/lib/rateLimit.ts, bdr/src/lib/clientIp.ts, bdr/src/proxy.ts, bdr/.env.example, bdr/src/components/GoogleAnalytics.tsx, bdr/src/components/RichText.tsx, bdr/src/content, bdr/src/components, bdr/src/flow, bdr/src/server, bdr/src/app/api, bdr/test, bdr/public, bdr/mockup/styles.css |
|
|
10
10
|
| [Web Funnel — Next.js UI + Config-Driven Campaign Content](features/web-funnel-content-model.md) | > **Status note (RESOLVED 2026-07-16):** the content **contract** below (schema, > provider seam, adapters) is BUILT and QA'd (PLAN Phases 0–5). | bdr/src/content/schema.ts, bdr/src/content/provider.ts, bdr/src/content/useCampaign.ts, bdr/src/content/default.ts, bdr/src/lib/resolveCampaignId.ts, bdr/src/server/leadSink.ts, bdr/src/server/callbackService.ts, bdr/src/app/api/call-now/route.ts, bdr/src/app/api/call-later/route.ts, bdr/src/app/api/contact/route.ts |
|
|
11
11
|
| [New-Campaign Onboarding (6-phase runbook)](workflows/new-campaign-onboarding.md) | The end-to-end procedure for launching a new outbound BDR campaign (industry, offer, target persona, geography). | ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/calcom-new-campaign-event-guide.md, ai-bdr/prompts/templates/campaign-template.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py |
|
|
@@ -22,6 +22,7 @@ files:
|
|
|
22
22
|
- bdr/src/lib/formatPhone.ts
|
|
23
23
|
- bdr/src/lib/analytics.ts
|
|
24
24
|
- bdr/src/lib/rateLimit.ts
|
|
25
|
+
- bdr/src/lib/clientIp.ts
|
|
25
26
|
- bdr/src/proxy.ts
|
|
26
27
|
- bdr/.env.example
|
|
27
28
|
- bdr/src/components/GoogleAnalytics.tsx
|
|
@@ -56,13 +57,17 @@ fully implemented at commit `34a3910`: `page.tsx primeLead` reads `?hsContactId=
|
|
|
56
57
|
campaign attribution → only `togaUuid` crosses to the client). The 2026-07-16 session
|
|
57
58
|
added the deltas below (phone prefill + live input mask, `submitOk` on Success, GA4
|
|
58
59
|
wiring) and resolved §9.10 (content mechanism — see `web-funnel-content-model.md`) and
|
|
59
|
-
§9.11 (consent links). A later 2026-07-16 session added an **
|
|
60
|
-
mitigation** for the `?hsContactId=` enumeration risk (`src/proxy.ts` + `src/lib/rateLimit.ts`
|
|
60
|
+
§9.11 (consent links). A later 2026-07-16 session added an **app-layer rate-limit
|
|
61
|
+
mitigation** for the `?hsContactId=` enumeration risk (`src/proxy.ts` + `src/lib/rateLimit.ts`
|
|
62
|
+
+ `src/lib/clientIp.ts`),
|
|
61
63
|
documented the **fully server-side request-a-call seam**, the **outbound dialer eligibility
|
|
62
64
|
rules** (see `../../worker2/features/vapi-webhook-handler.md`), the **phone-less-contact
|
|
63
65
|
dialer bug**, and the **local dev env wiring**. Developer QA verdict: the UI is "very close,
|
|
64
|
-
minimal adjustments to be near exact" vs. the mockup.
|
|
65
|
-
|
|
66
|
+
minimal adjustments to be near exact" vs. the mockup. The rate-limit mitigation then passed an
|
|
67
|
+
**independent `cso` security review — final verdict SHIP after two rounds** — and was HARDENED
|
|
68
|
+
to its ship state (trusted-IP position for X-Forwarded-For; O(1) LRU-capped limiter map; the
|
|
69
|
+
`?hsCampaignId=` page-prime param now `isTogaUuid`-validated). All checks green: tsc, eslint,
|
|
70
|
+
**104/104 Vitest**, `next build` (build output labels the proxy `ƒ Proxy (Middleware)`).
|
|
66
71
|
|
|
67
72
|
- **Repo:** `agilantsolutions/BDR` on GitHub, working branch `BDR-Development`. The
|
|
68
73
|
machine-local `.claude/` harness is **gitignored** — committing it would snapshot
|
|
@@ -83,15 +88,19 @@ minimal adjustments to be near exact" vs. the mockup. All checks green: tsc, esl
|
|
|
83
88
|
- **ESLint flat config** carries a **no-em-dash rule** on shipped strings
|
|
84
89
|
(`Literal` / `TemplateElement` / `JSXText` selectors) — enforces the mockup copy
|
|
85
90
|
rule on in-repo strings (`eslint.config.mjs`).
|
|
86
|
-
- **Vitest** test suite: **
|
|
87
|
-
phone format/prefill/mask, live-anchor consent links, `submitOk` race;
|
|
88
|
-
|
|
89
|
-
+ build + a live SSR `curl` before
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
the
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
- **Vitest** test suite: **104 tests, all green** (13 added in the first 2026-07-16 session:
|
|
92
|
+
phone format/prefill/mask, live-anchor consent links, `submitOk` race; the later 2026-07-16
|
|
93
|
+
session added the rate-limiter + trusted-IP + LRU-eviction + `hsCampaignId` validation tests
|
|
94
|
+
to reach 104). Every phase was verified with `tsc` + lint + build + a live SSR `curl` before
|
|
95
|
+
commit.
|
|
96
|
+
- **Edge proxy (Next 16 `proxy.ts`) — 3-file rate-limit mitigation.** `src/proxy.ts` is the
|
|
97
|
+
Next 16 request proxy (the successor to the deprecated `middleware.ts` convention — see the
|
|
98
|
+
Next 16 gotcha) carrying the rate-limit mitigation. It composes two libs: `src/lib/rateLimit.ts`,
|
|
99
|
+
a pure clock-injected in-memory token-bucket limiter with an O(1) LRU-capped map (no
|
|
100
|
+
`server-only`, so it runs in the edge proxy and is unit-testable in Node); and
|
|
101
|
+
`src/lib/clientIp.ts`, the pure client-IP resolution logic (no `next` import, so it is
|
|
102
|
+
independently testable) that parses `X-Forwarded-For` from a **trusted** position. See the two
|
|
103
|
+
rate-limiter security gotchas below.
|
|
95
104
|
- **Deploy target:** AWS Amplify (SSR), `amplify.yml` present (Phase 8 not done).
|
|
96
105
|
|
|
97
106
|
## Directory map (what got built)
|
|
@@ -115,13 +124,14 @@ minimal adjustments to be near exact" vs. the mockup. All checks green: tsc, esl
|
|
|
115
124
|
`web-funnel-content-model.md`).
|
|
116
125
|
- `src/app/api/*` — route handlers `call-now`, `call-later`, `contact`. **These do NOT
|
|
117
126
|
place a call themselves** — see "Request-a-call is fully server-side" below.
|
|
118
|
-
- `src/proxy.ts` — Next 16 edge proxy (rate limiting; see
|
|
119
|
-
the token-bucket limiter it uses.
|
|
127
|
+
- `src/proxy.ts` — Next 16 edge proxy (rate limiting; see gotchas). `src/lib/rateLimit.ts` —
|
|
128
|
+
the LRU-capped token-bucket limiter it uses. `src/lib/clientIp.ts` — pure client-IP
|
|
129
|
+
resolution (trusted `X-Forwarded-For` position; no `next` import).
|
|
120
130
|
- `public/*` — assets pruned to the **default campaign only**, **plus AutoBuild's
|
|
121
131
|
FLOURISH characters** (Owl 1 `owl-02` + Robot 1 `robot-02`, art + talk videos) which
|
|
122
132
|
the assembly cycle needs even though they are not the campaign agent. See the
|
|
123
133
|
asset-pruning gotcha below.
|
|
124
|
-
- `test/*` — the
|
|
134
|
+
- `test/*` — the Vitest suite (104 tests).
|
|
125
135
|
|
|
126
136
|
## How the built app behaves (key runtime facts)
|
|
127
137
|
|
|
@@ -248,6 +258,16 @@ PRODUCTION credential set (`TOGA_API_BASE_URL = https://api.togahub.com/v2`).
|
|
|
248
258
|
- **Remaining work is decision-gated, not code:** leadership's first-campaign copy;
|
|
249
259
|
Phase 8 Amplify SSR (incl. a **per-IP WAF rate rule** — see the enumeration gotcha) +
|
|
250
260
|
`togatech` integration. GA4 (Phase 7) is now wired; §9.10 and §9.11 are resolved.
|
|
261
|
+
- **Non-gating security residuals (recorded in `BDR/PLAN.md` §10 / Phase 8 checklist — do not
|
|
262
|
+
re-litigate; the `cso` review flagged these as follow-ups, not ship-blockers):** (a) **lock
|
|
263
|
+
the Amplify origin** so nothing reaches it without transiting CloudFront — any
|
|
264
|
+
direct-to-origin path lets an attacker own the whole XFF chain, the standing caveat behind
|
|
265
|
+
every header-based IP-trust decision above; (b) confirm the distribution **injects + strips**
|
|
266
|
+
`CloudFront-Viewer-Address` before setting `RATE_LIMIT_TRUST_CF_VIEWER_HEADER=true`;
|
|
267
|
+
(c) `readJsonObject` buffers the body **before** the size check and measures UTF-16 length,
|
|
268
|
+
not bytes (bounded in practice by the Lambda payload cap + the call limiter); (d) the proxy
|
|
269
|
+
matcher is an **exact-path allowlist** — re-verify no un-normalized path variant reaches a
|
|
270
|
+
handler off-matcher.
|
|
251
271
|
|
|
252
272
|
## Gotchas
|
|
253
273
|
|
|
@@ -263,21 +283,50 @@ PRODUCTION credential set (`TOGA_API_BASE_URL = https://api.togahub.com/v2`).
|
|
|
263
283
|
server-rendered prefill, (b) scrape the **full** contact record via `GET /api/contact`
|
|
264
284
|
(see next gotcha), and (c) drive the page-prime Toga upsert, polluting contacts/attribution.
|
|
265
285
|
**Inherited from `info`'s CRM-link URL contract — cannot be changed unilaterally.**
|
|
266
|
-
**
|
|
267
|
-
rate-limit per client
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
286
|
+
**App-layer mitigation (BUILT + security-reviewed SHIP, 2026-07-16):** `src/proxy.ts` +
|
|
287
|
+
`src/lib/rateLimit.ts` + `src/lib/clientIp.ts` rate-limit per client (keyed off a **trusted**
|
|
288
|
+
client IP — see the two rate-limiter gotchas below). Entry pages carrying `?hsContactId=`
|
|
289
|
+
**soft-degrade** over the limit (strip `hsContactId`, `NextResponse.rewrite` to render
|
|
290
|
+
anonymously — a real shared-NAT user keeps the funnel minus prefill; a scraper gets no PII and
|
|
291
|
+
triggers no Toga upsert; `hsCampaignId` is preserved). `/api/contact`, `/api/call-now`,
|
|
292
|
+
`/api/call-later` return a `429 {isSuccess:false,…}` envelope with `Retry-After` over the
|
|
293
|
+
limit. **This is defense-in-depth ONLY** — the limiter is per-runtime-instance in-memory
|
|
294
|
+
(Amplify SSR may run several instances), so it is not authoritative. The authoritative
|
|
295
|
+
controls — a **per-IP edge WAF rate rule** (Phase 8) and **opaque/signed CRM link tokens**
|
|
296
|
+
(raise with the CRM owner) — remain deferred. Tracked in `BDR/PLAN.md` §10/§11. An independent
|
|
297
|
+
`cso` review of this mitigation returned a **final verdict of SHIP after two rounds**; the
|
|
298
|
+
hardening from that review (trusted-IP position, LRU-capped map, `hsCampaignId` validation) is
|
|
299
|
+
documented in the gotchas below.
|
|
277
300
|
- **HIGH (security) — `GET /api/contact?hsContactId=` returns the FULL contact record.** Name,
|
|
278
301
|
email, phone, company, and job title by id — a **more direct** PII scrape vector than the SSR
|
|
279
302
|
phone prefill. Now covered by the same rate limiter; **any change to
|
|
280
303
|
`src/app/api/contact/route.ts` must keep it rate-limited.**
|
|
304
|
+
- **HIGH (security) — rate limiting behind CloudFront MUST key off a TRUSTED IP position, never
|
|
305
|
+
the leftmost `X-Forwarded-For`.** The leftmost XFF entry is client-prependable, so keying the
|
|
306
|
+
limiter off it is trivially bypassable (a scraper spoofs a fresh left-most IP per request and
|
|
307
|
+
never hits the limit). Correct approach (in `src/lib/clientIp.ts`): parse XFF
|
|
308
|
+
**right-to-left** and take index `parts.length - 1 - RATE_LIMIT_TRUSTED_PROXY_HOPS` (default
|
|
309
|
+
`0` ⇒ the **rightmost**, CloudFront-appended, viewer IP). `x-real-ip` is **NOT** trusted.
|
|
310
|
+
`CloudFront-Viewer-Address` is trusted **only** when env
|
|
311
|
+
`RATE_LIMIT_TRUST_CF_VIEWER_HEADER=true` (default **OFF**) — because an unverified
|
|
312
|
+
distribution that forwards a client-supplied copy of that header reintroduces the bypass; ops
|
|
313
|
+
enables it only after confirming CloudFront both injects **and** strips it. `normalizeIp()`
|
|
314
|
+
handles port strip, `[ipv6]:port`, and `::ffff:` IPv4-mapped forms so one client = one key.
|
|
315
|
+
This whole approach is a **reusable pattern for any Next app behind CloudFront/Amplify** — do
|
|
316
|
+
not copy a naive leftmost-XFF limiter into another repo.
|
|
317
|
+
- **HIGH (security) — a naive in-memory limiter sweep is itself a DoS.** A sweep that only
|
|
318
|
+
reclaims fully-refilled buckets leaves an **unbounded map** plus an O(n) sweep per insert;
|
|
319
|
+
under a unique-key flood (e.g. rotated IPv6 sources) that is a self-inflicted DoS. Fix (in
|
|
320
|
+
`src/lib/rateLimit.ts`): a **HARD ceiling** `MAX_TRACKED_KEYS = 10_000` enforced by **O(1) LRU
|
|
321
|
+
eviction** (Map insertion order; every check re-inserts the key to move it to MRU), **plus**
|
|
322
|
+
the O(n) reclaim throttled to at most **once per window**. This guarantees the map never
|
|
323
|
+
exceeds the cap and per-insert work is O(1) amortized. Do not "simplify" the LRU re-insert or
|
|
324
|
+
the throttle away — both are load-bearing against the flood case.
|
|
325
|
+
- **`?hsCampaignId=` page-prime param must be UUID-validated before it crosses to the Toga
|
|
326
|
+
upsert.** It flows into the upsert as `togaCampaignUuid`; it previously had only a
|
|
327
|
+
`typeof === 'string'` guard. `page.tsx` now `isTogaUuid`-validates it before use, dropping a
|
|
328
|
+
malformed value to `undefined`. Never pass an unvalidated inbound query param into the Toga
|
|
329
|
+
upsert.
|
|
281
330
|
- **Next 16 — `middleware.ts` is DEPRECATED; use `proxy.ts`.** Next 16.2.3 warns "The
|
|
282
331
|
middleware file convention is deprecated. Please use proxy instead"
|
|
283
332
|
(nextjs.org/docs/messages/middleware-to-proxy). Correct convention: `src/proxy.ts` exporting
|
|
@@ -322,6 +371,23 @@ PRODUCTION credential set (`TOGA_API_BASE_URL = https://api.togahub.com/v2`).
|
|
|
322
371
|
harness; pull it with `npx toga-ai`.
|
|
323
372
|
|
|
324
373
|
## Change history
|
|
374
|
+
- 2026-07-16 — Rate-limit mitigation HARDENED to ship state after an independent `cso` security
|
|
375
|
+
review returned **SHIP after two rounds** (supersedes the mid-state recorded in the entry
|
|
376
|
+
below: test count is **104**, not 89, and the review is complete, not "still running"). Added
|
|
377
|
+
`src/lib/clientIp.ts` (pure, `next`-free, testable) → the mitigation is now **3 source files**
|
|
378
|
+
(`proxy.ts` + `rateLimit.ts` + `clientIp.ts`). Security hardening: (1) rate limiting behind
|
|
379
|
+
CloudFront keys off a **trusted** XFF position (parse right-to-left, index
|
|
380
|
+
`len-1-RATE_LIMIT_TRUSTED_PROXY_HOPS`, default rightmost/viewer IP; `x-real-ip` untrusted;
|
|
381
|
+
`CloudFront-Viewer-Address` trusted only when `RATE_LIMIT_TRUST_CF_VIEWER_HEADER=true`,
|
|
382
|
+
default OFF) — never the client-prependable leftmost IP; `normalizeIp` collapses port/
|
|
383
|
+
`[ipv6]:port`/`::ffff:` so one client = one key; (2) the limiter map is bounded by a HARD
|
|
384
|
+
`MAX_TRACKED_KEYS=10_000` via O(1) LRU eviction + reclaim throttled to once/window, closing a
|
|
385
|
+
unique-key-flood DoS in the naive sweep; (3) `?hsCampaignId=` is now `isTogaUuid`-validated in
|
|
386
|
+
`page.tsx` before it crosses to the Toga upsert (was a bare `typeof`-string guard). Recorded
|
|
387
|
+
the non-gating `cso` follow-ups (lock Amplify origin to CloudFront; verify CF-Viewer-Address
|
|
388
|
+
inject+strip before trusting it; `readJsonObject` buffers-then-measures UTF-16 not bytes;
|
|
389
|
+
proxy matcher exact-path allowlist). All green: tsc, eslint, **104/104** Vitest, `next build`.
|
|
390
|
+
(tcox)
|
|
325
391
|
- 2026-07-16 — Later same-day session. BUILT an interim app-layer rate-limit mitigation for the
|
|
326
392
|
`?hsContactId=` enumeration risk: `src/lib/rateLimit.ts` (pure clock-injected in-memory
|
|
327
393
|
token-bucket, idle-key eviction, no `server-only`) + `src/proxy.ts` (Next 16 proxy). Policy:
|
package/package.json
CHANGED