@fungi.computer/burrow 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +111 -2
- package/dist/artifacts.d.ts +53 -0
- package/dist/artifacts.js +1 -0
- package/dist/cloudflare.d.ts +98 -0
- package/dist/cloudflare.js +595 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/namespace.d.ts +93 -0
- package/dist/namespace.js +1 -0
- package/dist/rest.d.ts +17 -0
- package/dist/rest.js +288 -0
- package/docs/api/README.md +15 -0
- package/docs/api/cloudflare.md +271 -0
- package/docs/api/index.md +207 -0
- package/docs/api/namespace.md +234 -0
- package/docs/home.ts +27 -0
- package/package.json +68 -3
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
import { listServerRefs } from "isomorphic-git";
|
|
3
|
+
import http from "isomorphic-git/http/web";
|
|
4
|
+
import * as Exit from "effect/Exit";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
const artifactsErrorSchema = Schema.Struct({
|
|
7
|
+
code: Schema.Literals([
|
|
8
|
+
"NOT_FOUND",
|
|
9
|
+
"ALREADY_EXISTS",
|
|
10
|
+
"IMPORT_IN_PROGRESS",
|
|
11
|
+
"FORK_IN_PROGRESS",
|
|
12
|
+
]),
|
|
13
|
+
});
|
|
14
|
+
const REPOSITORY_NAME = /^[a-zA-Z0-9._-]{1,128}$/;
|
|
15
|
+
const REMOTE_PATH = /^\/git\/([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)\.git$/;
|
|
16
|
+
const RESPONSE_HEADERS = ["cache-control", "content-type", "pragma"];
|
|
17
|
+
const REPOSITORY_READY_POLL_INTERVAL_MS = 500;
|
|
18
|
+
const REPOSITORY_READY_TIMEOUT_MS = 120_000;
|
|
19
|
+
const REPOSITORY_METADATA_RETRY_DELAYS_MS = [
|
|
20
|
+
20, 40, 80, 160, 320, 640, 800,
|
|
21
|
+
];
|
|
22
|
+
const repositoryStatusPageSchema = Schema.Struct({
|
|
23
|
+
repos: Schema.Array(Schema.Struct({
|
|
24
|
+
name: Schema.String,
|
|
25
|
+
status: Schema.Literals(["ready", "importing", "forking"]),
|
|
26
|
+
})),
|
|
27
|
+
total: Schema.Number,
|
|
28
|
+
cursor: Schema.optional(Schema.String),
|
|
29
|
+
});
|
|
30
|
+
/** A repository did not finish importing or forking within the readiness window. */
|
|
31
|
+
export class ArtifactsForkStuckError extends Error {
|
|
32
|
+
repository;
|
|
33
|
+
status;
|
|
34
|
+
name = "ArtifactsForkStuckError";
|
|
35
|
+
constructor(repository, status) {
|
|
36
|
+
super(`Artifacts repository ${repository} did not become ready (${status})`);
|
|
37
|
+
this.repository = repository;
|
|
38
|
+
this.status = status;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function readRepositoryStatus(binding, name) {
|
|
42
|
+
let cursor;
|
|
43
|
+
do {
|
|
44
|
+
const options = cursor === undefined ? { limit: 200 } : { limit: 200, cursor };
|
|
45
|
+
const raw = await binding.list(options);
|
|
46
|
+
const page = Schema.decodeUnknownSync(repositoryStatusPageSchema)(raw);
|
|
47
|
+
const repository = page.repos.find((item) => item.name === name);
|
|
48
|
+
if (repository)
|
|
49
|
+
return repository.status;
|
|
50
|
+
cursor = page.cursor;
|
|
51
|
+
} while (cursor !== undefined);
|
|
52
|
+
return "missing";
|
|
53
|
+
}
|
|
54
|
+
async function repositoryHandleWhenReady(binding, name) {
|
|
55
|
+
const deadline = Date.now() + REPOSITORY_READY_TIMEOUT_MS;
|
|
56
|
+
let status = "missing";
|
|
57
|
+
while (Date.now() < deadline) {
|
|
58
|
+
status = await readRepositoryStatus(binding, name);
|
|
59
|
+
if (status === "ready" || status === "missing") {
|
|
60
|
+
try {
|
|
61
|
+
const repository = await binding.get(name);
|
|
62
|
+
// Workers get resolves the repository; REST get only addresses it.
|
|
63
|
+
// Metadata promotion requires the latter's live provider check.
|
|
64
|
+
if (repository.info !== undefined)
|
|
65
|
+
await repository.info();
|
|
66
|
+
return repository;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const code = error instanceof Error ? artifactsErrorCode(error) : undefined;
|
|
70
|
+
// Namespace listing and handle availability can disagree during a fork.
|
|
71
|
+
// Only the provider's documented readiness refusals extend this wait;
|
|
72
|
+
// NOT_FOUND still allows ordinary repository discovery to create it.
|
|
73
|
+
if (code === "FORK_IN_PROGRESS")
|
|
74
|
+
status = "forking";
|
|
75
|
+
else if (code === "IMPORT_IN_PROGRESS")
|
|
76
|
+
status = "importing";
|
|
77
|
+
else
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const remaining = deadline - Date.now();
|
|
82
|
+
if (remaining <= 0)
|
|
83
|
+
break;
|
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(REPOSITORY_READY_POLL_INTERVAL_MS, remaining)));
|
|
85
|
+
}
|
|
86
|
+
throw new ArtifactsForkStuckError(name, status);
|
|
87
|
+
}
|
|
88
|
+
function repositoryName(name) {
|
|
89
|
+
if (!REPOSITORY_NAME.test(name))
|
|
90
|
+
throw new TypeError("Invalid repository name");
|
|
91
|
+
return name;
|
|
92
|
+
}
|
|
93
|
+
function repositoryMetadata(repository, parentRepository) {
|
|
94
|
+
if (repository.id.length === 0 ||
|
|
95
|
+
!REPOSITORY_NAME.test(repository.name) ||
|
|
96
|
+
repository.defaultBranch !== "main")
|
|
97
|
+
throw new Error("Artifacts repository identity is invalid");
|
|
98
|
+
artifactsRemote(repository.remote, repository.name);
|
|
99
|
+
if (parentRepository !== null) {
|
|
100
|
+
repositoryName(parentRepository);
|
|
101
|
+
if (parentRepository === repository.name)
|
|
102
|
+
throw new Error("Artifacts repository parent identity is invalid");
|
|
103
|
+
}
|
|
104
|
+
return Object.freeze({
|
|
105
|
+
id: repository.id,
|
|
106
|
+
name: repository.name,
|
|
107
|
+
defaultBranch: repository.defaultBranch,
|
|
108
|
+
remote: repository.remote,
|
|
109
|
+
parentRepository,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function repositoryIdentity(repository) {
|
|
113
|
+
return Object.freeze({
|
|
114
|
+
id: repository.id,
|
|
115
|
+
name: repository.name,
|
|
116
|
+
defaultBranch: repository.defaultBranch,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function artifactsErrorCode(error) {
|
|
120
|
+
const decoded = Schema.decodeUnknownExit(artifactsErrorSchema)(error);
|
|
121
|
+
return Exit.isSuccess(decoded) ? decoded.value.code : undefined;
|
|
122
|
+
}
|
|
123
|
+
async function withRevokedInitialToken(options) {
|
|
124
|
+
let repository;
|
|
125
|
+
const operation = Promise.resolve().then(async () => {
|
|
126
|
+
repository = await options.lookup();
|
|
127
|
+
return options.use(repository);
|
|
128
|
+
});
|
|
129
|
+
const outcome = await operation.then((value) => ({ kind: "succeeded", value }), (error) => ({ kind: "failed", error }));
|
|
130
|
+
const revocation = await Promise.resolve()
|
|
131
|
+
.then(async () => {
|
|
132
|
+
// A transient read-back failure must not skip revocation. Retry the lookup
|
|
133
|
+
// in the cleanup path so a token returned by create/fork is still revoked.
|
|
134
|
+
const owner = repository ?? (await options.lookup());
|
|
135
|
+
if (!(await owner.revokeToken(options.token)))
|
|
136
|
+
throw new Error("Artifacts initial token could not be revoked");
|
|
137
|
+
})
|
|
138
|
+
.then(() => ({ kind: "revoked" }), (error) => ({ kind: "failed", error }));
|
|
139
|
+
if (outcome.kind === "failed") {
|
|
140
|
+
if (revocation.kind === "failed")
|
|
141
|
+
throw new AggregateError([outcome.error, revocation.error], "Artifacts operation failed and token cleanup failed");
|
|
142
|
+
if (outcome.error instanceof Error)
|
|
143
|
+
throw outcome.error;
|
|
144
|
+
throw new Error("Artifacts operation failed", { cause: outcome.error });
|
|
145
|
+
}
|
|
146
|
+
if (revocation.kind === "failed") {
|
|
147
|
+
if (revocation.error instanceof Error)
|
|
148
|
+
throw revocation.error;
|
|
149
|
+
throw new Error("Artifacts token cleanup failed", {
|
|
150
|
+
cause: revocation.error,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return outcome.value;
|
|
154
|
+
}
|
|
155
|
+
async function recordPendingWithRetry(repositories, metadata, initialToken) {
|
|
156
|
+
for (let attempt = 0;; attempt += 1) {
|
|
157
|
+
try {
|
|
158
|
+
await repositories.recordPending(metadata, initialToken);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
const delayMs = REPOSITORY_METADATA_RETRY_DELAYS_MS[attempt];
|
|
163
|
+
if (delayMs === undefined)
|
|
164
|
+
throw error;
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function finishPendingRepository(binding, repositories, name) {
|
|
170
|
+
const pending = await repositories.readPending(name);
|
|
171
|
+
if (pending === null)
|
|
172
|
+
return null;
|
|
173
|
+
const metadata = parseStoredMetadata(pending.metadata, name);
|
|
174
|
+
const repository = await repositoryHandleWhenReady(binding, name);
|
|
175
|
+
try {
|
|
176
|
+
// Maintained Workers types define false as "not found". Replay uses the
|
|
177
|
+
// persisted plaintext, so this concerns this exact token; unrelated repo
|
|
178
|
+
// tokens do not affect promotion.
|
|
179
|
+
await repository.revokeToken(pending.initialToken);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// The provider error may echo its argument; never attach this plaintext
|
|
183
|
+
// token-bearing failure as a cause that a caller might log.
|
|
184
|
+
throw new Error("Artifacts initial token cleanup could not be confirmed");
|
|
185
|
+
}
|
|
186
|
+
await repositories.activate(metadata, pending.initialToken);
|
|
187
|
+
return metadata;
|
|
188
|
+
}
|
|
189
|
+
async function readDefaultBranchHead(binding, repositories, name, gitHttp = http) {
|
|
190
|
+
const safeName = repositoryName(name);
|
|
191
|
+
const metadata = await requireRepositoryMetadata(binding, repositories, safeName);
|
|
192
|
+
const repository = await repositoryHandleWhenReady(binding, safeName);
|
|
193
|
+
const remote = artifactsRemote(metadata.remote, metadata.name).url;
|
|
194
|
+
const token = await repository.createToken("read", 60);
|
|
195
|
+
return withRevokedInitialToken({
|
|
196
|
+
lookup: () => Promise.resolve(repository),
|
|
197
|
+
token: token.id,
|
|
198
|
+
use: async () => {
|
|
199
|
+
const refs = await listServerRefs({
|
|
200
|
+
http: gitHttp,
|
|
201
|
+
url: remote.toString(),
|
|
202
|
+
headers: { Authorization: `Bearer ${token.plaintext}` },
|
|
203
|
+
prefix: "refs/heads/main",
|
|
204
|
+
});
|
|
205
|
+
const mainRefs = refs.filter((ref) => ref.ref === "refs/heads/main");
|
|
206
|
+
if (mainRefs.length === 0)
|
|
207
|
+
return null;
|
|
208
|
+
const [main] = mainRefs;
|
|
209
|
+
if (mainRefs.length !== 1 || !main || !/^[0-9a-f]{40}$/u.test(main.oid))
|
|
210
|
+
throw new Error("Artifacts main ref is ambiguous or invalid");
|
|
211
|
+
return main.oid;
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async function readRefs(binding, repositories, name, gitHttp = http) {
|
|
216
|
+
const safeName = repositoryName(name);
|
|
217
|
+
const metadata = await requireRepositoryMetadata(binding, repositories, safeName);
|
|
218
|
+
const repository = await repositoryHandleWhenReady(binding, safeName);
|
|
219
|
+
const remote = artifactsRemote(metadata.remote, metadata.name).url;
|
|
220
|
+
const token = await repository.createToken("read", 60);
|
|
221
|
+
return withRevokedInitialToken({
|
|
222
|
+
lookup: () => Promise.resolve(repository),
|
|
223
|
+
token: token.id,
|
|
224
|
+
use: async () => {
|
|
225
|
+
const refs = await listServerRefs({
|
|
226
|
+
http: gitHttp,
|
|
227
|
+
url: remote.toString(),
|
|
228
|
+
headers: { Authorization: `Bearer ${token.plaintext}` },
|
|
229
|
+
});
|
|
230
|
+
return Object.freeze(refs.map(({ ref, oid }) => ({ name: ref, hash: oid })));
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function artifactsRemote(remote, repository) {
|
|
235
|
+
let target;
|
|
236
|
+
try {
|
|
237
|
+
target = new URL(remote);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
throw new TypeError("Artifacts returned an invalid Git remote");
|
|
241
|
+
}
|
|
242
|
+
const suffix = ".artifacts.cloudflare.net";
|
|
243
|
+
const account = target.hostname.toLowerCase().endsWith(suffix)
|
|
244
|
+
? target.hostname.slice(0, -suffix.length)
|
|
245
|
+
: "";
|
|
246
|
+
const match = REMOTE_PATH.exec(target.pathname);
|
|
247
|
+
if (target.protocol !== "https:" ||
|
|
248
|
+
account.length === 0 ||
|
|
249
|
+
account.includes(".") ||
|
|
250
|
+
target.port.length > 0 ||
|
|
251
|
+
target.username.length > 0 ||
|
|
252
|
+
target.password.length > 0 ||
|
|
253
|
+
target.search.length > 0 ||
|
|
254
|
+
target.hash.length > 0 ||
|
|
255
|
+
!match?.[1] ||
|
|
256
|
+
match[2] !== repository)
|
|
257
|
+
throw new TypeError("Artifacts returned an invalid Git remote");
|
|
258
|
+
return { url: target, namespace: match[1] };
|
|
259
|
+
}
|
|
260
|
+
function parseStoredMetadata(metadata, expectedName) {
|
|
261
|
+
const parsed = repositoryMetadata(metadata, metadata.parentRepository);
|
|
262
|
+
if (parsed.name !== expectedName)
|
|
263
|
+
throw new Error("Artifacts repository metadata identity conflicts");
|
|
264
|
+
return parsed;
|
|
265
|
+
}
|
|
266
|
+
function sameRepositoryMetadata(left, right) {
|
|
267
|
+
return (left.id === right.id &&
|
|
268
|
+
left.name === right.name &&
|
|
269
|
+
left.defaultBranch === right.defaultBranch &&
|
|
270
|
+
left.remote === right.remote &&
|
|
271
|
+
left.parentRepository === right.parentRepository);
|
|
272
|
+
}
|
|
273
|
+
async function finishOrReadCreatedRepository(binding, repositories, expected) {
|
|
274
|
+
const finished = await finishPendingRepository(binding, repositories, expected.name);
|
|
275
|
+
const saved = finished ?? (await repositories.read(expected.name));
|
|
276
|
+
if (saved === null)
|
|
277
|
+
throw new Error("Artifacts repository creation was not recorded");
|
|
278
|
+
const verified = parseStoredMetadata(saved, expected.name);
|
|
279
|
+
if (!sameRepositoryMetadata(verified, expected))
|
|
280
|
+
throw new Error("Artifacts repository creation metadata conflicts");
|
|
281
|
+
await repositoryHandleWhenReady(binding, expected.name);
|
|
282
|
+
return verified;
|
|
283
|
+
}
|
|
284
|
+
async function knownRepositoryMetadata(binding, repositories, name) {
|
|
285
|
+
await finishPendingRepository(binding, repositories, name);
|
|
286
|
+
const stored = await repositories.read(name);
|
|
287
|
+
return stored === null ? null : parseStoredMetadata(stored, name);
|
|
288
|
+
}
|
|
289
|
+
async function existingRepositoryMetadata(binding, repositories, name) {
|
|
290
|
+
const stored = await knownRepositoryMetadata(binding, repositories, name);
|
|
291
|
+
if (stored !== null) {
|
|
292
|
+
await repositoryHandleWhenReady(binding, name);
|
|
293
|
+
return stored;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
await repositoryHandleWhenReady(binding, name);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (error instanceof Error && artifactsErrorCode(error) === "NOT_FOUND")
|
|
300
|
+
return null;
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
return waitForRecordedRepositoryMetadata(binding, repositories, name);
|
|
304
|
+
}
|
|
305
|
+
async function requireRepositoryMetadata(binding, repositories, name) {
|
|
306
|
+
const known = await knownRepositoryMetadata(binding, repositories, name);
|
|
307
|
+
if (known !== null)
|
|
308
|
+
return known;
|
|
309
|
+
try {
|
|
310
|
+
await repositoryHandleWhenReady(binding, name);
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
if (error instanceof Error && artifactsErrorCode(error) === "NOT_FOUND")
|
|
314
|
+
throw new Error("Artifacts repository metadata is unavailable", {
|
|
315
|
+
cause: error,
|
|
316
|
+
});
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
return waitForRecordedRepositoryMetadata(binding, repositories, name);
|
|
320
|
+
}
|
|
321
|
+
async function waitForRecordedRepositoryMetadata(binding, repositories, name) {
|
|
322
|
+
for (const delayMs of REPOSITORY_METADATA_RETRY_DELAYS_MS) {
|
|
323
|
+
const metadata = await knownRepositoryMetadata(binding, repositories, name);
|
|
324
|
+
if (metadata !== null)
|
|
325
|
+
return metadata;
|
|
326
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
327
|
+
}
|
|
328
|
+
const metadata = await knownRepositoryMetadata(binding, repositories, name);
|
|
329
|
+
if (metadata !== null)
|
|
330
|
+
return metadata;
|
|
331
|
+
throw new Error("Artifacts repository creation race was not recorded");
|
|
332
|
+
}
|
|
333
|
+
function gitTarget(remote, repository, operation) {
|
|
334
|
+
const { url: target } = artifactsRemote(remote, repository);
|
|
335
|
+
const service = operation.startsWith("upload-pack")
|
|
336
|
+
? "git-upload-pack"
|
|
337
|
+
: "git-receive-pack";
|
|
338
|
+
if (operation.endsWith("advertisement")) {
|
|
339
|
+
target.pathname += "/info/refs";
|
|
340
|
+
target.searchParams.set("service", service);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
target.pathname += `/${service}`;
|
|
344
|
+
}
|
|
345
|
+
return target;
|
|
346
|
+
}
|
|
347
|
+
function requestMethod(operation) {
|
|
348
|
+
return operation.endsWith("advertisement") ? "GET" : "POST";
|
|
349
|
+
}
|
|
350
|
+
function gitContentType(operation) {
|
|
351
|
+
if (operation === "upload-pack-advertisement")
|
|
352
|
+
return "application/x-git-upload-pack-advertisement";
|
|
353
|
+
if (operation === "receive-pack-advertisement")
|
|
354
|
+
return "application/x-git-receive-pack-advertisement";
|
|
355
|
+
if (operation === "upload-pack")
|
|
356
|
+
return "application/x-git-upload-pack-request";
|
|
357
|
+
return "application/x-git-receive-pack-request";
|
|
358
|
+
}
|
|
359
|
+
function gitAcceptType(operation) {
|
|
360
|
+
if (operation === "upload-pack-advertisement")
|
|
361
|
+
return "application/x-git-upload-pack-advertisement";
|
|
362
|
+
if (operation === "receive-pack-advertisement")
|
|
363
|
+
return "application/x-git-receive-pack-advertisement";
|
|
364
|
+
if (operation === "upload-pack")
|
|
365
|
+
return "application/x-git-upload-pack-result";
|
|
366
|
+
return "application/x-git-receive-pack-result";
|
|
367
|
+
}
|
|
368
|
+
function readScope(operation) {
|
|
369
|
+
return operation.startsWith("upload-pack") ? "read" : "write";
|
|
370
|
+
}
|
|
371
|
+
/** Execute Git with an already-held repo credential. The custody owner chooses
|
|
372
|
+
* metadata and credential; caller URLs and authorization headers never enter. */
|
|
373
|
+
export async function exchangeCloudflareArtifactsGit(metadata, input, credential, gitFetch) {
|
|
374
|
+
const name = repositoryName(input.repository);
|
|
375
|
+
if (metadata.name !== name)
|
|
376
|
+
throw new Error("Artifacts repository mismatch");
|
|
377
|
+
const remote = gitTarget(metadata.remote, name, input.operation);
|
|
378
|
+
const method = requestMethod(input.operation);
|
|
379
|
+
const headers = new Headers({
|
|
380
|
+
authorization: `Bearer ${credential}`,
|
|
381
|
+
accept: gitAcceptType(input.operation),
|
|
382
|
+
});
|
|
383
|
+
if (method === "POST")
|
|
384
|
+
headers.set("content-type", gitContentType(input.operation) ?? "application/octet-stream");
|
|
385
|
+
if (input.protocol !== undefined)
|
|
386
|
+
headers.set("Git-Protocol", input.protocol);
|
|
387
|
+
const request = {
|
|
388
|
+
method,
|
|
389
|
+
headers,
|
|
390
|
+
redirect: "manual",
|
|
391
|
+
signal: input.signal ?? null,
|
|
392
|
+
};
|
|
393
|
+
if ("body" in input)
|
|
394
|
+
request.body = input.body;
|
|
395
|
+
const upstream = await gitFetch(remote, request);
|
|
396
|
+
if (upstream.status >= 300 && upstream.status < 400)
|
|
397
|
+
throw new Error("Artifacts Git upstream redirected");
|
|
398
|
+
return upstream;
|
|
399
|
+
}
|
|
400
|
+
async function exchange(binding, repositories, input, gitFetch = (input, init) => fetch(input, init)) {
|
|
401
|
+
const name = repositoryName(input.repository);
|
|
402
|
+
const metadata = await requireRepositoryMetadata(binding, repositories, name);
|
|
403
|
+
const repository = await repositoryHandleWhenReady(binding, name);
|
|
404
|
+
// Preserve the existing refusal before minting if registered metadata is invalid.
|
|
405
|
+
artifactsRemote(metadata.remote, name);
|
|
406
|
+
const token = await repository.createToken(readScope(input.operation), 300);
|
|
407
|
+
let revokePromise;
|
|
408
|
+
const revoke = () => {
|
|
409
|
+
revokePromise ??= repository.revokeToken(token.id).then((revoked) => {
|
|
410
|
+
if (!revoked)
|
|
411
|
+
throw new Error("Artifacts Git token could not be revoked");
|
|
412
|
+
});
|
|
413
|
+
return revokePromise;
|
|
414
|
+
};
|
|
415
|
+
try {
|
|
416
|
+
const upstream = await exchangeCloudflareArtifactsGit(metadata, input, token.plaintext, gitFetch);
|
|
417
|
+
const responseHeaders = new Headers();
|
|
418
|
+
for (const header of RESPONSE_HEADERS) {
|
|
419
|
+
const value = upstream.headers.get(header);
|
|
420
|
+
if (value !== null)
|
|
421
|
+
responseHeaders.set(header, value);
|
|
422
|
+
}
|
|
423
|
+
if (upstream.body === null) {
|
|
424
|
+
await revoke();
|
|
425
|
+
return new Response(null, {
|
|
426
|
+
status: upstream.status,
|
|
427
|
+
headers: responseHeaders,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
const upstreamBody = upstream.body;
|
|
431
|
+
const reader = upstreamBody.getReader();
|
|
432
|
+
const body = new ReadableStream({
|
|
433
|
+
async pull(controller) {
|
|
434
|
+
try {
|
|
435
|
+
const result = await reader.read();
|
|
436
|
+
if (result.done) {
|
|
437
|
+
controller.close();
|
|
438
|
+
await revoke();
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
controller.enqueue(result.value);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
const cancellation = await reader.cancel(error).then(() => ({ kind: "cancelled" }), (cancelError) => cancelError === error
|
|
446
|
+
? { kind: "same-error" }
|
|
447
|
+
: { kind: "failed", error: cancelError });
|
|
448
|
+
const revocation = await revoke().then(() => ({ kind: "revoked" }), (revokeError) => ({
|
|
449
|
+
kind: "failed",
|
|
450
|
+
error: revokeError,
|
|
451
|
+
}));
|
|
452
|
+
const cleanupFailures = [
|
|
453
|
+
...(cancellation.kind === "failed" ? [cancellation.error] : []),
|
|
454
|
+
...(revocation.kind === "failed" ? [revocation.error] : []),
|
|
455
|
+
];
|
|
456
|
+
controller.error(cleanupFailures.length === 0
|
|
457
|
+
? error
|
|
458
|
+
: new AggregateError([error, ...cleanupFailures], "Artifacts Git stream read and cleanup failed", { cause: error }));
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
async cancel(reason) {
|
|
462
|
+
try {
|
|
463
|
+
await reader.cancel(reason);
|
|
464
|
+
}
|
|
465
|
+
finally {
|
|
466
|
+
await revoke();
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
return new Response(body, {
|
|
471
|
+
status: upstream.status,
|
|
472
|
+
headers: responseHeaders,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
await revoke();
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/** Configure the Artifacts owner once; provider remotes and credentials stay private. */
|
|
481
|
+
export function createCloudflareArtifacts(options) {
|
|
482
|
+
const binding = options.namespace;
|
|
483
|
+
const repositories = options.repositories;
|
|
484
|
+
const gitHttp = options.gitHttp ?? http;
|
|
485
|
+
const gitFetch = options.gitFetch ?? ((input, init) => fetch(input, init));
|
|
486
|
+
const recoverForkRepository = async (input) => {
|
|
487
|
+
const template = repositoryName(input.template);
|
|
488
|
+
const destination = repositoryName(input.repository);
|
|
489
|
+
const existing = await existingRepositoryMetadata(binding, repositories, destination);
|
|
490
|
+
if (existing === null)
|
|
491
|
+
return null;
|
|
492
|
+
if (existing.parentRepository !== template)
|
|
493
|
+
throw new Error("Artifacts fork destination belongs to another source");
|
|
494
|
+
const mainHead = await readDefaultBranchHead(binding, repositories, destination);
|
|
495
|
+
if (mainHead === null)
|
|
496
|
+
throw new Error("Artifacts fork destination has no main commit");
|
|
497
|
+
return {
|
|
498
|
+
repository: repositoryIdentity(existing),
|
|
499
|
+
mainHead,
|
|
500
|
+
disposition: "existing",
|
|
501
|
+
};
|
|
502
|
+
};
|
|
503
|
+
return Object.freeze({
|
|
504
|
+
ensureRepository: async (name) => {
|
|
505
|
+
const safeName = repositoryName(name);
|
|
506
|
+
const existing = await existingRepositoryMetadata(binding, repositories, safeName);
|
|
507
|
+
if (existing !== null)
|
|
508
|
+
return repositoryIdentity(existing);
|
|
509
|
+
let created;
|
|
510
|
+
try {
|
|
511
|
+
created = await binding.create(safeName, {
|
|
512
|
+
readOnly: false,
|
|
513
|
+
setDefaultBranch: "main",
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
if (!(error instanceof Error))
|
|
518
|
+
throw error;
|
|
519
|
+
if (artifactsErrorCode(error) !== "ALREADY_EXISTS")
|
|
520
|
+
throw error;
|
|
521
|
+
const raced = await waitForRecordedRepositoryMetadata(binding, repositories, safeName);
|
|
522
|
+
return repositoryIdentity(raced);
|
|
523
|
+
}
|
|
524
|
+
const metadata = repositoryMetadata(created, null);
|
|
525
|
+
if (metadata.name !== safeName)
|
|
526
|
+
throw new Error("Artifacts create returned another repository");
|
|
527
|
+
await recordPendingWithRetry(repositories, metadata, created.token);
|
|
528
|
+
const activated = await finishOrReadCreatedRepository(binding, repositories, metadata);
|
|
529
|
+
return repositoryIdentity(activated);
|
|
530
|
+
},
|
|
531
|
+
forkRepository: async (input) => {
|
|
532
|
+
const recovered = await recoverForkRepository(input);
|
|
533
|
+
if (recovered !== null)
|
|
534
|
+
return recovered;
|
|
535
|
+
const templateName = repositoryName(input.template);
|
|
536
|
+
const destination = repositoryName(input.repository);
|
|
537
|
+
const templateHead = await readDefaultBranchHead(binding, repositories, templateName);
|
|
538
|
+
if (templateHead === null || templateHead !== input.expectedMainHead)
|
|
539
|
+
throw new Error("Artifacts template main changed before fork");
|
|
540
|
+
const template = await repositoryHandleWhenReady(binding, templateName);
|
|
541
|
+
let forked;
|
|
542
|
+
try {
|
|
543
|
+
forked = await template.fork(destination, {
|
|
544
|
+
readOnly: false,
|
|
545
|
+
defaultBranchOnly: true,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
if (!(error instanceof Error) ||
|
|
550
|
+
artifactsErrorCode(error) !== "ALREADY_EXISTS")
|
|
551
|
+
throw error;
|
|
552
|
+
const stored = await waitForRecordedRepositoryMetadata(binding, repositories, destination);
|
|
553
|
+
if (stored.parentRepository !== templateName)
|
|
554
|
+
throw new Error("Artifacts fork destination belongs to another source", { cause: error });
|
|
555
|
+
const mainHead = await readDefaultBranchHead(binding, repositories, destination);
|
|
556
|
+
if (mainHead === null)
|
|
557
|
+
throw new Error("Artifacts fork destination has no main commit", {
|
|
558
|
+
cause: error,
|
|
559
|
+
});
|
|
560
|
+
const recovered = {
|
|
561
|
+
repository: repositoryIdentity(stored),
|
|
562
|
+
mainHead,
|
|
563
|
+
disposition: "existing",
|
|
564
|
+
};
|
|
565
|
+
return recovered;
|
|
566
|
+
}
|
|
567
|
+
const metadata = repositoryMetadata(forked, templateName);
|
|
568
|
+
if (metadata.name !== destination)
|
|
569
|
+
throw new Error("Artifacts fork returned another repository");
|
|
570
|
+
await recordPendingWithRetry(repositories, metadata, forked.token);
|
|
571
|
+
const activated = await finishOrReadCreatedRepository(binding, repositories, metadata);
|
|
572
|
+
const mainHead = await readDefaultBranchHead(binding, repositories, destination);
|
|
573
|
+
if (mainHead === null || mainHead !== templateHead)
|
|
574
|
+
throw new Error("Artifacts fork main does not match template main");
|
|
575
|
+
return {
|
|
576
|
+
repository: repositoryIdentity(activated),
|
|
577
|
+
mainHead,
|
|
578
|
+
disposition: "created",
|
|
579
|
+
};
|
|
580
|
+
},
|
|
581
|
+
recoverForkRepository,
|
|
582
|
+
resolveRepository: async (name) => {
|
|
583
|
+
const safeName = repositoryName(name);
|
|
584
|
+
const metadata = await requireRepositoryMetadata(binding, repositories, safeName);
|
|
585
|
+
await repositoryHandleWhenReady(binding, safeName);
|
|
586
|
+
return repositoryIdentity(metadata);
|
|
587
|
+
},
|
|
588
|
+
readDefaultBranchHead: (name) => readDefaultBranchHead(binding, repositories, name, gitHttp),
|
|
589
|
+
readBranchHeads: (name) => readRefs(binding, repositories, name, gitHttp).then((refs) => refs
|
|
590
|
+
.filter((ref) => ref.name.startsWith("refs/heads/"))
|
|
591
|
+
.map((ref) => ref.hash)),
|
|
592
|
+
readRefs: (name) => readRefs(binding, repositories, name, gitHttp),
|
|
593
|
+
exchangeGit: (input) => exchange(binding, repositories, input, gitFetch),
|
|
594
|
+
});
|
|
595
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { Artifacts, ArtifactsRepository, ArtifactsForkResult, GitExchange, } from "./artifacts.js";
|
|
2
|
+
export { createCloudflareArtifacts, type CloudflareArtifactsNamespace, type CloudflareArtifactsOptions, } from "./cloudflare.js";
|
|
3
|
+
export { createArtifactsRestNamespace, ArtifactsRestError, type ArtifactsRestOptions, } from "./rest.js";
|
|
4
|
+
export type { ArtifactsNamespace, ArtifactsCommit, ArtifactsRepositoryInfo, } from "./namespace.js";
|
package/dist/index.js
ADDED