@cavsnode/cavs-sdk 0.1.0

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/index.cjs ADDED
@@ -0,0 +1,1186 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ AuthenticationError: () => AuthenticationError,
34
+ AuthorizationError: () => AuthorizationError,
35
+ CAVS: () => CAVS,
36
+ CAVSError: () => CAVSError,
37
+ ChecksumError: () => ChecksumError,
38
+ ConflictError: () => ConflictError,
39
+ DEFAULT_API: () => DEFAULT_API,
40
+ DownloadError: () => DownloadError,
41
+ InvalidURIError: () => InvalidURIError,
42
+ NotFoundError: () => NotFoundError,
43
+ QuotaExceededError: () => QuotaExceededError,
44
+ RateLimitError: () => RateLimitError,
45
+ TemporaryServiceError: () => TemporaryServiceError,
46
+ UploadError: () => UploadError,
47
+ VERSION: () => VERSION,
48
+ sha256File: () => sha256File,
49
+ uri: () => uri_exports
50
+ });
51
+ module.exports = __toCommonJS(index_exports);
52
+
53
+ // src/errors.ts
54
+ var CAVSError = class extends Error {
55
+ code;
56
+ status;
57
+ requestId;
58
+ retryAfterMs;
59
+ constructor(message, options = {}) {
60
+ super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
61
+ this.name = new.target.name;
62
+ this.code = options.code;
63
+ this.status = options.status;
64
+ this.requestId = options.requestId;
65
+ this.retryAfterMs = options.retryAfterMs;
66
+ Object.setPrototypeOf(this, new.target.prototype);
67
+ }
68
+ };
69
+ var AuthenticationError = class extends CAVSError {
70
+ };
71
+ var AuthorizationError = class extends CAVSError {
72
+ };
73
+ var NotFoundError = class extends CAVSError {
74
+ };
75
+ var ConflictError = class extends CAVSError {
76
+ };
77
+ var RateLimitError = class extends CAVSError {
78
+ };
79
+ var QuotaExceededError = class extends CAVSError {
80
+ };
81
+ var ChecksumError = class extends CAVSError {
82
+ };
83
+ var UploadError = class extends CAVSError {
84
+ };
85
+ var DownloadError = class extends CAVSError {
86
+ };
87
+ var TemporaryServiceError = class extends CAVSError {
88
+ };
89
+ function toCAVSError(status, body, extra = {}) {
90
+ const envelope = body ?? {};
91
+ const code = envelope.error?.code;
92
+ const message = envelope.error?.message ?? `CAVS request failed with status ${status}`;
93
+ const opts = {
94
+ code,
95
+ status,
96
+ requestId: extra.requestId,
97
+ retryAfterMs: extra.retryAfterMs
98
+ };
99
+ switch (status) {
100
+ case 401:
101
+ return new AuthenticationError(message, opts);
102
+ case 403:
103
+ return new AuthorizationError(message, opts);
104
+ case 404:
105
+ return new NotFoundError(message, opts);
106
+ case 409:
107
+ return new ConflictError(message, opts);
108
+ case 402:
109
+ case 413:
110
+ return new QuotaExceededError(message, opts);
111
+ case 429:
112
+ return new RateLimitError(message, opts);
113
+ case 426:
114
+ return new CAVSError(message, opts);
115
+ default:
116
+ if (status >= 500) {
117
+ return new TemporaryServiceError(message, opts);
118
+ }
119
+ if (code === "quota_exceeded") {
120
+ return new QuotaExceededError(message, opts);
121
+ }
122
+ return new CAVSError(message, opts);
123
+ }
124
+ }
125
+
126
+ // src/auth.ts
127
+ function resolveToken(explicit, env = process.env) {
128
+ const token = explicit ?? env.CAVS_TOKEN;
129
+ if (!token || token.trim() === "") {
130
+ throw new AuthenticationError(
131
+ "No CAVS token provided. Pass { token } or set the CAVS_TOKEN environment variable."
132
+ );
133
+ }
134
+ return token.trim();
135
+ }
136
+
137
+ // src/telemetry.ts
138
+ var VERSION = "0.1.0";
139
+ var CLIENT_ID = "javascript-sdk";
140
+ function telemetryHeaders(opts = {}) {
141
+ const headers = {
142
+ "User-Agent": `cavs-node/${VERSION}`,
143
+ "X-CAVS-Client": CLIENT_ID,
144
+ "X-CAVS-Integration": opts.integration ?? CLIENT_ID,
145
+ "X-CAVS-Integration-Version": opts.integrationVersion ?? VERSION
146
+ };
147
+ if (opts.runId) {
148
+ headers["X-CAVS-Run-ID"] = opts.runId;
149
+ }
150
+ return headers;
151
+ }
152
+
153
+ // src/http.ts
154
+ var RETRYABLE_STATUS = (status) => status === 429 || status >= 500;
155
+ var HttpClient = class {
156
+ baseUrl;
157
+ token;
158
+ fetchImpl;
159
+ telemetry;
160
+ maxRetries;
161
+ maxRedirects;
162
+ timeoutMs;
163
+ retryBaseMs;
164
+ maxBackoffMs;
165
+ onDeprecation;
166
+ sleepImpl;
167
+ constructor(opts) {
168
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
169
+ this.token = opts.token;
170
+ this.fetchImpl = opts.fetch ?? fetch;
171
+ this.telemetry = opts.telemetry ?? {};
172
+ this.maxRetries = opts.maxRetries ?? 4;
173
+ this.maxRedirects = opts.maxRedirects ?? 5;
174
+ this.timeoutMs = opts.timeoutMs ?? 3e4;
175
+ this.retryBaseMs = opts.retryBaseMs ?? 200;
176
+ this.maxBackoffMs = opts.maxBackoffMs ?? 3e4;
177
+ this.onDeprecation = opts.onDeprecation ?? ((message) => {
178
+ console.warn(`[cavs] deprecation: ${message}`);
179
+ });
180
+ this.sleepImpl = opts.sleepImpl ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
181
+ }
182
+ /** Build the standard header set for a control-plane request. */
183
+ baseHeaders(extra) {
184
+ return {
185
+ Authorization: `Bearer ${this.token}`,
186
+ Accept: "application/json",
187
+ ...telemetryHeaders(this.telemetry),
188
+ ...extra
189
+ };
190
+ }
191
+ buildUrl(path2, query) {
192
+ const suffix = path2.startsWith("/") ? path2 : `/${path2}`;
193
+ const full = new URL(this.baseUrl + suffix);
194
+ if (query) {
195
+ for (const [key, value] of Object.entries(query)) {
196
+ if (value !== void 0) {
197
+ full.searchParams.set(key, String(value));
198
+ }
199
+ }
200
+ }
201
+ return full.toString();
202
+ }
203
+ /** Perform a JSON control-plane request with retries and typed errors. */
204
+ async requestJson(opts) {
205
+ const method = opts.method ?? "GET";
206
+ const url = this.buildUrl(opts.path, opts.query);
207
+ const headers = this.baseHeaders(opts.headers);
208
+ let body;
209
+ if (opts.body !== void 0) {
210
+ body = JSON.stringify(opts.body);
211
+ headers["Content-Type"] = "application/json";
212
+ }
213
+ if (opts.idempotencyKey) {
214
+ headers["Idempotency-Key"] = opts.idempotencyKey;
215
+ }
216
+ const res = await this.executeWithRetry(url, { method, headers, body }, opts.signal);
217
+ return this.parseJson(res);
218
+ }
219
+ /**
220
+ * Send a request whose body is a (possibly streamed) factory, retrying with a
221
+ * fresh body each attempt. Used for presigned PUT uploads. Returns the raw
222
+ * Response. Presigned URLs must never be logged.
223
+ */
224
+ async sendStream(url, opts) {
225
+ return this.executeWithRetry(
226
+ url,
227
+ {
228
+ method: opts.method,
229
+ headers: opts.headers ?? {},
230
+ bodyFactory: opts.bodyFactory,
231
+ extraInit: opts.init
232
+ },
233
+ opts.signal,
234
+ /* isDataPlane */
235
+ true
236
+ );
237
+ }
238
+ /** Open a streaming GET (presigned download). Returns the raw Response. */
239
+ async openStream(url, opts = {}) {
240
+ return this.executeWithRetry(
241
+ url,
242
+ { method: "GET", headers: {} },
243
+ opts.signal,
244
+ /* isDataPlane */
245
+ true
246
+ );
247
+ }
248
+ async executeWithRetry(url, req, externalSignal, isDataPlane = false) {
249
+ let lastError;
250
+ for (let attempt = 0; ; attempt++) {
251
+ const { signal, cleanup, timedOut } = this.armSignal(externalSignal);
252
+ try {
253
+ const body = req.bodyFactory ? req.bodyFactory() : req.body;
254
+ const init = {
255
+ method: req.method,
256
+ headers: req.headers,
257
+ signal,
258
+ ...req.extraInit
259
+ };
260
+ if (body !== void 0) {
261
+ init.body = body;
262
+ }
263
+ const res = await this.fetchWithRedirects(url, init);
264
+ if (!isDataPlane) {
265
+ const dep = res.headers.get("x-cavs-deprecation");
266
+ if (dep) {
267
+ this.safeDeprecation(dep);
268
+ }
269
+ }
270
+ if (res.ok) {
271
+ return res;
272
+ }
273
+ if (RETRYABLE_STATUS(res.status) && attempt < this.maxRetries) {
274
+ const wait = this.retryAfterMs(res) ?? this.backoff(attempt);
275
+ cleanup();
276
+ await this.sleepImpl(wait);
277
+ continue;
278
+ }
279
+ throw await this.toError(res);
280
+ } catch (err) {
281
+ if (externalSignal?.aborted) {
282
+ cleanup();
283
+ throw externalSignal.reason instanceof Error ? externalSignal.reason : new CAVSError("Request cancelled", { cause: externalSignal.reason });
284
+ }
285
+ if (err instanceof CAVSError) {
286
+ cleanup();
287
+ throw err;
288
+ }
289
+ lastError = err;
290
+ if (attempt < this.maxRetries) {
291
+ cleanup();
292
+ await this.sleepImpl(this.backoff(attempt));
293
+ continue;
294
+ }
295
+ cleanup();
296
+ const reason = timedOut() ? "request timed out" : "network error";
297
+ throw new TemporaryServiceError(`CAVS ${reason} after ${attempt + 1} attempt(s)`, {
298
+ cause: err
299
+ });
300
+ } finally {
301
+ cleanup();
302
+ }
303
+ }
304
+ throw new TemporaryServiceError("CAVS request failed", { cause: lastError });
305
+ }
306
+ /** Follow redirects manually, bounded by {@link maxRedirects}. */
307
+ async fetchWithRedirects(url, init) {
308
+ let current = url;
309
+ for (let i = 0; i <= this.maxRedirects; i++) {
310
+ const res = await this.fetchImpl(current, { ...init, redirect: "manual" });
311
+ const location = res.headers.get("location");
312
+ if (res.status >= 300 && res.status < 400 && location) {
313
+ if (i === this.maxRedirects) {
314
+ throw new TemporaryServiceError(
315
+ `Too many redirects (>${this.maxRedirects})`,
316
+ { status: res.status }
317
+ );
318
+ }
319
+ current = new URL(location, current).toString();
320
+ continue;
321
+ }
322
+ return res;
323
+ }
324
+ throw new TemporaryServiceError("Redirect handling failed");
325
+ }
326
+ armSignal(external) {
327
+ const controller = new AbortController();
328
+ let didTimeout = false;
329
+ const timer = setTimeout(() => {
330
+ didTimeout = true;
331
+ controller.abort(new Error("timeout"));
332
+ }, this.timeoutMs);
333
+ const onExternal = () => controller.abort(external?.reason);
334
+ if (external) {
335
+ if (external.aborted) {
336
+ controller.abort(external.reason);
337
+ } else {
338
+ external.addEventListener("abort", onExternal);
339
+ }
340
+ }
341
+ let done = false;
342
+ const cleanup = () => {
343
+ if (done) return;
344
+ done = true;
345
+ clearTimeout(timer);
346
+ external?.removeEventListener("abort", onExternal);
347
+ };
348
+ return { signal: controller.signal, cleanup, timedOut: () => didTimeout };
349
+ }
350
+ backoff(attempt) {
351
+ const base = this.retryBaseMs * Math.pow(2, attempt);
352
+ const jitter = Math.random() * this.retryBaseMs;
353
+ return Math.min(base + jitter, this.maxBackoffMs);
354
+ }
355
+ retryAfterMs(res) {
356
+ const header = res.headers.get("retry-after");
357
+ if (!header) return void 0;
358
+ const asInt = Number(header);
359
+ let ms;
360
+ if (!Number.isNaN(asInt)) {
361
+ ms = asInt * 1e3;
362
+ } else {
363
+ const date = Date.parse(header);
364
+ if (Number.isNaN(date)) return void 0;
365
+ ms = date - Date.now();
366
+ }
367
+ if (ms < 0) ms = 0;
368
+ return Math.min(ms, this.maxBackoffMs);
369
+ }
370
+ safeDeprecation(message) {
371
+ try {
372
+ this.onDeprecation(message);
373
+ } catch {
374
+ }
375
+ }
376
+ async toError(res) {
377
+ const requestId = res.headers.get("x-request-id") ?? res.headers.get("x-cavs-request-id") ?? void 0;
378
+ const retryAfterMs = this.retryAfterMs(res);
379
+ let body;
380
+ try {
381
+ const text = await res.text();
382
+ body = text ? JSON.parse(text) : void 0;
383
+ } catch {
384
+ body = void 0;
385
+ }
386
+ return toCAVSError(res.status, body, { requestId, retryAfterMs });
387
+ }
388
+ async parseJson(res) {
389
+ if (res.status === 204) {
390
+ return void 0;
391
+ }
392
+ const text = await res.text();
393
+ if (!text) {
394
+ return void 0;
395
+ }
396
+ try {
397
+ return JSON.parse(text);
398
+ } catch (err) {
399
+ throw new CAVSError("Failed to parse CAVS response as JSON", { cause: err });
400
+ }
401
+ }
402
+ };
403
+
404
+ // src/uri.ts
405
+ var uri_exports = {};
406
+ __export(uri_exports, {
407
+ InvalidURIError: () => InvalidURIError,
408
+ format: () => format,
409
+ parse: () => parse
410
+ });
411
+ var SCHEME = "cavs://";
412
+ var OID_RE = /^[0-9a-f]{64}$/;
413
+ var SHA_MARKER = "@sha256:";
414
+ var InvalidURIError = class extends CAVSError {
415
+ };
416
+ function parse(uri) {
417
+ if (typeof uri !== "string" || !uri.startsWith(SCHEME)) {
418
+ throw new InvalidURIError(`Invalid cavs:// URI: wrong scheme (${redact(uri)})`);
419
+ }
420
+ const rest = uri.slice(SCHEME.length);
421
+ const segments = rest.split("/");
422
+ if (segments.length !== 4) {
423
+ throw new InvalidURIError(
424
+ `Invalid cavs:// URI: expected org/project/kind/name, got ${segments.length} segment(s)`
425
+ );
426
+ }
427
+ const [org, project, kind, last] = segments;
428
+ if (!org || !project || !kind || !last) {
429
+ throw new InvalidURIError("Invalid cavs:// URI: empty path segment");
430
+ }
431
+ const shaIdx = last.indexOf(SHA_MARKER);
432
+ if (shaIdx !== -1) {
433
+ const name2 = last.slice(0, shaIdx);
434
+ const oid = last.slice(shaIdx + SHA_MARKER.length);
435
+ if (!name2) {
436
+ throw new InvalidURIError("Invalid cavs:// URI: empty name");
437
+ }
438
+ if (name2.includes(":")) {
439
+ throw new InvalidURIError("Invalid cavs:// URI: cannot specify both version and oid");
440
+ }
441
+ if (!OID_RE.test(oid)) {
442
+ throw new InvalidURIError("Invalid cavs:// URI: oid must be 64 lowercase hex chars");
443
+ }
444
+ return { org, project, kind, name: name2, version: null, oid, immutable: true };
445
+ }
446
+ const colonIdx = last.indexOf(":");
447
+ if (colonIdx === -1) {
448
+ throw new InvalidURIError("Invalid cavs:// URI: missing version or @sha256:oid");
449
+ }
450
+ const name = last.slice(0, colonIdx);
451
+ const version = last.slice(colonIdx + 1);
452
+ if (!name) {
453
+ throw new InvalidURIError("Invalid cavs:// URI: empty name");
454
+ }
455
+ if (!version) {
456
+ throw new InvalidURIError("Invalid cavs:// URI: empty version");
457
+ }
458
+ return { org, project, kind, name, version, oid: null, immutable: false };
459
+ }
460
+ function format(parsed) {
461
+ const { org, project, kind, name, version, oid } = parsed;
462
+ if (!org || !project || !kind || !name) {
463
+ throw new InvalidURIError("Cannot format cavs:// URI: missing required field");
464
+ }
465
+ if (oid) {
466
+ if (version) {
467
+ throw new InvalidURIError("Cannot format cavs:// URI: both version and oid provided");
468
+ }
469
+ if (!OID_RE.test(oid)) {
470
+ throw new InvalidURIError("Cannot format cavs:// URI: oid must be 64 lowercase hex chars");
471
+ }
472
+ return `${SCHEME}${org}/${project}/${kind}/${name}${SHA_MARKER}${oid}`;
473
+ }
474
+ if (!version) {
475
+ throw new InvalidURIError("Cannot format cavs:// URI: either version or oid is required");
476
+ }
477
+ return `${SCHEME}${org}/${project}/${kind}/${name}:${version}`;
478
+ }
479
+ function redact(uri) {
480
+ if (typeof uri !== "string") {
481
+ return String(uri);
482
+ }
483
+ return uri.length > 40 ? `${uri.slice(0, 40)}\u2026` : uri;
484
+ }
485
+
486
+ // src/resources/artifacts.ts
487
+ var import_node_crypto2 = require("crypto");
488
+
489
+ // src/transfer.ts
490
+ var import_node_crypto = require("crypto");
491
+ var import_node_fs = require("fs");
492
+ var fs = __toESM(require("fs/promises"), 1);
493
+ var path = __toESM(require("path"), 1);
494
+ var import_node_stream = require("stream");
495
+ var import_promises = require("stream/promises");
496
+ async function sha256File(filePath, opts = {}) {
497
+ throwIfAborted(opts.signal);
498
+ const hash = (0, import_node_crypto.createHash)("sha256");
499
+ let size = 0;
500
+ const stream = (0, import_node_fs.createReadStream)(filePath, { highWaterMark: opts.highWaterMark });
501
+ await new Promise((resolve2, reject) => {
502
+ const onAbort = () => stream.destroy(abortError(opts.signal));
503
+ if (opts.signal) {
504
+ opts.signal.addEventListener("abort", onAbort, { once: true });
505
+ }
506
+ stream.on("data", (chunk) => {
507
+ hash.update(chunk);
508
+ size += chunk.length;
509
+ opts.onProgress?.(size);
510
+ });
511
+ stream.on("error", (err) => {
512
+ opts.signal?.removeEventListener("abort", onAbort);
513
+ reject(err);
514
+ });
515
+ stream.on("end", () => {
516
+ opts.signal?.removeEventListener("abort", onAbort);
517
+ resolve2();
518
+ });
519
+ });
520
+ return { oid: hash.digest("hex"), size };
521
+ }
522
+ async function collectFiles(root) {
523
+ const stat2 = await fs.stat(root);
524
+ if (stat2.isFile()) {
525
+ return [{ absPath: path.resolve(root), relPath: path.basename(root) }];
526
+ }
527
+ if (!stat2.isDirectory()) {
528
+ throw new UploadError(`Not a regular file or directory: ${root}`);
529
+ }
530
+ const out = [];
531
+ const base = path.resolve(root);
532
+ async function walk(dir) {
533
+ const entries = await fs.readdir(dir, { withFileTypes: true });
534
+ for (const entry of entries) {
535
+ const abs = path.join(dir, entry.name);
536
+ if (entry.isDirectory()) {
537
+ await walk(abs);
538
+ } else if (entry.isFile()) {
539
+ const rel = path.relative(base, abs).split(path.sep).join("/");
540
+ out.push({ absPath: abs, relPath: rel });
541
+ }
542
+ }
543
+ }
544
+ await walk(base);
545
+ out.sort((a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0);
546
+ return out;
547
+ }
548
+ async function prepareUpload(root, opts = {}) {
549
+ const found = await collectFiles(root);
550
+ const files = [];
551
+ for (const f of found) {
552
+ const { oid, size } = await sha256File(f.absPath, {
553
+ signal: opts.signal,
554
+ onProgress: (loaded) => opts.onProgress?.({ phase: "hash", path: f.relPath, loaded, total: 0 })
555
+ });
556
+ files.push({ ...f, oid, size });
557
+ }
558
+ return files;
559
+ }
560
+ function countingReadStream(filePath, onChunk) {
561
+ const stream = (0, import_node_fs.createReadStream)(filePath);
562
+ stream.on("data", (chunk) => onChunk(chunk.length));
563
+ return stream;
564
+ }
565
+ async function downloadToFile(body, args) {
566
+ const target = safeJoin(args.destDir, args.relPath);
567
+ await fs.mkdir(path.dirname(target), { recursive: true });
568
+ const tmp = path.join(path.dirname(target), `.cavs-download-${args.oid}.part`);
569
+ if (!body) {
570
+ throw new DownloadError(`Empty response body while downloading ${args.relPath}`);
571
+ }
572
+ const hash = (0, import_node_crypto.createHash)("sha256");
573
+ let size = 0;
574
+ const nodeReadable = import_node_stream.Readable.fromWeb(body);
575
+ const hashing = new import_node_stream.Transform({
576
+ transform(chunk, _enc, cb) {
577
+ hash.update(chunk);
578
+ size += chunk.length;
579
+ args.onProgress?.({
580
+ phase: "download",
581
+ path: args.relPath,
582
+ loaded: size,
583
+ total: args.size ?? 0
584
+ });
585
+ cb(null, chunk);
586
+ }
587
+ });
588
+ try {
589
+ await (0, import_promises.pipeline)(nodeReadable, hashing, (0, import_node_fs.createWriteStream)(tmp), { signal: args.signal });
590
+ } catch (err) {
591
+ await safeUnlink(tmp);
592
+ throw err instanceof ChecksumError ? err : new DownloadError(`Failed to download ${args.relPath}`, { cause: err });
593
+ }
594
+ const digest = hash.digest("hex");
595
+ if (digest !== args.oid) {
596
+ await safeUnlink(tmp);
597
+ throw new ChecksumError(
598
+ `Checksum mismatch for ${args.relPath}: expected ${args.oid}, got ${digest}`
599
+ );
600
+ }
601
+ await fs.rename(tmp, target);
602
+ return { path: args.relPath, oid: args.oid, size };
603
+ }
604
+ function safeJoin(destDir, relPath) {
605
+ if (path.isAbsolute(relPath)) {
606
+ throw new DownloadError(`Refusing absolute path in artifact: ${relPath}`);
607
+ }
608
+ const base = path.resolve(destDir);
609
+ const target = path.resolve(base, relPath);
610
+ const withSep = base.endsWith(path.sep) ? base : base + path.sep;
611
+ if (target !== base && !target.startsWith(withSep)) {
612
+ throw new DownloadError(`Path traversal detected in artifact member: ${relPath}`);
613
+ }
614
+ return target;
615
+ }
616
+ function throwIfAborted(signal) {
617
+ if (signal?.aborted) {
618
+ throw abortError(signal);
619
+ }
620
+ }
621
+ function abortError(signal) {
622
+ const reason = signal?.reason;
623
+ return reason instanceof Error ? reason : new Error("Operation aborted");
624
+ }
625
+ async function safeUnlink(p) {
626
+ try {
627
+ await fs.unlink(p);
628
+ } catch {
629
+ }
630
+ }
631
+
632
+ // src/resources/artifacts.ts
633
+ var ArtifactsResource = class {
634
+ constructor(ctx) {
635
+ this.ctx = ctx;
636
+ }
637
+ ctx;
638
+ /** `GET /api/v1/artifacts/{id}`. */
639
+ async get(artifactId, signal) {
640
+ return this.ctx.http.requestJson({
641
+ path: `/api/v1/artifacts/${encodeURIComponent(artifactId)}`,
642
+ signal
643
+ });
644
+ }
645
+ /** `GET /api/v1/artifacts/{id}/versions`. */
646
+ async versions(artifactId, signal) {
647
+ return this.ctx.http.requestJson({
648
+ path: `/api/v1/artifacts/${encodeURIComponent(artifactId)}/versions`,
649
+ signal
650
+ });
651
+ }
652
+ /** `GET /api/v1/organizations/{org}/artifacts`. */
653
+ async list(opts = {}) {
654
+ const org = opts.org ?? this.ctx.defaultOrg;
655
+ if (!org) {
656
+ throw new CAVSError("No organization specified for artifacts.list (pass { org } or set CAVS_ORG).");
657
+ }
658
+ return this.ctx.http.requestJson({
659
+ path: `/api/v1/organizations/${encodeURIComponent(org)}/artifacts`,
660
+ query: { limit: opts.limit, type: opts.type, q: opts.q },
661
+ signal: opts.signal
662
+ });
663
+ }
664
+ /**
665
+ * Upload a file or directory as a new artifact version, following the
666
+ * choreography in CONTRACT.md §5. Streams every object; skips objects that
667
+ * already exist (dedup); resumable because step 3 re-reports `exists`.
668
+ */
669
+ async upload(opts) {
670
+ const repo = opts.project;
671
+ const files = await prepareUpload(opts.path, {
672
+ signal: opts.signal,
673
+ onProgress: opts.onProgress
674
+ });
675
+ if (files.length === 0) {
676
+ throw new UploadError(`Nothing to upload: ${opts.path} contains no files`);
677
+ }
678
+ const idempotencyKey = opts.idempotencyKey ?? `upload:${repo}:${opts.kind}:${opts.name}:${opts.version}`;
679
+ const repoPath = `/api/v1/repositories/${encodeURIComponent(repo)}`;
680
+ const session = await this.ctx.http.requestJson({
681
+ method: "POST",
682
+ path: `${repoPath}/uploads`,
683
+ idempotencyKey,
684
+ body: { expected_objects: files.length },
685
+ signal: opts.signal
686
+ });
687
+ const sessionId = session.session_id ?? session.id;
688
+ if (!sessionId) {
689
+ throw new UploadError("Upload session response did not include a session id");
690
+ }
691
+ const sessionPath = `${repoPath}/uploads/${encodeURIComponent(sessionId)}`;
692
+ const uniqueObjects = dedupeObjects(files);
693
+ const authResults = await this.ctx.http.requestJson({
694
+ method: "POST",
695
+ path: `${sessionPath}/objects`,
696
+ body: { objects: uniqueObjects.map((f) => ({ oid: f.oid, size: f.size })) },
697
+ signal: opts.signal
698
+ });
699
+ const authByOid = new Map(authResults.map((r) => [r.oid, r]));
700
+ const uploaded = [];
701
+ const doneOids = /* @__PURE__ */ new Set();
702
+ for (const file of files) {
703
+ const entry = authByOid.get(file.oid);
704
+ if (!entry) {
705
+ throw new UploadError(`Server did not return an authorization for oid ${file.oid}`);
706
+ }
707
+ if (entry.error) {
708
+ throw new UploadError(`Upload rejected for ${file.relPath}: ${entry.error}`);
709
+ }
710
+ const deduplicated = entry.exists === true;
711
+ if (!deduplicated && !doneOids.has(file.oid)) {
712
+ if (!entry.upload_url) {
713
+ throw new UploadError(`Missing upload URL for ${file.relPath}`);
714
+ }
715
+ await this.putObject(entry.upload_url, file, opts);
716
+ await this.ctx.http.requestJson({
717
+ method: "POST",
718
+ path: `${sessionPath}/objects/${encodeURIComponent(file.oid)}/complete`,
719
+ body: { size: file.size },
720
+ signal: opts.signal
721
+ });
722
+ }
723
+ doneOids.add(file.oid);
724
+ uploaded.push({
725
+ path: file.relPath,
726
+ oid: file.oid,
727
+ size: file.size,
728
+ deduplicated
729
+ });
730
+ }
731
+ const finalize = await this.ctx.http.requestJson({
732
+ method: "POST",
733
+ path: `${sessionPath}/finalize`,
734
+ idempotencyKey: `${idempotencyKey}:finalize`,
735
+ body: {
736
+ name: opts.name,
737
+ kind: opts.kind,
738
+ version: opts.version,
739
+ metadata: opts.metadata ?? {},
740
+ files: files.map((f) => ({ path: f.relPath, oid: f.oid, size: f.size }))
741
+ },
742
+ signal: opts.signal
743
+ });
744
+ const logical = finalize.logical_bytes ?? files.reduce((n, f) => n + f.size, 0);
745
+ return {
746
+ artifactId: finalize.artifact_id,
747
+ reference: finalize.artifact_reference ?? finalize.reference ?? format({
748
+ org: opts.org ?? this.ctx.defaultOrg ?? "org",
749
+ project: opts.project,
750
+ kind: opts.kind,
751
+ name: opts.name,
752
+ version: opts.version
753
+ }),
754
+ version: finalize.version,
755
+ sha256: finalize.sha256,
756
+ logicalBytes: logical,
757
+ physicalBytes: finalize.physical_bytes ?? logical,
758
+ deduplicatedBytes: finalize.deduplicated_bytes ?? 0,
759
+ deduplicationRatio: finalize.deduplication_ratio ?? 0,
760
+ files: uploaded
761
+ };
762
+ }
763
+ async putObject(uploadUrl, file, opts) {
764
+ const res = await this.ctx.http.sendStream(uploadUrl, {
765
+ method: "PUT",
766
+ headers: { "Content-Length": String(file.size) },
767
+ bodyFactory: () => countingReadStream(file.absPath, (bytes) => {
768
+ this.reportUpload(file, bytes, opts);
769
+ }),
770
+ init: { duplex: "half" },
771
+ signal: opts.signal
772
+ });
773
+ if (!res.ok) {
774
+ throw new UploadError(
775
+ `Presigned upload failed for ${file.relPath} (status ${res.status})`,
776
+ { status: res.status }
777
+ );
778
+ }
779
+ }
780
+ reportUpload(file, bytes, opts) {
781
+ if (!opts.onProgress) return;
782
+ const state = this.uploadProgress ??= /* @__PURE__ */ new Map();
783
+ const loaded = (state.get(file.oid) ?? 0) + bytes;
784
+ state.set(file.oid, loaded);
785
+ opts.onProgress({ phase: "upload", path: file.relPath, loaded, total: file.size });
786
+ }
787
+ uploadProgress;
788
+ /**
789
+ * Download an artifact by its `cavs://` reference into `opts.dest`, verifying
790
+ * each object's sha256 and writing atomically (CONTRACT.md §5).
791
+ */
792
+ async download(reference, opts) {
793
+ parse(reference);
794
+ const resolved = await this.ctx.http.requestJson({
795
+ path: "/api/v1/resolve",
796
+ query: { uri: reference },
797
+ signal: opts.signal
798
+ });
799
+ const repo = resolved.repository ?? resolved.repository_id ?? resolved.repo;
800
+ if (!repo) {
801
+ throw new CAVSError("Resolve response did not include a repository for the reference");
802
+ }
803
+ const files = resolved.files ?? [];
804
+ if (files.length === 0) {
805
+ throw new CAVSError(`No files found for reference ${reference}`);
806
+ }
807
+ const oids = [...new Set(files.map((f) => f.oid))];
808
+ const auth = await this.ctx.http.requestJson({
809
+ method: "POST",
810
+ path: `/api/v1/repositories/${encodeURIComponent(repo)}/downloads/authorize`,
811
+ body: { oids },
812
+ signal: opts.signal
813
+ });
814
+ const urlByOid = new Map(auth.map((a) => [a.oid, a]));
815
+ const results = [];
816
+ for (const file of files) {
817
+ const entry = urlByOid.get(file.oid);
818
+ if (!entry || entry.error || !entry.download_url) {
819
+ throw new CAVSError(
820
+ `Download authorization failed for ${file.path}${entry?.error ? `: ${entry.error}` : ""}`
821
+ );
822
+ }
823
+ const res = await this.ctx.http.openStream(entry.download_url, { signal: opts.signal });
824
+ const written = await downloadToFile(res.body, {
825
+ destDir: opts.dest,
826
+ relPath: file.path,
827
+ oid: file.oid,
828
+ size: file.size,
829
+ onProgress: opts.onProgress,
830
+ signal: opts.signal
831
+ });
832
+ results.push(written);
833
+ }
834
+ return { dest: opts.dest, files: results };
835
+ }
836
+ };
837
+ function dedupeObjects(files) {
838
+ const seen = /* @__PURE__ */ new Set();
839
+ const out = [];
840
+ for (const f of files) {
841
+ if (!seen.has(f.oid)) {
842
+ seen.add(f.oid);
843
+ out.push(f);
844
+ }
845
+ }
846
+ return out;
847
+ }
848
+
849
+ // src/resources/snapshots.ts
850
+ var SnapshotsResource = class {
851
+ constructor(ctx) {
852
+ this.ctx = ctx;
853
+ }
854
+ ctx;
855
+ /** `GET /api/v1/repositories/{repo}/snapshots`. */
856
+ async list(project, signal) {
857
+ return this.ctx.http.requestJson({
858
+ path: `/api/v1/repositories/${encodeURIComponent(project)}/snapshots`,
859
+ signal
860
+ });
861
+ }
862
+ /** `POST /api/v1/repositories/{repo}/snapshots`. */
863
+ async create(project, opts = {}) {
864
+ return this.ctx.http.requestJson({
865
+ method: "POST",
866
+ path: `/api/v1/repositories/${encodeURIComponent(project)}/snapshots`,
867
+ idempotencyKey: opts.idempotencyKey,
868
+ body: { message: opts.message, metadata: opts.metadata ?? {} },
869
+ signal: opts.signal
870
+ });
871
+ }
872
+ };
873
+
874
+ // src/resources/releases.ts
875
+ var ReleasesResource = class {
876
+ constructor(ctx) {
877
+ this.ctx = ctx;
878
+ }
879
+ ctx;
880
+ /** `GET /api/v1/repositories/{repo}/releases`. */
881
+ async list(project, signal) {
882
+ return this.ctx.http.requestJson({
883
+ path: `/api/v1/repositories/${encodeURIComponent(project)}/releases`,
884
+ signal
885
+ });
886
+ }
887
+ /** `GET /api/v1/repositories/{repo}/releases/{tag}`. */
888
+ async get(project, tag, signal) {
889
+ return this.ctx.http.requestJson({
890
+ path: `/api/v1/repositories/${encodeURIComponent(project)}/releases/${encodeURIComponent(tag)}`,
891
+ signal
892
+ });
893
+ }
894
+ };
895
+
896
+ // src/resources/runs.ts
897
+ var RunsResource = class {
898
+ constructor(ctx) {
899
+ this.ctx = ctx;
900
+ }
901
+ ctx;
902
+ base(project) {
903
+ return `/api/v1/repositories/${encodeURIComponent(project)}/runs`;
904
+ }
905
+ /** `POST /api/v1/repositories/{repo}/runs`. */
906
+ async create(project, opts) {
907
+ return this.ctx.http.requestJson({
908
+ method: "POST",
909
+ path: this.base(project),
910
+ idempotencyKey: opts.idempotencyKey,
911
+ body: {
912
+ name: opts.name,
913
+ system: opts.system,
914
+ external_id: opts.externalId,
915
+ git_commit: opts.gitCommit,
916
+ pipeline: opts.pipeline,
917
+ environment: opts.environment,
918
+ status: opts.status,
919
+ metadata: opts.metadata ?? {}
920
+ },
921
+ signal: opts.signal
922
+ });
923
+ }
924
+ /** `GET /api/v1/repositories/{repo}/runs`. */
925
+ async list(project, signal) {
926
+ return this.ctx.http.requestJson({ path: this.base(project), signal });
927
+ }
928
+ /** `GET /api/v1/repositories/{repo}/runs/{run}`. */
929
+ async get(project, runId, signal) {
930
+ return this.ctx.http.requestJson({
931
+ path: `${this.base(project)}/${encodeURIComponent(runId)}`,
932
+ signal
933
+ });
934
+ }
935
+ /** `PATCH /api/v1/repositories/{repo}/runs/{run}`. */
936
+ async update(project, runId, opts) {
937
+ return this.ctx.http.requestJson({
938
+ method: "PATCH",
939
+ path: `${this.base(project)}/${encodeURIComponent(runId)}`,
940
+ idempotencyKey: opts.idempotencyKey,
941
+ body: { status: opts.status, ended_at: opts.endedAt, metadata: opts.metadata },
942
+ signal: opts.signal
943
+ });
944
+ }
945
+ /** `POST /api/v1/repositories/{repo}/runs/{run}/artifacts` — record lineage. */
946
+ async addArtifacts(project, runId, artifacts, opts = {}) {
947
+ return this.ctx.http.requestJson({
948
+ method: "POST",
949
+ path: `${this.base(project)}/${encodeURIComponent(runId)}/artifacts`,
950
+ idempotencyKey: opts.idempotencyKey,
951
+ body: { artifacts },
952
+ signal: opts.signal
953
+ });
954
+ }
955
+ };
956
+
957
+ // src/resources/lineage.ts
958
+ var LineageResource = class {
959
+ constructor(ctx) {
960
+ this.ctx = ctx;
961
+ }
962
+ ctx;
963
+ /** `GET /api/v1/repositories/{repo}/graph`. */
964
+ async graph(project, signal) {
965
+ return this.ctx.http.requestJson({
966
+ path: `/api/v1/repositories/${encodeURIComponent(project)}/graph`,
967
+ signal
968
+ });
969
+ }
970
+ /** `POST /api/v1/repositories/{repo}/graph/edges`. */
971
+ async addEdge(project, opts) {
972
+ return this.ctx.http.requestJson({
973
+ method: "POST",
974
+ path: `/api/v1/repositories/${encodeURIComponent(project)}/graph/edges`,
975
+ idempotencyKey: opts.idempotencyKey,
976
+ body: {
977
+ from_type: opts.fromType,
978
+ from_key: opts.fromKey,
979
+ to_type: opts.toType,
980
+ to_key: opts.toKey,
981
+ relation: opts.relation,
982
+ metadata: opts.metadata ?? {}
983
+ },
984
+ signal: opts.signal
985
+ });
986
+ }
987
+ };
988
+
989
+ // src/resources/usage.ts
990
+ var UsageResource = class {
991
+ constructor(ctx) {
992
+ this.ctx = ctx;
993
+ }
994
+ ctx;
995
+ /** `GET /api/v1/organizations/{org}/usage`. */
996
+ async get(opts = {}) {
997
+ const org = opts.org ?? this.ctx.defaultOrg;
998
+ if (!org) {
999
+ throw new CAVSError("No organization specified for usage.get (pass { org } or set CAVS_ORG).");
1000
+ }
1001
+ return this.ctx.http.requestJson({
1002
+ path: `/api/v1/organizations/${encodeURIComponent(org)}/usage`,
1003
+ signal: opts.signal
1004
+ });
1005
+ }
1006
+ };
1007
+
1008
+ // src/resources/serviceAccounts.ts
1009
+ var ServiceAccountsResource = class {
1010
+ constructor(ctx) {
1011
+ this.ctx = ctx;
1012
+ }
1013
+ ctx;
1014
+ org(explicit) {
1015
+ const org = explicit ?? this.ctx.defaultOrg;
1016
+ if (!org) {
1017
+ throw new CAVSError("No organization specified (pass { org } or set CAVS_ORG).");
1018
+ }
1019
+ return org;
1020
+ }
1021
+ base(org) {
1022
+ return `/api/v1/organizations/${encodeURIComponent(this.org(org))}/service-accounts`;
1023
+ }
1024
+ /** `GET …/service-accounts`. */
1025
+ async list(opts = {}) {
1026
+ return this.ctx.http.requestJson({ path: this.base(opts.org), signal: opts.signal });
1027
+ }
1028
+ /** `POST …/service-accounts`. */
1029
+ async create(opts) {
1030
+ return this.ctx.http.requestJson({
1031
+ method: "POST",
1032
+ path: this.base(opts.org),
1033
+ body: {
1034
+ name: opts.name,
1035
+ description: opts.description,
1036
+ scopes: opts.scopes,
1037
+ repository_ids: opts.repositoryIds,
1038
+ environment: opts.environment
1039
+ },
1040
+ signal: opts.signal
1041
+ });
1042
+ }
1043
+ /** `PATCH …/service-accounts/{id}` — enable/disable, edit scopes/repos. */
1044
+ async update(id, patch, opts = {}) {
1045
+ return this.ctx.http.requestJson({
1046
+ method: "PATCH",
1047
+ path: `${this.base(opts.org)}/${encodeURIComponent(id)}`,
1048
+ body: {
1049
+ disabled: patch.disabled,
1050
+ scopes: patch.scopes,
1051
+ repository_ids: patch.repositoryIds
1052
+ },
1053
+ signal: opts.signal
1054
+ });
1055
+ }
1056
+ /** `DELETE …/service-accounts/{id}`. */
1057
+ async delete(id, opts = {}) {
1058
+ await this.ctx.http.requestJson({
1059
+ method: "DELETE",
1060
+ path: `${this.base(opts.org)}/${encodeURIComponent(id)}`,
1061
+ signal: opts.signal
1062
+ });
1063
+ }
1064
+ /** `POST …/service-accounts/{id}/keys` — the secret is returned once. */
1065
+ async createKey(id, opts = {}) {
1066
+ return this.ctx.http.requestJson({
1067
+ method: "POST",
1068
+ path: `${this.base(opts.org)}/${encodeURIComponent(id)}/keys`,
1069
+ signal: opts.signal
1070
+ });
1071
+ }
1072
+ /** `POST …/service-accounts/{id}/keys/{key}/rotate`. */
1073
+ async rotateKey(id, keyId, opts = {}) {
1074
+ return this.ctx.http.requestJson({
1075
+ method: "POST",
1076
+ path: `${this.base(opts.org)}/${encodeURIComponent(id)}/keys/${encodeURIComponent(keyId)}/rotate`,
1077
+ signal: opts.signal
1078
+ });
1079
+ }
1080
+ /** `DELETE …/service-accounts/{id}/keys/{key}`. */
1081
+ async deleteKey(id, keyId, opts = {}) {
1082
+ await this.ctx.http.requestJson({
1083
+ method: "DELETE",
1084
+ path: `${this.base(opts.org)}/${encodeURIComponent(id)}/keys/${encodeURIComponent(keyId)}`,
1085
+ signal: opts.signal
1086
+ });
1087
+ }
1088
+ };
1089
+
1090
+ // src/client.ts
1091
+ var DEFAULT_API = "https://api.cavs.dev";
1092
+ var CAVS = class _CAVS {
1093
+ http;
1094
+ defaultOrg;
1095
+ artifacts;
1096
+ snapshots;
1097
+ releases;
1098
+ runs;
1099
+ lineage;
1100
+ usage;
1101
+ serviceAccounts;
1102
+ constructor(options = {}) {
1103
+ const token = resolveToken(options.token);
1104
+ const api = (options.api ?? process.env.CAVS_API ?? DEFAULT_API).replace(/\/+$/, "");
1105
+ this.defaultOrg = options.org ?? process.env.CAVS_ORG;
1106
+ this.http = new HttpClient({
1107
+ baseUrl: api,
1108
+ token,
1109
+ fetch: options.fetch,
1110
+ telemetry: {
1111
+ integration: options.integration,
1112
+ integrationVersion: options.integrationVersion,
1113
+ runId: options.runId
1114
+ },
1115
+ timeoutMs: options.timeoutMs,
1116
+ maxRetries: options.maxRetries,
1117
+ onDeprecation: options.onDeprecation,
1118
+ retryBaseMs: options.retryBaseMs,
1119
+ sleepImpl: options.sleepImpl
1120
+ });
1121
+ const ctx = { http: this.http, defaultOrg: this.defaultOrg };
1122
+ this.artifacts = new ArtifactsResource(ctx);
1123
+ this.snapshots = new SnapshotsResource(ctx);
1124
+ this.releases = new ReleasesResource(ctx);
1125
+ this.runs = new RunsResource(ctx);
1126
+ this.lineage = new LineageResource(ctx);
1127
+ this.usage = new UsageResource(ctx);
1128
+ this.serviceAccounts = new ServiceAccountsResource(ctx);
1129
+ }
1130
+ /** Construct a client from the environment (`CAVS_TOKEN`, `CAVS_API`, `CAVS_ORG`). */
1131
+ static fromEnv(options = {}) {
1132
+ return new _CAVS(options);
1133
+ }
1134
+ /** Resolve the org to use for org-scoped calls. */
1135
+ resolveOrg(explicit) {
1136
+ const org = explicit ?? this.defaultOrg;
1137
+ if (!org) {
1138
+ throw new CAVSError(
1139
+ "No organization specified. Pass { org } to the call or the constructor, or set CAVS_ORG."
1140
+ );
1141
+ }
1142
+ return org;
1143
+ }
1144
+ /** `GET /api/v1/users/me` — the authenticated identity. */
1145
+ async me(signal) {
1146
+ return this.http.requestJson({ path: "/api/v1/users/me", signal });
1147
+ }
1148
+ /** `GET /api/v1/search?q=` — cross-org search. */
1149
+ async search(q, signal) {
1150
+ return this.http.requestJson({ path: "/api/v1/search", query: { q }, signal });
1151
+ }
1152
+ /** `GET /api/v1/resolve?uri=` — resolve a `cavs://` reference to concrete objects. */
1153
+ async resolve(reference, signal) {
1154
+ parse(reference);
1155
+ return this.http.requestJson({ path: "/api/v1/resolve", query: { uri: reference }, signal });
1156
+ }
1157
+ /** `GET /api/v1/organizations/{org}/repositories`. */
1158
+ async listRepositories(org, signal) {
1159
+ const slug = this.resolveOrg(org);
1160
+ return this.http.requestJson({
1161
+ path: `/api/v1/organizations/${encodeURIComponent(slug)}/repositories`,
1162
+ signal
1163
+ });
1164
+ }
1165
+ };
1166
+ // Annotate the CommonJS export names for ESM import in node:
1167
+ 0 && (module.exports = {
1168
+ AuthenticationError,
1169
+ AuthorizationError,
1170
+ CAVS,
1171
+ CAVSError,
1172
+ ChecksumError,
1173
+ ConflictError,
1174
+ DEFAULT_API,
1175
+ DownloadError,
1176
+ InvalidURIError,
1177
+ NotFoundError,
1178
+ QuotaExceededError,
1179
+ RateLimitError,
1180
+ TemporaryServiceError,
1181
+ UploadError,
1182
+ VERSION,
1183
+ sha256File,
1184
+ uri
1185
+ });
1186
+ //# sourceMappingURL=index.cjs.map