• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / roku-deploy / 26912207197

03 Jun 2026 08:50PM UTC coverage: 89.655% (-10.3%) from 100.0%
26912207197

Pull #124

github

web-flow
Merge a26572902 into 146a489c5
Pull Request #124: v4

461 of 528 branches covered (87.31%)

Branch coverage included in aggregate %.

296 of 308 new or added lines in 10 files covered. (96.1%)

59 existing lines in 3 files now uncovered.

735 of 806 relevant lines covered (91.19%)

31.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

11.84
/src/fetch.ts
1
import * as crypto from 'crypto';
3✔
2

3
// Module seam for `fetch` so tests can stub it. On Node 18, `fetch` is a lazy
4
// getter on `globalThis` (not an own property), so `sinon.stub(globalThis, 'fetch')`
5
// fails there — routing calls through this object gives a regular, stubbable export.
6
export const httpClient = {
3✔
7
    fetch: globalThis.fetch?.bind(globalThis)
9✔
8
};
9

10
/**
11
 * Issue an HTTP request with digest authentication.
12
 * Performs the two-step challenge/response dance: the first request
13
 * collects the `WWW-Authenticate` challenge, the second sends a computed
14
 * `Authorization` header. Response bodies are not consumed — callers get
15
 * the raw `Response` and inspect status/headers only.
16
 */
17
export async function fetchWithDigest(
3✔
18
    url: string,
19
    init: RequestInit & { method: string; username: string; password: string; timeout: number }
20
): Promise<Response> {
UNCOV
21
    const { username, password, timeout, ...fetchInit } = init;
×
UNCOV
22
    const method = fetchInit.method.toUpperCase();
×
23

24
    // Step 1 — issue the request unauthenticated to collect the challenge.
UNCOV
25
    const step1 = await fetchWithTimeout(url, fetchInit, timeout);
×
UNCOV
26
    if (step1.status !== 401) {
×
UNCOV
27
        return step1;
×
28
    }
UNCOV
29
    const wwwAuth = step1.headers.get('www-authenticate');
×
UNCOV
30
    if (!wwwAuth) {
×
UNCOV
31
        return step1;
×
32
    }
33

34
    // Step 2 — compute the digest response and retry.
UNCOV
35
    const challenge = parseDigestChallenge(wwwAuth);
×
UNCOV
36
    const uri = new URL(url).pathname;
×
UNCOV
37
    const authorization = buildDigestAuthorization({
×
38
        username: username,
39
        password: password,
40
        method: method,
41
        uri: uri,
42
        challenge: challenge
43
    });
UNCOV
44
    return fetchWithTimeout(url, {
×
45
        ...fetchInit,
46
        headers: { ...fetchInit.headers, Authorization: authorization }
47
    }, timeout);
48
}
49

50
function fetchWithTimeout(url: string, init: RequestInit, timeout: number): Promise<Response> {
UNCOV
51
    const controller = new AbortController();
×
UNCOV
52
    const timer = setTimeout(() => controller.abort(), timeout);
×
UNCOV
53
    return httpClient.fetch(url, { ...init, signal: controller.signal })
×
UNCOV
54
        .finally(() => clearTimeout(timer));
×
55
}
56

57
//parse the comma-separated key/value pairs out of a `WWW-Authenticate: Digest ...` header. Values may be bare or double-quoted.
58
export function parseDigestChallenge(header: string): Record<string, string> {
3✔
UNCOV
59
    const out: Record<string, string> = {};
×
UNCOV
60
    const body = header.replace(/^Digest\s+/i, '');
×
UNCOV
61
    const re = /([a-zA-Z]+)=(?:"((?:[^"\\]|\\.)*)"|([^,]+))/g;
×
62
    let m: RegExpExecArray | null;
UNCOV
63
    while ((m = re.exec(body)) !== null) {
×
UNCOV
64
        out[m[1].toLowerCase()] = m[2] ?? m[3].trim();
×
65
    }
UNCOV
66
    return out;
×
67
}
68

69
function md5(input: string): string {
UNCOV
70
    return crypto.createHash('md5').update(input).digest('hex');
×
71
}
72

73
//build an RFC 2617 `Authorization: Digest ...` header from a parsed challenge.
74
export function buildDigestAuthorization(params: {
3✔
75
    username: string;
76
    password: string;
77
    method: string;
78
    uri: string;
79
    challenge: Record<string, string>;
80
}): string {
UNCOV
81
    const { username, password, method, uri, challenge } = params;
×
UNCOV
82
    const realm = challenge.realm ?? '';
×
UNCOV
83
    const nonce = challenge.nonce ?? '';
×
UNCOV
84
    const qop = challenge.qop;
×
UNCOV
85
    const algorithm = (challenge.algorithm ?? 'MD5').toUpperCase();
×
UNCOV
86
    const cnonce = crypto.randomBytes(8).toString('hex');
×
UNCOV
87
    const nc = '00000001';
×
88

UNCOV
89
    const ha1 = algorithm === 'MD5-SESS'
×
90
        ? md5(`${md5(`${username}:${realm}:${password}`)}:${nonce}:${cnonce}`)
91
        : md5(`${username}:${realm}:${password}`);
UNCOV
92
    const ha2 = md5(`${method}:${uri}`);
×
UNCOV
93
    const response = qop
×
94
        ? md5(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`)
95
        : md5(`${ha1}:${nonce}:${ha2}`);
96

UNCOV
97
    const parts = [
×
98
        `username="${username}"`,
99
        `realm="${realm}"`,
100
        `nonce="${nonce}"`,
101
        `uri="${uri}"`,
102
        `algorithm=${algorithm}`,
103
        `response="${response}"`
104
    ];
UNCOV
105
    if (qop) {
×
UNCOV
106
        parts.push(`qop=${qop}`, `nc=${nc}`, `cnonce="${cnonce}"`);
×
107
    }
UNCOV
108
    if (challenge.opaque) {
×
UNCOV
109
        parts.push(`opaque="${challenge.opaque}"`);
×
110
    }
UNCOV
111
    return `Digest ${parts.join(', ')}`;
×
112
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc