@vltpkg/vsr 0.0.0-27 → 0.0.0-28

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 (80) hide show
  1. package/DEPLOY.md +163 -0
  2. package/LICENSE +114 -10
  3. package/config.ts +221 -0
  4. package/dist/README.md +1 -1
  5. package/dist/bin/vsr.js +8 -6
  6. package/dist/index.js +3 -6
  7. package/dist/index.js.map +2 -2
  8. package/drizzle.config.js +40 -0
  9. package/info/COMPARISONS.md +37 -0
  10. package/info/CONFIGURATION.md +143 -0
  11. package/info/CONTRIBUTING.md +32 -0
  12. package/info/DATABASE_SETUP.md +108 -0
  13. package/info/GRANULAR_ACCESS_TOKENS.md +160 -0
  14. package/info/PROJECT_STRUCTURE.md +291 -0
  15. package/info/ROADMAP.md +27 -0
  16. package/info/SUPPORT.md +39 -0
  17. package/info/TESTING.md +301 -0
  18. package/info/USER_SUPPORT.md +31 -0
  19. package/package.json +49 -6
  20. package/scripts/build-assets.js +31 -0
  21. package/scripts/build-bin.js +63 -0
  22. package/src/assets/public/images/bg.png +0 -0
  23. package/src/assets/public/images/clients/logo-bun.png +0 -0
  24. package/src/assets/public/images/clients/logo-deno.png +0 -0
  25. package/src/assets/public/images/clients/logo-npm.png +0 -0
  26. package/src/assets/public/images/clients/logo-pnpm.png +0 -0
  27. package/src/assets/public/images/clients/logo-vlt.png +0 -0
  28. package/src/assets/public/images/clients/logo-yarn.png +0 -0
  29. package/src/assets/public/images/favicon/apple-touch-icon.png +0 -0
  30. package/src/assets/public/images/favicon/favicon-96x96.png +0 -0
  31. package/src/assets/public/images/favicon/favicon.ico +0 -0
  32. package/src/assets/public/images/favicon/favicon.svg +3 -0
  33. package/src/assets/public/images/favicon/site.webmanifest +21 -0
  34. package/src/assets/public/images/favicon/web-app-manifest-192x192.png +0 -0
  35. package/src/assets/public/images/favicon/web-app-manifest-512x512.png +0 -0
  36. package/src/assets/public/styles/styles.css +231 -0
  37. package/src/bin/demo/package.json +6 -0
  38. package/src/bin/demo/vlt.json +1 -0
  39. package/src/bin/vsr.ts +496 -0
  40. package/src/db/client.ts +590 -0
  41. package/src/db/migrations/0000_faulty_ricochet.sql +14 -0
  42. package/src/db/migrations/0000_initial.sql +29 -0
  43. package/src/db/migrations/0001_uuid_validation.sql +35 -0
  44. package/src/db/migrations/0001_wealthy_magdalene.sql +7 -0
  45. package/src/db/migrations/drop.sql +3 -0
  46. package/src/db/migrations/meta/0000_snapshot.json +104 -0
  47. package/src/db/migrations/meta/0001_snapshot.json +155 -0
  48. package/src/db/migrations/meta/_journal.json +20 -0
  49. package/src/db/schema.ts +43 -0
  50. package/src/index.ts +434 -0
  51. package/src/middleware/config.ts +79 -0
  52. package/src/middleware/telemetry.ts +43 -0
  53. package/src/queue/index.ts +97 -0
  54. package/src/routes/access.ts +852 -0
  55. package/src/routes/docs.ts +63 -0
  56. package/src/routes/misc.ts +469 -0
  57. package/src/routes/packages.ts +2823 -0
  58. package/src/routes/ping.ts +39 -0
  59. package/src/routes/search.ts +131 -0
  60. package/src/routes/static.ts +74 -0
  61. package/src/routes/tokens.ts +259 -0
  62. package/src/routes/users.ts +68 -0
  63. package/src/utils/auth.ts +202 -0
  64. package/src/utils/cache.ts +587 -0
  65. package/src/utils/config.ts +50 -0
  66. package/src/utils/database.ts +69 -0
  67. package/src/utils/docs.ts +146 -0
  68. package/src/utils/packages.ts +453 -0
  69. package/src/utils/response.ts +125 -0
  70. package/src/utils/routes.ts +64 -0
  71. package/src/utils/spa.ts +52 -0
  72. package/src/utils/tracing.ts +52 -0
  73. package/src/utils/upstream.ts +172 -0
  74. package/tsconfig.json +16 -0
  75. package/tsconfig.worker.json +3 -0
  76. package/typedoc.mjs +2 -0
  77. package/types.ts +598 -0
  78. package/vitest.config.ts +25 -0
  79. package/vlt.json.example +56 -0
  80. package/wrangler.json +65 -0
