@package-broker/core 0.7.1 → 0.8.0

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 (197) hide show
  1. package/dist/factory.d.ts +2 -2
  2. package/dist/factory.d.ts.map +1 -1
  3. package/dist/factory.js +186 -66
  4. package/dist/factory.js.map +1 -1
  5. package/dist/modules/admin/admin.handlers.d.ts +25 -0
  6. package/dist/modules/admin/admin.handlers.d.ts.map +1 -0
  7. package/dist/modules/admin/admin.handlers.js +97 -0
  8. package/dist/modules/admin/admin.handlers.js.map +1 -0
  9. package/dist/modules/admin/admin.routes.d.ts +147 -0
  10. package/dist/modules/admin/admin.routes.d.ts.map +1 -0
  11. package/dist/modules/admin/admin.routes.js +130 -0
  12. package/dist/modules/admin/admin.routes.js.map +1 -0
  13. package/dist/modules/admin/index.d.ts +9 -0
  14. package/dist/modules/admin/index.d.ts.map +1 -0
  15. package/dist/modules/admin/index.js +19 -0
  16. package/dist/modules/admin/index.js.map +1 -0
  17. package/dist/modules/artifacts/artifacts.handlers.d.ts +18 -0
  18. package/dist/modules/artifacts/artifacts.handlers.d.ts.map +1 -0
  19. package/dist/modules/artifacts/artifacts.handlers.js +47 -0
  20. package/dist/modules/artifacts/artifacts.handlers.js.map +1 -0
  21. package/dist/modules/artifacts/artifacts.routes.d.ts +80 -0
  22. package/dist/modules/artifacts/artifacts.routes.d.ts.map +1 -0
  23. package/dist/modules/artifacts/artifacts.routes.js +79 -0
  24. package/dist/modules/artifacts/artifacts.routes.js.map +1 -0
  25. package/dist/modules/artifacts/index.d.ts +8 -0
  26. package/dist/modules/artifacts/index.d.ts.map +1 -0
  27. package/dist/modules/artifacts/index.js +14 -0
  28. package/dist/modules/artifacts/index.js.map +1 -0
  29. package/dist/modules/auth/auth.handlers.d.ts +51 -0
  30. package/dist/modules/auth/auth.handlers.d.ts.map +1 -0
  31. package/dist/modules/auth/auth.handlers.js +278 -0
  32. package/dist/modules/auth/auth.handlers.js.map +1 -0
  33. package/dist/modules/auth/auth.routes.d.ts +187 -0
  34. package/dist/modules/auth/auth.routes.d.ts.map +1 -0
  35. package/dist/modules/auth/auth.routes.js +136 -0
  36. package/dist/modules/auth/auth.routes.js.map +1 -0
  37. package/dist/modules/auth/index.d.ts +11 -0
  38. package/dist/modules/auth/index.d.ts.map +1 -0
  39. package/dist/modules/auth/index.js +34 -0
  40. package/dist/modules/auth/index.js.map +1 -0
  41. package/dist/modules/composer/composer.handlers.d.ts +3 -0
  42. package/dist/modules/composer/composer.handlers.d.ts.map +1 -0
  43. package/dist/modules/composer/composer.handlers.js +12 -0
  44. package/dist/modules/composer/composer.handlers.js.map +1 -0
  45. package/dist/modules/composer/index.d.ts +11 -0
  46. package/dist/modules/composer/index.d.ts.map +1 -0
  47. package/dist/modules/composer/index.js +40 -0
  48. package/dist/modules/composer/index.js.map +1 -0
  49. package/dist/modules/packages/index.d.ts +8 -0
  50. package/dist/modules/packages/index.d.ts.map +1 -0
  51. package/dist/modules/packages/index.js +19 -0
  52. package/dist/modules/packages/index.js.map +1 -0
  53. package/dist/modules/packages/packages.handlers.d.ts +50 -0
  54. package/dist/modules/packages/packages.handlers.d.ts.map +1 -0
  55. package/dist/modules/packages/packages.handlers.js +670 -0
  56. package/dist/modules/packages/packages.handlers.js.map +1 -0
  57. package/dist/modules/packages/packages.routes.d.ts +172 -0
  58. package/dist/modules/packages/packages.routes.d.ts.map +1 -0
  59. package/dist/modules/packages/packages.routes.js +160 -0
  60. package/dist/modules/packages/packages.routes.js.map +1 -0
  61. package/dist/modules/repositories/index.d.ts +8 -0
  62. package/dist/modules/repositories/index.d.ts.map +1 -0
  63. package/dist/modules/repositories/index.js +19 -0
  64. package/dist/modules/repositories/index.js.map +1 -0
  65. package/dist/modules/repositories/repositories.handlers.d.ts +29 -0
  66. package/dist/modules/repositories/repositories.handlers.d.ts.map +1 -0
  67. package/dist/modules/repositories/repositories.handlers.js +261 -0
  68. package/dist/modules/repositories/repositories.handlers.js.map +1 -0
  69. package/dist/modules/repositories/repositories.routes.d.ts +451 -0
  70. package/dist/modules/repositories/repositories.routes.d.ts.map +1 -0
  71. package/dist/modules/repositories/repositories.routes.js +264 -0
  72. package/dist/modules/repositories/repositories.routes.js.map +1 -0
  73. package/dist/modules/system/index.d.ts +8 -0
  74. package/dist/modules/system/index.d.ts.map +1 -0
  75. package/dist/modules/system/index.js +13 -0
  76. package/dist/modules/system/index.js.map +1 -0
  77. package/dist/modules/system/system.handlers.d.ts +9 -0
  78. package/dist/modules/system/system.handlers.d.ts.map +1 -0
  79. package/dist/modules/system/system.handlers.js +22 -0
  80. package/dist/modules/system/system.handlers.js.map +1 -0
  81. package/dist/modules/system/system.routes.d.ts +24 -0
  82. package/dist/modules/system/system.routes.d.ts.map +1 -0
  83. package/dist/modules/system/system.routes.js +27 -0
  84. package/dist/modules/system/system.routes.js.map +1 -0
  85. package/dist/modules/tokens/index.d.ts +8 -0
  86. package/dist/modules/tokens/index.d.ts.map +1 -0
  87. package/dist/modules/tokens/index.js +16 -0
  88. package/dist/modules/tokens/index.js.map +1 -0
  89. package/dist/modules/tokens/tokens.handlers.d.ts +22 -0
  90. package/dist/modules/tokens/tokens.handlers.d.ts.map +1 -0
  91. package/dist/modules/tokens/tokens.handlers.js +150 -0
  92. package/dist/modules/tokens/tokens.handlers.js.map +1 -0
  93. package/dist/modules/tokens/tokens.routes.d.ts +202 -0
  94. package/dist/modules/tokens/tokens.routes.d.ts.map +1 -0
  95. package/dist/modules/tokens/tokens.routes.js +143 -0
  96. package/dist/modules/tokens/tokens.routes.js.map +1 -0
  97. package/dist/modules/users/index.d.ts +8 -0
  98. package/dist/modules/users/index.d.ts.map +1 -0
  99. package/dist/modules/users/index.js +15 -0
  100. package/dist/modules/users/index.js.map +1 -0
  101. package/dist/modules/users/users.handlers.d.ts +6 -0
  102. package/dist/modules/users/users.handlers.d.ts.map +1 -0
  103. package/dist/modules/users/users.handlers.js +120 -0
  104. package/dist/modules/users/users.handlers.js.map +1 -0
  105. package/dist/modules/users/users.routes.d.ts +190 -0
  106. package/dist/modules/users/users.routes.d.ts.map +1 -0
  107. package/dist/modules/users/users.routes.js +132 -0
  108. package/dist/modules/users/users.routes.js.map +1 -0
  109. package/dist/routes/api/artifacts.d.ts +5 -3
  110. package/dist/routes/api/artifacts.d.ts.map +1 -1
  111. package/dist/routes/api/artifacts.js +2 -2
  112. package/dist/routes/api/artifacts.js.map +1 -1
  113. package/dist/routes/api/auth.d.ts +5 -3
  114. package/dist/routes/api/auth.d.ts.map +1 -1
  115. package/dist/routes/api/auth.js +2 -12
  116. package/dist/routes/api/auth.js.map +1 -1
  117. package/dist/routes/api/index.d.ts +1 -0
  118. package/dist/routes/api/index.d.ts.map +1 -1
  119. package/dist/routes/api/index.js +1 -0
  120. package/dist/routes/api/index.js.map +1 -1
  121. package/dist/routes/api/openapi/artifacts.d.ts +80 -0
  122. package/dist/routes/api/openapi/artifacts.d.ts.map +1 -0
  123. package/dist/routes/api/openapi/artifacts.js +73 -0
  124. package/dist/routes/api/openapi/artifacts.js.map +1 -0
  125. package/dist/routes/api/openapi/auth.d.ts +187 -0
  126. package/dist/routes/api/openapi/auth.d.ts.map +1 -0
  127. package/dist/routes/api/openapi/auth.js +135 -0
  128. package/dist/routes/api/openapi/auth.js.map +1 -0
  129. package/dist/routes/api/openapi/health.d.ts +24 -0
  130. package/dist/routes/api/openapi/health.d.ts.map +1 -0
  131. package/dist/routes/api/openapi/health.js +25 -0
  132. package/dist/routes/api/openapi/health.js.map +1 -0
  133. package/dist/routes/api/openapi/index.d.ts +10 -0
  134. package/dist/routes/api/openapi/index.d.ts.map +1 -0
  135. package/dist/routes/api/openapi/index.js +16 -0
  136. package/dist/routes/api/openapi/index.js.map +1 -0
  137. package/dist/routes/api/openapi/packages.d.ts +172 -0
  138. package/dist/routes/api/openapi/packages.d.ts.map +1 -0
  139. package/dist/routes/api/openapi/packages.js +126 -0
  140. package/dist/routes/api/openapi/packages.js.map +1 -0
  141. package/dist/routes/api/openapi/repositories.d.ts +451 -0
  142. package/dist/routes/api/openapi/repositories.d.ts.map +1 -0
  143. package/dist/routes/api/openapi/repositories.js +238 -0
  144. package/dist/routes/api/openapi/repositories.js.map +1 -0
  145. package/dist/routes/api/openapi/settings.d.ts +90 -0
  146. package/dist/routes/api/openapi/settings.d.ts.map +1 -0
  147. package/dist/routes/api/openapi/settings.js +72 -0
  148. package/dist/routes/api/openapi/settings.js.map +1 -0
  149. package/dist/routes/api/openapi/stats.d.ts +59 -0
  150. package/dist/routes/api/openapi/stats.d.ts.map +1 -0
  151. package/dist/routes/api/openapi/stats.js +53 -0
  152. package/dist/routes/api/openapi/stats.js.map +1 -0
  153. package/dist/routes/api/openapi/tokens.d.ts +202 -0
  154. package/dist/routes/api/openapi/tokens.d.ts.map +1 -0
  155. package/dist/routes/api/openapi/tokens.js +132 -0
  156. package/dist/routes/api/openapi/tokens.js.map +1 -0
  157. package/dist/routes/api/openapi/users.d.ts +190 -0
  158. package/dist/routes/api/openapi/users.d.ts.map +1 -0
  159. package/dist/routes/api/openapi/users.js +126 -0
  160. package/dist/routes/api/openapi/users.js.map +1 -0
  161. package/dist/routes/api/packages.d.ts +7 -4
  162. package/dist/routes/api/packages.d.ts.map +1 -1
  163. package/dist/routes/api/packages.js +6 -7
  164. package/dist/routes/api/packages.js.map +1 -1
  165. package/dist/routes/api/repositories.d.ts +8 -6
  166. package/dist/routes/api/repositories.d.ts.map +1 -1
  167. package/dist/routes/api/repositories.js +26 -22
  168. package/dist/routes/api/repositories.js.map +1 -1
  169. package/dist/routes/api/settings.d.ts +4 -3
  170. package/dist/routes/api/settings.d.ts.map +1 -1
  171. package/dist/routes/api/settings.js +1 -1
  172. package/dist/routes/api/settings.js.map +1 -1
  173. package/dist/routes/api/stats.d.ts +3 -3
  174. package/dist/routes/api/stats.d.ts.map +1 -1
  175. package/dist/routes/api/stats.js +1 -2
  176. package/dist/routes/api/stats.js.map +1 -1
  177. package/dist/routes/api/tokens.d.ts +6 -5
  178. package/dist/routes/api/tokens.d.ts.map +1 -1
  179. package/dist/routes/api/tokens.js +4 -7
  180. package/dist/routes/api/tokens.js.map +1 -1
  181. package/dist/routes/api/types.d.ts +17 -0
  182. package/dist/routes/api/types.d.ts.map +1 -0
  183. package/dist/routes/api/types.js +7 -0
  184. package/dist/routes/api/types.js.map +1 -0
  185. package/dist/routes/api/users.d.ts +5 -4
  186. package/dist/routes/api/users.d.ts.map +1 -1
  187. package/dist/routes/api/users.js +2 -12
  188. package/dist/routes/api/users.js.map +1 -1
  189. package/dist/routes/composer.js +1 -1
  190. package/dist/routes/composer.js.map +1 -1
  191. package/dist/routes/health.d.ts +3 -1
  192. package/dist/routes/health.d.ts.map +1 -1
  193. package/dist/routes/health.js.map +1 -1
  194. package/dist/utils/encryption.d.ts.map +1 -1
  195. package/dist/utils/encryption.js +8 -0
  196. package/dist/utils/encryption.js.map +1 -1
  197. package/package.json +3 -2
