zyrk 0.3.7 → 0.3.9
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/VERSION +1 -1
- package/bin/zyrk.js +1 -1
- package/dist/cli/main.d.ts +54 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +473 -0
- package/dist/cli/npm-delegate.d.ts +38 -0
- package/dist/cli/npm-delegate.d.ts.map +1 -0
- package/{src/cli/npm-delegate.ts → dist/cli/npm-delegate.js} +26 -55
- package/dist/cli/parse.d.ts +40 -0
- package/dist/cli/parse.d.ts.map +1 -0
- package/dist/cli/parse.js +78 -0
- package/dist/commands/dependencies.d.ts +12 -0
- package/dist/commands/dependencies.d.ts.map +1 -0
- package/dist/commands/dependencies.js +118 -0
- package/dist/commands/deploy.d.ts +15 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +506 -0
- package/dist/commands/maintenance.d.ts +27 -0
- package/dist/commands/maintenance.d.ts.map +1 -0
- package/dist/commands/maintenance.js +198 -0
- package/dist/commands/patch.d.ts +38 -0
- package/dist/commands/patch.d.ts.map +1 -0
- package/dist/commands/patch.js +336 -0
- package/dist/commands/project.d.ts +15 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +396 -0
- package/dist/commands/store.d.ts +10 -0
- package/dist/commands/store.d.ts.map +1 -0
- package/dist/commands/store.js +551 -0
- package/dist/config/paths.d.ts +37 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +66 -0
- package/dist/dlx/create.d.ts +23 -0
- package/dist/dlx/create.d.ts.map +1 -0
- package/dist/dlx/create.js +59 -0
- package/dist/dlx/run.d.ts +54 -0
- package/dist/dlx/run.d.ts.map +1 -0
- package/dist/dlx/run.js +274 -0
- package/dist/errors.d.ts +68 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +111 -0
- package/dist/extract/tarball.d.ts +82 -0
- package/dist/extract/tarball.d.ts.map +1 -0
- package/dist/extract/tarball.js +645 -0
- package/dist/fetch/all.d.ts +48 -0
- package/dist/fetch/all.d.ts.map +1 -0
- package/dist/fetch/all.js +127 -0
- package/dist/fetch/package.d.ts +45 -0
- package/dist/fetch/package.d.ts.map +1 -0
- package/dist/fetch/package.js +124 -0
- package/dist/fetch/registry.d.ts +109 -0
- package/dist/fetch/registry.d.ts.map +1 -0
- package/dist/fetch/registry.js +418 -0
- package/dist/install/import-npm.d.ts +4 -0
- package/dist/install/import-npm.d.ts.map +1 -0
- package/dist/install/import-npm.js +147 -0
- package/dist/install/lock.d.ts +8 -0
- package/dist/install/lock.d.ts.map +1 -0
- package/dist/install/lock.js +146 -0
- package/dist/install/metadata.d.ts +19 -0
- package/dist/install/metadata.d.ts.map +1 -0
- package/dist/install/metadata.js +79 -0
- package/dist/install/registry.d.ts +8 -0
- package/dist/install/registry.d.ts.map +1 -0
- package/dist/install/registry.js +59 -0
- package/dist/install/run.d.ts +32 -0
- package/dist/install/run.d.ts.map +1 -0
- package/dist/install/run.js +545 -0
- package/dist/lockfile/fingerprints.d.ts +35 -0
- package/dist/lockfile/fingerprints.d.ts.map +1 -0
- package/dist/lockfile/fingerprints.js +56 -0
- package/dist/lockfile/read.d.ts +27 -0
- package/dist/lockfile/read.d.ts.map +1 -0
- package/dist/lockfile/read.js +126 -0
- package/dist/lockfile/types.d.ts +74 -0
- package/dist/lockfile/types.d.ts.map +1 -0
- package/dist/lockfile/types.js +17 -0
- package/dist/lockfile/write.d.ts +39 -0
- package/dist/lockfile/write.d.ts.map +1 -0
- package/dist/lockfile/write.js +180 -0
- package/dist/log.d.ts +21 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +79 -0
- package/dist/project/projection.d.ts +50 -0
- package/dist/project/projection.d.ts.map +1 -0
- package/dist/project/projection.js +276 -0
- package/dist/resolve/resolver.d.ts +75 -0
- package/dist/resolve/resolver.d.ts.map +1 -0
- package/dist/resolve/resolver.js +533 -0
- package/dist/resolve/spec.d.ts +26 -0
- package/dist/resolve/spec.d.ts.map +1 -0
- package/dist/resolve/spec.js +87 -0
- package/dist/store/db.d.ts +49 -0
- package/dist/store/db.d.ts.map +1 -0
- package/{src/store/db.ts → dist/store/db.js} +106 -206
- package/dist/store/graph.d.ts +60 -0
- package/dist/store/graph.d.ts.map +1 -0
- package/dist/store/graph.js +115 -0
- package/dist/store/layout.d.ts +71 -0
- package/dist/store/layout.d.ts.map +1 -0
- package/dist/store/layout.js +282 -0
- package/dist/store/manifests.d.ts +78 -0
- package/dist/store/manifests.d.ts.map +1 -0
- package/dist/store/manifests.js +238 -0
- package/dist/store/objects.d.ts +56 -0
- package/dist/store/objects.d.ts.map +1 -0
- package/dist/store/objects.js +157 -0
- package/dist/store/realize.d.ts +38 -0
- package/dist/store/realize.d.ts.map +1 -0
- package/dist/store/realize.js +227 -0
- package/dist/store/source.d.ts +42 -0
- package/dist/store/source.d.ts.map +1 -0
- package/dist/store/source.js +169 -0
- package/dist/store/txn.d.ts +37 -0
- package/dist/store/txn.d.ts.map +1 -0
- package/dist/store/txn.js +97 -0
- package/dist/store/verify.d.ts +40 -0
- package/dist/store/verify.d.ts.map +1 -0
- package/dist/store/verify.js +396 -0
- package/dist/util/fsx.d.ts +54 -0
- package/dist/util/fsx.d.ts.map +1 -0
- package/dist/util/fsx.js +259 -0
- package/dist/util/hash.d.ts +48 -0
- package/dist/util/hash.d.ts.map +1 -0
- package/dist/util/hash.js +80 -0
- package/dist/util/ids.d.ts +19 -0
- package/dist/util/ids.d.ts.map +1 -0
- package/dist/util/ids.js +38 -0
- package/dist/util/integrity.d.ts +97 -0
- package/dist/util/integrity.d.ts.map +1 -0
- package/dist/util/integrity.js +192 -0
- package/dist/util/json.d.ts +14 -0
- package/dist/util/json.d.ts.map +1 -0
- package/dist/util/json.js +78 -0
- package/dist/util/locks.d.ts +40 -0
- package/dist/util/locks.d.ts.map +1 -0
- package/dist/util/locks.js +186 -0
- package/dist/util/proc.d.ts +32 -0
- package/dist/util/proc.d.ts.map +1 -0
- package/dist/util/proc.js +84 -0
- package/dist/version.d.ts +27 -0
- package/dist/version.d.ts.map +1 -0
- package/{src/version.ts → dist/version.js} +0 -8
- package/package.json +5 -3
- package/src/cli/main.ts +0 -468
- package/src/cli/parse.ts +0 -106
- package/src/commands/dependencies.ts +0 -154
- package/src/commands/deploy.ts +0 -777
- package/src/commands/maintenance.ts +0 -260
- package/src/commands/patch.ts +0 -408
- package/src/commands/project.ts +0 -598
- package/src/commands/store.ts +0 -689
- package/src/config/paths.ts +0 -119
- package/src/dlx/create.ts +0 -69
- package/src/dlx/run.ts +0 -389
- package/src/errors.ts +0 -142
- package/src/extract/tarball.ts +0 -868
- package/src/fetch/all.ts +0 -206
- package/src/fetch/package.ts +0 -181
- package/src/fetch/registry.ts +0 -615
- package/src/install/import-npm.ts +0 -177
- package/src/install/lock.ts +0 -176
- package/src/install/metadata.ts +0 -122
- package/src/install/registry.ts +0 -88
- package/src/install/run.ts +0 -760
- package/src/lockfile/fingerprints.ts +0 -74
- package/src/lockfile/read.ts +0 -198
- package/src/lockfile/types.ts +0 -75
- package/src/lockfile/write.ts +0 -257
- package/src/log.ts +0 -100
- package/src/project/projection.ts +0 -390
- package/src/resolve/resolver.ts +0 -757
- package/src/resolve/spec.ts +0 -131
- package/src/store/graph.ts +0 -175
- package/src/store/layout.ts +0 -431
- package/src/store/manifests.ts +0 -347
- package/src/store/objects.ts +0 -229
- package/src/store/realize.ts +0 -322
- package/src/store/source.ts +0 -249
- package/src/store/txn.ts +0 -118
- package/src/store/verify.ts +0 -433
- package/src/types/vendor.d.ts +0 -120
- package/src/util/fsx.ts +0 -289
- package/src/util/hash.ts +0 -117
- package/src/util/ids.ts +0 -42
- package/src/util/integrity.ts +0 -269
- package/src/util/json.ts +0 -89
- package/src/util/locks.ts +0 -266
- package/src/util/proc.ts +0 -108
package/src/fetch/registry.ts
DELETED
|
@@ -1,615 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Registry adapter (SPEC 13.1-13.3). Fetches metadata and tarballs through
|
|
3
|
-
* npm-maintained libraries behind a typed surface with zyrk's own policy:
|
|
4
|
-
* bounded retries, bounded download size, mandatory integrity verification of
|
|
5
|
-
* the bytes we accept, and stable error mapping.
|
|
6
|
-
*
|
|
7
|
-
* This adapter never runs package code. Credential handling is deliberately
|
|
8
|
-
* not implemented yet: callers may pass through fetch options (an .npmrc-
|
|
9
|
-
* reading adapter lands later with its own review), and nothing here can
|
|
10
|
-
* disable TLS verification.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { open, rm } from "node:fs/promises";
|
|
14
|
-
import regFetch, {
|
|
15
|
-
json as fetchJson,
|
|
16
|
-
type FetchOptions,
|
|
17
|
-
} from "npm-registry-fetch";
|
|
18
|
-
import { ZyrkError } from "../errors.ts";
|
|
19
|
-
import type { Logger } from "../log.ts";
|
|
20
|
-
import {
|
|
21
|
-
assertIntegrityOk,
|
|
22
|
-
IntegrityHasher,
|
|
23
|
-
base64FromHex,
|
|
24
|
-
type HashedBytes,
|
|
25
|
-
} from "../util/integrity.ts";
|
|
26
|
-
|
|
27
|
-
/** 512 MiB: large for an npm package, small enough to bound mishaps. */
|
|
28
|
-
export const DEFAULT_MAX_TARBALL_BYTES = 512 * 1024 * 1024;
|
|
29
|
-
|
|
30
|
-
/** Default registry for tool environments (SPEC 13.1). */
|
|
31
|
-
export const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
|
|
32
|
-
|
|
33
|
-
/** npm's abbreviated packument media type, used for installs. */
|
|
34
|
-
const INSTALL_V1_ACCEPT = "application/vnd.npm.install-v1+json";
|
|
35
|
-
|
|
36
|
-
export interface PackumentDist {
|
|
37
|
-
tarball: string;
|
|
38
|
-
integrity?: string;
|
|
39
|
-
shasum?: string;
|
|
40
|
-
unpackedSize?: number;
|
|
41
|
-
fileCount?: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface PackumentVersion {
|
|
45
|
-
name: string;
|
|
46
|
-
version: string;
|
|
47
|
-
dist: PackumentDist;
|
|
48
|
-
dependencies?: Record<string, string>;
|
|
49
|
-
optionalDependencies?: Record<string, string>;
|
|
50
|
-
peerDependencies?: Record<string, string>;
|
|
51
|
-
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
|
|
52
|
-
bundledDependencies?: string[] | boolean;
|
|
53
|
-
bin?: string | Record<string, string>;
|
|
54
|
-
os?: string[];
|
|
55
|
-
cpu?: string[];
|
|
56
|
-
libc?: string[];
|
|
57
|
-
engines?: Record<string, string>;
|
|
58
|
-
deprecated?: string;
|
|
59
|
-
hasInstallScript?: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface Packument {
|
|
63
|
-
name: string;
|
|
64
|
-
distTags: Record<string, string>;
|
|
65
|
-
versions: Record<string, PackumentVersion>;
|
|
66
|
-
modified?: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface RegistryClientOptions {
|
|
70
|
-
/** Registry base URL; must be http(s). Trailing slash optional. */
|
|
71
|
-
registry: string;
|
|
72
|
-
userAgent: string;
|
|
73
|
-
logger?: Logger;
|
|
74
|
-
/** Bounded retries for retryable transport failures (default 2). */
|
|
75
|
-
retries?: number;
|
|
76
|
-
/** Per-request inactivity timeout in ms; passed through when set. */
|
|
77
|
-
timeoutMs?: number;
|
|
78
|
-
maxTarballBytes?: number;
|
|
79
|
-
/** Extra options for npm-registry-fetch. Never includes anything unsafe by default. */
|
|
80
|
-
fetchOptions?: FetchOptions;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface DownloadResult {
|
|
84
|
-
url: string;
|
|
85
|
-
bytes: number;
|
|
86
|
-
hashed: HashedBytes;
|
|
87
|
-
/** The digest accepted as the verified integrity for this download. */
|
|
88
|
-
verifiedSri: string;
|
|
89
|
-
/** True when only legacy sha1 integrity was available and accepted. */
|
|
90
|
-
weakIntegrity: boolean;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export class RegistryClient {
|
|
94
|
-
readonly #registry: string;
|
|
95
|
-
readonly #origin: string;
|
|
96
|
-
readonly #userAgent: string;
|
|
97
|
-
readonly #logger: Logger | undefined;
|
|
98
|
-
readonly #fetchOptions: FetchOptions;
|
|
99
|
-
readonly #maxTarballBytes: number;
|
|
100
|
-
|
|
101
|
-
constructor(options: RegistryClientOptions) {
|
|
102
|
-
const registry = normalizeRegistryUrl(options.registry);
|
|
103
|
-
this.#registry = registry;
|
|
104
|
-
this.#origin = credentialFreeOrigin(registry);
|
|
105
|
-
this.#userAgent = options.userAgent;
|
|
106
|
-
this.#logger = options.logger;
|
|
107
|
-
this.#maxTarballBytes =
|
|
108
|
-
options.maxTarballBytes ?? DEFAULT_MAX_TARBALL_BYTES;
|
|
109
|
-
|
|
110
|
-
const retries = options.retries ?? 2;
|
|
111
|
-
const base: FetchOptions = {
|
|
112
|
-
registry,
|
|
113
|
-
userAgent: options.userAgent,
|
|
114
|
-
retry: {
|
|
115
|
-
retries,
|
|
116
|
-
factor: 2,
|
|
117
|
-
minTimeout: 250,
|
|
118
|
-
maxTimeout: 8_000,
|
|
119
|
-
randomize: true,
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
if (options.timeoutMs !== undefined) base.timeout = options.timeoutMs;
|
|
123
|
-
this.#fetchOptions = { ...base, ...options.fetchOptions };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Credential-free registry origin; participates in source identity (§8.1). */
|
|
127
|
-
get origin(): string {
|
|
128
|
-
return this.#origin;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
get registry(): string {
|
|
132
|
-
return this.#registry;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Fetch the (abbreviated) packument for an exact package name. */
|
|
136
|
-
async packument(name: string): Promise<Packument> {
|
|
137
|
-
validateRegistryPackageName(name);
|
|
138
|
-
const url = `${this.#registry}${escapePackageName(name)}`;
|
|
139
|
-
this.#logger?.debug("fetching packument", { name, url });
|
|
140
|
-
let raw: unknown;
|
|
141
|
-
try {
|
|
142
|
-
raw = await fetchJson(url, {
|
|
143
|
-
...this.#fetchOptions,
|
|
144
|
-
headers: { accept: `${INSTALL_V1_ACCEPT}, application/json` },
|
|
145
|
-
});
|
|
146
|
-
} catch (error) {
|
|
147
|
-
throw mapFetchError(error, `fetch metadata for ${name}`);
|
|
148
|
-
}
|
|
149
|
-
return validatePackument(raw, name, this.#origin);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Download a tarball to `destPath` (which is owned by the caller's
|
|
154
|
-
* transaction staging and must not exist), verify its integrity, and
|
|
155
|
-
* fsync it. The file is removed on any failure.
|
|
156
|
-
*/
|
|
157
|
-
async downloadTarball(options: {
|
|
158
|
-
url: string;
|
|
159
|
-
/** Expected SRI. When undefined, strict callers must reject the download. */
|
|
160
|
-
expectedIntegrity: string | undefined;
|
|
161
|
-
destPath: string;
|
|
162
|
-
/** Human label used in errors, e.g. `foo@1.2.3`. */
|
|
163
|
-
label: string;
|
|
164
|
-
maxBytes?: number;
|
|
165
|
-
allowWeakIntegrity?: boolean;
|
|
166
|
-
allowMissingIntegrity?: boolean;
|
|
167
|
-
}): Promise<DownloadResult> {
|
|
168
|
-
const parsedUrl = parseTarballUrl(options.url, options.label);
|
|
169
|
-
const maxBytes = options.maxBytes ?? this.#maxTarballBytes;
|
|
170
|
-
this.#logger?.debug("downloading tarball", {
|
|
171
|
-
label: options.label,
|
|
172
|
-
url: parsedUrl.href,
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
let handle;
|
|
176
|
-
try {
|
|
177
|
-
handle = await open(options.destPath, "wx", 0o600);
|
|
178
|
-
} catch (error) {
|
|
179
|
-
if (isErrnoCode(error, "EEXIST")) {
|
|
180
|
-
throw new ZyrkError(
|
|
181
|
-
"ZYRK_INTERNAL",
|
|
182
|
-
`staging file already exists: ${options.destPath}`,
|
|
183
|
-
{
|
|
184
|
-
detail: { destPath: options.destPath },
|
|
185
|
-
},
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
throw error;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const hasher = new IntegrityHasher();
|
|
192
|
-
const startedAt = Date.now();
|
|
193
|
-
try {
|
|
194
|
-
const response = await regFetch(parsedUrl.href, {
|
|
195
|
-
...this.#fetchOptions,
|
|
196
|
-
headers: { accept: "application/octet-stream" },
|
|
197
|
-
});
|
|
198
|
-
if (!response.ok) {
|
|
199
|
-
// Drain a little of the body so the connection can be reused, then
|
|
200
|
-
// report by status.
|
|
201
|
-
response.body.resume();
|
|
202
|
-
throw httpStatusError(
|
|
203
|
-
response.status,
|
|
204
|
-
`download tarball for ${options.label}`,
|
|
205
|
-
options.url,
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
let written = 0;
|
|
210
|
-
for await (const chunk of response.body) {
|
|
211
|
-
const buffer = Buffer.isBuffer(chunk)
|
|
212
|
-
? chunk
|
|
213
|
-
: Buffer.from(chunk as Uint8Array);
|
|
214
|
-
written += buffer.byteLength;
|
|
215
|
-
if (written > maxBytes) {
|
|
216
|
-
response.body.destroy();
|
|
217
|
-
throw new ZyrkError(
|
|
218
|
-
"ZYRK_FETCH_REFUSED",
|
|
219
|
-
`tarball for ${options.label} exceeds the ${formatMiB(maxBytes)} download bound`,
|
|
220
|
-
{
|
|
221
|
-
detail: {
|
|
222
|
-
label: options.label,
|
|
223
|
-
url: options.url,
|
|
224
|
-
limitBytes: maxBytes,
|
|
225
|
-
},
|
|
226
|
-
remedy:
|
|
227
|
-
"verify this is the package you expect; raise the bound deliberately if the artifact is legitimate",
|
|
228
|
-
},
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
hasher.update(buffer);
|
|
232
|
-
await handle.write(buffer);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
await handle.sync();
|
|
236
|
-
await handle.close();
|
|
237
|
-
handle = undefined;
|
|
238
|
-
|
|
239
|
-
const hashed = hasher.finalize();
|
|
240
|
-
let verified: { verifiedSri: string; weak: boolean };
|
|
241
|
-
try {
|
|
242
|
-
verified = assertIntegrityOk(hashed, options.expectedIntegrity, {
|
|
243
|
-
what: `tarball for ${options.label}`,
|
|
244
|
-
...(options.allowWeakIntegrity === undefined
|
|
245
|
-
? {}
|
|
246
|
-
: { allowWeak: options.allowWeakIntegrity }),
|
|
247
|
-
...(options.allowMissingIntegrity === undefined
|
|
248
|
-
? {}
|
|
249
|
-
: { allowMissing: options.allowMissingIntegrity }),
|
|
250
|
-
});
|
|
251
|
-
} catch (error) {
|
|
252
|
-
await rm(options.destPath, { force: true });
|
|
253
|
-
throw error;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
this.#logger?.debug("tarball verified", {
|
|
257
|
-
label: options.label,
|
|
258
|
-
bytes: hashed.bytes,
|
|
259
|
-
ms: Date.now() - startedAt,
|
|
260
|
-
weakIntegrity: verified.weak,
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
return {
|
|
264
|
-
url: parsedUrl.href,
|
|
265
|
-
bytes: hashed.bytes,
|
|
266
|
-
hashed,
|
|
267
|
-
verifiedSri: verified.verifiedSri,
|
|
268
|
-
weakIntegrity: verified.weak,
|
|
269
|
-
};
|
|
270
|
-
} catch (error) {
|
|
271
|
-
if (handle !== undefined) {
|
|
272
|
-
try {
|
|
273
|
-
await handle.close();
|
|
274
|
-
} catch {
|
|
275
|
-
// The original failure is more useful.
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
await rm(options.destPath, { force: true });
|
|
279
|
-
throw mapFetchError(error, `download tarball for ${options.label}`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Pick the expected integrity string from dist metadata: prefer SRI, fall
|
|
286
|
-
* back to synthesizing sha1 from the legacy `shasum` field.
|
|
287
|
-
*/
|
|
288
|
-
export function expectedIntegrityFromDist(
|
|
289
|
-
dist: PackumentDist,
|
|
290
|
-
): string | undefined {
|
|
291
|
-
if (typeof dist.integrity === "string" && dist.integrity.trim() !== "") {
|
|
292
|
-
return dist.integrity;
|
|
293
|
-
}
|
|
294
|
-
if (typeof dist.shasum === "string" && /^[0-9a-f]{40}$/.test(dist.shasum)) {
|
|
295
|
-
return `sha1-${base64FromHex(dist.shasum)}`;
|
|
296
|
-
}
|
|
297
|
-
return undefined;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ---------------------------------------------------------------------------
|
|
301
|
-
|
|
302
|
-
function normalizeRegistryUrl(registry: string): string {
|
|
303
|
-
let url: URL;
|
|
304
|
-
try {
|
|
305
|
-
url = new URL(registry);
|
|
306
|
-
} catch (error) {
|
|
307
|
-
throw new ZyrkError(
|
|
308
|
-
"ZYRK_MANIFEST_INVALID",
|
|
309
|
-
`registry URL is not valid: ${registry}`,
|
|
310
|
-
{
|
|
311
|
-
remedy: "set a registry URL such as https://registry.npmjs.org/",
|
|
312
|
-
cause: error,
|
|
313
|
-
},
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
317
|
-
throw new ZyrkError(
|
|
318
|
-
"ZYRK_MANIFEST_INVALID",
|
|
319
|
-
`registry URL must be http(s): ${registry}`,
|
|
320
|
-
{
|
|
321
|
-
remedy: "set an http(s) registry URL",
|
|
322
|
-
},
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
if (url.username !== "" || url.password !== "") {
|
|
326
|
-
throw new ZyrkError(
|
|
327
|
-
"ZYRK_MANIFEST_INVALID",
|
|
328
|
-
"registry URL must not embed credentials",
|
|
329
|
-
{
|
|
330
|
-
remedy:
|
|
331
|
-
"configure credentials through the supported auth adapter instead of the URL",
|
|
332
|
-
},
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
url.search = "";
|
|
336
|
-
url.hash = "";
|
|
337
|
-
const href = url.href.endsWith("/") ? url.href : `${url.href}/`;
|
|
338
|
-
return href;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/** scheme://host[:port][/path] with credentials, query and fragment removed. */
|
|
342
|
-
function credentialFreeOrigin(registry: string): string {
|
|
343
|
-
const url = new URL(registry);
|
|
344
|
-
const path = url.pathname.endsWith("/")
|
|
345
|
-
? url.pathname.slice(0, -1)
|
|
346
|
-
: url.pathname;
|
|
347
|
-
return `${url.protocol}//${url.host}${path}`;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Reject package names that cannot be safely interpolated into a registry
|
|
352
|
-
* URL. Registry names are a restricted charset; anything else is a caller
|
|
353
|
-
* bug or an attack, not a package.
|
|
354
|
-
*/
|
|
355
|
-
export function validateRegistryPackageName(name: string): void {
|
|
356
|
-
const bad = (why: string): never => {
|
|
357
|
-
throw new ZyrkError(
|
|
358
|
-
"ZYRK_RESOLUTION_FAILED",
|
|
359
|
-
`invalid package name "${name}" (${why})`,
|
|
360
|
-
{
|
|
361
|
-
remedy: "use a real npm package name",
|
|
362
|
-
},
|
|
363
|
-
);
|
|
364
|
-
};
|
|
365
|
-
if (name === "" || name.length > 214) bad("empty or too long");
|
|
366
|
-
if (name.includes("\\") || name.includes("%") || name.includes("\0"))
|
|
367
|
-
bad("forbidden character");
|
|
368
|
-
// eslint-disable-next-line no-control-regex
|
|
369
|
-
if (/[\u0000-\u0020\u007f]/.test(name))
|
|
370
|
-
bad("control or whitespace character");
|
|
371
|
-
if (!/^(?:@[^/@]+\/)?[^/@]+$/.test(name))
|
|
372
|
-
bad("shape is not `name` or `@scope/name`");
|
|
373
|
-
for (const segment of name.split("/")) {
|
|
374
|
-
if (segment === "." || segment === "..") bad("path-like segment");
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function escapePackageName(name: string): string {
|
|
379
|
-
return name.replace("/", "%2f");
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function parseTarballUrl(url: string, label: string): URL {
|
|
383
|
-
let parsed: URL;
|
|
384
|
-
try {
|
|
385
|
-
parsed = new URL(url);
|
|
386
|
-
} catch (error) {
|
|
387
|
-
throw new ZyrkError(
|
|
388
|
-
"ZYRK_FETCH_REFUSED",
|
|
389
|
-
`registry metadata for ${label} has an invalid tarball URL`,
|
|
390
|
-
{
|
|
391
|
-
detail: { label, url },
|
|
392
|
-
remedy:
|
|
393
|
-
"report the registry response; zyrk refuses to fetch a malformed URL",
|
|
394
|
-
cause: error,
|
|
395
|
-
},
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
399
|
-
throw new ZyrkError(
|
|
400
|
-
"ZYRK_FETCH_REFUSED",
|
|
401
|
-
`tarball URL for ${label} uses unsupported protocol ${parsed.protocol}`,
|
|
402
|
-
{
|
|
403
|
-
detail: { label, url },
|
|
404
|
-
remedy: "only http(s) tarball URLs are supported",
|
|
405
|
-
},
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
return parsed;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function formatMiB(bytes: number): string {
|
|
412
|
-
return `${Math.round(bytes / (1024 * 1024))} MiB`;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function isErrnoCode(error: unknown, code: string): boolean {
|
|
416
|
-
return (
|
|
417
|
-
typeof error === "object" &&
|
|
418
|
-
error !== null &&
|
|
419
|
-
(error as NodeJS.ErrnoException).code === code
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const RETRYABLE_NETWORK_CODES = new Set([
|
|
424
|
-
"ECONNRESET",
|
|
425
|
-
"ECONNREFUSED",
|
|
426
|
-
"ECONNABORTED",
|
|
427
|
-
"ETIMEDOUT",
|
|
428
|
-
"ENOTFOUND",
|
|
429
|
-
"EAI_AGAIN",
|
|
430
|
-
"EPIPE",
|
|
431
|
-
"EHOSTUNREACH",
|
|
432
|
-
"ENETUNREACH",
|
|
433
|
-
"EAI_AGAIN",
|
|
434
|
-
"EPROTO",
|
|
435
|
-
"ERR_SSL_PROTOCOL_ERROR",
|
|
436
|
-
]);
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Map library/transport errors to stable codes. Keeps the library's message
|
|
440
|
-
* and status available in `detail` without dumping config or headers.
|
|
441
|
-
*/
|
|
442
|
-
function mapFetchError(error: unknown, what: string): ZyrkError {
|
|
443
|
-
if (error instanceof ZyrkError) return error;
|
|
444
|
-
|
|
445
|
-
const record = (error ?? {}) as {
|
|
446
|
-
message?: unknown;
|
|
447
|
-
code?: unknown;
|
|
448
|
-
statusCode?: unknown;
|
|
449
|
-
errno?: unknown;
|
|
450
|
-
};
|
|
451
|
-
const message =
|
|
452
|
-
typeof record.message === "string" ? record.message : String(error);
|
|
453
|
-
const status =
|
|
454
|
-
typeof record.statusCode === "number" ? record.statusCode : undefined;
|
|
455
|
-
const code = typeof record.code === "string" ? record.code : undefined;
|
|
456
|
-
|
|
457
|
-
if (status === 401 || status === 403 || code === "EOTP") {
|
|
458
|
-
return new ZyrkError(
|
|
459
|
-
"ZYRK_REGISTRY_AUTH",
|
|
460
|
-
`registry authorization failed while trying to ${what}`,
|
|
461
|
-
{
|
|
462
|
-
detail: { status, code },
|
|
463
|
-
remedy:
|
|
464
|
-
"verify the credentials configured for this registry (and their scope); zyrk never falls back to the public registry",
|
|
465
|
-
cause: error,
|
|
466
|
-
},
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
if (status !== undefined) {
|
|
470
|
-
return httpStatusError(status, what, undefined, error);
|
|
471
|
-
}
|
|
472
|
-
if (code !== undefined && RETRYABLE_NETWORK_CODES.has(code)) {
|
|
473
|
-
return new ZyrkError(
|
|
474
|
-
"ZYRK_FETCH_FAILED",
|
|
475
|
-
`network failure while trying to ${what}: ${message}`,
|
|
476
|
-
{
|
|
477
|
-
detail: { errno: code },
|
|
478
|
-
remedy:
|
|
479
|
-
"check network connectivity and registry availability, then retry",
|
|
480
|
-
cause: error,
|
|
481
|
-
},
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
return new ZyrkError("ZYRK_FETCH_FAILED", `failed to ${what}: ${message}`, {
|
|
485
|
-
...(code === undefined ? {} : { detail: { errno: code } }),
|
|
486
|
-
remedy: "check registry availability, then retry",
|
|
487
|
-
cause: error,
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function httpStatusError(
|
|
492
|
-
status: number,
|
|
493
|
-
what: string,
|
|
494
|
-
url?: string,
|
|
495
|
-
cause?: unknown,
|
|
496
|
-
): ZyrkError {
|
|
497
|
-
const detail: Record<string, unknown> = { status };
|
|
498
|
-
if (url !== undefined) detail.url = url;
|
|
499
|
-
if (status === 404) {
|
|
500
|
-
return new ZyrkError(
|
|
501
|
-
"ZYRK_RESOLUTION_FAILED",
|
|
502
|
-
`not found while trying to ${what} (HTTP 404)`,
|
|
503
|
-
{
|
|
504
|
-
detail,
|
|
505
|
-
remedy:
|
|
506
|
-
"verify the package name or version exists in the configured registry",
|
|
507
|
-
...(cause === undefined ? {} : { cause }),
|
|
508
|
-
},
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
if (status === 401 || status === 403) {
|
|
512
|
-
return new ZyrkError(
|
|
513
|
-
"ZYRK_REGISTRY_AUTH",
|
|
514
|
-
`registry authorization failed while trying to ${what} (HTTP ${status})`,
|
|
515
|
-
{
|
|
516
|
-
detail,
|
|
517
|
-
remedy: "verify the credentials configured for this registry",
|
|
518
|
-
...(cause === undefined ? {} : { cause }),
|
|
519
|
-
},
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
return new ZyrkError(
|
|
523
|
-
"ZYRK_FETCH_FAILED",
|
|
524
|
-
`registry responded HTTP ${status} while trying to ${what}`,
|
|
525
|
-
{
|
|
526
|
-
detail,
|
|
527
|
-
remedy: "retry later; if it persists, check the registry's status",
|
|
528
|
-
...(cause === undefined ? {} : { cause }),
|
|
529
|
-
},
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function validatePackument(
|
|
534
|
-
raw: unknown,
|
|
535
|
-
requestedName: string,
|
|
536
|
-
origin: string,
|
|
537
|
-
): Packument {
|
|
538
|
-
const bad = (why: string): never => {
|
|
539
|
-
throw new ZyrkError(
|
|
540
|
-
"ZYRK_RESOLUTION_FAILED",
|
|
541
|
-
`registry returned a malformed packument for ${requestedName} (${why})`,
|
|
542
|
-
{
|
|
543
|
-
detail: { name: requestedName, origin },
|
|
544
|
-
remedy:
|
|
545
|
-
"report the registry response; retry against a well-formed npm registry",
|
|
546
|
-
},
|
|
547
|
-
);
|
|
548
|
-
};
|
|
549
|
-
if (typeof raw !== "object" || raw === null) bad("not an object");
|
|
550
|
-
const doc = raw as Record<string, unknown>;
|
|
551
|
-
if (typeof doc["name"] !== "string") bad("missing name");
|
|
552
|
-
if ((doc["name"] as string).toLowerCase() !== requestedName.toLowerCase()) {
|
|
553
|
-
bad(
|
|
554
|
-
`name mismatch: asked for ${requestedName}, got ${doc["name"] as string}`,
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
const versionsRaw = doc["versions"];
|
|
558
|
-
if (
|
|
559
|
-
typeof versionsRaw !== "object" ||
|
|
560
|
-
versionsRaw === null ||
|
|
561
|
-
Array.isArray(versionsRaw)
|
|
562
|
-
) {
|
|
563
|
-
bad("missing versions map");
|
|
564
|
-
}
|
|
565
|
-
const distTagsRaw = doc["dist-tags"] ?? doc["distTags"];
|
|
566
|
-
if (
|
|
567
|
-
distTagsRaw !== undefined &&
|
|
568
|
-
(typeof distTagsRaw !== "object" || distTagsRaw === null)
|
|
569
|
-
) {
|
|
570
|
-
bad("dist-tags is not an object");
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const versions: Record<string, PackumentVersion> = {};
|
|
574
|
-
for (const [version, value] of Object.entries(
|
|
575
|
-
versionsRaw as Record<string, unknown>,
|
|
576
|
-
)) {
|
|
577
|
-
if (typeof value !== "object" || value === null)
|
|
578
|
-
bad(`version ${version} is not an object`);
|
|
579
|
-
const entry = value as Record<string, unknown>;
|
|
580
|
-
if (entry["version"] !== undefined && entry["version"] !== version) {
|
|
581
|
-
bad(
|
|
582
|
-
`version key ${version} disagrees with entry version ${String(entry["version"])}`,
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
|
-
const dist = entry["dist"];
|
|
586
|
-
if (typeof dist !== "object" || dist === null)
|
|
587
|
-
bad(`version ${version} has no dist block`);
|
|
588
|
-
const distRecord = dist as Record<string, unknown>;
|
|
589
|
-
if (
|
|
590
|
-
typeof distRecord["tarball"] !== "string" ||
|
|
591
|
-
distRecord["tarball"] === ""
|
|
592
|
-
) {
|
|
593
|
-
bad(`version ${version} has no tarball URL`);
|
|
594
|
-
}
|
|
595
|
-
versions[version] = {
|
|
596
|
-
...(entry as unknown as PackumentVersion),
|
|
597
|
-
name: typeof entry["name"] === "string" ? entry["name"] : requestedName,
|
|
598
|
-
version,
|
|
599
|
-
dist: dist as unknown as PackumentDist,
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const distTags: Record<string, string> = {};
|
|
604
|
-
if (distTagsRaw !== undefined) {
|
|
605
|
-
for (const [tag, target] of Object.entries(
|
|
606
|
-
distTagsRaw as Record<string, unknown>,
|
|
607
|
-
)) {
|
|
608
|
-
if (typeof target === "string") distTags[tag] = target;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const packument: Packument = { name: requestedName, distTags, versions };
|
|
613
|
-
if (typeof doc["modified"] === "string") packument.modified = doc["modified"];
|
|
614
|
-
return packument;
|
|
615
|
-
}
|