maven-proxy 1.3.2 → 1.3.3

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 CHANGED
@@ -288,11 +288,11 @@ Environment variables:
288
288
  - OUTBOUND_KEEP_ALIVE_INTERVAL: keep-alive interval (supports s/m/h/d), for example 1s.
289
289
  - OUTBOUND_MAX_SOCKETS: max outbound sockets per origin.
290
290
  - OUTBOUND_MAX_FREE_SOCKETS: max idle outbound sockets per origin.
291
- - MAVEN_AFFINITY_ENABLED: enable Maven affinity index.
292
- - MAVEN_AFFINITY_INDEX_DIR: Maven affinity index directory. Default data/index.
293
- - MAVEN_NEGATIVE_CACHE_TTL: negative cache TTL (supports s/m/h/d), for example 24h.
294
- - MAVEN_AFFINITY_FLUSH_INTERVAL: flush interval for affinity event log (supports s/m/h/d), for example 5s.
295
- - MAVEN_AFFINITY_EVENT_MAX_MB: max size threshold for affinity event log compaction in MB.
291
+ - MAVEN_NEGATIVE_ENABLED: enable Maven negative index. Default true.
292
+ - MAVEN_NEGATIVE_INDEX_DIR: Maven negative index directory. Default data/index.
293
+ - MAVEN_NEGATIVE_CACHE_TTL: negative cache TTL (supports s/m/h/d), for example 24h.
294
+ - MAVEN_NEGATIVE_FLUSH_INTERVAL: flush interval for negative event log (supports s/m/h/d), for example 5s.
295
+ - MAVEN_NEGATIVE_EVENT_MAX_MB: max size threshold for negative event log compaction in MB.
296
296
  - MAVEN_PROXY_CONFIG_MODE: development or user.
297
297
  - MAVEN_PROXY_CONFIG_FILE: explicit config file path.
298
298
  - EXISTING_TRUST_STORE_PATH: optional existing truststore path. If present, truststore init prefers it as source.
@@ -343,11 +343,11 @@ Priority:
343
343
  - `OUTBOUND_KEEP_ALIVE_INTERVAL`: Keep-alive interval (supports `s/m/h/d`). Default `1s`.
344
344
  - `OUTBOUND_MAX_SOCKETS`: Max outbound sockets per origin. Default `64`.
345
345
  - `OUTBOUND_MAX_FREE_SOCKETS`: Max idle outbound sockets per origin. Default `16`.
346
- - `MAVEN_AFFINITY_ENABLED`: Enable Maven affinity cache index. Default `true`.
347
- - `MAVEN_AFFINITY_INDEX_DIR`: Maven affinity index directory. Default `data/index`.
348
- - `MAVEN_NEGATIVE_CACHE_TTL`: Negative cache TTL (supports `s/m/h/d`). Default `24h`.
349
- - `MAVEN_AFFINITY_FLUSH_INTERVAL`: Flush interval for affinity event log (supports `s/m/h/d`). Default `5s`.
350
- - `MAVEN_AFFINITY_EVENT_MAX_MB`: Max size threshold for affinity event log compaction in MB. Default `8`.
346
+ - `MAVEN_NEGATIVE_ENABLED`: Enable Maven negative cache index. Default `true`.
347
+ - `MAVEN_NEGATIVE_INDEX_DIR`: Maven negative index directory. Default `data/index`.
348
+ - `MAVEN_NEGATIVE_CACHE_TTL`: Negative cache TTL (supports `s/m/h/d`). Default `24h`.
349
+ - `MAVEN_NEGATIVE_FLUSH_INTERVAL`: Flush interval for negative event log (supports `s/m/h/d`). Default `5s`.
350
+ - `MAVEN_NEGATIVE_EVENT_MAX_MB`: Max size threshold for negative event log compaction in MB. Default `8`.
351
351
  - `UPSTREAM_PROXY_URL`: Generic upstream proxy URL (fallback for HTTP/HTTPS).
352
352
  - `UPSTREAM_HTTP_PROXY_URL`: Upstream proxy URL for HTTP requests.
