@superblocksteam/vite-plugin-file-sync 2.0.123 → 2.0.124-next.1
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/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts +16 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts.map +1 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js +32 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.d.ts.map +1 -1
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.js +3 -1
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/api-comparator.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/apis/api-comparator.js +1 -4
- package/dist/ai-service/agent/tools/apis/api-comparator.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/api-testing-state.js +7 -0
- package/dist/ai-service/agent/tools/apis/api-testing-state.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/get-api-docs.d.ts +1 -1
- package/dist/ai-service/agent/tools/apis/write-api.d.ts +2 -2
- package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-capture-screenshot.js +11 -33
- package/dist/ai-service/agent/tools/build-capture-screenshot.js.map +1 -1
- package/dist/ai-service/agent/tools/build-copy-directory.d.ts +1 -1
- package/dist/ai-service/agent/tools/build-finalize.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-finalize.js +11 -2
- package/dist/ai-service/agent/tools/build-finalize.js.map +1 -1
- package/dist/ai-service/agent/tools/build-install-packages.d.ts +41 -1
- package/dist/ai-service/agent/tools/build-install-packages.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-install-packages.js +217 -0
- package/dist/ai-service/agent/tools/build-install-packages.js.map +1 -1
- package/dist/ai-service/agent/tools/build-lookup-npm-package.d.ts +28 -0
- package/dist/ai-service/agent/tools/build-lookup-npm-package.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/build-lookup-npm-package.js +78 -0
- package/dist/ai-service/agent/tools/build-lookup-npm-package.js.map +1 -0
- package/dist/ai-service/agent/tools/build-manage-checklist.d.ts +4 -4
- package/dist/ai-service/agent/tools/build-navigate-preview.d.ts +16 -0
- package/dist/ai-service/agent/tools/build-navigate-preview.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/build-navigate-preview.js +68 -0
- package/dist/ai-service/agent/tools/build-navigate-preview.js.map +1 -0
- package/dist/ai-service/agent/tools/build-write-file.d.ts +1 -1
- package/dist/ai-service/agent/tools/databases/dev-database.d.ts +9 -9
- package/dist/ai-service/agent/tools/index.d.ts +2 -0
- package/dist/ai-service/agent/tools/index.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/index.js +2 -0
- package/dist/ai-service/agent/tools/index.js.map +1 -1
- package/dist/ai-service/agent/tools/integrations/execute-request.d.ts +3 -3
- package/dist/ai-service/agent/tools/report-security-findings.d.ts +11 -5
- package/dist/ai-service/agent/tools/report-security-findings.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/report-security-findings.js +1 -0
- package/dist/ai-service/agent/tools/report-security-findings.js.map +1 -1
- package/dist/ai-service/agent/tools.d.ts.map +1 -1
- package/dist/ai-service/agent/tools.js +3 -1
- package/dist/ai-service/agent/tools.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/git.d.ts +2 -2
- package/dist/ai-service/agent/tools2/tools/grep.d.ts +2 -2
- package/dist/ai-service/agent/tools2/tools/update-test-case-status.d.ts +2 -2
- package/dist/ai-service/app-interface/npm-error-parser.d.ts +161 -0
- package/dist/ai-service/app-interface/npm-error-parser.d.ts.map +1 -0
- package/dist/ai-service/app-interface/npm-error-parser.js +510 -0
- package/dist/ai-service/app-interface/npm-error-parser.js.map +1 -0
- package/dist/ai-service/app-interface/npm-package-lookup.d.ts +286 -0
- package/dist/ai-service/app-interface/npm-package-lookup.d.ts.map +1 -0
- package/dist/ai-service/app-interface/npm-package-lookup.js +793 -0
- package/dist/ai-service/app-interface/npm-package-lookup.js.map +1 -0
- package/dist/ai-service/app-interface/npm-registry.d.ts +735 -47
- package/dist/ai-service/app-interface/npm-registry.d.ts.map +1 -1
- package/dist/ai-service/app-interface/npm-registry.js +1882 -153
- package/dist/ai-service/app-interface/npm-registry.js.map +1 -1
- package/dist/ai-service/app-interface/shell.d.ts +118 -1
- package/dist/ai-service/app-interface/shell.d.ts.map +1 -1
- package/dist/ai-service/app-interface/shell.js +590 -157
- package/dist/ai-service/app-interface/shell.js.map +1 -1
- package/dist/ai-service/app-skills/manager.d.ts +1 -1
- package/dist/ai-service/app-skills/manager.d.ts.map +1 -1
- package/dist/ai-service/app-skills/manager.js +39 -7
- package/dist/ai-service/app-skills/manager.js.map +1 -1
- package/dist/ai-service/checklist/api-migration-checklist-gate.d.ts +9 -0
- package/dist/ai-service/checklist/api-migration-checklist-gate.d.ts.map +1 -0
- package/dist/ai-service/checklist/api-migration-checklist-gate.js +30 -0
- package/dist/ai-service/checklist/api-migration-checklist-gate.js.map +1 -0
- package/dist/ai-service/checklist/api-migration-origins.d.ts +4 -0
- package/dist/ai-service/checklist/api-migration-origins.d.ts.map +1 -0
- package/dist/ai-service/checklist/api-migration-origins.js +8 -0
- package/dist/ai-service/checklist/api-migration-origins.js.map +1 -0
- package/dist/ai-service/checklist/persisted-checklist-store.d.ts +2 -3
- package/dist/ai-service/checklist/persisted-checklist-store.d.ts.map +1 -1
- package/dist/ai-service/checklist/persisted-checklist-store.js +6 -7
- package/dist/ai-service/checklist/persisted-checklist-store.js.map +1 -1
- package/dist/ai-service/context-archive-paths.d.ts +4 -0
- package/dist/ai-service/context-archive-paths.d.ts.map +1 -0
- package/dist/ai-service/context-archive-paths.js +28 -0
- package/dist/ai-service/context-archive-paths.js.map +1 -0
- package/dist/ai-service/context-download.d.ts +3 -1
- package/dist/ai-service/context-download.d.ts.map +1 -1
- package/dist/ai-service/context-download.js +48 -9
- package/dist/ai-service/context-download.js.map +1 -1
- package/dist/ai-service/context-upload.d.ts +3 -2
- package/dist/ai-service/context-upload.d.ts.map +1 -1
- package/dist/ai-service/context-upload.js +44 -26
- package/dist/ai-service/context-upload.js.map +1 -1
- package/dist/ai-service/dev-database-client.d.ts +3 -11
- package/dist/ai-service/dev-database-client.d.ts.map +1 -1
- package/dist/ai-service/dev-database-client.js.map +1 -1
- package/dist/ai-service/features.d.ts +4 -0
- package/dist/ai-service/features.d.ts.map +1 -1
- package/dist/ai-service/features.js +8 -0
- package/dist/ai-service/features.js.map +1 -1
- package/dist/ai-service/filter-disabled-tools-for-migration.d.ts.map +1 -1
- package/dist/ai-service/filter-disabled-tools-for-migration.js +4 -0
- package/dist/ai-service/filter-disabled-tools-for-migration.js.map +1 -1
- package/dist/ai-service/index.d.ts +103 -0
- package/dist/ai-service/index.d.ts.map +1 -1
- package/dist/ai-service/index.js +610 -25
- package/dist/ai-service/index.js.map +1 -1
- package/dist/ai-service/llm/client.d.ts +38 -3
- package/dist/ai-service/llm/client.d.ts.map +1 -1
- package/dist/ai-service/llm/client.js +73 -22
- package/dist/ai-service/llm/client.js.map +1 -1
- package/dist/ai-service/llm/context-v2/adapter.d.ts +3 -1
- package/dist/ai-service/llm/context-v2/adapter.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/adapter.js +11 -11
- package/dist/ai-service/llm/context-v2/adapter.js.map +1 -1
- package/dist/ai-service/llm/context-v2/compaction/client-side.d.ts +44 -0
- package/dist/ai-service/llm/context-v2/compaction/client-side.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/compaction/client-side.js +170 -0
- package/dist/ai-service/llm/context-v2/compaction/client-side.js.map +1 -0
- package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.d.ts +56 -0
- package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.js +9 -0
- package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.js.map +1 -0
- package/dist/ai-service/llm/context-v2/compaction/index.d.ts +5 -0
- package/dist/ai-service/llm/context-v2/compaction/index.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/compaction/index.js +3 -0
- package/dist/ai-service/llm/context-v2/compaction/index.js.map +1 -0
- package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts +30 -0
- package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/compaction/server-side.js +130 -0
- package/dist/ai-service/llm/context-v2/compaction/server-side.js.map +1 -0
- package/dist/ai-service/llm/context-v2/context-management.d.ts +203 -0
- package/dist/ai-service/llm/context-v2/context-management.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/context-management.js +183 -0
- package/dist/ai-service/llm/context-v2/context-management.js.map +1 -0
- package/dist/ai-service/llm/context-v2/context.d.ts +64 -22
- package/dist/ai-service/llm/context-v2/context.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/context.js +233 -157
- package/dist/ai-service/llm/context-v2/context.js.map +1 -1
- package/dist/ai-service/llm/context-v2/conversation-context.d.ts +24 -7
- package/dist/ai-service/llm/context-v2/conversation-context.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/conversation-context.js +1 -1
- package/dist/ai-service/llm/context-v2/index.d.ts +0 -4
- package/dist/ai-service/llm/context-v2/index.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/index.js +0 -4
- package/dist/ai-service/llm/context-v2/index.js.map +1 -1
- package/dist/ai-service/llm/context-v2/manager.d.ts +24 -3
- package/dist/ai-service/llm/context-v2/manager.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/manager.js +146 -4
- package/dist/ai-service/llm/context-v2/manager.js.map +1 -1
- package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.d.ts +2 -7
- package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.js +5 -73
- package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.js.map +1 -1
- package/dist/ai-service/llm/context-v2/storage/event-store.d.ts +57 -0
- package/dist/ai-service/llm/context-v2/storage/event-store.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/storage/event-store.js +10 -0
- package/dist/ai-service/llm/context-v2/storage/event-store.js.map +1 -0
- package/dist/ai-service/llm/context-v2/storage/event-types.d.ts +50 -0
- package/dist/ai-service/llm/context-v2/storage/event-types.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/storage/event-types.js +10 -0
- package/dist/ai-service/llm/context-v2/storage/event-types.js.map +1 -0
- package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.d.ts +87 -0
- package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.js +184 -0
- package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.js.map +1 -0
- package/dist/ai-service/llm/context-v2/storage/migration.d.ts +49 -0
- package/dist/ai-service/llm/context-v2/storage/migration.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/storage/migration.js +126 -0
- package/dist/ai-service/llm/context-v2/storage/migration.js.map +1 -0
- package/dist/ai-service/llm/context-v2/types.d.ts +16 -11
- package/dist/ai-service/llm/context-v2/types.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/types.js +2 -2
- package/dist/ai-service/llm/context-v2/types.js.map +1 -1
- package/dist/ai-service/llm/impl/clark.d.ts +8 -0
- package/dist/ai-service/llm/impl/clark.d.ts.map +1 -1
- package/dist/ai-service/llm/impl/clark.js +8 -0
- package/dist/ai-service/llm/impl/clark.js.map +1 -1
- package/dist/ai-service/llm/interaction/adapters/vercel.d.ts.map +1 -1
- package/dist/ai-service/llm/interaction/adapters/vercel.js +17 -1
- package/dist/ai-service/llm/interaction/adapters/vercel.js.map +1 -1
- package/dist/ai-service/llm/provider.d.ts.map +1 -1
- package/dist/ai-service/llm/provider.js +1 -3
- package/dist/ai-service/llm/provider.js.map +1 -1
- package/dist/ai-service/llm/stream/observers/context.d.ts +23 -0
- package/dist/ai-service/llm/stream/observers/context.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/observers/context.js +62 -0
- package/dist/ai-service/llm/stream/observers/context.js.map +1 -1
- package/dist/ai-service/llm/tool-context-integrity.d.ts +25 -0
- package/dist/ai-service/llm/tool-context-integrity.d.ts.map +1 -0
- package/dist/ai-service/llm/tool-context-integrity.js +141 -0
- package/dist/ai-service/llm/tool-context-integrity.js.map +1 -0
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.d.ts +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.js +6 -1
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.js.map +1 -1
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts +1 -1
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.js +3 -2
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.js.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.d.ts +23 -2
- package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.js +37 -2
- package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.js +29 -2
- package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/awaiting-user.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/awaiting-user.js +9 -0
- package/dist/ai-service/state-machine/handlers/awaiting-user.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.js +25 -2
- package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.d.ts +6 -0
- package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.d.ts.map +1 -0
- package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.js +36 -0
- package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.js.map +1 -0
- package/dist/ai-service/state-machine/helpers/stable-peer.d.ts +30 -1
- package/dist/ai-service/state-machine/helpers/stable-peer.d.ts.map +1 -1
- package/dist/ai-service/state-machine/helpers/stable-peer.js +85 -2
- package/dist/ai-service/state-machine/helpers/stable-peer.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/user-preferences.d.ts +8 -0
- package/dist/ai-service/state-machine/helpers/user-preferences.d.ts.map +1 -0
- package/dist/ai-service/state-machine/helpers/user-preferences.js +11 -0
- package/dist/ai-service/state-machine/helpers/user-preferences.js.map +1 -0
- package/dist/ai-service/template-renderer.d.ts +18 -1
- package/dist/ai-service/template-renderer.d.ts.map +1 -1
- package/dist/ai-service/template-renderer.js +85 -20
- package/dist/ai-service/template-renderer.js.map +1 -1
- package/dist/ai-service/types.d.ts +157 -0
- package/dist/ai-service/types.d.ts.map +1 -1
- package/dist/ai-service/types.js +137 -0
- package/dist/ai-service/types.js.map +1 -1
- package/dist/ai-service/util/llm-config-utils.d.ts +21 -22
- package/dist/ai-service/util/llm-config-utils.d.ts.map +1 -1
- package/dist/ai-service/util/llm-config-utils.js +40 -67
- package/dist/ai-service/util/llm-config-utils.js.map +1 -1
- package/dist/file-sync-vite-plugin.d.ts.map +1 -1
- package/dist/file-sync-vite-plugin.js +5 -0
- package/dist/file-sync-vite-plugin.js.map +1 -1
- package/dist/file-system-helpers.d.ts +17 -0
- package/dist/file-system-helpers.d.ts.map +1 -1
- package/dist/file-system-helpers.js +22 -0
- package/dist/file-system-helpers.js.map +1 -1
- package/dist/git-service/index.d.ts.map +1 -1
- package/dist/git-service/index.js +15 -1
- package/dist/git-service/index.js.map +1 -1
- package/dist/migration/migration-routes.d.ts.map +1 -1
- package/dist/migration/migration-routes.js +26 -3
- package/dist/migration/migration-routes.js.map +1 -1
- package/dist/migration/translation-prompt.d.ts.map +1 -1
- package/dist/migration/translation-prompt.js +5 -0
- package/dist/migration/translation-prompt.js.map +1 -1
- package/dist/migration-templates/app-fullstack/package.json +2 -2
- package/dist/policy-gate-callback-mapper.d.ts +24 -0
- package/dist/policy-gate-callback-mapper.d.ts.map +1 -0
- package/dist/policy-gate-callback-mapper.js +61 -0
- package/dist/policy-gate-callback-mapper.js.map +1 -0
- package/dist/policy-gate-runner.d.ts +32 -0
- package/dist/policy-gate-runner.d.ts.map +1 -0
- package/dist/policy-gate-runner.js +191 -0
- package/dist/policy-gate-runner.js.map +1 -0
- package/dist/router-parser.d.ts.map +1 -1
- package/dist/router-parser.js +107 -11
- package/dist/router-parser.js.map +1 -1
- package/dist/sync-service/list-dir.d.ts.map +1 -1
- package/dist/sync-service/list-dir.js +13 -3
- package/dist/sync-service/list-dir.js.map +1 -1
- package/package.json +28 -10
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
import { metrics, } from "@opentelemetry/api";
|
|
2
|
+
import semver from "semver";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import { getLogger } from "../../util/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* Meter name for the npm-package-lookup gauge. Picked to match the
|
|
7
|
+
* `superblocks.npm.*` instrument family established by APPS-4189
|
|
8
|
+
* (`superblocks.npm.install.*` counter + histogram); the meter is the
|
|
9
|
+
* "instrumentation scope" the OTel collector tags every emitted metric
|
|
10
|
+
* with, so keeping the npm tools under one scope name makes the family
|
|
11
|
+
* easy to filter on in Grafana.
|
|
12
|
+
*/
|
|
13
|
+
const METER_NAME = "superblocks.npm";
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the cache-size gauge against the *currently registered* global
|
|
16
|
+
* meter provider. Resolved per-instance (in the constructor) rather than
|
|
17
|
+
* memoized at module load because:
|
|
18
|
+
*
|
|
19
|
+
* 1. The global meter provider can be installed after this module is
|
|
20
|
+
* imported — telemetry init is async and runs alongside the rest of
|
|
21
|
+
* the dev-server bootstrap. A memoized lookup taken at import time
|
|
22
|
+
* would bind to the no-op meter and never emit.
|
|
23
|
+
* 2. Tests swap the global meter provider per case (so each case has its
|
|
24
|
+
* own in-memory exporter). A memoized gauge would observe through a
|
|
25
|
+
* provider that's been disabled, missing the assertion.
|
|
26
|
+
*
|
|
27
|
+
* The OpenTelemetry SDK already de-duplicates same-name instruments inside
|
|
28
|
+
* the same meter (returns the existing one and logs a warning the first
|
|
29
|
+
* time a mismatched description is seen), so resolving per-instance is
|
|
30
|
+
* cheap; we're not creating multiple physical instruments.
|
|
31
|
+
*/
|
|
32
|
+
function resolveCacheSizeGauge() {
|
|
33
|
+
return metrics
|
|
34
|
+
.getMeter(METER_NAME)
|
|
35
|
+
.createObservableGauge("superblocks.npm.lookup.cache_size", {
|
|
36
|
+
description: "Current number of entries in the NpmPackageLookup in-memory cache (successful lookups and 'not_in_registry' misses; transient failures are not cached).",
|
|
37
|
+
unit: "{entry}",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the eviction counter against the *currently registered* global
|
|
42
|
+
* meter provider. Mirrors `resolveCacheSizeGauge` (same per-instance
|
|
43
|
+
* resolution rationale, same `superblocks.npm.*` instrument family) — the
|
|
44
|
+
* gauge reports the live footprint, this counter reports the cumulative
|
|
45
|
+
* number of entries the size cap has forced out so the two can be read
|
|
46
|
+
* together (sustained eviction at a flat `cache_size` = the cap is the
|
|
47
|
+
* bottleneck, not just churn).
|
|
48
|
+
*/
|
|
49
|
+
function resolveCacheEvictionCounter() {
|
|
50
|
+
return metrics
|
|
51
|
+
.getMeter(METER_NAME)
|
|
52
|
+
.createCounter("superblocks.npm.lookup.cache_evictions", {
|
|
53
|
+
description: "Cumulative number of entries evicted from the NpmPackageLookup in-memory cache to stay under the size cap (expired entries first, then oldest-expiring).",
|
|
54
|
+
unit: "{entry}",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const DEFAULT_TTL_MS = 60_000;
|
|
58
|
+
const DEFAULT_FETCH_TIMEOUT_MS = 5_000;
|
|
59
|
+
/**
|
|
60
|
+
* Hard cap on the in-memory cache size. The lookup instance lives for the
|
|
61
|
+
* whole dev-server process, and TTL is only checked reactively on read, so
|
|
62
|
+
* without a cap a long session full of unique lookups (one per package the
|
|
63
|
+
* agent ever considers) would grow the heap unbounded — expired entries
|
|
64
|
+
* linger until they're re-accessed, which a never-repeated name never is.
|
|
65
|
+
* On insert at/over the cap we evict expired entries first, then the
|
|
66
|
+
* oldest-expiring entries until under the cap (see `evictToCapacity`).
|
|
67
|
+
* Sized well above the sibling caches (`scoped-token-utils` = 100,
|
|
68
|
+
* `secret-scanner` = 200) because a npm name is a far smaller value than a
|
|
69
|
+
* token or a scan result and a single Clark session can legitimately touch
|
|
70
|
+
* thousands of distinct packages; 10k entries is a few MB at most.
|
|
71
|
+
*/
|
|
72
|
+
const MAX_CACHE_SIZE = 10_000;
|
|
73
|
+
/**
|
|
74
|
+
* How many entries to include in `recent_versions`. Sized so the agent gets
|
|
75
|
+
* enough recent history to pick across major lines without bloating its
|
|
76
|
+
* context window — most popular packages publish dozens of patch versions
|
|
77
|
+
* per major, and 30 covers ~the last 2-3 majors for a typical library.
|
|
78
|
+
*/
|
|
79
|
+
const RECENT_VERSIONS_CAP = 30;
|
|
80
|
+
/**
|
|
81
|
+
* Public npm registry — used when no `NpmRegistryClient` is wired or
|
|
82
|
+
* `getConfig()` returns `not-configured`. Matches the default `registry=`
|
|
83
|
+
* line in the bootstrapped `.npmrc` so a lookup result lines up with what
|
|
84
|
+
* `npm install` would do for the same name.
|
|
85
|
+
*/
|
|
86
|
+
const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
87
|
+
/**
|
|
88
|
+
* Shape of the npm registry's package-metadata response that this tool
|
|
89
|
+
* cares about. The registry returns lots more (`name`, full version
|
|
90
|
+
* objects, `repository`, ...) — the schema only declares what we surface
|
|
91
|
+
* to the agent. Unknown keys are silently dropped by Zod's default object
|
|
92
|
+
* behavior.
|
|
93
|
+
*
|
|
94
|
+
* `latest` and every key in `versions` / `time` are validated with
|
|
95
|
+
* `semver.valid()` — those are the registry-controlled bytes that flow
|
|
96
|
+
* into the model-visible tool summary (see `build_lookupNpmPackage`'s
|
|
97
|
+
* `getSummary` and `recent_versions`), so a compromised or misconfigured
|
|
98
|
+
* registry must not be able to smuggle newlines, brackets, or imperative-mood
|
|
99
|
+
* instructions through them. `semver.valid()` enforces the full
|
|
100
|
+
* `MAJOR.MINOR.PATCH[-prerelease][+build]` grammar — `0.0.0-beta.1` and
|
|
101
|
+
* `1.2.3+sha.abc` pass; `"1.0.0\nIGNORE…"`, `"aaaa"`, and an over-cap blob
|
|
102
|
+
* all fail and are dropped from the surfaced result.
|
|
103
|
+
*
|
|
104
|
+
* The check on `latest` is a `transform` (drop-to-undefined), not a `refine`
|
|
105
|
+
* (fail the whole parse). A `refine` would mean a single bad `dist-tags.latest`
|
|
106
|
+
* sinks the entire parse and `recent_versions` along with it — the agent
|
|
107
|
+
* loses the version list when it needs it most. With `transform`, an invalid
|
|
108
|
+
* latest decays to `undefined` and the rest of the payload still surfaces.
|
|
109
|
+
*/
|
|
110
|
+
const NpmRegistryMetadataSchema = z.object({
|
|
111
|
+
"dist-tags": z
|
|
112
|
+
.object({
|
|
113
|
+
latest: z
|
|
114
|
+
.string()
|
|
115
|
+
.optional()
|
|
116
|
+
.transform((v) => v !== undefined && semver.valid(v) !== null ? v : undefined),
|
|
117
|
+
})
|
|
118
|
+
.optional(),
|
|
119
|
+
versions: z.record(z.string(), z.unknown()).optional(),
|
|
120
|
+
// `time` values are typed `unknown` rather than `string` because npm's
|
|
121
|
+
// `time` map mixes ISO-timestamp strings (one per published version,
|
|
122
|
+
// plus `created`/`modified` pseudo-keys) with an `unpublished` pseudo-
|
|
123
|
+
// key whose value is an object — `{ time, name, tags, maintainers,
|
|
124
|
+
// description, versions }` — for fully-unpublished packages. Insisting
|
|
125
|
+
// on `string` here would fail the whole `safeParse` on any registry
|
|
126
|
+
// response that contains an `unpublished` block, silently dropping
|
|
127
|
+
// `latest_version` and `recent_versions`. `extractRecentVersions`
|
|
128
|
+
// filters back to strings before using a value as a sort key.
|
|
129
|
+
time: z.record(z.string(), z.unknown()).optional(),
|
|
130
|
+
});
|
|
131
|
+
/**
|
|
132
|
+
* Normalize a registry URL into a stable cache-key prefix. Lowercases the
|
|
133
|
+
* scheme+host (RFC 3986 §3) and drops a single trailing slash so
|
|
134
|
+
* `https://artifactory.example.com/api/npm/repo-a/` and
|
|
135
|
+
* `HTTPS://Artifactory.Example.com/api/npm/repo-a` hash to the same entry —
|
|
136
|
+
* but `…/repo-a/` and `…/repo-b/` stay distinct, which is the property
|
|
137
|
+
* Artifactory-style virtual repos depend on. The path is preserved
|
|
138
|
+
* case-sensitively because registry sub-paths are usually case-sensitive
|
|
139
|
+
* (Artifactory treats `/repo-a` and `/Repo-A` as different repos).
|
|
140
|
+
*/
|
|
141
|
+
function normalizeRegistryUrl(raw) {
|
|
142
|
+
try {
|
|
143
|
+
const u = new URL(raw);
|
|
144
|
+
const origin = `${u.protocol.toLowerCase()}//${u.host.toLowerCase()}`;
|
|
145
|
+
const path = u.pathname.replace(/\/$/, "");
|
|
146
|
+
return `${origin}${path}`;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Malformed URL — fall back to the raw string trimmed of trailing
|
|
150
|
+
// slashes. This shouldn't happen in practice because `resolveRegistry`
|
|
151
|
+
// only ever returns URLs that the `new URL(registry.url)` call above
|
|
152
|
+
// it accepted, but the cache-key path must not throw.
|
|
153
|
+
return raw.replace(/\/$/, "");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Encode an npm package name into the URL-path segment npm registries expect.
|
|
158
|
+
*
|
|
159
|
+
* For scoped packages (`@scope/name`) the `/` is percent-encoded to `%2F` so
|
|
160
|
+
* the path collapses to a single registry segment — npm, Artifactory, and
|
|
161
|
+
* Verdaccio all accept that form, and it sidesteps the ambiguity where a
|
|
162
|
+
* literal `/` could be confused with a registry sub-path on some proxies.
|
|
163
|
+
* The leading `@` is left as-is to match npm CLI URL shape, but the rest of
|
|
164
|
+
* the scope name is percent-encoded along with the sub-name so a hostile or
|
|
165
|
+
* malformed input (`@evil?probe=1/pkg`) cannot smuggle query-/path-segment
|
|
166
|
+
* characters into the authenticated request.
|
|
167
|
+
*/
|
|
168
|
+
function encodePackagePath(name) {
|
|
169
|
+
if (name.startsWith("@")) {
|
|
170
|
+
const slash = name.indexOf("/");
|
|
171
|
+
if (slash > 0) {
|
|
172
|
+
const scope = name.slice(1, slash);
|
|
173
|
+
const sub = name.slice(slash + 1);
|
|
174
|
+
return `@${encodeURIComponent(scope)}%2F${encodeURIComponent(sub)}`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return encodeURIComponent(name);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Pick the registry that `npm install` would route a given package name to,
|
|
181
|
+
* given the current `NpmRegistryConfig`. Mirrors npm's scope-mapping rule:
|
|
182
|
+
* `@scope/foo` matches `config.scopes["@scope"]` if present, otherwise the
|
|
183
|
+
* default registry. Unscoped names always use the default.
|
|
184
|
+
*
|
|
185
|
+
* Returns `undefined` only when the config has scope mappings but no default
|
|
186
|
+
* AND the requested name doesn't match any scope — in that case we have no
|
|
187
|
+
* URL to hit. The caller falls back to public npm; `npm install` would do
|
|
188
|
+
* the same thing.
|
|
189
|
+
*/
|
|
190
|
+
function selectRegistry(config, name) {
|
|
191
|
+
if (name.startsWith("@")) {
|
|
192
|
+
const slash = name.indexOf("/");
|
|
193
|
+
if (slash > 0) {
|
|
194
|
+
const scope = name.slice(0, slash);
|
|
195
|
+
const scoped = config.scopes?.[scope];
|
|
196
|
+
if (scoped) {
|
|
197
|
+
return scoped;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return config.default;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Pre-flight check for npm package availability against the registry
|
|
205
|
+
* Clark's install path would actually use. Designed so the agent can call
|
|
206
|
+
* this BEFORE attempting `npm install` and either skip a doomed install
|
|
207
|
+
* (the registry doesn't carry the package), pick a different package up
|
|
208
|
+
* front, or pick a recent version from `recent_versions` — rather than
|
|
209
|
+
* burning ~30s on a build pod recovering from `NpmInstallBlocked` after
|
|
210
|
+
* the fact.
|
|
211
|
+
*
|
|
212
|
+
* Registry resolution per call (mirrors `npm install`'s scope-mapping):
|
|
213
|
+
*
|
|
214
|
+
* 1. `await client.getConfig()` if a client is wired.
|
|
215
|
+
* - `source: configured | stale` → use `config.scopes[@scope]` for a
|
|
216
|
+
* scoped name; else `config.default`. Both yield `mode: "private"`.
|
|
217
|
+
* If neither matches (scoped-only mappings, no default, no scope
|
|
218
|
+
* match) the result is `mode: "fail-closed"` with
|
|
219
|
+
* `reason: "registry_unreachable"` — see `resolveRegistry`.
|
|
220
|
+
* - `source: not-configured` → public npm (`mode: "public-npm"`).
|
|
221
|
+
* - `source: unreachable`, or `getConfig()` threw → `mode: "fail-closed"`.
|
|
222
|
+
* 2. No client wired → public npm (`mode: "public-npm"`).
|
|
223
|
+
* Matches the default `registry=` line we bootstrap into every app's
|
|
224
|
+
* `.npmrc`.
|
|
225
|
+
*
|
|
226
|
+
* Behaviour matrix (post-resolve):
|
|
227
|
+
*
|
|
228
|
+
* | HTTP 200 | `available: true`, `latest_version`, `recent_versions` |
|
|
229
|
+
* | HTTP 404 | `available: false, reason: not_in_registry` |
|
|
230
|
+
* | HTTP 401 / 403 | `available: false, reason: registry_auth_failed` |
|
|
231
|
+
* | HTTP 5xx / DNS / TCP / TLS / fetch throw | `available: false, reason: registry_unreachable` |
|
|
232
|
+
*
|
|
233
|
+
* `mode: "public-npm"` + `reason: "registry_unreachable"` is the strong
|
|
234
|
+
* signal that the operator's network blocks egress to npmjs.org without
|
|
235
|
+
* a configured private registry; the agent can surface this as a
|
|
236
|
+
* connectivity / configuration issue rather than a missing package.
|
|
237
|
+
* `mode: "fail-closed"` + `reason: "registry_unreachable"` covers the
|
|
238
|
+
* cases where the private-registry config itself is in a bad state
|
|
239
|
+
* (auth, transient unreachable) or doesn't cover the requested name
|
|
240
|
+
* (scoped-only mappings, no default, no scope match).
|
|
241
|
+
*
|
|
242
|
+
* Token hygiene: the token comes back from `client.getConfig()` per call
|
|
243
|
+
* and lives only in a local inside `lookup()`. Nothing token-bearing is
|
|
244
|
+
* stored on the instance, so `JSON.stringify(this)` cannot surface it.
|
|
245
|
+
* Bodies of 4xx/5xx responses are drained without inspection — a hostile
|
|
246
|
+
* registry could echo the submitted token back in a 401 body, and the only
|
|
247
|
+
* way to guarantee the token never reaches the tool output / log / span is
|
|
248
|
+
* to never read those bytes at all.
|
|
249
|
+
*
|
|
250
|
+
* Caching: per-instance, in-memory, keyed on
|
|
251
|
+
* `<normalized-registry-base-url>:<name>` with a 60s TTL. The full base
|
|
252
|
+
* URL (not just the host) is required because Artifactory-style proxies
|
|
253
|
+
* host multiple npm repos under one host (`…/api/npm/repo-a/` vs
|
|
254
|
+
* `…/api/npm/repo-b/`) — keying on host alone would let a 200 or 404
|
|
255
|
+
* from one virtual repo bleed into the other. Per-process (the dev-server
|
|
256
|
+
* runs a single AppShell), no cross-pod sharing — same as
|
|
257
|
+
* `NpmRegistryClient`. Only stable outcomes (`available: true` and
|
|
258
|
+
* `not_in_registry`) are cached; `registry_auth_failed` and
|
|
259
|
+
* `registry_unreachable` are transient and must recover on the next call
|
|
260
|
+
* (otherwise a token rotation or a short network blip would pin the
|
|
261
|
+
* failure for a full minute). The URL-keyed prefix also means a swap
|
|
262
|
+
* between a private registry and the public-npm fallback can't accidentally
|
|
263
|
+
* serve a result from the wrong source. The cache is bounded at
|
|
264
|
+
* `MAX_CACHE_SIZE` entries (APPS-4429): TTL is checked only reactively on
|
|
265
|
+
* read, so without a cap a long session of unique lookups would grow the
|
|
266
|
+
* heap unbounded. On insert at/over the cap, `evictToCapacity` drops expired
|
|
267
|
+
* entries first, then the oldest-expiring live entries, emitting the count
|
|
268
|
+
* on the `cache_evictions` counter.
|
|
269
|
+
*
|
|
270
|
+
* In-flight dedup: concurrent `lookup(name)` calls that miss the cache and
|
|
271
|
+
* resolve to the same cache key share a single fetch — the first caller
|
|
272
|
+
* stores the pending promise in an inflight map; subsequent callers await
|
|
273
|
+
* it instead of firing a second HTTP request. Clark batch-installs (one
|
|
274
|
+
* lookup per package) hit this path regularly. The slot is cleared in
|
|
275
|
+
* `.finally()` so a transient-failure resolution doesn't pin every future
|
|
276
|
+
* caller to the same rejected promise. See `inFlight` for the keying
|
|
277
|
+
* contract (same string as the cache, so two scopes never share a result).
|
|
278
|
+
*/
|
|
279
|
+
export class NpmPackageLookup {
|
|
280
|
+
client;
|
|
281
|
+
fetchImpl;
|
|
282
|
+
nowFn;
|
|
283
|
+
ttlMs;
|
|
284
|
+
fetchTimeoutMs;
|
|
285
|
+
cache = new Map();
|
|
286
|
+
/**
|
|
287
|
+
* In-flight fetches keyed by the same `<normalized-registry-base-url>:<name>`
|
|
288
|
+
* string the cache uses. The first caller for a given key kicks off the
|
|
289
|
+
* fetch and stores the pending promise here; subsequent concurrent callers
|
|
290
|
+
* (Clark batch-installs hit this regularly) await the same promise instead
|
|
291
|
+
* of firing a second HTTP request against the registry. The slot is cleared
|
|
292
|
+
* in `.finally()` regardless of fetch outcome — including the transient
|
|
293
|
+
* failure paths (`registry_auth_failed`, `registry_unreachable`) that the
|
|
294
|
+
* cache deliberately skips — so a single failed fetch can't pin every
|
|
295
|
+
* future caller to the same rejected promise.
|
|
296
|
+
*
|
|
297
|
+
* Keying on the same string as `cache` (not on `name` alone) means
|
|
298
|
+
* concurrent lookups for the same package name routed to different
|
|
299
|
+
* registries (scoped vs. default, or a swap between private and public)
|
|
300
|
+
* each get their own inflight slot and never share a result across trust
|
|
301
|
+
* boundaries.
|
|
302
|
+
*/
|
|
303
|
+
inFlight = new Map();
|
|
304
|
+
/**
|
|
305
|
+
* Resolved gauge and bound callback held so `removeCallback` can detach.
|
|
306
|
+
* The gauge handle is captured at construction so `dispose()` always
|
|
307
|
+
* detaches from the same instrument it attached to, even if the global
|
|
308
|
+
* meter provider has been replaced in the meantime.
|
|
309
|
+
*/
|
|
310
|
+
gauge;
|
|
311
|
+
gaugeCallback;
|
|
312
|
+
/**
|
|
313
|
+
* Counter for entries the size cap forces out (see `evictToCapacity`).
|
|
314
|
+
* Cumulative, no labels — same partitioning rationale as the gauge. Held
|
|
315
|
+
* `undefined` when `skipMetrics` is set so tests don't leak instruments
|
|
316
|
+
* against the process-global meter.
|
|
317
|
+
*/
|
|
318
|
+
evictionCounter;
|
|
319
|
+
constructor(deps = {}) {
|
|
320
|
+
this.client = deps.client;
|
|
321
|
+
this.fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
322
|
+
this.nowFn = deps.now ?? Date.now;
|
|
323
|
+
this.ttlMs = deps.ttlMs ?? DEFAULT_TTL_MS;
|
|
324
|
+
this.fetchTimeoutMs = deps.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
325
|
+
if (!deps.skipMetrics) {
|
|
326
|
+
// The callback observes `cache.size` directly. No labels — the meter's
|
|
327
|
+
// resource attributes (`service.name`, `deployment.environment.name`)
|
|
328
|
+
// already partition by process; further per-instance attributes would
|
|
329
|
+
// double-count when more than one `NpmPackageLookup` lives in the same
|
|
330
|
+
// process (e.g. if a future Clark code path constructs a second one).
|
|
331
|
+
// OTel resolves duplicate observations for the same gauge+attributes by
|
|
332
|
+
// taking the last value within a collection cycle, so even then we'd
|
|
333
|
+
// surface only one of them; emitting them under separate attribute sets
|
|
334
|
+
// would inflate the series count without adding signal.
|
|
335
|
+
this.gauge = resolveCacheSizeGauge();
|
|
336
|
+
this.gaugeCallback = (result) => {
|
|
337
|
+
result.observe(this.cache.size);
|
|
338
|
+
};
|
|
339
|
+
this.gauge.addCallback(this.gaugeCallback);
|
|
340
|
+
this.evictionCounter = resolveCacheEvictionCounter();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Detach the cache-size gauge callback. Call when retiring a long-lived
|
|
345
|
+
* instance (e.g. tearing down a Clark session that owned its own lookup).
|
|
346
|
+
* The OTel API keeps a strong reference to every registered callback, so
|
|
347
|
+
* letting the instance go out of scope without calling this leaves the
|
|
348
|
+
* callback (and the cache it closes over) alive for the lifetime of the
|
|
349
|
+
* process meter. Production code paths that hold the lookup for the
|
|
350
|
+
* lifetime of the process never need to call this; it exists so tests and
|
|
351
|
+
* future cleanup paths can detach cleanly.
|
|
352
|
+
*/
|
|
353
|
+
dispose() {
|
|
354
|
+
if (this.gauge && this.gaugeCallback) {
|
|
355
|
+
this.gauge.removeCallback(this.gaugeCallback);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async lookup(name) {
|
|
359
|
+
const resolved = await this.resolveRegistry(name);
|
|
360
|
+
if (resolved === "fail-closed") {
|
|
361
|
+
// We have evidence of a private-registry org but no safe destination
|
|
362
|
+
// for this name (hard-fail auth, or a transient `source: unreachable`).
|
|
363
|
+
// Returning `registry_unreachable` with `mode: "fail-closed"` tells the
|
|
364
|
+
// agent "the lookup didn't run" without leaking a potentially-private
|
|
365
|
+
// package name to npmjs.org.
|
|
366
|
+
return {
|
|
367
|
+
available: false,
|
|
368
|
+
mode: "fail-closed",
|
|
369
|
+
reason: "registry_unreachable",
|
|
370
|
+
registry_host: "",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const { registry, mode } = resolved;
|
|
374
|
+
const registryBase = registry.url.replace(/\/$/, "");
|
|
375
|
+
const registryHost = new URL(registry.url).host;
|
|
376
|
+
// Key on the normalized registry base URL — host alone collides on
|
|
377
|
+
// Artifactory-style proxies that host multiple npm repos under one
|
|
378
|
+
// host (e.g. `…/api/npm/repo-a/` vs `…/api/npm/repo-b/`). Lowercasing
|
|
379
|
+
// the scheme+host (per RFC 3986 §3) and dropping the trailing slash
|
|
380
|
+
// gives a stable identity for the same registry across capitalisation
|
|
381
|
+
// and slash-trailing variants in the config. `registry_host` in the
|
|
382
|
+
// result keeps the bare host — the cache-key shape is an internal
|
|
383
|
+
// detail callers don't need.
|
|
384
|
+
const cacheKey = `${normalizeRegistryUrl(registry.url)}:${name}`;
|
|
385
|
+
const now = this.nowFn();
|
|
386
|
+
const cached = this.cache.get(cacheKey);
|
|
387
|
+
if (cached && cached.expiresAt > now) {
|
|
388
|
+
return cached.result;
|
|
389
|
+
}
|
|
390
|
+
// Share a single in-flight fetch across concurrent callers for the same
|
|
391
|
+
// key (Clark batch-installs are the hot path). `inFlight.set` happens
|
|
392
|
+
// synchronously before the first `await`, so a second call that enters
|
|
393
|
+
// `lookup()` while the first is still pending always sees the existing
|
|
394
|
+
// promise here and skips the duplicate fetch.
|
|
395
|
+
const inFlight = this.inFlight.get(cacheKey);
|
|
396
|
+
if (inFlight) {
|
|
397
|
+
return inFlight;
|
|
398
|
+
}
|
|
399
|
+
const url = `${registryBase}/${encodePackagePath(name)}`;
|
|
400
|
+
const headers = { accept: "application/json" };
|
|
401
|
+
if (registry.token) {
|
|
402
|
+
headers.authorization = `Bearer ${registry.token}`;
|
|
403
|
+
}
|
|
404
|
+
const promise = this.fetchAndMap(url, headers, registryHost, mode)
|
|
405
|
+
.then((result) => {
|
|
406
|
+
// Cache only stable outcomes. Transient failures must retry on the
|
|
407
|
+
// next call so a token rotation or a short network blip can't pin
|
|
408
|
+
// the failure for a full TTL window.
|
|
409
|
+
if (result.available || result.reason === "not_in_registry") {
|
|
410
|
+
// Bound the cache before inserting a *new* key. Re-inserting an
|
|
411
|
+
// existing key (the common refetch-after-TTL path) is a Map
|
|
412
|
+
// overwrite, not growth, so it never needs eviction — gating on
|
|
413
|
+
// `!has(cacheKey)` keeps the steady-state hot path free of the
|
|
414
|
+
// eviction scan.
|
|
415
|
+
if (!this.cache.has(cacheKey)) {
|
|
416
|
+
this.evictToCapacity(now);
|
|
417
|
+
}
|
|
418
|
+
this.cache.set(cacheKey, { result, expiresAt: now + this.ttlMs });
|
|
419
|
+
}
|
|
420
|
+
return result;
|
|
421
|
+
})
|
|
422
|
+
.finally(() => {
|
|
423
|
+
// Always clear the inflight slot, including on the transient-failure
|
|
424
|
+
// paths the cache skips. Otherwise a single failed fetch would pin
|
|
425
|
+
// every future caller to the same rejected promise.
|
|
426
|
+
this.inFlight.delete(cacheKey);
|
|
427
|
+
});
|
|
428
|
+
this.inFlight.set(cacheKey, promise);
|
|
429
|
+
return promise;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Number of cached entries (successful lookups and `not_in_registry`
|
|
433
|
+
* misses; transient failures aren't cached). Exposed as a gauge for
|
|
434
|
+
* monitoring — keeps the cache footprint visible without poking at
|
|
435
|
+
* private state from outside the class.
|
|
436
|
+
*/
|
|
437
|
+
size() {
|
|
438
|
+
return this.cache.size;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Make room for one new entry when the cache is at/over `MAX_CACHE_SIZE`.
|
|
442
|
+
* Called from the insert path before adding a key that isn't already
|
|
443
|
+
* present.
|
|
444
|
+
*
|
|
445
|
+
* Eviction order, keyed on `expiresAt`:
|
|
446
|
+
*
|
|
447
|
+
* 1. Expired entries first (`expiresAt <= now`). TTL is only checked
|
|
448
|
+
* reactively on read, so a never-re-accessed name's entry would
|
|
449
|
+
* otherwise linger forever; dropping the already-dead ones reclaims
|
|
450
|
+
* space without evicting anything a future read could have used.
|
|
451
|
+
* 2. If that didn't get us under the cap, the oldest-expiring live
|
|
452
|
+
* entries (smallest `expiresAt`) — those are closest to expiry anyway,
|
|
453
|
+
* so they're the cheapest live entries to lose.
|
|
454
|
+
*
|
|
455
|
+
* Sorting the whole keyset is O(n log n), but it only runs once the cache
|
|
456
|
+
* is full (n = `MAX_CACHE_SIZE`) and only on inserts of genuinely-new keys,
|
|
457
|
+
* so it's off the steady-state hot path. Emits the number evicted on the
|
|
458
|
+
* eviction counter so a sustained-eviction-at-flat-size signal is visible
|
|
459
|
+
* in Grafana alongside the existing `cache_size` gauge.
|
|
460
|
+
*/
|
|
461
|
+
evictToCapacity(now) {
|
|
462
|
+
if (this.cache.size < MAX_CACHE_SIZE) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
// Target one slot below the cap so the imminent `set` lands at the cap,
|
|
466
|
+
// not over it.
|
|
467
|
+
const target = MAX_CACHE_SIZE - 1;
|
|
468
|
+
let evicted = 0;
|
|
469
|
+
// Pass 1: expired entries.
|
|
470
|
+
for (const [key, entry] of this.cache) {
|
|
471
|
+
if (this.cache.size <= target) {
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
if (entry.expiresAt <= now) {
|
|
475
|
+
this.cache.delete(key);
|
|
476
|
+
evicted += 1;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// Pass 2: oldest-expiring live entries, if still over the cap.
|
|
480
|
+
if (this.cache.size > target) {
|
|
481
|
+
const byExpiry = Array.from(this.cache.entries()).sort((a, b) => a[1].expiresAt - b[1].expiresAt);
|
|
482
|
+
for (const [key] of byExpiry) {
|
|
483
|
+
if (this.cache.size <= target) {
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
this.cache.delete(key);
|
|
487
|
+
evicted += 1;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (evicted > 0) {
|
|
491
|
+
this.evictionCounter?.add(evicted);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Resolve the registry this lookup will hit:
|
|
496
|
+
*
|
|
497
|
+
* - No client wired (SaaS mode): public npm.
|
|
498
|
+
* - `source: not-configured` (operator never opted in): public npm.
|
|
499
|
+
* - `source: configured | stale`, name matches `config.scopes[@scope]`:
|
|
500
|
+
* scoped private registry.
|
|
501
|
+
* - `source: configured | stale`, no scope match, `config.default`
|
|
502
|
+
* present: default private registry.
|
|
503
|
+
*
|
|
504
|
+
* Fail-closed (returns `"fail-closed"`; caller surfaces
|
|
505
|
+
* `registry_unreachable` and never touches the network):
|
|
506
|
+
*
|
|
507
|
+
* - `getConfig()` threw on hard-fail auth (revoked JWT, double 401
|
|
508
|
+
* against the Superblocks server). In that state we'd silently
|
|
509
|
+
* downgrade from "checking the private registry" to "checking
|
|
510
|
+
* public npm" — different trust boundary, different answer. The
|
|
511
|
+
* agent's reasoning about install feasibility depends on which
|
|
512
|
+
* registry actually answered; failing closed surfaces the auth
|
|
513
|
+
* problem instead of hiding it behind a misleading public-npm
|
|
514
|
+
* result.
|
|
515
|
+
* - `source: unreachable` AND `everConfigured === true` (transient
|
|
516
|
+
* cold-cache outage against the Superblocks server for an org that
|
|
517
|
+
* has at some prior point successfully resolved a configured
|
|
518
|
+
* registry). Same trust gap as the throw branch: we don't know the
|
|
519
|
+
* org's current registry state, but the on-disk marker says it has
|
|
520
|
+
* a private registry, so leaking the name to npmjs would be a silent
|
|
521
|
+
* supply-chain leak during the outage window. Symmetric with
|
|
522
|
+
* `syncHomeNpmrc` and `maybeWriteNpmrcForDir`, both of which refuse
|
|
523
|
+
* to act on `unreachable` for the same reason.
|
|
524
|
+
*
|
|
525
|
+
* When `everConfigured` is `false` (greenfield org / no disk marker)
|
|
526
|
+
* or `undefined` (no store wired in — older call sites, test
|
|
527
|
+
* fixtures), we do NOT fail closed. The `unreachable` config is
|
|
528
|
+
* `{ configured: false }`, so we proceed exactly as the install path
|
|
529
|
+
* does (`AppShell.maybeShortCircuitForUnreachable` only blocks when
|
|
530
|
+
* `everConfigured === true`): fall through to public npm. A
|
|
531
|
+
* greenfield org has no private registry, so a metadata lookup
|
|
532
|
+
* against public npm during a config-server outage leaks nothing
|
|
533
|
+
* sensitive — and keeping the lookup aligned with the install gate
|
|
534
|
+
* removes the lookup-vs-install divergence where the lookup reported
|
|
535
|
+
* `fail-closed` while a direct install of the same package would
|
|
536
|
+
* have proceeded (APPS-4425).
|
|
537
|
+
* - `source: configured | stale`, no scope match, no `config.default`.
|
|
538
|
+
* The operator wired scoped-only mappings and the requested name
|
|
539
|
+
* doesn't match any of them — there's no in-policy destination, so
|
|
540
|
+
* answering from npmjs.org would report availability from the wrong
|
|
541
|
+
* trust boundary. Same shape as the other two: surface the gap to
|
|
542
|
+
* the agent rather than silently downgrading.
|
|
543
|
+
*/
|
|
544
|
+
async resolveRegistry(name) {
|
|
545
|
+
if (!this.client) {
|
|
546
|
+
return {
|
|
547
|
+
registry: { url: PUBLIC_NPM_REGISTRY },
|
|
548
|
+
mode: "public-npm",
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
let fetchResult;
|
|
552
|
+
try {
|
|
553
|
+
fetchResult = await this.client.getConfig();
|
|
554
|
+
}
|
|
555
|
+
catch (err) {
|
|
556
|
+
getLogger().warn("[npm-package-lookup] registry client getConfig threw; failing closed rather than silently downgrading to public npm", { error: err instanceof Error ? err.message : String(err) });
|
|
557
|
+
return "fail-closed";
|
|
558
|
+
}
|
|
559
|
+
if (fetchResult.source === "unreachable") {
|
|
560
|
+
// Transient cold-cache outage against the Superblocks server. Gate
|
|
561
|
+
// the fail-closed decision on `everConfigured` so the lookup matches
|
|
562
|
+
// the install gate (`AppShell.maybeShortCircuitForUnreachable`), which
|
|
563
|
+
// blocks only when `everConfigured === true` (APPS-4425).
|
|
564
|
+
if (fetchResult.everConfigured === true) {
|
|
565
|
+
// The on-disk marker says this org has at some prior point resolved
|
|
566
|
+
// a configured private registry. We don't know its current state
|
|
567
|
+
// during the outage, so the same trust-gap argument as the
|
|
568
|
+
// `getConfig threw` branch applies: falling through to public npm
|
|
569
|
+
// would leak the requested name — which the agent picked precisely
|
|
570
|
+
// because it's targeting the (possibly-private) registry — to
|
|
571
|
+
// npmjs.org. Fail-closed; caller surfaces `registry_unreachable`.
|
|
572
|
+
// Symmetric with `syncHomeNpmrc` and `maybeWriteNpmrcForDir`, both
|
|
573
|
+
// of which refuse to act on `source: "unreachable"`.
|
|
574
|
+
getLogger().warn("[npm-package-lookup] registry config unreachable for a previously-configured org; failing closed rather than silently downgrading to public npm");
|
|
575
|
+
return "fail-closed";
|
|
576
|
+
}
|
|
577
|
+
// `everConfigured` is `false` (greenfield org / no disk marker) or
|
|
578
|
+
// `undefined` (no store wired in). A greenfield org has no private
|
|
579
|
+
// registry, so a metadata lookup against public npm during the
|
|
580
|
+
// config-server outage leaks nothing sensitive — and a direct
|
|
581
|
+
// install of the same name would proceed through public npm anyway,
|
|
582
|
+
// since the install path only short-circuits when `everConfigured ===
|
|
583
|
+
// true`. Proceed to public npm so the lookup doesn't diverge from
|
|
584
|
+
// install reality. The `unreachable` config is `{ configured: false }`,
|
|
585
|
+
// so this is the same destination `not-configured` resolves to.
|
|
586
|
+
return {
|
|
587
|
+
registry: { url: PUBLIC_NPM_REGISTRY },
|
|
588
|
+
mode: "public-npm",
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
if (fetchResult.source === "not-configured") {
|
|
592
|
+
return {
|
|
593
|
+
registry: { url: PUBLIC_NPM_REGISTRY },
|
|
594
|
+
mode: "public-npm",
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const selected = selectRegistry(fetchResult.config, name);
|
|
598
|
+
if (selected) {
|
|
599
|
+
return { registry: selected, mode: "private" };
|
|
600
|
+
}
|
|
601
|
+
// Configured org with scoped-only mappings and no default — the
|
|
602
|
+
// requested name doesn't match any scope, so we have no in-policy
|
|
603
|
+
// destination. Fail closed rather than silently downgrading to public
|
|
604
|
+
// npm: the operator's `npm-registry` config implies a trust boundary
|
|
605
|
+
// (only mapped scopes are blessed for install), and answering
|
|
606
|
+
// `available: true` from npmjs.org for `react` against an org that
|
|
607
|
+
// never wired a default registry reports availability from the wrong
|
|
608
|
+
// boundary. The agent should treat this as a configuration problem,
|
|
609
|
+
// not a green-light to install. (Symmetric with the `getConfig`-threw
|
|
610
|
+
// and `source: unreachable` branches above.)
|
|
611
|
+
getLogger().warn("[npm-package-lookup] configured registry has scopes but no default and the requested name does not match any scope; failing closed rather than silently downgrading to public npm", { name });
|
|
612
|
+
return "fail-closed";
|
|
613
|
+
}
|
|
614
|
+
async fetchAndMap(url, headers, registryHost, mode) {
|
|
615
|
+
let response;
|
|
616
|
+
try {
|
|
617
|
+
response = await this.fetchImpl(url, {
|
|
618
|
+
method: "GET",
|
|
619
|
+
headers,
|
|
620
|
+
signal: AbortSignal.timeout(this.fetchTimeoutMs),
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
catch (err) {
|
|
624
|
+
getLogger().warn("[npm-package-lookup] registry unreachable", {
|
|
625
|
+
registryHost,
|
|
626
|
+
error: err instanceof Error ? err.message : String(err),
|
|
627
|
+
});
|
|
628
|
+
return {
|
|
629
|
+
available: false,
|
|
630
|
+
mode,
|
|
631
|
+
reason: "registry_unreachable",
|
|
632
|
+
registry_host: registryHost,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
if (response.status === 200) {
|
|
636
|
+
let latest;
|
|
637
|
+
let recent;
|
|
638
|
+
try {
|
|
639
|
+
const parsed = NpmRegistryMetadataSchema.safeParse(await response.json());
|
|
640
|
+
if (parsed.success) {
|
|
641
|
+
latest = parsed.data["dist-tags"]?.latest;
|
|
642
|
+
recent = extractRecentVersions(parsed.data);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
// 200 with non-JSON body: package still exists; just no latest
|
|
647
|
+
// and no version list.
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
available: true,
|
|
651
|
+
mode,
|
|
652
|
+
registry_host: registryHost,
|
|
653
|
+
...(latest ? { latest_version: latest } : {}),
|
|
654
|
+
...(recent && recent.length > 0 ? { recent_versions: recent } : {}),
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
if (response.status === 404) {
|
|
658
|
+
await drainSilently(response);
|
|
659
|
+
return {
|
|
660
|
+
available: false,
|
|
661
|
+
mode,
|
|
662
|
+
reason: "not_in_registry",
|
|
663
|
+
registry_host: registryHost,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
if (response.status === 401 || response.status === 403) {
|
|
667
|
+
getLogger().warn("[npm-package-lookup] registry auth failed", {
|
|
668
|
+
registryHost,
|
|
669
|
+
status: response.status,
|
|
670
|
+
});
|
|
671
|
+
await drainSilently(response);
|
|
672
|
+
return {
|
|
673
|
+
available: false,
|
|
674
|
+
mode,
|
|
675
|
+
reason: "registry_auth_failed",
|
|
676
|
+
registry_host: registryHost,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
// 5xx, unexpected 4xx, etc. → treat as a transient infra problem rather
|
|
680
|
+
// than `not_in_registry`. Mapping them to `not_in_registry` would let a
|
|
681
|
+
// flaky proxy silently convince Clark the package is missing.
|
|
682
|
+
getLogger().warn("[npm-package-lookup] registry returned unexpected status", {
|
|
683
|
+
registryHost,
|
|
684
|
+
status: response.status,
|
|
685
|
+
});
|
|
686
|
+
await drainSilently(response);
|
|
687
|
+
return {
|
|
688
|
+
available: false,
|
|
689
|
+
mode,
|
|
690
|
+
reason: "registry_unreachable",
|
|
691
|
+
registry_host: registryHost,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Pull recent published versions out of a parsed npm metadata response,
|
|
697
|
+
* capped at `RECENT_VERSIONS_CAP`. Only `semver.valid()` strings are kept
|
|
698
|
+
* so a hostile or misconfigured registry can't smuggle non-version bytes
|
|
699
|
+
* into the model-visible output.
|
|
700
|
+
*
|
|
701
|
+
* Ordering:
|
|
702
|
+
* - If the `time` map is present (npmjs, Verdaccio, and most proxies
|
|
703
|
+
* include it), sort versions that have a string timestamp by
|
|
704
|
+
* published-at descending. This gives the agent the most-recently-
|
|
705
|
+
* released versions first, which matches the "use a recent version"
|
|
706
|
+
* instruction in the base prompt.
|
|
707
|
+
* - Versions present in `versions` but missing a string timestamp in
|
|
708
|
+
* `time` (some private proxies drop per-version entries while keeping
|
|
709
|
+
* others, or npm's object-valued `unpublished` pseudo-key takes the
|
|
710
|
+
* slot a real timestamp would have occupied) sort by semver descending
|
|
711
|
+
* and are appended AFTER the timestamped block. Bucketing this way
|
|
712
|
+
* keeps the comparator transitive — a single mixed comparator that
|
|
713
|
+
* falls back to semver per-pair-when-either-side-is-missing is
|
|
714
|
+
* non-transitive (time-order and semver-order can disagree, producing
|
|
715
|
+
* cycles), which makes JS `Array.prototype.sort` undefined behaviour.
|
|
716
|
+
* - If `time` is absent entirely, sort by semver descending. Some
|
|
717
|
+
* private registries strip `time` from their proxy responses; fall
|
|
718
|
+
* back to a deterministic ordering rather than returning nothing.
|
|
719
|
+
*
|
|
720
|
+
* Pseudo-keys in the `time` map (`created`, `modified`, `unpublished`) are
|
|
721
|
+
* dropped — they aren't real versions, so `semver.valid()` rejects them.
|
|
722
|
+
*/
|
|
723
|
+
function extractRecentVersions(parsed) {
|
|
724
|
+
const candidateSet = new Set();
|
|
725
|
+
if (parsed.versions) {
|
|
726
|
+
for (const key of Object.keys(parsed.versions)) {
|
|
727
|
+
if (semver.valid(key) !== null) {
|
|
728
|
+
candidateSet.add(key);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (parsed.time) {
|
|
733
|
+
for (const key of Object.keys(parsed.time)) {
|
|
734
|
+
if (semver.valid(key) !== null) {
|
|
735
|
+
candidateSet.add(key);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (candidateSet.size === 0) {
|
|
740
|
+
return undefined;
|
|
741
|
+
}
|
|
742
|
+
const candidates = Array.from(candidateSet);
|
|
743
|
+
if (parsed.time) {
|
|
744
|
+
const time = parsed.time;
|
|
745
|
+
const withTimestamp = [];
|
|
746
|
+
const withoutTimestamp = [];
|
|
747
|
+
for (const version of candidates) {
|
|
748
|
+
const t = time[version];
|
|
749
|
+
if (typeof t === "string") {
|
|
750
|
+
withTimestamp.push({ version, timestamp: t });
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
withoutTimestamp.push(version);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
withTimestamp.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
757
|
+
withoutTimestamp.sort((a, b) => semver.rcompare(a, b));
|
|
758
|
+
return [
|
|
759
|
+
...withTimestamp.map((entry) => entry.version),
|
|
760
|
+
...withoutTimestamp,
|
|
761
|
+
].slice(0, RECENT_VERSIONS_CAP);
|
|
762
|
+
}
|
|
763
|
+
candidates.sort((a, b) => semver.rcompare(a, b));
|
|
764
|
+
return candidates.slice(0, RECENT_VERSIONS_CAP);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Consume the response body and discard it. Called on every non-200 path
|
|
768
|
+
* for two reasons that happen to be solved by the same operation:
|
|
769
|
+
*
|
|
770
|
+
* 1. Connection-pool hygiene. Node's undici-backed `fetch` keeps the
|
|
771
|
+
* underlying TCP connection pinned to a response until its body is
|
|
772
|
+
* consumed. Skipping the read leaves the connection unavailable to the
|
|
773
|
+
* next call until GC reclaims it, which over a long Clark session
|
|
774
|
+
* against the same registry host adds up.
|
|
775
|
+
* 2. Token-echo defense for 401/403 specifically. A hostile or
|
|
776
|
+
* misconfigured registry can include the submitted bearer token in the
|
|
777
|
+
* error body (Artifactory does this in some configs). The only way to
|
|
778
|
+
* guarantee those bytes never reach the tool's output, a log line, or
|
|
779
|
+
* a tracing span is to never read them.
|
|
780
|
+
*
|
|
781
|
+
* The catch is intentionally broad: if the stream errored or was already
|
|
782
|
+
* consumed, there is nothing useful to do, and surfacing that failure would
|
|
783
|
+
* shadow the real status code the caller branches on.
|
|
784
|
+
*/
|
|
785
|
+
async function drainSilently(response) {
|
|
786
|
+
try {
|
|
787
|
+
await response.text();
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
// Body already consumed or stream errored — nothing more to do.
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
//# sourceMappingURL=npm-package-lookup.js.map
|