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