@logbrew/sdk 0.1.38 → 0.1.39

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 (90) hide show
  1. package/automatic-delivery.cjs +271 -0
  2. package/batch-sender.cjs +164 -0
  3. package/bounded-number.cjs +34 -0
  4. package/browser-breadcrumbs.cjs +346 -136
  5. package/capture-support.cjs +100 -0
  6. package/client-options.cjs +292 -0
  7. package/client-report.cjs +232 -0
  8. package/client.cjs +447 -0
  9. package/console-arguments.cjs +151 -0
  10. package/console-capture.cjs +194 -0
  11. package/core.cjs +30 -2810
  12. package/delivery-health.cjs +133 -0
  13. package/error-causes.cjs +130 -0
  14. package/error-details.cjs +124 -0
  15. package/error-issue.cjs +187 -0
  16. package/event-clone.cjs +55 -0
  17. package/event-metadata.cjs +40 -0
  18. package/event-pipeline.cjs +325 -0
  19. package/event-queue.cjs +306 -0
  20. package/event-store.cjs +237 -0
  21. package/event-validation.cjs +159 -0
  22. package/examples/agent-timeline.cjs +57 -46
  23. package/examples/agent-timeline.mjs +41 -35
  24. package/examples/index.mjs +122 -94
  25. package/examples/readme-example.cjs +36 -27
  26. package/examples/readme-example.mjs +21 -17
  27. package/examples/real-user-smoke.cjs +41 -32
  28. package/examples/real-user-smoke.mjs +26 -22
  29. package/exception-chain.cjs +205 -0
  30. package/index.d.ts +144 -1076
  31. package/index.js +1 -1
  32. package/issue-exception.cjs +13 -8
  33. package/metric-validation.cjs +95 -0
  34. package/network-milestone.cjs +155 -0
  35. package/null-value.cjs +4 -0
  36. package/opentelemetry-bridge.cjs +330 -0
  37. package/opentelemetry-context.cjs +245 -0
  38. package/opentelemetry-exceptions.cjs +69 -0
  39. package/opentelemetry-keys.cjs +183 -0
  40. package/opentelemetry-span.cjs +286 -0
  41. package/opentelemetry-summary.cjs +213 -0
  42. package/opentelemetry-time.cjs +131 -0
  43. package/opentelemetry.cjs +99 -1068
  44. package/package.json +55 -1
  45. package/pino-destination.cjs +108 -0
  46. package/pino-record.cjs +237 -0
  47. package/product-action.cjs +163 -0
  48. package/provided-breadcrumbs.cjs +117 -0
  49. package/random-hex.cjs +29 -0
  50. package/react-native.js +1 -1
  51. package/recording-transport.cjs +68 -0
  52. package/release-artifacts-build.cjs +263 -84
  53. package/release-artifacts-common.js +347 -66
  54. package/release-artifacts-debug-id.js +178 -0
  55. package/release-artifacts-inspect.js +213 -0
  56. package/release-artifacts-issue-event.js +99 -0
  57. package/release-artifacts-manifest-artifact.js +107 -0
  58. package/release-artifacts-manifest.js +77 -0
  59. package/release-artifacts-mappings.js +265 -0
  60. package/release-artifacts-prepare.js +297 -0
  61. package/release-artifacts-source-context.js +210 -0
  62. package/release-artifacts-symbolication.js +272 -421
  63. package/release-artifacts-upload-files.js +122 -0
  64. package/release-artifacts-upload-request.js +246 -0
  65. package/release-artifacts-upload.js +155 -300
  66. package/release-artifacts-urls.js +49 -0
  67. package/release-artifacts.js +189 -662
  68. package/retry-delay.cjs +32 -0
  69. package/span-validation.cjs +191 -0
  70. package/stored-events.cjs +243 -0
  71. package/types/client.d.ts +67 -0
  72. package/types/delivery.d.ts +125 -0
  73. package/types/issues.d.ts +249 -0
  74. package/types/logging.d.ts +175 -0
  75. package/types/opentelemetry.d.ts +205 -0
  76. package/types/product.d.ts +128 -0
  77. package/types/recording-transport.d.ts +15 -0
  78. package/types/sdk-error.d.ts +9 -0
  79. package/types/telemetry.d.ts +171 -0
  80. package/types/trace-context.d.ts +110 -0
  81. package/types/transport-error.d.ts +10 -0
  82. package/utf8.cjs +37 -0
  83. package/validation.cjs +49 -1
  84. package/vite-release-artifacts-config.cjs +120 -0
  85. package/vite-release-artifacts.cjs +199 -99
  86. package/vite-release-artifacts.d.cts +65 -40
  87. package/vite-release-artifacts.d.ts +65 -40
  88. package/vite-release-artifacts.js +2 -2
  89. package/winston-info.cjs +205 -0
  90. package/winston.cjs +189 -289
@@ -1,337 +1,192 @@
1
- /* global AbortController, clearTimeout */
2
-
3
- import { Buffer } from "node:buffer";
4
- import crypto from "node:crypto";
5
- import fs from "node:fs";
6
- import net from "node:net";
7
- import path from "node:path";
8
-
9
1
  import {
10
- byteSize,
11
- normalizeProjectId,
2
+ encodeMultipart,
3
+ endpointWithoutQuery,
4
+ requireUploadEndpoint,
5
+ uploadWithRetries
6
+ } from "./release-artifacts-upload-request.js";
7
+ import {
8
+ errorMessage,
12
9
  parseOptions,
10
+ pathExists,
13
11
  printJson,
14
12
  readJsonObject,
15
- requireOption,
16
13
  requireBuildDir,
17
- safeResolve,
18
- sha256File,
19
- stableJson
14
+ requireOption
20
15
  } from "./release-artifacts-common.js";
16
+ import { collectUploadFiles } from "./release-artifacts-upload-files.js";
17
+ import path from "node:path";
18
+
19
+ /**
20
+ * @import { CliOptions, JsonObject } from "./release-artifacts-common.js"
21
+ * @import { UploadFile } from "./release-artifacts-upload-request.js"
22
+ * @typedef {{ readonly endpoint: string, readonly files: readonly UploadFile[], readonly manifest: JsonObject, readonly report: JsonObject }} PreparedUpload
23
+ */
21
24
 
22
25
  const DEFAULT_UPLOAD_TOKEN_ENV = "LOGBREW_RELEASE_ARTIFACT_TOKEN";
23
- const HOSTED_UPLOAD_ENDPOINT = "https://api.logbrew.co/api/release-artifacts";
24
- const NON_RETRYABLE_UPLOAD_STATUSES = new Set([400, 401, 403, 413]);
25
- const RETRYABLE_UPLOAD_STATUSES = new Set([408, 429]);
26
26
  const SCRIPT_VERSION = "0.1.0";
27
-
27
+ const NO_RETRIES = 0;
28
+ const DECIMAL = 10;
29
+ const NON_NEGATIVE_INTEGER_RE = /^\d+$/u;
30
+ const UPLOADER = { name: "logbrew-js-release-artifact-upload-verifier", version: SCRIPT_VERSION };
31
+ const EXIT_OK = 0;
32
+ const EXIT_AUTH_MISSING = 2;
33
+ const EXIT_AUTH_FAILED = 3;
34
+ const EXIT_VALIDATION_FAILED = 4;
35
+ const EXIT_CODES = new Map([
36
+ ["auth_failed", EXIT_AUTH_FAILED],
37
+ ["auth_missing", EXIT_AUTH_MISSING],
38
+ ["dry_run", EXIT_OK],
39
+ ["uploaded", EXIT_OK],
40
+ ["validation_failed", EXIT_VALIDATION_FAILED]
41
+ ]);
42
+ const UNKNOWN_FAILURE_EXIT_CODE = 5;
43
+
44
+ /**
45
+ * @param {string} value
46
+ * @param {string} label
47
+ * @returns {number}
48
+ */
28
49
  function parseNonNegativeInteger(value, label) {
29
50
  const trimmed = value.trim();
30
- if (!/^\d+$/u.test(trimmed)) {
51
+ if (!NON_NEGATIVE_INTEGER_RE.test(trimmed)) {
31
52
  throw new Error(`${label} must be a non-negative integer`);
32
53
  }
33
- return Number.parseInt(trimmed, 10);
54
+ return Number.parseInt(trimmed, DECIMAL);
34
55
  }
35
56
 
57
+ /**
58
+ * @param {string} value
59
+ * @param {string} label
60
+ * @returns {number}
61
+ */
36
62
  function parseNonNegativeNumber(value, label) {
37
- const trimmed = value.trim();
38
- const parsed = Number(trimmed);
39
- if (!Number.isFinite(parsed) || parsed < 0) {
63
+ const parsed = Number(value.trim());
64
+ if (!Number.isFinite(parsed) || parsed < NO_RETRIES) {
40
65
  throw new Error(`${label} must be a non-negative number`);
41
66
  }
42
67
  return parsed;
43
68
  }
44
69
 
45
- function endpointWithoutQuery(endpoint) {
46
- const parsed = new URL(endpoint);
47
- return `${parsed.protocol}//${parsed.host}${parsed.pathname || "/"}`;
48
- }
49
-
50
- function parseEndpoint(endpoint) {
51
- let parsed;
52
- try {
53
- parsed = new URL(endpoint);
54
- } catch (error) {
55
- throw new Error(`upload endpoint is not a valid URL: ${error.message}`, { cause: error });
56
- }
57
- if (!["http:", "https:"].includes(parsed.protocol)) {
58
- throw new Error("release artifact upload proof endpoint must use http or https");
59
- }
60
- return parsed;
61
- }
62
-
63
- function isLoopbackEndpoint(parsed) {
64
- const hostname = parsed.hostname.toLowerCase();
65
- return (
66
- hostname === "localhost" ||
67
- hostname === "[::1]" ||
68
- hostname === "::1" ||
69
- (net.isIP(hostname) !== 0 && (hostname.startsWith("127.") || hostname === "::1"))
70
- );
71
- }
72
-
73
- function requireUploadEndpoint(endpoint, allowHosted) {
74
- const parsed = parseEndpoint(endpoint);
75
- if (isLoopbackEndpoint(parsed)) {
76
- return parsed;
77
- }
78
- if (!allowHosted) {
79
- throw new Error("release artifact hosted upload requires explicit --allow-hosted; use loopback endpoints for local proof");
80
- }
81
- if (endpoint !== HOSTED_UPLOAD_ENDPOINT) {
82
- throw new Error(`hosted release artifact uploads must use ${HOSTED_UPLOAD_ENDPOINT}`);
83
- }
84
- return parsed;
85
- }
86
-
87
- function quoteMultipartValue(value) {
88
- return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
89
- }
90
-
91
- function encodeMultipart(manifest, files) {
92
- const boundary = `logbrew-${crypto.randomUUID().replaceAll("-", "")}`;
93
- const chunks = [];
94
- const appendPart = (name, filename, contentType, bytes) => {
95
- chunks.push(Buffer.from(`--${boundary}\r\n`, "ascii"));
96
- chunks.push(
97
- Buffer.from(
98
- `Content-Disposition: form-data; name="${quoteMultipartValue(name)}"; filename="${quoteMultipartValue(filename)}"\r\n`,
99
- "utf8"
100
- )
101
- );
102
- chunks.push(Buffer.from(`Content-Type: ${contentType}\r\n\r\n`, "ascii"));
103
- chunks.push(Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes));
104
- chunks.push(Buffer.from("\r\n", "ascii"));
105
- };
106
-
107
- appendPart("manifest", "manifest.json", "application/json", Buffer.from(stableJson(manifest), "utf8"));
108
- for (const [name, filePath] of files) {
109
- appendPart(name, path.basename(filePath), "application/octet-stream", fs.readFileSync(filePath));
110
- }
111
- chunks.push(Buffer.from(`--${boundary}--\r\n`, "ascii"));
112
- return { body: Buffer.concat(chunks), boundary };
113
- }
114
-
115
- function classifyUploadStatus(status) {
116
- if (status >= 200 && status < 300) {
117
- return "uploaded";
118
- }
119
- if (status === 401 || status === 403) {
120
- return "auth_failed";
121
- }
122
- if (NON_RETRYABLE_UPLOAD_STATUSES.has(status)) {
123
- return "validation_failed";
124
- }
125
- if (RETRYABLE_UPLOAD_STATUSES.has(status) || status >= 500) {
126
- return "retryable_error";
127
- }
128
- return "upload_failed";
129
- }
130
-
131
- function sleep(seconds) {
132
- return new Promise((resolve) => {
133
- setTimeout(resolve, seconds * 1000);
134
- });
135
- }
136
-
137
- async function postMultipart(endpoint, token, body, boundary, timeoutSeconds) {
138
- const controller = new AbortController();
139
- const timeout = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
140
- try {
141
- const response = await fetch(endpoint, {
142
- method: "POST",
143
- headers: {
144
- Authorization: `Bearer ${token}`,
145
- "Content-Type": `multipart/form-data; boundary=${boundary}`,
146
- "User-Agent": `logbrew-release-artifact-verifier/${SCRIPT_VERSION}`
147
- },
148
- body,
149
- signal: controller.signal
150
- });
151
- await response.arrayBuffer();
152
- return response.status;
153
- } finally {
154
- clearTimeout(timeout);
155
- }
156
- }
157
-
158
- async function uploadWithRetries({ endpoint, token, body, boundary, maxRetries, retryDelaySeconds, timeoutSeconds }) {
159
- const attempts = [];
160
- for (let attempt = 1; attempt <= maxRetries + 1; attempt += 1) {
161
- let result;
162
- try {
163
- const httpStatus = await postMultipart(endpoint, token, body, boundary, timeoutSeconds);
164
- result = classifyUploadStatus(httpStatus);
165
- attempts.push({ attempt, httpStatus, result });
166
- } catch {
167
- result = "retryable_error";
168
- attempts.push({ attempt, result });
169
- }
170
-
171
- if (result === "uploaded") {
172
- break;
173
- }
174
- if (result !== "retryable_error" || attempt > maxRetries) {
175
- break;
176
- }
177
- if (retryDelaySeconds > 0) {
178
- await sleep(retryDelaySeconds);
179
- }
180
- }
181
- const finalResult = attempts.at(-1)?.result ?? "upload_failed";
70
+ /**
71
+ * @param {string} status
72
+ * @returns {number}
73
+ */
74
+ function exitCodeForUploadStatus(status) {
75
+ return EXIT_CODES.get(status) ?? UNKNOWN_FAILURE_EXIT_CODE;
76
+ }
77
+
78
+ /**
79
+ * @param {CliOptions} options
80
+ * @param {string} name
81
+ * @param {string} fallback
82
+ * @returns {string}
83
+ */
84
+ function optionOrDefault(options, name, fallback) {
85
+ const value = options[name];
86
+ return typeof value === "string" ? value : fallback;
87
+ }
88
+
89
+ /**
90
+ * @param {CliOptions} options
91
+ * @returns {Promise<PreparedUpload>}
92
+ */
93
+ async function prepareUpload(options) {
94
+ const endpoint = requireOption(options, "endpoint");
95
+ const loopback = requireUploadEndpoint(endpoint, options["allow-hosted"] === true);
96
+ const buildDir = await requireBuildDir(requireOption(options, "build-dir"));
97
+ const manifestPath = path.resolve(requireOption(options, "manifest"));
98
+ if (!(await pathExists(manifestPath))) {
99
+ throw new Error(`manifest file does not exist: ${String(options["manifest"])}`);
100
+ }
101
+ const manifest = await readJsonObject(manifestPath, "manifest");
102
+ const { artifactCount, files } = await collectUploadFiles(manifest, buildDir, !loopback);
182
103
  return {
183
- status: finalResult,
184
- attempts,
185
- retryCount: Math.max(0, attempts.length - 1)
186
- };
187
- }
188
-
189
- function requireReadyJavaScriptManifest(manifest, requireProjectId) {
190
- if (manifest.artifactType !== "javascript_source_map_manifest") {
191
- throw new Error("only javascript_source_map_manifest uploads are supported by this verifier");
192
- }
193
- if (manifest.validation?.status !== "ready") {
194
- throw new Error("manifest validation status must be ready before upload");
195
- }
196
- if (!Array.isArray(manifest.artifacts) || manifest.artifacts.length === 0) {
197
- throw new Error("manifest must contain at least one JavaScript release artifact");
198
- }
199
- normalizeProjectId(
200
- manifest.projectId,
201
- requireProjectId
202
- ? "hosted release artifact uploads require manifest projectId as a UUID"
203
- : "manifest projectId must be a UUID when provided",
204
- requireProjectId,
205
- );
206
- }
207
-
208
- function requireArtifactFile(artifact, buildDir, section, requiredFields) {
209
- const payload = artifact[section];
210
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
211
- throw new Error(`artifact is missing ${section}`);
212
- }
213
- for (const field of requiredFields) {
214
- if (payload[field] === undefined || payload[field] === "") {
215
- throw new Error(`${section} is missing ${field}`);
216
- }
217
- }
218
- const filePath = safeResolve(path.join(buildDir, String(payload.path)), buildDir);
219
- if (!filePath || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
220
- throw new Error(`${section} file is missing: ${payload.path}`);
221
- }
222
- if (byteSize(filePath) !== Number(payload.byteSize)) {
223
- throw new Error(`${section} byte size changed after manifest creation: ${payload.path}`);
224
- }
225
- if (sha256File(filePath) !== String(payload.artifactSha256)) {
226
- throw new Error(`${section} sha256 changed after manifest creation: ${payload.path}`);
227
- }
228
- return filePath;
229
- }
230
-
231
- function collectUploadFiles(manifest, buildDir, requireProjectId) {
232
- requireReadyJavaScriptManifest(manifest, requireProjectId);
233
- const files = [];
234
- for (const [index, artifact] of manifest.artifacts.entries()) {
235
- if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) {
236
- throw new Error("artifact entries must be JSON objects");
104
+ endpoint,
105
+ files,
106
+ manifest,
107
+ report: {
108
+ artifactCount,
109
+ artifactType: manifest["artifactType"],
110
+ dryRun: options["dry-run"] === true,
111
+ endpoint: endpointWithoutQuery(endpoint),
112
+ environment: manifest["environment"],
113
+ filePartCount: files.length,
114
+ release: manifest["release"],
115
+ service: manifest["service"],
116
+ uploader: UPLOADER
237
117
  }
238
- files.push([
239
- `minified_source_${index}`,
240
- requireArtifactFile(artifact, buildDir, "minifiedSource", ["path", "artifactSha256", "byteSize"])
241
- ]);
242
- files.push([
243
- `source_map_${index}`,
244
- requireArtifactFile(artifact, buildDir, "sourceMap", ["path", "artifactSha256", "byteSize"])
245
- ]);
246
- }
247
- return files;
248
- }
249
-
250
- function buildUploadReport({ endpoint, manifest, files, dryRun }) {
251
- return {
252
- uploader: { name: "logbrew-js-release-artifact-upload-verifier", version: SCRIPT_VERSION },
253
- endpoint: endpointWithoutQuery(endpoint),
254
- dryRun,
255
- release: manifest.release,
256
- environment: manifest.environment,
257
- service: manifest.service,
258
- artifactType: manifest.artifactType,
259
- artifactCount: manifest.artifacts.length,
260
- filePartCount: files.length
261
118
  };
262
119
  }
263
120
 
264
- function exitCodeForUploadStatus(status) {
265
- return {
266
- uploaded: 0,
267
- dry_run: 0,
268
- auth_missing: 2,
269
- auth_failed: 3,
270
- validation_failed: 4
271
- }[status] ?? 5;
272
- }
273
-
274
- export async function runUploadJs(args) {
121
+ /**
122
+ * @param {CliOptions} options
123
+ * @param {PreparedUpload} prepared
124
+ * @param {string} token
125
+ * @returns {Promise<number>}
126
+ */
127
+ async function sendUpload(options, { endpoint, files, manifest, report }, token) {
128
+ const maxRetries = parseNonNegativeInteger(optionOrDefault(options, "max-retries", "2"), "--max-retries");
129
+ const retryDelaySeconds = parseNonNegativeNumber(optionOrDefault(options, "retry-delay", "0.25"), "--retry-delay");
130
+ const timeoutSeconds = parseNonNegativeNumber(optionOrDefault(options, "timeout", "5"), "--timeout");
131
+ const { body, boundary } = await encodeMultipart(manifest, files);
132
+ const uploadReport = await uploadWithRetries({ body, boundary, endpoint, maxRetries, retryDelaySeconds, timeoutSeconds, token });
133
+ printJson({ ...report, ...uploadReport });
134
+ return exitCodeForUploadStatus(uploadReport.status);
135
+ }
136
+
137
+ /**
138
+ * @param {CliOptions} options
139
+ * @returns {string}
140
+ */
141
+ function requireTokenEnv(options) {
142
+ const tokenEnv = optionOrDefault(options, "token-env", DEFAULT_UPLOAD_TOKEN_ENV).trim();
143
+ if (tokenEnv === "") {
144
+ throw new Error("--token-env must not be empty");
145
+ }
146
+ return tokenEnv;
147
+ }
148
+
149
+ /**
150
+ * @param {CliOptions} options
151
+ * @returns {Promise<number>}
152
+ */
153
+ async function uploadFromOptions(options) {
154
+ const prepared = await prepareUpload(options);
155
+ if (options["dry-run"] === true) {
156
+ printJson({ ...prepared.report, attempts: [], retryCount: NO_RETRIES, status: "dry_run" });
157
+ return exitCodeForUploadStatus("dry_run");
158
+ }
159
+ const tokenEnv = requireTokenEnv(options);
160
+ const token = (process.env[tokenEnv] ?? "").trim();
161
+ if (token === "") {
162
+ printJson({ ...prepared.report, attempts: [], auth: { tokenEnv }, retryCount: NO_RETRIES, status: "auth_missing" });
163
+ return exitCodeForUploadStatus("auth_missing");
164
+ }
165
+ return sendUpload(options, prepared, token);
166
+ }
167
+
168
+ /**
169
+ * @param {readonly string[]} args
170
+ * @returns {Promise<number>}
171
+ */
172
+ async function runUploadJs(args) {
275
173
  const options = parseOptions(args, {
174
+ "allow-hosted": "boolean",
276
175
  "build-dir": "string",
277
- manifest: "string",
278
- endpoint: "string",
279
- "token-env": "string",
280
176
  "dry-run": "boolean",
281
- "allow-hosted": "boolean",
177
+ endpoint: "string",
178
+ manifest: "string",
282
179
  "max-retries": "string",
283
180
  "retry-delay": "string",
284
- timeout: "string"
181
+ timeout: "string",
182
+ "token-env": "string"
285
183
  });
286
184
  try {
287
- const endpoint = requireOption(options, "endpoint");
288
- const parsedEndpoint = requireUploadEndpoint(endpoint, Boolean(options["allow-hosted"]));
289
- const buildDir = requireBuildDir(requireOption(options, "build-dir"));
290
- const manifestPath = path.resolve(requireOption(options, "manifest"));
291
- if (!fs.existsSync(manifestPath)) {
292
- throw new Error(`manifest file does not exist: ${options.manifest}`);
293
- }
294
- const manifest = readJsonObject(manifestPath, "manifest");
295
- const files = collectUploadFiles(manifest, buildDir, !isLoopbackEndpoint(parsedEndpoint));
296
- const dryRun = Boolean(options["dry-run"]);
297
- const report = buildUploadReport({ endpoint, manifest, files, dryRun });
298
-
299
- if (dryRun) {
300
- printJson({ ...report, status: "dry_run", attempts: [], retryCount: 0 });
301
- return 0;
302
- }
303
-
304
- const tokenEnv = (options["token-env"] ?? DEFAULT_UPLOAD_TOKEN_ENV).trim();
305
- if (tokenEnv === "") {
306
- throw new Error("--token-env must not be empty");
307
- }
308
- const token = (process.env[tokenEnv] ?? "").trim();
309
- if (!token) {
310
- printJson({ ...report, status: "auth_missing", attempts: [], retryCount: 0, auth: { tokenEnv } });
311
- return exitCodeForUploadStatus("auth_missing");
312
- }
313
-
314
- const maxRetries = parseNonNegativeInteger(options["max-retries"] ?? "2", "--max-retries");
315
- const retryDelaySeconds = parseNonNegativeNumber(options["retry-delay"] ?? "0.25", "--retry-delay");
316
- const timeoutSeconds = parseNonNegativeNumber(options.timeout ?? "5", "--timeout");
317
- const { body, boundary } = encodeMultipart(manifest, files);
318
- const uploadReport = await uploadWithRetries({
319
- endpoint,
320
- token,
321
- body,
322
- boundary,
323
- maxRetries,
324
- retryDelaySeconds,
325
- timeoutSeconds
326
- });
327
- printJson({ ...report, ...uploadReport });
328
- return exitCodeForUploadStatus(uploadReport.status);
185
+ return await uploadFromOptions(options);
329
186
  } catch (error) {
330
- printJson({
331
- status: "validation_failed",
332
- uploader: { name: "logbrew-js-release-artifact-upload-verifier", version: SCRIPT_VERSION },
333
- validation: { errors: [error.message] }
334
- });
187
+ printJson({ status: "validation_failed", uploader: UPLOADER, validation: { errors: [errorMessage(error)] } });
335
188
  return exitCodeForUploadStatus("validation_failed");
336
189
  }
337
190
  }
191
+
192
+ export { runUploadJs };
@@ -0,0 +1,49 @@
1
+ import { fileReference } from "./release-artifacts-common.js";
2
+ import path from "node:path";
3
+
4
+ const TRAILING_SLASHES_RE = /\/+$/u;
5
+ const LEADING_SLASHES_RE = /^\/+/u;
6
+ const URL_WITH_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//u;
7
+
8
+ /**
9
+ * @param {Readonly<{ host: string, protocol: string }>} parsed
10
+ * @param {string} pathname
11
+ * @returns {string}
12
+ */
13
+ function urlWithPath({ host, protocol }, pathname) {
14
+ return host === "" ? `${protocol}//${pathname}` : `${protocol}//${host}${pathname}`;
15
+ }
16
+
17
+ /**
18
+ * @param {string} value
19
+ * @returns {string}
20
+ */
21
+ function normalizeUrlOrPath(value) {
22
+ const trimmed = value.trim();
23
+ if (URL_WITH_SCHEME_RE.test(trimmed)) {
24
+ const { host, pathname, protocol } = new URL(trimmed);
25
+ const normalizedPath = path.posix.normalize(pathname === "" ? "/" : pathname).replace(TRAILING_SLASHES_RE, "");
26
+ return urlWithPath({ host, protocol }, normalizedPath === "" ? "/" : normalizedPath);
27
+ }
28
+ return fileReference(trimmed).trim().replace(TRAILING_SLASHES_RE, "");
29
+ }
30
+
31
+ /**
32
+ * @param {string} prefix
33
+ * @param {string} relativePath
34
+ * @returns {string}
35
+ */
36
+ function joinUrlOrPath(prefix, relativePath) {
37
+ const normalizedPrefix = normalizeUrlOrPath(prefix);
38
+ const normalizedRelative = relativePath.replaceAll("\\", "/").replace(LEADING_SLASHES_RE, "");
39
+ if (URL_WITH_SCHEME_RE.test(normalizedPrefix)) {
40
+ const { host, pathname, protocol } = new URL(normalizedPrefix);
41
+ const joinedPath = path.posix.join(pathname.replace(TRAILING_SLASHES_RE, ""), normalizedRelative);
42
+ return urlWithPath({ host, protocol }, joinedPath.startsWith("/") ? joinedPath : `/${joinedPath}`);
43
+ }
44
+ return normalizedPrefix === ""
45
+ ? normalizedRelative
46
+ : `${normalizedPrefix.replace(TRAILING_SLASHES_RE, "")}/${normalizedRelative}`;
47
+ }
48
+
49
+ export { joinUrlOrPath, normalizeUrlOrPath };