Skip to content

feat(tendlc): 10DLC Registration Center foundation (PR 1 of 5) - #27

Open
kshahbw wants to merge 22 commits into
mainfrom
feat/reg-center-10dlc-direct
Open

feat(tendlc): 10DLC Registration Center foundation (PR 1 of 5)#27
kshahbw wants to merge 22 commits into
mainfrom
feat/reg-center-10dlc-direct

Conversation

@kshahbw

@kshahbw kshahbw commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

BLUF

Foundation for 10DLC Registration Center support — PR 1 of 5. No write operations and no user-visible flags: this lands the shared plumbing that PRs 2–5 need, plus one new command (band tendlc status).

Design: docs/specs/2026-08-12-tendlc-direct-registration-center-design.md · Plan: docs/plans/2026-08-13-tendlc-pr1-foundation.md
Both live under a gitignored docs/, so they are not in this diff.

What's in it

New packages

  • internal/tendlc — typed service: ListBrands, GetBrand, ListCampaigns, GetCampaign
  • internal/customerprofile — typed service: List, Get

Shared plumbing added to internal/api

  • Envelope/Page parsing with ParseEnvelope, List(), Object(), Truncated()
  • deepObject query encoding (Filter, EncodeQuery) — the API wants status[eq]=REGISTERED; the plain form is accepted and then silently ignored, so the tests assert exact query strings
  • APIError.Header + RetryAfter() so 429s can eventually be honored

Existing code, additively extended

  • cmdutil.FlagError makes ExitFlagError (6) reachable — it was defined but nothing mapped to it, so flag errors exited 1
  • PollConfig.Context makes --wait loops cancellable between attempts. All 12 existing Poll callers are untouched
  • band auth status gains a tendlc tri-state and a customer_profiles boolean. campaign_management keeps its exact existing meaning

New command

  • band tendlc status — one call, reports Registration Center access

Deliberately not here

Option structs, --all, filter flags, read-modify-write, --confirm, receipts, and every write method. They land with their first consumers in PRs 2–5; scaffolding without callers would inflate this PR without making it useful.

Notable decisions

Responses are map[string]any, not typed structs. Production returns fields the published spec omits — attMessageClass, subId, universalEin, referenceId — and encoding/json silently drops unknown fields into structs. Requests will be typed; responses stay lossless.

band tendlc status reports access only, never account mode. Direct-vs-import is a property of the customer's Bandwidth setup, not a runtime fact. Live probing killed the obvious inference: brand.imported is true on both a direct and an import account. mode is always present and always unknown, because omitting it invites callers to assume a default.

A 403 exits 0. The probe answered its question. Only a probe that fails to answer exits non-zero — and it still emits stable JSON on stdout first, including on bare transport failures.

Verification

go build ./..., go test ./..., go vet ./..., and the doc-contract test all pass. knownDrift untouched.

Live-verified against a direct account (9901287) and an import account (9901303):

$ band tendlc status --account-id 9901287 --plain
{"access":"available","mode":{"reason":"not_discoverable","status":"unknown"},"reason":"probe_succeeded"}

AGENTS.md's documented example matches that output byte for byte.

Known gaps, deliberately deferred

Test coverage:

  • RetryAfter's nil-Header branch is untested (the test uses http.Header{}, not nil)
  • client.go PostXMLReturnLocation header capture has no regression test; only doRaw is covered
  • the zero-argument request never asserts RawQuery == ""

Belongs to the flag layer (PRs 3–5):

  • an empty Filter.Value is skipped, so it cannot express "filter on empty" vs "unset"
  • negative limit/offset silently vanish instead of erroring

Cosmetic:

  • tendlcCapability's doc comment doesn't enumerate the future probe's full reason vocabulary the way sipCapability's does
  • the new TenDLC field shifts Error's JSON key order — irrelevant to parsers

Found along the way, out of scope

Two real bugs in internal/sip, both pre-existing and left alone rather than smuggled into this PR:

  • internal/sip/service.go:94 builds an api.APIError with a *RawResponse in scope and discards its headers, so RetryAfter() is always false for SIP errors
  • cmd/sip/status.go has the same transport-error-produces-empty-stdout gap that this PR fixes in tendlc

Related

A running log of spec-vs-production divergences found while building this is at docs/specs/2026-08-13-tendlc-spec-vs-prod-mismatches.md, to be filed with the tendlc API team once the stack lands.

kshahbw added 21 commits August 13, 2026 14:41
…ssertion

Go 1.23+ made timer channels unbuffered; Stop() cancels any in-flight
send, so there is never anything left in the channel to drain after
Stop() returns. The old drain-after-Stop idiom was dead code on this
toolchain and would deadlock if it were ever reached. Create a fresh
timer per iteration instead and only Stop() it on the cancellation
path as a courtesy; unreferenced timers are garbage collected.

Also fix TestPollRespectsContextCancellation: the calls == 0 assertion
could never fail since Check always runs at least once before context
cancellation can be observed. Assert calls > 1 instead, which actually
exercises loop iteration before interruption.
…ract

List() no longer treats null/absent data as an empty result: production
sends data:[] for genuinely empty lists, so nil is a malformed response,
not zero results, and must error like any other shape mismatch.

Truncated's parameter is renamed to returnedSoFar and documented as the
cumulative count across all pages walked, not the current page's length,
to prevent downstream callers from misreading it on the last page of a
paginated walk.

Shape-mismatch errors now describe the JSON shape (array/object/null)
instead of printing a Go type name.
Transport errors (connection refused, DNS, TLS, timeout) never unwrap to
*api.APIError, so the status command fell through to the error path
without emitting anything on stdout. Every probeErr path now defaults to
the unknown/probe_failed result before checking for a more specific
outcome, so callers parsing stdout always get stable JSON regardless of
how the probe failed.

Adds command-level tests exercising RunE against stub servers, covering
the success path, both 403 outcomes, a 5xx, and the transport-failure
regression.
@kshahbw
kshahbw requested review from a team as code owners August 14, 2026 13:54
@bwappsec

bwappsec commented Aug 14, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

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