chore(release): 0.2.0-alpha.3 — quality, hardening, typed wire shapes#4
Merged
Conversation
Webhook signature header (Issue A — review HIGH-8/HIGH-9, security LOW-2): - Multi-value `Langos-Signature` (string[] from proxies that split duplicate headers) is now joined with `,` so every `v1=` entry is considered. Previously only `[0]` was used, silently dropping signatures from rotation copies. - Empty arrays now throw a clear `LangosSignatureVerificationError` instead of crashing on the `[0]!` non-null assertion. - Reject signature headers longer than 4096 chars before parsing — guards against attacker-controlled CPU-DoS via `,`-splitting an oversize string. - Distinguish payload-parse failures from signature failures: bodies that pass HMAC verification but fail `JSON.parse` now throw the new `LangosWebhookPayloadError`. Different operational response (file upstream ticket vs rotate secret), so collapsing both into one error class hid the remediation from partner handlers. Retry-After cap (Issue B — review HIGH-7, security MEDIUM-2): - Raise the cap from 32s (`MAX_MS * 4`) to 5 minutes. Real-world rate-limit windows commonly run 60–300s; the prior cap silently truncated those values and defeated the header. - Make the cap configurable per-client via the new `maxRetryAfterMs` option (default `300_000`). - Reject negative, NaN, and over-cap `Retry-After` values: fall back to exponential backoff rather than honoring an attacker-controlled "sleep for 3 years" header. - HTTP-date `Retry-After` (RFC 7231) is documented as not honored — values fail the finiteness check and fall through to backoff. Adding HTTP-date parsing was deemed out of scope; the server emits delta-seconds and the graceful fallback is correct, just not optimal. Tests: +10 unit tests covering the new behavior. `pnpm test` 79 passing, `pnpm run lint` clean, `pnpm run build` clean.
…-key reuse / retry-after Add test/unit/failure-modes.test.ts (22 tests) addressing gaps from MEDIUM-16: - LangosTimeoutError: 4 tests verifying the timeout path fires, timeoutMs is set correctly, message includes the value, and per-call RequestOptions override works - LangosConnectionError: 4 tests for DNS/refused errors, cause propagation, message content, retry exhaustion (3 total calls on maxRetries:2) - AbortSignal propagation: 3 tests covering pre-aborted signal, mid-flight abort, and no-retry on abort; documents current raw-throw bug - Idempotency-Key reuse: 3 tests asserting same UUID across all POST retry attempts, user-supplied key preserved, independent calls get distinct keys - Retry-After parsing: 8 tests covering numeric (small + cap), HTTP-date fallback, negative, NaN, missing, and two integration round-trip tests Total: 71 → 93 tests. All existing tests unaffected.
Tightens the SDK's HTTP boundary so partner code gets real type-safety
all the way from the transport up to the public types. Before this
change, every `*FromWire` transform took `any` and every
`this.get<...>` / `this.post<...>` resource call typed responses with
`any` or `any[]` — narrowing in the public types was effectively
defensive theater because the wire was already widened.
What changed:
- `src/core/transform.ts` — declared `Wire<Resource>` interfaces for
Assessment, Challenge, Candidate, Session (+ Submission, Feedback,
Analytics, Insights), Account (+ PlanCaps, Integration), and
WebhookEndpoint. All `*FromWire` signatures now consume the typed
shape. Added a `numOrNull` helper to coerce `number | string | null |
undefined` (Postgres NUMERIC drivers can serialize as string) into
`number | null` without `any`-casts.
- `src/resources/{assessments,candidates,challenges,sessions,account}.ts`
— every `this.get<any>` / `this.post<any>` / `this.delete<any>` /
`this.patch<any>` is now parameterized with the matching
`Wire<Resource>` (or `WireListResponse<Wire<Resource>>` for paginated
endpoints).
- `src/types.ts` — dropped the `[key: string]: unknown` index
signature on `SessionInsights`. The catch-all silently widened the
typed `aiUsagePercent` and `testPassRate` fields back to `unknown`
and defeated narrowing. The server doesn't emit additional keys
today, so dropping it has no observed runtime impact.
- `src/types.ts` — narrowed `Account.billingCycle` from `string` to
`BillingCycle | null` (`'monthly' | 'yearly'`) and `Account.status`
from `string` to `AccountStatus`
(`'trialing' | 'active' | 'past_due' | 'canceled' | 'pending'`).
Verified against `services/customer/v1Service.js#retrieveAccount` +
`internalBilling.js#normalizeCompanyStatus`. Both new types are
exported from the package root.
Forward-compat behavior: every narrowed enum has a deploy-skew fallback
(unknown plan_tier → `'free'`, billing_cycle → `null`, status →
`'active'`, candidate status → `'error'`, session status → `'pending'`,
challenge status → `'draft'`). The SDK never throws when the server
emits a value it doesn't recognize.
Tests: extended `test/unit/transform.test.ts` with 19 new cases
covering known-good wire shapes per resource, missing optional fields
producing null fallbacks, numeric-string score coercion, the index-sig
removal on insights, and forward-compat fallback on every narrowed
enum. Total unit tests: 71 → 90.
Subtle behavior change for partners: code holding `account.billingCycle`
or `account.status` in a wider `string` typed variable will need to
widen explicitly or narrow on the union. Partners depending on
arbitrary keys leaking through `session.insights[k]` will lose that —
the catch-all index sig is gone.
Out of scope (left for a follow-up): two `any`-typed parameters in the
class-mixin helper in `src/core/errors.ts`. That's the documented
TypeScript idiom for variadic constructor mixins; it's not
wire-handling code.
# Conflicts: # CHANGELOG.md
Consolidates four parallel quality improvements + two surfaced bug fixes. ### Webhook + retry hardening - Multi-header join in Webhooks.constructEvent (string[] no longer drops entries) - 4096-char length cap on signature header parsing - New LangosWebhookPayloadError for malformed-JSON payloads (distinct from sig failure) - Retry-After cap raised 32s -> 300s, configurable via maxRetryAfterMs - Hostile/NaN/negative Retry-After values fall back to exponential backoff ### Typed wire shapes - Eliminated any from wire-handling layer (22 -> 2 occurrences, remaining are mixin idiom) - Internal Wire<Resource> interfaces in core/transform.ts - Narrowed Account.billingCycle, Account.status to typed unions (forward-compat fallbacks for unknown values) - Dropped SessionInsights catch-all index signature ### Failure-mode test coverage - +22 tests across LangosTimeoutError, LangosConnectionError, AbortSignal propagation, Idempotency-Key reuse across retries, Retry-After parsing edge cases ### Surfaced bugs fixed - LangosAbortError: partner-supplied AbortSignal cancellations now throw typed error instead of raw AbortError DOMException - LangosResponseFormatError: 2xx responses with non-JSON content-type now throw loudly instead of silently parsing HTML into garbage resource objects Tests: 71 -> 120. Lint + build clean.
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
Consolidates four parallel quality streams + two surfaced bug fixes into a single alpha.3 release.
Changes
Webhook + retry hardening
Webhooks.constructEvent(proxies that split duplicate headers no longer drop entries)LangosWebhookPayloadErrorfor malformed-JSON payloads (distinct from signature failure)Retry-Aftercap raised 32s → 300s, configurable viamaxRetryAfterMsRetry-Aftervalues fall back to exponential backoffTyped wire shapes
anyfrom wire-handling layer (22 → 2 occurrences, the remaining 2 are class-mixin idiom)Wire<Resource>interfaces incore/transform.tsAccount.billingCycletoBillingCycle | null('monthly' | 'yearly')Account.statustoAccountStatus('trialing' | 'active' | 'past_due' | 'canceled' | 'pending')null/'active')SessionInsightscatch-all index signatureFailure-mode test coverage (+22 tests)
LangosTimeoutError,LangosConnectionError,AbortSignalpropagationIdempotency-Keyreuse across retries (POST/PATCH/DELETE)Retry-Afterparsing edge cases (HTTP-date, negative, NaN, over-cap)Surfaced bugs fixed
LangosAbortError: partner-suppliedAbortSignalcancellations now throw a typed error instead of the rawAbortErrorDOMExceptionLangosResponseFormatError: 2xx responses with non-JSON content-type now throw loudly (e.g. SPA HTML at a misconfigured baseUrl) instead of silently parsing garbage into resource objectsBreaking changes (alpha-fair-game pre-1.0)
account.billingCyclewasstring, nowBillingCycle | nullaccount.statuswasstring, nowAccountStatusSessionInsights[someKey]no longer compiles (catch-all index sig removed)AbortErrorDOMException needs to catchLangosAbortErrorinsteadValidation
pnpm run lintcleanpnpm run buildclean (ESM 28.x KB, CJS 28.x KB, d.ts 25 KB)pnpm test: 120/120 passing (was 71)sdk-contractafter releaseRelease path
After merge → tag
v0.2.0-alpha.3→release.ymlpublishes to npm with provenance.