@@ -0,0 +1,670 @@
1
+ // Packages API routes
2
+ import { packages, artifacts, repositories } from '../../db/schema';
3
+ import { eq, like, and } from 'drizzle-orm';
4
+ import { unzipSync, strFromU8 } from 'fflate';
5
+ import { buildStorageKey, buildReadmeStorageKey, buildChangelogStorageKey } from '../../storage/driver';
6
+ import { downloadFromSource } from '../../utils/download';
7
+ import { decryptCredentials } from '../../utils/encryption';
8
+ import { nanoid } from 'nanoid';
9
+ import { COMPOSER_USER_AGENT } from '@package-broker/shared';
10
+ import { isPackagistMirroringEnabled } from '../admin';
11
+ /**
12
+ * GET /api/packages
13
+ * List all packages (with optional search)
14
+ */
15
+ export async function listPackages(c) {
16
+ const db = c.get('database');
17
+ const query = c.req.valid('query');
18
+ const search = query?.search;
19
+ let allPackages;
20
+ if (search) {
21
+ allPackages = await db
22
+ .select()
23
+ .from(packages)
24
+ .where(like(packages.name, `%${search}%`))
25
+ .orderBy(packages.name);
26
+ }
27
+ else {
28
+ allPackages = await db.select().from(packages).orderBy(packages.name);
29
+ }
30
+ return c.json(allPackages);
31
+ }
32
+ /**
33
+ * GET /api/packages/:name
34
+ * Get a single package with all versions
35
+ */
36
+ export async function getPackage(c) {
37
+ const { name: nameParam } = c.req.valid('param');
38
+ // Decode URL-encoded package name (handles slashes like amasty/cron-schedule-list)
39
+ const name = decodeURIComponent(nameParam);
40
+ const db = c.get('database');
41
+ const packageVersions = await db
42
+ .select()
43
+ .from(packages)
44
+ .where(eq(packages.name, name))
45
+ .orderBy(packages.released_at);
46
+ if (packageVersions.length === 0) {
47
+ return c.json({ error: 'Not Found', message: 'Package not found' }, 404);
48
+ }
49
+ return c.json({
50
+ name,
51
+ versions: packageVersions,
52
+ });
53
+ }
54
+ /**
55
+ * Extract README.md or README.mdown from ZIP archive
56
+ */
57
+ function extractReadme(zipData) {
58
+ try {
59
+ const files = unzipSync(zipData);
60
+ // Look for README in common locations (case-insensitive)
61
+ // Prefer .md over .mdown if both exist
62
+ const readmeNames = [
63
+ 'README.md', 'readme.md', 'README.MD', 'Readme.md',
64
+ 'README.mdown', 'readme.mdown', 'README.MDOWN', 'Readme.mdown'
65
+ ];
66
+ // First pass: look for .md files
67
+ for (const [path, content] of Object.entries(files)) {
68
+ const filename = path.split('/').pop() || '';
69
+ if (readmeNames.slice(0, 4).includes(filename)) {
70
+ return strFromU8(content);
71
+ }
72
+ }
73
+ // Second pass: look for .mdown files
74
+ for (const [path, content] of Object.entries(files)) {
75
+ const filename = path.split('/').pop() || '';
76
+ if (readmeNames.slice(4).includes(filename)) {
77
+ return strFromU8(content);
78
+ }
79
+ }
80
+ return null;
81
+ }
82
+ catch (error) {
83
+ console.error('Error extracting README from ZIP:', error);
84
+ return null;
85
+ }
86
+ }
87
+ /**
88
+ * Extract CHANGELOG.md or CHANGELOG.mdown from ZIP archive
89
+ */
90
+ function extractChangelog(zipData) {
91
+ try {
92
+ const files = unzipSync(zipData);
93
+ // Look for CHANGELOG in common locations (case-insensitive)
94
+ // Prefer .md over .mdown if both exist
95
+ const changelogNames = [
96
+ 'CHANGELOG.md', 'changelog.md', 'CHANGELOG.MD', 'Changelog.md',
97
+ 'CHANGELOG.mdown', 'changelog.mdown', 'CHANGELOG.MDOWN', 'Changelog.mdown'
98
+ ];
99
+ // First pass: look for .md files
100
+ for (const [path, content] of Object.entries(files)) {
101
+ const filename = path.split('/').pop() || '';
102
+ if (changelogNames.slice(0, 4).includes(filename)) {
103
+ return strFromU8(content);
104
+ }
105
+ }
106
+ // Second pass: look for .mdown files
107
+ for (const [path, content] of Object.entries(files)) {
108
+ const filename = path.split('/').pop() || '';
109
+ if (changelogNames.slice(4).includes(filename)) {
110
+ return strFromU8(content);
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+ catch (error) {
116
+ console.error('Error extracting CHANGELOG from ZIP:', error);
117
+ return null;
118
+ }
119
+ }
120
+ /**
121
+ * GET /api/packages/:name/:version/readme
122
+ * Get README.md content for a specific package version
123
+ * Uses R2/S3 storage instead of KV for better scalability
124
+ */
125
+ export async function getPackageReadme(c) {
126
+ const { name: nameParam, version } = c.req.valid('param');
127
+ // Decode URL-encoded package name (handles slashes like amasty/cron-schedule-list)
128
+ const name = decodeURIComponent(nameParam);
129
+ if (!name || !version) {
130
+ return c.json({ error: 'Bad Request', message: 'Missing package name or version' }, 400);
131
+ }
132
+ // 1. Get package from database to find repo_id
133
+ const db = c.get('database');
134
+ const [pkg] = await db
135
+ .select()
136
+ .from(packages)
137
+ .where(and(eq(packages.name, name), eq(packages.version, version)))
138
+ .limit(1);
139
+ if (!pkg) {
140
+ return c.json({ error: 'Not Found', message: 'Package version not found' }, 404);
141
+ }
142
+ // 2. Determine storage type (public for Packagist, private for others)
143
+ const storageType = pkg.repo_id === 'packagist' ? 'public' : 'private';
144
+ const readmeStorageKey = buildReadmeStorageKey(storageType, pkg.repo_id, name, version);
145
+ const storage = c.var.storage;
146
+ // 3. Check if README already exists in R2/S3 storage
147
+ const existingReadme = await storage.get(readmeStorageKey);
148
+ if (existingReadme) {
149
+ // Read the stream to check if it's a "NOT_FOUND" marker
150
+ const chunks = [];
151
+ const reader = existingReadme.getReader();
152
+ while (true) {
153
+ const { done, value } = await reader.read();
154
+ if (done)
155
+ break;
156
+ if (value) {
157
+ chunks.push(value);
158
+ }
159
+ }
160
+ const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
161
+ const content = new Uint8Array(totalSize);
162
+ let offset = 0;
163
+ for (const chunk of chunks) {
164
+ content.set(chunk, offset);
165
+ offset += chunk.length;
166
+ }
167
+ const textContent = new TextDecoder().decode(content);
168
+ // If it's a NOT_FOUND marker, return 404
169
+ if (textContent === 'NOT_FOUND') {
170
+ return c.json({
171
+ error: 'Not Found',
172
+ message: 'No README file exists in this package version'
173
+ }, 404);
174
+ }
175
+ // Return cached README with aggressive CDN caching
176
+ return new Response(textContent, {
177
+ headers: {
178
+ 'Content-Type': 'text/markdown; charset=utf-8',
179
+ 'Cache-Control': 'public, max-age=31536000, immutable',
180
+ 'X-README-Source': 'storage',
181
+ },
182
+ });
183
+ }
184
+ // 4. README not in storage - need to extract from ZIP
185
+ // Get artifact to find ZIP storage key
186
+ let [artifact] = await db
187
+ .select()
188
+ .from(artifacts)
189
+ .where(and(eq(artifacts.repo_id, pkg.repo_id), eq(artifacts.package_name, name), eq(artifacts.version, version)))
190
+ .limit(1);
191
+ let zipData = null;
192
+ // 5. If artifact doesn't exist, try on-demand download
193
+ if (!artifact) {
194
+ // Check if we can download from source
195
+ if (!pkg.source_dist_url) {
196
+ return c.json({ error: 'Not Found', message: 'Artifact not found and source URL unavailable. Package may need to be downloaded first.' }, 404);
197
+ }
198
+ // Validate it's actually a URL
199
+ if (!pkg.source_dist_url.startsWith('http://') && !pkg.source_dist_url.startsWith('https://')) {
200
+ return c.json({ error: 'Not Found', message: 'Invalid source URL. Please re-sync the repository to update package metadata.' }, 404);
201
+ }
202
+ // Get repository for credentials
203
+ const [repo] = await db
204
+ .select()
205
+ .from(repositories)
206
+ .where(eq(repositories.id, pkg.repo_id))
207
+ .limit(1);
208
+ if (!repo) {
209
+ return c.json({ error: 'Not Found', message: 'Repository not found' }, 404);
210
+ }
211
+ try {
212
+ // Decrypt credentials
213
+ const credentialsJson = await decryptCredentials(repo.auth_credentials, c.env.ENCRYPTION_KEY);
214
+ const credentials = JSON.parse(credentialsJson);
215
+ // Download from source with authentication
216
+ const sourceResponse = await downloadFromSource(pkg.source_dist_url, repo.credential_type, credentials);
217
+ // Read the response body
218
+ const sourceStream = sourceResponse.body;
219
+ if (!sourceStream) {
220
+ throw new Error('Source response has no body');
221
+ }
222
+ // Read all chunks into memory
223
+ const chunks = [];
224
+ const reader = sourceStream.getReader();
225
+ let totalSize = 0;
226
+ while (true) {
227
+ const { done, value } = await reader.read();
228
+ if (done)
229
+ break;
230
+ if (value) {
231
+ chunks.push(value);
232
+ totalSize += value.length;
233
+ }
234
+ }
235
+ // Combine chunks into a single Uint8Array
236
+ zipData = new Uint8Array(totalSize);
237
+ let offset = 0;
238
+ for (const chunk of chunks) {
239
+ zipData.set(chunk, offset);
240
+ offset += chunk.length;
241
+ }
242
+ // Store artifact in storage
243
+ const storageType = pkg.repo_id === 'packagist' ? 'public' : 'private';
244
+ const storageKey = buildStorageKey(storageType, pkg.repo_id, name, version);
245
+ // Convert to ArrayBuffer (not SharedArrayBuffer) for storage
246
+ const arrayBuffer = zipData.buffer.slice(zipData.byteOffset, zipData.byteOffset + zipData.byteLength);
247
+ try {
248
+ await storage.put(storageKey, arrayBuffer);
249
+ console.log(`Successfully stored artifact for README extraction: ${storageKey} (${totalSize} bytes)`);
250
+ }
251
+ catch (err) {
252
+ console.error(`Error storing artifact ${storageKey}:`, err);
253
+ // Continue - we can still extract README from zipData
254
+ }
255
+ // Create artifact record
256
+ const artifactId = nanoid();
257
+ const now = Math.floor(Date.now() / 1000);
258
+ try {
259
+ await db.insert(artifacts).values({
260
+ id: artifactId,
261
+ repo_id: pkg.repo_id,
262
+ package_name: name,
263
+ version: version,
264
+ file_key: storageKey,
265
+ size: totalSize,
266
+ download_count: 0,
267
+ created_at: now,
268
+ });
269
+ artifact = {
270
+ id: artifactId,
271
+ repo_id: pkg.repo_id,
272
+ package_name: name,
273
+ version: version,
274
+ file_key: storageKey,
275
+ size: totalSize,
276
+ download_count: 0,
277
+ created_at: now,
278
+ last_downloaded_at: null,
279
+ };
280
+ }
281
+ catch (err) {
282
+ console.error(`Error creating artifact record:`, err);
283
+ // Continue - we can still extract README from zipData
284
+ }
285
+ }
286
+ catch (error) {
287
+ console.error(`Error downloading artifact from source:`, error);
288
+ return c.json({
289
+ error: 'Internal Server Error',
290
+ message: error instanceof Error ? error.message : 'Failed to download artifact',
291
+ }, 500);
292
+ }
293
+ }
294
+ // 6. Get ZIP from storage if we don't already have it in memory
295
+ if (!zipData) {
296
+ if (!artifact) {
297
+ return c.json({ error: 'Not Found', message: 'Artifact not found' }, 404);
298
+ }
299
+ const zipStream = await storage.get(artifact.file_key);
300
+ if (!zipStream) {
301
+ return c.json({ error: 'Not Found', message: 'Artifact file not found in storage' }, 404);
302
+ }
303
+ // Read ZIP into memory
304
+ const zipChunks = [];
305
+ const zipReader = zipStream.getReader();
306
+ while (true) {
307
+ const { done, value } = await zipReader.read();
308
+ if (done)
309
+ break;
310
+ if (value) {
311
+ zipChunks.push(value);
312
+ }
313
+ }
314
+ // Combine chunks
315
+ const totalSize = zipChunks.reduce((sum, chunk) => sum + chunk.length, 0);
316
+ zipData = new Uint8Array(totalSize);
317
+ let offset = 0;
318
+ for (const chunk of zipChunks) {
319
+ zipData.set(chunk, offset);
320
+ offset += chunk.length;
321
+ }
322
+ }
323
+ // 7. Extract README from ZIP
324
+ const readmeContent = extractReadme(zipData);
325
+ if (!readmeContent) {
326
+ // Store NOT_FOUND marker to avoid repeated extraction attempts
327
+ const notFoundMarker = new TextEncoder().encode('NOT_FOUND');
328
+ await storage.put(readmeStorageKey, notFoundMarker).catch((err) => {
329
+ console.error(`Error storing NOT_FOUND marker for ${readmeStorageKey}:`, err);
330
+ });
331
+ return c.json({
332
+ error: 'Not Found',
333
+ message: 'No README file exists in this package version'
334
+ }, 404);
335
+ }
336
+ // 8. Store README in R2/S3 for future requests
337
+ const readmeBytes = new TextEncoder().encode(readmeContent);
338
+ await storage.put(readmeStorageKey, readmeBytes).catch((err) => {
339
+ console.error(`Error storing README for ${readmeStorageKey}:`, err);
340
+ // Continue even if storage fails - we'll still return the content
341
+ });
342
+ // 9. Return with aggressive CDN caching headers
343
+ return new Response(readmeContent, {
344
+ headers: {
345
+ 'Content-Type': 'text/markdown; charset=utf-8',
346
+ 'Cache-Control': 'public, max-age=31536000, immutable',
347
+ 'X-README-Source': 'extracted',
348
+ },
349
+ });
350
+ }
351
+ /**
352
+ * GET /api/packages/:name/:version/changelog
353
+ * Get CHANGELOG.md content for a specific package version
354
+ * Uses R2/S3 storage instead of KV for better scalability
355
+ */
356
+ export async function getPackageChangelog(c) {
357
+ const { name: nameParam, version } = c.req.valid('param');
358
+ // Decode URL-encoded package name (handles slashes like amasty/cron-schedule-list)
359
+ const name = decodeURIComponent(nameParam);
360
+ if (!name || !version) {
361
+ return c.json({ error: 'Bad Request', message: 'Missing package name or version' }, 400);
362
+ }
363
+ // 1. Get package from database to find repo_id
364
+ const db = c.get('database');
365
+ const [pkg] = await db
366
+ .select()
367
+ .from(packages)
368
+ .where(and(eq(packages.name, name), eq(packages.version, version)))
369
+ .limit(1);
370
+ if (!pkg) {
371
+ return c.json({ error: 'Not Found', message: 'Package version not found' }, 404);
372
+ }
373
+ // 2. Determine storage type (public for Packagist, private for others)
374
+ const storageType = pkg.repo_id === 'packagist' ? 'public' : 'private';
375
+ const changelogStorageKey = buildChangelogStorageKey(storageType, pkg.repo_id, name, version);
376
+ const storage = c.var.storage;
377
+ // 3. Check if CHANGELOG already exists in R2/S3 storage
378
+ const existingChangelog = await storage.get(changelogStorageKey);
379
+ if (existingChangelog) {
380
+ // Read the stream to check if it's a "NOT_FOUND" marker
381
+ const chunks = [];
382
+ const reader = existingChangelog.getReader();
383
+ while (true) {
384
+ const { done, value } = await reader.read();
385
+ if (done)
386
+ break;
387
+ if (value) {
388
+ chunks.push(value);
389
+ }
390
+ }
391
+ const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
392
+ const content = new Uint8Array(totalSize);
393
+ let offset = 0;
394
+ for (const chunk of chunks) {
395
+ content.set(chunk, offset);
396
+ offset += chunk.length;
397
+ }
398
+ const textContent = new TextDecoder().decode(content);
399
+ // If it's a NOT_FOUND marker, return 404
400
+ if (textContent === 'NOT_FOUND') {
401
+ return c.json({
402
+ error: 'Not Found',
403
+ message: 'No CHANGELOG file exists in this package version'
404
+ }, 404);
405
+ }
406
+ // Return cached CHANGELOG with aggressive CDN caching
407
+ return new Response(textContent, {
408
+ headers: {
409
+ 'Content-Type': 'text/markdown; charset=utf-8',
410
+ 'Cache-Control': 'public, max-age=31536000, immutable',
411
+ 'X-CHANGELOG-Source': 'storage',
412
+ },
413
+ });
414
+ }
415
+ // 4. CHANGELOG not in storage - need to extract from ZIP
416
+ // Get artifact to find ZIP storage key
417
+ let [artifact] = await db
418
+ .select()
419
+ .from(artifacts)
420
+ .where(and(eq(artifacts.repo_id, pkg.repo_id), eq(artifacts.package_name, name), eq(artifacts.version, version)))
421
+ .limit(1);
422
+ let zipData = null;
423
+ // 5. If artifact doesn't exist, try on-demand download
424
+ if (!artifact) {
425
+ // Check if we can download from source
426
+ if (!pkg.source_dist_url) {
427
+ return c.json({ error: 'Not Found', message: 'Artifact not found and source URL unavailable. Package may need to be downloaded first.' }, 404);
428
+ }
429
+ // Validate it's actually a URL
430
+ if (!pkg.source_dist_url.startsWith('http://') && !pkg.source_dist_url.startsWith('https://')) {
431
+ return c.json({ error: 'Not Found', message: 'Invalid source URL. Please re-sync the repository to update package metadata.' }, 404);
432
+ }
433
+ // Get repository for credentials
434
+ const [repo] = await db
435
+ .select()
436
+ .from(repositories)
437
+ .where(eq(repositories.id, pkg.repo_id))
438
+ .limit(1);
439
+ if (!repo) {
440
+ return c.json({ error: 'Not Found', message: 'Repository not found' }, 404);
441
+ }
442
+ try {
443
+ // Decrypt credentials
444
+ const credentialsJson = await decryptCredentials(repo.auth_credentials, c.env.ENCRYPTION_KEY);
445
+ const credentials = JSON.parse(credentialsJson);
446
+ // Download from source with authentication
447
+ const sourceResponse = await downloadFromSource(pkg.source_dist_url, repo.credential_type, credentials);
448
+ // Read the response body
449
+ const sourceStream = sourceResponse.body;
450
+ if (!sourceStream) {
451
+ throw new Error('Source response has no body');
452
+ }
453
+ // Read all chunks into memory
454
+ const chunks = [];
455
+ const reader = sourceStream.getReader();
456
+ let totalSize = 0;
457
+ while (true) {
458
+ const { done, value } = await reader.read();
459
+ if (done)
460
+ break;
461
+ if (value) {
462
+ chunks.push(value);
463
+ totalSize += value.length;
464
+ }
465
+ }
466
+ // Combine chunks into a single Uint8Array
467
+ zipData = new Uint8Array(totalSize);
468
+ let offset = 0;
469
+ for (const chunk of chunks) {
470
+ zipData.set(chunk, offset);
471
+ offset += chunk.length;
472
+ }
473
+ // Store artifact in storage
474
+ const storageKey = buildStorageKey(storageType, pkg.repo_id, name, version);
475
+ // Convert to ArrayBuffer (not SharedArrayBuffer) for storage
476
+ const arrayBuffer = zipData.buffer.slice(zipData.byteOffset, zipData.byteOffset + zipData.byteLength);
477
+ try {
478
+ await storage.put(storageKey, arrayBuffer);
479
+ console.log(`Successfully stored artifact for CHANGELOG extraction: ${storageKey} (${totalSize} bytes)`);
480
+ }
481
+ catch (err) {
482
+ console.error(`Error storing artifact ${storageKey}:`, err);
483
+ // Continue - we can still extract CHANGELOG from zipData
484
+ }
485
+ // Create artifact record
486
+ const artifactId = nanoid();
487
+ const now = Math.floor(Date.now() / 1000);
488
+ try {
489
+ await db.insert(artifacts).values({
490
+ id: artifactId,
491
+ repo_id: pkg.repo_id,
492
+ package_name: name,
493
+ version: version,
494
+ file_key: storageKey,
495
+ size: totalSize,
496
+ download_count: 0,
497
+ created_at: now,
498
+ });
499
+ artifact = {
500
+ id: artifactId,
501
+ repo_id: pkg.repo_id,
502
+ package_name: name,
503
+ version: version,
504
+ file_key: storageKey,
505
+ size: totalSize,
506
+ download_count: 0,
507
+ created_at: now,
508
+ last_downloaded_at: null,
509
+ };
510
+ }
511
+ catch (err) {
512
+ console.error(`Error creating artifact record:`, err);
513
+ // Continue - we can still extract CHANGELOG from zipData
514
+ }
515
+ }
516
+ catch (error) {
517
+ console.error(`Error downloading artifact from source:`, error);
518
+ return c.json({
519
+ error: 'Internal Server Error',
520
+ message: error instanceof Error ? error.message : 'Failed to download artifact',
521
+ }, 500);
522
+ }
523
+ }
524
+ // 6. Get ZIP from storage if we don't already have it in memory
525
+ if (!zipData) {
526
+ if (!artifact) {
527
+ return c.json({ error: 'Not Found', message: 'Artifact not found' }, 404);
528
+ }
529
+ const zipStream = await storage.get(artifact.file_key);
530
+ if (!zipStream) {
531
+ return c.json({ error: 'Not Found', message: 'Artifact file not found in storage' }, 404);
532
+ }
533
+ // Read ZIP into memory
534
+ const zipChunks = [];
535
+ const zipReader = zipStream.getReader();
536
+ while (true) {
537
+ const { done, value } = await zipReader.read();
538
+ if (done)
539
+ break;
540
+ if (value) {
541
+ zipChunks.push(value);
542
+ }
543
+ }
544
+ // Combine chunks
545
+ const totalSize = zipChunks.reduce((sum, chunk) => sum + chunk.length, 0);
546
+ zipData = new Uint8Array(totalSize);
547
+ let offset = 0;
548
+ for (const chunk of zipChunks) {
549
+ zipData.set(chunk, offset);
550
+ offset += chunk.length;
551
+ }
552
+ }
553
+ // 7. Extract CHANGELOG from ZIP
554
+ const changelogContent = extractChangelog(zipData);
555
+ if (!changelogContent) {
556
+ // Store NOT_FOUND marker to avoid repeated extraction attempts
557
+ const notFoundMarker = new TextEncoder().encode('NOT_FOUND');
558
+ await storage.put(changelogStorageKey, notFoundMarker).catch((err) => {
559
+ console.error(`Error storing NOT_FOUND marker for ${changelogStorageKey}:`, err);
560
+ });
561
+ return c.json({
562
+ error: 'Not Found',
563
+ message: 'No CHANGELOG file exists in this package version'
564
+ }, 404);
565
+ }
566
+ // 8. Store CHANGELOG in R2/S3 for future requests
567
+ const changelogBytes = new TextEncoder().encode(changelogContent);
568
+ await storage.put(changelogStorageKey, changelogBytes).catch((err) => {
569
+ console.error(`Error storing CHANGELOG for ${changelogStorageKey}:`, err);
570
+ // Continue even if storage fails - we'll still return the content
571
+ });
572
+ // 9. Return with aggressive CDN caching headers
573
+ return new Response(changelogContent, {
574
+ headers: {
575
+ 'Content-Type': 'text/markdown; charset=utf-8',
576
+ 'Cache-Control': 'public, max-age=31536000, immutable',
577
+ 'X-CHANGELOG-Source': 'extracted',
578
+ },
579
+ });
580
+ }
581
+ /**
582
+ * POST /api/packages/add-from-mirror
583
+ * Manually fetch and store packages from a selected mirror repository
584
+ */
585
+ export async function addPackagesFromMirror(c) {
586
+ const body = await c.req.json();
587
+ if (!body.repository_id || !Array.isArray(body.package_names) || body.package_names.length === 0) {
588
+ return c.json({ error: 'Bad Request', message: 'repository_id and package_names array are required' }, 400);
589
+ }
590
+ const db = c.get('database');
591
+ const url = new URL(c.req.url);
592
+ const baseUrl = `${url.protocol}//${url.host}`;
593
+ const results = [];
594
+ // Handle Packagist repository
595
+ if (body.repository_id === 'packagist') {
596
+ const mirroringEnabled = await isPackagistMirroringEnabled(c.env.KV);
597
+ if (!mirroringEnabled) {
598
+ return c.json({ error: 'Bad Request', message: 'Packagist mirroring is not enabled' }, 400);
599
+ }
600
+ const { ensurePackagistRepository } = await import('../composer');
601
+ await ensurePackagistRepository(db, c.env.ENCRYPTION_KEY, c.env.KV);
602
+ // Fetch each package from Packagist
603
+ for (const packageName of body.package_names) {
604
+ try {
605
+ const packagistUrl = `https://repo.packagist.org/p2/${packageName}.json`;
606
+ const response = await fetch(packagistUrl, {
607
+ headers: {
608
+ 'User-Agent': COMPOSER_USER_AGENT,
609
+ },
610
+ });
611
+ if (!response.ok) {
612
+ if (response.status === 404) {
613
+ results.push({ package: packageName, success: false, error: 'Package not found' });
614
+ continue;
615
+ }
616
+ results.push({ package: packageName, success: false, error: `HTTP ${response.status}` });
617
+ continue;
618
+ }
619
+ const packageData = await response.json();
620
+ const { transformPackageDistUrls } = await import('../composer');
621
+ const { storedCount, errors } = await transformPackageDistUrls(packageData, 'packagist', baseUrl, db);
622
+ if (storedCount > 0) {
623
+ results.push({ package: packageName, success: true, versions: storedCount });
624
+ }
625
+ else {
626
+ results.push({ package: packageName, success: false, error: errors.join('; ') || 'No versions stored' });
627
+ }
628
+ }
629
+ catch (error) {
630
+ results.push({
631
+ package: packageName,
632
+ success: false,
633
+ error: error instanceof Error ? error.message : 'Unknown error',
634
+ });
635
+ }
636
+ }
637
+ }
638
+ else {
639
+ // Handle other Composer repositories
640
+ const [repo] = await db
641
+ .select()
642
+ .from(repositories)
643
+ .where(eq(repositories.id, body.repository_id))
644
+ .limit(1);
645
+ if (!repo) {
646
+ return c.json({ error: 'Not Found', message: 'Repository not found' }, 404);
647
+ }
648
+ if (repo.vcs_type !== 'composer') {
649
+ return c.json({ error: 'Bad Request', message: 'Only Composer repositories can be used for manual package addition' }, 400);
650
+ }
651
+ if (repo.status !== 'active') {
652
+ return c.json({ error: 'Bad Request', message: 'Repository is not active' }, 400);
653
+ }
654
+ // TODO: Implement manual package addition for other composer repositories
655
+ // This requires fetching the package metadata from the source repository
656
+ // which is more complex than just fetching from Packagist
657
+ return c.json({ error: 'Not Implemented', message: 'Manual package addition is currently only supported for Packagist' }, 501);
658
+ }
659
+ return c.json({ results });
660
+ }
661
+ /**
662
+ * POST /packages/cleanup-numeric-versions
663
+ * Temporary utility to fix versioning issues
664
+ */
665
+ export async function cleanupNumericVersions(c) {
666
+ // Stub implementation to satisfy export requirements
667
+ // Real implementation would clean up numeric versions like x.y.z.0
668
+ return c.json({ message: 'Cleanup not implemented in this adapter version' });
669
+ }
670
+ //# sourceMappingURL=packages.handlers.js.map