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,717 @@
|
|
|
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 { spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import http from "node:http";
|
|
7
|
+
import https from "node:https";
|
|
8
|
+
import { isIP } from "node:net";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { PassThrough, Readable } from "node:stream";
|
|
12
|
+
import { createBrotliDecompress, createGunzip, createInflate } from "node:zlib";
|
|
13
|
+
import { bareHostname } from "../core/network-policy.js";
|
|
14
|
+
import { nodePinnedRequestHelperSource } from "./pinned-request-helper.js";
|
|
15
|
+
const MAX_HELPER_PRELUDE_BYTES = 64 * 1024;
|
|
16
|
+
const MAX_HELPER_FRAME_BYTES = 1024 * 1024;
|
|
17
|
+
const MAX_HELPER_STDERR_BYTES = 8 * 1024;
|
|
18
|
+
const MAX_REGISTRY_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
|
|
19
|
+
const FRAME_PRELUDE = 0;
|
|
20
|
+
const FRAME_BODY = 1;
|
|
21
|
+
const FRAME_END = 2;
|
|
22
|
+
const FRAME_ERROR = 3;
|
|
23
|
+
export class RegistryPinnedTransportError extends Error {
|
|
24
|
+
code = "REGISTRY_PINNED_TRANSPORT";
|
|
25
|
+
constructor(message, options) {
|
|
26
|
+
super(message, options);
|
|
27
|
+
this.name = "RegistryPinnedTransportError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Connect to `address` while retaining the URL hostname for Host, TLS SNI, and
|
|
32
|
+
* certificate verification. Node uses a request-level pinned lookup. Bun uses
|
|
33
|
+
* the same Node transport in a fresh one-request helper because Bun's TLS
|
|
34
|
+
* client currently drops SNI when the connection is pinned to a numeric IP.
|
|
35
|
+
*/
|
|
36
|
+
export async function requestRegistryAddressPinned(url, address, init, timeoutMs, testOptions) {
|
|
37
|
+
if (isIP(address) === 0)
|
|
38
|
+
throw new Error(`Pinned registry address is not an IP literal: ${address}`);
|
|
39
|
+
const nodeExecutable = process.versions.bun ? resolveNodeExecutable(testOptions?.nodeExecutable) : undefined;
|
|
40
|
+
assertNodeRuntimeVersion();
|
|
41
|
+
const prepared = await prepareRequest(url, init);
|
|
42
|
+
if (process.versions.bun) {
|
|
43
|
+
if (!nodeExecutable)
|
|
44
|
+
throw new RegistryPinnedTransportError("Pinned registry Node helper is unavailable");
|
|
45
|
+
return requestWithNodeHelper(url, address, prepared, init, timeoutMs, nodeExecutable, testOptions);
|
|
46
|
+
}
|
|
47
|
+
return requestWithNodePinnedLookup(url, address, prepared, init, timeoutMs, testOptions);
|
|
48
|
+
}
|
|
49
|
+
/** Fail before DNS or socket activity when this runtime cannot provide the pinned transport. */
|
|
50
|
+
export function assertRegistryPinnedTransportAvailable() {
|
|
51
|
+
if (process.versions.bun) {
|
|
52
|
+
resolveNodeExecutable(undefined);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
assertNodeRuntimeVersion();
|
|
56
|
+
}
|
|
57
|
+
async function requestWithNodePinnedLookup(url, address, prepared, init, timeoutMs, testOptions) {
|
|
58
|
+
const hostname = bareHostname(url.hostname);
|
|
59
|
+
const requestOptions = {
|
|
60
|
+
protocol: url.protocol,
|
|
61
|
+
hostname,
|
|
62
|
+
port: url.port || undefined,
|
|
63
|
+
path: `${url.pathname}${url.search}`,
|
|
64
|
+
method: prepared.method,
|
|
65
|
+
headers: requestHeaders(prepared.headers, url),
|
|
66
|
+
lookup: pinnedLookup(address),
|
|
67
|
+
// Never reuse a connection selected for an earlier DNS answer.
|
|
68
|
+
agent: false,
|
|
69
|
+
signal: init?.signal ?? undefined,
|
|
70
|
+
};
|
|
71
|
+
if (url.protocol === "https:" && isIP(hostname) === 0) {
|
|
72
|
+
requestOptions.servername = hostname;
|
|
73
|
+
if (testOptions?.ca)
|
|
74
|
+
requestOptions.ca = testOptions.ca;
|
|
75
|
+
}
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
let settled = false;
|
|
78
|
+
let headerTimer;
|
|
79
|
+
const requestFn = url.protocol === "https:" ? https.request : http.request;
|
|
80
|
+
const request = requestFn(requestOptions, (incoming) => {
|
|
81
|
+
if (settled) {
|
|
82
|
+
incoming.destroy();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
settled = true;
|
|
86
|
+
if (headerTimer)
|
|
87
|
+
clearTimeout(headerTimer);
|
|
88
|
+
resolve(responseFromIncoming(incoming, prepared.method, init?.signal));
|
|
89
|
+
});
|
|
90
|
+
headerTimer = setTimeout(() => {
|
|
91
|
+
if (settled)
|
|
92
|
+
return;
|
|
93
|
+
settled = true;
|
|
94
|
+
const error = new Error(`Registry request timed out after ${timeoutMs}ms while awaiting response headers`);
|
|
95
|
+
reject(error);
|
|
96
|
+
request.destroy(error);
|
|
97
|
+
}, timeoutMs);
|
|
98
|
+
request.once("error", (error) => {
|
|
99
|
+
if (headerTimer)
|
|
100
|
+
clearTimeout(headerTimer);
|
|
101
|
+
if (settled)
|
|
102
|
+
return;
|
|
103
|
+
settled = true;
|
|
104
|
+
reject(error);
|
|
105
|
+
});
|
|
106
|
+
writeRequestBody(request, prepared.body).catch((error) => request.destroy(error));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async function requestWithNodeHelper(url, address, prepared, init, timeoutMs, executable, testOptions) {
|
|
110
|
+
const artifacts = createHelperArtifacts(testOptions?.ca);
|
|
111
|
+
const env = helperEnvironment(artifacts.caPath);
|
|
112
|
+
const args = [artifacts.helperPath];
|
|
113
|
+
let child;
|
|
114
|
+
try {
|
|
115
|
+
child = spawn(executable, args, {
|
|
116
|
+
env,
|
|
117
|
+
shell: false,
|
|
118
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
119
|
+
windowsHide: true,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
artifacts.cleanup();
|
|
124
|
+
throw new RegistryPinnedTransportError("Unable to start the pinned registry Node helper", { cause: error });
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
testOptions?.onHelperSpawn?.({
|
|
128
|
+
executable,
|
|
129
|
+
args: [...args],
|
|
130
|
+
env: { ...env },
|
|
131
|
+
directory: artifacts.directory,
|
|
132
|
+
pid: child.pid,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
child.kill("SIGKILL");
|
|
137
|
+
artifacts.cleanup();
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
let promiseSettled = false;
|
|
141
|
+
let responseProduced = false;
|
|
142
|
+
let protocolTerminal = false;
|
|
143
|
+
let childKilled = false;
|
|
144
|
+
let consumerCancelled = false;
|
|
145
|
+
const rawBody = new PassThrough();
|
|
146
|
+
const stderr = collectLimitedStderr(child.stderr).catch(() => "");
|
|
147
|
+
const exited = new Promise((resolve, reject) => {
|
|
148
|
+
child.once("error", reject);
|
|
149
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
150
|
+
});
|
|
151
|
+
void exited.then(artifacts.cleanup, artifacts.cleanup);
|
|
152
|
+
const killChild = () => {
|
|
153
|
+
if (childKilled || child.exitCode !== null || child.signalCode !== null)
|
|
154
|
+
return;
|
|
155
|
+
childKilled = true;
|
|
156
|
+
child.kill("SIGKILL");
|
|
157
|
+
};
|
|
158
|
+
const abortReason = () => {
|
|
159
|
+
const reason = init?.signal?.reason;
|
|
160
|
+
return reason instanceof Error ? reason : new Error("Registry request aborted");
|
|
161
|
+
};
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
const rejectBeforeResponse = (error) => {
|
|
164
|
+
if (responseProduced) {
|
|
165
|
+
if (!rawBody.destroyed)
|
|
166
|
+
rawBody.destroy(asTransportError(error));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (promiseSettled)
|
|
170
|
+
return;
|
|
171
|
+
promiseSettled = true;
|
|
172
|
+
reject(asTransportError(error));
|
|
173
|
+
};
|
|
174
|
+
const onAbort = () => {
|
|
175
|
+
const error = abortReason();
|
|
176
|
+
killChild();
|
|
177
|
+
rejectBeforeResponse(error);
|
|
178
|
+
};
|
|
179
|
+
if (init?.signal?.aborted) {
|
|
180
|
+
onAbort();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
init?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
184
|
+
const headerTimer = setTimeout(() => {
|
|
185
|
+
const error = new RegistryPinnedTransportError(`Registry request timed out after ${timeoutMs}ms while awaiting response headers`);
|
|
186
|
+
killChild();
|
|
187
|
+
rejectBeforeResponse(error);
|
|
188
|
+
}, timeoutMs);
|
|
189
|
+
void writeHelperRequest(child, url, address, prepared, timeoutMs).catch((error) => {
|
|
190
|
+
if (protocolTerminal || promiseSettled)
|
|
191
|
+
return;
|
|
192
|
+
killChild();
|
|
193
|
+
rejectBeforeResponse(error);
|
|
194
|
+
});
|
|
195
|
+
void pumpHelperFrames(child.stdout, rawBody, exited, {
|
|
196
|
+
onPrelude: (prelude) => {
|
|
197
|
+
if (responseProduced)
|
|
198
|
+
throw new RegistryPinnedTransportError("Registry helper sent more than one prelude");
|
|
199
|
+
if (promiseSettled)
|
|
200
|
+
throw new RegistryPinnedTransportError("Registry helper responded after the request ended");
|
|
201
|
+
responseProduced = true;
|
|
202
|
+
promiseSettled = true;
|
|
203
|
+
clearTimeout(headerTimer);
|
|
204
|
+
const response = responseFromRaw(rawBody, prelude, prepared.method, init?.signal, () => {
|
|
205
|
+
consumerCancelled = true;
|
|
206
|
+
if (!protocolTerminal)
|
|
207
|
+
killChild();
|
|
208
|
+
});
|
|
209
|
+
resolve(response);
|
|
210
|
+
},
|
|
211
|
+
onTerminal: () => {
|
|
212
|
+
protocolTerminal = true;
|
|
213
|
+
},
|
|
214
|
+
})
|
|
215
|
+
.catch(async (error) => {
|
|
216
|
+
clearTimeout(headerTimer);
|
|
217
|
+
killChild();
|
|
218
|
+
if (consumerCancelled || init?.signal?.aborted) {
|
|
219
|
+
if (!promiseSettled)
|
|
220
|
+
rejectBeforeResponse(abortReason());
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Always drain but never surface helper stderr: a failed executable
|
|
224
|
+
// could echo the private stdin request, including authorization.
|
|
225
|
+
await stderr;
|
|
226
|
+
rejectBeforeResponse(new RegistryPinnedTransportError(asErrorMessage(error), { cause: error }));
|
|
227
|
+
})
|
|
228
|
+
.finally(() => {
|
|
229
|
+
clearTimeout(headerTimer);
|
|
230
|
+
init?.signal?.removeEventListener("abort", onAbort);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function resolveNodeExecutable(override) {
|
|
235
|
+
if (override === null) {
|
|
236
|
+
throw new RegistryPinnedTransportError("Pinned registry networking under Bun requires Node.js >= 24 on PATH; no Node executable is available");
|
|
237
|
+
}
|
|
238
|
+
const candidates = override === undefined
|
|
239
|
+
? (process.env.PATH ?? "")
|
|
240
|
+
.split(path.delimiter)
|
|
241
|
+
.filter(Boolean)
|
|
242
|
+
.map((directory) => path.join(directory, process.platform === "win32" ? "node.exe" : "node"))
|
|
243
|
+
: [override];
|
|
244
|
+
for (const candidate of candidates) {
|
|
245
|
+
if (!path.isAbsolute(candidate))
|
|
246
|
+
continue;
|
|
247
|
+
try {
|
|
248
|
+
const resolved = fs.realpathSync(candidate);
|
|
249
|
+
if (!path.isAbsolute(resolved) || !fs.statSync(resolved).isFile())
|
|
250
|
+
continue;
|
|
251
|
+
fs.accessSync(resolved, fs.constants.X_OK);
|
|
252
|
+
if (override === undefined && !isSupportedNodeExecutable(resolved))
|
|
253
|
+
continue;
|
|
254
|
+
return resolved;
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Try the next PATH entry. The helper itself enforces Node >= 24.
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
throw new RegistryPinnedTransportError("Pinned registry networking under Bun requires Node.js >= 24 on PATH. Install Node.js or run akm with Node.");
|
|
261
|
+
}
|
|
262
|
+
function isSupportedNodeExecutable(executable) {
|
|
263
|
+
const result = spawnSync(executable, ["--version"], {
|
|
264
|
+
encoding: "utf8",
|
|
265
|
+
env: helperEnvironment(undefined),
|
|
266
|
+
maxBuffer: 1_024,
|
|
267
|
+
shell: false,
|
|
268
|
+
timeout: 3_000,
|
|
269
|
+
windowsHide: true,
|
|
270
|
+
});
|
|
271
|
+
if (result.error || result.status !== 0)
|
|
272
|
+
return false;
|
|
273
|
+
const match = /^v(\d+)\./.exec(result.stdout.trim());
|
|
274
|
+
const major = match?.[1];
|
|
275
|
+
return major !== undefined && Number.parseInt(major, 10) >= 24;
|
|
276
|
+
}
|
|
277
|
+
function assertNodeRuntimeVersion() {
|
|
278
|
+
if (process.versions.bun)
|
|
279
|
+
return;
|
|
280
|
+
const major = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
281
|
+
if (!Number.isInteger(major) || major < 24) {
|
|
282
|
+
throw new RegistryPinnedTransportError(`Pinned registry networking requires Node.js >= 24; found ${process.versions.node}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function createHelperArtifacts(ca) {
|
|
286
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "akm-registry-helper-"));
|
|
287
|
+
fs.chmodSync(directory, 0o700);
|
|
288
|
+
const helperPath = path.join(directory, "request.mjs");
|
|
289
|
+
const caPath = ca === undefined ? undefined : path.join(directory, "ca.pem");
|
|
290
|
+
const cleanup = () => {
|
|
291
|
+
for (const file of [helperPath, caPath]) {
|
|
292
|
+
if (!file)
|
|
293
|
+
continue;
|
|
294
|
+
try {
|
|
295
|
+
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
296
|
+
if (stat?.isFile())
|
|
297
|
+
fs.unlinkSync(file);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// A private one-request directory is best-effort cleanup after exit.
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
fs.rmdirSync(directory);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// Preserve unexpected contents for diagnosis; never recurse here.
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
try {
|
|
311
|
+
const source = nodePinnedRequestHelperSource();
|
|
312
|
+
fs.writeFileSync(helperPath, source, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
313
|
+
if (caPath && ca !== undefined)
|
|
314
|
+
fs.writeFileSync(caPath, ca, { flag: "wx", mode: 0o600 });
|
|
315
|
+
for (const file of [helperPath, caPath]) {
|
|
316
|
+
if (!file)
|
|
317
|
+
continue;
|
|
318
|
+
const stat = fs.lstatSync(file);
|
|
319
|
+
if (!stat.isFile())
|
|
320
|
+
throw new RegistryPinnedTransportError("Registry helper artifact is not a regular file");
|
|
321
|
+
}
|
|
322
|
+
return { directory, helperPath, caPath, cleanup };
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
cleanup();
|
|
326
|
+
if (error instanceof RegistryPinnedTransportError)
|
|
327
|
+
throw error;
|
|
328
|
+
throw new RegistryPinnedTransportError("Unable to prepare the pinned registry Node helper", { cause: error });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function helperEnvironment(caPath) {
|
|
332
|
+
const env = {};
|
|
333
|
+
for (const name of [
|
|
334
|
+
"SystemRoot",
|
|
335
|
+
"WINDIR",
|
|
336
|
+
"TMPDIR",
|
|
337
|
+
"TEMP",
|
|
338
|
+
"TMP",
|
|
339
|
+
"LANG",
|
|
340
|
+
"LC_ALL",
|
|
341
|
+
"TZ",
|
|
342
|
+
"SSL_CERT_FILE",
|
|
343
|
+
"SSL_CERT_DIR",
|
|
344
|
+
"NODE_EXTRA_CA_CERTS",
|
|
345
|
+
]) {
|
|
346
|
+
const value = process.env[name];
|
|
347
|
+
if (value !== undefined)
|
|
348
|
+
env[name] = value;
|
|
349
|
+
}
|
|
350
|
+
if (caPath)
|
|
351
|
+
env.NODE_EXTRA_CA_CERTS = caPath;
|
|
352
|
+
return env;
|
|
353
|
+
}
|
|
354
|
+
async function writeHelperRequest(child, url, address, prepared, timeoutMs) {
|
|
355
|
+
const prelude = Buffer.from(JSON.stringify({
|
|
356
|
+
url: url.toString(),
|
|
357
|
+
address,
|
|
358
|
+
method: prepared.method,
|
|
359
|
+
headers: [...prepared.headers.entries()],
|
|
360
|
+
timeoutMs,
|
|
361
|
+
bodyPresent: prepared.body !== undefined,
|
|
362
|
+
}));
|
|
363
|
+
if (prelude.byteLength === 0 || prelude.byteLength > MAX_HELPER_PRELUDE_BYTES) {
|
|
364
|
+
throw new RegistryPinnedTransportError("Registry helper request prelude exceeds its protocol limit");
|
|
365
|
+
}
|
|
366
|
+
const length = Buffer.allocUnsafe(4);
|
|
367
|
+
length.writeUInt32BE(prelude.byteLength, 0);
|
|
368
|
+
await writeNodeChunk(child.stdin, length);
|
|
369
|
+
await writeNodeChunk(child.stdin, prelude);
|
|
370
|
+
if (prepared.body instanceof Uint8Array) {
|
|
371
|
+
await writeNodeChunk(child.stdin, prepared.body);
|
|
372
|
+
}
|
|
373
|
+
else if (prepared.body) {
|
|
374
|
+
for await (const chunk of Readable.fromWeb(prepared.body)) {
|
|
375
|
+
await writeNodeChunk(child.stdin, Buffer.from(chunk));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
await endNodeWritable(child.stdin);
|
|
379
|
+
}
|
|
380
|
+
async function writeNodeChunk(stream, chunk) {
|
|
381
|
+
await new Promise((resolve, reject) => {
|
|
382
|
+
stream.write(chunk, (error) => (error ? reject(error) : resolve()));
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
async function endNodeWritable(stream) {
|
|
386
|
+
await new Promise((resolve, reject) => {
|
|
387
|
+
stream.once("error", reject);
|
|
388
|
+
stream.end(() => resolve());
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
async function pumpHelperFrames(stdout, rawBody, exited, callbacks) {
|
|
392
|
+
const iterator = stdout[Symbol.asyncIterator]();
|
|
393
|
+
let buffered = Buffer.alloc(0);
|
|
394
|
+
const readExact = async (length) => {
|
|
395
|
+
while (buffered.byteLength < length) {
|
|
396
|
+
const next = await iterator.next();
|
|
397
|
+
if (next.done)
|
|
398
|
+
throw new RegistryPinnedTransportError("Registry helper output ended before a terminal frame");
|
|
399
|
+
buffered = Buffer.concat([buffered, Buffer.from(next.value)]);
|
|
400
|
+
}
|
|
401
|
+
const result = buffered.subarray(0, length);
|
|
402
|
+
buffered = buffered.subarray(length);
|
|
403
|
+
return result;
|
|
404
|
+
};
|
|
405
|
+
let sawPrelude = false;
|
|
406
|
+
while (true) {
|
|
407
|
+
const frameHeader = await readExact(5);
|
|
408
|
+
const type = frameHeader[0];
|
|
409
|
+
const length = frameHeader.readUInt32BE(1);
|
|
410
|
+
const limit = type === FRAME_BODY ? MAX_HELPER_FRAME_BYTES : MAX_HELPER_PRELUDE_BYTES;
|
|
411
|
+
if (length > limit)
|
|
412
|
+
throw new RegistryPinnedTransportError("Registry helper frame exceeds its protocol limit");
|
|
413
|
+
const payload = await readExact(length);
|
|
414
|
+
if (type === FRAME_PRELUDE) {
|
|
415
|
+
if (sawPrelude)
|
|
416
|
+
throw new RegistryPinnedTransportError("Registry helper sent duplicate response metadata");
|
|
417
|
+
const prelude = parseHelperPrelude(payload);
|
|
418
|
+
sawPrelude = true;
|
|
419
|
+
callbacks.onPrelude(prelude);
|
|
420
|
+
// A HEAD or null-body status cancels the transport synchronously from
|
|
421
|
+
// the prelude callback. Do not consume any body frames already buffered
|
|
422
|
+
// behind that prelude or wait for a helper that has been terminated.
|
|
423
|
+
if (rawBody.destroyed)
|
|
424
|
+
return;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (type === FRAME_BODY) {
|
|
428
|
+
if (!sawPrelude)
|
|
429
|
+
throw new RegistryPinnedTransportError("Registry helper sent body data before response metadata");
|
|
430
|
+
if (payload.byteLength > 0 && !rawBody.write(payload)) {
|
|
431
|
+
await new Promise((resolve, reject) => {
|
|
432
|
+
const onDrain = () => {
|
|
433
|
+
cleanup();
|
|
434
|
+
resolve();
|
|
435
|
+
};
|
|
436
|
+
const onError = (error) => {
|
|
437
|
+
cleanup();
|
|
438
|
+
reject(error);
|
|
439
|
+
};
|
|
440
|
+
const cleanup = () => {
|
|
441
|
+
rawBody.off("drain", onDrain);
|
|
442
|
+
rawBody.off("error", onError);
|
|
443
|
+
};
|
|
444
|
+
rawBody.once("drain", onDrain);
|
|
445
|
+
rawBody.once("error", onError);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (type === FRAME_END) {
|
|
451
|
+
if (!sawPrelude || length !== 0)
|
|
452
|
+
throw new RegistryPinnedTransportError("Registry helper sent an invalid end frame");
|
|
453
|
+
const result = await exited;
|
|
454
|
+
if (result.code !== 0 || result.signal) {
|
|
455
|
+
throw new RegistryPinnedTransportError(`Registry helper exited unexpectedly (${result.signal ?? `code ${result.code ?? "unknown"}`})`);
|
|
456
|
+
}
|
|
457
|
+
callbacks.onTerminal();
|
|
458
|
+
rawBody.end();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (type === FRAME_ERROR) {
|
|
462
|
+
const detail = parseHelperError(payload);
|
|
463
|
+
throw new RegistryPinnedTransportError(detail);
|
|
464
|
+
}
|
|
465
|
+
throw new RegistryPinnedTransportError(`Registry helper sent unknown frame type ${String(type)}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function parseHelperPrelude(payload) {
|
|
469
|
+
let candidate;
|
|
470
|
+
try {
|
|
471
|
+
candidate = JSON.parse(payload.toString("utf8"));
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
throw new RegistryPinnedTransportError("Registry helper sent invalid response metadata");
|
|
475
|
+
}
|
|
476
|
+
if (typeof candidate !== "object" || candidate === null) {
|
|
477
|
+
throw new RegistryPinnedTransportError("Registry helper sent invalid response metadata");
|
|
478
|
+
}
|
|
479
|
+
const value = candidate;
|
|
480
|
+
if (typeof value.status !== "number" ||
|
|
481
|
+
!Number.isInteger(value.status) ||
|
|
482
|
+
value.status < 200 ||
|
|
483
|
+
value.status > 599 ||
|
|
484
|
+
typeof value.statusText !== "string" ||
|
|
485
|
+
!Array.isArray(value.rawHeaders) ||
|
|
486
|
+
value.rawHeaders.length > 512 ||
|
|
487
|
+
value.rawHeaders.length % 2 !== 0 ||
|
|
488
|
+
value.rawHeaders.some((entry) => typeof entry !== "string" || entry.length > 16_384)) {
|
|
489
|
+
throw new RegistryPinnedTransportError("Registry helper sent invalid response metadata");
|
|
490
|
+
}
|
|
491
|
+
return { status: value.status, statusText: value.statusText, rawHeaders: value.rawHeaders };
|
|
492
|
+
}
|
|
493
|
+
function parseHelperError(payload) {
|
|
494
|
+
try {
|
|
495
|
+
const candidate = JSON.parse(payload.toString("utf8"));
|
|
496
|
+
const message = typeof candidate.message === "string" ? candidate.message : "Pinned registry helper failed";
|
|
497
|
+
const code = typeof candidate.code === "string" ? ` [${candidate.code}]` : "";
|
|
498
|
+
return `${message}${code}`;
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return "Pinned registry helper failed with an invalid error frame";
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function collectLimitedStderr(stderr) {
|
|
505
|
+
return (async () => {
|
|
506
|
+
const chunks = [];
|
|
507
|
+
let total = 0;
|
|
508
|
+
for await (const chunk of stderr) {
|
|
509
|
+
if (total >= MAX_HELPER_STDERR_BYTES)
|
|
510
|
+
continue;
|
|
511
|
+
const buffer = Buffer.from(chunk);
|
|
512
|
+
const selected = buffer.subarray(0, MAX_HELPER_STDERR_BYTES - total);
|
|
513
|
+
chunks.push(selected);
|
|
514
|
+
total += selected.byteLength;
|
|
515
|
+
}
|
|
516
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
517
|
+
})();
|
|
518
|
+
}
|
|
519
|
+
function pinnedLookup(address) {
|
|
520
|
+
const family = isIP(address);
|
|
521
|
+
if (family === 0)
|
|
522
|
+
throw new Error(`Pinned registry address is not an IP literal: ${address}`);
|
|
523
|
+
return ((_hostname, options, callback) => {
|
|
524
|
+
if (typeof options === "object" && options.all) {
|
|
525
|
+
callback(null, [{ address, family }]);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
callback(null, address, family);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
async function prepareRequest(url, init) {
|
|
532
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
533
|
+
const headers = new Headers(init?.headers);
|
|
534
|
+
const body = init?.body;
|
|
535
|
+
if (body === undefined || body === null)
|
|
536
|
+
return { method, headers };
|
|
537
|
+
if (method === "GET" || method === "HEAD") {
|
|
538
|
+
throw new TypeError(`Request with ${method} method cannot have a body`);
|
|
539
|
+
}
|
|
540
|
+
if (typeof body === "string") {
|
|
541
|
+
if (!headers.has("content-type"))
|
|
542
|
+
headers.set("content-type", "text/plain;charset=UTF-8");
|
|
543
|
+
return { method, headers, body: boundedRequestBytes(new TextEncoder().encode(body)) };
|
|
544
|
+
}
|
|
545
|
+
if (body instanceof URLSearchParams) {
|
|
546
|
+
if (!headers.has("content-type")) {
|
|
547
|
+
headers.set("content-type", "application/x-www-form-urlencoded;charset=UTF-8");
|
|
548
|
+
}
|
|
549
|
+
return { method, headers, body: boundedRequestBytes(new TextEncoder().encode(body.toString())) };
|
|
550
|
+
}
|
|
551
|
+
if (body instanceof Blob) {
|
|
552
|
+
if (body.type && !headers.has("content-type"))
|
|
553
|
+
headers.set("content-type", body.type);
|
|
554
|
+
if (body.size > MAX_REGISTRY_REQUEST_BODY_BYTES)
|
|
555
|
+
throw requestBodyTooLarge(body.size);
|
|
556
|
+
return { method, headers, body: boundedRequestBytes(new Uint8Array(await body.arrayBuffer())) };
|
|
557
|
+
}
|
|
558
|
+
if (body instanceof FormData) {
|
|
559
|
+
const encoded = new Request(url, { method, body });
|
|
560
|
+
for (const [name, value] of encoded.headers) {
|
|
561
|
+
if (!headers.has(name))
|
|
562
|
+
headers.set(name, value);
|
|
563
|
+
}
|
|
564
|
+
return { method, headers, body: boundedRequestBytes(new Uint8Array(await encoded.arrayBuffer())) };
|
|
565
|
+
}
|
|
566
|
+
if (body instanceof ArrayBuffer) {
|
|
567
|
+
return { method, headers, body: boundedRequestBytes(new Uint8Array(body)) };
|
|
568
|
+
}
|
|
569
|
+
if (ArrayBuffer.isView(body)) {
|
|
570
|
+
return {
|
|
571
|
+
method,
|
|
572
|
+
headers,
|
|
573
|
+
body: boundedRequestBytes(new Uint8Array(body.buffer, body.byteOffset, body.byteLength)),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
if (body instanceof ReadableStream) {
|
|
577
|
+
return { method, headers, body: boundedRequestStream(body) };
|
|
578
|
+
}
|
|
579
|
+
throw new TypeError("Unsupported registry request body type");
|
|
580
|
+
}
|
|
581
|
+
function boundedRequestBytes(body) {
|
|
582
|
+
if (body.byteLength > MAX_REGISTRY_REQUEST_BODY_BYTES)
|
|
583
|
+
throw requestBodyTooLarge(body.byteLength);
|
|
584
|
+
return body;
|
|
585
|
+
}
|
|
586
|
+
function boundedRequestStream(body) {
|
|
587
|
+
let total = 0;
|
|
588
|
+
return body.pipeThrough(new TransformStream({
|
|
589
|
+
transform(chunk, controller) {
|
|
590
|
+
if (!(chunk instanceof Uint8Array)) {
|
|
591
|
+
controller.error(new RegistryPinnedTransportError("Registry request body stream must contain byte chunks"));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
total += chunk.byteLength;
|
|
595
|
+
if (total > MAX_REGISTRY_REQUEST_BODY_BYTES) {
|
|
596
|
+
controller.error(requestBodyTooLarge(total));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
controller.enqueue(chunk);
|
|
600
|
+
},
|
|
601
|
+
}));
|
|
602
|
+
}
|
|
603
|
+
function requestBodyTooLarge(observedBytes) {
|
|
604
|
+
return new RegistryPinnedTransportError(`Registry request body exceeds ${MAX_REGISTRY_REQUEST_BODY_BYTES} bytes (observed: ${observedBytes})`);
|
|
605
|
+
}
|
|
606
|
+
function requestHeaders(headers, url) {
|
|
607
|
+
const result = {};
|
|
608
|
+
for (const [name, value] of headers)
|
|
609
|
+
result[name] = value;
|
|
610
|
+
for (const name of [
|
|
611
|
+
"connection",
|
|
612
|
+
"keep-alive",
|
|
613
|
+
"proxy-authorization",
|
|
614
|
+
"proxy-connection",
|
|
615
|
+
"te",
|
|
616
|
+
"trailer",
|
|
617
|
+
"transfer-encoding",
|
|
618
|
+
"upgrade",
|
|
619
|
+
]) {
|
|
620
|
+
delete result[name];
|
|
621
|
+
}
|
|
622
|
+
// Never let a caller pair the validated socket/TLS identity with an
|
|
623
|
+
// unrelated HTTP virtual host.
|
|
624
|
+
result.host = url.host;
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
async function writeRequestBody(request, body) {
|
|
628
|
+
if (!body) {
|
|
629
|
+
request.end();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (body instanceof Uint8Array) {
|
|
633
|
+
request.end(body);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const readable = Readable.fromWeb(body);
|
|
637
|
+
readable.once("error", (error) => request.destroy(error));
|
|
638
|
+
readable.pipe(request);
|
|
639
|
+
}
|
|
640
|
+
function responseFromIncoming(incoming, method, signal) {
|
|
641
|
+
return responseFromRaw(incoming, {
|
|
642
|
+
status: incoming.statusCode ?? 500,
|
|
643
|
+
statusText: incoming.statusMessage ?? "",
|
|
644
|
+
rawHeaders: incoming.rawHeaders,
|
|
645
|
+
}, method, signal, () => incoming.destroy());
|
|
646
|
+
}
|
|
647
|
+
function responseFromRaw(raw, metadata, method, signal, onCancel) {
|
|
648
|
+
const headers = responseHeaders(metadata.rawHeaders);
|
|
649
|
+
const { status, statusText } = metadata;
|
|
650
|
+
if (responseMustNotHaveBody(method, status)) {
|
|
651
|
+
// Draining is unsafe here: a hostile peer can attach an endless chunked
|
|
652
|
+
// body to a null-body status and retain the pinned socket/helper forever.
|
|
653
|
+
// Terminate the underlying transport before exposing the bodyless response.
|
|
654
|
+
onCancel();
|
|
655
|
+
if (!raw.destroyed)
|
|
656
|
+
raw.destroy();
|
|
657
|
+
return new Response(null, { status, statusText, headers });
|
|
658
|
+
}
|
|
659
|
+
const decoded = decodeResponseStream(raw, headers.get("content-encoding") ?? "");
|
|
660
|
+
const onAbort = () => {
|
|
661
|
+
const reason = signal?.reason;
|
|
662
|
+
decoded.destroy(reason instanceof Error ? reason : new Error("Registry response body aborted"));
|
|
663
|
+
onCancel();
|
|
664
|
+
};
|
|
665
|
+
if (signal) {
|
|
666
|
+
if (signal.aborted)
|
|
667
|
+
onAbort();
|
|
668
|
+
else
|
|
669
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
670
|
+
decoded.once("close", () => signal.removeEventListener("abort", onAbort));
|
|
671
|
+
}
|
|
672
|
+
decoded.once("error", onCancel);
|
|
673
|
+
decoded.once("close", onCancel);
|
|
674
|
+
const body = Readable.toWeb(decoded);
|
|
675
|
+
return new Response(body, { status, statusText, headers });
|
|
676
|
+
}
|
|
677
|
+
/** Fetch null-body cases accepted by the helper's final-response protocol. */
|
|
678
|
+
function responseMustNotHaveBody(method, status) {
|
|
679
|
+
// Interim 1xx responses never enter this function: node:http reports them
|
|
680
|
+
// separately and the helper protocol accepts final statuses from 200 onward.
|
|
681
|
+
return method === "HEAD" || status === 204 || status === 205 || status === 304;
|
|
682
|
+
}
|
|
683
|
+
function responseHeaders(rawHeaders) {
|
|
684
|
+
const headers = new Headers();
|
|
685
|
+
for (let index = 0; index < rawHeaders.length; index += 2) {
|
|
686
|
+
const name = rawHeaders[index];
|
|
687
|
+
const value = rawHeaders[index + 1];
|
|
688
|
+
if (name !== undefined && value !== undefined)
|
|
689
|
+
headers.append(name, value);
|
|
690
|
+
}
|
|
691
|
+
return headers;
|
|
692
|
+
}
|
|
693
|
+
function decodeResponseStream(raw, contentEncoding) {
|
|
694
|
+
const encodings = contentEncoding
|
|
695
|
+
.split(",")
|
|
696
|
+
.map((encoding) => encoding.trim().toLowerCase())
|
|
697
|
+
.filter(Boolean)
|
|
698
|
+
.reverse();
|
|
699
|
+
let stream = raw;
|
|
700
|
+
for (const encoding of encodings) {
|
|
701
|
+
if (encoding === "gzip" || encoding === "x-gzip")
|
|
702
|
+
stream = stream.pipe(createGunzip());
|
|
703
|
+
else if (encoding === "deflate")
|
|
704
|
+
stream = stream.pipe(createInflate());
|
|
705
|
+
else if (encoding === "br")
|
|
706
|
+
stream = stream.pipe(createBrotliDecompress());
|
|
707
|
+
}
|
|
708
|
+
return stream;
|
|
709
|
+
}
|
|
710
|
+
function asTransportError(error) {
|
|
711
|
+
if (error instanceof Error)
|
|
712
|
+
return error;
|
|
713
|
+
return new RegistryPinnedTransportError(String(error));
|
|
714
|
+
}
|
|
715
|
+
function asErrorMessage(error) {
|
|
716
|
+
return error instanceof Error ? error.message : String(error);
|
|
717
|
+
}
|