353
353
  - `UPSTREAM_HTTPS_PROXY_URL`: Upstream proxy URL for HTTPS requests.
@@ -193,11 +193,11 @@ function getDefaultConfigTemplate() {
193
193
  appendEntry("OUTBOUND_KEEP_ALIVE_INTERVAL", "1s", "keep-alive 间隔(支持 s/m/h/d),默认 1s。", "Keep-alive interval (supports s/m/h/d). Default: 1s.");
194
194
  appendEntry("OUTBOUND_MAX_SOCKETS", "64", "每个源站最大出站连接数,默认 64。", "Maximum outbound sockets per upstream host. Default: 64.");
195
195
  appendEntry("OUTBOUND_MAX_FREE_SOCKETS", "16", "每个源站可保留空闲连接上限,默认 16。", "Maximum free outbound sockets kept per upstream host. Default: 16.");
196
- appendEntry("MAVEN_AFFINITY_ENABLED", "true", "是否启用 Maven affinity 缓存索引,默认 true。", "Enable Maven affinity index. Default: true.");
197
- appendEntry("MAVEN_AFFINITY_INDEX_DIR", "data/index", "Maven affinity 索引目录,默认 data/index。", "Maven affinity index directory. Default: data/index.");
196
+ appendEntry("MAVEN_NEGATIVE_ENABLED", "true", "是否启用 Maven negative 索引,默认 true。", "Enable Maven negative index. Default: true.");
197
+ appendEntry("MAVEN_NEGATIVE_INDEX_DIR", "data/index", "Maven negative 索引目录,默认 data/index。", "Maven negative index directory. Default: data/index.");
198
198
  appendEntry("MAVEN_NEGATIVE_CACHE_TTL", "24h", "负缓存 TTL(支持 s/m/h/d),默认 24h。", "Negative cache TTL (supports s/m/h/d). Default: 24h.");
199
- appendEntry("MAVEN_AFFINITY_FLUSH_INTERVAL", "5s", "affinity 事件日志 flush 周期(支持 s/m/h/d),默认 5s。", "Affinity event log flush interval (supports s/m/h/d). Default: 5s.");
200
- appendEntry("MAVEN_AFFINITY_EVENT_MAX_MB", "8", "affinity 事件日志压缩阈值(MB),默认 8。", "Affinity event log compaction threshold in MB. Default: 8.");
199
+ appendEntry("MAVEN_NEGATIVE_FLUSH_INTERVAL", "5s", "negative 事件日志 flush 周期(支持 s/m/h/d),默认 5s。", "Negative event log flush interval (supports s/m/h/d). Default: 5s.");
200
+ appendEntry("MAVEN_NEGATIVE_EVENT_MAX_MB", "8", "negative 事件日志压缩阈值(MB),默认 8。", "Negative event log compaction threshold in MB. Default: 8.");
201
201
  appendEntry("UPSTREAM_PROXY_URL", "", "通用上级代理地址(HTTP/HTTPS 兜底)。", "Generic upstream proxy URL fallback for HTTP/HTTPS.");
202
202
  appendEntry("UPSTREAM_HTTP_PROXY_URL", "", "HTTP 请求使用的上级代理地址。", "Upstream proxy URL for HTTP requests.");
203
203
  appendEntry("UPSTREAM_HTTPS_PROXY_URL", "", "HTTPS 请求使用的上级代理地址。", "Upstream proxy URL for HTTPS requests.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maven-proxy",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "Maven proxy with cache, HTTPS MITM for selected domains, and local repo publishing",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -35,8 +35,8 @@
35
35
  "npm:version:patch": "npm version patch",
36
36
  "npm:view:version": "npm view maven-proxy version",
37
37
  "npm:view:dist-tags": "npm view maven-proxy dist-tags",
38
- "replay:affinity": "node --test test/replay-affinity.test.js",
39
- "test:replay": "node --test test/replay-affinity.test.js",
38
+ "replay:negative": "node --test test/replay-negative.test.js",
39
+ "test:replay": "node --test test/replay-negative.test.js",
40
40
  "test": "node --test test/*.test.js"
41
41
  },
42
42
  "keywords": [],
@@ -31,16 +31,6 @@ function readJsonFile(filePath, fallback) {
31
31
  }
32
32
  }
