wyvrnpm 1.3.2 → 2.0.2

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.
@@ -1,14 +1,28 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
+ const path = require('path');
4
5
  const BaseProvider = require('./base');
5
6
 
6
- // Matches: s3://bucket/...
7
- const RE_S3_URI = /^s3:\/\//;
8
- // Matches: https://bucket.s3.amazonaws.com/... or https://bucket.s3.us-east-1.amazonaws.com/...
7
+ const RE_S3_URI = /^s3:\/\//;
9
8
  const RE_S3_VIRTUAL_HOSTED = /^https?:\/\/([^.]+)\.s3[.-][^/]*\.amazonaws\.com/;
10
- // Matches: https://s3.amazonaws.com/bucket/... or https://s3.us-east-1.amazonaws.com/bucket/...
11
- const RE_S3_PATH_STYLE = /^https?:\/\/s3[.-][^/]*\.amazonaws\.com\/([^/]+)/;
9
+ const RE_S3_PATH_STYLE = /^https?:\/\/s3[.-][^/]*\.amazonaws\.com\/([^/]+)/;
10
+
11
+ function pinDependencies(rawDeps, lockedDeps) {
12
+ if (!rawDeps || !lockedDeps || Object.keys(lockedDeps).length === 0) return rawDeps;
13
+ const result = {};
14
+ for (const [pkgName, value] of Object.entries(rawDeps)) {
15
+ const locked = lockedDeps[pkgName];
16
+ if (locked) {
17
+ result[pkgName] = typeof value === 'object' && value !== null
18
+ ? { ...value, version: locked }
19
+ : locked;
20
+ } else {
21
+ result[pkgName] = value;
22
+ }
23
+ }
24
+ return result;
25
+ }
12
26
 
