Skip to content

Wire talk edge to CoTrackPro hub + SSM→Vercel config sync#47

Merged
dougdevitre merged 3 commits into
mainfrom
claude/admiring-tesla-EsP34
Jun 7, 2026
Merged

Wire talk edge to CoTrackPro hub + SSM→Vercel config sync#47
dougdevitre merged 3 commits into
mainfrom
claude/admiring-tesla-EsP34

Conversation

@dougdevitre

Copy link
Copy Markdown
Owner

Summary

Implements the talk-side half of the hub↔talk seam (so an inbound caller is recognized as a signed-in user, and an unlinked caller is texted a one-time sign-in link), and makes AWS SSM the single source of truth mirrored into Vercel env at deploy time.

The hub owns identity / OTP / token minting / tier (PR #182, cotrackpro-antigravity). This is the transport + inbound voice prompt only.

1. Talk → Hub (src/services/hub.ts)

resolvePhone() and sendAuthLink() present the shared bearer, with AbortController timeout, phone-masked logs, and fail-open discriminated results (network/timeout/misconfig → error, never throws).

2. Hub → Talk: POST /api/sms/send (src/core/sms.ts + Vercel + Fastify adapters)

Constant-time bearer verify, E.164/country validation, KV rate limit, idempotent on dedupeKey, sends body verbatim. A2P: sends through TWILIO_MESSAGING_SERVICE_SID, not a bare from-number; production fails closed (500 sms_misconfigured) if the SID is unset. body never logged; phones masked.

3. Inbound voice loop

resolveInboundCaller() in core/twiml.ts; both /call/incoming handlers resolve the caller and, when unlinked, trigger send-auth-link. subject + authNotice flow through as <Stream> params; callHandler stores subject on the session and speaks the sign-in prompt after the greeting. Fails open to anonymous (crisis resources + anonymous help never gated).

4. Config sync — scripts/sync-ssm-to-vercel.sh

Positional STAGE ∈ {prod, test} → Vercel production/preview. Mirrors exactly the 7 hub-registry params (talk/outbound_api_key, twilio/{account_sid,auth_token,messaging_service_sid,phone_number}, elevenlabs/{api_key,voice_id_doug}), read with --with-decryption. Fail-closed two-phase (validate all, then write) so a missing/empty required param exits non-zero and writes nothing. Idempotent rm-then-add; values piped via stdin, never echoed. CI: .github/workflows/vercel-env-sync.yml.

Config notes

  • Shared bearer read from TALK_OUTBOUND_API_KEY (legacy OUTBOUND_API_KEY fallback).
  • New env: HUB_BASE_URL, HUB_TIMEOUT_MS, TWILIO_MESSAGING_SERVICE_SID, ELEVENLABS_VOICE_ID_DOUG, SMS_RATE_LIMIT_*.
  • The sync script handles only the 7 registry secrets; non-registry Vercel env (ANTHROPIC_API_KEY, COTRACKPRO_MCP_URL, INBOUND_PHONE_VOICE_MAP) is set separately — see docs/GO_LIVE-inbound-voice.md.

Deferred (called out, not silently skipped)

  • Hub's one-shot /api/call/outbound "play line in voiceId" variant (existing /call/outbound already serves authenticated, idempotent outbound to the interactive loop).
  • Per-call tier-gating of paid tools (the subject hook point exists).
  • STOP/HELP/START + suppression/quiet-hours compliance gating.

Tests

Adds tests/hub.test.ts, tests/sms.test.ts, tests/inboundCallerResolve.test.ts (auth, idempotency, fail-open, status mapping, A2P routing, TwiML params). Full suite green (370), typecheck clean. Sync script verified with fake aws/vercel (invalid stage→2, missing token→2, missing/empty param→1 with zero writes, happy path writes 7, no secret in argv, idempotent re-run).

Docs: docs/hub-talk-seam.md, docs/GO_LIVE-inbound-voice.md, README, .env.example.

https://claude.ai/code/session_01GERbuNjdJ6WhzPBkpjZMWG


Generated by Claude Code

claude added 3 commits June 7, 2026 00:47
Implements the talk-side half of the hub↔talk seam so an inbound caller
is recognized as a signed-in user, and an unlinked caller is texted a
one-time sign-in link. The hub owns identity/OTP/token minting/tier; this
is the transport + inbound voice prompt only.

Talk → Hub (src/services/hub.ts):
- resolvePhone() and sendAuthLink() present the shared bearer
  (OUTBOUND_API_KEY, SSM .../talk/outbound_api_key), with AbortController
  timeout, phone-masked logs, and fail-open discriminated results.

Hub → Talk:
- POST /api/sms/send (src/core/sms.ts + Vercel + Fastify adapters):
  constant-time bearer verify, E.164/country validation, KV rate limit,
  idempotent on dedupeKey, sends body verbatim via Twilio. Body never
  logged; phone masked.

Inbound voice loop:
- resolveInboundCaller() in core/twiml.ts; both /call/incoming handlers
  resolve the caller and, when unlinked, trigger send-auth-link. subject +
  authNotice flow through as <Stream> parameters; callHandler stores
  subject on the session and speaks the sign-in prompt. Fails open to
  anonymous (crisis resources + anonymous help never gated).

Config: HUB_BASE_URL, HUB_TIMEOUT_MS, SMS_RATE_LIMIT_* added to env +
.env.example. Docs in docs/hub-talk-seam.md.

The hub's one-shot /api/call/outbound "play line in voiceId" variant is
documented as deferred (existing /call/outbound already serves
authenticated, idempotent outbound to the interactive voice loop).

Adds tests/hub.test.ts, tests/sms.test.ts, tests/inboundCallerResolve.test.ts
(29 tests). Full suite green (369), typecheck clean.
Make AWS SSM /cotrackpro/<stage>/ the canonical config source and mirror
it into Vercel env at deploy time (the talk app can't read SSM at
runtime).

scripts/sync-ssm-to-vercel.sh:
- Add --stage: derives the SSM namespace (/cotrackpro/<stage>) and the
  Vercel environment (prod → production, else → preview).
- Extend the mapping: talk/outbound_api_key → TALK_OUTBOUND_API_KEY,
  twilio/messaging_service_sid → TWILIO_MESSAGING_SERVICE_SID,
  elevenlabs/voice_id_doug → ELEVENLABS_VOICE_ID_DOUG (existing
  app-required values kept).
- CI-friendly: honor VERCEL_TOKEN (+ VERCEL_ORG_ID/VERCEL_PROJECT_ID).

.github/workflows/vercel-env-sync.yml: new workflow that runs the sync
in CI with an AWS IAM credential (ssm:GetParameter(sByPath) + kms:Decrypt)
and VERCEL_TOKEN, mirroring fly-deploy.yml's environment scoping.

A2P enforcement: outbound SMS now sends THROUGH TWILIO_MESSAGING_SERVICE_SID
(the A2P-registered service), not a bare from-number, so every send is
attributed to the approved brand/campaign. src/core/sms.ts fails closed
(500 sms_misconfigured) in production when the service SID is unset; non-
prod falls back to the from-number for local testing.

env.ts: add twilioMessagingServiceSid + elevenLabsVoiceIdDoug; read the
shared bearer from TALK_OUTBOUND_API_KEY with an OUTBOUND_API_KEY fallback
so existing deploys keep working through the rename.

.env.example + docs/hub-talk-seam.md updated. Adds an A2P-routing unit
test. Full suite green (370), typecheck clean.
Replace the broad multi-target script with the precise, hub-registry
contract: AWS SSM /cotrackpro/<stage>/ is the single source of truth,
mirrored into this app's Vercel env (Vercel can't read SSM at runtime).
Must stay in lockstep with dougdevitre/cotrackpro-antigravity
docs/ops/ssm-parameters.md.

