Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
de05910
docs: open the audit #67 decision ledger and index it
Wahbeh-Mohammad Sep 4, 2026
ae46aed
docs(serde): stop claiming a top-level wire null can never decode
Wahbeh-Mohammad Sep 4, 2026
217fe70
docs(core): correct six TSDoc blocks that state the opposite of the code
Wahbeh-Mohammad Sep 4, 2026
1fddb21
docs(core): name RequestBodyNotAllowedError on buildRequest, and pin it
Wahbeh-Mohammad Sep 4, 2026
8d777c9
test(transport-conformance): stop claiming TRANSPORT-22 the suite nev…
Wahbeh-Mohammad Sep 4, 2026
0c4c94a
docs: re-anchor deviations.md items 3, 5, 15, 17 and the OBS-29 row
Wahbeh-Mohammad Sep 4, 2026
c983c0c
docs: make three as-built guides match the functions they document
Wahbeh-Mohammad Sep 4, 2026
66ee76d
fix: give CTX-15's no-op instrumentation bundle a no-op span (#69)
Wahbeh-Mohammad Sep 4, 2026
2287649
test: pin REDIR-3's current-hop reading and PAGE-19's relative-refere…
Wahbeh-Mohammad Sep 4, 2026
e341d3d
docs: record the deviations the 2026-09-04 audit found outside a phas…
Wahbeh-Mohammad Sep 4, 2026
fe3e4a0
docs: ledger — record the #69 outcome, the span.ts split and the stri…
Wahbeh-Mohammad Sep 4, 2026
73c64d0
docs: ledger — record the #68 outcome and its round-2 scope
Wahbeh-Mohammad Sep 4, 2026
70c1a3e
docs: re-anchor the last five stale citations in deviations.md
Wahbeh-Mohammad Sep 4, 2026
bd396b6
docs(core): InstrumentationBundle no longer claims nothing consumes i…
Wahbeh-Mohammad Sep 4, 2026
6b88166
Merge branch 'audit/67/68-reanchor-deviations-tsdoc' into audit/remed…
Wahbeh-Mohammad Sep 4, 2026
840f355
Merge branch 'audit/67/69-record-unledgered-deviations' into audit/re…
Wahbeh-Mohammad Sep 4, 2026
e99f9d3
docs: ledger — wave 1 landed; D8/D9 for wave 2
Wahbeh-Mohammad Sep 4, 2026
f03a847
fix: build the redirect error messages from redactUrl (#70)
Wahbeh-Mohammad Sep 4, 2026
e5ea278
feat: carry a redacted url.full on http.redirect.rejected (#70)
Wahbeh-Mohammad Sep 4, 2026
86091f4
test: prove the rejected-redirect path leaks no secret end to end (#70)
Wahbeh-Mohammad Sep 4, 2026
b92fb72
docs: state the redacted-message / raw-property split in the errors g…
Wahbeh-Mohammad Sep 4, 2026
9798d22
feat: redact the Basic and Digest passwords behind credential classes…
Wahbeh-Mohammad Sep 4, 2026
0c06d74
fix: guard a challenge replay whenever the outbound hop was guarded (…
Wahbeh-Mohammad Sep 4, 2026
d5acd9e
docs: rewrite auth.md's credential shape and ledger the two AUTH-8/XC…
Wahbeh-Mohammad Sep 4, 2026
60174dc
docs: ledger — record the #70 outcome and the api-extractor link trap
Wahbeh-Mohammad Sep 4, 2026
fc33e90
docs: ledger — record the #71 outcome
Wahbeh-Mohammad Sep 4, 2026
fab9b70
Merge branch 'audit/67/70-redact-redirect-urls' into audit/remediatio…
Wahbeh-Mohammad Sep 4, 2026
9ce8943
Merge branch 'audit/67/71-auth-credential-hygiene' into audit/remedia…
Wahbeh-Mohammad Sep 4, 2026
4487660
docs: ledger — wave 2 landed, run paused before wave 3
Wahbeh-Mohammad Sep 4, 2026
cc27e81
docs: ledger — pre-take wave 3 decisions D10–D12 and the file partition
Wahbeh-Mohammad Sep 5, 2026
034a20c
fix(auth): read every value of the challenge header, parsed per value
Wahbeh-Mohammad Sep 5, 2026
db430f4
fix(auth): emit cnonce for every -sess Digest algorithm, negotiated q…
Wahbeh-Mohammad Sep 5, 2026
fb2a631
test(rx): pin the SSE ownership transfer by counting the owned resource
Wahbeh-Mohammad Sep 5, 2026
624f53b
fix(auth): an empty Digest realm or nonce is unsatisfiable, not present
Wahbeh-Mohammad Sep 5, 2026
9126a04
docs(rx): state the SSE ownership transfer outright
Wahbeh-Mohammad Sep 5, 2026
f6d2510
test(node): pin the SSE ownership transfer's single release on Node
Wahbeh-Mohammad Sep 5, 2026
19b0602
test(transport): both transports must surface every repeated challeng…
Wahbeh-Mohammad Sep 5, 2026
be1b823
docs: ledger the -sess cnonce departure from AUTH-22's letter
Wahbeh-Mohammad Sep 5, 2026
6152d27
docs: record the challenge-list and -sess cnonce behaviour in auth.md
Wahbeh-Mohammad Sep 5, 2026
74e4fd9
docs: ledger the RxJS SSE ownership transfer as an ASYNC-21 departure
Wahbeh-Mohammad Sep 5, 2026
2cfbe5c
docs(transport-conformance): the suite header no longer claims TRANSP…
Wahbeh-Mohammad Sep 5, 2026
21fa0ba
fix(transport-conformance): the fixture's header array must be mutabl…
Wahbeh-Mohammad Sep 5, 2026
36378b3
fix(retry): surface the final typed error, not a SuppressedError wrapper
Wahbeh-Mohammad Sep 5, 2026
32c9f2b
test(xcut): conformance rows for the surfaced retry error and its trail
Wahbeh-Mohammad Sep 5, 2026
0778a88
docs: the retry pillar throws the last attempt's error, and retryAtte…
Wahbeh-Mohammad Sep 5, 2026
c377fea
test(node): the retry trail case moves from "same wrapper" to "no wra…
Wahbeh-Mohammad Sep 5, 2026
4578841
docs(retry): the trail is not an attempt count — drop the false arith…
Wahbeh-Mohammad Sep 5, 2026
d883cd7
Merge branch 'audit/67/75-rx-sse-ownership' into audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
a81a156
Merge branch 'audit/67/74-auth-challenges' into audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
2ea8b3e
Merge branch 'audit/67/72-retry-typed-error' into audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
c644473
docs: ledger — wave 3 landed; re-anchor deviations item 3 after #72
Wahbeh-Mohammad Sep 5, 2026
25b987a
docs: ledger — pre-take wave 4 decisions D13–D15 and the file partition
Wahbeh-Mohammad Sep 5, 2026
dba7743
fix(core): a path placeholder is satisfied only by an own property (S…
Wahbeh-Mohammad Sep 5, 2026
3629ec5
fix(core): reject an invalid Date in the conditional-request setters …
Wahbeh-Mohammad Sep 5, 2026
8036fbc
fix(core): raise on a zero-length chunk during an exact-length body copy
Wahbeh-Mohammad Sep 5, 2026
ef7df30
fix(core): quote the multipart boundary parameter when it is not a token
Wahbeh-Mohammad Sep 5, 2026
5a49bb1
fix(core): the timeout setter enforces AbortSignal.timeout()'s own ra…
Wahbeh-Mohammad Sep 5, 2026
58a4d3e
fix(core): stop the response logging tap from poisoning itself after …
Wahbeh-Mohammad Sep 5, 2026
fbaf269
fix(core): one idempotency key per logical request across retry attempts
Wahbeh-Mohammad Sep 5, 2026
dad81b4
docs: record the RETRY-44 reading behind the single-application reque…
Wahbeh-Mohammad Sep 5, 2026
01dfdfb
test(node-conformance): cover the five Web Streams bridges, and pin t…
Wahbeh-Mohammad Sep 5, 2026
6c1343d
fix(core): reject an unpaired surrogate where it is supplied (HTTP-29…
Wahbeh-Mohammad Sep 5, 2026
2d3785c
fix(test): type the multipart drain helper as a view over a plain Arr…
Wahbeh-Mohammad Sep 5, 2026
e414e26
fix(core): getAll returns a frozen list on the absent-name path too (…
Wahbeh-Mohammad Sep 5, 2026
54b5b90
fix(core): TeeSink rejects a non-integral tap limit at the constructo…
Wahbeh-Mohammad Sep 5, 2026
d357f61
refactor(core): consolidate EMPTY_VALUE_LIST in http/builder.ts
Wahbeh-Mohammad Sep 5, 2026
63e2aec
docs(core): errors.ts TSDoc matches what the two classes now cover
Wahbeh-Mohammad Sep 5, 2026
18ba420
docs: http.md states the real timeoutMs range
Wahbeh-Mohammad Sep 5, 2026
179696e
docs: deviations — two rows for #76, per D0
Wahbeh-Mohammad Sep 5, 2026
10ebe03
Merge branch 'audit/67/73-idempotency-key-once' into audit/remediatio…
Wahbeh-Mohammad Sep 5, 2026
4212de3
Merge branch 'audit/67/77-body-lifecycle' into audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
36c3d04
Merge branch 'audit/67/76-domain-model-validation' into audit/remedia…
Wahbeh-Mohammad Sep 5, 2026
8e32ee2
docs: ledger — wave 4 landed; pre-take wave 5 decisions D16–D18
Wahbeh-Mohammad Sep 5, 2026
71c9997
test(retry): pin one retryability answer per I/O error class
Wahbeh-Mohammad Sep 5, 2026
b937175
fix(retry): a zero initial delay never yields a NaN backoff
Wahbeh-Mohammad Sep 5, 2026
426838b
fix(retry): a non-finite delayOverride falls back like a throwing one
Wahbeh-Mohammad Sep 5, 2026
1c3f46a
fix(core): scope send()'s async stores with run(), not enterWith (#80)
Wahbeh-Mohammad Sep 5, 2026
d8d3ad5
docs: item 17 states what the cause-walk matches, and why
Wahbeh-Mohammad Sep 5, 2026
1254707
fix(codec-json): race the abort signal against a pending read or writ…
Wahbeh-Mohammad Sep 5, 2026
b6b4041
feat(core): a public way to instrument a pipeline (#80)
Wahbeh-Mohammad Sep 5, 2026
f08bb19
fix(core): close the response on every paginator exit, including a ma…
Wahbeh-Mohammad Sep 5, 2026
6655f9d
test(retry): keep the new retry cases inside the lint caps
Wahbeh-Mohammad Sep 5, 2026
b757a55
fix(codec-json): a present Tristate can never carry null (SERDE-14)
Wahbeh-Mohammad Sep 5, 2026
a83c206
fix(core): the logging step says when a body capture fails, and names…
Wahbeh-Mohammad Sep 5, 2026
ead5a62
fix(core): report an SSE release failure once, not once per channel (…
Wahbeh-Mohammad Sep 5, 2026
b2e1d9b
docs(core): TSDoc for the reachable operation span, and for what reda…
Wahbeh-Mohammad Sep 5, 2026
0bcc6f2
docs(retry): RetryStepOptions.delayOverride states the failure semantics
Wahbeh-Mohammad Sep 5, 2026
80e16d7
fix(core): a pagination cursor with no UTF-8 form fails inside the er…
Wahbeh-Mohammad Sep 5, 2026
c6bbc3a
test(node-conformance): the caller's async context survives Runtime.s…
Wahbeh-Mohammad Sep 5, 2026
620cf8d
docs: how to instrument a pipeline, and what makes DEXPACE_LOG_LEVEL …
Wahbeh-Mohammad Sep 5, 2026
c44ce53
docs: deviations — OBS-29 closed, and an OBS-35 row for the default k…
Wahbeh-Mohammad Sep 5, 2026
835ac44
docs(core): send() states what it restores in the caller's async cont…
Wahbeh-Mohammad Sep 5, 2026
8093e8a
docs(core): the shipped .d.ts names the splice site and the abort race
Wahbeh-Mohammad Sep 5, 2026
118d56c
docs: a paging strategy owes a well-formed PageInfo (PAGE-4)
Wahbeh-Mohammad Sep 5, 2026
6c7fb65
Merge branch 'audit/67/78-retry-classify-backoff' into audit/remediat…
Wahbeh-Mohammad Sep 5, 2026
b9eef12
Merge branch 'audit/67/80-instrumentation' into audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
4576658
Merge branch 'audit/67/79-serde-pagination-sse' into audit/remediatio…
Wahbeh-Mohammad Sep 5, 2026
a9c5b43
docs: ledger — wave 5 landed; pre-take wave 6 decisions D19–D20
Wahbeh-Mohammad Sep 5, 2026
f02dd8e
fix(transport): degrade the headers a native client refuses, on both …
Wahbeh-Mohammad Sep 5, 2026
61fce6d
fix(transport-undici): write a file body through its own writeTo (BOD…
Wahbeh-Mohammad Sep 5, 2026
2a75c5b
feat(transport-undici): refuse a proxy type undici cannot honour, at …
Wahbeh-Mohammad Sep 5, 2026
6df9645
docs: record the SOCKS gap, and the two transport rules #81 was about
Wahbeh-Mohammad Sep 5, 2026
9c6f145
docs: re-anchor two citations the same change moved
Wahbeh-Mohammad Sep 5, 2026
ca682c5
test(transport): name the intact file-body fixture's size and range
Wahbeh-Mohammad Sep 5, 2026
0eebc23
docs(transport-fetch): the README's drop set was three names short
Wahbeh-Mohammad Sep 5, 2026
808f6b0
Merge branch 'audit/67/81-undici-parity' into audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
fe04249
docs: ledger — wave 6a landed (#81)
Wahbeh-Mohammad Sep 5, 2026
3c72ef0
fix(transport-shared): CONTROL_BYTE was excepting LF as well as HTAB
Wahbeh-Mohammad Sep 5, 2026
73af7c5
fix(transport): one classification table for a permanent native failure
Wahbeh-Mohammad Sep 5, 2026
55f06ac
fix(transport): a body-less response reports body === null on both ad…
Wahbeh-Mohammad Sep 5, 2026
deb32e0
fix(transport): a producer failure aborts the native call it raced
Wahbeh-Mohammad Sep 5, 2026
ac26cb0
fix(transport): validate defaultTimeoutMs at both factories
Wahbeh-Mohammad Sep 5, 2026
a889d5d
docs(transport): record the parity work, and the TRANSPORT-20 reading
Wahbeh-Mohammad Sep 5, 2026
901b50e
Merge branch 'audit/67/82-transport-parity' into audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
262fb8f
docs: ledger — wave 6b landed (#82), run complete; re-anchor item 13 …
Wahbeh-Mohammad Sep 5, 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 CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ until it was dissolved on 2026-09-04.
| [`docs/work/mvp/2026-09-04-open-items-dissolution.md`](docs/work/mvp/2026-09-04-open-items-dissolution.md) | **Archive of record, not a register.** The dissolved open-items register, moved here whole on 2026-09-04 with every open question in it decided. Nothing is appended to it; its item IDs stay reserved and still resolve, because they are cited from source. | no — archive |
| `docs/first-release.md` | **Live.** Release-readiness: what the release path already does, and the blockers that must clear before a first publish. Was the `NFR-16` row of the dissolved deferral register. | yes |
| `docs/deviations.md` | **Register.** The as-built audit of §10, and where a deviation found outside a phase lands. | yes |
| `docs/audit-67-decisions.md` | **Ledger.** Decisions taken during the audit #67 remediation run, and the release-machinery work it deferred. | yes |
| `docs/assets/` | Vendored wordmark SVGs the root `README.md` renders. | yes |

**Frozen means a maintenance tool refuses to write there**, not merely that you should not. The
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ is the index; the rule is that nothing in `docs/` is unowned, and nothing is wri
| [the dissolved open-items register](./work/mvp/2026-09-04-open-items-dissolution.md) | **Archive of record.** Everything the register held when it was dissolved on 2026-09-04, with every open question in it decided. Item IDs stay reserved and still resolve | — | No — nothing is appended |
| [`first-release.md`](./first-release.md) | Release readiness: what the release path already does, the confirmed mechanics, and the blockers that must clear before a first publish. Was the `NFR-16` row of `deferred-items.md`, which was dissolved on 2026-09-04 — its five still-live rows are archived under *Live deferrals* in [`work/mvp/2026-09-04-register-retirement-purge.md`](./work/mvp/2026-09-04-register-retirement-purge.md) | The maintainer | Yes — edited as blockers clear |
| [`deviations.md`](./deviations.md) | The as-built audit of §10, and the landing point for a deviation found outside a phase | An audit or review | Yes — appends |
| [`audit-67-decisions.md`](./audit-67-decisions.md) | Decision ledger for the audit #67 remediation run: cross-task decisions, rejected alternatives, and the release-machinery work deferred from it | The remediation supervisor | Yes — appends |
| [`assets/`](./assets/) | Vendored wordmark SVGs the root `README.md` renders | Copied from `dexpace/morphic` | Yes |

## Frozen means frozen
Expand Down
665 changes: 665 additions & 0 deletions docs/audit-67-decisions.md

Large diffs are not rendered by default.

138 changes: 101 additions & 37 deletions docs/deviations.md

Large diffs are not rendered by default.

66 changes: 57 additions & 9 deletions docs/sdk-documentation/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ interface AuthTiers {

The most specific tier that is set wins (`AUTH-4`). `perCall` comes from
`RequestOptions.newBuilder().auth(descriptor)`, which is how one call opts out of, or into, something
different from the client default. Only the *selection* is tiered; whether a selected requirement can
be satisfied at all is `AUTH-6`, and failing it is an `AuthResolutionError`.
different from the client default. **`operation` comes from `RequestOptions.operationAuth`**, set with
`RequestOptions.newBuilder().operationAuth(descriptor)` — both per-call slots ride on the same
`RequestOptions`, and the AUTH pillar step reads them into the two tiers
(`packages/core/src/auth/auth-step.ts:252,756`; `packages/core/src/auth/resolve.ts:19` names the mapping). `client` is the
configured default. Only the *selection* is tiered; whether a selected requirement can be satisfied at
all is `AUTH-6`, and failing it is an `AuthResolutionError`.

An `AuthDescriptor` is a list of `AuthRequirement`s, each a scheme plus optional scopes and
parameters. Both are built by factory, never as an object literal:
Expand All @@ -39,8 +43,8 @@ The factories validate and freeze (`AUTH-3`). A descriptor containing a `NO_AUTH
```typescript
interface AuthCredentialSet {
readonly bearer?: {provider: TokenProvider; marginMs?: number};
readonly basic?: {username: string; password: string};
readonly digest?: {username: string; password: string; algorithmPreference?: DigestAlgorithm[]};
readonly basic?: BasicCredential;
readonly digest?: DigestCredential;
readonly apiKey?: {credential: ApiKeyCredential | NameKeyCredential; headerName?: string; prefix?: string};
}
```
Expand All @@ -49,18 +53,37 @@ The five schemes are `OAUTH2`, `API_KEY`, `BASIC`, `DIGEST` and `NO_AUTH`. A req
scheme; the credential set supplies the material for it. A requirement with no matching credential is
an `AuthResolutionError` at send time, not a silent unauthenticated request.

**`ApiKeyCredential`, `NameKeyCredential` and `BearerToken` are nominal, not structural.** Each carries
a `#private` field, so no caller-side object literal is assignable to them and the validation in each
factory cannot be routed around. They also override `toString()` and Node's inspect symbol, so a
credential cannot leak into a log line or a stack trace by accident.
**Every credential type is nominal, not structural.** `ApiKeyCredential`, `NameKeyCredential`,
`BearerToken`, `BasicCredential` and `DigestCredential` each carry a `#private` field, so no
caller-side object literal is assignable to them and the validation behind each cannot be routed
around. All five override `toString()` and Node's inspect symbol, so a credential cannot leak into a
log line or a stack trace by accident.

```typescript
import {ApiKeyCredential, createBearerToken} from '@dexpace/core';
import {
ApiKeyCredential,
BasicCredential,
createBearerToken,
DigestCredential,
} from '@dexpace/core';

const credentials = {
basic: new BasicCredential('alice', 'super-secret'),
digest: new DigestCredential('bob', 'super-secret', ['SHA-256', 'MD5']),
apiKey: {credential: new ApiKeyCredential('super-secret'), headerName: 'X-Api-Key'},
};

String(new ApiKeyCredential('super-secret')); // 'ApiKeyCredential{key=***}'
String(createBearerToken('t', 1)); // 'BearerToken{token=***, expiresAt=1}' — the expiry survives
String(credentials.basic); // 'BasicCredential{username=alice, password=***}'
```

There is no `password` property to read back, by design: `BasicCredential` and `DigestCredential`
shipped as plain `{username, password}` records until 2026-09-04, and a plain property is reachable
through `credential['password']`, `Object.keys`, `JSON.stringify` and a default `util.inspect` — so
`util.inspect(credentials)` printed the password in clear beside `ApiKeyCredential{key=***}`. The
username and the Digest algorithm preference stay visible; `AUTH-8` permits non-secret fields to.

The inspect symbol matters as much as `toString`: `console.log(credential)` and `util.inspect` do not
route an object argument through `toString`, so both are overridden (`AUTH-8`).

Expand Down Expand Up @@ -102,6 +125,14 @@ request is dispatched (`AUTH-28`). There is no option to disable it. `NO_AUTH` n
is why an auth-less `standardResilience()` installs a `NO_AUTH`-only step rather than no step at all —
the pillar slot stays filled and the behaviour stays uniform.

**Once guarded, always guarded.** A `challengeHook` may return any request it likes, including one
whose URL has been downgraded to `http://`. When the outbound pass ran the guard, the replay is
guarded too — unconditionally, without inspecting a single header name, because
`ApiKeyCredentialConfig.headerName` lets this step stamp a header no fixed list would contain. On a
`NO_AUTH` hop, which is never guarded outbound, a replacement carrying `Authorization` or
`Proxy-Authorization` still trips the guard. A genuinely credential-free re-issue over `http://` is
what `XCUT-16` explicitly permits, and it still proceeds.

## Challenges

```typescript
Expand Down Expand Up @@ -130,6 +161,23 @@ when the deferral register was dissolved on 2026-09-04).
**Basic and Digest never stamp preemptively.** They react to a challenge. That is an interpretation of
`§11` rather than a stated requirement, and it is ledgered as one.

**Every challenge the response offered is considered, not just the first.** A server may send
`WWW-Authenticate` (or `Proxy-Authenticate`) once with several comma-separated challenges, or once per
challenge — RFC 9110 §5.3 permits both, and RFC 7616 §3.3 recommends the repeated form for Digest
algorithm discovery. Which of the two shapes reaches the step is partly the transport's accident:
`@dexpace/transport-fetch` comma-joins repeated values because WHATWG `Headers` does,
`@dexpace/transport-undici` keeps them apart. The step reads *every* value and parses each on its own,
so the offer is the same list either way, and `@dexpace/transport-conformance` carries a row asserting
that. A challenge the SDK cannot answer — an unsupported algorithm, an `auth-int`-only `qop`, a realm
this client cannot echo back, an empty `realm` or `nonce` — is declined and the next one is tried; a
`401` offering nothing answerable surfaces unchanged and unclosed.

**Digest `-sess` sends `cnonce` whether or not `qop` was negotiated.** `MD5-sess` and `SHA-256-sess`
fold the client nonce into HA1 (RFC 7616 §3.4.2), so a server handed a `-sess` response without one
cannot verify it. `nc` and `qop` stay conditional on a negotiated `qop`. `AUTH-22`'s literal wording
says all three are conditional; the departure is a row in
[`deviations.md`](../deviations.md).

## Redirects and credentials

Credentials are attached at an origin and must not follow a request to a different one. The redirect
Expand Down
25 changes: 24 additions & 1 deletion docs/sdk-documentation/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ export async function call(): Promise<void> {
the two apart at the point of abort — which is exactly how a transport decides which of the two
errors to raise.

**Neither narrowing changes when a retry pillar is installed.** What `retryStep` throws once it gives
up is the final attempt's own error, unwrapped, so the two `instanceof` checks above read the same at
`maxAttempts: 1` and at `maxAttempts: 3`. The earlier attempts ride beside it and are read with
`retryAttempts(error)` — see [`pipelines.md`](./pipelines.md#the-four-shipped-pillars).

## Narrowing helpers

Three predicates exist for the cases where `instanceof` on a union is tedious:
Expand Down Expand Up @@ -162,12 +167,30 @@ Eight classes that a `@throws` tag named but no package exported were promoted t
| `CursorAlreadyAdvancedError` | a step reusing an already-invoked `next()`/`fork()` continuation |
| `EndOfStreamError` | a `BufferedSource` read that required more bytes than the source delivered |

**The two redirect errors carry redacted messages and raw properties, and the split is deliberate.**
`SchemeDowngradeError.message` and `NonReplayableBodyError.message` name their URLs in the redacted
form `OBS-11`/`OBS-12` define — userinfo as `***:***@`, every non-allow-listed query value as `***` —
because a message is rendered by every logger, every `cause` chain and every consumer `console.error`,
and `http.redirect.rejected` hands the error straight to `LogEvent.cause()`. The unredacted URLs stay
on `fromUrl` / `toUrl` / `targetUrl`, which is where program code reads them:

```ts
try {
await client.send(request);
} catch (error) {
if (error instanceof SchemeDowngradeError) {
console.error(error.message); // https://***:***@api.example.com/v1?token=***
retargetTo(error.toUrl); // the real URL, for code rather than for a log
}
}
```

Two more joined them on the same date, both from `XCUT-8`'s "never fabricate a successful exception":

| Error | Raised by |
|---|---|
| `HttpStatusValidationError` | `new HttpStatusError(status, …)` when `status` is not an integer in 400–599. The constructor validated nothing before, so a consumer could build an `HttpStatusError` claiming a `200`. `toHttpError` is the total form — it returns `null` instead of throwing |
| `RetryDiscardedResponseError` | the retry engine's suppressed trail, for a response it discarded whose status is outside 400–599. Reachable only if you widen `RetrySettings.retryableStatuses` to include a non-error code; the trail previously said `HttpStatusError` for it, which claimed an HTTP failure that had not happened |
| `RetryDiscardedResponseError` | the retry engine's prior-attempt trail (`retryAttempts()`), for a response it discarded whose status is outside 400–599. Reachable only if you widen `RetrySettings.retryableStatuses` to include a non-error code; the trail previously said `HttpStatusError` for it, which claimed an HTTP failure that had not happened |

All of them descend from `DexpaceError`, so the broad catch works too.

Expand Down
5 changes: 3 additions & 2 deletions docs/sdk-documentation/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ const options = RequestOptions.newBuilder()
`RequestOptions.EMPTY` is the shared no-op instance. A step reads it as `ctx.options`, and a
transport receives it as `send()`'s second argument. Both range checks are the **full** range, not
only the lower bound: `maxRetries` rejects anything that is not a non-negative integer, and
`timeoutMs` rejects zero, negatives, `Infinity` and `NaN` alike (`HTTP-35`). A fractional
`timeoutMs` is accepted — a timeout is a duration, not a count.
`timeoutMs` rejects zero, negatives, `Infinity`, `NaN`, a fractional value and anything above
`2**32 - 1` (`HTTP-35`) — the range is `AbortSignal.timeout()`'s, the only one a transport can
honor.

`auth` on the builder is the **per-call** auth tier, the highest-precedence one; see
[`auth.md`](./auth.md).
Expand Down
Loading
Loading