@package-broker/core 0.2.15

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 (217) hide show
  1. package/dist/cache/index.d.ts +2 -0
  2. package/dist/cache/index.d.ts.map +1 -0
  3. package/dist/cache/index.js +2 -0
  4. package/dist/cache/index.js.map +1 -0
  5. package/dist/cache/memory-driver.d.ts +15 -0
  6. package/dist/cache/memory-driver.d.ts.map +1 -0
  7. package/dist/cache/memory-driver.js +56 -0
  8. package/dist/cache/memory-driver.js.map +1 -0
  9. package/dist/db/d1-driver.d.ts +3 -0
  10. package/dist/db/d1-driver.d.ts.map +1 -0
  11. package/dist/db/d1-driver.js +7 -0
  12. package/dist/db/d1-driver.js.map +1 -0
  13. package/dist/db/index.d.ts +5 -0
  14. package/dist/db/index.d.ts.map +1 -0
  15. package/dist/db/index.js +4 -0
  16. package/dist/db/index.js.map +1 -0
  17. package/dist/db/schema.d.ts +696 -0
  18. package/dist/db/schema.d.ts.map +1 -0
  19. package/dist/db/schema.js +99 -0
  20. package/dist/db/schema.js.map +1 -0
  21. package/dist/factory.d.ts +34 -0
  22. package/dist/factory.d.ts.map +1 -0
  23. package/dist/factory.js +121 -0
  24. package/dist/factory.js.map +1 -0
  25. package/dist/index.d.ts +16 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +17 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/jobs/index.d.ts +2 -0
  30. package/dist/jobs/index.d.ts.map +1 -0
  31. package/dist/jobs/index.js +7 -0
  32. package/dist/jobs/index.js.map +1 -0
  33. package/dist/jobs/processor.d.ts +49 -0
  34. package/dist/jobs/processor.d.ts.map +1 -0
  35. package/dist/jobs/processor.js +118 -0
  36. package/dist/jobs/processor.js.map +1 -0
  37. package/dist/middleware/auth.d.ts +52 -0
  38. package/dist/middleware/auth.d.ts.map +1 -0
  39. package/dist/middleware/auth.js +300 -0
  40. package/dist/middleware/auth.js.map +1 -0
  41. package/dist/middleware/composer-version.d.ts +7 -0
  42. package/dist/middleware/composer-version.d.ts.map +1 -0
  43. package/dist/middleware/composer-version.js +18 -0
  44. package/dist/middleware/composer-version.js.map +1 -0
  45. package/dist/middleware/error-handler.d.ts +7 -0
  46. package/dist/middleware/error-handler.d.ts.map +1 -0
  47. package/dist/middleware/error-handler.js +45 -0
  48. package/dist/middleware/error-handler.js.map +1 -0
  49. package/dist/middleware/index.d.ts +5 -0
  50. package/dist/middleware/index.d.ts.map +1 -0
  51. package/dist/middleware/index.js +6 -0
  52. package/dist/middleware/index.js.map +1 -0
  53. package/dist/middleware/request-id.d.ts +9 -0
  54. package/dist/middleware/request-id.d.ts.map +1 -0
  55. package/dist/middleware/request-id.js +36 -0
  56. package/dist/middleware/request-id.js.map +1 -0
  57. package/dist/ports.d.ts +32 -0
  58. package/dist/ports.d.ts.map +1 -0
  59. package/dist/ports.js +4 -0
  60. package/dist/ports.js.map +1 -0
  61. package/dist/queue/consumer.d.ts +18 -0
  62. package/dist/queue/consumer.d.ts.map +1 -0
  63. package/dist/queue/consumer.js +82 -0
  64. package/dist/queue/consumer.js.map +1 -0
  65. package/dist/queue/index.d.ts +2 -0
  66. package/dist/queue/index.d.ts.map +1 -0
  67. package/dist/queue/index.js +2 -0
  68. package/dist/queue/index.js.map +1 -0
  69. package/dist/queue/memory-driver.d.ts +13 -0
  70. package/dist/queue/memory-driver.d.ts.map +1 -0
  71. package/dist/queue/memory-driver.js +22 -0
  72. package/dist/queue/memory-driver.js.map +1 -0
  73. package/dist/queue/types.d.ts +19 -0
  74. package/dist/queue/types.d.ts.map +1 -0
  75. package/dist/queue/types.js +3 -0
  76. package/dist/queue/types.js.map +1 -0
  77. package/dist/routes/api/artifacts.d.ts +25 -0
  78. package/dist/routes/api/artifacts.d.ts.map +1 -0
  79. package/dist/routes/api/artifacts.js +57 -0
  80. package/dist/routes/api/artifacts.js.map +1 -0
  81. package/dist/routes/api/auth.d.ts +50 -0
  82. package/dist/routes/api/auth.d.ts.map +1 -0
  83. package/dist/routes/api/auth.js +268 -0
  84. package/dist/routes/api/auth.js.map +1 -0
  85. package/dist/routes/api/index.d.ts +9 -0
  86. package/dist/routes/api/index.d.ts.map +1 -0
  87. package/dist/routes/api/index.js +10 -0
  88. package/dist/routes/api/index.js.map +1 -0
  89. package/dist/routes/api/packages.d.ts +47 -0
  90. package/dist/routes/api/packages.d.ts.map +1 -0
  91. package/dist/routes/api/packages.js +671 -0
  92. package/dist/routes/api/packages.js.map +1 -0
  93. package/dist/routes/api/repositories.d.ts +56 -0
  94. package/dist/routes/api/repositories.d.ts.map +1 -0
  95. package/dist/routes/api/repositories.js +317 -0
  96. package/dist/routes/api/repositories.js.map +1 -0
  97. package/dist/routes/api/settings.d.ts +28 -0
  98. package/dist/routes/api/settings.d.ts.map +1 -0
  99. package/dist/routes/api/settings.js +81 -0
  100. package/dist/routes/api/settings.js.map +1 -0
  101. package/dist/routes/api/stats.d.ts +21 -0
  102. package/dist/routes/api/stats.d.ts.map +1 -0
  103. package/dist/routes/api/stats.js +52 -0
  104. package/dist/routes/api/stats.js.map +1 -0
  105. package/dist/routes/api/tokens.d.ts +39 -0
  106. package/dist/routes/api/tokens.d.ts.map +1 -0
  107. package/dist/routes/api/tokens.js +191 -0
  108. package/dist/routes/api/tokens.js.map +1 -0
  109. package/dist/routes/api/users.d.ts +5 -0
  110. package/dist/routes/api/users.d.ts.map +1 -0
  111. package/dist/routes/api/users.js +125 -0
  112. package/dist/routes/api/users.js.map +1 -0
  113. package/dist/routes/composer.d.ts +133 -0
  114. package/dist/routes/composer.d.ts.map +1 -0
  115. package/dist/routes/composer.js +1179 -0
  116. package/dist/routes/composer.js.map +1 -0
  117. package/dist/routes/dist.d.ts +32 -0
  118. package/dist/routes/dist.d.ts.map +1 -0
  119. package/dist/routes/dist.js +761 -0
  120. package/dist/routes/dist.js.map +1 -0
  121. package/dist/routes/health.d.ts +7 -0
  122. package/dist/routes/health.d.ts.map +1 -0
  123. package/dist/routes/health.js +22 -0
  124. package/dist/routes/health.js.map +1 -0
  125. package/dist/routes/index.d.ts +5 -0
  126. package/dist/routes/index.d.ts.map +1 -0
  127. package/dist/routes/index.js +6 -0
  128. package/dist/routes/index.js.map +1 -0
  129. package/dist/services/EmailService.d.ts +20 -0
  130. package/dist/services/EmailService.d.ts.map +1 -0
  131. package/dist/services/EmailService.js +27 -0
  132. package/dist/services/EmailService.js.map +1 -0
  133. package/dist/services/UserService.d.ts +27 -0
  134. package/dist/services/UserService.d.ts.map +1 -0
  135. package/dist/services/UserService.js +164 -0
  136. package/dist/services/UserService.js.map +1 -0
  137. package/dist/storage/driver.d.ts +65 -0
  138. package/dist/storage/driver.d.ts.map +1 -0
  139. package/dist/storage/driver.js +59 -0
  140. package/dist/storage/driver.js.map +1 -0
  141. package/dist/storage/index.d.ts +4 -0
  142. package/dist/storage/index.d.ts.map +1 -0
  143. package/dist/storage/index.js +5 -0
  144. package/dist/storage/index.js.map +1 -0
  145. package/dist/storage/r2-driver.d.ts +16 -0
  146. package/dist/storage/r2-driver.d.ts.map +1 -0
  147. package/dist/storage/r2-driver.js +28 -0
  148. package/dist/storage/r2-driver.js.map +1 -0
  149. package/dist/storage/s3-driver.d.ts +22 -0
  150. package/dist/storage/s3-driver.d.ts.map +1 -0
  151. package/dist/storage/s3-driver.js +66 -0
  152. package/dist/storage/s3-driver.js.map +1 -0
  153. package/dist/sync/github-sync.d.ts +15 -0
  154. package/dist/sync/github-sync.d.ts.map +1 -0
  155. package/dist/sync/github-sync.js +39 -0
  156. package/dist/sync/github-sync.js.map +1 -0
  157. package/dist/sync/index.d.ts +5 -0
  158. package/dist/sync/index.d.ts.map +1 -0
  159. package/dist/sync/index.js +6 -0
  160. package/dist/sync/index.js.map +1 -0
  161. package/dist/sync/repository-sync.d.ts +18 -0
  162. package/dist/sync/repository-sync.d.ts.map +1 -0
  163. package/dist/sync/repository-sync.js +214 -0
  164. package/dist/sync/repository-sync.js.map +1 -0
  165. package/dist/sync/strategies/composer-repo.d.ts +11 -0
  166. package/dist/sync/strategies/composer-repo.d.ts.map +1 -0
  167. package/dist/sync/strategies/composer-repo.js +269 -0
  168. package/dist/sync/strategies/composer-repo.js.map +1 -0
  169. package/dist/sync/strategies/github-api.d.ts +6 -0
  170. package/dist/sync/strategies/github-api.d.ts.map +1 -0
  171. package/dist/sync/strategies/github-api.js +137 -0
  172. package/dist/sync/strategies/github-api.js.map +1 -0
  173. package/dist/sync/strategies/github-packages.d.ts +7 -0
  174. package/dist/sync/strategies/github-packages.d.ts.map +1 -0
  175. package/dist/sync/strategies/github-packages.js +66 -0
  176. package/dist/sync/strategies/github-packages.js.map +1 -0
  177. package/dist/sync/strategies/index.d.ts +4 -0
  178. package/dist/sync/strategies/index.d.ts.map +1 -0
  179. package/dist/sync/strategies/index.js +5 -0
  180. package/dist/sync/strategies/index.js.map +1 -0
  181. package/dist/sync/types.d.ts +60 -0
  182. package/dist/sync/types.d.ts.map +1 -0
  183. package/dist/sync/types.js +3 -0
  184. package/dist/sync/types.js.map +1 -0
  185. package/dist/utils/analytics.d.ts +142 -0
  186. package/dist/utils/analytics.d.ts.map +1 -0
  187. package/dist/utils/analytics.js +229 -0
  188. package/dist/utils/analytics.js.map +1 -0
  189. package/dist/utils/download.d.ts +10 -0
  190. package/dist/utils/download.d.ts.map +1 -0
  191. package/dist/utils/download.js +34 -0
  192. package/dist/utils/download.js.map +1 -0
  193. package/dist/utils/encryption.d.ts +20 -0
  194. package/dist/utils/encryption.d.ts.map +1 -0
  195. package/dist/utils/encryption.js +76 -0
  196. package/dist/utils/encryption.js.map +1 -0
  197. package/dist/utils/index.d.ts +5 -0
  198. package/dist/utils/index.d.ts.map +1 -0
  199. package/dist/utils/index.js +6 -0
  200. package/dist/utils/index.js.map +1 -0
  201. package/dist/utils/logger.d.ts +78 -0
  202. package/dist/utils/logger.d.ts.map +1 -0
  203. package/dist/utils/logger.js +134 -0
  204. package/dist/utils/logger.js.map +1 -0
  205. package/dist/utils/upstream-fetch.d.ts +15 -0
  206. package/dist/utils/upstream-fetch.d.ts.map +1 -0
  207. package/dist/utils/upstream-fetch.js +108 -0
  208. package/dist/utils/upstream-fetch.js.map +1 -0
  209. package/dist/workflows/index.d.ts +3 -0
  210. package/dist/workflows/index.d.ts.map +1 -0
  211. package/dist/workflows/index.js +8 -0
  212. package/dist/workflows/index.js.map +1 -0
  213. package/dist/workflows/package-storage.d.ts +47 -0
  214. package/dist/workflows/package-storage.d.ts.map +1 -0
  215. package/dist/workflows/package-storage.js +136 -0
  216. package/dist/workflows/package-storage.js.map +1 -0
  217. package/package.json +62 -0
