@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.
- package/dist/cache/index.d.ts +2 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +2 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/memory-driver.d.ts +15 -0
- package/dist/cache/memory-driver.d.ts.map +1 -0
- package/dist/cache/memory-driver.js +56 -0
- package/dist/cache/memory-driver.js.map +1 -0
- package/dist/db/d1-driver.d.ts +3 -0
- package/dist/db/d1-driver.d.ts.map +1 -0
- package/dist/db/d1-driver.js +7 -0
- package/dist/db/d1-driver.js.map +1 -0
- package/dist/db/index.d.ts +5 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +4 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +696 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +99 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/factory.d.ts +34 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +121 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs/index.d.ts +2 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +7 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/processor.d.ts +49 -0
- package/dist/jobs/processor.d.ts.map +1 -0
- package/dist/jobs/processor.js +118 -0
- package/dist/jobs/processor.js.map +1 -0
- package/dist/middleware/auth.d.ts +52 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +300 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/composer-version.d.ts +7 -0
- package/dist/middleware/composer-version.d.ts.map +1 -0
- package/dist/middleware/composer-version.js +18 -0
- package/dist/middleware/composer-version.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +7 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +45 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/index.d.ts +5 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +6 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/request-id.d.ts +9 -0
- package/dist/middleware/request-id.d.ts.map +1 -0
- package/dist/middleware/request-id.js +36 -0
- package/dist/middleware/request-id.js.map +1 -0
- package/dist/ports.d.ts +32 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +4 -0
- package/dist/ports.js.map +1 -0
- package/dist/queue/consumer.d.ts +18 -0
- package/dist/queue/consumer.d.ts.map +1 -0
- package/dist/queue/consumer.js +82 -0
- package/dist/queue/consumer.js.map +1 -0
- package/dist/queue/index.d.ts +2 -0
- package/dist/queue/index.d.ts.map +1 -0
- package/dist/queue/index.js +2 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/memory-driver.d.ts +13 -0
- package/dist/queue/memory-driver.d.ts.map +1 -0
- package/dist/queue/memory-driver.js +22 -0
- package/dist/queue/memory-driver.js.map +1 -0
- package/dist/queue/types.d.ts +19 -0
- package/dist/queue/types.d.ts.map +1 -0
- package/dist/queue/types.js +3 -0
- package/dist/queue/types.js.map +1 -0
- package/dist/routes/api/artifacts.d.ts +25 -0
- package/dist/routes/api/artifacts.d.ts.map +1 -0
- package/dist/routes/api/artifacts.js +57 -0
- package/dist/routes/api/artifacts.js.map +1 -0
- package/dist/routes/api/auth.d.ts +50 -0
- package/dist/routes/api/auth.d.ts.map +1 -0
- package/dist/routes/api/auth.js +268 -0
- package/dist/routes/api/auth.js.map +1 -0
- package/dist/routes/api/index.d.ts +9 -0
- package/dist/routes/api/index.d.ts.map +1 -0
- package/dist/routes/api/index.js +10 -0
- package/dist/routes/api/index.js.map +1 -0
- package/dist/routes/api/packages.d.ts +47 -0
- package/dist/routes/api/packages.d.ts.map +1 -0
- package/dist/routes/api/packages.js +671 -0
- package/dist/routes/api/packages.js.map +1 -0
- package/dist/routes/api/repositories.d.ts +56 -0
- package/dist/routes/api/repositories.d.ts.map +1 -0
- package/dist/routes/api/repositories.js +317 -0
- package/dist/routes/api/repositories.js.map +1 -0
- package/dist/routes/api/settings.d.ts +28 -0
- package/dist/routes/api/settings.d.ts.map +1 -0
- package/dist/routes/api/settings.js +81 -0
- package/dist/routes/api/settings.js.map +1 -0
- package/dist/routes/api/stats.d.ts +21 -0
- package/dist/routes/api/stats.d.ts.map +1 -0
- package/dist/routes/api/stats.js +52 -0
- package/dist/routes/api/stats.js.map +1 -0
- package/dist/routes/api/tokens.d.ts +39 -0
- package/dist/routes/api/tokens.d.ts.map +1 -0
- package/dist/routes/api/tokens.js +191 -0
- package/dist/routes/api/tokens.js.map +1 -0
- package/dist/routes/api/users.d.ts +5 -0
- package/dist/routes/api/users.d.ts.map +1 -0
- package/dist/routes/api/users.js +125 -0
- package/dist/routes/api/users.js.map +1 -0
- package/dist/routes/composer.d.ts +133 -0
- package/dist/routes/composer.d.ts.map +1 -0
- package/dist/routes/composer.js +1179 -0
- package/dist/routes/composer.js.map +1 -0
- package/dist/routes/dist.d.ts +32 -0
- package/dist/routes/dist.d.ts.map +1 -0
- package/dist/routes/dist.js +761 -0
- package/dist/routes/dist.js.map +1 -0
- package/dist/routes/health.d.ts +7 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +22 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +5 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +6 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/services/EmailService.d.ts +20 -0
- package/dist/services/EmailService.d.ts.map +1 -0
- package/dist/services/EmailService.js +27 -0
- package/dist/services/EmailService.js.map +1 -0
- package/dist/services/UserService.d.ts +27 -0
- package/dist/services/UserService.d.ts.map +1 -0
- package/dist/services/UserService.js +164 -0
- package/dist/services/UserService.js.map +1 -0
- package/dist/storage/driver.d.ts +65 -0
- package/dist/storage/driver.d.ts.map +1 -0
- package/dist/storage/driver.js +59 -0
- package/dist/storage/driver.js.map +1 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/r2-driver.d.ts +16 -0
- package/dist/storage/r2-driver.d.ts.map +1 -0
- package/dist/storage/r2-driver.js +28 -0
- package/dist/storage/r2-driver.js.map +1 -0
- package/dist/storage/s3-driver.d.ts +22 -0
- package/dist/storage/s3-driver.d.ts.map +1 -0
- package/dist/storage/s3-driver.js +66 -0
- package/dist/storage/s3-driver.js.map +1 -0
- package/dist/sync/github-sync.d.ts +15 -0
- package/dist/sync/github-sync.d.ts.map +1 -0
- package/dist/sync/github-sync.js +39 -0
- package/dist/sync/github-sync.js.map +1 -0
- package/dist/sync/index.d.ts +5 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +6 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/repository-sync.d.ts +18 -0
- package/dist/sync/repository-sync.d.ts.map +1 -0
- package/dist/sync/repository-sync.js +214 -0
- package/dist/sync/repository-sync.js.map +1 -0
- package/dist/sync/strategies/composer-repo.d.ts +11 -0
- package/dist/sync/strategies/composer-repo.d.ts.map +1 -0
- package/dist/sync/strategies/composer-repo.js +269 -0
- package/dist/sync/strategies/composer-repo.js.map +1 -0
- package/dist/sync/strategies/github-api.d.ts +6 -0
- package/dist/sync/strategies/github-api.d.ts.map +1 -0
- package/dist/sync/strategies/github-api.js +137 -0
- package/dist/sync/strategies/github-api.js.map +1 -0
- package/dist/sync/strategies/github-packages.d.ts +7 -0
- package/dist/sync/strategies/github-packages.d.ts.map +1 -0
- package/dist/sync/strategies/github-packages.js +66 -0
- package/dist/sync/strategies/github-packages.js.map +1 -0
- package/dist/sync/strategies/index.d.ts +4 -0
- package/dist/sync/strategies/index.d.ts.map +1 -0
- package/dist/sync/strategies/index.js +5 -0
- package/dist/sync/strategies/index.js.map +1 -0
- package/dist/sync/types.d.ts +60 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +3 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/utils/analytics.d.ts +142 -0
- package/dist/utils/analytics.d.ts.map +1 -0
- package/dist/utils/analytics.js +229 -0
- package/dist/utils/analytics.js.map +1 -0
- package/dist/utils/download.d.ts +10 -0
- package/dist/utils/download.d.ts.map +1 -0
- package/dist/utils/download.js +34 -0
- package/dist/utils/download.js.map +1 -0
- package/dist/utils/encryption.d.ts +20 -0
- package/dist/utils/encryption.d.ts.map +1 -0
- package/dist/utils/encryption.js +76 -0
- package/dist/utils/encryption.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +78 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +134 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/upstream-fetch.d.ts +15 -0
- package/dist/utils/upstream-fetch.d.ts.map +1 -0
- package/dist/utils/upstream-fetch.js +108 -0
- package/dist/utils/upstream-fetch.js.map +1 -0
- package/dist/workflows/index.d.ts +3 -0
- package/dist/workflows/index.d.ts.map +1 -0
- package/dist/workflows/index.js +8 -0
- package/dist/workflows/index.js.map +1 -0
- package/dist/workflows/package-storage.d.ts +47 -0
- package/dist/workflows/package-storage.d.ts.map +1 -0
- package/dist/workflows/package-storage.js +136 -0
- package/dist/workflows/package-storage.js.map +1 -0
- 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
|