33
33
 
34
- function serializeSnapshot(positiveMap, negativeMap, conflictMap) {
35
- return {
36
- version: 1,
37
- generatedAt: new Date().toISOString(),
38
- positive: [...positiveMap.entries()],
39
- negative: [...negativeMap.entries()],
40
- conflicts: [...conflictMap.entries()],
41
- };
42
- }
43
-
44
34
  function normalizeRequestPath(pathname) {
45
35
  return String(pathname || "")
46
36
  .replace(/\\/g, "/")
@@ -64,22 +54,26 @@ function buildNegativeKey(scope, canonicalKey) {
64
54
  return `${String(scope || "").toLowerCase()}|${canonicalKey}`;
65
55
  }
66
56
 
67
- export class MavenAffinityIndex {
57
+ function serializeSnapshot(negativeMap) {
58
+ return {
59
+ version: 1,
60
+ generatedAt: new Date().toISOString(),
61
+ negative: [...negativeMap.entries()],
62
+ };
63
+ }
64
+
65
+ export class MavenNegativeIndex {
68
66
  constructor(config) {
69
- this.enabled = toBool(config.mavenAffinityEnabled, true);
70
- this.indexDir = config.mavenAffinityIndexDir;
67
+ this.enabled = toBool(config.mavenNegativeEnabled, true);
68
+ this.indexDir = config.mavenNegativeIndexDir;
71
69
  this.negativeTtlMs = toPositiveInt(config.mavenNegativeCacheTtlMs, 24 * 60 * 60 * 1000);
72
- this.flushIntervalMs = toPositiveInt(config.mavenAffinityFlushIntervalMs, 5000);
73
- this.maxEventBytes = toPositiveInt(config.mavenAffinityEventMaxBytes, 8 * 1024 * 1024);
70
+ this.flushIntervalMs = toPositiveInt(config.mavenNegativeFlushIntervalMs, 5000);
71
+ this.maxEventBytes = toPositiveInt(config.mavenNegativeEventMaxBytes, 8 * 1024 * 1024);
74
72
 
75
- this.snapshotPath = path.join(this.indexDir, "maven-affinity.snapshot.json");
76
- this.eventLogPath = path.join(this.indexDir, "maven-affinity.events.log");
73
+ this.snapshotPath = path.join(this.indexDir, "maven-negative.snapshot.json");
74
+ this.eventLogPath = path.join(this.indexDir, "maven-negative.events.log");
77
75
 
78
- // Positive entries are persistent and have no TTL. They are removed only
79
- // when the cache file disappears or a conflict is detected.
80
- this.positive = new Map();
81
76
  this.negative = new Map();
82
- this.conflicts = new Map();
83
77
 
84
78
  this.pendingEvents = [];
85
79
  this.flushTimer = null;
@@ -98,7 +92,7 @@ export class MavenAffinityIndex {
98
92
 
99
93
  this.flushTimer = setInterval(() => {
100
94
  this.flush().catch((error) => {
101
- console.error(`[affinity] flush failed: ${error.message}`);
95
+ console.error(`[maven-negative] flush failed: ${error.message}`);
102
96
  });
103
97
  }, this.flushIntervalMs);
104
98
 
@@ -113,20 +107,12 @@ export class MavenAffinityIndex {
113
107
  return;
114
108
  }
115
109
 
116
- for (const [key, value] of snapshot.positive || []) {
117
- this.positive.set(key, value);
118
- }
119
-
120
110
  const currentTime = nowMs();
121
111
  for (const [key, value] of snapshot.negative || []) {
122
112
  if (value?.expireAt && value.expireAt > currentTime) {
123
113
  this.negative.set(key, value);
124
114
  }
125
115
  }
126
-
127
- for (const [key, value] of snapshot.conflicts || []) {
128
- this.conflicts.set(key, value);
129
- }
130
116
  }
131
117
 
132
118
  #replayEventLog() {
@@ -167,22 +153,6 @@ export class MavenAffinityIndex {
167
153
  return;
168
154
  }
169
155
 
170
- if (type === "positive_upsert") {
171
- this.positive.set(payload.key, payload.value);
172
- if (append) {
173
- this.#enqueueEvent(type, payload);
174
- }
175
- return;
176
- }
177
-
178
- if (type === "positive_remove") {
179
- this.positive.delete(payload.key);
180
- if (append) {
181
- this.#enqueueEvent(type, payload);
182
- }
183
- return;
184
- }
185
-
186
156
  if (type === "negative_upsert") {
187
157
  if (payload.value?.expireAt > nowMs()) {
188
158
  this.negative.set(payload.key, payload.value);
@@ -202,50 +172,6 @@ export class MavenAffinityIndex {
202
172
  }
203
173
  return;
204
174
  }
205
-
206
- if (type === "conflict_set") {
207
- this.conflicts.set(payload.key, payload.value);
208
- if (append) {
209
- this.#enqueueEvent(type, payload);
210
- }
211
- return;
212
- }
213
-
214
- if (type === "conflict_clear") {
215
- this.conflicts.delete(payload.key);
216
- if (append) {
217
- this.#enqueueEvent(type, payload);
218
- }
219
- }
220
- }
221
-
222
- async resolvePreferredCachePath(canonicalKey) {
223
- if (!this.enabled || !canonicalKey) {
224
- return "";
225
- }
226
-
227
- if (this.conflicts.has(canonicalKey)) {
228
- return "";
229
- }
230
-
231
- const existing = this.positive.get(canonicalKey);
232
- if (!existing?.cachePath) {
233
- return "";
234
- }
235
-
236
- try {
237
- const stats = await fs.promises.stat(existing.cachePath);
238
- if (!stats.isFile()) {
239
- throw new Error("not-file");
240
- }
241
- return existing.cachePath;
242
- } catch {
243
- this.#applyEvent({
244
- type: "positive_remove",
245
- payload: { key: canonicalKey },
246
- });
247
- return "";
248
- }
249
175
  }
250
176
 
251
177
  shouldSkipRequest(canonicalKey, urlObj) {
@@ -271,58 +197,6 @@ export class MavenAffinityIndex {
271
197
  return true;
272
198
  }
273
199
 
274
- recordSuccess({ canonicalKey, host, cachePath, fileName, urlObj = null }) {
275
- if (!this.enabled || !canonicalKey || !cachePath || !fileName) {
276
- return;
277
- }
278
-
279
- const previous = this.positive.get(canonicalKey);
280
- if (previous && previous.fileName !== fileName) {
281
- this.#applyEvent({
282
- type: "conflict_set",
283
- payload: {
284
- key: canonicalKey,
285
- value: {
286
- reason: "file-name-mismatch",
287
- updatedAt: nowMs(),
288
- previousFileName: previous.fileName,
289
- currentFileName: fileName,
290
- },
291
- },
292
- });
293
-
294
- this.#applyEvent({
295
- type: "positive_remove",
296
- payload: { key: canonicalKey },
297
- });
298
- return;
299
- }
300
-
301
- this.#applyEvent({
302
- type: "positive_upsert",
303
- payload: {
304
- key: canonicalKey,
305
- value: {
306
- cachePath,
307
- fileName,
308
- host: String(host || "").toLowerCase(),
309
- updatedAt: nowMs(),
310
- },
311
- },
312
- });
313
-
314
- const successScope = buildNegativeScope(urlObj);
315
- if (successScope) {
316
- const negativeKey = buildNegativeKey(successScope, canonicalKey);
317
- if (this.negative.has(negativeKey)) {
318
- this.#applyEvent({
319
- type: "negative_remove",
320
- payload: { key: negativeKey },
321
- });
322
- }
323
- }
324
- }
325
-
326
200
  recordNegative({ canonicalKey, urlObj, statusCode = 404, ttlMs = this.negativeTtlMs }) {
327
201
  const scope = buildNegativeScope(urlObj);
328
202
  if (!this.enabled || !canonicalKey || !scope) {
@@ -345,6 +219,16 @@ export class MavenAffinityIndex {
345
219
  });
346
220
  }