scripts/sync-ssm-to-vercel.sh:
- Positional STAGE ∈ {prod, test} (default prod); small case statement
  maps prod→production, test→preview.
- Exactly the 7 registry params (talk/outbound_api_key, twilio/{account_sid,
  auth_token,messaging_service_sid,phone_number}, elevenlabs/{api_key,
  voice_id_doug}) → their env var names, read with --with-decryption.
- FAIL CLOSED: two-phase (fetch+validate all, then write) so a single
  missing/empty REQUIRED param exits non-zero and writes NOTHING — never
  an empty TALK_OUTBOUND_API_KEY or TWILIO_MESSAGING_SERVICE_SID.
- Idempotent rm-then-add; value piped via stdin (never argv); secrets
  never echoed.
- VERCEL_TOKEN required; reads VERCEL_ORG_ID/PROJECT_ID + AWS_REGION from
  env. CLI preflight checks.

vercel-env-sync.yml: call as positional STAGE (prod/test choice).
README + GO_LIVE + hub-talk-seam docs: note shared secrets are owned in
SSM and mirrored by this script (never set by hand); INBOUND_PHONE_VOICE_MAP
and other non-registry config are synced separately.

Verified with fake aws/vercel on PATH: invalid stage→2, missing token→2,
missing/empty param→1 with zero writes, happy path writes all 7 (rm+add),
no secret in argv, re-run identical (idempotent). Suite green (370).
@vercel

vercel Bot commented Jun 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cotrackpro-talk Ready Ready Preview, Comment Jun 7, 2026 3:39am

Request Review

@dougdevitre dougdevitre merged commit dfb810e into main Jun 7, 2026
3 checks passed
dougdevitre pushed a commit that referenced this pull request Jun 7, 2026
Follow-up to #47, reconciled against the actual SSM registry (which uses
prod + dev namespaces; there is no "test").

- sync-ssm-to-vercel.sh: STAGE ∈ {prod, dev} (was {prod, test}); dev →
  Vercel preview. Updated usage/validation + the vercel-env-sync workflow
  stage choice + README/GO_LIVE references.
- scripts/show-a2p-status.ts (npm run show:a2p): read-only A2P 10DLC /
  TrustHub check. Reads twilio account/auth + messaging_service_sid,
  brand_sid, campaign_sid from the environment (pull from SSM
  /cotrackpro/<stage>/twilio/*) and reports the Messaging Service sender
  pool, the us_app_to_person campaign status, and the Brand registration
  status — exits non-zero if anything isn't APPROVED/VERIFIED/present.
  Never prints the auth token.

Typecheck clean (scripts included), suite green (370).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants