Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6bdf670
feat(node): expand webhook subscription + endpoint write methods
infinityplusone May 12, 2026
a64eb51
feat(node): add webhook alert write methods
infinityplusone May 12, 2026
f535dfa
feat(node): add retry with exponential backoff to HttpClient
infinityplusone May 12, 2026
21bad65
feat(node): accept timeout shorthand and retry options in TangoClient
infinityplusone May 12, 2026
a1eb9bf
test(node): add live smoke test script for webhook writes
infinityplusone May 12, 2026
46208ce
feat(client): add read methods for lookups, awards completeness, and …
infinityplusone May 12, 2026
cffe868
chore(scripts): add live smoke harness for new read methods
infinityplusone May 12, 2026
4675fec
feat(node): add webhook signature helpers (verifySignature/generateSi…
infinityplusone May 12, 2026
d9866a7
feat(node): add async iterator pagination helper
infinityplusone May 12, 2026
ebc599c
feat(node): read TANGO_BASE_URL from environment when baseUrl not pro…
infinityplusone May 12, 2026
094826f
test(node): add live smoke script for signing + iterator + base URL env
infinityplusone May 12, 2026
848278f
merge: consolidate reads branch into api-parity
infinityplusone May 12, 2026
99ee068
feat(node): full Python parity — entity/agency sub-resources, typed m…
infinityplusone May 12, 2026
649a7f5
test(node): unit + smoke coverage for Python-parity methods
infinityplusone May 12, 2026
5cf2233
fix(node): drop invalid base_and_exercised_options_value from IDVS_CO…
infinityplusone May 12, 2026
5adbd87
docs(changelog): record API parity + retry + signing + iterators
infinityplusone May 12, 2026
98eebbd
docs: correct README, API_REFERENCE, and SHAPES against actual surface
infinityplusone May 12, 2026
0b52873
docs: add WEBHOOKS.md and DEVELOPERS.md for SDK doc parity
infinityplusone May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ coverage/
# Other
ROADMAP.md
yoni/
.worktrees/
# >>> mg-tools >>>
# --- mg-tools (per-developer; re-run 'mg-tools install' after clone) ---
.claude-plugin/plugin-pointer.json
Expand Down
83 changes: 83 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,93 @@ This project follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

This release brings `tango-node` to **full feature parity** with both the Tango API and the `tango-python` SDK. Every method available on `tango_python.TangoClient` now has an idiomatic camelCase counterpart on `TangoClient`. 84 public methods, 16 test files, 111 passing unit tests, 82% line coverage.

### Added

#### API parity — read methods

- **Lookups**: `listNaics`, `getNaics`, `listPsc`, `getPsc`, `listMasSins`, `getMasSin`, `listAssistanceListings`, `getAssistanceListing`, `listOrganizations`, `getOrganization`, `listOffices`, `getOffice`, `listDepartments` (`@deprecated` JSDoc), `getDepartment`, `getBusinessType`.
- **Awards completeness**: `listOtas`, `getOta`, `listOtidvs`, `getOtidv`, `listOtidvAwards`, `listSubawards`, `listGsaElibraryContracts`, `listLcats` (accepts `{ uei }` or `{ idvKey }`).
- **Other resources**: `listProtests`, `getProtest`, `listItDashboard`, `getItDashboard`, `listMetrics` (parameterized over `ownerType` since the API exposes metrics only under owner-scoped paths).
- **Utility endpoints**: `resolve(input)` (POST `/api/resolve/` — returns `{ candidates, count }`), `validate(input)` (POST `/api/validate/`).

#### API parity — typed wrappers for Python's `get_*_metrics` helpers

- `getEntityMetrics(uei, months, periodGrouping)`
- `getNaicsMetrics(code, months, periodGrouping)`
- `getPscMetrics(code, months, periodGrouping)`

#### API parity — entity, IDV, and agency sub-resources

- `listEntityContracts`, `listEntityIdvs`, `listEntityOtas`, `listEntityOtidvs`, `listEntitySubawards`, `listEntityLcats`
- `listIdvLcats(key, options?)` — typed sibling of the generic `listLcats({ idvKey })`
- `listAgencyAwardingContracts`, `listAgencyFundingContracts`

#### Webhook write API

- Subscriptions: `createWebhookSubscription`, `updateWebhookSubscription`, `deleteWebhookSubscription`. Accepts both the canonical snake_case payload (`subscription_name`, `subscription_type`, `endpoint`, `query_type`, `filter_definition`, …) and the legacy `{ subscriptionName, payload }` camelCase shape for backward compatibility.
- Endpoints: `createWebhookEndpoint` (now `name` is first-class; defaults to URL host if omitted), `updateWebhookEndpoint`, `deleteWebhookEndpoint`. `testWebhookEndpoint(endpointId)` is the canonical method using the API's `{ endpoint_id }` body key; the prior `testWebhookDelivery` kept as an alias.
- Alerts (filter-subscription convenience wrapper): `listWebhookAlerts`, `getWebhookAlert`, `createWebhookAlert`, `updateWebhookAlert`, `deleteWebhookAlert`. Note: `createWebhookAlert` auto-resolves the caller's sole endpoint; accounts with multiple endpoints currently get a 400 from the API — tracked at [makegov/tango#2256](https://github.com/makegov/tango/issues/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 signature helpers (parity with `tango_python.webhooks.signing`)

- `verifySignature(body, header, secret)` — constant-time HMAC-SHA256 verification. Accepts `"sha256=<hex>"` and bare-hex forms. Returns `boolean`, never throws.
- `generateSignature(body, secret)` — emits `"sha256=<hex>"` matching the dispatcher format.
- `parseSignatureHeader(header)` — returns `{ algorithm, signature } | null` for cleaner branching in receivers.

All exported from the package root; receivers don't need to instantiate `TangoClient`.

#### Async iterator pagination

For convenience, list methods now have async-iterator wrappers that handle `next` / `cursor` for you:

```typescript
for await (const contract of client.iterateContracts({ awarding_agency: "9700" })) {
console.log(contract.piid, contract.total_contract_value);
}
```

Typed iterators: `iterateContracts`, `iterateEntities`, `iterateOpportunities`, `iterateNotices`, `iterateGrants`, `iterateForecasts`, `iterateIdvs`, `iterateVehicles`. Iteration is sequential (no concurrent requests) to respect API rate limits.

#### Retry with exponential backoff

`HttpClient` now automatically retries failed requests:

- Retries on 5xx, 408 (Request Timeout), 429 (Too Many Requests), network errors, and client-side timeouts.
- Does **not** retry on other 4xx — those surface as the appropriate `Tango*` error immediately.
- Exponential backoff: base `retryBackoffMs` (default 250ms), doubled per attempt, capped at 10s.
- Honors `Retry-After` headers (delta-seconds and HTTP-date) on 429/503.

#### Constructor surface

- `retries` (default `3`) and `retryBackoffMs` (default `250`) options on `TangoClientOptions`. Set `retries: 0` to disable.
- `timeout` accepted as a shorthand alias for `timeoutMs` (both in ms; `timeoutMs` wins if both are supplied).

#### Environment variable fallback

- `TANGO_BASE_URL` env var is now read when `baseUrl` is not passed to the constructor — parity with `TANGO_API_KEY`.

#### Misc

- `searchOpportunityAttachments`, `getVersion`, `listApiKeys` round out parity with the Python SDK's introspection / search surface.

### Changed

- `createWebhookSubscription`, `createWebhookEndpoint`, and related write methods accept the canonical Tango API payload shape in addition to the previous camelCase wrappers — see the new typed input interfaces.

### Fixed

- `ShapeConfig.IDVS_COMPREHENSIVE` no longer includes `base_and_exercised_options_value`, which is not a valid IDV shape field — the API was returning `400 Invalid shape` on this preset. Now aligned with `tango_python.IDVS_COMPREHENSIVE`. Also reconciled `recipient.cage_code` → `recipient.cage` to match the Python preset exactly.

### 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.
- ESM build (`tsc -p tsconfig.json`) clean.

## [0.3.0] - 2026-02-09

### Added
Expand Down
80 changes: 66 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,18 +165,70 @@ The Node SDK mirrors the Python client's behavior for `shape`, `flat`, and `flat

## API Methods

The Node client mirrors the Python SDK's high-level API:

- `listAgencies(options)`
- `getAgency(code)`
- `listBusinessTypes(options)`
- `listContracts(options)`
- `listEntities(options)`
- `getEntity(ueiOrCage, options)`
- `listForecasts(options)`
- `listOpportunities(options)`
- `listNotices(options)`
- `listGrants(options)`
The Node client mirrors the Python SDK's high-level API. Selected highlights:

**Agencies / Offices / Organizations / Departments**
- `listAgencies(options)` / `getAgency(code)`
- `listOffices(options)` / `getOffice(code)`
- `listOrganizations(options)` / `getOrganization(identifier)`
- `listDepartments(options)` / `getDepartment(code)`

**Contracts / IDVs / OTAs / OTIDVs / Subawards**
- `listContracts(options)` / `listIdvs(options)` / `getIdv(key, options)`
- `listIdvAwards(key, options)` / `listIdvChildIdvs({key, ...options})` / `listIdvTransactions(key, options)`
- `getIdvSummary(identifier)` / `listIdvSummaryAwards(identifier, options)`
- `listOtas(options)` / `getOta(key)` / `listOtidvs(options)` / `getOtidv(key)` / `listOtidvAwards(key, options)`
- `listSubawards(options)`

**Vehicles**
- `listVehicles(options)` / `getVehicle(uuid, options)` / `listVehicleAwardees(uuid, options)`

**Entities**
- `listEntities(options)` / `getEntity(ueiOrCage, options)`
- `listEntityContracts(uei, options)` / `listEntityIdvs(uei, options)` / `listEntityOtas(uei, options)`
- `listEntityOtidvs(uei, options)` / `listEntitySubawards(uei, options)` / `listEntityLcats(uei, options)`
- `getEntityMetrics(uei, months, periodGrouping)`

**Forecasts / Opportunities / Notices / Grants**
- `listForecasts(options)` / `listOpportunities(options)` / `listNotices(options)` / `listGrants(options)`
- `searchOpportunityAttachments(options)`

**GSA eLibrary / Protests / IT Dashboard / Subawards / LCATs**
- `listGsaElibraryContracts(options)` / `listProtests(options)` / `getProtest(caseNumber)`
- `listItDashboard(options)` / `getItDashboard(uii)`
- `listLcats(options)` / `listIdvLcats(key, options)`

**Reference / Lookups**
- `listBusinessTypes(options)` / `getBusinessType(code)`
- `listNaics(options)` / `getNaics(code)` / `getNaicsMetrics(code, months, periodGrouping)`
- `listPsc(options)` / `getPsc(code)` / `getPscMetrics(code, months, periodGrouping)`
- `listMasSins(options)` / `getMasSin(sin)`
- `listAssistanceListings(options)` / `getAssistanceListing(number)`
- `listMetrics(options)` / `listAgencyAwardingContracts(code, options)` / `listAgencyFundingContracts(code, options)`

**Resolve / Validate**
- `resolve(input)` — resolve a free-text name to ranked entity/org candidates
- `validate(input)` — validate a PIID, solicitation number, or UEI

**Webhooks**
- `listWebhookEventTypes()` / `listWebhookSubscriptions(options)` / `getWebhookSubscription(id)`
- `createWebhookSubscription(...)` / `updateWebhookSubscription(id, patch)` / `deleteWebhookSubscription(id)`
- `listWebhookEndpoints(options)` / `getWebhookEndpoint(id)`
- `createWebhookEndpoint(...)` / `updateWebhookEndpoint(id, patch)` / `deleteWebhookEndpoint(id)`
- `testWebhookEndpoint(endpointId)` (preferred) / `testWebhookDelivery(options?)` (legacy alias)
- `getWebhookSamplePayload(options?)`
- `listWebhookAlerts(options)` / `getWebhookAlert(id)` / `createWebhookAlert(input)`
- `updateWebhookAlert(id, patch)` / `deleteWebhookAlert(id)`

**Async iteration helpers**
- `iterate(method, options)` — generic async iterator over any supported list method
- `iterateContracts` / `iterateEntities` / `iterateOpportunities` / `iterateNotices`
- `iterateGrants` / `iterateForecasts` / `iterateIdvs` / `iterateVehicles`

**Utility**
- `getVersion()` / `listApiKeys()`

See [docs/API_REFERENCE.md](docs/API_REFERENCE.md) for full signatures and parameters.

All list methods return a paginated response:

Expand Down Expand Up @@ -254,7 +306,7 @@ tango-node/
├── docs/ # Documentation
│ ├── API_REFERENCE.md
│ ├── DYNAMIC_MODELS.md
│ └── SHAPED.md
│ └── SHAPES.md
├── tests/ # Test suite (Vitest)
│ └── unit/
│ ├── client.test.ts
Expand Down Expand Up @@ -305,7 +357,7 @@ Useful scripts:

- [API Reference](docs/API_REFERENCE.md) - Detailed API documentation
- [Shape System Guide](docs/SHAPES.md) - Comprehensive guide to response shaping
- [Dynamic Models Guide](docs/DYNAMIC_MODELS.md) - ynamic shaping system\*\* works.
- [Dynamic Models Guide](docs/DYNAMIC_MODELS.md) - How the dynamic shaping system works.

## License

Expand Down
Loading