Part of the audit remediation umbrella #67. Milestone 4. Severity: HIGH (first item), MEDIUM (rest). Requirements: SEAM-27, HTTP-50, HTTP-35, HTTP-5, typed-errors rule.
Current SDK behavior
- Prototype keys satisfy a path placeholder.
substitutePathParams
(packages/core/src/seams/operation.ts:106) reads pathParams?.[name] on a plain object.
buildRequest('https://api.example.com', {method:'GET', pathTemplate:'/users/{constructor}', pathParams:{}})
builds https://api.example.com/users/function%20Object%28%29%20%7B%20%5Bnative%20code%5D%20%7D
instead of throwing OperationAssemblyError. {toString} and {__proto__} behave the same.
- Invalid
Date goes on the wire. RequestConditionsBuilder.ifModifiedSince /
ifUnmodifiedSince (http/request-conditions.ts:204) accept any Date. toRfc1123 (:30)
is date.toUTCString(). An invalid Date renders If-Modified-Since: Invalid Date, which
passes HTTP-18 outbound validation.
- Fractional
timeoutMs throws at send. RequestOptionsBuilder.timeoutMs(1.5) is accepted
by design (http/request-options.ts:175-186, pinned by a test). composeSignal
(seams/transport.ts:89) passes it to AbortSignal.timeout(), which throws
RangeError: The value of "delay" is out of range. It must be an integer. inside the
transport. Same for values above 4294967295. composeSignal has no @throws.
- Lone surrogate throws
URIError. encodeRfc3986Component (http/rfc3986.ts:13) calls
encodeURIComponent. QueryParams.newBuilder().add('c', '\uD800').build().encode() throws
URIError: URI malformed, outside the DexpaceError tree, with no @throws on encode() or
equals(). buildRequest surfaces it for query and path params.
getAll on an absent name returns an unfrozen array. headers.ts:133,136 and
query-params.ts:141,144 return a fresh []; the TSDoc promises a frozen list. No test
asserts the present-name array is frozen.
Headers.equals has no direct test. headers.ts:184; exercised only through
Request.equals.
TeeSink accepts a fractional tapLimit. io/tee-sink.ts:34-37 checks >= 0 only; the
fault surfaces later in ByteQueue.copyTo.
Expected behavior
A builder or factory rejects an invalid input at the call site that supplied it, with an
error from the DexpaceError tree. A value the model accepts does not throw later in another
package. A getter that promises a frozen list returns one on every path. Public methods that
throw name the error class in @throws.
Notes and leads
- Path params:
Object.hasOwn(pathParams, name), or copy into a null-prototype object.
- Dates: reject
Number.isNaN(date.getTime()) in both setters.
timeoutMs: decide once. Reject non-integer and > 2**32 - 1 in the setter (update the TSDoc
and the existing fractional-accepting test), or round with Math.ceil and clamp in
composeSignal. The setter is the HTTP-35-faithful place.
- Surrogates:
String.prototype.isWellFormed() exists on the engines.node >= 20.3 floor.
Check at add() / set() time and throw the existing query-param validation error; or catch
in encodeRfc3986Component and rethrow a DexpaceError with {cause}.
getAll: return one shared frozen empty array.
TeeSink: Number.isInteger(tapLimit) || tapLimit === Number.POSITIVE_INFINITY.
- Tests: one per bullet, in the colocated
*.test.ts; {constructor} with empty pathParams;
Headers.equals direct cases (order of names, order of values, subset).
api:local after the @throws edits. Patch changeset for @dexpace/core.
Part of the audit remediation umbrella #67. Milestone 4. Severity: HIGH (first item), MEDIUM (rest). Requirements: SEAM-27, HTTP-50, HTTP-35, HTTP-5, typed-errors rule.
Current SDK behavior
substitutePathParams(
packages/core/src/seams/operation.ts:106) readspathParams?.[name]on a plain object.buildRequest('https://api.example.com', {method:'GET', pathTemplate:'/users/{constructor}', pathParams:{}})builds
https://api.example.com/users/function%20Object%28%29%20%7B%20%5Bnative%20code%5D%20%7Dinstead of throwing
OperationAssemblyError.{toString}and{__proto__}behave the same.Dategoes on the wire.RequestConditionsBuilder.ifModifiedSince/ifUnmodifiedSince(http/request-conditions.ts:204) accept anyDate.toRfc1123(:30)is
date.toUTCString(). An invalidDaterendersIf-Modified-Since: Invalid Date, whichpasses HTTP-18 outbound validation.
timeoutMsthrows at send.RequestOptionsBuilder.timeoutMs(1.5)is acceptedby design (
http/request-options.ts:175-186, pinned by a test).composeSignal(
seams/transport.ts:89) passes it toAbortSignal.timeout(), which throwsRangeError: The value of "delay" is out of range. It must be an integer.inside thetransport. Same for values above 4294967295.
composeSignalhas no@throws.URIError.encodeRfc3986Component(http/rfc3986.ts:13) callsencodeURIComponent.QueryParams.newBuilder().add('c', '\uD800').build().encode()throwsURIError: URI malformed, outside theDexpaceErrortree, with no@throwsonencode()orequals().buildRequestsurfaces it for query and path params.getAllon an absent name returns an unfrozen array.headers.ts:133,136andquery-params.ts:141,144return a fresh[]; the TSDoc promises a frozen list. No testasserts the present-name array is frozen.
Headers.equalshas no direct test.headers.ts:184; exercised only throughRequest.equals.TeeSinkaccepts a fractionaltapLimit.io/tee-sink.ts:34-37checks>= 0only; thefault surfaces later in
ByteQueue.copyTo.Expected behavior
A builder or factory rejects an invalid input at the call site that supplied it, with an
error from the
DexpaceErrortree. A value the model accepts does not throw later in anotherpackage. A getter that promises a frozen list returns one on every path. Public methods that
throw name the error class in
@throws.Notes and leads
Object.hasOwn(pathParams, name), or copy into a null-prototype object.Number.isNaN(date.getTime())in both setters.timeoutMs: decide once. Reject non-integer and> 2**32 - 1in the setter (update the TSDocand the existing fractional-accepting test), or round with
Math.ceiland clamp incomposeSignal. The setter is the HTTP-35-faithful place.String.prototype.isWellFormed()exists on theengines.node >= 20.3floor.Check at
add()/set()time and throw the existing query-param validation error; or catchin
encodeRfc3986Componentand rethrow aDexpaceErrorwith{cause}.getAll: return one shared frozen empty array.TeeSink:Number.isInteger(tapLimit) || tapLimit === Number.POSITIVE_INFINITY.*.test.ts;{constructor}with emptypathParams;Headers.equalsdirect cases (order of names, order of values, subset).api:localafter the@throwsedits. Patch changeset for@dexpace/core.