@@ -0,0 +1,761 @@
1
+ /*
2
+ * PACKAGE.broker
3
+ * Copyright (C) 2025 Łukasz Bajsarowicz
4
+ * Licensed under AGPL-3.0
5
+ */
6
+ import { artifacts, packages, repositories } from '../db/schema';
7
+ import { and, eq, or } from 'drizzle-orm';
8
+ import { buildStorageKey, buildReadmeStorageKey, buildChangelogStorageKey } from '../storage/driver';
9
+ import { downloadFromSource } from '../utils/download';
10
+ import { decryptCredentials } from '../utils/encryption';
11
+ import { COMPOSER_USER_AGENT } from '@package-broker/shared';
12
+ import { nanoid } from 'nanoid';
13
+ import { unzipSync, strFromU8 } from 'fflate';
14
+ import { getLogger } from '../utils/logger';
15
+ import { getAnalytics } from '../utils/analytics';
16
+ /**
17
+ * Extract README.md or README.mdown from ZIP archive
18
+ */
19
+ function extractReadme(zipData) {
20
+ try {
21
+ const files = unzipSync(zipData);
22
+ // Look for README in common locations (case-insensitive)
23
+ // Prefer .md over .mdown if both exist
24
+ const readmeNames = [
25
+ 'README.md', 'readme.md', 'README.MD', 'Readme.md',
26
+ 'README.mdown', 'readme.mdown', 'README.MDOWN', 'Readme.mdown'
27
+ ];
28
+ // First pass: look for .md files
29
+ for (const [path, content] of Object.entries(files)) {
30
+ const filename = path.split('/').pop() || '';
31
+ if (readmeNames.slice(0, 4).includes(filename)) {
32
+ return strFromU8(content);
33
+ }
34
+ }
35
+ // Second pass: look for .mdown files
36
+ for (const [path, content] of Object.entries(files)) {
37
+ const filename = path.split('/').pop() || '';
38
+ if (readmeNames.slice(4).includes(filename)) {
39
+ return strFromU8(content);
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ catch (error) {
45
+ const logger = getLogger();
46
+ logger.error('Error extracting README from ZIP', {}, error instanceof Error ? error : new Error(String(error)));
47
+ return null;
48
+ }
49
+ }
50
+ /**
51
+ * Extract CHANGELOG.md or CHANGELOG.mdown from ZIP archive
52
+ */
53
+ function extractChangelog(zipData) {
54
+ try {
55
+ const files = unzipSync(zipData);
56
+ // Look for CHANGELOG in common locations (case-insensitive)
57
+ // Prefer .md over .mdown if both exist
58
+ const changelogNames = [
59
+ 'CHANGELOG.md', 'changelog.md', 'CHANGELOG.MD', 'Changelog.md',
60
+ 'CHANGELOG.mdown', 'changelog.mdown', 'CHANGELOG.MDOWN', 'Changelog.mdown'
61
+ ];
62
+ // First pass: look for .md files
63
+ for (const [path, content] of Object.entries(files)) {
64
+ const filename = path.split('/').pop() || '';
65
+ if (changelogNames.slice(0, 4).includes(filename)) {
66
+ return strFromU8(content);
67
+ }
68
+ }
69
+ // Second pass: look for .mdown files
70
+ for (const [path, content] of Object.entries(files)) {
71
+ const filename = path.split('/').pop() || '';
72
+ if (changelogNames.slice(4).includes(filename)) {
73
+ return strFromU8(content);
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+ catch (error) {
79
+ const logger = getLogger();
80
+ logger.error('Error extracting CHANGELOG from ZIP', {}, error instanceof Error ? error : new Error(String(error)));
81
+ return null;
82
+ }
83
+ }
84
+ /**
85
+ * Proactively extract and store README and CHANGELOG from ZIP data
86
+ * Runs in background to not block the response
87
+ */
88
+ async function extractAndStoreReadme(storage, zipData, storageType, repoId, packageName, version) {
89
+ try {
90
+ const logger = getLogger();
91
+ // Extract and store README
92
+ const readmeContent = extractReadme(zipData);
93
+ const readmeStorageKey = buildReadmeStorageKey(storageType, repoId, packageName, version);
94
+ if (readmeContent) {
95
+ // Store README in R2/S3
96
+ const readmeBytes = new TextEncoder().encode(readmeContent);
97
+ await storage.put(readmeStorageKey, readmeBytes);
98
+ logger.info('Proactively stored README', { packageName, version, storageType, repoId });
99
+ }
100
+ else {
101
+ // Store NOT_FOUND marker to avoid repeated extraction attempts
102
+ const notFoundMarker = new TextEncoder().encode('NOT_FOUND');
103
+ await storage.put(readmeStorageKey, notFoundMarker);
104
+ }
105
+ // Extract and store CHANGELOG
106
+ const changelogContent = extractChangelog(zipData);
107
+ const changelogStorageKey = buildChangelogStorageKey(storageType, repoId, packageName, version);
108
+ if (changelogContent) {
109
+ // Store CHANGELOG in R2/S3
110
+ const changelogBytes = new TextEncoder().encode(changelogContent);
111
+ await storage.put(changelogStorageKey, changelogBytes);
112
+ logger.info('Proactively stored CHANGELOG', { packageName, version, storageType, repoId });
113
+ }
114
+ else {
115
+ // Store NOT_FOUND marker to avoid repeated extraction attempts
116
+ const notFoundMarker = new TextEncoder().encode('NOT_FOUND');
117
+ await storage.put(changelogStorageKey, notFoundMarker);
118
+ }
119
+ }
120
+ catch (error) {
121
+ // Don't fail the main request if README extraction fails
122
+ const logger = getLogger();
123
+ logger.error('Error extracting/storing README', { packageName, version, storageType, repoId }, error instanceof Error ? error : new Error(String(error)));
124
+ }
125
+ }
126
+ /**
127
+ * GET /dist/:repo_id/:vendor/:package/:version.zip
128
+ * OR
129
+ * GET /dist/:vendor/:package/:version/r:reference.zip (mirror URL format)
130
+ * Serve cached artifact with streaming, Last-Modified headers, and conditional requests
131
+ */
132
+ export async function distRoute(c) {
133
+ let repoId = c.req.param('repo_id');
134
+ let vendor = c.req.param('vendor');
135
+ let pkgParam = c.req.param('package');
136
+ let packageName;
137
+ let version = c.req.param('version')?.replace('.zip', '') || '';
138
+ const reference = c.req.param('reference');
139
+ const db = c.get('database');
140
+ // Handle mirror URL format: /dist/:package/:version/r:reference.zip
141
+ // In this case, repo_id and vendor/package split are not in the URL
142
+ if (!repoId && !vendor && pkgParam) {
143
+ const fullPackageName = pkgParam;
144
+ // Look up repo_id from package name
145
+ const [pkg] = await db
146
+ .select({ repo_id: packages.repo_id })
147
+ .from(packages)
148
+ .where(and(eq(packages.name, fullPackageName), eq(packages.version, version)))
149
+ .limit(1);
150
+ if (pkg) {
151
+ repoId = pkg.repo_id;
152
+ packageName = fullPackageName;
153
+ // Split package name into vendor/package for compatibility
154
+ const parts = fullPackageName.split('/');
155
+ if (parts.length === 2) {
156
+ vendor = parts[0];
157
+ pkgParam = parts[1];
158
+ }
159
+ else {
160
+ return c.json({ error: 'Bad Request', message: 'Invalid package name format' }, 400);
161
+ }
162
+ }
163
+ else {
164
+ return c.json({ error: 'Not Found', message: 'Package not found' }, 404);
165
+ }
166
+ }
167
+ else {
168
+ // Standard format: /dist/:repo_id/:vendor/:package/:version
169
+ packageName = `${vendor}/${pkgParam}`;
170
+ }
171
+ if (!repoId || !packageName || !version) {
172
+ return c.json({ error: 'Bad Request', message: 'Missing required parameters' }, 400);
173
+ }
174
+ const storage = c.var.storage;
175
+ // Handle Packagist packages (repo_id = "packagist") - cache in storage
176
+ if (repoId === 'packagist') {
177
+ // Use public storage key for Packagist packages
178
+ const storageKey = buildStorageKey('public', 'packagist', packageName, version);
179
+ // Look up artifact in database
180
+ let artifact = (await db
181
+ .select()
182
+ .from(artifacts)
183
+ .where(and(eq(artifacts.repo_id, 'packagist'), eq(artifacts.package_name, packageName), eq(artifacts.version, version)))
184
+ .limit(1))[0];
185
+ // Check if artifact exists in storage
186
+ let stream = await storage.get(storageKey);
187
+ // If not in storage, fetch from Packagist and cache
188
+ if (!stream) {
189
+ const packagistUrl = `https://repo.packagist.org/p2/${packageName}.json`;
190
+ try {
191
+ const response = await fetch(packagistUrl, {
192
+ headers: {
193
+ 'User-Agent': COMPOSER_USER_AGENT,
194
+ },
195
+ });
196
+ if (response.ok) {
197
+ const packagistData = await response.json();
198
+ const versions = packagistData.packages?.[packageName];
199
+ // Find version in array (Composer 2 p2 format) or dictionary (legacy format)
200
+ // Handle version normalization:
201
+ // - 1.5.9.0 → 1.5.9 (trailing .0)
202
+ // - 3.9999999.9999999.9999999-dev → 3.x-dev (dev version)
203
+ // - 2.4.8.0-patch3 → 2.4.8-p3 (patch alias)
204
+ const shortVersion = version.replace(/\.0$/, '');
205
+ const devMatch = version.match(/^(\d+)\.9999999\.9999999\.9999999-dev$/);
206
+ const xDevVersion = devMatch ? `${devMatch[1]}.x-dev` : null;
207
+ const patchVersion = version.includes('-patch')
208
+ ? version.replace(/\.0(-|$)/, '$1').replace('-patch', '-p')
209
+ : null;
210
+ let versionData = null;
211
+ if (Array.isArray(versions)) {
212
+ // Composer 2 p2 format: array of version objects
213
+ versionData = versions.find((v) => v.version === version ||
214
+ v.version === shortVersion ||
215
+ v.version_normalized === version ||
216
+ (xDevVersion && v.version === xDevVersion) ||
217
+ (patchVersion && v.version === patchVersion));
218
+ }
219
+ else if (versions) {
220
+ // Legacy format: dictionary keyed by version
221
+ versionData = versions[version] ||
222
+ versions[shortVersion] ||
223
+ (xDevVersion && versions[xDevVersion]) ||
224
+ (patchVersion && versions[patchVersion]);
225
+ }
226
+ if (versionData?.dist?.url) {
227
+ // Download from Packagist
228
+ const sourceResponse = await fetch(versionData.dist.url, {
229
+ headers: {
230
+ 'User-Agent': COMPOSER_USER_AGENT,
231
+ },
232
+ });
233
+ if (sourceResponse.ok && sourceResponse.body) {
234
+ // Read the response body as a stream
235
+ const sourceStream = sourceResponse.body;
236
+ const chunks = [];
237
+ const reader = sourceStream.getReader();
238
+ let totalSize = 0;
239
+ // Read all chunks
240
+ while (true) {
241
+ const { done, value } = await reader.read();
242
+ if (done)
243
+ break;
244
+ if (value) {
245
+ chunks.push(value);
246
+ totalSize += value.length;
247
+ }
248
+ }
249
+ // Combine chunks into a single Uint8Array
250
+ const combined = new Uint8Array(totalSize);
251
+ let offset = 0;
252
+ for (const chunk of chunks) {
253
+ combined.set(chunk, offset);
254
+ offset += chunk.length;
255
+ }
256
+ // Store in storage (synchronous)
257
+ const arrayBuffer = combined.buffer.slice(combined.byteOffset, combined.byteOffset + combined.byteLength);
258
+ try {
259
+ await storage.put(storageKey, arrayBuffer);
260
+ const logger = getLogger();
261
+ logger.info('Successfully stored Packagist artifact in storage', { storageKey, size: totalSize, packageName, version });
262
+ // Proactively extract and store README in background
263
+ c.executionCtx.waitUntil(extractAndStoreReadme(storage, combined, 'public', 'packagist', packageName, version));
264
+ }
265
+ catch (err) {
266
+ const logger = getLogger();
267
+ logger.error('Error storing Packagist artifact in storage', { storageKey, size: totalSize, packageName, version }, err instanceof Error ? err : new Error(String(err)));
268
+ // Don't fail the download if storage fails - still return the file to user
269
+ }
270
+ // Create or update artifact record
271
+ const now = Math.floor(Date.now() / 1000);
272
+ if (artifact) {
273
+ // Update existing artifact record
274
+ c.executionCtx.waitUntil(db
275
+ .update(artifacts)
276
+ .set({
277
+ size: totalSize,
278
+ created_at: now,
279
+ })
280
+ .where(eq(artifacts.id, artifact.id))
281
+ .catch((err) => {
282
+ const logger = getLogger();
283
+ logger.error('Error updating Packagist artifact record', { artifactId: artifact?.id, packageName, version }, err instanceof Error ? err : new Error(String(err)));
284
+ }));
285
+ }
286
+ else {
287
+ // Create new artifact record
288
+ const artifactId = nanoid();
289
+ c.executionCtx.waitUntil(db
290
+ .insert(artifacts)
291
+ .values({
292
+ id: artifactId,
293
+ repo_id: 'packagist',
294
+ package_name: packageName,
295
+ version: version,
296
+ file_key: storageKey,
297
+ size: totalSize,
298
+ download_count: 0,
299
+ created_at: now,
300
+ })
301
+ .catch((err) => {
302
+ const logger = getLogger();
303
+ logger.error('Error creating Packagist artifact record', { artifactId, packageName, version }, err instanceof Error ? err : new Error(String(err)));
304
+ }));
305
+ // Set artifact for download count update
306
+ artifact = {
307
+ id: artifactId,
308
+ repo_id: 'packagist',
309
+ package_name: packageName,
310
+ version: version,
311
+ file_key: storageKey,
312
+ size: totalSize,
313
+ download_count: 0,
314
+ created_at: now,
315
+ last_downloaded_at: null,
316
+ };
317
+ }
318
+ // Create stream from combined data
319
+ stream = new Response(combined).body;
320
+ }
321
+ }
322
+ }
323
+ }
324
+ catch (error) {
325
+ const logger = getLogger();
326
+ logger.error('Error proxying Packagist artifact', { packageName, version }, error instanceof Error ? error : new Error(String(error)));
327
+ }
328
+ // If Packagist fetch failed or version not found, try local DB fallback
329
+ if (!stream) {
330
+ // Fallback: Check if we have the package in our local DB
331
+ // This handles cases where metadata is cached in KV/DB (so composer found it)
332
+ // but upstream Packagist no longer lists it (so dist fetch failed)
333
+ try {
334
+ // Normalize version for DB lookup (remove trailing .0 if present)
335
+ const shortVersion = version.replace(/\.0$/, '');
336
+ let [pkg] = await db
337
+ .select()
338
+ .from(packages)
339
+ .where(and(eq(packages.repo_id, 'packagist'), eq(packages.name, packageName), or(eq(packages.version, version), eq(packages.version, shortVersion))))
340
+ .limit(1);
341
+ // Handle 3.999... -> 3.x-dev normalization for DB lookup
342
+ if (!pkg && version.includes('9999999') && version.endsWith('-dev')) {
343
+ const devMatch = version.match(/^(\d+)\.9999999\.9999999\.9999999-dev$/);
344
+ if (devMatch) {
345
+ const xDevVersion = `${devMatch[1]}.x-dev`;
346
+ [pkg] = await db
347
+ .select()
348
+ .from(packages)
349
+ .where(and(eq(packages.repo_id, 'packagist'), eq(packages.name, packageName), eq(packages.version, xDevVersion)))
350
+ .limit(1);
351
+ }
352
+ }
353
+ if (pkg?.source_dist_url) {
354
+ const logger = getLogger();
355
+ logger.info('Found package in local DB fallback', { packageName, version, sourceDistUrl: pkg.source_dist_url });
356
+ // Download from source
357
+ const sourceResponse = await fetch(pkg.source_dist_url, {
358
+ headers: {
359
+ 'User-Agent': COMPOSER_USER_AGENT,
360
+ },
361
+ });
362
+ if (sourceResponse.ok && sourceResponse.body) {
363
+ // Read the response body as a stream
364
+ const sourceStream = sourceResponse.body;
365
+ const chunks = [];
366
+ const reader = sourceStream.getReader();
367
+ let totalSize = 0;
368
+ // Read all chunks
369
+ while (true) {
370
+ const { done, value } = await reader.read();
371
+ if (done)
372
+ break;
373
+ if (value) {
374
+ chunks.push(value);
375
+ totalSize += value.length;
376
+ }
377
+ }
378
+ // Combine chunks into a single Uint8Array
379
+ const combined = new Uint8Array(totalSize);
380
+ let offset = 0;
381
+ for (const chunk of chunks) {
382
+ combined.set(chunk, offset);
383
+ offset += chunk.length;
384
+ }
385
+ // Store in storage (synchronous)
386
+ const arrayBuffer = combined.buffer.slice(combined.byteOffset, combined.byteOffset + combined.byteLength);
387
+ try {
388
+ await storage.put(storageKey, arrayBuffer);
389
+ // Proactively extract and store README
390
+ c.executionCtx.waitUntil(extractAndStoreReadme(storage, combined, 'public', 'packagist', packageName, version));
391
+ }
392
+ catch (err) {
393
+ logger.error('Error storing artifact from DB fallback', { packageName, version }, err instanceof Error ? err : new Error(String(err)));
394
+ }
395
+ // Create stream from combined data
396
+ stream = new Response(combined).body;
397
+ // Create or update artifact record
398
+ const now = Math.floor(Date.now() / 1000);
399
+ const artifactId = artifact?.id || nanoid();
400
+ const artifactData = {
401
+ id: artifactId,
402
+ repo_id: 'packagist',
403
+ package_name: packageName,
404
+ version: version,
405
+ file_key: storageKey,
406
+ size: totalSize,
407
+ created_at: now,
408
+ download_count: (artifact?.download_count || 0),
409
+ last_downloaded_at: artifact?.last_downloaded_at || null
410
+ };
411
+ if (artifact) {
412
+ c.executionCtx.waitUntil(db.update(artifacts)
413
+ .set({ size: totalSize, created_at: now })
414
+ .where(eq(artifacts.id, artifact.id))
415
+ .catch(() => { }));
416
+ }
417
+ else {
418
+ c.executionCtx.waitUntil(db.insert(artifacts)
419
+ .values({ ...artifactData, download_count: 0 })
420
+ .catch(() => { }));
421
+ // Update local artifact var so download count tracking works
422
+ artifact = artifactData;
423
+ }
424
+ }
425
+ }
426
+ }
427
+ catch (e) {
428
+ const logger = getLogger();
429
+ logger.error('Error in DB fallback check', { packageName, version }, e instanceof Error ? e : new Error(String(e)));
430
+ }
431
+ }
432
+ // If we get here, Packagist fetch failed AND DB fallback failed
433
+ if (!stream) {
434
+ return c.json({
435
+ error: 'Not Found',
436
+ message: 'Package not found on Packagist'
437
+ }, 404);
438
+ }
439
+ }
440
+ // Update download count (non-blocking) - only if artifact exists
441
+ if (artifact) {
442
+ const updateDownloadCount = async () => {
443
+ if (c.env.QUEUE && typeof c.env.QUEUE.send === 'function') {
444
+ // Use Queue for async processing (Paid plan)
445
+ await c.env.QUEUE.send({
446
+ type: 'update_artifact_download',
447
+ artifactId: artifact.id,
448
+ timestamp: Math.floor(Date.now() / 1000),
449
+ });
450
+ }
451
+ else {
452
+ // Fallback: update directly in database (Free tier)
453
+ await db
454
+ .update(artifacts)
455
+ .set({
456
+ download_count: (artifact.download_count || 0) + 1,
457
+ last_downloaded_at: Math.floor(Date.now() / 1000),
458
+ })
459
+ .where(eq(artifacts.id, artifact.id));
460
+ }
461
+ };
462
+ // Run in background to not block the response
463
+ c.executionCtx.waitUntil(updateDownloadCount());
464
+ }
465
+ // Build response headers
466
+ const headers = new Headers();
467
+ headers.set('Content-Type', 'application/zip');
468
+ // Format filename as vendor--module-name--version.zip (replace / with --)
469
+ const filename = `${packageName.replace('/', '--')}--${version}.zip`;
470
+ headers.set('Content-Disposition', `attachment; filename="${filename}"`);
471
+ if (artifact?.size) {
472
+ headers.set('Content-Length', String(artifact.size));
473
+ }
474
+ if (artifact?.created_at) {
475
+ headers.set('Last-Modified', new Date(artifact.created_at * 1000).toUTCString());
476
+ }
477
+ // Cache immutable artifacts
478
+ headers.set('Cache-Control', 'public, max-age=31536000, immutable');
479
+ // Track package download analytics
480
+ const analytics = getAnalytics();
481
+ const requestId = c.get('requestId');
482
+ analytics.trackPackageDownload({
483
+ requestId,
484
+ packageName,
485
+ version,
486
+ repoId: 'packagist',
487
+ size: artifact?.size ?? undefined,
488
+ cacheHit: !!stream, // Stream exists means it was cached
489
+ });
490
+ return new Response(stream, {
491
+ status: 200,
492
+ headers,
493
+ });
494
+ }
495
+ // For private repositories, use private storage key
496
+ const storageKey = buildStorageKey('private', repoId, packageName, version);
497
+ // Look up artifact in database
498
+ let artifact = (await db
499
+ .select()
500
+ .from(artifacts)
501
+ .where(and(eq(artifacts.repo_id, repoId), eq(artifacts.package_name, packageName), eq(artifacts.version, version)))
502
+ .limit(1))[0];
503
+ // Look up package to get source_dist_url (needed for on-demand mirroring)
504
+ let [pkg] = await db
505
+ .select()
506
+ .from(packages)
507
+ .where(and(eq(packages.repo_id, repoId), eq(packages.name, packageName), eq(packages.version, version)))
508
+ .limit(1);
509
+ // If not found with specific repo_id, try to find it with any repo_id
510
+ // This handles cases where package was stored but repo_id doesn't match
511
+ if (!pkg) {
512
+ const [pkgAnyRepo] = await db
513
+ .select()
514
+ .from(packages)
515
+ .where(and(eq(packages.name, packageName), eq(packages.version, version)))
516
+ .limit(1);
517
+ if (pkgAnyRepo) {
518
+ const logger = getLogger();
519
+ logger.warn('Package found but with different repo_id', { packageName, version, foundRepoId: pkgAnyRepo.repo_id, expectedRepoId: repoId });
520
+ pkg = pkgAnyRepo;
521
+ }
522
+ }
523
+ if (!pkg) {
524
+ // Package not found in database - try to fetch from repository directly
525
+ // This handles race conditions where metadata wasn't stored yet
526
+ const [repo] = await db
527
+ .select()
528
+ .from(repositories)
529
+ .where(eq(repositories.id, repoId))
530
+ .limit(1);
531
+ if (repo && repo.vcs_type === 'composer') {
532
+ try {
533
+ // Try to fetch package metadata from upstream and get source_dist_url
534
+ const { fetchPackageFromUpstream } = await import('../utils/upstream-fetch');
535
+ const packageData = await fetchPackageFromUpstream({
536
+ id: repo.id,
537
+ url: repo.url,
538
+ vcs_type: repo.vcs_type,
539
+ credential_type: repo.credential_type,
540
+ auth_credentials: repo.auth_credentials,
541
+ package_filter: repo.package_filter,
542
+ }, packageName, c.env.ENCRYPTION_KEY);
543
+ const packageVersion = packageData?.packages?.[packageName]?.[version];
544
+ if (packageVersion?.dist?.url) {
545
+ const sourceDistUrl = packageVersion.dist.url;
546
+ // Download from source and stream to client
547
+ const credentialsJson = await decryptCredentials(repo.auth_credentials, c.env.ENCRYPTION_KEY);
548
+ const credentials = JSON.parse(credentialsJson);
549
+ const sourceResponse = await downloadFromSource(sourceDistUrl, repo.credential_type, credentials);
550
+ if (sourceResponse.ok && sourceResponse.body) {
551
+ const headers = new Headers();
552
+ headers.set('Content-Type', 'application/zip');
553
+ headers.set('Cache-Control', 'public, max-age=3600');
554
+ return new Response(sourceResponse.body, {
555
+ status: 200,
556
+ headers,
557
+ });
558
+ }
559
+ }
560
+ }
561
+ catch (error) {
562
+ const logger = getLogger();
563
+ logger.error('Error fetching package from upstream', { packageName, version, repoId }, error instanceof Error ? error : new Error(String(error)));
564
+ }
565
+ }
566
+ // Package not found in database and couldn't fetch from upstream
567
+ const logger = getLogger();
568
+ logger.warn('Package not found in DB for repo', { packageName, version, repoId, note: 'This may indicate a race condition or missing package metadata' });
569
+ return c.json({
570
+ error: 'Not Found',
571
+ message: 'Package not found. The package metadata may not be available yet. Try refreshing package metadata.'
572
+ }, 404);
573
+ }
574
+ // Check conditional request (If-Modified-Since) if artifact exists
575
+ if (artifact) {
576
+ const ifModifiedSince = c.req.header('If-Modified-Since');
577
+ if (ifModifiedSince && artifact.created_at) {
578
+ const clientDate = new Date(ifModifiedSince).getTime();
579
+ const artifactDate = artifact.created_at * 1000;
580
+ if (clientDate >= artifactDate) {
581
+ return new Response(null, { status: 304 });
582
+ }
583
+ }
584
+ }
585
+ // Get artifact from storage
586
+ let stream = await storage.get(storageKey);
587
+ // If artifact not in storage, try on-demand mirroring
588
+ if (!stream) {
589
+ // Validate source_dist_url exists and is a valid URL
590
+ if (!pkg.source_dist_url) {
591
+ const logger = getLogger();
592
+ logger.error('Package found but source_dist_url is missing', { packageName, version, repoId });
593
+ return c.json({ error: 'Not Found', message: 'Artifact file not found and source URL unavailable. Please re-sync the repository.' }, 404);
594
+ }
595
+ // Validate it's actually a URL (not a placeholder or column name)
596
+ if (!pkg.source_dist_url.startsWith('http://') && !pkg.source_dist_url.startsWith('https://')) {
597
+ const logger = getLogger();
598
+ logger.error('Package has invalid source_dist_url', { packageName, version, repoId, sourceDistUrl: pkg.source_dist_url });
599
+ return c.json({ error: 'Not Found', message: 'Invalid source URL. Please re-sync the repository to update package metadata.' }, 404);
600
+ }
601
+ // Get repository for credentials
602
+ const [repo] = await db
603
+ .select()
604
+ .from(repositories)
605
+ .where(eq(repositories.id, repoId))
606
+ .limit(1);
607
+ if (!repo) {
608
+ return c.json({ error: 'Not Found', message: 'Repository not found' }, 404);
609
+ }
610
+ try {
611
+ // Decrypt credentials
612
+ const credentialsJson = await decryptCredentials(repo.auth_credentials, c.env.ENCRYPTION_KEY);
613
+ const credentials = JSON.parse(credentialsJson);
614
+ // Download from source with authentication
615
+ const sourceResponse = await downloadFromSource(pkg.source_dist_url, repo.credential_type, credentials);
616
+ // Read the response body as a stream
617
+ const sourceStream = sourceResponse.body;
618
+ if (!sourceStream) {
619
+ throw new Error('Source response has no body');
620
+ }
621
+ // Store in R2 storage (non-blocking for response, but we need to wait for it)
622
+ // We'll stream to user while storing in background
623
+ const chunks = [];
624
+ const reader = sourceStream.getReader();
625
+ let totalSize = 0;
626
+ // Read all chunks (we need to buffer for storage anyway)
627
+ while (true) {
628
+ const { done, value } = await reader.read();
629
+ if (done)
630
+ break;
631
+ if (value) {
632
+ chunks.push(value);
633
+ totalSize += value.length;
634
+ }
635
+ }
636
+ // Combine chunks
637
+ const combined = new Uint8Array(totalSize);
638
+ let offset = 0;
639
+ for (const chunk of chunks) {
640
+ combined.set(chunk, offset);
641
+ offset += chunk.length;
642
+ }
643
+ // Create stream for response
644
+ stream = new Response(combined).body;
645
+ // Store in storage (background)
646
+ c.executionCtx.waitUntil((async () => {
647
+ try {
648
+ // Store artifact
649
+ // Convert to ArrayBuffer
650
+ const arrayBuffer = combined.buffer.slice(combined.byteOffset, combined.byteOffset + combined.byteLength);
651
+ await storage.put(storageKey, arrayBuffer);
652
+ const logger = getLogger();
653
+ logger.info('Successfully stored artifact on-demand', { storageKey, size: totalSize });
654
+ // Create or update artifact record
655
+ const now = Math.floor(Date.now() / 1000);
656
+ if (artifact) {
657
+ await db
658
+ .update(artifacts)
659
+ .set({
660
+ size: totalSize,
661
+ created_at: now,
662
+ })
663
+ .where(eq(artifacts.id, artifact.id));
664
+ }
665
+ else {
666
+ const artifactId = nanoid();
667
+ await db.insert(artifacts).values({
668
+ id: artifactId,
669
+ repo_id: repoId,
670
+ package_name: packageName,
671
+ version: version,
672
+ file_key: storageKey,
673
+ size: totalSize,
674
+ download_count: 0,
675
+ created_at: now,
676
+ });
677
+ // Update local artifact for download count
678
+ artifact = {
679
+ id: artifactId,
680
+ repo_id: repoId,
681
+ package_name: packageName,
682
+ version: version,
683
+ file_key: storageKey,
684
+ size: totalSize,
685
+ download_count: 0,
686
+ created_at: now,
687
+ last_downloaded_at: null,
688
+ };
689
+ }
690
+ // Proactively extract and store README
691
+ await extractAndStoreReadme(storage, combined, 'private', repoId, packageName, version);
692
+ }
693
+ catch (error) {
694
+ const logger = getLogger();
695
+ logger.error('Error storing artifact on-demand', { storageKey }, error instanceof Error ? error : new Error(String(error)));
696
+ }
697
+ })());
698
+ }
699
+ catch (error) {
700
+ const logger = getLogger();
701
+ logger.error('Error downloading artifact from source', { sourceDistUrl: pkg.source_dist_url }, error instanceof Error ? error : new Error(String(error)));
702
+ return c.json({
703
+ error: 'Bad Gateway',
704
+ message: 'Failed to download package from source. The source URL may be invalid or accessible.'
705
+ }, 502);
706
+ }
707
+ }
708
+ // Update download count (non-blocking)
709
+ if (artifact) {
710
+ c.executionCtx.waitUntil((async () => {
711
+ if (c.env.QUEUE && typeof c.env.QUEUE.send === 'function') {
712
+ await c.env.QUEUE.send({
713
+ type: 'update_artifact_download',
714
+ artifactId: artifact.id,
715
+ timestamp: Math.floor(Date.now() / 1000),
716
+ });
717
+ }
718
+ else {
719
+ await db
720
+ .update(artifacts)
721
+ .set({
722
+ download_count: (artifact.download_count || 0) + 1,
723
+ last_downloaded_at: Math.floor(Date.now() / 1000),
724
+ })
725
+ .where(eq(artifacts.id, artifact.id));
726
+ }
727
+ })());
728
+ }
729
+ // Build response
730
+ const headers = new Headers();
731
+ headers.set('Content-Type', 'application/zip');
732
+ const filename = `${packageName.replace('/', '--')}--${version}.zip`;
733
+ headers.set('Content-Disposition', `attachment; filename="${filename}"`);
734
+ if (artifact?.size) {
735
+ headers.set('Content-Length', String(artifact.size));
736
+ }
737
+ if (artifact?.created_at) {
738
+ headers.set('Last-Modified', new Date(artifact.created_at * 1000).toUTCString());
739
+ }
740
+ // Cache settings
741
+ headers.set('Cache-Control', 'public, max-age=31536000, immutable');
742
+ // Track package download analytics
743
+ const analytics = getAnalytics();
744
+ const requestId = c.get('requestId');
745
+ analytics.trackPackageDownload({
746
+ requestId,
747
+ packageName,
748
+ version,
749
+ repoId,
750
+ size: artifact?.size ?? undefined,
751
+ cacheHit: !!stream,
752
+ });
753
+ return new Response(stream, {
754
+ status: 200,
755
+ headers,
756
+ });
757
+ }
758
+ // Aliases for specific route patterns handled by the same function
759
+ export const distMirrorRoute = distRoute;
760
+ export const distLockfileRoute = distRoute;
761
+ //# sourceMappingURL=dist.js.map