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.
@@ -1,136 +1,273 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const BaseProvider = require('./base');
5
-
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/...
9
- 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\/([^/]+)/;
12
-
13
- class S3Provider extends BaseProvider {
14
- static canHandle(source) {
15
- return (
16
- RE_S3_URI.test(source) ||
17
- RE_S3_VIRTUAL_HOSTED.test(source) ||
18
- RE_S3_PATH_STYLE.test(source)
19
- );
20
- }
21
-
22
- static get providerName() {
23
- return 'S3';
24
- }
25
-
26
- /** Parse source into { bucket, prefix }. prefix has no leading/trailing slashes. */
27
- _parseSource(source) {
28
- if (RE_S3_URI.test(source)) {
29
- const rest = source.slice(5); // strip "s3://"
30
- const slash = rest.indexOf('/');
31
- if (slash === -1) return { bucket: rest, prefix: '' };
32
- return {
33
- bucket: rest.slice(0, slash),
34
- prefix: rest.slice(slash + 1).replace(/\/$/, ''),
35
- };
36
- }
37
-
38
- const url = new URL(source);
39
- const vhMatch = url.hostname.match(/^([^.]+)\.s3/);
40
- if (vhMatch) {
41
- return { bucket: vhMatch[1], prefix: url.pathname.slice(1).replace(/\/$/, '') };
42
- }
43
-
44
- // path-style: first path segment is the bucket
45
- const parts = url.pathname.slice(1).split('/');
46
- return {
47
- bucket: parts[0],
48
- prefix: parts.slice(1).join('/').replace(/\/$/, ''),
49
- };
50
- }
51
-
52
- async exists(options) {
53
- let S3Client, HeadObjectCommand, fromSSO;
54
- try {
55
- ({ S3Client, HeadObjectCommand } = require('@aws-sdk/client-s3'));
56
- ({ fromSSO } = require('@aws-sdk/credential-providers'));
57
- } catch {
58
- throw new Error(
59
- 'AWS SDK packages are required for S3 publishing.\n' +
60
- 'Run: npm install @aws-sdk/client-s3 @aws-sdk/credential-providers',
61
- );
62
- }
63
-
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('/');
67
-
68
- const clientConfig = profile ? { credentials: fromSSO({ profile }) } : {};
69
- const client = new S3Client(clientConfig);
70
- try {
71
- await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
72
- return true;
73
- } catch (err) {
74
- if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) return false;
75
- throw err;
76
- }
77
- }
78
-
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;
82
- 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
- );
90
- }
91
-
92
- const { source, platform, name, version, profile } = options;
93
- const { bucket, prefix } = this._parseSource(source);
94
-
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('/');
99
-
100
- const clientConfig = profile ? { credentials: fromSSO({ profile }) } : {};
101
- const client = new S3Client(clientConfig);
102
-
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);
105
-
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
- }
110
-
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
- }),
120
- );
121
- }
122
-
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
- }),
132
- );
133
- }
134
- }
135
-
136
- module.exports = S3Provider;
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const BaseProvider = require('./base');
6
+
7
+ const RE_S3_URI = /^s3:\/\//;
8
+ const RE_S3_VIRTUAL_HOSTED = /^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
+ }
26
+
27
+ class S3Provider extends BaseProvider {
28
+ static canHandle(source) {
29
+ return (
30
+ RE_S3_URI.test(source) ||
31
+ RE_S3_VIRTUAL_HOSTED.test(source) ||
32
+ RE_S3_PATH_STYLE.test(source)
33
+ );
34
+ }
35
+
36
+ static get providerName() {
37
+ return 'S3';
38
+ }
39
+
40
+ // ── helpers ──────────────────────────────────────────────────────────────
41
+
42
+ _parseSource(source) {
43
+ if (RE_S3_URI.test(source)) {
44
+ const rest = source.slice(5);
45
+ const slash = rest.indexOf('/');
46
+ if (slash === -1) return { bucket: rest, prefix: '' };
47
+ return { bucket: rest.slice(0, slash), prefix: rest.slice(slash + 1).replace(/\/$/, '') };
48
+ }
49
+ const url = new URL(source);
50
+ const vhMatch = url.hostname.match(/^([^.]+)\.s3/);
51
+ if (vhMatch) {
52
+ return { bucket: vhMatch[1], prefix: url.pathname.slice(1).replace(/\/$/, '') };
53
+ }
54
+ const parts = url.pathname.slice(1).split('/');
55
+ return { bucket: parts[0], prefix: parts.slice(1).join('/').replace(/\/$/, '') };
56
+ }
57
+
58
+ _key(prefix, ...parts) {
59
+ return [prefix, ...parts].filter(Boolean).join('/');
60
+ }
61
+
62
+ _loadSdk() {
63
+ try {
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 };
68
+ } catch {
69
+ throw new Error(
70
+ 'AWS SDK packages are required for S3 publishing.\n' +
71
+ 'Run: npm install @aws-sdk/client-s3 @aws-sdk/credential-providers',
72
+ );
73
+ }
74
+ }
75
+
76
+ _makeClient(awsProfile) {
77
+ const { S3Client, fromSSO } = this._loadSdk();
78
+ return new S3Client(awsProfile ? { credentials: fromSSO({ profile: awsProfile }) } : {});
79
+ }
80
+
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');
135
+ try {
136
+ await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
137
+ return true;
138
+ } catch (err) {
139
+ if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) return false;
140
+ throw err;
141
+ }
142
+ }
143
+
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');
167
+ try {
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;
173
+ }
174
+ }
175
+
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);
183
+ const { bucket, prefix } = this._parseSource(source);
184
+
185
+ const v2root = this._key(prefix, 'v2', name);
186
+ const buildRoot = `${v2root}/${version}/${profileHash}`;
187
+
188
+ // 1) Upload zip
189
+ await this._putFile(client, bucket, `${buildRoot}/wyvrn.zip`, files.zip, 'application/zip', PutObjectCommand);
190
+
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
+ );
211
+
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
+ }
220
+
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,
238
+ );
239
+
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,
245
+ );
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
+ }
271
+ }
272
+
273
+ module.exports = S3Provider;