@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/README.md +85 -0
- package/dist/index.cjs +1186 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +735 -0
- package/dist/index.d.ts +735 -0
- package/dist/index.js +1139 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
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
|