feat(node): full API + Python-SDK parity (84 methods, retries, signing, iterators)#5
Draft
makegov-hal[bot] wants to merge 18 commits into
Draft
feat(node): full API + Python-SDK parity (84 methods, retries, signing, iterators)#5makegov-hal[bot] wants to merge 18 commits into
makegov-hal[bot] wants to merge 18 commits into
Conversation
Bring `createWebhookSubscription` / `updateWebhookSubscription` and
`createWebhookEndpoint` / `updateWebhookEndpoint` up to API parity:
- Subscriptions now accept the canonical request shape directly
(`subscription_name`, `endpoint`, `subscription_type`, `event_type`,
`subject_type`, `subject_ids`, `query_type`, `filter_definition`,
`frequency`, `cron_expression`, ...). Legacy `{ subscriptionName,
payload }` still works.
- Endpoints now accept `name` (required by the API), in addition to
`callback_url` and `is_active`. Legacy `{ callbackUrl, isActive }`
still works; missing `name` defaults to the URL host so old call sites
don't need updating immediately.
- Add a dedicated `testWebhookEndpoint(endpointId)` method that mirrors
the API's `endpoint_id` request key. The pre-existing
`testWebhookDelivery({ endpointId? })` is kept as a legacy alias.
New types in `models/Webhooks.ts`:
`WebhookSubscriptionCreateInput`, `WebhookSubscriptionUpdateInput`,
`WebhookEndpointCreateInput`, `WebhookEndpointUpdateInput`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `createWebhookAlert(input)` and `deleteWebhookAlert(id)` for the
convenience `/api/webhooks/alerts/` API.
This endpoint creates filter-based webhook subscriptions with a
simpler input shape than the canonical subscriptions endpoint. Field
naming differs:
- `name` (here) vs `subscription_name` (canonical)
- `filters` (here) vs `filter_definition` (canonical)
- `query_type` is SINGULAR in both ("contract" not "contracts")
New types in `models/Webhooks.ts`: `WebhookAlertCreateInput`,
`WebhookAlert`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The README/docs have long claimed "configurable retries with exponential backoff" but the SDK never actually retried. Fix that. Implementation: - `HttpClient` accepts `retries` (default 3) and `retryBackoffMs` (default 250) via constructor options. - Retries on: - 5xx responses - 408 (Request Timeout) - 429 (Too Many Requests) — honors `Retry-After` - Network-level errors (DNS / connection refused / fetch network) - Local timeout (`AbortError`) - Does NOT retry on other 4xx (401/403/404/400/...). - Backoff: exponential, base `retryBackoffMs`, doubling per attempt, capped at 10s. `Retry-After` (delta-seconds or HTTP-date) overrides. - Existing tests pinned to `retries: 0` where they assert single-call semantics; new tests cover the retry/backoff/Retry-After paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The audit caught the README claiming `timeout` and `retries` constructor options that didn't exist. Make the docs correct in both directions: - `timeoutMs` (canonical, already supported) keeps working. - `timeout` is now accepted as a shorthand alias — both are in milliseconds. If both are supplied, `timeoutMs` wins. - `retries` and `retryBackoffMs` are accepted on `TangoClient` and forwarded to `HttpClient`. Defaults: 3 retries, 250ms initial backoff (doubles per attempt, capped at 10s). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`scripts/smoke-writes.ts` exercises the new webhook write methods end-to-end against a running Tango instance (defaults to http://localhost:8000) and prints PASS/FAIL per step. Steps: 1. createWebhookEndpoint 2. updateWebhookEndpoint (flips is_active=true) 3. createWebhookSubscription 4. updateWebhookSubscription 5. testWebhookEndpoint (Tango returns 502 when receiver unreachable — handled as expected) 6. createWebhookAlert (auto-skips with PASS if user already has >1 endpoint — the alerts endpoint requires exactly one; the SDK call itself is verified by inspecting the structured error from Tango) 7. deleteWebhookAlert (if created) 8. deleteWebhookSubscription 9. deleteWebhookEndpoint Run with: `npx tsx scripts/smoke-writes.ts`. Honors `TANGO_BASE_URL` and `TANGO_API_KEY` env vars. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…other resources Brings the Node SDK up to parity with the Tango read API. Adds: Lookups: listNaics/getNaics, listPsc/getPsc, listMasSins/getMasSin, listAssistanceListings/getAssistanceListing, listOrganizations/getOrganization, listOffices/getOffice, listDepartments (marked @deprecated — use listOrganizations({ level: 1 }) instead). Awards completeness: listOtas/getOta, listOtidvs/getOtidv, listOtidvAwards, listSubawards, listGsaElibraryContracts, listLcats (accepts { uei } or { idvKey }). Other: listProtests/getProtest, listItDashboard/getItDashboard, listMetrics (parameterized by ownerType: naics | psc | entity), plus resolve() and validate() POST endpoints. All option interfaces re-exported from src/index.ts for downstream typing. No model classes added — returns Record<string, unknown> / PaginatedResponse<Record<string, unknown>>, matching the loose shape contract already in place for vehicles/idvs. Webhook write methods and constructor/retry surface untouched — owned by feat/api-parity-writes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scripts/smoke-reads.ts hits every newly-added read method against a running local Tango (http://localhost:8000) and reports PASS/FAIL per method. Probes list endpoints first, then derives an id for the corresponding detail call — so it stays green as long as the local DB has at least one row per resource. Read-only; safe to run anytime. Run with: npx tsx scripts/smoke-reads.ts. Honors TANGO_BASE_URL and TANGO_API_KEY env overrides. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gnature/parseSignatureHeader)
Port `tango-python`'s `tango.webhooks.signing` to TypeScript so Node SDK
users can verify incoming webhook deliveries without rolling their own
HMAC code. Mirrors the Python algorithm byte-for-byte (lowercase hex
HMAC-SHA256, `sha256=<hex>` header format) and accepts the same legacy
bare-hex input.
`parseSignatureHeader` returns `{ algorithm, signature }` (instead of
the Python helper's bare string) so callers can introspect the
algorithm — the Node API gets a tiny ergonomic upgrade since we're
defining the shape fresh.
`verifySignature` uses Node's `timingSafeEqual` for constant-time
comparison and never throws on mismatch; returns false for missing,
empty, malformed, wrong-algorithm, or wrong-length headers.
Test vectors ported verbatim from
`tango-python/tests/test_webhooks_signing.py`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a generic `client.iterate(method, options)` plus typed convenience
wrappers (`iterateContracts`, `iterateEntities`, `iterateOpportunities`,
`iterateNotices`, `iterateGrants`, `iterateForecasts`, `iterateIdvs`,
`iterateVehicles`) so callers can drop the hand-rolled
`while (next) { ... }` loop and write:
for await (const contract of client.iterateContracts({ awarding_agency: "9700" })) {
...
}
The iterator parses `?page=` (offset) or `?cursor=` (cursor) out of the
API's `next` URL and re-calls the underlying list method with the same
caller options. Sequential by design — Tango's rate limits would crush
concurrent paginate, and serial matches user expectations for
`for await`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…vided Symmetric with the existing TANGO_API_KEY env-var fallback. Precedence: 1. explicit `options.baseUrl` 2. `process.env.TANGO_BASE_URL` 3. `DEFAULT_BASE_URL` Also tightens the iterate() helper's reflective method lookup to drop the `any` casts — same runtime behavior, no lint noise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verifies the three SDK parity additions end-to-end against the local
Tango instance:
1. generate/parse/verify signature round-trip (no network)
2. `iterateContracts` yields ~30 records and honors `break`
3. constructing a client with no explicit `baseUrl` while
`TANGO_BASE_URL` is set sends requests to the env-var host
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the 28 read methods from feat/api-parity-reads into the writes branch so a single branch carries reads + writes + retry + signing + iterator + env support. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…etrics, webhook alerts CRUD, misc
Brings TangoClient to full feature parity with tango-python.TangoClient.
New methods (19):
Sub-detail:
- getDepartment(code)
- getBusinessType(code)
Entity sub-resources (all take uei):
- listEntityContracts, listEntityIdvs, listEntityOtas,
listEntityOtidvs, listEntitySubawards, listEntityLcats
- getEntityMetrics(uei, months, periodGrouping)
IDV sub-resources:
- listIdvLcats(key, options)
Agency sub-resources:
- listAgencyAwardingContracts(code, options)
- listAgencyFundingContracts(code, options)
Typed metrics wrappers (sit alongside the generic listMetrics):
- getNaicsMetrics(code, months, periodGrouping)
- getPscMetrics(code, months, periodGrouping)
Webhook alerts CRUD parity (list/get/update — create+delete already shipped):
- listWebhookAlerts({ page, pageSize })
- getWebhookAlert(id)
- updateWebhookAlert(id, { name, frequency, cronExpression, isActive })
Misc:
- searchOpportunityAttachments({ q, topK, includeExtractedText })
- getVersion()
- listApiKeys()
All methods follow existing camelCase conventions, accept option objects
where the Python version uses kwargs, and map options to the server's
snake_case wire format internally (e.g. cronExpression → cron_expression).
New option interfaces are exported from index.ts:
EntitySubresourceOptions, EntitySubawardsOptions, EntityLcatsOptions,
AgencyContractsOptions, SearchOpportunityAttachmentsOptions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- tests/unit/client.parity.test.ts: 29 unit tests covering every new method (URL shape, query params, body translation, validation errors). - scripts/smoke-parity.ts: live PASS/FAIL harness that hits each new method against http://localhost:8000. Auto-discovers UEIs / IDV keys / agency codes / department codes so it works against any Tango env. Test counts: 80 → 109 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…MPREHENSIVE
The field exists on Contract responses but not on IDV — sending it as part
of the comprehensive shape caused 400s from /api/idvs/{key}/. Aligns with
the Python SDK's IDVS_COMPREHENSIVE preset, which never had it.
Also fixes recipient(...,cage_code) → recipient(...,cage) to match the
Python preset and the actual IDV recipient field name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comprehensive Unreleased entry covering everything landed on feat/api-parity: full Python parity (entity/agency sub-resources, typed metrics, alerts CRUD, misc utilities), webhook write methods, retry with exponential backoff, async iterator pagination, signing helpers, TANGO_BASE_URL env fallback, IDVS_COMPREHENSIVE fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR expands @makegov/tango-node to near-complete Tango API and tango-python parity by adding a large set of new TangoClient methods, plus SDK “quality-of-life” features (retries, async iterators, webhook signing helpers) and supporting tests/smoke scripts.
Changes:
- Added many missing read APIs, sub-resource APIs, typed metrics helpers, and webhook alert/subscription/endpoint write APIs to
TangoClient. - Introduced
HttpClientretry-with-exponential-backoff (withRetry-Aftersupport), async-iterator pagination helpers, andTANGO_BASE_URLconstructor fallback. - Added webhook HMAC signing utilities and significantly expanded unit + smoke coverage, plus a ShapeConfig fix for
IDVS_COMPREHENSIVE.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/webhooks.signing.test.ts | Adds unit tests for new webhook signing helpers. |
| tests/unit/utils.http.test.ts | Updates/extends HttpClient tests to cover retries/backoff behavior. |
| tests/unit/config.shapes.test.ts | Adds regression tests for ShapeConfig.IDVS_COMPREHENSIVE. |
| tests/unit/client.parity.test.ts | Adds unit tests for newly added Python-parity client methods. |
| tests/unit/client.iterate.test.ts | Adds tests for async iterator pagination (page/cursor/break handling). |
| tests/unit/client.baseurl.test.ts | Adds tests for TANGO_BASE_URL constructor fallback precedence. |
| src/webhooks/signing.ts | Implements generateSignature, verifySignature, parseSignatureHeader, and related constants. |
| src/utils/http.ts | Adds retry/backoff logic and Retry-After parsing to the HTTP client. |
| src/types.ts | Extends TangoClientOptions with timeout alias and retry settings. |
| src/models/Webhooks.ts | Adds/extends webhook-related models and new typed create/update inputs. |
| src/models/index.ts | Re-exports newly added webhook model/input types. |
| src/index.ts | Re-exports new client option types and webhook signing helpers from package root. |
| src/config.ts | Fixes ShapeConfig.IDVS_COMPREHENSIVE fields to match API/Python preset. |
| src/client.ts | Major expansion: new parity methods, webhook CRUD, iterators, base URL fallback, retry options wiring. |
| scripts/smoke-writes.ts | Adds live smoke harness for webhook write APIs. |
| scripts/smoke-reads.ts | Adds live smoke harness for newly added read-only parity methods. |
| scripts/smoke-parity.ts | Adds live smoke harness for parity methods (incl. webhook alerts CRUD). |
| scripts/smoke-extras.ts | Adds live smoke harness for signing helpers, iteration, and base URL env fallback. |
| CHANGELOG.md | Documents the new parity surface, retries, iterators, signing helpers, and fixes. |
| .gitignore | Ignores .worktrees/. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async createWebhookAlert(input: WebhookAlertCreateInput): Promise<WebhookAlert> { | ||
| if (!input?.name) throw new TangoValidationError("Webhook alert name is required"); | ||
| if (!input.query_type) throw new TangoValidationError("Webhook alert query_type is required (singular, e.g. \"contract\")"); | ||
| if (!input.filters || typeof input.filters !== "object") { |
| const body = toSubscriptionRequestBody(input as AnyRecord); | ||
| if (!body.subscription_name) { | ||
| throw new TangoValidationError("Webhook subscription_name is required"); | ||
| } |
| expect(seen).toEqual(["A1", "A2", "B1", "B2", "C1"]); | ||
| expect(calls.length).toBe(3); | ||
|
|
||
| // First call should NOT carry a page; subsequent should carry page=2 then page=3. |
Audit pass on the parity branch's internal docs vs `src/client.ts` and `dist/client.d.ts`.
README.md:
- Fixed broken link `SHAPED.md` -> `SHAPES.md` in the project structure tree.
- Repaired garbled Dynamic Models link ("ynamic shaping system** works.").
- Replaced 10-method stub API list with a comprehensive breakdown of all ~84 public methods.
docs/API_REFERENCE.md:
- Added sections for previously undocumented method groups: Organizations/Offices/Departments, OTAs, OTIDVs, Subawards, GSA eLibrary, Protests, IT Dashboard, LCATs, Metrics, Reference Lookups (NAICS/PSC/MAS/CFDA), Resolve/Validate, Entity Sub-resources, Agency Sub-resources, Opportunity Attachments, Async Iteration helpers, Utility, Webhook Alerts.
- Removed a duplicated `listBusinessTypes` section.
- Added `@deprecated` notice to `listDepartments` (source marks it deprecated in favor of `listOrganizations({ level: 1 })`).
- Fixed `searchOpportunityAttachments` example — `limit` doesn't exist on that method; actual interface is `{ q, topK?, includeExtractedText? }`.
- `testWebhookDelivery` is the legacy alias; `testWebhookEndpoint(endpointId)` is the preferred form.
- Expanded `createWebhookSubscription` section to document both canonical and legacy payload shapes.
docs/SHAPES.md:
- Fixed "Flat Responses" section — was self-referential and didn't explain that `flat: true` causes the API to return dotted keys. Added before/after example.
- Added `ShapeConfig Presets` section listing the 12 available presets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both files mirror the equivalents in tango-python so the two SDK repos have matching internal-docs structure (`API_REFERENCE.md` / `SHAPES.md` / `DYNAMIC_MODELS.md` / `WEBHOOKS.md` / `DEVELOPERS.md`). docs/WEBHOOKS.md (576 lines): - Full signing API: `verifySignature`, `generateSignature`, `parseSignatureHeader`, `SIGNATURE_PREFIX`. Arg order clearly documented (note: differs from Python). - Framework examples for Express (`express.raw`) and Fastify (raw body parser) to prevent the "verify on re-serialized body" footgun. - Full CRUD coverage for Endpoints, Subscriptions (subject + filter, canonical + legacy form), Alerts, event types, sample payloads, test delivery. - Complete TypeScript interface shapes for the typed inputs/outputs. - Common workflows: fresh setup, unit testing without network, wire-format inspection. - Sections from Python that don't apply to Node (CLI, WebhookReceiver, simulate helpers) are omitted rather than fabricated. docs/DEVELOPERS.md (605 lines): - Overview, Getting Started, Predefined Shapes, Custom Shapes, Type Safety, Performance, Troubleshooting, SDK conformance. - Translated entirely to Node/TypeScript reality: vitest, npm, package.json, fetchImpl mocking, npm publish via the publish.yml workflow. - Explicit notes where Node lacks a Python feature (no VCR cassettes; uses `fetchImpl` mocks and live smoke scripts instead). Commands verified: `npm test -- --run` (111/111), `npm run build`, `npm run typecheck`, `npm run coverage`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| } | ||
|
|
||
| export interface WebhookEndpointCreateInput { | ||
| name: string; |
| async createWebhookAlert(input: WebhookAlertCreateInput): Promise<WebhookAlert> { | ||
| if (!input?.name) throw new TangoValidationError("Webhook alert name is required"); | ||
| if (!input.query_type) throw new TangoValidationError("Webhook alert query_type is required (singular, e.g. \"contract\")"); | ||
| if (!input.filters || typeof input.filters !== "object") { |
| expect(seen).toEqual(["A1", "A2", "B1", "B2", "C1"]); | ||
| expect(calls.length).toBe(3); | ||
|
|
||
| // First call should NOT carry a page; subsequent should carry page=2 then page=3. |
Comment on lines
+644
to
+651
| await client.createWebhookSubscription({ | ||
| subscription_name: “Track specific vendors”, | ||
| endpoint: “ENDPOINT_UUID”, | ||
| subscription_type: “subject”, | ||
| event_type: “awards.new_award”, | ||
| subject_type: “entity”, | ||
| subject_ids: [“UEI123ABC”], | ||
| }); |
Comment on lines
656
to
663
| ```ts | ||
| await client.createWebhookSubscription({ | ||
| subscriptionName: "Track specific vendors", | ||
| subscriptionName: “Track specific vendors”, | ||
| payload: { | ||
| records: [ | ||
| { event_type: "awards.new_award", subject_type: "entity", subject_ids: ["UEI123ABC"] }, | ||
| { event_type: "awards.new_transaction", subject_type: "entity", subject_ids: ["UEI123ABC"] }, | ||
| { event_type: “awards.new_award”, subject_type: “entity”, subject_ids: [“UEI123ABC”] }, | ||
| { event_type: “awards.new_transaction”, subject_type: “entity”, subject_ids: [“UEI123ABC”] }, | ||
| ], |
| ### Internal | ||
|
|
||
| - Live smoke harnesses at `scripts/smoke-{reads,writes,extras,parity}.ts` exercise every new method against a running Tango instance. All four require `TANGO_API_KEY` in the environment (hard-fail if unset — no fallback). | ||
| - 4 new unit test files (`tests/unit/{client.parity,client.iterate,client.baseurl,webhooks.signing,config.shapes}.test.ts`) added; total suite is now 16 files / 111 tests / 82% line coverage. |
|
|
||
| ### `listOtas(options?)` | ||
|
|
||
| Uses **keyset pagination** (`cursor` + `limit`). |
Comment on lines
+362
to
+363
| const page1 = await client.listContracts({ shape: SHAPE }); | ||
| const page2 = await client.listContracts({ shape: SHAPE, offset: 25 }); |
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
Brings
@makegov/tango-nodeto full feature parity with both the Tango API andtango-python. Every method ontango_python.TangoClientnow has an idiomatic camelCase counterpart onTangoClient. Adds retry-with-backoff, async iterator pagination, webhook signing helpers, andTANGO_BASE_URLenv fallback along the way. Branch is 16 commits ahead ofmain.TangoClient(up from ~30 onmain)main); 82% line coveragescripts/smoke-*.ts), zero leaked resourcesWhat's new
API parity — read methods
The Node SDK was missing list/get methods for ~half the Tango API surface. Now covered:
listNaics/getNaics,listPsc/getPsc,listMasSins/getMasSin,listAssistanceListings/getAssistanceListing,listOrganizations/getOrganization,listOffices/getOffice,listDepartments/getDepartment,getBusinessTypelistOtas/getOta,listOtidvs/getOtidv/listOtidvAwards,listSubawards,listGsaElibraryContracts,listLcats(accepts{ uei }or{ idvKey })listProtests/getProtest,listItDashboard/getItDashboard,listMetrics,resolve,validateAPI parity — typed metrics wrappers
tango-pythonexposes typed metric methods scoped to each owner type; Node now matches:getEntityMetrics(uei, months, periodGrouping)getNaicsMetrics(code, months, periodGrouping)getPscMetrics(code, months, periodGrouping)(The generic
listMetrics({ ownerType, ownerId, months, periodGrouping })from the reads branch is preserved for low-level use.)API parity — entity, IDV, agency sub-resources
listEntityContracts,listEntityIdvs,listEntityOtas,listEntityOtidvs,listEntitySubawards,listEntityLcatslistIdvLcats(typed sibling of the genericlistLcats({ idvKey }))listAgencyAwardingContracts,listAgencyFundingContractsWebhook write API
createWebhookSubscription,updateWebhookSubscription,deleteWebhookSubscription. Accepts the canonical Tango payload shape (subscription_name,subscription_type,endpoint,query_type,filter_definition, …) and a legacy{ subscriptionName, payload }camelCase shape for backward compatibility.createWebhookEndpoint(nameis first-class — defaults to URL host if omitted),updateWebhookEndpoint,deleteWebhookEndpoint.testWebhookEndpoint(endpointId)is the canonical method (the API's body key isendpoint_id— see makegov/tango#2252);testWebhookDeliverykept as a legacy alias.listWebhookAlerts,getWebhookAlert,createWebhookAlert,updateWebhookAlert,deleteWebhookAlert. Note: multi-endpoint accounts hit a 400 from the backend oncreateWebhookAlert— tracked at makegov/tango#2256.New typed input interfaces exported from the package root:
WebhookSubscriptionCreateInput,WebhookSubscriptionUpdateInput,WebhookEndpointCreateInput,WebhookEndpointUpdateInput,WebhookAlertCreateInput,WebhookAlert, plus options types for the new sub-resources.Webhook signing helpers
Three standalone functions, exported from the package root (no
TangoClientrequired):verifySignature(body, header, secret)— constant-time HMAC-SHA256 verification. Accepts"sha256=<hex>"and bare-hex forms. Returnsboolean, never throws.generateSignature(body, secret)— emits"sha256=<hex>"matching the dispatcher format.parseSignatureHeader(header)— returns{ algorithm, signature } | nullfor clean branching in receivers.Mirrors
tango_python.webhooks.signingbyte-for-byte; the unit tests are ported from there.Async iterator pagination
For convenience, list methods now have async-iterator wrappers that handle
next/cursorfor you:Typed iterators:
iterateContracts,iterateEntities,iterateOpportunities,iterateNotices,iterateGrants,iterateForecasts,iterateIdvs,iterateVehicles. Sequential (no concurrent requests) to respect rate limits.Retry with exponential backoff
HttpClientnow retries failed requests automatically:Tango*errorretryBackoffMs(default 250ms), doubled per attempt, capped at 10sRetry-Afterheaders (delta-seconds and HTTP-date) on 429/503Constructor surface
TANGO_BASE_URLenv fallbackWhen
baseUrlis not passed andprocess.env.TANGO_BASE_URLis set, the client uses that — parity withTANGO_API_KEY. Useful for.env-driven local/staging configs.Misc
searchOpportunityAttachments,getVersion,listApiKeysround out parity with the Python SDK's introspection / search surface.Fixed
ShapeConfig.IDVS_COMPREHENSIVEno longer includesbase_and_exercised_options_value, which is not a valid IDV shape field — the API was returning400 Invalid shapeon this preset. Now aligned withtango_python.IDVS_COMPREHENSIVE. Also reconciledrecipient.cage_code→recipient.cageto match the Python preset exactly.Testing
npm test(vitest): 111/111 pass, 16 test files (up from 51 / 9 files onmain). New test files:tests/unit/{client.parity,client.iterate,client.baseurl,webhooks.signing,config.shapes}.test.ts.npm run coverage: 82.04% lines / 74.08% branches / 73.79% functions.npm run build: clean tsc compile.scripts/smoke-{reads,writes,extras,parity}.ts. Each requiresTANGO_API_KEYin env (hard-fails if unset — no fallback). Latest runs againsthttp://localhost:8000: smoke-reads 27/27, smoke-writes 8/8, smoke-extras 8/8, smoke-parity 19/21 (2 environmental:searchOpportunityAttachments404 locally;createWebhookAlerthit by the multi-endpoint tango#2256 issue).Decisions worth flagging
parseSignatureHeaderreturns{ algorithm, signature }instead of a bare string — small ergonomic upgrade over Python's bare-string return.algorithmdefaults to"sha256"when callers pass a legacy bare-hex value.verifySignaturearg order is(body, header, secret)(header before secret) to match TS conventions. Different from Python's(body, secret, signature_header).?cursor=fromnextbefore?page=; cursor wins if both are present.Known issues tracked on tango (not blockers)
endpointvsendpoint_idfield-name inconsistency.orderingon/notices/,/protests/,/subawards/but viewsets reject most values./api/webhooks/alerts/should accept an explicitendpointfield so multi-endpoint accounts can use the convenience wrapper.Risks
3— a small risk that previously-loud transient failures now retry silently. Setretries: 0to opt out.git filter-branchbefore push — branch history is clean.Sibling work
Companion PR in
tango-pythoncovers the same parity work for the Python SDK. Docs inmakegov/docs#9are updated for both SDKs' new surfaces.– Hal 🤖