347
221
 
222
+ clearNegative({ canonicalKey, urlObj }) {
223
+ const scope = buildNegativeScope(urlObj);
224
+ if (!this.enabled || !canonicalKey || !scope) {
225
+ return;
226
+ }
227
+
228
+ const key = buildNegativeKey(scope, canonicalKey);
229
+ this.#applyEvent({ type: "negative_remove", payload: { key } });
230
+ }
231
+
348
232
  async flush() {
349
233
  if (!this.enabled || this.flushing) {
350
234
  return;
@@ -370,7 +254,7 @@ export class MavenAffinityIndex {
370
254
  }
371
255
 
372
256
  async #writeSnapshotAndResetEventLog() {
373
- const snapshot = serializeSnapshot(this.positive, this.negative, this.conflicts);
257
+ const snapshot = serializeSnapshot(this.negative);
374
258
  const tempPath = `${this.snapshotPath}.tmp`;
375
259
  await fs.promises.writeFile(tempPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
376
260
  await fs.promises.rename(tempPath, this.snapshotPath);
@@ -230,15 +230,16 @@ const multiThreadMinSizeBytes = Math.max(0, toInt(process.env.MULTI_THREAD_MIN_S
230
230
  const downloadTimeout = process.env.DOWNLOAD_TIMEOUT || "60s";
231
231
  const outboundKeepAliveInterval = process.env.OUTBOUND_KEEP_ALIVE_INTERVAL || "1s";
232
232
  const mavenNegativeCacheTtl = process.env.MAVEN_NEGATIVE_CACHE_TTL || "24h";
233
- const mavenAffinityFlushInterval = process.env.MAVEN_AFFINITY_FLUSH_INTERVAL || "5s";
233
+ // Prefer new MAVEN_NEGATIVE_* names but fall back to legacy MAVEN_AFFINITY_* for compatibility
234
+ const mavenNegativeFlushInterval = process.env.MAVEN_NEGATIVE_FLUSH_INTERVAL || process.env.MAVEN_AFFINITY_FLUSH_INTERVAL || "5s";
234
235
  const logRetention = process.env.LOG_RETENTION || "7d";
235
236
 
236
237
  const downloadTimeoutMs = Math.max(1, parseDurationToMs(downloadTimeout, 60 * 1000));
237
238
  const outboundKeepAliveMsecs = Math.max(1, parseDurationToMs(outboundKeepAliveInterval, 1000));
238
239
  const mavenNegativeCacheTtlMs = Math.max(1, parseDurationToMs(mavenNegativeCacheTtl, 24 * 60 * 60 * 1000));
239
- const mavenAffinityFlushIntervalMs = Math.max(1, parseDurationToMs(mavenAffinityFlushInterval, 5 * 1000));
240
+ const mavenNegativeFlushIntervalMs = Math.max(1, parseDurationToMs(mavenNegativeFlushInterval, 5 * 1000));
240
241
  const logRetentionDays = Math.max(1, Math.ceil(parseDurationToMs(logRetention, 7 * 24 * 60 * 60 * 1000) / (24 * 60 * 60 * 1000)));
241
- const mavenAffinityEventMaxBytes = Math.max(1, toInt(process.env.MAVEN_AFFINITY_EVENT_MAX_MB, 8)) * 1024 * 1024;
242
+ const mavenNegativeEventMaxBytes = Math.max(1, toInt(process.env.MAVEN_NEGATIVE_EVENT_MAX_MB ?? process.env.MAVEN_AFFINITY_EVENT_MAX_MB, 8)) * 1024 * 1024;
242
243
 
243
244
  export const config = {
244
245
  configMode,
@@ -268,13 +269,20 @@ export const config = {
268
269
  outboundKeepAliveMsecs,
269
270
  outboundMaxSockets: Math.max(1, toInt(process.env.OUTBOUND_MAX_SOCKETS, 64)),
270
271
  outboundMaxFreeSockets: Math.max(1, toInt(process.env.OUTBOUND_MAX_FREE_SOCKETS, 16)),
271
- mavenAffinityEnabled: toBool(process.env.MAVEN_AFFINITY_ENABLED, true),
272
- mavenAffinityIndexDir: path.resolve(configBaseDir, process.env.MAVEN_AFFINITY_INDEX_DIR || "data/index"),
272
+ // Negative-only index configuration (new names)
273
+ mavenNegativeEnabled: toBool(process.env.MAVEN_NEGATIVE_ENABLED ?? process.env.MAVEN_AFFINITY_ENABLED, true),
274
+ mavenNegativeIndexDir: path.resolve(configBaseDir, process.env.MAVEN_NEGATIVE_INDEX_DIR || process.env.MAVEN_AFFINITY_INDEX_DIR || "data/index"),
273
275
  mavenNegativeCacheTtl,
274
276
  mavenNegativeCacheTtlMs,
275
- mavenAffinityFlushInterval,
276
- mavenAffinityFlushIntervalMs,
277
- mavenAffinityEventMaxBytes,
277
+ mavenNegativeFlushInterval,
278
+ mavenNegativeFlushIntervalMs,
279
+ mavenNegativeEventMaxBytes,
280
+ // Backwards-compatible aliases for older code that still references affinity names
281
+ mavenAffinityEnabled: toBool(process.env.MAVEN_NEGATIVE_ENABLED ?? process.env.MAVEN_AFFINITY_ENABLED, true),
282
+ mavenAffinityIndexDir: path.resolve(configBaseDir, process.env.MAVEN_NEGATIVE_INDEX_DIR || process.env.MAVEN_AFFINITY_INDEX_DIR || "data/index"),
283
+ mavenAffinityFlushInterval: mavenNegativeFlushInterval,
284
+ mavenAffinityFlushIntervalMs: mavenNegativeFlushIntervalMs,
285
+ mavenAffinityEventMaxBytes: mavenNegativeEventMaxBytes,
278
286
  cacheCleanupEnabled: toBool(process.env.CACHE_CLEANUP_ENABLED, true),
279
287
  cacheCleanupDailyAt: process.env.CACHE_CLEANUP_DAILY_AT || "03:00",
280
288
  cacheCleanupCheckMinInterval: process.env.CACHE_CLEANUP_CHECK_MIN_INTERVAL || "10m",
package/src/index.js CHANGED
@@ -7,7 +7,7 @@ import { startProxyServer } from "./proxy/proxy-server.js";
7
7
  import { startRepoServer } from "./repo/repo-server.js";
8
8
  import { getTrustStoreCommands } from "./cert/truststore-utils.js";
9
9
  import { UpstreamProxyManager } from "./proxy/upstream-proxy.js";
10
- import { MavenAffinityIndex } from "./cache/maven-affinity-index.js";
10
+ import { MavenNegativeIndex } from "./cache/maven-negative-index.js";
11
11
  import { CacheCleanupManager } from "./cache/cache-cleanup-manager.js";
12
12
  import { installConsoleLogFileMirror, installGlobalErrorLogging } from "./common/console-log-file.js";
13
13
 
@@ -84,8 +84,8 @@ async function main() {
84
84
  }
85
85
 
86
86
  const upstreamProxyManager = new UpstreamProxyManager(config, matchesDomain);
87
- const mavenAffinityIndex = new MavenAffinityIndex(config);
88
- await mavenAffinityIndex.init();
87
+ const mavenNegativeIndex = new MavenNegativeIndex(config);
88
+ await mavenNegativeIndex.init();
89
89
  const cacheCleanupManager = new CacheCleanupManager(config);
90
90
  await cacheCleanupManager.init();
91
91
 
@@ -97,7 +97,7 @@ async function main() {
97
97
  downloader,
98
98
  matchesDomain,
99
99
  upstreamProxyManager,
100
- mavenAffinityIndex,
100
+ mavenNegativeIndex,
101
101
  cacheCleanupManager,
102
102
  );
103
103
  const repoServer = startRepoServer(config, downloader, cacheCleanupManager);
@@ -132,12 +132,12 @@ async function main() {
132
132
  startupInfo(`[maven-proxy] outbound keepAlive interval: ${config.outboundKeepAliveInterval}`);
133
133
  startupInfo(`[maven-proxy] outbound maxSockets: ${config.outboundMaxSockets}`);
134
134
  startupInfo(`[maven-proxy] outbound maxFreeSockets: ${config.outboundMaxFreeSockets}`);
135
- startupInfo(`[maven-proxy] maven affinity enabled: ${config.mavenAffinityEnabled}`);
136
- startupInfo(`[maven-proxy] maven affinity index dir: ${config.mavenAffinityIndexDir}`);
135
+ startupInfo(`[maven-proxy] maven negative index enabled: ${config.mavenNegativeEnabled}`);
136
+ startupInfo(`[maven-proxy] maven negative index dir: ${config.mavenNegativeIndexDir}`);
137
137
  startupInfo(`[maven-proxy] maven negative cache ttl: ${config.mavenNegativeCacheTtl}`);
138
- startupInfo(`[maven-proxy] maven affinity flush interval: ${config.mavenAffinityFlushInterval}`);
138
+ startupInfo(`[maven-proxy] maven negative flush interval: ${config.mavenNegativeFlushInterval}`);
139
139
  startupInfo(`[maven-proxy] download timeout: ${config.downloadTimeout}`);
140
- startupInfo(`[maven-proxy] maven affinity event max(MB): ${config.mavenAffinityEventMaxBytes / (1024 * 1024)}`);
140
+ startupInfo(`[maven-proxy] maven negative event max(MB): ${config.mavenNegativeEventMaxBytes / (1024 * 1024)}`);
141
141
  startupInfo(`[maven-proxy] cache cleanup enabled: ${config.cacheCleanupEnabled}`);
142
142
  startupInfo(`[maven-proxy] cache cleanup daily at: ${config.cacheCleanupDailyAt}`);
143
143
  startupInfo(`[maven-proxy] cache touch on hit: ${config.cacheTouchOnHit}`);
@@ -169,7 +169,7 @@ async function main() {
169
169
  repoServer.close();
170
170
  upstreamProxyManager.destroy();
171
171
  void cacheCleanupManager.destroy();
172
- void mavenAffinityIndex.destroy();
172
+ void mavenNegativeIndex.destroy();
173
173
  };
174
174
 
175
175
  process.on("SIGINT", shutdown);
@@ -36,6 +36,44 @@ function pickClient(protocol) {
36
36
  return protocol === "https:" ? https : http;
37
37
  }
38
38
 
39
+ function hasFileExtension(urlObj) {
40
+ try {
41
+ const pathname = String(urlObj?.pathname || "");
42
+ const base = path.basename(pathname || "").toLowerCase();
43
+ if (!base) return false;
44
+
45
+ const knownSuffixes = [
46
+ ".pom",
47
+ ".jar",
48
+ ".aar",
49
+ ".war",
50
+ ".zip",
51
+ ".module",
52
+ ".xml",
53
+ ".sha1",
54
+ ".md5",
55
+ ".sha256",
56
+ ".sha512",
57
+ ".asc",
58
+ ".json",
59
+ ".toml",
60
+ ".klib",
61
+ ".tgz",
62
+ ".tar.gz",
63
+ ];
64
+
65
+ if (knownSuffixes.some((s) => base.endsWith(s))) return true;
66
+
67
+ const ext = path.extname(base);
68
+ if (!ext) return false;
69
+
70
+ // Treat numeric-only extensions (like version segments) as NOT an extension.
71
+ return /[a-zA-Z]/.test(ext);
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
39
77
  function sanitizeHeaders(headers = {}) {
40
78
  const result = { ...headers };
41
79
  const blocked = [
@@ -80,11 +118,7 @@ function sendErrorText(res, statusCode, message, context = "proxy") {
80
118
  sendText(res, statusCode, message);
81
119
  }
82
120
 
83
- export function isPositiveAffinityEligible(fileName) {
84
- const lower = String(fileName || "").toLowerCase();
85
- const base = lower.replace(/\.(sha1|sha256|sha512|md5|asc)$/i, "");
86
- return /\.(jar|aar|war)$/i.test(base);
87
- }
121
+ // Positive affinity removed: only negative index (404/410) is retained.
88
122
 
89
123
  function buildUrl(req, forcedProtocol = null) {
90
124
  const raw = req.url || "/";
@@ -204,7 +238,7 @@ export function createHttpRequestHandler({
204
238
  mavenCacheIgnorePathPrefixRules: config.mavenCacheIgnorePathPrefixRules,
205
239
  });
206
240
 
207
- if (ecosystem === "maven" && mavenAffinityIndex?.enabled) {
241
+ if (ecosystem === "maven" && mavenAffinityIndex) {
208
242
  canonical = parseMavenReleaseCanonical(urlObj);
209
243
  }
210
244
  } catch (error) {
@@ -221,24 +255,21 @@ export function createHttpRequestHandler({
221
255
  }
222
256
 
223
257
  if (canonical && mavenAffinityIndex) {
224
- const canUsePositiveAffinity = !config.mavenCacheUseDomainDir && isPositiveAffinityEligible(canonical.fileName);
225
-
226
- if (canUsePositiveAffinity) {
227
- const preferredPath = await mavenAffinityIndex.resolvePreferredCachePath(canonical.canonicalKey);
228
- if (preferredPath) {
229
- console.log(`[proxy] affinity hit canonical=${canonical.canonicalKey} host=${urlObj.hostname}`);
230
- await serveFile(res, req, preferredPath, cacheCleanupManager);
231
- return;
232
- }
233
- }
234
-
235
258
  if (mavenAffinityIndex.shouldSkipRequest(canonical.canonicalKey, urlObj)) {
236
- console.log(`[proxy] affinity negative skip canonical=${canonical.canonicalKey} host=${urlObj.hostname}`);
259
+ console.log(`[proxy] negative skip canonical=${canonical.canonicalKey} host=${urlObj.hostname}`);
237
260
  sendText(res, 404, "Not Found");
238
261
  return;
239
262
  }
240
263
  }
241
264
 
265
+ // If the requested resource has no file extension, do not cache it.
266
+ // If a file without extension already exists in cache, it was handled above.
267
+ if (!hasFileExtension(urlObj)) {
268
+ console.log(`[proxy] skip caching for extensionless path host=${urlObj.hostname} path=${urlObj.pathname}`);
269
+ forwardDirectRequest(req, res, urlObj, config.downloadTimeoutMs, upstreamProxyManager);
270
+ return;
271
+ }
272
+
242
273
  try {
243
274
  console.log(`[proxy] local cache miss host=${urlObj.hostname} path=${urlObj.pathname}`);
244
275
  if (cacheCleanupManager) {
@@ -247,19 +278,13 @@ export function createHttpRequestHandler({
247
278
  await fs.promises.mkdir(path.dirname(cachePath), { recursive: true });
248
279
  await downloader.ensureCached(urlObj, cachePath, req.headers);
249
280
 
250
- if (
251
- canonical &&
252
- mavenAffinityIndex &&
253
- !config.mavenCacheUseDomainDir &&
254
- isPositiveAffinityEligible(canonical.fileName)
255
- ) {
256
- mavenAffinityIndex.recordSuccess({
257
- canonicalKey: canonical.canonicalKey,
258
- host: urlObj.hostname,
259
- cachePath,
260
- fileName: canonical.fileName,
261
- urlObj,
262
- });
281
+ if (canonical && mavenAffinityIndex) {
282
+ try {
283
+ // Clear any negative entry for this request scope on successful fetch.
284
+ mavenAffinityIndex.clearNegative({ canonicalKey: canonical.canonicalKey, urlObj });
285
+ } catch (err) {
286
+ console.error(`[proxy] clearing negative index failed: ${err?.message || err}`);
287
+ }
263
288
  }
264
289
 
265
290
  res.setHeader("x-cache", "MISS");