Skip to content

Domain-model input validation: prototype keys, invalid Date, fractional timeoutMs, lone surrogates, frozen getAll #76

Description

@Wahbeh-Mohammad

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:coreCore HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-*type:bugDefect in shipped behavior

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions