akm-cli 0.9.1 → 0.9.2-alpha.2
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/CHANGELOG.md +103 -28
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +5 -6
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +53 -22
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +901 -177
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +85 -35
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +31 -33
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +203 -229
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +64 -71
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +159 -36
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -224
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { isIP } from "node:net";
|
|
5
|
+
import { bareHostname, classifyNetworkAddress, classifyNetworkHostname, resolveHostnameAddresses, } from "../core/network-policy.js";
|
|
6
|
+
import { assertRegistryPinnedTransportAvailable, requestRegistryAddressPinned, } from "./pinned-transport.js";
|
|
7
|
+
export { requestRegistryAddressPinned } from "./pinned-transport.js";
|
|
8
|
+
export class RegistryNetworkError extends Error {
|
|
9
|
+
code = "REGISTRY_NETWORK_POLICY";
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "RegistryNetworkError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const GITHUB_API_ORIGIN = "https://api.github.com";
|
|
16
|
+
const MAX_REGISTRY_REDIRECTS = 8;
|
|
17
|
+
const MAX_REGISTRY_RETRY_DELAY_MS = 30_000;
|
|
18
|
+
let allowRegistryFixturesForTests = false;
|
|
19
|
+
let registryHostnameResolverForTests;
|
|
20
|
+
let registryPinnedRequestForTests;
|
|
21
|
+
/**
|
|
22
|
+
* Single outbound HTTP boundary for registry metadata requests.
|
|
23
|
+
*
|
|
24
|
+
* Every retry and redirect returns through this function's resolve, validate,
|
|
25
|
+
* and pinned-connect sequence before another request can start.
|
|
26
|
+
*/
|
|
27
|
+
export async function fetchRegistryResponse(rawUrl, init, options, redirectCount = 0) {
|
|
28
|
+
const url = parseRegistryUrl(rawUrl);
|
|
29
|
+
const response = await requestRegistryHop(url, init, options);
|
|
30
|
+
if (!isRedirect(response.status))
|
|
31
|
+
return response;
|
|
32
|
+
if (options.policy.kind === "github-api") {
|
|
33
|
+
await cancelRegistryResponse(response);
|
|
34
|
+
throw new RegistryNetworkError("GitHub API registry metadata policy does not permit redirects");
|
|
35
|
+
}
|
|
36
|
+
if (redirectCount >= MAX_REGISTRY_REDIRECTS) {
|
|
37
|
+
await cancelRegistryResponse(response);
|
|
38
|
+
throw new RegistryNetworkError(`Too many redirects while fetching registry metadata from ${url.origin}`);
|
|
39
|
+
}
|
|
40
|
+
const location = response.headers.get("location");
|
|
41
|
+
if (!location) {
|
|
42
|
+
await cancelRegistryResponse(response);
|
|
43
|
+
throw new RegistryNetworkError(`Redirect from registry host ${url.hostname} did not include Location`);
|
|
44
|
+
}
|
|
45
|
+
await cancelRegistryResponse(response);
|
|
46
|
+
let nextUrl;
|
|
47
|
+
try {
|
|
48
|
+
nextUrl = new URL(location, url);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new RegistryNetworkError(`Redirect from registry host ${url.hostname} included an invalid Location`);
|
|
52
|
+
}
|
|
53
|
+
const nextInit = redirectInit(init, url, nextUrl, response.status);
|
|
54
|
+
return fetchRegistryResponse(nextUrl.toString(), nextInit, options, redirectCount + 1);
|
|
55
|
+
}
|
|
56
|
+
async function requestRegistryHop(url, init, options) {
|
|
57
|
+
const maxRetries = options.retries ?? 3;
|
|
58
|
+
const injectedTransport = options.requestPinnedForTesting ?? registryPinnedRequestForTests;
|
|
59
|
+
if (!injectedTransport)
|
|
60
|
+
assertRegistryPinnedTransportAvailable();
|
|
61
|
+
const transport = injectedTransport ?? requestRegistryAddressPinned;
|
|
62
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
63
|
+
const attemptDeadline = Date.now() + options.timeoutMs;
|
|
64
|
+
const allowPrivate = privateNetworkAllowed(url, options);
|
|
65
|
+
const addresses = await validatedRegistryAddresses(url, options, allowPrivate, options.timeoutMs, init?.signal);
|
|
66
|
+
if (Date.now() >= attemptDeadline) {
|
|
67
|
+
throw new RegistryNetworkError(`Registry request to ${url.hostname} attempt deadline expired before transport could start`);
|
|
68
|
+
}
|
|
69
|
+
const address = addresses[attempt % addresses.length];
|
|
70
|
+
if (!address) {
|
|
71
|
+
throw new RegistryNetworkError(`Refusing registry request to ${url.hostname}: no validated address available`);
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const remainingMs = attemptDeadline - Date.now();
|
|
75
|
+
if (remainingMs <= 0) {
|
|
76
|
+
throw new RegistryNetworkError(`Registry request to ${url.hostname} attempt deadline expired before transport could start`);
|
|
77
|
+
}
|
|
78
|
+
const response = await transport(url, address, init, remainingMs);
|
|
79
|
+
if (attempt < maxRetries && shouldRetry(response.status)) {
|
|
80
|
+
const delay = retryDelay(response, attempt);
|
|
81
|
+
await cancelRegistryResponse(response);
|
|
82
|
+
await abortableDelay(delay, init?.signal);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
return response;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (attempt >= maxRetries || init?.signal?.aborted)
|
|
89
|
+
throw error;
|
|
90
|
+
await abortableDelay(backoffDelay(attempt), init?.signal);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw new Error("Registry retry loop is unreachable");
|
|
94
|
+
}
|
|
95
|
+
function parseRegistryUrl(rawUrl) {
|
|
96
|
+
let url;
|
|
97
|
+
try {
|
|
98
|
+
url = new URL(rawUrl);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
throw new RegistryNetworkError("Registry request URL is invalid");
|
|
102
|
+
}
|
|
103
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
104
|
+
throw new RegistryNetworkError(`Registry requests require HTTP(S), not ${url.protocol || "an empty scheme"}`);
|
|
105
|
+
}
|
|
106
|
+
if (url.username || url.password) {
|
|
107
|
+
throw new RegistryNetworkError(`Registry request URL must not contain embedded credentials: ${url.hostname}`);
|
|
108
|
+
}
|
|
109
|
+
return url;
|
|
110
|
+
}
|
|
111
|
+
function privateNetworkAllowed(url, options) {
|
|
112
|
+
if (options.allowPrivateHostsForTesting === true)
|
|
113
|
+
return true;
|
|
114
|
+
if (options.policy.kind !== "npm-api" || !options.policy.allowPrivateRegistryOrigin)
|
|
115
|
+
return false;
|
|
116
|
+
const expectedOrigin = normalizedOrigin(options.policy.registryOrigin, "npm registry");
|
|
117
|
+
return url.origin === expectedOrigin;
|
|
118
|
+
}
|
|
119
|
+
async function validatedRegistryAddresses(url, options, allowPrivate, timeoutMs, signal) {
|
|
120
|
+
assertPolicyOrigin(url, options.policy);
|
|
121
|
+
const hostname = bareHostname(url.hostname);
|
|
122
|
+
const literal = isIP(hostname) !== 0;
|
|
123
|
+
const hostClass = classifyNetworkHostname(hostname);
|
|
124
|
+
assertAllowedClass(hostClass, hostname, allowPrivate);
|
|
125
|
+
if (literal)
|
|
126
|
+
return [hostname];
|
|
127
|
+
let addresses;
|
|
128
|
+
try {
|
|
129
|
+
addresses = await resolveAddressesWithDeadline(hostname, options.resolveHostname ?? registryHostnameResolverForTests, timeoutMs, signal);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof RegistryNetworkError || signal?.aborted)
|
|
133
|
+
throw error;
|
|
134
|
+
throw new RegistryNetworkError(`Refusing registry request to ${hostname}: DNS resolution failed`);
|
|
135
|
+
}
|
|
136
|
+
if (addresses.length === 0) {
|
|
137
|
+
throw new RegistryNetworkError(`Refusing registry request to ${hostname}: hostname resolved to no addresses`);
|
|
138
|
+
}
|
|
139
|
+
for (const address of addresses) {
|
|
140
|
+
const addressClass = classifyNetworkAddress(address);
|
|
141
|
+
assertAllowedClass(addressClass, `${hostname} (${address})`, allowPrivate, true);
|
|
142
|
+
}
|
|
143
|
+
return addresses;
|
|
144
|
+
}
|
|
145
|
+
async function resolveAddressesWithDeadline(hostname, resolver, timeoutMs, signal) {
|
|
146
|
+
if (signal?.aborted)
|
|
147
|
+
throw signal.reason ?? new Error("Registry request aborted");
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
let settled = false;
|
|
150
|
+
const cleanup = () => {
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
signal?.removeEventListener("abort", onAbort);
|
|
153
|
+
};
|
|
154
|
+
const finish = (action) => {
|
|
155
|
+
if (settled)
|
|
156
|
+
return;
|
|
157
|
+
settled = true;
|
|
158
|
+
cleanup();
|
|
159
|
+
action();
|
|
160
|
+
};
|
|
161
|
+
const onAbort = () => finish(() => reject(signal?.reason ?? new Error("Registry request aborted")));
|
|
162
|
+
const timer = setTimeout(() => finish(() => reject(new RegistryNetworkError(`Refusing registry request to ${hostname}: DNS resolution timed out after ${timeoutMs}ms`))), Math.max(1, timeoutMs));
|
|
163
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
164
|
+
void resolveHostnameAddresses(hostname, resolver).then((addresses) => finish(() => resolve(addresses)), (error) => finish(() => reject(error)));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function assertPolicyOrigin(url, policy) {
|
|
168
|
+
if (policy.kind === "github-api" && url.origin !== GITHUB_API_ORIGIN) {
|
|
169
|
+
throw new RegistryNetworkError(`GitHub API origin policy rejected registry request to ${url.origin}`);
|
|
170
|
+
}
|
|
171
|
+
if (policy.kind === "npm-api") {
|
|
172
|
+
const expected = normalizedOrigin(policy.registryOrigin, "npm registry");
|
|
173
|
+
if (url.origin !== expected && classifyNetworkHostname(url.hostname) !== "public") {
|
|
174
|
+
throw new RegistryNetworkError(`npm registry redirect policy rejected non-public origin ${url.origin}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function normalizedOrigin(rawUrl, label) {
|
|
179
|
+
try {
|
|
180
|
+
return new URL(rawUrl).origin;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
throw new RegistryNetworkError(`Configured ${label} origin is invalid`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function assertAllowedClass(addressClass, destination, allowPrivate, resolved = false) {
|
|
187
|
+
if (addressClass === "public")
|
|
188
|
+
return;
|
|
189
|
+
if (allowPrivate && (addressClass === "private" || addressClass === "loopback"))
|
|
190
|
+
return;
|
|
191
|
+
const qualifier = resolved ? "resolves to " : "is ";
|
|
192
|
+
throw new RegistryNetworkError(`Refusing registry request: ${destination} ${qualifier}non-public ${addressClass} destination`);
|
|
193
|
+
}
|
|
194
|
+
function redirectInit(init, currentUrl, nextUrl, status) {
|
|
195
|
+
if (!init)
|
|
196
|
+
return init;
|
|
197
|
+
const headers = new Headers(init.headers);
|
|
198
|
+
if (currentUrl.origin !== nextUrl.origin) {
|
|
199
|
+
headers.delete("authorization");
|
|
200
|
+
headers.delete("cookie");
|
|
201
|
+
headers.delete("proxy-authorization");
|
|
202
|
+
}
|
|
203
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
204
|
+
const switchToGet = ((status === 301 || status === 302) && method === "POST") ||
|
|
205
|
+
(status === 303 && method !== "GET" && method !== "HEAD");
|
|
206
|
+
if (!switchToGet)
|
|
207
|
+
return { ...init, headers };
|
|
208
|
+
for (const name of ["content-encoding", "content-language", "content-length", "content-location", "content-type"]) {
|
|
209
|
+
headers.delete(name);
|
|
210
|
+
}
|
|
211
|
+
return { ...init, method: "GET", body: undefined, headers };
|
|
212
|
+
}
|
|
213
|
+
function isRedirect(status) {
|
|
214
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
215
|
+
}
|
|
216
|
+
/** Release a terminal response body so pinned helper/socket resources cannot linger. */
|
|
217
|
+
export async function cancelRegistryResponse(response) {
|
|
218
|
+
await response.body?.cancel().catch(() => undefined);
|
|
219
|
+
}
|
|
220
|
+
function shouldRetry(status) {
|
|
221
|
+
return status === 429 || status >= 500;
|
|
222
|
+
}
|
|
223
|
+
function retryDelay(response, attempt) {
|
|
224
|
+
const retryAfter = response.headers.get("retry-after");
|
|
225
|
+
if (retryAfter) {
|
|
226
|
+
const seconds = Number(retryAfter);
|
|
227
|
+
if (Number.isFinite(seconds)) {
|
|
228
|
+
return seconds >= 0 ? Math.min(MAX_REGISTRY_RETRY_DELAY_MS, seconds * 1_000) : backoffDelay(attempt);
|
|
229
|
+
}
|
|
230
|
+
const date = Date.parse(retryAfter);
|
|
231
|
+
if (Number.isFinite(date))
|
|
232
|
+
return Math.min(MAX_REGISTRY_RETRY_DELAY_MS, Math.max(0, date - Date.now()));
|
|
233
|
+
}
|
|
234
|
+
return backoffDelay(attempt);
|
|
235
|
+
}
|
|
236
|
+
function backoffDelay(attempt) {
|
|
237
|
+
return Math.min(MAX_REGISTRY_RETRY_DELAY_MS, 500 * 2 ** attempt * (0.5 + Math.random() * 0.5));
|
|
238
|
+
}
|
|
239
|
+
/** Test-only visibility for the server-controlled Retry-After clamp. */
|
|
240
|
+
export function _registryRetryDelayForTests(response, attempt) {
|
|
241
|
+
return retryDelay(response, attempt);
|
|
242
|
+
}
|
|
243
|
+
function abortableDelay(ms, signal) {
|
|
244
|
+
if (!signal)
|
|
245
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
246
|
+
if (signal.aborted)
|
|
247
|
+
return Promise.reject(signal.reason ?? new Error("Registry request aborted"));
|
|
248
|
+
return new Promise((resolve, reject) => {
|
|
249
|
+
const timer = setTimeout(() => {
|
|
250
|
+
signal.removeEventListener("abort", onAbort);
|
|
251
|
+
resolve();
|
|
252
|
+
}, ms);
|
|
253
|
+
const onAbort = () => {
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
reject(signal.reason ?? new Error("Registry request aborted"));
|
|
256
|
+
};
|
|
257
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/** Existing local HTTP fixtures remain available only inside the test process. */
|
|
261
|
+
export function allowPrivateRegistryFixtureForTests(rawUrl) {
|
|
262
|
+
if (!allowRegistryFixturesForTests)
|
|
263
|
+
return false;
|
|
264
|
+
try {
|
|
265
|
+
const url = new URL(rawUrl);
|
|
266
|
+
const hostClass = classifyNetworkHostname(url.hostname);
|
|
267
|
+
const port = Number.parseInt(url.port, 10);
|
|
268
|
+
return hostClass === "loopback" && Number.isInteger(port) && port >= 1024 && port <= 65_535;
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** Test-harness seam; production composition never enables these overrides. */
|
|
275
|
+
export function _setRegistryNetworkOverridesForTests(overrides) {
|
|
276
|
+
allowRegistryFixturesForTests = overrides?.allowLoopbackFixtures === true;
|
|
277
|
+
registryHostnameResolverForTests = overrides?.resolveHostname;
|
|
278
|
+
registryPinnedRequestForTests = overrides?.requestPinned;
|
|
279
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/** Parse and validate the private stdin request before any socket is opened. */
|
|
5
|
+
async function readNodePinnedHelperRequest(isIP) {
|
|
6
|
+
const MAX_PRELUDE_BYTES = 64 * 1024;
|
|
7
|
+
const iterator = process.stdin[Symbol.asyncIterator]();
|
|
8
|
+
let buffered = Buffer.alloc(0);
|
|
9
|
+
const readExact = async (length) => {
|
|
10
|
+
while (buffered.byteLength < length) {
|
|
11
|
+
const next = await iterator.next();
|
|
12
|
+
if (next.done)
|
|
13
|
+
throw new Error("Registry helper input ended before the request prelude was complete");
|
|
14
|
+
buffered = Buffer.concat([buffered, Buffer.from(next.value)]);
|
|
15
|
+
}
|
|
16
|
+
const result = buffered.subarray(0, length);
|
|
17
|
+
buffered = buffered.subarray(length);
|
|
18
|
+
return result;
|
|
19
|
+
};
|
|
20
|
+
const preludeLength = (await readExact(4)).readUInt32BE(0);
|
|
21
|
+
if (preludeLength === 0 || preludeLength > MAX_PRELUDE_BYTES) {
|
|
22
|
+
throw new Error("Registry helper request prelude exceeds its protocol limit");
|
|
23
|
+
}
|
|
24
|
+
const raw = JSON.parse((await readExact(preludeLength)).toString("utf8"));
|
|
25
|
+
if (typeof raw.url !== "string" || typeof raw.address !== "string") {
|
|
26
|
+
throw new Error("Registry helper request prelude is invalid");
|
|
27
|
+
}
|
|
28
|
+
const url = new URL(raw.url);
|
|
29
|
+
if ((url.protocol !== "http:" && url.protocol !== "https:") || url.username || url.password) {
|
|
30
|
+
throw new Error("Registry helper accepts only credential-free HTTP(S) URLs");
|
|
31
|
+
}
|
|
32
|
+
const addressFamily = isIP(raw.address);
|
|
33
|
+
if (addressFamily === 0)
|
|
34
|
+
throw new Error("Registry helper requires a numeric pinned address");
|
|
35
|
+
if (typeof raw.timeoutMs !== "number" || !Number.isSafeInteger(raw.timeoutMs) || raw.timeoutMs <= 0) {
|
|
36
|
+
throw new Error("Registry helper timeout is invalid");
|
|
37
|
+
}
|
|
38
|
+
if (!Array.isArray(raw.headers) || raw.headers.length > 256) {
|
|
39
|
+
throw new Error("Registry helper headers are invalid");
|
|
40
|
+
}
|
|
41
|
+
const headers = {};
|
|
42
|
+
for (const pair of raw.headers) {
|
|
43
|
+
if (!Array.isArray(pair) ||
|
|
44
|
+
pair.length !== 2 ||
|
|
45
|
+
typeof pair[0] !== "string" ||
|
|
46
|
+
typeof pair[1] !== "string" ||
|
|
47
|
+
pair[0].length > 256 ||
|
|
48
|
+
pair[1].length > 16_384) {
|
|
49
|
+
throw new Error("Registry helper header entry is invalid");
|
|
50
|
+
}
|
|
51
|
+
headers[pair[0].toLowerCase()] = pair[1];
|
|
52
|
+
}
|
|
53
|
+
for (const name of [
|
|
54
|
+
"connection",
|
|
55
|
+
"keep-alive",
|
|
56
|
+
"proxy-authorization",
|
|
57
|
+
"proxy-connection",
|
|
58
|
+
"te",
|
|
59
|
+
"trailer",
|
|
60
|
+
"transfer-encoding",
|
|
61
|
+
"upgrade",
|
|
62
|
+
]) {
|
|
63
|
+
delete headers[name];
|
|
64
|
+
}
|
|
65
|
+
headers.host = url.host;
|
|
66
|
+
return {
|
|
67
|
+
addressFamily: addressFamily,
|
|
68
|
+
bodyPresent: raw.bodyPresent === true,
|
|
69
|
+
headers,
|
|
70
|
+
initialBody: buffered,
|
|
71
|
+
iterator,
|
|
72
|
+
method: typeof raw.method === "string" ? raw.method.toUpperCase() : "GET",
|
|
73
|
+
pinnedAddress: raw.address,
|
|
74
|
+
timeoutMs: raw.timeoutMs,
|
|
75
|
+
url,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The body of the one-request Node helper used by Bun's registry transport.
|
|
80
|
+
* Its parser is passed explicitly when the parent serializes both functions.
|
|
81
|
+
*/
|
|
82
|
+
export async function nodePinnedRequestHelperMain(readRequest, http, https, isIP) {
|
|
83
|
+
const MAX_FRAME_BYTES = 1024 * 1024;
|
|
84
|
+
const FRAME_PRELUDE = 0;
|
|
85
|
+
const FRAME_BODY = 1;
|
|
86
|
+
const FRAME_END = 2;
|
|
87
|
+
const FRAME_ERROR = 3;
|
|
88
|
+
const writeBuffer = async (value) => {
|
|
89
|
+
await new Promise((resolve, reject) => {
|
|
90
|
+
process.stdout.write(value, (error) => (error ? reject(error) : resolve()));
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
const writeFrame = async (type, payload = new Uint8Array()) => {
|
|
94
|
+
if (payload.byteLength > MAX_FRAME_BYTES && type !== FRAME_BODY) {
|
|
95
|
+
throw new Error("Registry helper frame exceeds its protocol limit");
|
|
96
|
+
}
|
|
97
|
+
if (type === FRAME_BODY && payload.byteLength > MAX_FRAME_BYTES) {
|
|
98
|
+
for (let offset = 0; offset < payload.byteLength; offset += MAX_FRAME_BYTES) {
|
|
99
|
+
await writeFrame(type, payload.subarray(offset, Math.min(payload.byteLength, offset + MAX_FRAME_BYTES)));
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const header = Buffer.allocUnsafe(5);
|
|
104
|
+
header[0] = type;
|
|
105
|
+
header.writeUInt32BE(payload.byteLength, 1);
|
|
106
|
+
await writeBuffer(header);
|
|
107
|
+
if (payload.byteLength > 0)
|
|
108
|
+
await writeBuffer(payload);
|
|
109
|
+
};
|
|
110
|
+
const errorPayload = (error) => {
|
|
111
|
+
const candidate = error;
|
|
112
|
+
return Buffer.from(JSON.stringify({
|
|
113
|
+
name: typeof candidate?.name === "string" ? candidate.name.slice(0, 128) : "Error",
|
|
114
|
+
message: typeof candidate?.message === "string" ? candidate.message.slice(0, 8_192) : String(error).slice(0, 8_192),
|
|
115
|
+
code: typeof candidate?.code === "string" ? candidate.code.slice(0, 128) : undefined,
|
|
116
|
+
}));
|
|
117
|
+
};
|
|
118
|
+
try {
|
|
119
|
+
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
120
|
+
if (!Number.isInteger(nodeMajor) || nodeMajor < 24) {
|
|
121
|
+
throw new Error(`The pinned registry helper requires Node.js >= 24; found ${process.versions.node}`);
|
|
122
|
+
}
|
|
123
|
+
const { addressFamily, bodyPresent, headers, initialBody, iterator, method, pinnedAddress, timeoutMs, url } = await readRequest(isIP);
|
|
124
|
+
const hostname = url.hostname.startsWith("[") && url.hostname.endsWith("]") ? url.hostname.slice(1, -1) : url.hostname;
|
|
125
|
+
const lookup = (_hostname, options, callback) => {
|
|
126
|
+
if (typeof options === "object" && options !== null && "all" in options && options.all === true) {
|
|
127
|
+
callback(null, [{ address: pinnedAddress, family: addressFamily }]);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
callback(null, pinnedAddress, addressFamily);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const requestOptions = {
|
|
134
|
+
protocol: url.protocol,
|
|
135
|
+
hostname,
|
|
136
|
+
port: url.port || undefined,
|
|
137
|
+
path: `${url.pathname}${url.search}`,
|
|
138
|
+
method,
|
|
139
|
+
headers,
|
|
140
|
+
lookup,
|
|
141
|
+
// One helper owns exactly one fresh connection. It never pools a socket
|
|
142
|
+
// selected for an earlier DNS answer.
|
|
143
|
+
agent: false,
|
|
144
|
+
...(url.protocol === "https:" && isIP(hostname) === 0 ? { servername: hostname } : {}),
|
|
145
|
+
};
|
|
146
|
+
let responseStarted = false;
|
|
147
|
+
let requestFinished = false;
|
|
148
|
+
let request;
|
|
149
|
+
const responseDone = new Promise((resolve, reject) => {
|
|
150
|
+
const requestFn = url.protocol === "https:" ? https.request : http.request;
|
|
151
|
+
request = requestFn(requestOptions, async (response) => {
|
|
152
|
+
responseStarted = true;
|
|
153
|
+
try {
|
|
154
|
+
await writeFrame(FRAME_PRELUDE, Buffer.from(JSON.stringify({
|
|
155
|
+
status: response.statusCode ?? 500,
|
|
156
|
+
statusText: response.statusMessage ?? "",
|
|
157
|
+
rawHeaders: response.rawHeaders,
|
|
158
|
+
})));
|
|
159
|
+
for await (const chunk of response) {
|
|
160
|
+
await writeFrame(FRAME_BODY, Buffer.from(chunk));
|
|
161
|
+
}
|
|
162
|
+
await writeFrame(FRAME_END);
|
|
163
|
+
resolve();
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
reject(error);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
const headerTimer = setTimeout(() => {
|
|
170
|
+
request?.destroy(new Error(`Registry request timed out after ${timeoutMs}ms while awaiting response headers`));
|
|
171
|
+
}, timeoutMs);
|
|
172
|
+
request.once("response", () => clearTimeout(headerTimer));
|
|
173
|
+
request.once("error", (error) => {
|
|
174
|
+
clearTimeout(headerTimer);
|
|
175
|
+
if (!responseStarted)
|
|
176
|
+
reject(error);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
const writeRequestChunk = async (chunk) => {
|
|
180
|
+
if (!request || request.destroyed)
|
|
181
|
+
throw new Error("Registry helper request closed while streaming its body");
|
|
182
|
+
const activeRequest = request;
|
|
183
|
+
if (activeRequest.write(chunk))
|
|
184
|
+
return;
|
|
185
|
+
await new Promise((resolve, reject) => {
|
|
186
|
+
const onDrain = () => {
|
|
187
|
+
cleanup();
|
|
188
|
+
resolve();
|
|
189
|
+
};
|
|
190
|
+
const onError = (error) => {
|
|
191
|
+
cleanup();
|
|
192
|
+
reject(error);
|
|
193
|
+
};
|
|
194
|
+
const cleanup = () => {
|
|
195
|
+
activeRequest.off("drain", onDrain);
|
|
196
|
+
activeRequest.off("error", onError);
|
|
197
|
+
};
|
|
198
|
+
activeRequest.once("drain", onDrain);
|
|
199
|
+
activeRequest.once("error", onError);
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
const bodyDone = (async () => {
|
|
203
|
+
if (!request)
|
|
204
|
+
throw new Error("Registry helper request was not initialized");
|
|
205
|
+
if (bodyPresent) {
|
|
206
|
+
if (initialBody.byteLength > 0) {
|
|
207
|
+
await writeRequestChunk(initialBody);
|
|
208
|
+
}
|
|
209
|
+
while (true) {
|
|
210
|
+
const next = await iterator.next();
|
|
211
|
+
if (next.done)
|
|
212
|
+
break;
|
|
213
|
+
await writeRequestChunk(Buffer.from(next.value));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
requestFinished = true;
|
|
217
|
+
request.end();
|
|
218
|
+
})();
|
|
219
|
+
try {
|
|
220
|
+
await Promise.all([responseDone, bodyDone]);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (!requestFinished && !responseStarted)
|
|
224
|
+
request?.destroy(error);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
try {
|
|
230
|
+
await writeFrame(FRAME_ERROR, errorPayload(error));
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// The parent also treats an unframed EOF as a transport failure.
|
|
234
|
+
}
|
|
235
|
+
process.exitCode = 1;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Materialize both self-contained passes into a one-request Node program. */
|
|
239
|
+
export function nodePinnedRequestHelperSource() {
|
|
240
|
+
return (`import * as http from "node:http";\n` +
|
|
241
|
+
`import * as https from "node:https";\n` +
|
|
242
|
+
`import { isIP } from "node:net";\n` +
|
|
243
|
+
`(${nodePinnedRequestHelperMain.toString()})(${readNodePinnedHelperRequest.toString()}, http, https, isIP).catch((error) => {\n` +
|
|
244
|
+
` console.error(error instanceof Error ? error.message : String(error));\n` +
|
|
245
|
+
` process.exitCode = 1;\n` +
|
|
246
|
+
`});\n`);
|
|
247
|
+
}
|