@gjsify/npm-registry 0.3.12 → 0.3.14
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/lib/esm/index.js +220 -218
- package/package.json +3 -3
package/lib/esm/index.js
CHANGED
|
@@ -1,248 +1,250 @@
|
|
|
1
|
+
//#region src/index.ts
|
|
1
2
|
const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
|
|
3
|
+
/** Strict-validate a packument shape. Throws on schema mismatch. */
|
|
2
4
|
function assertPackument(name, body) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
5
|
+
if (!body || typeof body !== "object") {
|
|
6
|
+
throw new TypeError(`registry: ${name} packument is not an object`);
|
|
7
|
+
}
|
|
8
|
+
const p = body;
|
|
9
|
+
if (typeof p.name !== "string") {
|
|
10
|
+
throw new TypeError(`registry: ${name} packument missing string name`);
|
|
11
|
+
}
|
|
12
|
+
if (!p.versions || typeof p.versions !== "object") {
|
|
13
|
+
throw new TypeError(`registry: ${name} packument missing versions map`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Pick the right registry URL for a package name (scoped overrides win). */
|
|
14
17
|
function registryFor(name, npmrc) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
18
|
+
if (npmrc && name.startsWith("@")) {
|
|
19
|
+
const scope = name.slice(0, name.indexOf("/"));
|
|
20
|
+
const override = npmrc.scopes[scope];
|
|
21
|
+
if (override) return ensureTrailingSlash(override);
|
|
22
|
+
}
|
|
23
|
+
if (npmrc?.registry) return ensureTrailingSlash(npmrc.registry);
|
|
24
|
+
return DEFAULT_REGISTRY;
|
|
25
|
+
}
|
|
26
|
+
/** Build the GET URL for a packument. Handles `@scope/name` URL-encoding. */
|
|
23
27
|
function packumentUrl(name, registry) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
28
|
+
const base = ensureTrailingSlash(registry);
|
|
29
|
+
if (name.startsWith("@")) {
|
|
30
|
+
const slash = name.indexOf("/");
|
|
31
|
+
if (slash < 0) throw new TypeError(`Invalid scoped package name: ${name}`);
|
|
32
|
+
const scope = name.slice(0, slash);
|
|
33
|
+
const rest = name.slice(slash + 1);
|
|
34
|
+
return `${base}${encodeURIComponent(scope)}/${encodeURIComponent(rest)}`;
|
|
35
|
+
}
|
|
36
|
+
return `${base}${encodeURIComponent(name)}`;
|
|
37
|
+
}
|
|
38
|
+
/** Fetch + parse a packument. */
|
|
34
39
|
async function fetchPackument(name, opts = {}) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
const registry = opts.registry ?? registryFor(name, opts.npmrc);
|
|
41
|
+
const url = packumentUrl(name, registry);
|
|
42
|
+
const headers = buildHeaders(url, opts);
|
|
43
|
+
headers["accept"] ??= "application/vnd.npm.install-v1+json";
|
|
44
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
45
|
+
if (!fetchImpl) throw new Error("@gjsify/npm-registry: globalThis.fetch is missing");
|
|
46
|
+
const res = await fetchImpl(url, {
|
|
47
|
+
headers,
|
|
48
|
+
signal: opts.signal
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
if (res.status === 404) throw new PackageNotFoundError(name, url);
|
|
52
|
+
throw new Error(`registry GET ${url} -> ${res.status} ${res.statusText}`);
|
|
53
|
+
}
|
|
54
|
+
const body = await res.json();
|
|
55
|
+
assertPackument(name, body);
|
|
56
|
+
return body;
|
|
57
|
+
}
|
|
58
|
+
/** Download a tarball as bytes. Verifies SRI `integrity` when supplied. */
|
|
50
59
|
async function fetchTarball(url, opts = {}) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
const headers = buildHeaders(url, opts);
|
|
61
|
+
headers["accept"] ??= "application/octet-stream";
|
|
62
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
63
|
+
if (!fetchImpl) throw new Error("@gjsify/npm-registry: globalThis.fetch is missing");
|
|
64
|
+
const res = await fetchImpl(url, {
|
|
65
|
+
headers,
|
|
66
|
+
signal: opts.signal
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) throw new Error(`tarball GET ${url} -> ${res.status} ${res.statusText}`);
|
|
69
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
70
|
+
if (opts.integrity) {
|
|
71
|
+
const ok = await verifyIntegrity(buf, opts.integrity);
|
|
72
|
+
if (!ok) throw new IntegrityError(url, opts.integrity);
|
|
73
|
+
}
|
|
74
|
+
return buf;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Verify an SRI string (e.g. `sha512-base64==`) against bytes.
|
|
78
|
+
* Multiple hashes (space-separated) accepted; any match passes.
|
|
79
|
+
*/
|
|
64
80
|
async function verifyIntegrity(data, integrity) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
const parts = integrity.trim().split(/\s+/);
|
|
82
|
+
for (const part of parts) {
|
|
83
|
+
const dash = part.indexOf("-");
|
|
84
|
+
if (dash < 0) continue;
|
|
85
|
+
const algo = part.slice(0, dash).toLowerCase();
|
|
86
|
+
const expected = part.slice(dash + 1);
|
|
87
|
+
const subtle = globalThis.crypto?.subtle;
|
|
88
|
+
if (!subtle) throw new Error("@gjsify/npm-registry: globalThis.crypto.subtle is missing");
|
|
89
|
+
const algoName = subriToWebCryptoAlgo(algo);
|
|
90
|
+
if (!algoName) continue;
|
|
91
|
+
const digest = await subtle.digest(algoName, dataAsArrayBuffer(data));
|
|
92
|
+
const got = bytesToBase64(new Uint8Array(digest));
|
|
93
|
+
if (got === expected) return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
80
96
|
}
|
|
81
97
|
function subriToWebCryptoAlgo(sri) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
case "sha512":
|
|
90
|
-
return "SHA-512";
|
|
91
|
-
default:
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
98
|
+
switch (sri) {
|
|
99
|
+
case "sha1": return "SHA-1";
|
|
100
|
+
case "sha256": return "SHA-256";
|
|
101
|
+
case "sha384": return "SHA-384";
|
|
102
|
+
case "sha512": return "SHA-512";
|
|
103
|
+
default: return null;
|
|
104
|
+
}
|
|
94
105
|
}
|
|
95
106
|
function dataAsArrayBuffer(data) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
if (data.byteOffset === 0 && data.byteLength === data.buffer.byteLength) {
|
|
108
|
+
return data.buffer;
|
|
109
|
+
}
|
|
110
|
+
const copy = new Uint8Array(data.byteLength);
|
|
111
|
+
copy.set(data);
|
|
112
|
+
return copy.buffer;
|
|
102
113
|
}
|
|
103
114
|
function bytesToBase64(bytes) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
115
|
+
let bin = "";
|
|
116
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
117
|
+
bin += String.fromCharCode(bytes[i]);
|
|
118
|
+
}
|
|
119
|
+
return btoa(bin);
|
|
109
120
|
}
|
|
121
|
+
/** Parse a `.npmrc` text body. Unknown keys are kept on the result for callers. */
|
|
110
122
|
function parseNpmrc(text) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
123
|
+
const out = {
|
|
124
|
+
registry: DEFAULT_REGISTRY,
|
|
125
|
+
scopes: {},
|
|
126
|
+
authTokens: {},
|
|
127
|
+
basicAuth: {}
|
|
128
|
+
};
|
|
129
|
+
const lines = text.split(/\r?\n/);
|
|
130
|
+
const basic = {};
|
|
131
|
+
for (const raw of lines) {
|
|
132
|
+
const line = raw.replace(/^\s+|\s+$/g, "");
|
|
133
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
134
|
+
const eq = line.indexOf("=");
|
|
135
|
+
if (eq < 0) continue;
|
|
136
|
+
const key = line.slice(0, eq).trim();
|
|
137
|
+
const value = expandEnv(stripQuotes(line.slice(eq + 1).trim()));
|
|
138
|
+
if (key === "registry") {
|
|
139
|
+
out.registry = ensureTrailingSlash(value);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const scopeRegistry = key.match(/^(@[^:]+):registry$/);
|
|
143
|
+
if (scopeRegistry) {
|
|
144
|
+
out.scopes[scopeRegistry[1]] = ensureTrailingSlash(value);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const tokenMatch = key.match(/^\/\/(.+):_authToken$/);
|
|
148
|
+
if (tokenMatch) {
|
|
149
|
+
out.authTokens[normalizeAuthHost(tokenMatch[1])] = value;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const userMatch = key.match(/^\/\/(.+):username$/);
|
|
153
|
+
if (userMatch) {
|
|
154
|
+
(basic[normalizeAuthHost(userMatch[1])] ??= {}).user = value;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const passMatch = key.match(/^\/\/(.+):_password$/);
|
|
158
|
+
if (passMatch) {
|
|
159
|
+
const decoded = base64Decode(value);
|
|
160
|
+
(basic[normalizeAuthHost(passMatch[1])] ??= {}).pass = decoded;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const [host, creds] of Object.entries(basic)) {
|
|
165
|
+
if (creds.user && creds.pass !== undefined) {
|
|
166
|
+
out.basicAuth[host] = {
|
|
167
|
+
username: creds.user,
|
|
168
|
+
password: creds.pass
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
/** Build auth + UA headers for a request URL. Pure (no I/O). */
|
|
159
175
|
function buildHeaders(url, opts) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
176
|
+
const headers = { "user-agent": "gjsify-install/0.3.7" };
|
|
177
|
+
if (opts.npmrc) {
|
|
178
|
+
const auth = resolveAuthForUrl(url, opts.npmrc);
|
|
179
|
+
if (auth) headers["authorization"] = auth;
|
|
180
|
+
}
|
|
181
|
+
if (opts.headers) {
|
|
182
|
+
for (const [k, v] of Object.entries(opts.headers)) headers[k.toLowerCase()] = v;
|
|
183
|
+
}
|
|
184
|
+
return headers;
|
|
185
|
+
}
|
|
186
|
+
/** Resolve an `Authorization` header for a URL given a parsed .npmrc. */
|
|
170
187
|
function resolveAuthForUrl(url, npmrc) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
const u = new URL(url);
|
|
189
|
+
const candidates = pathPrefixes(u);
|
|
190
|
+
for (const prefix of candidates) {
|
|
191
|
+
const token = npmrc.authTokens[prefix];
|
|
192
|
+
if (token) return `Bearer ${token}`;
|
|
193
|
+
const basic = npmrc.basicAuth[prefix];
|
|
194
|
+
if (basic) {
|
|
195
|
+
const enc = btoa(`${basic.username}:${basic.password}`);
|
|
196
|
+
return `Basic ${enc}`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
183
200
|
}
|
|
184
201
|
function pathPrefixes(u) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
202
|
+
const segments = u.pathname.split("/").filter(Boolean);
|
|
203
|
+
const prefixes = [];
|
|
204
|
+
for (let i = segments.length; i >= 0; i--) {
|
|
205
|
+
const tail = segments.slice(0, i).join("/");
|
|
206
|
+
prefixes.push(tail ? `//${u.host}/${tail}` : `//${u.host}`);
|
|
207
|
+
}
|
|
208
|
+
return prefixes;
|
|
192
209
|
}
|
|
193
210
|
function normalizeAuthHost(captured) {
|
|
194
|
-
|
|
195
|
-
|
|
211
|
+
const trimmed = captured.replace(/\/+$/, "");
|
|
212
|
+
return `//${trimmed}`;
|
|
196
213
|
}
|
|
197
214
|
function ensureTrailingSlash(s) {
|
|
198
|
-
|
|
215
|
+
return s.endsWith("/") ? s : s + "/";
|
|
199
216
|
}
|
|
200
217
|
function stripQuotes(s) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
218
|
+
if (s.startsWith("\"") && s.endsWith("\"") || s.startsWith("'") && s.endsWith("'")) {
|
|
219
|
+
return s.slice(1, -1);
|
|
220
|
+
}
|
|
221
|
+
return s;
|
|
205
222
|
}
|
|
206
223
|
function expandEnv(s) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
224
|
+
return s.replace(/\$\{([A-Z0-9_]+)\}/gi, (_m, name) => {
|
|
225
|
+
const env = globalThis.process?.env;
|
|
226
|
+
return env?.[name] ?? "";
|
|
227
|
+
});
|
|
211
228
|
}
|
|
212
229
|
function base64Decode(s) {
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this.name = "IntegrityError";
|
|
231
|
-
}
|
|
232
|
-
url;
|
|
233
|
-
integrity;
|
|
234
|
-
}
|
|
235
|
-
export {
|
|
236
|
-
DEFAULT_REGISTRY,
|
|
237
|
-
IntegrityError,
|
|
238
|
-
PackageNotFoundError,
|
|
239
|
-
assertPackument,
|
|
240
|
-
buildHeaders,
|
|
241
|
-
fetchPackument,
|
|
242
|
-
fetchTarball,
|
|
243
|
-
packumentUrl,
|
|
244
|
-
parseNpmrc,
|
|
245
|
-
registryFor,
|
|
246
|
-
resolveAuthForUrl,
|
|
247
|
-
verifyIntegrity
|
|
230
|
+
return atob(s);
|
|
231
|
+
}
|
|
232
|
+
var PackageNotFoundError = class extends Error {
|
|
233
|
+
constructor(name, url) {
|
|
234
|
+
super(`Package not found in registry: ${name} (${url})`);
|
|
235
|
+
this.name = name;
|
|
236
|
+
this.url = url;
|
|
237
|
+
this.name = "PackageNotFoundError";
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
var IntegrityError = class extends Error {
|
|
241
|
+
constructor(url, integrity) {
|
|
242
|
+
super(`Tarball integrity mismatch for ${url} (expected ${integrity})`);
|
|
243
|
+
this.url = url;
|
|
244
|
+
this.integrity = integrity;
|
|
245
|
+
this.name = "IntegrityError";
|
|
246
|
+
}
|
|
248
247
|
};
|
|
248
|
+
|
|
249
|
+
//#endregion
|
|
250
|
+
export { DEFAULT_REGISTRY, IntegrityError, PackageNotFoundError, assertPackument, buildHeaders, fetchPackument, fetchTarball, packumentUrl, parseNpmrc, registryFor, resolveAuthForUrl, verifyIntegrity };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/npm-registry",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
4
4
|
"description": "npm registry client for the gjsify install backend — packuments, tarballs, .npmrc auth (Node + GJS)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "lib/esm/index.js",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
],
|
|
35
35
|
"license": "MIT",
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@gjsify/cli": "^0.3.
|
|
38
|
-
"@gjsify/unit": "^0.3.
|
|
37
|
+
"@gjsify/cli": "^0.3.14",
|
|
38
|
+
"@gjsify/unit": "^0.3.14",
|
|
39
39
|
"typescript": "^6.0.3"
|
|
40
40
|
}
|
|
41
41
|
}
|