Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@dexpace/core": minor
---

Add the configuration subsystem and the shared platform primitives (Phase 7a): the layered `Configuration` model with its `ConfigurationBuilder`, substitutable env/property seams, never-throw typed accessors, copy-on-write `derive`, the process-wide global slot, and the well-known `CFG_KEY_*` constants; the injectable `Clock` seam and `defaultClock`; RFC 1123 `formatHttpDate`/`parseHttpDate`; the shared `isRetryableStatus`/`RETRYABLE_STATUSES` classifier; `randomUuid`; the `ProxyOptions` model with `createProxyOptions`, `formatProxyOptions`, `shouldBypassProxy`, and `resolveProxyOptions`; and the `BuildInfo` descriptor behind `getBuildInfo`.

`@dexpace/core`'s own version is now compiled in at build time by `scripts/gen-version.mjs`, which the package's `prebuild` step runs — so a runtime-emitted identifier reports the real version rather than an `unknown` placeholder, with no runtime `package.json` read on any runtime.
61 changes: 61 additions & 0 deletions .changeset/2026-08-27-configuration-review-pass-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
"@dexpace/core": minor
---

Phase 7a review pass 2 (adversarial). Fourteen defects found by enumerating boundaries, failure paths, and
lifetimes against the running code. The public API surface is unchanged — `etc/core.api.md` is
byte-identical — but several of these change observable behavior, so they are recorded here.

Security and availability:

- `shouldBypassProxy` no longer compiles bypass globs to a regular expression. Translating `*` to `.*`
produced adjacent unanchored runs, and a non-matching host then drove catastrophic backtracking: the
operator-supplied `NO_PROXY` entry `*a*a*a*a*a*a*a*a*a*b` against a 60-character host blocked the event
loop for 38 seconds. A two-pointer wildcard walk replaces it — 0.02ms on that case, `O(pattern × text)` at
worst (CFG-23).
- `getBuildInfo().identityTokens` is now header-safe at its source. The runtime identity is read from
ambient values (`process.version`, `Deno.version.deno`, `navigator.userAgent`) that were returned
untrimmed and unvalidated, so a single non-ASCII byte in a browser `navigator.userAgent` made the default
`clientIdentityStep` reject **every** outbound request with a `HeaderValidationError`. An unusable value
now falls back to `unknown` (CFG-36, RECOV-33, NFR-15).
- `RETRYABLE_STATUSES` is genuinely immutable. The `ReadonlySet` type is compile-time only and
`Object.freeze` does not seal a `Set`'s internal slots, so `(RETRYABLE_STATUSES as Set<number>).add(418)`
succeeded and permanently rewrote the process-wide retry classifier for the whole program. `add`, `delete`,
and `clear` now throw (CFG-35, RETRY-1).

Correctness:

- `resolveProxyOptions` honors an explicitly written default port. The WHATWG URL parser normalizes a
special scheme's default port to the empty string, so `HTTP_PROXY=http://proxy:80` and
`HTTPS_PROXY=https://proxy:443` — the two most common proxy configurations there are — both resolved to
`null` and routed direct. CFG-25 bans *guessing* an absent port, not honoring one the operator wrote; a
URL with no port at all is still rejected (CFG-25).
- `resolveProxyOptions` no longer throws a `URIError` on a literal `%` in proxy credentials. The
percent-decode sat outside the parse `try`, and an un-encoded password containing `%` is ordinary operator
input (CFG-24).
- The layered lookup is total against any seam. A `Record`-backed source — `process.env` included — resolves
a key named `__proto__`, `constructor`, or `toString` through `Object.prototype`, so `getString` returned a
*function* typed as `string | undefined` and `getInt`/`getBoolean`/`getDuration` died on a raw `TypeError`.
A seam that throws escaped unwrapped through the same accessors. Both now fall through as "this layer
supplies nothing" (CFG-5, CFG-6, CFG-7, CFG-11).
- `Clock.sleep` rejects a duration above `2 ** 31 - 1` ms instead of firing almost immediately. `setTimeout`
silently clamps a larger delay to `1`, so `sleep(2 ** 31)` returned in 7ms rather than waiting 24.8 days —
an overflowed retry backoff became no backoff at all (CFG-17).
- `Clock.sleep(0)` yields to the event loop rather than only to the microtask queue. The previous
`Promise.resolve()` short-circuit let a zero-backoff loop spin 4.1 million times in 300ms without a pending
`setTimeout(fn, 0)` ever running (CFG-17).
- `formatHttpDate` rejects an instant outside the four-digit-year span RFC 1123 renders. `padStart(4, '0')`
emitted the malformed `00-1` for year −1 and `275760` for `Date`'s upper limit, neither of which survived a
round-trip back through `parseHttpDate` (CFG-29).
- The proxy port accepts only a bare run of decimal digits. Bare `Number()` also read `0x10` as port 16,
`1e2` as 100, `0b11` as 3, and `80.0`/`+80`, silently connecting to a port the operator never wrote
(CFG-25).
- An IPv6 proxy address resolves to the same bare form from either configuration tier, rather than bracketed
from the environment URL and bare from the system property (CFG-22, CFG-24).
- An empty user name means no credentials on both tiers, so a blank `https.proxyUser` no longer fabricates a
masked `***:***@` for a proxy that has none (CFG-24).
- `randomUuid` names its missing dependency when a runtime exposes no global WebCrypto, instead of reporting
`TypeError: Cannot read properties of undefined (reading 'getRandomValues')` (CFG-32).
- `setGlobalConfiguration` rejects a present-but-wrong value rather than only a null one, matching every
other CFG-37 guard in the module (CFG-37).
- `Configuration.getInt` normalizes `-0` onto `0`.
30 changes: 30 additions & 0 deletions .changeset/2026-08-27-configuration-review-pass-3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
"@dexpace/core": patch
---

Phase 7a review pass 3 (readability and convention). No behavior changes. Two public parameter names change,
which is the whole of the `etc/core.api.md` diff:

- `Clock.sleep(ms, signal)` becomes `Clock.sleep(durationMs, signal)`. A bare `ms` is a unit with no concept
attached, and the report carried it two lines above `composeSignal(userSignal, timeoutMs)` — the same
package stating the same kind of quantity two different ways
(`docs/knowledge/naming-conventions.md:36`).
- `Configuration.getDuration(key, fallback)` becomes `getDuration(key, fallbackMs)`. The accessor returns
and accepts milliseconds, and said so only in prose while its own private collaborator is named
`parseDurationMs`.

Positional callers are unaffected; only the name shown in editor hints and the emitted `.d.ts` changes.

The rest of the pass is documentation and test strength, with nothing observable to a consumer. The
documentation fixes worth naming, because each was a comment that had stopped matching its code:

- `Clock.sleep`'s TSDoc claimed the timer was cleared "on both the resolve and the abort path". Only the
abort path clears a timer; the resolve path detaches the abort listener.
- `randomUuid` carried a comment describing an `unknown` widening that no longer exists, and pointed at
`setGlobalConfiguration` for a shape it no longer shares.
- `composeHeaders`'s doc block sat on the interface declared above it, so the function was undocumented and
the interface was described as if it wrote headers.
- Every `Configuration` and `ConfigurationBuilder` `@throws` said "when `x` is absent"; every guard is a
`typeof` shape check, which is what the module's own comment says they are.
- The package barrel justified not exporting `deepEqual`/`deepHash` partly on "in-package consumers import
the module directly". They have no in-package consumer, which `docs/open-items.md` G16 already recorded.
Loading
Loading