@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.
Files changed (2) hide show
  1. package/lib/esm/index.js +220 -218
  2. 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
- if (!body || typeof body !== "object") {
4
- throw new TypeError(`registry: ${name} packument is not an object`);
5
- }
6
- const p = body;
7
- if (typeof p.name !== "string") {
8
- throw new TypeError(`registry: ${name} packument missing string name`);
9
- }
10
- if (!p.versions || typeof p.versions !== "object") {
11
- throw new TypeError(`registry: ${name} packument missing versions map`);
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
- if (npmrc && name.startsWith("@")) {
16
- const scope = name.slice(0, name.indexOf("/"));
17
- const override = npmrc.scopes[scope];
18
- if (override) return ensureTrailingSlash(override);
19
- }
20
- if (npmrc?.registry) return ensureTrailingSlash(npmrc.registry);
21
- return DEFAULT_REGISTRY;
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
- const base = ensureTrailingSlash(registry);
25
- if (name.startsWith("@")) {
26
- const slash = name.indexOf("/");
27
- if (slash < 0) throw new TypeError(`Invalid scoped package name: ${name}`);
28
- const scope = name.slice(0, slash);
29
- const rest = name.slice(slash + 1);
30
- return `${base}${encodeURIComponent(scope)}/${encodeURIComponent(rest)}`;
31
- }
32
- return `${base}${encodeURIComponent(name)}`;
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
- const registry = opts.registry ?? registryFor(name, opts.npmrc);
36
- const url = packumentUrl(name, registry);
37
- const headers = buildHeaders(url, opts);
38
- headers["accept"] ??= "application/vnd.npm.install-v1+json";
39
- const fetchImpl = opts.fetch ?? globalThis.fetch;
40
- if (!fetchImpl) throw new Error("@gjsify/npm-registry: globalThis.fetch is missing");
41
- const res = await fetchImpl(url, { headers, signal: opts.signal });
42
- if (!res.ok) {
43
- if (res.status === 404) throw new PackageNotFoundError(name, url);
44
- throw new Error(`registry GET ${url} -> ${res.status} ${res.statusText}`);
45
- }
46
- const body = await res.json();
47
- assertPackument(name, body);
48
- return body;
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
- const headers = buildHeaders(url, opts);
52
- headers["accept"] ??= "application/octet-stream";
53
- const fetchImpl = opts.fetch ?? globalThis.fetch;
54
- if (!fetchImpl) throw new Error("@gjsify/npm-registry: globalThis.fetch is missing");
55
- const res = await fetchImpl(url, { headers, signal: opts.signal });
56
- if (!res.ok) throw new Error(`tarball GET ${url} -> ${res.status} ${res.statusText}`);
57
- const buf = new Uint8Array(await res.arrayBuffer());
58
- if (opts.integrity) {
59
- const ok = await verifyIntegrity(buf, opts.integrity);
60
- if (!ok) throw new IntegrityError(url, opts.integrity);
61
- }
62
- return buf;
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
- const parts = integrity.trim().split(/\s+/);
66
- for (const part of parts) {
67
- const dash = part.indexOf("-");
68
- if (dash < 0) continue;
69
- const algo = part.slice(0, dash).toLowerCase();
70
- const expected = part.slice(dash + 1);
71
- const subtle = globalThis.crypto?.subtle;
72
- if (!subtle) throw new Error("@gjsify/npm-registry: globalThis.crypto.subtle is missing");
73
- const algoName = subriToWebCryptoAlgo(algo);
74
- if (!algoName) continue;
75
- const digest = await subtle.digest(algoName, dataAsArrayBuffer(data));
76
- const got = bytesToBase64(new Uint8Array(digest));
77
- if (got === expected) return true;
78
- }
79
- return false;
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
- switch (sri) {
83
- case "sha1":
84
- return "SHA-1";
85
- case "sha256":
86
- return "SHA-256";
87
- case "sha384":
88
- return "SHA-384";
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
- if (data.byteOffset === 0 && data.byteLength === data.buffer.byteLength) {
97
- return data.buffer;
98
- }
99
- const copy = new Uint8Array(data.byteLength);
100
- copy.set(data);
101
- return copy.buffer;
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
- let bin = "";
105
- for (let i = 0; i < bytes.length; i++) {
106
- bin += String.fromCharCode(bytes[i]);
107
- }
108
- return btoa(bin);
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
- const out = {
112
- registry: DEFAULT_REGISTRY,
113
- scopes: {},
114
- authTokens: {},
115
- basicAuth: {}
116
- };
117
- const lines = text.split(/\r?\n/);
118
- const basic = {};
119
- for (const raw of lines) {
120
- const line = raw.replace(/^\s+|\s+$/g, "");
121
- if (!line || line.startsWith("#") || line.startsWith(";")) continue;
122
- const eq = line.indexOf("=");
123
- if (eq < 0) continue;
124
- const key = line.slice(0, eq).trim();
125
- const value = expandEnv(stripQuotes(line.slice(eq + 1).trim()));
126
- if (key === "registry") {
127
- out.registry = ensureTrailingSlash(value);
128
- continue;
129
- }
130
- const scopeRegistry = key.match(/^(@[^:]+):registry$/);
131
- if (scopeRegistry) {
132
- out.scopes[scopeRegistry[1]] = ensureTrailingSlash(value);
133
- continue;
134
- }
135
- const tokenMatch = key.match(/^\/\/(.+):_authToken$/);
136
- if (tokenMatch) {
137
- out.authTokens[normalizeAuthHost(tokenMatch[1])] = value;
138
- continue;
139
- }
140
- const userMatch = key.match(/^\/\/(.+):username$/);
141
- if (userMatch) {
142
- (basic[normalizeAuthHost(userMatch[1])] ??= {}).user = value;
143
- continue;
144
- }
145
- const passMatch = key.match(/^\/\/(.+):_password$/);
146
- if (passMatch) {
147
- const decoded = base64Decode(value);
148
- (basic[normalizeAuthHost(passMatch[1])] ??= {}).pass = decoded;
149
- continue;
150
- }
151
- }
152
- for (const [host, creds] of Object.entries(basic)) {
153
- if (creds.user && creds.pass !== void 0) {
154
- out.basicAuth[host] = { username: creds.user, password: creds.pass };
155
- }
156
- }
157
- return out;
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
- const headers = { "user-agent": "gjsify-install/0.3.7" };
161
- if (opts.npmrc) {
162
- const auth = resolveAuthForUrl(url, opts.npmrc);
163
- if (auth) headers["authorization"] = auth;
164
- }
165
- if (opts.headers) {
166
- for (const [k, v] of Object.entries(opts.headers)) headers[k.toLowerCase()] = v;
167
- }
168
- return headers;
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
- const u = new URL(url);
172
- const candidates = pathPrefixes(u);
173
- for (const prefix of candidates) {
174
- const token = npmrc.authTokens[prefix];
175
- if (token) return `Bearer ${token}`;
176
- const basic = npmrc.basicAuth[prefix];
177
- if (basic) {
178
- const enc = btoa(`${basic.username}:${basic.password}`);
179
- return `Basic ${enc}`;
180
- }
181
- }
182
- return null;
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
- const segments = u.pathname.split("/").filter(Boolean);
186
- const prefixes = [];
187
- for (let i = segments.length; i >= 0; i--) {
188
- const tail = segments.slice(0, i).join("/");
189
- prefixes.push(tail ? `//${u.host}/${tail}` : `//${u.host}`);
190
- }
191
- return prefixes;
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
- const trimmed = captured.replace(/\/+$/, "");
195
- return `//${trimmed}`;
211
+ const trimmed = captured.replace(/\/+$/, "");
212
+ return `//${trimmed}`;
196
213
  }
197
214
  function ensureTrailingSlash(s) {
198
- return s.endsWith("/") ? s : s + "/";
215
+ return s.endsWith("/") ? s : s + "/";
199
216
  }
200
217
  function stripQuotes(s) {
201
- if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
202
- return s.slice(1, -1);
203
- }
204
- return s;
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
- return s.replace(/\$\{([A-Z0-9_]+)\}/gi, (_m, name) => {
208
- const env = globalThis.process?.env;
209
- return env?.[name] ?? "";
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
- return atob(s);
214
- }
215
- class PackageNotFoundError extends Error {
216
- constructor(name, url) {
217
- super(`Package not found in registry: ${name} (${url})`);
218
- this.name = name;
219
- this.url = url;
220
- this.name = "PackageNotFoundError";
221
- }
222
- name;
223
- url;
224
- }
225
- class IntegrityError extends Error {
226
- constructor(url, integrity) {
227
- super(`Tarball integrity mismatch for ${url} (expected ${integrity})`);
228
- this.url = url;
229
- this.integrity = integrity;
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.12",
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.12",
38
- "@gjsify/unit": "^0.3.12",
37
+ "@gjsify/cli": "^0.3.14",
38
+ "@gjsify/unit": "^0.3.14",
39
39
  "typescript": "^6.0.3"
40
40
  }
41
41
  }