13
27
  class S3Provider extends BaseProvider {
14
28
  static canHandle(source) {
@@ -23,50 +37,101 @@ class S3Provider extends BaseProvider {
23
37
  return 'S3';
24
38
  }
25
39
 
26
- /** Parse source into { bucket, prefix }. prefix has no leading/trailing slashes. */
40
+ // ── helpers ──────────────────────────────────────────────────────────────
41
+
27
42
  _parseSource(source) {
28
43
  if (RE_S3_URI.test(source)) {
29
- const rest = source.slice(5); // strip "s3://"
44
+ const rest = source.slice(5);
30
45
  const slash = rest.indexOf('/');
31
46
  if (slash === -1) return { bucket: rest, prefix: '' };
32
- return {
33
- bucket: rest.slice(0, slash),
34
- prefix: rest.slice(slash + 1).replace(/\/$/, ''),
35
- };
47
+ return { bucket: rest.slice(0, slash), prefix: rest.slice(slash + 1).replace(/\/$/, '') };
36
48
  }
37
-
38
- const url = new URL(source);
49
+ const url = new URL(source);
39
50
  const vhMatch = url.hostname.match(/^([^.]+)\.s3/);
40
51
  if (vhMatch) {
41
52
  return { bucket: vhMatch[1], prefix: url.pathname.slice(1).replace(/\/$/, '') };
42
53
  }
43
-
44
- // path-style: first path segment is the bucket
45
54
  const parts = url.pathname.slice(1).split('/');
46
- return {
47
- bucket: parts[0],
48
- prefix: parts.slice(1).join('/').replace(/\/$/, ''),
49
- };
55
+ return { bucket: parts[0], prefix: parts.slice(1).join('/').replace(/\/$/, '') };
56
+ }
57
+
58
+ _key(prefix, ...parts) {
59
+ return [prefix, ...parts].filter(Boolean).join('/');
50
60
  }
51
61
 
52
- async exists(options) {
53
- let S3Client, HeadObjectCommand, fromSSO;
62
+ _loadSdk() {
54
63
  try {
55
- ({ S3Client, HeadObjectCommand } = require('@aws-sdk/client-s3'));
56
- ({ fromSSO } = require('@aws-sdk/credential-providers'));
64
+ const { S3Client, PutObjectCommand, HeadObjectCommand, GetObjectCommand } =
65
+ require('@aws-sdk/client-s3');
66
+ const { fromSSO } = require('@aws-sdk/credential-providers');
67
+ return { S3Client, PutObjectCommand, HeadObjectCommand, GetObjectCommand, fromSSO };
57
68
  } catch {
58
69
  throw new Error(
59
70
  'AWS SDK packages are required for S3 publishing.\n' +
60
71
  'Run: npm install @aws-sdk/client-s3 @aws-sdk/credential-providers',
61
72
  );
62
73
  }
74
+ }
63
75
 
64
- const { source, platform, name, version, profile } = options;
65
- const { bucket, prefix } = this._parseSource(source);
66
- const key = [prefix, platform, name, version, 'wyvrn.json'].filter(Boolean).join('/');
76
+ _makeClient(awsProfile) {
77
+ const { S3Client, fromSSO } = this._loadSdk();
78
+ return new S3Client(awsProfile ? { credentials: fromSSO({ profile: awsProfile }) } : {});
79
+ }
67
80
 
68
- const clientConfig = profile ? { credentials: fromSSO({ profile }) } : {};
69
- const client = new S3Client(clientConfig);
81
+ async _putFile(client, bucket, key, filePath, contentType, PutObjectCommand) {
82
+ console.log(`[wyvrn] → s3://${bucket}/${key}`);
83
+ await client.send(new PutObjectCommand({
84
+ Bucket: bucket, Key: key,
85
+ Body: fs.readFileSync(filePath),
86
+ ContentType: contentType,
87
+ }));
88
+ }
89
+
90
+ async _putBuffer(client, bucket, key, content, contentType, PutObjectCommand) {
91
+ console.log(`[wyvrn] → s3://${bucket}/${key}`);
92
+ await client.send(new PutObjectCommand({
93
+ Bucket: bucket, Key: key,
94
+ Body: content,
95
+ ContentType: contentType,
96
+ }));
97
+ }
98
+
99
+ async _getJson(client, bucket, key, GetObjectCommand) {
100
+ try {
101
+ const resp = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
102
+ const chunks = [];
103
+ for await (const chunk of resp.Body) chunks.push(chunk);
104
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
105
+ } catch (err) {
106
+ if (err.name === 'NoSuchKey' || err.$metadata?.httpStatusCode === 404) return null;
107
+ throw err;
108
+ }
109
+ }
110
+
111
+ async _downloadToFile(client, bucket, key, destPath, GetObjectCommand) {
112
+ try {
113
+ const resp = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
114
+ const out = fs.createWriteStream(destPath);
115
+ await new Promise((resolve, reject) => {
116
+ resp.Body.pipe(out);
117
+ out.on('finish', resolve);
118
+ out.on('error', reject);
119
+ resp.Body.on('error', reject);
120
+ });
121
+ return true;
122
+ } catch (err) {
123
+ if (err.name === 'NoSuchKey' || err.$metadata?.httpStatusCode === 404) return false;
124
+ throw err;
125
+ }
126
+ }
127
+
128
+ // ── v1 (legacy) API ───────────────────────────────────────────────────────
129
+
130
+ async exists({ source, platform, name, version, awsProfile }) {
131
+ const { HeadObjectCommand } = this._loadSdk();
132
+ const client = this._makeClient(awsProfile);
133
+ const { bucket, prefix } = this._parseSource(source);
134
+ const key = this._key(prefix, platform, name, version, 'wyvrn.json');
70
135
  try {
71
136
  await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
72
137
  return true;
@@ -76,61 +141,133 @@ class S3Provider extends BaseProvider {
76
141
  }
77
142
  }
78
143
 
79
- async publish(files, options) {
80
- // Lazy-load AWS SDK so the tool works without it when only HTTP is used.
81
- let S3Client, PutObjectCommand, fromSSO;
144
+ async publish(files, { source, platform, name, version, awsProfile }) {
145
+ const { PutObjectCommand } = this._loadSdk();
146
+ const client = this._makeClient(awsProfile);
147
+ const { bucket, prefix } = this._parseSource(source);
148
+
149
+ const keyBase = this._key(prefix, platform, name, version);
150
+ const packageBase = this._key(prefix, platform, name);
151
+
152
+ await this._putFile(client, bucket, `${keyBase}/wyvrn.json`, files.manifest, 'application/json', PutObjectCommand);
153
+ await this._putFile(client, bucket, `${keyBase}/wyvrn.zip`, files.zip, 'application/zip', PutObjectCommand);
154
+ await this._putBuffer(
155
+ client, bucket, `${packageBase}/latest.json`,
156
+ Buffer.from(JSON.stringify({ version })), 'application/json', PutObjectCommand,
157
+ );
158
+ }
159
+
160
+ // ── v2 API ────────────────────────────────────────────────────────────────
161
+
162
+ async v2Exists({ source, name, version, profileHash, awsProfile }) {
163
+ const { HeadObjectCommand } = this._loadSdk();
164
+ const client = this._makeClient(awsProfile);
165
+ const { bucket, prefix } = this._parseSource(source);
166
+ const key = this._key(prefix, 'v2', name, version, profileHash, 'wyvrn.json');
82
167
  try {
83
- ({ S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'));
84
- ({ fromSSO } = require('@aws-sdk/credential-providers'));
85
- } catch {
86
- throw new Error(
87
- 'AWS SDK packages are required for S3 publishing.\n' +
88
- 'Run: npm install @aws-sdk/client-s3 @aws-sdk/credential-providers',
89
- );
168
+ await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
169
+ return true;
170
+ } catch (err) {
171
+ if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) return false;
172
+ throw err;
90
173
  }
174
+ }
91
175
 
92
- const { source, platform, name, version, profile } = options;
176
+ async v2Publish(files, options) {
177
+ const {
178
+ source, name, version, profileHash, buildSettings,
179
+ contentSha256, gitSha, gitRepo, lockedDependencies, awsProfile,
180
+ } = options;
181
+ const { PutObjectCommand, GetObjectCommand } = this._loadSdk();
182
+ const client = this._makeClient(awsProfile);
93
183
  const { bucket, prefix } = this._parseSource(source);
94
184
 
95
- // Destination key base: [prefix/]platform/name/version
96
- const keyBase = [prefix, platform, name, version].filter(Boolean).join('/');
97
- // Package root (without version) for latest.json
98
- const packageBase = [prefix, platform, name].filter(Boolean).join('/');
185
+ const v2root = this._key(prefix, 'v2', name);
186
+ const buildRoot = `${v2root}/${version}/${profileHash}`;
99
187
 
100
- const clientConfig = profile ? { credentials: fromSSO({ profile }) } : {};
101
- const client = new S3Client(clientConfig);
188
+ // 1) Upload zip
189
+ await this._putFile(client, bucket, `${buildRoot}/wyvrn.zip`, files.zip, 'application/zip', PutObjectCommand);
102
190
 
103
- await this._put(client, bucket, `${keyBase}/wyvrn.json`, files.manifest, 'application/json', PutObjectCommand);
104
- await this._put(client, bucket, `${keyBase}/wyvrn.zip`, files.zip, 'application/zip', PutObjectCommand);
191
+ // 2) Upload enriched wyvrn.json
192
+ // buildSettings is stored for debugging purposes only.
193
+ // Dependencies are replaced with locked versions so the server never sees "latest".
194
+ const rawManifest = JSON.parse(fs.readFileSync(files.manifest, 'utf8'));
195
+ const pinnedDeps = pinDependencies(rawManifest.dependencies, lockedDependencies);
196
+ const v2Meta = {
197
+ ...rawManifest,
198
+ ...(pinnedDeps !== undefined ? { dependencies: pinnedDeps } : {}),
199
+ schemaVersion: 2,
200
+ profileHash,
201
+ buildSettings,
202
+ contentSha256,
203
+ gitSha: gitSha ?? null,
204
+ gitRepo: gitRepo ?? null,
205
+ publishedAt: new Date().toISOString(),
206
+ };
207
+ await this._putBuffer(
208
+ client, bucket, `${buildRoot}/wyvrn.json`,
209
+ Buffer.from(JSON.stringify(v2Meta, null, 2)), 'application/json', PutObjectCommand,
210
+ );
105
211
 
106
- // Write latest.json at the package root so consumers can resolve "latest"
107
- const latestContent = Buffer.from(JSON.stringify({ version }));
108
- await this._putBuffer(client, bucket, `${packageBase}/latest.json`, latestContent, 'application/json', PutObjectCommand);
109
- }
212
+ // 3) source.json
213
+ if (gitSha || gitRepo) {
214
+ await this._putBuffer(
215
+ client, bucket, `${v2root}/${version}/source.json`,
216
+ Buffer.from(JSON.stringify({ gitRepo: gitRepo ?? null, gitSha: gitSha ?? null }, null, 2)),
217
+ 'application/json', PutObjectCommand,
218
+ );
219
+ }
110
220
 
111
- async _put(client, bucket, key, filePath, contentType, PutObjectCommand) {
112
- console.log(`[wyvrn] → s3://${bucket}/${key}`);
113
- await client.send(
114
- new PutObjectCommand({
115
- Bucket: bucket,
116
- Key: key,
117
- Body: fs.readFileSync(filePath),
118
- ContentType: contentType,
119
- }),
221
+ // 4) Read-modify-write versions.json
222
+ let versionsIdx = await this._getJson(client, bucket, `${v2root}/versions.json`, GetObjectCommand)
223
+ ?? { name, latest: version, versions: {} };
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
+ client, bucket, `${v2root}/versions.json`,
237
+ Buffer.from(JSON.stringify(versionsIdx, null, 2)), 'application/json', PutObjectCommand,
120
238
  );
121
- }
122
239
 
123
- async _putBuffer(client, bucket, key, content, contentType, PutObjectCommand) {
124
- console.log(`[wyvrn] → s3://${bucket}/${key}`);
125
- await client.send(
126
- new PutObjectCommand({
127
- Bucket: bucket,
128
- Key: key,
129
- Body: content,
130
- ContentType: contentType,
131
- }),
240
+ // 5) latest.json
241
+ await this._putBuffer(
242
+ client, bucket, `${v2root}/latest.json`,
243
+ Buffer.from(JSON.stringify({ version, profileHash, contentSha256 })),
244
+ 'application/json', PutObjectCommand,
132
245
  );
133
246
  }
247
+
248
+ async v2GetMeta({ source, name, version, profileHash, awsProfile }) {
249
+ const { GetObjectCommand } = this._loadSdk();
250
+ const client = this._makeClient(awsProfile);
251
+ const { bucket, prefix } = this._parseSource(source);
252
+ const key = this._key(prefix, 'v2', name, version, profileHash, 'wyvrn.json');
253
+ return this._getJson(client, bucket, key, GetObjectCommand);
254
+ }
255
+
256
+ async v2GetVersionsIndex({ source, name, awsProfile }) {
257
+ const { GetObjectCommand } = this._loadSdk();
258
+ const client = this._makeClient(awsProfile);
259
+ const { bucket, prefix } = this._parseSource(source);
260
+ const key = this._key(prefix, 'v2', name, 'versions.json');
261
+ return this._getJson(client, bucket, key, GetObjectCommand);
262
+ }
263
+
264
+ async v2DownloadZip({ source, name, version, profileHash, awsProfile }, destPath) {
265
+ const { GetObjectCommand } = this._loadSdk();
266
+ const client = this._makeClient(awsProfile);
267
+ const { bucket, prefix } = this._parseSource(source);
268
+ const key = this._key(prefix, 'v2', name, version, profileHash, 'wyvrn.zip');
269
+ return this._downloadToFile(client, bucket, key, destPath, GetObjectCommand);
270
+ }
134
271
  }
135
272
 
136
273
  module.exports = S3Provider;
package/src/resolve.js CHANGED
@@ -73,9 +73,12 @@ function compareVersions(a, b) {
73
73
  * @returns {Promise<string>} The resolved version string.
74
74
  */
75
75
  async function resolveLatestVersion(name, packageSources, platform, httpClient) {
76
+ let best = null;
77
+
76
78
  for (const source of packageSources) {
77
79
  const base = source.replace(/\/$/, '');
78
80
  const candidates = [
81
+ `${base}/v2/${name}/latest.json`,
79
82
  `${base}/${platform}/${name}/latest.json`,
80
83
  `${base}/common/${name}/latest.json`,
81
84
  ];
@@ -84,12 +87,18 @@ async function resolveLatestVersion(name, packageSources, platform, httpClient)
84
87
  const response = await httpClient(url);
85
88
  if (!response.ok) continue;
86
89
  const data = await response.json();
87
- if (data && data.version) return data.version;
90
+ if (data?.version) {
91
+ if (!best || compareVersions(data.version, best) > 0) {
92
+ best = data.version;
93
+ }
94
+ }
88
95
  } catch {
89
96
  // Try next candidate.
90
97
  }
91
98
  }
92
99
  }
100
+
101
+ if (best) return best;
93
102
  throw new Error(`Could not resolve "latest" version for "${name}" from any source`);
94
103
  }
95
104
 
@@ -229,8 +238,9 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
229
238
  }
230
239
 
231
240
  // Support both formats:
232
- // PascalCase array: { Dependencies: [ { Name, Version }, ... ] }
233
- // lowercase object: { dependencies: { name: version, ... } }
241
+ // PascalCase array : { Dependencies: [ { Name, Version }, ... ] }
242
+ // string object : { dependencies: { name: "version", ... } }
243
+ // object object : { dependencies: { name: { version, settings }, ... } }
234
244
  let deps = [];
235
245
  if (Array.isArray(manifest.Dependencies)) {
236
246
  deps = manifest.Dependencies.filter((d) => d.Name && d.Version).map((d) => ({
@@ -238,10 +248,14 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
238
248
  version: d.Version,
239
249
  }));
240
250
  } else if (manifest.dependencies && typeof manifest.dependencies === 'object') {
241
- deps = Object.entries(manifest.dependencies).map(([name, version]) => ({ name, version }));
251
+ deps = Object.entries(manifest.dependencies).map(([depName, depValue]) => ({
252
+ name: depName,
253
+ // Handle both "1.0.0" and { version: "1.0.0", settings: {...} }
254
+ version: typeof depValue === 'string' ? depValue : (depValue?.version ?? depValue?.Version ?? ''),
255
+ }));
242
256
  }
243
257
  for (const dep of deps) {
244
- queue.push({ name: dep.name, version: dep.version });
258
+ if (dep.version) queue.push({ name: dep.name, version: dep.version });
245
259
  }
246
260
  }
247
261