Skip to content

flowreel 0.4.0

Choose a tag to compare

@github-actions github-actions released this 22 Oct 08:02
· 11 commits to main since this release
9e14834

Install: npm install --save-dev @confidee/flowreel@0.4.0 (npm, published by the release workflow with a provenance attestation).

Every request flowreel makes is bounded, and a failure says what it
means. The Microsoft 365 driver never follows a redirect and says whether
a failed send may have gone out; a mail wait rides out throttling and
brief network drops; the sign-in, session and persona requests have a time
limit; and the shared summary no longer carries Playwright's call log,
which could hold a live session cookie. Two security fixes: upgrade if
you use the Microsoft 365 driver or share summary.md.

Security

  • GraphMailDriver no longer follows redirects (#22). Affects 0.3.0
    and earlier. The driver's requests used fetch's default
    redirect: "follow", and the fetch standard keeps the body on a 307 or
    308: a 307 on sendMail sent the mail a second time (and reported the
    send as successful), a redirect loop sent it up to 21 times, and a 307
    on the token request re-sent the form body, client_secret included,
    to the redirect target. Every Graph and token request now uses
    redirect: "manual": a 3xx is a GraphHttpError that sent nothing
    (mayHaveSent: false), never followed and never retried. A project
    that passes its own fetch must make one attempt per call and never
    follow redirects.
  • summary.md, summary.json and summary.lines no longer carry
    Playwright's call log (#26).
    Affects 0.3.0 and earlier. The summary
    reporter copied up to 4000 characters of every error, and a failed
    Playwright request's error goes on, after its first line, with a
    "Call log" that names the full URL with its query string, every
    request header (the persona's session cookie, a CSRF token) and a
    response's set-cookie (on a stalled sign-in, the fresh session
    cookie): the file flowreel tells you to share could carry a live
    session. The summary now keeps an error up to its call log and drops
    every http(s):// URL's query string and fragment; the HTML report
    (where a toHaveURL failure that differs only in its query still shows
    the difference) and traces keep
    the whole error and stay credential-bearing. flowreel's own sign-in
    seam and session probes rethrow the first line of Playwright's error
    only, without a cause, and so do the template and example adapters. If
    you shared a summary.md from a run that failed on a request, check it
    for a Call log: block and revoke the sessions it names (a completed
    teardown already revoked them).

Added

  • requestTimeoutMs for GraphMailDriver (@confidee/flowreel/mail-graph):
    every Graph and token request, from the connection to the last byte of
    the body, gets at most this long (default 30 s), and inside waitFor no
    more than the wait has left. A whole number of milliseconds from 1 to
    2147483647; anything else is a ConfigError.

  • GraphRequestTimeoutError from @confidee/flowreel/mail-graph
    (timeoutMs, plus operation, mayHaveSent and clientRequestId from
    GraphRequestError): what a request that got no answer throws. A
    sendMail or a reply's send sets mayHaveSent, says the mail may have
    been sent and names its client-request-id; it is never retried.

  • GraphRequestError from @confidee/flowreel/mail-graph: the base of
    every error a Graph or token request ends in (operation,
    mayHaveSent, always a boolean, and clientRequestId), with a
    one-line message. A bad 2xx answer (a body that is not JSON, a token
    answer without access_token) throws it as is, with mayHaveSent
    false. GraphHttpError, GraphRequestTimeoutError and the new
    GraphTransportError extend it (#17).

  • GraphTransportError from @confidee/flowreel/mail-graph (code, such
    as ECONNRESET or UND_ERR_SOCKET; no cause): what a failure below
    HTTP throws, instead of a plain Error. The message is unchanged:
    Graph <operation> failed: fetch failed (ECONNREFUSED) (#17).

  • GraphHttpError takes an optional fifth argument, a details object
    (mayHaveSent, clientRequestId, requestId, date, traceId,
    correlationId, code), and exposes those fields; its four positional
    arguments are unchanged (#17, #19).

  • Request ids for GraphMailDriver: every Graph and token request carries
    a fresh random client-request-id, and the token request sends
    return-client-request-id: true. Every error keeps it; a
    GraphHttpError also keeps Graph's request-id and date, and a token
    error Entra's trace_id and correlation_id, when GUID-shaped. A token
    error's message always names its trace_id and correlation_id on the
    detail line; a Graph request's message names its ids only when the mail
    may have been sent (For Microsoft support: client-request-id …) (#19).

  • requestTimeoutMs for both seam HTTP clients
    (@confidee/flowreel/rails-inertia), 30 s by default:
    seamHttpFromRequest(request, { requestTimeoutMs }) passes it as
    Playwright's per-call timeout (inside @playwright/test a request
    context otherwise has none), and seamHttpFromFetch(baseURL, options)
    now also takes { fetch, requestTimeoutMs, setTimeout, clearTimeout }
    besides the fetch function it took before. A seam request that runs
    out is aborted and throws
    Seam <METHOD> <path> got no answer within 30 s; raise requestTimeoutMs if a job drain legitimately takes longer
    (#20).

  • requestTimeoutMs for flowreel's session requests (#24), 30 s by
    default, validated like every flowreel request bound (a whole number of
    milliseconds from 1 to 2147483647, else a ConfigError naming the
    option): on seamSignIn's options (@confidee/flowreel/rails-inertia),
    on SessionProtocol (isSessionSignedOut; revokeSession, which never
    throws, returns revoked: false naming the option, so teardown warns
    loudly), and on
    createPersonaApis and tourFixtures() for every persona request. It
    is the timeout of each request context they open.

  • requestFailure(error) from @confidee/flowreel/core: what may be
    said of a failed Playwright request, the first line of its message
    without a URL's query string, at most 300 characters. For a project's
    own adapter (#26).

  • /flowreel:doctor advises when e2e/flowreel/adapter.ts opens request
    contexts without a timeout (an adapter scaffolded before 0.4.0). It
    only reads the file, with grep.

Changed

  • GraphMailDriver.waitFor: a temporary network failure on a mail read
    or the token request is now re-polled instead of ending the wait (#23):
    ECONNRESET, UND_ERR_SOCKET, EPIPE, ECONNABORTED, ETIMEDOUT,
    EAI_AGAIN and undici's connect, headers and body timeouts (an
    AggregateError only when every inner code is listed), with the
    token's back-off (5 s, then 10 s, 20 s … up to 60 s, never past the
    wait's end, on top of the wait's own poll interval), counted apart for the token request and the reads. Only
    once that endpoint has answered the driver: at first contact the same
    error still ends the wait, so a firewall or broken DNS fails fast. Any
    other code, or none, stays fatal (ECONNREFUSED, ENOTFOUND, TLS, a
    custom fetch that throws a plain error), nothing that may have sent
    mail is ever re-polled, and nothing is retried outside waitFor. A
    wait that times out reports the step, the code and the count only
    (Last observed: Graph read failed (ECONNRESET), 3 in a row, no mailbox read completed in this wait, retrying).
  • GraphMailDriver.waitFor re-polls a 429, 503 or 504 only on a
    GET read; a write's never was reachable there, and now cannot be.
  • The persona request contexts (createPersonaApis, the tour's as()
    and personaApis) now have a 30 s timeout of their own, which
    overrides the project's actionTimeout for them (15 s under flowreel's
    createConfig); a request's own timeout still wins. Set
    requestTimeoutMs on tourFixtures() for a longer one.
  • Global teardown (signOutAll) now survives an AppAdapter.signOut
    that throws, against its contract: that persona gets the loud "could
    not sign out" warning (with the first line of the error) and the other
    personas are still signed out. Before, one throw skipped every later
    persona.
  • seamSignIn and isSessionSignedOut throw
    Test sign-in (POST <path>) failed: … and
    Session probe GET <path> failed: … with the first line of
    Playwright's error, instead of Playwright's error itself (#26).
  • The template adapter (/flowreel:init) and the example's set a
    REQUEST_TIMEOUT_MS (30 s) on every request context and on SESSION,
    describe a failed request with requestFailure(), and health()
    returns a failed request's first line instead of throwing; the
    template ships unit/adapter.spec.ts, which checks both. /flowreel:init
    never rewrites an existing adapter: copy these lines into one you
    scaffolded before (the doctor points it out).
  • GraphMailDriver: a failed sendMail or reply send now says whether
    the mail may have been sent from everything observed, not only a
    timeout. Any 5xx but 509 (503 included) and any transport failure that
    does not prove the request never left (a reset or closed socket,
    terminated, EPIPE, any code on a read or write, an unknown or
    missing code) set mayHaveSent; a 3xx, a 4xx (429 included), 509 and
    a DNS, connect or TLS failure (a getaddrinfo or connect syscall, or
    a listed code on an error without a syscall) do not. An error status
    whose body does not arrive within the bound stays a may-have-sent
    timeout. A 2xx status now resolves the send at once, even when its empty
    body then fails or never ends. Nothing is retried (#17).
  • GraphMailDriver.waitFor: a token request that is throttled or meets a
    server error (HTTP 429, 500, 502, 503, 504, or OAuth error
    temporarily_unavailable) is now re-polled instead of ending the wait,
    after the response's Retry-After exactly, else 5 s, then 10 s, 20 s …
    up to 60 s, never past the wait's end. A token request that got no
    answer gets the same back-off. Credentials failures stay fatal (token
    transport errors: see #23 above), and nothing is retried outside
    waitFor (#18). Only consecutive failures double the back-off: a token
    obtained resets it.
  • GraphMailDriver.health(): a request that could not connect (a DNS,
    connect or TLS failure) now points at the network path, not the
    credentials.
  • GraphMailDriver: an error code from a response body (Graph's
    error.code, Entra's error) is shown only when code-shaped, else as
    UnknownError; and the client secret is redacted before the message is
    joined into one line, so a secret split across a line break is not
    reassembled.

Fixed

  • flowreel's session requests could hang (#24). The sign-in seam, the
    session probes and sign-out, the persona API contexts and the template
    adapter opened request contexts without a timeout, so they got
    Playwright's default: 30 s in global setup and teardown, the project's
    actionTimeout in a test (15 s under createConfig), and none at all
    with actionTimeout: 0 or without flowreel's config, where a server
    that never answered, or a body that never ended, hung the run until
    the test or fixture timeout. Every one of them is now bounded by
    requestTimeoutMs (30 s by default), body included.
  • A Microsoft Graph request that never answered could stall a run (#16).
    A hung request ended only at Node's fetch limit of 300 s, a body that
    trickled in never ended, a hung token request held up every later call
    until it ended, and waitFor's own timeoutMs could not end a request
    in flight. Now every request is bounded, a wait ends at its timeoutMs
    (a read that got no answer is re-polled, and after the first attempt
    none starts once no time is left), a timed-out token request is dropped
    so the next call asks again, health() points at the network instead
    of the credentials when a request got no answer, and a transport error
    keeps its cause's error code (fetch failed (ECONNREFUSED)) and nothing
    else of it.
  • GraphMailDriver: a 2xx answer whose body cannot be read now throws the
    named, redacted transport error (Graph <operation> failed: …) instead of
    the raw stream error, which could echo the access token.
  • GraphMailDriver: a token answer whose body is JSON null now fails with
    "no access_token returned" instead of a bare TypeError.
  • GraphMailDriver redacts the client secret and the access token in their
    URI-encoded and form-encoded forms too, so a transport error that echoes
    the token request's body no longer carries the secret.