wyvrnpm 1.2.1 → 2.0.1
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/README.md +513 -413
- package/bin/wyvrn.js +277 -163
- package/package.json +37 -37
- package/src/commands/install.js +156 -109
- package/src/commands/link.js +316 -0
- package/src/commands/profile.js +171 -0
- package/src/commands/publish.js +232 -180
- package/src/config.js +63 -60
- package/src/download.js +325 -164
- package/src/index.js +10 -9
- package/src/link-utils.js +95 -0
- package/src/manifest.js +133 -77
- package/src/profile.js +377 -0
- package/src/providers/base.js +133 -56
- package/src/providers/file.js +181 -71
- package/src/providers/http.js +268 -118
- package/src/providers/s3.js +273 -136
- package/src/resolve.js +262 -200
package/src/providers/http.js
CHANGED
|
@@ -1,118 +1,268 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const http = require('http');
|
|
5
|
-
const https = require('https');
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
req.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
},
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const { pipeline } = require('stream/promises');
|
|
7
|
+
const { Readable } = require('stream');
|
|
8
|
+
const BaseProvider = require('./base');
|
|
9
|
+
|
|
10
|
+
function pinDependencies(rawDeps, lockedDeps) {
|
|
11
|
+
if (!rawDeps || !lockedDeps || Object.keys(lockedDeps).length === 0) return rawDeps;
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const [pkgName, value] of Object.entries(rawDeps)) {
|
|
14
|
+
const locked = lockedDeps[pkgName];
|
|
15
|
+
if (locked) {
|
|
16
|
+
result[pkgName] = typeof value === 'object' && value !== null
|
|
17
|
+
? { ...value, version: locked }
|
|
18
|
+
: locked;
|
|
19
|
+
} else {
|
|
20
|
+
result[pkgName] = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class HttpProvider extends BaseProvider {
|
|
27
|
+
static canHandle(source) {
|
|
28
|
+
return /^https?:\/\//.test(source);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static get providerName() {
|
|
32
|
+
return 'HTTP';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── helpers ──────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
_lib(url) {
|
|
38
|
+
return url.startsWith('https') ? https : http;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** PUT a file from disk to url. */
|
|
42
|
+
_put(url, filePath, contentType, token) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const body = fs.readFileSync(filePath);
|
|
45
|
+
const parsed = new URL(url);
|
|
46
|
+
const headers = { 'Content-Type': contentType, 'Content-Length': body.length };
|
|
47
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
48
|
+
console.log(`[wyvrn] → PUT ${url}`);
|
|
49
|
+
const req = this._lib(url).request(
|
|
50
|
+
{ hostname: parsed.hostname, port: parsed.port || undefined, path: parsed.pathname, method: 'PUT', headers },
|
|
51
|
+
(res) => {
|
|
52
|
+
res.resume();
|
|
53
|
+
if (res.statusCode >= 200 && res.statusCode < 300) resolve();
|
|
54
|
+
else reject(new Error(`HTTP PUT ${url} failed: ${res.statusCode}`));
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
req.on('error', reject);
|
|
58
|
+
req.write(body);
|
|
59
|
+
req.end();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** PUT a Buffer to url. */
|
|
64
|
+
_putBuffer(url, buffer, contentType, token) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const parsed = new URL(url);
|
|
67
|
+
const headers = { 'Content-Type': contentType, 'Content-Length': buffer.length };
|
|
68
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
69
|
+
console.log(`[wyvrn] → PUT ${url}`);
|
|
70
|
+
const req = this._lib(url).request(
|
|
71
|
+
{ hostname: parsed.hostname, port: parsed.port || undefined, path: parsed.pathname, method: 'PUT', headers },
|
|
72
|
+
(res) => {
|
|
73
|
+
res.resume();
|
|
74
|
+
if (res.statusCode >= 200 && res.statusCode < 300) resolve();
|
|
75
|
+
else reject(new Error(`HTTP PUT ${url} failed: ${res.statusCode}`));
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
req.on('error', reject);
|
|
79
|
+
req.write(buffer);
|
|
80
|
+
req.end();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** HEAD check — returns true on 2xx. */
|
|
85
|
+
_head(url, token) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const parsed = new URL(url);
|
|
88
|
+
const headers = {};
|
|
89
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
90
|
+
const req = this._lib(url).request(
|
|
91
|
+
{ hostname: parsed.hostname, port: parsed.port || undefined, path: parsed.pathname, method: 'HEAD', headers },
|
|
92
|
+
(res) => { res.resume(); resolve(res.statusCode >= 200 && res.statusCode < 300); },
|
|
93
|
+
);
|
|
94
|
+
req.on('error', reject);
|
|
95
|
+
req.end();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* GET url → parsed JSON, or null on 404 / error.
|
|
101
|
+
* @returns {Promise<object|null>}
|
|
102
|
+
*/
|
|
103
|
+
_getJson(url, token) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const parsed = new URL(url);
|
|
106
|
+
const headers = { Accept: 'application/json' };
|
|
107
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
108
|
+
const req = this._lib(url).request(
|
|
109
|
+
{ hostname: parsed.hostname, port: parsed.port || undefined, path: parsed.pathname, method: 'GET', headers },
|
|
110
|
+
(res) => {
|
|
111
|
+
if (res.statusCode === 404) { res.resume(); resolve(null); return; }
|
|
112
|
+
if (res.statusCode < 200 || res.statusCode >= 300) { res.resume(); resolve(null); return; }
|
|
113
|
+
const chunks = [];
|
|
114
|
+
res.on('data', (c) => chunks.push(c));
|
|
115
|
+
res.on('end', () => {
|
|
116
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
|
|
117
|
+
catch { resolve(null); }
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
req.on('error', () => resolve(null));
|
|
122
|
+
req.end();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** GET url → stream download to destPath. Returns true on success. */
|
|
127
|
+
async _download(url, destPath, token, timeoutMs) {
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs ?? 300_000);
|
|
130
|
+
try {
|
|
131
|
+
const headers = {};
|
|
132
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
133
|
+
const response = await fetch(url, { signal: controller.signal, headers });
|
|
134
|
+
if (!response.ok) return false;
|
|
135
|
+
const fileStream = fs.createWriteStream(destPath);
|
|
136
|
+
await pipeline(Readable.fromWeb(response.body), fileStream);
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
} finally {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── v1 (legacy) API ───────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
async exists({ source, platform, name, version, token }) {
|
|
148
|
+
const base = source.replace(/\/$/, '');
|
|
149
|
+
return this._head(`${base}/${platform}/${name}/${version}/wyvrn.json`, token);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async publish(files, { source, platform, name, version, token }) {
|
|
153
|
+
const base = source.replace(/\/$/, '');
|
|
154
|
+
const urlBase = `${base}/${platform}/${name}/${version}`;
|
|
155
|
+
const packageBase = `${base}/${platform}/${name}`;
|
|
156
|
+
|
|
157
|
+
await this._put(`${urlBase}/wyvrn.json`, files.manifest, 'application/json', token);
|
|
158
|
+
await this._put(`${urlBase}/wyvrn.zip`, files.zip, 'application/zip', token);
|
|
159
|
+
await this._putBuffer(
|
|
160
|
+
`${packageBase}/latest.json`,
|
|
161
|
+
Buffer.from(JSON.stringify({ version })),
|
|
162
|
+
'application/json',
|
|
163
|
+
token,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── v2 API ────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
async v2Exists({ source, name, version, profileHash, token }) {
|
|
170
|
+
const base = source.replace(/\/$/, '');
|
|
171
|
+
return this._head(`${base}/v2/${name}/${version}/${profileHash}/wyvrn.json`, token);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async v2Publish(files, options) {
|
|
175
|
+
const {
|
|
176
|
+
source, name, version, profileHash, buildSettings,
|
|
177
|
+
contentSha256, gitSha, gitRepo, lockedDependencies, token,
|
|
178
|
+
} = options;
|
|
179
|
+
const base = source.replace(/\/$/, '');
|
|
180
|
+
const v2root = `${base}/v2/${name}`;
|
|
181
|
+
const buildRoot = `${v2root}/${version}/${profileHash}`;
|
|
182
|
+
|
|
183
|
+
// 1) Upload the binary zip
|
|
184
|
+
await this._put(`${buildRoot}/wyvrn.zip`, files.zip, 'application/zip', token);
|
|
185
|
+
|
|
186
|
+
// 2) Upload enriched wyvrn.json (original manifest + v2 metadata)
|
|
187
|
+
// buildSettings is stored for debugging purposes only — not for configuration.
|
|
188
|
+
// Dependencies are replaced with locked versions so the server never sees "latest".
|
|
189
|
+
const rawManifest = JSON.parse(fs.readFileSync(files.manifest, 'utf8'));
|
|
190
|
+
const pinnedDeps = pinDependencies(rawManifest.dependencies, lockedDependencies);
|
|
191
|
+
const v2Meta = {
|
|
192
|
+
...rawManifest,
|
|
193
|
+
...(pinnedDeps !== undefined ? { dependencies: pinnedDeps } : {}),
|
|
194
|
+
schemaVersion: 2,
|
|
195
|
+
profileHash,
|
|
196
|
+
buildSettings, // copy of the profile used — for debugging
|
|
197
|
+
contentSha256,
|
|
198
|
+
gitSha: gitSha ?? null,
|
|
199
|
+
gitRepo: gitRepo ?? null,
|
|
200
|
+
publishedAt: new Date().toISOString(),
|
|
201
|
+
};
|
|
202
|
+
await this._putBuffer(
|
|
203
|
+
`${buildRoot}/wyvrn.json`,
|
|
204
|
+
Buffer.from(JSON.stringify(v2Meta, null, 2)),
|
|
205
|
+
'application/json',
|
|
206
|
+
token,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// 3) Upload source.json (git metadata for build-from-source)
|
|
210
|
+
if (gitSha || gitRepo) {
|
|
211
|
+
const srcMeta = { gitRepo: gitRepo ?? null, gitSha: gitSha ?? null };
|
|
212
|
+
await this._putBuffer(
|
|
213
|
+
`${v2root}/${version}/source.json`,
|
|
214
|
+
Buffer.from(JSON.stringify(srcMeta, null, 2)),
|
|
215
|
+
'application/json',
|
|
216
|
+
token,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 4) Read-modify-write versions.json index (non-atomic, best-effort)
|
|
221
|
+
let versionsIdx = await this._getJson(`${v2root}/versions.json`, token) ?? {
|
|
222
|
+
name, latest: version, versions: {},
|
|
223
|
+
};
|
|
224
|
+
if (!versionsIdx.versions) versionsIdx.versions = {};
|
|
225
|
+
if (!versionsIdx.versions[version]) versionsIdx.versions[version] = { profiles: {} };
|
|
226
|
+
versionsIdx.versions[version].profiles[profileHash] = {
|
|
227
|
+
contentSha256,
|
|
228
|
+
publishedAt: new Date().toISOString(),
|
|
229
|
+
buildSettings,
|
|
230
|
+
};
|
|
231
|
+
if (gitSha || gitRepo) {
|
|
232
|
+
versionsIdx.versions[version].source = { gitRepo: gitRepo ?? null, gitSha: gitSha ?? null };
|
|
233
|
+
}
|
|
234
|
+
versionsIdx.latest = version;
|
|
235
|
+
await this._putBuffer(
|
|
236
|
+
`${v2root}/versions.json`,
|
|
237
|
+
Buffer.from(JSON.stringify(versionsIdx, null, 2)),
|
|
238
|
+
'application/json',
|
|
239
|
+
token,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// 5) Update v2 latest.json
|
|
243
|
+
await this._putBuffer(
|
|
244
|
+
`${v2root}/latest.json`,
|
|
245
|
+
Buffer.from(JSON.stringify({ version, profileHash, contentSha256 })),
|
|
246
|
+
'application/json',
|
|
247
|
+
token,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async v2GetMeta({ source, name, version, profileHash, token }) {
|
|
252
|
+
const base = source.replace(/\/$/, '');
|
|
253
|
+
return this._getJson(`${base}/v2/${name}/${version}/${profileHash}/wyvrn.json`, token);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async v2GetVersionsIndex({ source, name, token }) {
|
|
257
|
+
const base = source.replace(/\/$/, '');
|
|
258
|
+
return this._getJson(`${base}/v2/${name}/versions.json`, token);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async v2DownloadZip({ source, name, version, profileHash, token }, destPath, timeoutMs) {
|
|
262
|
+
const base = source.replace(/\/$/, '');
|
|
263
|
+
const url = `${base}/v2/${name}/${version}/${profileHash}/wyvrn.zip`;
|
|
264
|
+
return this._download(url, destPath, token, timeoutMs);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = HttpProvider;
|