depvitals 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -54
- package/dist/cli/entry.d.ts +59 -0
- package/dist/cli/entry.d.ts.map +1 -0
- package/dist/cli/entry.js +95 -0
- package/dist/cli/entry.js.map +1 -0
- package/dist/cli/index.js +14 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/notifier/eligibility.d.ts +65 -0
- package/dist/cli/notifier/eligibility.d.ts.map +1 -0
- package/dist/cli/notifier/eligibility.js +123 -0
- package/dist/cli/notifier/eligibility.js.map +1 -0
- package/dist/cli/notifier/registryLookup.d.ts +111 -0
- package/dist/cli/notifier/registryLookup.d.ts.map +1 -0
- package/dist/cli/notifier/registryLookup.js +211 -0
- package/dist/cli/notifier/registryLookup.js.map +1 -0
- package/dist/cli/notifier/updateCache.d.ts +101 -0
- package/dist/cli/notifier/updateCache.d.ts.map +1 -0
- package/dist/cli/notifier/updateCache.js +352 -0
- package/dist/cli/notifier/updateCache.js.map +1 -0
- package/dist/cli/notifier/updateCheck.d.ts +88 -0
- package/dist/cli/notifier/updateCheck.d.ts.map +1 -0
- package/dist/cli/notifier/updateCheck.js +166 -0
- package/dist/cli/notifier/updateCheck.js.map +1 -0
- package/dist/cli/notifier/versionCompare.d.ts +48 -0
- package/dist/cli/notifier/versionCompare.d.ts.map +1 -0
- package/dist/cli/notifier/versionCompare.js +89 -0
- package/dist/cli/notifier/versionCompare.js.map +1 -0
- package/dist/cli/runCli.d.ts +2 -0
- package/dist/cli/runCli.d.ts.map +1 -1
- package/dist/cli/runCli.js +30 -3
- package/dist/cli/runCli.js.map +1 -1
- package/dist/core/analyzeProject.d.ts +44 -6
- package/dist/core/analyzeProject.d.ts.map +1 -1
- package/dist/core/analyzeProject.js +110 -11
- package/dist/core/analyzeProject.js.map +1 -1
- package/dist/detectors/eslintManifestConfig.d.ts +111 -0
- package/dist/detectors/eslintManifestConfig.d.ts.map +1 -0
- package/dist/detectors/eslintManifestConfig.js +348 -0
- package/dist/detectors/eslintManifestConfig.js.map +1 -0
- package/dist/detectors/eslintRcJsonConfig.d.ts +85 -0
- package/dist/detectors/eslintRcJsonConfig.d.ts.map +1 -0
- package/dist/detectors/eslintRcJsonConfig.js +188 -0
- package/dist/detectors/eslintRcJsonConfig.js.map +1 -0
- package/dist/detectors/jestManifestConfig.d.ts +76 -0
- package/dist/detectors/jestManifestConfig.d.ts.map +1 -0
- package/dist/detectors/jestManifestConfig.js +292 -0
- package/dist/detectors/jestManifestConfig.js.map +1 -0
- package/dist/reporters/terminalReporter.js +27 -1
- package/dist/reporters/terminalReporter.js.map +1 -1
- package/dist/scanner/moduleSpecifiers.d.ts +9 -1
- package/dist/scanner/moduleSpecifiers.d.ts.map +1 -1
- package/dist/scanner/moduleSpecifiers.js +2352 -173
- package/dist/scanner/moduleSpecifiers.js.map +1 -1
- package/dist/scanner/projectManifest.d.ts +14 -0
- package/dist/scanner/projectManifest.d.ts.map +1 -1
- package/dist/scanner/projectManifest.js +40 -0
- package/dist/scanner/projectManifest.js.map +1 -1
- package/dist/scanner/scriptUsage.d.ts +11 -5
- package/dist/scanner/scriptUsage.d.ts.map +1 -1
- package/dist/scanner/scriptUsage.js +320 -16
- package/dist/scanner/scriptUsage.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One bounded public lookup of the DepVitals `latest` dist-tag (Step 66).
|
|
3
|
+
*
|
|
4
|
+
* This is the **only** place in the whole product that may perform a network request, and the only
|
|
5
|
+
* thing it can ever ask for is the public dist-tag metadata of the `depvitals` package itself.
|
|
6
|
+
* Nothing about the analyzed project is used here: no project path, no manifest content, no
|
|
7
|
+
* dependency name, no version, no user data and no credential of any kind. Core analysis stays
|
|
8
|
+
* fully offline - this module lives under `src/cli/notifier/` and is never reachable from
|
|
9
|
+
* `core/`, `scanner/`, `detectors/` or `reporters/` (asserted by tests), and it stays unwired until
|
|
10
|
+
* the CLI integration step introduces the notifier orchestrator.
|
|
11
|
+
*
|
|
12
|
+
* Contract enforced by this module:
|
|
13
|
+
*
|
|
14
|
+
* 1. the URL is the fixed constant {@link NPM_DIST_TAGS_ENDPOINT}; nothing is appended to it and no
|
|
15
|
+
* caller-supplied string reaches it;
|
|
16
|
+
* 2. the whole fetch + body + parse operation is bounded by a short timeout, and the bound cannot
|
|
17
|
+
* be loosened past {@link UPDATE_LOOKUP_MAX_TIMEOUT_MS}; when the bound elapses the request is
|
|
18
|
+
* abandoned (the abort signal tears the request down) and the result is `null`;
|
|
19
|
+
* 3. a successful HTTP status is required, the body must stay at or below
|
|
20
|
+
* {@link MAX_UPDATE_RESPONSE_BYTES}, it must parse as a JSON object with an own `latest` string,
|
|
21
|
+
* and that string must be an exact stable version according to the single grammar owned by
|
|
22
|
+
* Step 63 (`versionCompare.parseStableVersion`);
|
|
23
|
+
* 4. **every** failure mode - DNS, TLS, abort/timeout, non-2xx status, empty/oversized/malformed
|
|
24
|
+
* body, body stream error, invalid version - yields `null`. The function never throws and never
|
|
25
|
+
* logs, because an optional convenience must not be able to break or delay a scan;
|
|
26
|
+
* 5. there are no retries: a failed lookup costs exactly one request and the caller falls back to
|
|
27
|
+
* "no notice", and the request carries no headers, so no npm token can leak to the registry.
|
|
28
|
+
*
|
|
29
|
+
* Testability: `fetch` and the abort-signal factory are injected, so the tests never touch the real
|
|
30
|
+
* network, never depend on a real DNS/TLS stack and can simulate a hanging request deterministically.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* The fixed public endpoint of the update check, exactly as mandated by the update-notifier
|
|
34
|
+
* contract. It is a constant so that no project-derived value can ever influence the request.
|
|
35
|
+
*/
|
|
36
|
+
export declare const NPM_DIST_TAGS_ENDPOINT = "https://registry.npmjs.org/-/package/depvitals/dist-tags";
|
|
37
|
+
/**
|
|
38
|
+
* Default hard bound for the complete request: one fetch plus its body and parse. Kept inside the
|
|
39
|
+
* 300-500 ms window agreed for the notifier, so a slow or unreachable registry can only ever cost a
|
|
40
|
+
* fraction of a second of an already finished scan.
|
|
41
|
+
*/
|
|
42
|
+
export declare const UPDATE_LOOKUP_TIMEOUT_MS = 400;
|
|
43
|
+
/**
|
|
44
|
+
* Absolute ceiling for any injected timeout. Callers (and tests) may shorten the bound but cannot
|
|
45
|
+
* turn the "short hard timeout" of the contract into an unbounded wait.
|
|
46
|
+
*/
|
|
47
|
+
export declare const UPDATE_LOOKUP_MAX_TIMEOUT_MS = 500;
|
|
48
|
+
/**
|
|
49
|
+
* Largest response body that is accepted. The real dist-tag document is a few dozen bytes, so the
|
|
50
|
+
* cap only ever rejects something hostile or unexpected; it is enforced while streaming, not after
|
|
51
|
+
* buffering an arbitrarily large payload.
|
|
52
|
+
*/
|
|
53
|
+
export declare const MAX_UPDATE_RESPONSE_BYTES: number;
|
|
54
|
+
/**
|
|
55
|
+
* The single stream method this lookup needs from a response body, i.e. a byte stream that can be
|
|
56
|
+
* read incrementally and released early.
|
|
57
|
+
*/
|
|
58
|
+
export type RegistryStreamReader = {
|
|
59
|
+
read(): Promise<{
|
|
60
|
+
done: boolean;
|
|
61
|
+
value: Uint8Array | undefined;
|
|
62
|
+
}>;
|
|
63
|
+
cancel(reason?: unknown): Promise<void>;
|
|
64
|
+
};
|
|
65
|
+
/** Response body shape: readable as a byte stream, or absent (`null`, e.g. for a 204). */
|
|
66
|
+
export type RegistryResponseBody = {
|
|
67
|
+
getReader(): RegistryStreamReader;
|
|
68
|
+
};
|
|
69
|
+
/** The only response property this lookup consults: the HTTP status plus an optional body. */
|
|
70
|
+
export type RegistryResponse = {
|
|
71
|
+
readonly status: number;
|
|
72
|
+
readonly body: RegistryResponseBody | null;
|
|
73
|
+
};
|
|
74
|
+
/** The only request property this lookup sends: the timeout/abort signal. Never headers or bodies. */
|
|
75
|
+
export type RegistryRequestInit = {
|
|
76
|
+
readonly signal: AbortSignal;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The injected `fetch` implementation. It is required rather than defaulted so that no accidental
|
|
80
|
+
* call inside this repository can reach the network: production callers pass the Node built-in
|
|
81
|
+
* `fetch`, tests pass a stub returning canned responses and canned failures.
|
|
82
|
+
*/
|
|
83
|
+
export type RegistryFetch = (endpoint: string, init: RegistryRequestInit) => Promise<RegistryResponse>;
|
|
84
|
+
/** Dependencies of one lookup. Every field is injected, so nothing ambient is read implicitly. */
|
|
85
|
+
export type RegistryLookupOptions = {
|
|
86
|
+
/** Transport to use, e.g. the Node built-in `fetch` bound by the caller. */
|
|
87
|
+
readonly fetchImpl: RegistryFetch;
|
|
88
|
+
/**
|
|
89
|
+
* Optional shorter bound in milliseconds, clamped into `[0, UPDATE_LOOKUP_MAX_TIMEOUT_MS]`.
|
|
90
|
+
* Defaults to {@link UPDATE_LOOKUP_TIMEOUT_MS}.
|
|
91
|
+
*/
|
|
92
|
+
readonly timeoutMs?: number;
|
|
93
|
+
/**
|
|
94
|
+
* Abort-signal factory, defaulting to `AbortSignal.timeout`. Tests inject a factory returning a
|
|
95
|
+
* controller signal (or an already-aborted signal) to exercise the timeout path without waiting.
|
|
96
|
+
*/
|
|
97
|
+
readonly createAbortSignal?: (timeoutMs: number) => AbortSignal;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Fetch the public `latest` dist-tag of the **DepVitals package itself**, at most once.
|
|
101
|
+
*
|
|
102
|
+
* The call is fully isolated: it never throws, never retries, never writes to stdout or stderr and
|
|
103
|
+
* never sends project-derived data or credentials. Any problem - DNS failure, TLS failure, abort or
|
|
104
|
+
* timeout, HTTP error status, empty, oversized, HTML or malformed body, missing `latest`, body
|
|
105
|
+
* stream error - results in `null`, which the caller must treat as "no update information".
|
|
106
|
+
*
|
|
107
|
+
* @param options - Injected transport and (optionally) a shorter timeout plus abort-signal factory.
|
|
108
|
+
* @returns the validated stable version string, or `null` for every other outcome.
|
|
109
|
+
*/
|
|
110
|
+
export declare function fetchLatestDepVitalsVersion(options: RegistryLookupOptions): Promise<string | null>;
|
|
111
|
+
//# sourceMappingURL=registryLookup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registryLookup.d.ts","sourceRoot":"","sources":["../../../src/cli/notifier/registryLookup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH;;;GAGG;AACH,eAAO,MAAM,sBAAsB,6DAA6D,CAAC;AAEjG;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,QAAW,CAAC;AAElD;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE,CAAC,CAAC;IAClE,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC,CAAC;AAEF,0FAA0F;AAC1F,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,IAAI,oBAAoB,CAAC;CACnC,CAAC;AAEF,8FAA8F;AAC9F,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,oBAAoB,GAAG,IAAI,CAAC;CAC5C,CAAC;AAEF,sGAAsG;AACtG,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,mBAAmB,KACtB,OAAO,CAAC,gBAAgB,CAAC,CAAC;AAE/B,kGAAkG;AAClG,MAAM,MAAM,qBAAqB,GAAG;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,WAAW,CAAC;CACjE,CAAC;AA2KF;;;;;;;;;;GAUG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWxB"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One bounded public lookup of the DepVitals `latest` dist-tag (Step 66).
|
|
3
|
+
*
|
|
4
|
+
* This is the **only** place in the whole product that may perform a network request, and the only
|
|
5
|
+
* thing it can ever ask for is the public dist-tag metadata of the `depvitals` package itself.
|
|
6
|
+
* Nothing about the analyzed project is used here: no project path, no manifest content, no
|
|
7
|
+
* dependency name, no version, no user data and no credential of any kind. Core analysis stays
|
|
8
|
+
* fully offline - this module lives under `src/cli/notifier/` and is never reachable from
|
|
9
|
+
* `core/`, `scanner/`, `detectors/` or `reporters/` (asserted by tests), and it stays unwired until
|
|
10
|
+
* the CLI integration step introduces the notifier orchestrator.
|
|
11
|
+
*
|
|
12
|
+
* Contract enforced by this module:
|
|
13
|
+
*
|
|
14
|
+
* 1. the URL is the fixed constant {@link NPM_DIST_TAGS_ENDPOINT}; nothing is appended to it and no
|
|
15
|
+
* caller-supplied string reaches it;
|
|
16
|
+
* 2. the whole fetch + body + parse operation is bounded by a short timeout, and the bound cannot
|
|
17
|
+
* be loosened past {@link UPDATE_LOOKUP_MAX_TIMEOUT_MS}; when the bound elapses the request is
|
|
18
|
+
* abandoned (the abort signal tears the request down) and the result is `null`;
|
|
19
|
+
* 3. a successful HTTP status is required, the body must stay at or below
|
|
20
|
+
* {@link MAX_UPDATE_RESPONSE_BYTES}, it must parse as a JSON object with an own `latest` string,
|
|
21
|
+
* and that string must be an exact stable version according to the single grammar owned by
|
|
22
|
+
* Step 63 (`versionCompare.parseStableVersion`);
|
|
23
|
+
* 4. **every** failure mode - DNS, TLS, abort/timeout, non-2xx status, empty/oversized/malformed
|
|
24
|
+
* body, body stream error, invalid version - yields `null`. The function never throws and never
|
|
25
|
+
* logs, because an optional convenience must not be able to break or delay a scan;
|
|
26
|
+
* 5. there are no retries: a failed lookup costs exactly one request and the caller falls back to
|
|
27
|
+
* "no notice", and the request carries no headers, so no npm token can leak to the registry.
|
|
28
|
+
*
|
|
29
|
+
* Testability: `fetch` and the abort-signal factory are injected, so the tests never touch the real
|
|
30
|
+
* network, never depend on a real DNS/TLS stack and can simulate a hanging request deterministically.
|
|
31
|
+
*/
|
|
32
|
+
import { parseStableVersion } from './versionCompare.js';
|
|
33
|
+
/**
|
|
34
|
+
* The fixed public endpoint of the update check, exactly as mandated by the update-notifier
|
|
35
|
+
* contract. It is a constant so that no project-derived value can ever influence the request.
|
|
36
|
+
*/
|
|
37
|
+
export const NPM_DIST_TAGS_ENDPOINT = 'https://registry.npmjs.org/-/package/depvitals/dist-tags';
|
|
38
|
+
/**
|
|
39
|
+
* Default hard bound for the complete request: one fetch plus its body and parse. Kept inside the
|
|
40
|
+
* 300-500 ms window agreed for the notifier, so a slow or unreachable registry can only ever cost a
|
|
41
|
+
* fraction of a second of an already finished scan.
|
|
42
|
+
*/
|
|
43
|
+
export const UPDATE_LOOKUP_TIMEOUT_MS = 400;
|
|
44
|
+
/**
|
|
45
|
+
* Absolute ceiling for any injected timeout. Callers (and tests) may shorten the bound but cannot
|
|
46
|
+
* turn the "short hard timeout" of the contract into an unbounded wait.
|
|
47
|
+
*/
|
|
48
|
+
export const UPDATE_LOOKUP_MAX_TIMEOUT_MS = 500;
|
|
49
|
+
/**
|
|
50
|
+
* Largest response body that is accepted. The real dist-tag document is a few dozen bytes, so the
|
|
51
|
+
* cap only ever rejects something hostile or unexpected; it is enforced while streaming, not after
|
|
52
|
+
* buffering an arbitrarily large payload.
|
|
53
|
+
*/
|
|
54
|
+
export const MAX_UPDATE_RESPONSE_BYTES = 8 * 1024;
|
|
55
|
+
/** The Node built-in timeout signal, wrapped so the default never depends on `this`. */
|
|
56
|
+
function defaultCreateAbortSignal(timeoutMs) {
|
|
57
|
+
return AbortSignal.timeout(timeoutMs);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Clamp an injected timeout into the contract's hard bound. Non-finite values (NaN, Infinity) fall
|
|
61
|
+
* back to the default instead of disabling the bound.
|
|
62
|
+
*/
|
|
63
|
+
function boundTimeout(value) {
|
|
64
|
+
if (!Number.isFinite(value)) {
|
|
65
|
+
return UPDATE_LOOKUP_TIMEOUT_MS;
|
|
66
|
+
}
|
|
67
|
+
if (value < 0) {
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
return Math.min(value, UPDATE_LOOKUP_MAX_TIMEOUT_MS);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Run `operation` under an abort deadline. The first caller to settle wins: a request that ignores
|
|
74
|
+
* its signal can still not extend the scan, because the deadline resolution is produced here instead
|
|
75
|
+
* of relying on the transport to reject.
|
|
76
|
+
*
|
|
77
|
+
* @returns whatever `operation` resolved with, or `null` when the deadline aborted first.
|
|
78
|
+
*/
|
|
79
|
+
async function withAbortDeadline(signal, operation) {
|
|
80
|
+
if (signal.aborted) {
|
|
81
|
+
// The deadline is already gone: do not start a request that cannot be answered in time.
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
let onAbort;
|
|
85
|
+
const deadline = new Promise((resolve) => {
|
|
86
|
+
onAbort = () => {
|
|
87
|
+
resolve(null);
|
|
88
|
+
};
|
|
89
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
90
|
+
});
|
|
91
|
+
const request = operation();
|
|
92
|
+
try {
|
|
93
|
+
// `Promise.race` attaches handlers to both promises, so a request that fails after the deadline
|
|
94
|
+
// won the race cannot surface an unhandled rejection either.
|
|
95
|
+
return await Promise.race([request, deadline]);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
if (onAbort !== undefined) {
|
|
99
|
+
signal.removeEventListener('abort', onAbort);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Read a response body incrementally, accepting at most {@link MAX_UPDATE_RESPONSE_BYTES} in total.
|
|
105
|
+
*
|
|
106
|
+
* Bytes are concatenated before decoding, so a valid body split into chunks at any boundary (even
|
|
107
|
+
* inside a multi-byte UTF-8 character) decodes correctly, and an oversized body is abandoned as soon
|
|
108
|
+
* as the cap is crossed instead of being buffered.
|
|
109
|
+
*
|
|
110
|
+
* @returns the decoded text, or `null` when the body exceeded the cap.
|
|
111
|
+
*/
|
|
112
|
+
async function readCappedText(reader) {
|
|
113
|
+
const chunks = [];
|
|
114
|
+
let totalBytes = 0;
|
|
115
|
+
for (;;) {
|
|
116
|
+
const { done, value } = await reader.read();
|
|
117
|
+
if (done) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (value === undefined) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
totalBytes += value.byteLength;
|
|
124
|
+
if (totalBytes > MAX_UPDATE_RESPONSE_BYTES) {
|
|
125
|
+
// Release the stream; a failing `cancel` is irrelevant because the answer is already `null`.
|
|
126
|
+
await reader.cancel().catch(() => undefined);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
chunks.push(value);
|
|
130
|
+
}
|
|
131
|
+
const bytes = new Uint8Array(totalBytes);
|
|
132
|
+
let offset = 0;
|
|
133
|
+
for (const chunk of chunks) {
|
|
134
|
+
bytes.set(chunk, offset);
|
|
135
|
+
offset += chunk.byteLength;
|
|
136
|
+
}
|
|
137
|
+
return new TextDecoder('utf-8').decode(bytes);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Extract the `latest` dist-tag from the response text.
|
|
141
|
+
*
|
|
142
|
+
* Only a JSON object with an own `latest` **string** that is an exact stable DepVitals version is
|
|
143
|
+
* accepted. HTML, truncated JSON, a non-object root, a missing or wrongly-typed `latest`, and a
|
|
144
|
+
* prerelease/build-tagged/malformed version all mean "no answer".
|
|
145
|
+
*/
|
|
146
|
+
function extractLatestVersion(text) {
|
|
147
|
+
let document;
|
|
148
|
+
try {
|
|
149
|
+
document = JSON.parse(text);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
if (typeof document !== 'object' || document === null || Array.isArray(document)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (!Object.hasOwn(document, 'latest')) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const latest = document['latest'];
|
|
161
|
+
if (typeof latest !== 'string') {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
// One grammar for "exact stable version", owned by Step 63; prerelease and malformed never pass.
|
|
165
|
+
return parseStableVersion(latest) === undefined ? null : latest;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Perform one bounded read of the dist-tag document and return the validated `latest` version.
|
|
169
|
+
*
|
|
170
|
+
* @throws whatever the transport or the body stream throws; the public wrapper converts every
|
|
171
|
+
* failure into `null`.
|
|
172
|
+
*/
|
|
173
|
+
async function requestLatest(fetchImpl, signal) {
|
|
174
|
+
const response = await fetchImpl(NPM_DIST_TAGS_ENDPOINT, { signal });
|
|
175
|
+
// A redirect, 4xx or 5xx answer is not metadata; the body is not even read.
|
|
176
|
+
if (response.status < 200 || response.status >= 300) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (response.body === null) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const text = await readCappedText(response.body.getReader());
|
|
183
|
+
if (text === null) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return extractLatestVersion(text);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Fetch the public `latest` dist-tag of the **DepVitals package itself**, at most once.
|
|
190
|
+
*
|
|
191
|
+
* The call is fully isolated: it never throws, never retries, never writes to stdout or stderr and
|
|
192
|
+
* never sends project-derived data or credentials. Any problem - DNS failure, TLS failure, abort or
|
|
193
|
+
* timeout, HTTP error status, empty, oversized, HTML or malformed body, missing `latest`, body
|
|
194
|
+
* stream error - results in `null`, which the caller must treat as "no update information".
|
|
195
|
+
*
|
|
196
|
+
* @param options - Injected transport and (optionally) a shorter timeout plus abort-signal factory.
|
|
197
|
+
* @returns the validated stable version string, or `null` for every other outcome.
|
|
198
|
+
*/
|
|
199
|
+
export async function fetchLatestDepVitalsVersion(options) {
|
|
200
|
+
try {
|
|
201
|
+
const createAbortSignal = options.createAbortSignal ?? defaultCreateAbortSignal;
|
|
202
|
+
const timeoutMs = boundTimeout(options.timeoutMs ?? UPDATE_LOOKUP_TIMEOUT_MS);
|
|
203
|
+
const signal = createAbortSignal(timeoutMs);
|
|
204
|
+
return await withAbortDeadline(signal, () => requestLatest(options.fetchImpl, signal));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// No error detail is reported anywhere: an optional network convenience stays silent.
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=registryLookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registryLookup.js","sourceRoot":"","sources":["../../../src/cli/notifier/registryLookup.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort 24-hour cache for the optional CLI update check (Step 65).
|
|
3
|
+
*
|
|
4
|
+
* The update-notifier contract allows **at most one** bounded public registry lookup per user per
|
|
5
|
+
* day, so the version reported by a previous run is remembered in a tiny private cache file:
|
|
6
|
+
*
|
|
7
|
+
* ```json
|
|
8
|
+
* { "latest": "0.2.1", "checkedAt": 1792170000000 }
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* Design rules, all of them derived from the contract and from the repository invariants:
|
|
12
|
+
*
|
|
13
|
+
* - **Outside every analyzed project.** The path is always resolved from a user-scoped environment
|
|
14
|
+
* variable or from the user's home directory, and a candidate that is not absolute is rejected.
|
|
15
|
+
* No part of the path is ever derived from the scanned directory, from `cwd`, or from any project
|
|
16
|
+
* file, so scanning a project named `depvitals` - or running from any working directory - cannot
|
|
17
|
+
* place the cache inside that project.
|
|
18
|
+
* - **Minimal, private content.** Only the public `latest` version string and the timestamp of the
|
|
19
|
+
* check are stored: no project path, no manifest content, no dependency list, no credential, no
|
|
20
|
+
* machine identifier and no telemetry. The directory is created `0o700` and the file `0o600` on
|
|
21
|
+
* platforms that honor POSIX mode bits.
|
|
22
|
+
* - **Untrusted on read.** Cached data is treated exactly like untrusted registry data: missing
|
|
23
|
+
* file, unreadable file, oversized file, non-regular file (including a symlink), invalid JSON,
|
|
24
|
+
* wrong types, unexpected extra keys, a negative timestamp and a timestamp in the future are all
|
|
25
|
+
* cache *misses*, never errors.
|
|
26
|
+
* - **Invisible on failure.** Every filesystem operation is wrapped so that permission denied,
|
|
27
|
+
* read-only media, a missing parent directory or a failing rename resolve normally instead of
|
|
28
|
+
* throwing. Nothing here writes to stdout/stderr, and nothing here can change scan output or the
|
|
29
|
+
* process exit code.
|
|
30
|
+
*
|
|
31
|
+
* The module performs no network access, decides no eligibility, prints nothing and is deliberately
|
|
32
|
+
* not wired into the CLI yet: the update check is assembled in Step 67, and the registry lookup
|
|
33
|
+
* itself belongs to Step 66. Because both helpers are asynchronous, callers must await them so a
|
|
34
|
+
* short-lived process does not exit before the cache write finishes.
|
|
35
|
+
*/
|
|
36
|
+
/** Environment map shaped like the ambient environment, so tests can inject any values. */
|
|
37
|
+
export type UpdateCacheEnvironment = Readonly<Record<string, string | undefined>>;
|
|
38
|
+
/**
|
|
39
|
+
* The freshness window. An entry is usable while its age is `>= 0` and strictly below this value;
|
|
40
|
+
* an older entry is a miss, which is what allows a later step to refresh it.
|
|
41
|
+
*/
|
|
42
|
+
export declare const UPDATE_CACHE_TTL_MS: number;
|
|
43
|
+
/** Name of the cache file, exported so callers and documentation can name the exact path. */
|
|
44
|
+
export declare const UPDATE_CACHE_FILE_NAME = "update-check.json";
|
|
45
|
+
/** Everything that influences *where* the cache lives; all of it injectable. */
|
|
46
|
+
export type UpdateCacheLocationOptions = {
|
|
47
|
+
/**
|
|
48
|
+
* Explicit cache directory, intended for tests and for a future explicit override. Only an
|
|
49
|
+
* absolute path is honored: a relative value is ignored rather than resolved against `cwd`, and
|
|
50
|
+
* resolution continues with the user-scoped defaults below, which is what keeps the cache out of
|
|
51
|
+
* any analyzed project.
|
|
52
|
+
*/
|
|
53
|
+
readonly cacheDir?: string;
|
|
54
|
+
/** Environment map to consult, e.g. the caller's own environment object. */
|
|
55
|
+
readonly env?: UpdateCacheEnvironment;
|
|
56
|
+
/** Home directory provider, defaulting to `node:os` `homedir()`. */
|
|
57
|
+
readonly homedir?: () => string;
|
|
58
|
+
/** Platform identifier, defaulting to `process.platform`; `'win32'` selects Windows rules. */
|
|
59
|
+
readonly platform?: string;
|
|
60
|
+
};
|
|
61
|
+
/** A cache operation additionally needs the injected current time, in epoch milliseconds. */
|
|
62
|
+
export type UpdateCacheOptions = UpdateCacheLocationOptions & {
|
|
63
|
+
/** Current time in epoch milliseconds; a non-integer or negative value disables the operation. */
|
|
64
|
+
readonly now: number;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the absolute path of the cache file, or `undefined` when no safe location exists, i.e.
|
|
68
|
+
* when no usable cache environment variable and no absolute home directory is available. A
|
|
69
|
+
* relative `cacheDir` is ignored in favor of those user-scoped defaults. The result never depends
|
|
70
|
+
* on the scanned directory or on `cwd`.
|
|
71
|
+
*
|
|
72
|
+
* @param options - Location inputs (override directory, environment map, home provider, platform).
|
|
73
|
+
* @returns The absolute cache file path, or `undefined` when caching is impossible.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveUpdateCacheFile(options?: UpdateCacheLocationOptions): string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Look up a fresh cached `latest` version.
|
|
78
|
+
*
|
|
79
|
+
* Returns the cached string only when the record exists, is valid, and its age is `>= 0` and below
|
|
80
|
+
* {@link UPDATE_CACHE_TTL_MS}. Every other case - no file, invalid or oversized content, wrong
|
|
81
|
+
* types, an extra key, a negative or future timestamp, a permission error, a symlink, a locked-down
|
|
82
|
+
* home folder - returns `undefined`, i.e. a miss. This function never throws and never logs, so a
|
|
83
|
+
* broken cache can never change a scan.
|
|
84
|
+
*
|
|
85
|
+
* @param options - The injected clock plus the cache location inputs.
|
|
86
|
+
* @returns The cached `latest` version, or `undefined` when the cache must be treated as empty.
|
|
87
|
+
*/
|
|
88
|
+
export declare function readCachedLatest(options: UpdateCacheOptions): Promise<string | undefined>;
|
|
89
|
+
/**
|
|
90
|
+
* Record a `latest` version together with the injected current time.
|
|
91
|
+
*
|
|
92
|
+
* The write is best effort and completely silent: it creates the cache directory if needed
|
|
93
|
+
* (`0o700`), stages the file (`0o600`) and renames it into place, and it resolves normally even when
|
|
94
|
+
* nothing at all could be written. Only the two contract-approved fields are serialized.
|
|
95
|
+
*
|
|
96
|
+
* @param latest - The public `latest` version string to remember; anything whitespace/control-bearing,
|
|
97
|
+
* empty or longer than 64 characters is not written.
|
|
98
|
+
* @param options - The injected clock plus the cache location inputs.
|
|
99
|
+
*/
|
|
100
|
+
export declare function writeCachedLatest(latest: string, options: UpdateCacheOptions): Promise<void>;
|
|
101
|
+
//# sourceMappingURL=updateCache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updateCache.d.ts","sourceRoot":"","sources":["../../../src/cli/notifier/updateCache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAMH,2FAA2F;AAC3F,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAElF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,QAAsB,CAAC;AAEvD,6FAA6F;AAC7F,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AA6D1D,gFAAgF;AAChF,MAAM,MAAM,0BAA0B,GAAG;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,GAAG,CAAC,EAAE,sBAAsB,CAAC;IACtC,oEAAoE;IACpE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;IAChC,8FAA8F;IAC9F,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,6FAA6F;AAC7F,MAAM,MAAM,kBAAkB,GAAG,0BAA0B,GAAG;IAC5D,kGAAkG;IAClG,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB,CAAC;AAsHF;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,GAAE,0BAA+B,GACvC,MAAM,GAAG,SAAS,CAEpB;AA+ED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAoC/F;AA6CD;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAmBf"}
|