depvitals 0.1.2 → 0.3.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.
Files changed (86) hide show
  1. package/README.md +39 -61
  2. package/dist/cli/entry.d.ts +59 -0
  3. package/dist/cli/entry.d.ts.map +1 -0
  4. package/dist/cli/entry.js +95 -0
  5. package/dist/cli/entry.js.map +1 -0
  6. package/dist/cli/index.js +14 -18
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/notifier/eligibility.d.ts +65 -0
  9. package/dist/cli/notifier/eligibility.d.ts.map +1 -0
  10. package/dist/cli/notifier/eligibility.js +123 -0
  11. package/dist/cli/notifier/eligibility.js.map +1 -0
  12. package/dist/cli/notifier/registryLookup.d.ts +111 -0
  13. package/dist/cli/notifier/registryLookup.d.ts.map +1 -0
  14. package/dist/cli/notifier/registryLookup.js +211 -0
  15. package/dist/cli/notifier/registryLookup.js.map +1 -0
  16. package/dist/cli/notifier/updateCache.d.ts +101 -0
  17. package/dist/cli/notifier/updateCache.d.ts.map +1 -0
  18. package/dist/cli/notifier/updateCache.js +352 -0
  19. package/dist/cli/notifier/updateCache.js.map +1 -0
  20. package/dist/cli/notifier/updateCheck.d.ts +88 -0
  21. package/dist/cli/notifier/updateCheck.d.ts.map +1 -0
  22. package/dist/cli/notifier/updateCheck.js +166 -0
  23. package/dist/cli/notifier/updateCheck.js.map +1 -0
  24. package/dist/cli/notifier/versionCompare.d.ts +48 -0
  25. package/dist/cli/notifier/versionCompare.d.ts.map +1 -0
  26. package/dist/cli/notifier/versionCompare.js +89 -0
  27. package/dist/cli/notifier/versionCompare.js.map +1 -0
  28. package/dist/cli/runCli.d.ts +2 -0
  29. package/dist/cli/runCli.d.ts.map +1 -1
  30. package/dist/cli/runCli.js +36 -4
  31. package/dist/cli/runCli.js.map +1 -1
  32. package/dist/config/ignoreDependencies.d.ts +7 -0
  33. package/dist/config/ignoreDependencies.d.ts.map +1 -0
  34. package/dist/config/ignoreDependencies.js +18 -0
  35. package/dist/config/ignoreDependencies.js.map +1 -0
  36. package/dist/config/ignorePaths.d.ts +13 -0
  37. package/dist/config/ignorePaths.d.ts.map +1 -0
  38. package/dist/config/ignorePaths.js +47 -0
  39. package/dist/config/ignorePaths.js.map +1 -0
  40. package/dist/config/projectConfigError.d.ts +7 -0
  41. package/dist/config/projectConfigError.d.ts.map +1 -0
  42. package/dist/config/projectConfigError.js +10 -0
  43. package/dist/config/projectConfigError.js.map +1 -0
  44. package/dist/config/readProjectConfig.d.ts +23 -0
  45. package/dist/config/readProjectConfig.d.ts.map +1 -0
  46. package/dist/config/readProjectConfig.js +106 -0
  47. package/dist/config/readProjectConfig.js.map +1 -0
  48. package/dist/core/analyzeProject.d.ts +48 -6
  49. package/dist/core/analyzeProject.d.ts.map +1 -1
  50. package/dist/core/analyzeProject.js +123 -16
  51. package/dist/core/analyzeProject.js.map +1 -1
  52. package/dist/detectors/eslintManifestConfig.d.ts +111 -0
  53. package/dist/detectors/eslintManifestConfig.d.ts.map +1 -0
  54. package/dist/detectors/eslintManifestConfig.js +351 -0
  55. package/dist/detectors/eslintManifestConfig.js.map +1 -0
  56. package/dist/detectors/eslintRcJsonConfig.d.ts +85 -0
  57. package/dist/detectors/eslintRcJsonConfig.d.ts.map +1 -0
  58. package/dist/detectors/eslintRcJsonConfig.js +189 -0
  59. package/dist/detectors/eslintRcJsonConfig.js.map +1 -0
  60. package/dist/detectors/jestManifestConfig.d.ts +76 -0
  61. package/dist/detectors/jestManifestConfig.d.ts.map +1 -0
  62. package/dist/detectors/jestManifestConfig.js +294 -0
  63. package/dist/detectors/jestManifestConfig.js.map +1 -0
  64. package/dist/reporters/terminalReporter.js +27 -1
  65. package/dist/reporters/terminalReporter.js.map +1 -1
  66. package/dist/scanner/moduleSpecifiers.d.ts +9 -1
  67. package/dist/scanner/moduleSpecifiers.d.ts.map +1 -1
  68. package/dist/scanner/moduleSpecifiers.js +3317 -205
  69. package/dist/scanner/moduleSpecifiers.js.map +1 -1
  70. package/dist/scanner/packageSpecifier.d.ts +26 -2
  71. package/dist/scanner/packageSpecifier.d.ts.map +1 -1
  72. package/dist/scanner/packageSpecifier.js +70 -2
  73. package/dist/scanner/packageSpecifier.js.map +1 -1
  74. package/dist/scanner/projectManifest.d.ts +31 -1
  75. package/dist/scanner/projectManifest.d.ts.map +1 -1
  76. package/dist/scanner/projectManifest.js +83 -4
  77. package/dist/scanner/projectManifest.js.map +1 -1
  78. package/dist/scanner/scriptUsage.d.ts +2 -1
  79. package/dist/scanner/scriptUsage.d.ts.map +1 -1
  80. package/dist/scanner/scriptUsage.js +216 -39
  81. package/dist/scanner/scriptUsage.js.map +1 -1
  82. package/dist/scanner/sourceFiles.d.ts +16 -2
  83. package/dist/scanner/sourceFiles.d.ts.map +1 -1
  84. package/dist/scanner/sourceFiles.js +12 -6
  85. package/dist/scanner/sourceFiles.js.map +1 -1
  86. 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"}