Wire talk edge to CoTrackPro hub + SSM→Vercel config sync#47
Merged
Conversation
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).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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()andsendAuthLink()present the shared bearer, withAbortControllertimeout, 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, sendsbodyverbatim. A2P: sends throughTWILIO_MESSAGING_SERVICE_SID, not a bare from-number; production fails closed (500 sms_misconfigured) if the SID is unset.bodynever logged; phones masked.3. Inbound voice loop
resolveInboundCaller()incore/twiml.ts; both/call/incominghandlers resolve the caller and, when unlinked, triggersend-auth-link.subject+authNoticeflow through as<Stream>params;callHandlerstoressubjecton 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.shPositional
STAGE ∈ {prod, test}→ Vercelproduction/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. Idempotentrm-then-add; values piped via stdin, never echoed. CI:.github/workflows/vercel-env-sync.yml.Config notes
TALK_OUTBOUND_API_KEY(legacyOUTBOUND_API_KEYfallback).HUB_BASE_URL,HUB_TIMEOUT_MS,TWILIO_MESSAGING_SERVICE_SID,ELEVENLABS_VOICE_ID_DOUG,SMS_RATE_LIMIT_*.ANTHROPIC_API_KEY,COTRACKPRO_MCP_URL,INBOUND_PHONE_VOICE_MAP) is set separately — seedocs/GO_LIVE-inbound-voice.md.Deferred (called out, not silently skipped)
/api/call/outbound"playlineinvoiceId" variant (existing/call/outboundalready serves authenticated, idempotent outbound to the interactive loop).subjecthook point exists).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 fakeaws/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