@@ -0,0 +1,146 @@
1
+ export const apiBody = ({ YEAR }: { YEAR: number }) => {
2
+ return `The **vlt serverless registry** is the modern JavaScript package registry.
3
+
4
+ ### Compatible Clients
5
+
6
+ <table>
7
+ <tbody>
8
+ <tr>
9
+ <td><a href="https://vlt.sh" alt="vlt"><strong><code>vlt</code></strong></a></td>
10
+ <td><a href="https://npmjs.com/package/npm" alt="npm"><strong><code>npm</code></strong></a></td>
11
+ <td><a href="https://yarnpkg.com/" alt="yarn"><strong><code>yarn</code></strong></a></td>
12
+ <td><a href="https://pnpm.io/" alt="pnpm"><strong><code>pnpm</code></strong></a></td>
13
+ <td><a href="https://deno.com/" alt="deno"><strong><code>deno</code></strong></a></td>
14
+ <td><a href="https://bun.sh/" alt="bun"><strong><code>bun</code></strong></a></td>
15
+ </tr>
16
+ </tbody>
17
+ </table>
18
+
19
+ ### Resources
20
+
21
+ <ul alt="resources">
22
+ <li><a href="https://vlt.sh">https://<strong>vlt.sh</strong></a></li>
23
+ <li><a href="https://github.com/vltpkg/vsr">https://github.com/<strong>vltpkg/vsr</strong></a></li>
24
+ <li><a href="https://discord.gg/vltpkg">https://discord.gg/<strong>vltpkg</strong></a></li>
25
+ <li><a href="https://x.com/vltpkg">https://x.com/<strong>vltpkg</strong></a></li>
26
+ </ul>
27
+
28
+ ##### Trademark Disclaimer
29
+
30
+ <p alt="trademark-disclaimer">All trademarks, logos and brand names are the property of their respective owners. All company, product and service names used in this website are for identification purposes only. Use of these names, trademarks and brands does not imply endorsement.</p>
31
+
32
+ ### License
33
+
34
+ <details alt="license">
35
+ <summary><strong>Functional Source License</strong>, Version 1.1, MIT Future License</summary>
36
+ <h1>Functional Source License,<br />Version 1.1,<br />MIT Future License</h1>
37
+ <h2>Abbreviation</h2>
38
+
39
+ FSL-1.1-MIT
40
+
41
+ <h2>Notice</h2>
42
+
43
+ Copyright ${YEAR} vlt technology inc.
44
+
45
+ <h2>Terms and Conditions</h2>
46
+
47
+ <h3>Licensor ("We")</h3>
48
+
49
+ The party offering the Software under these Terms and Conditions.
50
+
51
+ <h3>The Software</h3>
52
+
53
+ The "Software" is each version of the software that we make available under
54
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
55
+ Conditions with the Software.
56
+
57
+ <h3>License Grant</h3>
58
+
59
+ Subject to your compliance with this License Grant and the Patents,
60
+ Redistribution and Trademark clauses below, we hereby grant you the right to
61
+ use, copy, modify, create derivative works, publicly perform, publicly display
62
+ and redistribute the Software for any Permitted Purpose identified below.
63
+
64
+ <h3>Permitted Purpose</h3>
65
+
66
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
67
+ means making the Software available to others in a commercial product or
68
+ service that:
69
+
70
+ 1. substitutes for the Software;
71
+
72
+ 2. substitutes for any other product or service we offer using the Software
73
+ that exists as of the date we make the Software available; or
74
+
75
+ 3. offers the same or substantially similar functionality as the Software.
76
+
77
+ Permitted Purposes specifically include using the Software:
78
+
79
+ 1. for your internal use and access;
80
+
81
+ 2. for non-commercial education;
82
+
83
+ 3. for non-commercial research; and
84
+
85
+ 4. in connection with professional services that you provide to a licensee
86
+ using the Software in accordance with these Terms and Conditions.
87
+
88
+ <h3>Patents</h3>
89
+
90
+ To the extent your use for a Permitted Purpose would necessarily infringe our
91
+ patents, the license grant above includes a license under our patents. If you
92
+ make a claim against any party that the Software infringes or contributes to
93
+ the infringement of any patent, then your patent license to the Software ends
94
+ immediately.
95
+
96
+ <h3>Redistribution</h3>
97
+
98
+ The Terms and Conditions apply to all copies, modifications and derivatives of
99
+ the Software.
100
+
101
+ If you redistribute any copies, modifications or derivatives of the Software,
102
+ you must include a copy of or a link to these Terms and Conditions and not
103
+ remove any copyright notices provided in or with the Software.
104
+
105
+ <h3>Disclaimer</h3>
106
+
107
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
108
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
109
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
110
+
111
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
112
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
113
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
114
+
115
+ <h3>Trademarks</h3>
116
+
117
+ Except for displaying the License Details and identifying us as the origin of
118
+ the Software, you have no right under these Terms and Conditions to use our
119
+ trademarks, trade names, service marks or product names.
120
+
121
+ <h2>Grant of Future License</h2>
122
+
123
+ We hereby irrevocably grant you an additional license to use the Software under
124
+ the MIT license that is effective on the second anniversary of the date we make
125
+ the Software available. On or after that date, you may use the Software under
126
+ the MIT license, in which case the following will apply:
127
+
128
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
129
+ this software and associated documentation files (the "Software"), to deal in
130
+ the Software without restriction, including without limitation the rights to
131
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
132
+ of the Software, and to permit persons to whom the Software is furnished to do
133
+ so, subject to the following conditions:
134
+
135
+ The above copyright notice and this permission notice shall be included in all
136
+ copies or substantial portions of the Software.
137
+
138
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
139
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
140
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
141
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
142
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
143
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
144
+ SOFTWARE.
145
+ </dialog>`
146
+ }
@@ -0,0 +1,453 @@
1
+ import * as semver from 'semver'
2
+ import validate from 'validate-npm-package-name'
3
+ import { URL } from '../../config.ts'
4
+ import type {
5
+ HonoContext,
6
+ PackageSpec,
7
+ PackageManifest,
8
+ ValidationResult,
9
+ } from '../../types.ts'
10
+
11
+ /**
12
+ * Extracts package.json from a tarball buffer
13
+ * @param {Uint8Array} _tarballBuffer - The tarball as a Uint8Array
14
+ * @returns {Promise<PackageManifest | null>} The parsed package.json content
15
+ */
16
+ export async function extractPackageJSON(
17
+ _tarballBuffer: Uint8Array,
18
+ ): Promise<PackageManifest | null> {
19
+ try {
20
+ // This would need to be implemented with a tarball extraction library
21
+ // For now, return null as a placeholder
22
+ return null
23
+ } catch (_error) {
24
+ return null
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Extracts package specification from context
30
+ * @param {HonoContext} c - The Hono context
31
+ * @returns {PackageSpec} Package specification object
32
+ */
33
+ export function packageSpec(c: HonoContext): PackageSpec {
34
+ const { scope, pkg } = c.req.param()
35
+
36
+ if (scope && pkg) {
37
+ // Scoped package
38
+ const name =
39
+ scope.startsWith('@') ? `${scope}/${pkg}` : `@${scope}/${pkg}`
40
+ return { name, scope, pkg }
41
+ } else if (scope) {
42
+ // Unscoped package (scope is actually the package name)
43
+ return { name: scope, pkg: scope }
44
+ }
45
+
46
+ return {}
47
+ }
48
+
49
+ /**
50
+ * Creates a file path for a package tarball
51
+ * @param {object} options - Object with pkg and version
52
+ * @param {string} options.pkg - Package name
53
+ * @param {string} options.version - Package version
54
+ * @returns {string} Tarball file path
55
+ */
56
+ export function createFile({
57
+ pkg,
58
+ version,
59
+ }: {
60
+ pkg: string
61
+ version: string
62
+ }): string {
63
+ try {
64
+ if (!pkg || !version) {
65
+ throw new Error('Missing required parameters')
66
+ }
67
+ // Generate the tarball path similar to npm registry format
68
+ const packageName = pkg.split('/').pop() || pkg
69
+ return `${pkg}/-/${packageName}-${version}.tgz`
70
+ } catch (_err) {
71
+ // Failed to create file path
72
+ throw new Error('Failed to generate tarball path')
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Creates a version specification string
78
+ * @param {string} packageName - The package name
79
+ * @param {string} version - The version
80
+ * @returns {string} Version specification string
81
+ */
82
+ export function createVersionSpec(
83
+ packageName: string,
84
+ version: string,
85
+ ): string {
86
+ return `${packageName}@${version}`
87
+ }
88
+
89
+ /**
90
+ * Creates a full version object with proper manifest structure
91
+ * @param {object} options - Object with pkg, version, and manifest
92
+ * @param {string} options.pkg - Package name
93
+ * @param {string} options.version - Package version
94
+ * @param {any} options.manifest - Package manifest data
95
+ * @returns {any} The manifest with proper name, version, and dist fields
96
+ */
97
+ interface ManifestInput {
98
+ name?: string
99
+ version?: string
100
+ dist?: {
101
+ tarball?: string
102
+ [key: string]: unknown
103
+ }
104
+ [key: string]: unknown
105
+ }
106
+
107
+ export function createVersion({
108
+ pkg,
109
+ version,
110
+ manifest,
111
+ }: {
112
+ pkg: string
113
+ version: string
114
+ manifest: unknown
115
+ }): ManifestInput {
116
+ // If manifest is a string, parse it
117
+ let parsedManifest: ManifestInput
118
+ if (typeof manifest === 'string') {
119
+ try {
120
+ parsedManifest = JSON.parse(manifest) as ManifestInput
121
+ } catch (_e) {
122
+ // If parsing fails, use empty object
123
+ parsedManifest = {}
124
+ }
125
+ } else {
126
+ parsedManifest = manifest as ManifestInput
127
+ }
128
+
129
+ // Create the final manifest with proper structure
130
+ const result: ManifestInput = {
131
+ ...parsedManifest,
132
+ name: pkg,
133
+ version: version,
134
+ dist: {
135
+ ...(parsedManifest.dist ?? {}),
136
+ tarball:
137
+ parsedManifest.dist?.tarball ??
138
+ `https://registry.npmjs.org/${pkg}/-/${pkg.split('/').pop()}-${version}.tgz`,
139
+ },
140
+ }
141
+
142
+ return result
143
+ }
144
+
145
+ interface SlimManifestContext {
146
+ protocol?: string
147
+ host?: string
148
+ upstream?: string
149
+ }
150
+
151
+ interface ParsedManifest {
152
+ name?: string
153
+ version?: string
154
+ description?: string
155
+ keywords?: string[]
156
+ homepage?: string
157
+ bugs?: unknown
158
+ license?: string
159
+ author?: unknown
160
+ contributors?: unknown[]
161
+ funding?: unknown
162
+ files?: string[]
163
+ main?: string
164
+ browser?: unknown
165
+ bin?: Record<string, string>
166
+ man?: unknown
167
+ directories?: unknown
168
+ repository?: unknown
169
+ scripts?: Record<string, string>
170
+ dependencies?: Record<string, string>
171
+ devDependencies?: Record<string, string>
172
+ peerDependencies?: Record<string, string>
173
+ optionalDependencies?: Record<string, string>
174
+ bundledDependencies?: string[]
175
+ peerDependenciesMeta?: Record<string, unknown>
176
+ engines?: Record<string, string>
177
+ os?: string[]
178
+ cpu?: string[]
179
+ types?: string
180
+ typings?: string
181
+ module?: string
182
+ exports?: unknown
183
+ imports?: unknown
184
+ type?: string
185
+ dist?: {
186
+ tarball?: string
187
+ integrity?: string
188
+ shasum?: string
189
+ [key: string]: unknown
190
+ }
191
+ [key: string]: unknown
192
+ }
193
+
194
+ /**
195
+ * Creates a slimmed down version of a package manifest
196
+ * Removes sensitive or unnecessary fields for public consumption
197
+ * @param {unknown} manifest - The full package manifest
198
+ * @param {SlimManifestContext} [context] - Optional context for URL rewriting
199
+ * @returns {ParsedManifest} Slimmed manifest
200
+ */
201
+ export function slimManifest(
202
+ manifest: unknown,
203
+ context?: SlimManifestContext,
204
+ ): ParsedManifest {
205
+ if (!manifest) return {} as ParsedManifest
206
+
207
+ try {
208
+ // Parse manifest if it's a string
209
+ let parsed: ParsedManifest
210
+ if (typeof manifest === 'string') {
211
+ try {
212
+ parsed = JSON.parse(manifest) as ParsedManifest
213
+ } catch (_e) {
214
+ // If parsing fails, return empty manifest
215
+ return {} as ParsedManifest
216
+ }
217
+ } else {
218
+ parsed = manifest as ParsedManifest
219
+ }
220
+
221
+ // Create a new object with only the fields we want to keep
222
+ const slimmed: ParsedManifest = {}
223
+
224
+ // Only add properties that exist
225
+ if (parsed.name !== undefined) slimmed.name = parsed.name
226
+ if (parsed.version !== undefined) slimmed.version = parsed.version
227
+ if (parsed.description !== undefined)
228
+ slimmed.description = parsed.description
229
+ if (parsed.keywords !== undefined)
230
+ slimmed.keywords = parsed.keywords
231
+ if (parsed.homepage !== undefined)
232
+ slimmed.homepage = parsed.homepage
233
+ if (parsed.bugs !== undefined) slimmed.bugs = parsed.bugs
234
+ if (parsed.license !== undefined) slimmed.license = parsed.license
235
+ if (parsed.author !== undefined) slimmed.author = parsed.author
236
+ if (parsed.contributors !== undefined)
237
+ slimmed.contributors = parsed.contributors
238
+ if (parsed.funding !== undefined) slimmed.funding = parsed.funding
239
+ if (parsed.files !== undefined) slimmed.files = parsed.files
240
+ if (parsed.main !== undefined) slimmed.main = parsed.main
241
+ if (parsed.browser !== undefined) slimmed.browser = parsed.browser
242
+ if (parsed.bin !== undefined) slimmed.bin = parsed.bin
243
+ if (parsed.man !== undefined) slimmed.man = parsed.man
244
+ if (parsed.directories !== undefined)
245
+ slimmed.directories = parsed.directories
246
+ if (parsed.repository !== undefined)
247
+ slimmed.repository = parsed.repository
248
+ if (parsed.scripts !== undefined) slimmed.scripts = parsed.scripts
249
+ // Always include dependencies as empty objects if not present
250
+ slimmed.dependencies = parsed.dependencies ?? {}
251
+ slimmed.devDependencies = parsed.devDependencies ?? {}
252
+ if (parsed.peerDependencies !== undefined)
253
+ slimmed.peerDependencies = parsed.peerDependencies
254
+ if (parsed.optionalDependencies !== undefined)
255
+ slimmed.optionalDependencies = parsed.optionalDependencies
256
+ if (parsed.bundledDependencies !== undefined)
257
+ slimmed.bundledDependencies = parsed.bundledDependencies
258
+ if (parsed.peerDependenciesMeta !== undefined)
259
+ slimmed.peerDependenciesMeta = parsed.peerDependenciesMeta
260
+ if (parsed.engines !== undefined) slimmed.engines = parsed.engines
261
+ if (parsed.os !== undefined) slimmed.os = parsed.os
262
+ if (parsed.cpu !== undefined) slimmed.cpu = parsed.cpu
263
+ if (parsed.types !== undefined) slimmed.types = parsed.types
264
+ if (parsed.typings !== undefined) slimmed.typings = parsed.typings
265
+ if (parsed.module !== undefined) slimmed.module = parsed.module
266
+ if (parsed.exports !== undefined) slimmed.exports = parsed.exports
267
+ if (parsed.imports !== undefined) slimmed.imports = parsed.imports
268
+ if (parsed.type !== undefined) slimmed.type = parsed.type
269
+
270
+ // Handle dist object specially - always include with defaults
271
+ slimmed.dist = {
272
+ ...(parsed.dist ?? {}),
273
+ tarball: rewriteTarballUrlIfNeeded(
274
+ parsed.dist?.tarball ?? '',
275
+ parsed.name ?? '',
276
+ parsed.version ?? '',
277
+ context,
278
+ ),
279
+ integrity: parsed.dist?.integrity ?? '',
280
+ shasum: parsed.dist?.shasum ?? '',
281
+ }
282
+
283
+ return slimmed
284
+ } catch (_err) {
285
+ // Failed to slim manifest
286
+ return {} as ParsedManifest // Return empty manifest if slimming fails
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Validates a package name using npm validation rules
292
+ * @param {string} packageName - The package name to validate
293
+ * @returns {ValidationResult} Validation result
294
+ */
295
+ export function validatePackageName(
296
+ packageName: string,
297
+ ): ValidationResult {
298
+ const result = validate(packageName)
299
+ return {
300
+ valid: result.validForNewPackages || result.validForOldPackages,
301
+
302
+ errors: result.errors ?? [],
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Validates a semver version string
308
+ * @param {string} version - The version to validate
309
+ * @returns {boolean} True if valid semver
310
+ */
311
+ export function validateVersion(version: string): boolean {
312
+ return semver.valid(version) !== null
313
+ }
314
+
315
+ /**
316
+ * Parses a version range and returns the best matching version from a list
317
+ * @param {string} range - The semver range
318
+ * @param {string[]} versions - Available versions
319
+ * @returns {string | null} Best matching version or null
320
+ */
321
+ export function getBestMatchingVersion(
322
+ range: string,
323
+ versions: string[],
324
+ ): string | null {
325
+ try {
326
+ return semver.maxSatisfying(versions, range)
327
+ } catch (_error) {
328
+ // Invalid semver range
329
+ return null
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Extracts the package name from a scoped or unscoped package identifier
335
+ * @param {string} identifier - Package identifier (e.g., "@scope/package" or "package")
336
+ * @returns {object} Package name components
337
+ */
338
+ export function parsePackageIdentifier(identifier: string): {
339
+ scope?: string
340
+ name: string
341
+ fullName: string
342
+ } {
343
+ if (identifier.startsWith('@')) {
344
+ const parts = identifier.split('/')
345
+ if (parts.length >= 2) {
346
+ const scope = parts[0]
347
+ return {
348
+ ...(scope && { scope }),
349
+ name: parts.slice(1).join('/'),
350
+ fullName: identifier,
351
+ }
352
+ }
353
+ }
354
+
355
+ return {
356
+ name: identifier,
357
+ fullName: identifier,
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Generates a tarball filename for a package version
363
+ * @param {string} packageName - The package name
364
+ * @param {string} version - The package version
365
+ * @returns {string} Tarball filename
366
+ */
367
+ export function generateTarballFilename(
368
+ packageName: string,
369
+ version: string,
370
+ ): string {
371
+ const name = packageName.split('/').pop() || packageName
372
+ return `${name}-${version}.tgz`
373
+ }
374
+
375
+ /**
376
+ * Rewrites tarball URLs if needed for local registry
377
+ * @param {string} _originalUrl - The original tarball URL
378
+ * @param {string} packageName - The package name
379
+ * @param {string} version - The package version
380
+ * @param {any} [context] - Optional context for URL rewriting
381
+ * @returns {string} Rewritten or original URL
382
+ */
383
+ function rewriteTarballUrlIfNeeded(
384
+ _originalUrl: string,
385
+ packageName: string,
386
+ version: string,
387
+ context?: SlimManifestContext,
388
+ ): string {
389
+ try {
390
+ // Check if we should rewrite URLs for this upstream
391
+ const upstream = context?.upstream
392
+ const protocol = context?.protocol
393
+ const host = context?.host
394
+
395
+ if (upstream && protocol && host) {
396
+ // Rewrite to our local registry format for upstream packages
397
+ return `${protocol}://${host}/${upstream}/${packageName}/-/${generateTarballFilename(packageName, version)}`
398
+ }
399
+
400
+ // Default to local registry format using DOMAIN
401
+ return `${URL}/${createFile({ pkg: packageName, version })}`
402
+ } catch (_err) {
403
+ // Fallback to local registry format
404
+ return `${URL}/${createFile({ pkg: packageName, version })}`
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Checks if a version satisfies a semver range
410
+ * @param {string} version - The version to check
411
+ * @param {string} range - The semver range
412
+ * @returns {boolean} True if version satisfies range
413
+ */
414
+ export function satisfiesRange(
415
+ version: string,
416
+ range: string,
417
+ ): boolean {
418
+ try {
419
+ return semver.satisfies(version, range)
420
+ } catch (_error) {
421
+ return false
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Sorts versions in descending order
427
+ * @param {string[]} versions - Array of version strings
428
+ * @returns {string[]} Sorted versions
429
+ */
430
+ export function sortVersionsDescending(versions: string[]): string[] {
431
+ return versions.sort((a, b) => semver.rcompare(a, b))
432
+ }
433
+
434
+ /**
435
+ * Gets the latest version from an array of versions
436
+ * @param {string[]} versions - Array of version strings
437
+ * @returns {string | null} Latest version or null if none
438
+ */
439
+ export function getLatestVersion(versions: string[]): string | null {
440
+ if (versions.length === 0) return null
441
+ const sorted = sortVersionsDescending(versions)
442
+ return sorted[0] || null
443
+ }
444
+
445
+ /**
446
+ * Validates a tarball buffer
447
+ * @param {Uint8Array} _tarballBuffer - The tarball buffer to validate
448
+ * @returns {boolean} True if valid tarball
449
+ */
450
+ export function validateTarball(_tarballBuffer: Uint8Array): boolean {
451
+ // Basic validation - could be extended
452
+ return true
453
+ }