@the-focus-ai/artifacts 1.0.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 +83 -0
- package/dist/src/auth-clerk.d.ts +21 -0
- package/dist/src/auth-clerk.d.ts.map +1 -0
- package/dist/src/auth-clerk.js +97 -0
- package/dist/src/auth-clerk.js.map +1 -0
- package/dist/src/auth.d.ts +68 -0
- package/dist/src/auth.d.ts.map +1 -0
- package/dist/src/auth.js +141 -0
- package/dist/src/auth.js.map +1 -0
- package/dist/src/cli.d.ts +23 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +186 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/http.d.ts +6 -0
- package/dist/src/http.d.ts.map +1 -0
- package/dist/src/http.js +62 -0
- package/dist/src/http.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/local-config.d.ts +44 -0
- package/dist/src/local-config.d.ts.map +1 -0
- package/dist/src/local-config.js +123 -0
- package/dist/src/local-config.js.map +1 -0
- package/dist/src/login-flow.d.ts +12 -0
- package/dist/src/login-flow.d.ts.map +1 -0
- package/dist/src/login-flow.js +124 -0
- package/dist/src/login-flow.js.map +1 -0
- package/dist/src/publication.d.ts +142 -0
- package/dist/src/publication.d.ts.map +1 -0
- package/dist/src/publication.js +703 -0
- package/dist/src/publication.js.map +1 -0
- package/dist/src/storage/artifact-content.d.ts +50 -0
- package/dist/src/storage/artifact-content.d.ts.map +1 -0
- package/dist/src/storage/artifact-content.js +117 -0
- package/dist/src/storage/artifact-content.js.map +1 -0
- package/dist/src/storage/publication-metadata.d.ts +67 -0
- package/dist/src/storage/publication-metadata.d.ts.map +1 -0
- package/dist/src/storage/publication-metadata.js +201 -0
- package/dist/src/storage/publication-metadata.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, extname, isAbsolute, relative, resolve, sep, } from "node:path";
|
|
4
|
+
import { authenticatePublisherToken, createNeonPublisherTokenStore, } from "./auth.js";
|
|
5
|
+
import { FilePublicationStateStore, resolvePublisherToken, } from "./local-config.js";
|
|
6
|
+
import { VercelBlobArtifactContentStore, } from "./storage/artifact-content.js";
|
|
7
|
+
import { createNeonPublicationMetadataStore, } from "./storage/publication-metadata.js";
|
|
8
|
+
export const singleFileEntryArtifactPath = "index.html";
|
|
9
|
+
export const defaultPublicBaseUrl = "https://artifacts.thefocus.ai";
|
|
10
|
+
const directoryManifestContentType = "application/vnd.thefocus.artifact-manifest+json; version=1";
|
|
11
|
+
const directoryManifestArtifactPath = "manifest.json";
|
|
12
|
+
const directoryManifestKind = "directory-artifact-manifest.v1";
|
|
13
|
+
const maxSingleFileBytes = 25 * 1024 * 1024;
|
|
14
|
+
const maxTotalArtifactBytes = 100 * 1024 * 1024;
|
|
15
|
+
const maxArtifactFileCount = 1_000;
|
|
16
|
+
export async function publishSingleFileArtifact(input) {
|
|
17
|
+
const absoluteFilePath = resolve(input.filePath);
|
|
18
|
+
const fileStats = await stat(absoluteFilePath);
|
|
19
|
+
assertFileWithinSingleFileLimit(fileStats.size, toArtifactPath(basename(absoluteFilePath)));
|
|
20
|
+
const html = await readFile(absoluteFilePath);
|
|
21
|
+
const opaqueId = input.opaqueId ?? createOpaqueId();
|
|
22
|
+
const manifestRef = input.manifestRef ?? createManifestRef(html);
|
|
23
|
+
const written = await input.contentStore.write({
|
|
24
|
+
publicationId: opaqueId,
|
|
25
|
+
manifestRef,
|
|
26
|
+
artifactPath: singleFileEntryArtifactPath,
|
|
27
|
+
body: html,
|
|
28
|
+
contentType: "text/html; charset=utf-8",
|
|
29
|
+
});
|
|
30
|
+
const publication = await input.metadataStore.create({
|
|
31
|
+
opaqueId,
|
|
32
|
+
publisherEmail: input.publisherEmail,
|
|
33
|
+
activeManifestRef: manifestRef,
|
|
34
|
+
activeArtifactLocator: written.url,
|
|
35
|
+
localSourcePath: absoluteFilePath,
|
|
36
|
+
revisionWindowExpiresAt: null,
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
opaqueId,
|
|
40
|
+
publicationUrlPath: publication.publicationUrlPath,
|
|
41
|
+
publicationUrl: absolutePublicationUrl(input.publicBaseUrl, publication.publicationUrlPath),
|
|
42
|
+
manifestRef,
|
|
43
|
+
artifactLocator: written.url,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export async function publishDirectoryArtifact(input) {
|
|
47
|
+
const absoluteDirectoryPath = resolve(input.directoryPath);
|
|
48
|
+
const directoryStats = await stat(absoluteDirectoryPath);
|
|
49
|
+
if (!directoryStats.isDirectory()) {
|
|
50
|
+
throw new Error(`Directory Artifact source is not a directory: ${input.directoryPath}`);
|
|
51
|
+
}
|
|
52
|
+
const entryArtifactPath = normalizeEntryPage(input.entryPage ?? "index.html");
|
|
53
|
+
const { files: artifactFiles, excludedArtifactPaths: dirExcludedArtifactPaths, } = await collectDirectoryArtifactFiles(absoluteDirectoryPath);
|
|
54
|
+
const entryFile = artifactFiles.find((file) => file.artifactPath === entryArtifactPath);
|
|
55
|
+
if (!entryFile) {
|
|
56
|
+
if (!input.entryPage) {
|
|
57
|
+
throw new Error("Directory Artifacts require a root index.html by default. Pass --entry-page <file.html> to choose a different HTML Entry Page.");
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Entry Page not found in directory Artifact: ${entryArtifactPath}`);
|
|
60
|
+
}
|
|
61
|
+
if (!entryArtifactPath.endsWith(".html")) {
|
|
62
|
+
throw new Error("Directory Artifact Entry Page must be an HTML file.");
|
|
63
|
+
}
|
|
64
|
+
const opaqueId = input.opaqueId ?? createOpaqueId();
|
|
65
|
+
const manifestRef = input.manifestRef ?? createManifestRef(Buffer.from(absoluteDirectoryPath));
|
|
66
|
+
const manifest = {
|
|
67
|
+
kind: directoryManifestKind,
|
|
68
|
+
entryArtifactPath,
|
|
69
|
+
files: {},
|
|
70
|
+
};
|
|
71
|
+
for (const file of artifactFiles) {
|
|
72
|
+
const body = await readFile(file.absolutePath);
|
|
73
|
+
const contentType = contentTypeForArtifactPath(file.artifactPath);
|
|
74
|
+
const written = await input.contentStore.write({
|
|
75
|
+
publicationId: opaqueId,
|
|
76
|
+
manifestRef,
|
|
77
|
+
artifactPath: file.artifactPath,
|
|
78
|
+
body,
|
|
79
|
+
contentType,
|
|
80
|
+
});
|
|
81
|
+
manifest.files[file.artifactPath] = {
|
|
82
|
+
locator: written.url,
|
|
83
|
+
contentType,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const manifestWritten = await input.contentStore.write({
|
|
87
|
+
publicationId: opaqueId,
|
|
88
|
+
manifestRef,
|
|
89
|
+
artifactPath: directoryManifestArtifactPath,
|
|
90
|
+
body: JSON.stringify(manifest),
|
|
91
|
+
contentType: directoryManifestContentType,
|
|
92
|
+
});
|
|
93
|
+
const publication = await input.metadataStore.create({
|
|
94
|
+
opaqueId,
|
|
95
|
+
publisherEmail: input.publisherEmail,
|
|
96
|
+
activeManifestRef: manifestRef,
|
|
97
|
+
activeArtifactLocator: manifestWritten.url,
|
|
98
|
+
localSourcePath: absoluteDirectoryPath,
|
|
99
|
+
revisionWindowExpiresAt: null,
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
opaqueId,
|
|
103
|
+
publicationUrlPath: publication.publicationUrlPath,
|
|
104
|
+
publicationUrl: absolutePublicationUrl(input.publicBaseUrl, publication.publicationUrlPath),
|
|
105
|
+
manifestRef,
|
|
106
|
+
manifestLocator: manifestWritten.url,
|
|
107
|
+
entryArtifactPath,
|
|
108
|
+
artifactPaths: Object.keys(manifest.files),
|
|
109
|
+
excludedArtifactPaths: dirExcludedArtifactPaths,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export async function publishArtifact(input) {
|
|
113
|
+
const now = input.now?.() ?? new Date();
|
|
114
|
+
const localSourcePath = await realpath(input.sourcePath);
|
|
115
|
+
const revisionWindowExpiresAt = addMinutes(now, 15);
|
|
116
|
+
const packaged = await packageArtifactSource(input, localSourcePath);
|
|
117
|
+
if (input.updatePublicationUrl) {
|
|
118
|
+
const opaqueId = opaqueIdFromPublicationUrl(input.updatePublicationUrl);
|
|
119
|
+
if (!opaqueId)
|
|
120
|
+
throw new Error("--update requires a Publication URL.");
|
|
121
|
+
return updateExistingPublication({
|
|
122
|
+
...input,
|
|
123
|
+
localSourcePath,
|
|
124
|
+
opaqueId,
|
|
125
|
+
packaged,
|
|
126
|
+
revisionWindowExpiresAt,
|
|
127
|
+
decision: "explicit-update",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const existingState = await input.stateStore.get(localSourcePath);
|
|
131
|
+
if (!input.forceNew && existingState) {
|
|
132
|
+
const publication = await input.metadataStore.getByOpaqueId(existingState.opaqueId);
|
|
133
|
+
if (publication?.status === "active") {
|
|
134
|
+
if (existingState.revisionWindowExpiresAt.getTime() >= now.getTime()) {
|
|
135
|
+
return updateExistingPublication({
|
|
136
|
+
...input,
|
|
137
|
+
localSourcePath,
|
|
138
|
+
opaqueId: existingState.opaqueId,
|
|
139
|
+
packaged,
|
|
140
|
+
revisionWindowExpiresAt,
|
|
141
|
+
decision: "updated",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return createFreshPublication({
|
|
145
|
+
...input,
|
|
146
|
+
localSourcePath,
|
|
147
|
+
packaged,
|
|
148
|
+
revisionWindowExpiresAt,
|
|
149
|
+
decision: "created-after-expiry",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return createFreshPublication({
|
|
153
|
+
...input,
|
|
154
|
+
localSourcePath,
|
|
155
|
+
packaged,
|
|
156
|
+
revisionWindowExpiresAt,
|
|
157
|
+
decision: "created-after-removed-local-state",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return createFreshPublication({
|
|
161
|
+
...input,
|
|
162
|
+
localSourcePath,
|
|
163
|
+
packaged,
|
|
164
|
+
revisionWindowExpiresAt,
|
|
165
|
+
decision: input.forceNew ? "forced-new" : "created",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
export async function servePublicationRequest({ request, metadataStore, contentStore, }) {
|
|
169
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
170
|
+
return new Response("Method not allowed", {
|
|
171
|
+
status: 405,
|
|
172
|
+
headers: publicationSafetyHeaders(),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const route = publicationRouteFromUrl(request.url);
|
|
176
|
+
if (!route) {
|
|
177
|
+
return new Response("Not found", {
|
|
178
|
+
status: 404,
|
|
179
|
+
headers: publicationSafetyHeaders(),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const publication = await metadataStore.getByOpaqueId(route.opaqueId);
|
|
183
|
+
if (!publication || publication.status !== "active") {
|
|
184
|
+
return new Response("Not found", {
|
|
185
|
+
status: 404,
|
|
186
|
+
headers: publicationSafetyHeaders(),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const activeContent = await contentStore.read(publication.activeArtifactLocator);
|
|
190
|
+
if (!activeContent) {
|
|
191
|
+
return new Response("Not found", {
|
|
192
|
+
status: 404,
|
|
193
|
+
headers: publicationSafetyHeaders(),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const content = isDirectoryManifest(activeContent)
|
|
197
|
+
? await readDirectoryArtifactContent(route.artifactPath, activeContent, contentStore)
|
|
198
|
+
: route.artifactPath === "" ||
|
|
199
|
+
route.artifactPath === singleFileEntryArtifactPath
|
|
200
|
+
? activeContent
|
|
201
|
+
: null;
|
|
202
|
+
if (!content) {
|
|
203
|
+
return new Response("Not found", {
|
|
204
|
+
status: 404,
|
|
205
|
+
headers: publicationSafetyHeaders(),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const headers = publicationSafetyHeaders({
|
|
209
|
+
"content-type": content.contentType ?? "text/html; charset=utf-8",
|
|
210
|
+
});
|
|
211
|
+
return new Response(request.method === "HEAD" ? null : new Uint8Array(content.body), {
|
|
212
|
+
status: 200,
|
|
213
|
+
headers,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
export function publicationSafetyHeaders(headers = {}) {
|
|
217
|
+
return new Headers({
|
|
218
|
+
"cache-control": "no-store",
|
|
219
|
+
"x-robots-tag": "noindex, nofollow",
|
|
220
|
+
...headers,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
export function opaqueIdFromPublicationUrl(url) {
|
|
224
|
+
return publicationRouteFromUrl(url)?.opaqueId ?? null;
|
|
225
|
+
}
|
|
226
|
+
export function publicationRouteFromUrl(url) {
|
|
227
|
+
const { pathname } = new URL(url);
|
|
228
|
+
const match = pathname.match(/^\/a\/([^/]+)(?:\/(.*))?$/);
|
|
229
|
+
if (!match?.[1])
|
|
230
|
+
return null;
|
|
231
|
+
return {
|
|
232
|
+
opaqueId: match[1],
|
|
233
|
+
artifactPath: decodeURIComponent(match[2] ?? ""),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
export function absolutePublicationUrl(publicBaseUrl, publicationUrlPath) {
|
|
237
|
+
const base = publicBaseUrl.endsWith("/")
|
|
238
|
+
? publicBaseUrl
|
|
239
|
+
: `${publicBaseUrl}/`;
|
|
240
|
+
return new URL(publicationUrlPath.replace(/^\/+/, ""), base).toString();
|
|
241
|
+
}
|
|
242
|
+
export function createOpaqueId() {
|
|
243
|
+
return randomBytes(8).toString("base64url");
|
|
244
|
+
}
|
|
245
|
+
function createManifestRef(body) {
|
|
246
|
+
const hash = createHash("sha256").update(body).digest("hex").slice(0, 16);
|
|
247
|
+
return `${Date.now().toString(36)}-${hash}-${randomUUID().slice(0, 8)}`;
|
|
248
|
+
}
|
|
249
|
+
export async function publishSingleFileArtifactWithPublisherToken(input) {
|
|
250
|
+
const publisherEmail = await authenticatePublisherToken({
|
|
251
|
+
token: input.publisherToken,
|
|
252
|
+
store: input.tokenStore,
|
|
253
|
+
});
|
|
254
|
+
return publishSingleFileArtifact({
|
|
255
|
+
filePath: input.filePath,
|
|
256
|
+
publicBaseUrl: input.publicBaseUrl,
|
|
257
|
+
publisherEmail,
|
|
258
|
+
metadataStore: input.metadataStore,
|
|
259
|
+
contentStore: input.contentStore,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
export async function publishArtifactFromEnvironment(sourcePath, options = {}) {
|
|
263
|
+
const env = options.env ?? process.env;
|
|
264
|
+
const token = await resolvePublisherToken({
|
|
265
|
+
env,
|
|
266
|
+
configDir: options.configDir,
|
|
267
|
+
});
|
|
268
|
+
if (!token.token)
|
|
269
|
+
throw new Error("A valid Publisher Token is required");
|
|
270
|
+
const publisherEmail = await authenticatePublisherToken({
|
|
271
|
+
token: token.token,
|
|
272
|
+
store: createNeonPublisherTokenStore(),
|
|
273
|
+
});
|
|
274
|
+
return publishArtifact({
|
|
275
|
+
sourcePath,
|
|
276
|
+
publicBaseUrl: options.publicBaseUrl ??
|
|
277
|
+
env.ARTIFACTS_PUBLIC_BASE_URL ??
|
|
278
|
+
defaultPublicBaseUrl,
|
|
279
|
+
publisherEmail,
|
|
280
|
+
metadataStore: createNeonPublicationMetadataStore(),
|
|
281
|
+
contentStore: new VercelBlobArtifactContentStore(),
|
|
282
|
+
stateStore: new FilePublicationStateStore(options.configDir),
|
|
283
|
+
entryPage: options.entryPage,
|
|
284
|
+
forceNew: options.forceNew,
|
|
285
|
+
updatePublicationUrl: options.updatePublicationUrl,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
export async function publishSingleFileArtifactFromEnvironment(filePath, options = {}) {
|
|
289
|
+
const env = options.env ?? process.env;
|
|
290
|
+
const token = await resolvePublisherToken({ env });
|
|
291
|
+
return publishSingleFileArtifactWithPublisherToken({
|
|
292
|
+
filePath,
|
|
293
|
+
publicBaseUrl: options.publicBaseUrl ??
|
|
294
|
+
env.ARTIFACTS_PUBLIC_BASE_URL ??
|
|
295
|
+
defaultPublicBaseUrl,
|
|
296
|
+
publisherToken: token.token ?? requiredEnv("THEFOCUS_ARTIFACTS_TOKEN"),
|
|
297
|
+
metadataStore: createNeonPublicationMetadataStore(),
|
|
298
|
+
contentStore: new VercelBlobArtifactContentStore(),
|
|
299
|
+
tokenStore: createNeonPublisherTokenStore(),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
export async function removePublication(input) {
|
|
303
|
+
await authenticatePublisherToken({
|
|
304
|
+
token: input.publisherToken,
|
|
305
|
+
store: input.tokenStore,
|
|
306
|
+
});
|
|
307
|
+
const opaqueId = opaqueIdFromPublicationUrl(input.publicationUrl);
|
|
308
|
+
if (!opaqueId)
|
|
309
|
+
throw new Error("remove requires a Publication URL.");
|
|
310
|
+
const publication = await input.metadataStore.getByOpaqueId(opaqueId);
|
|
311
|
+
if (!publication || publication.status !== "active") {
|
|
312
|
+
return {
|
|
313
|
+
opaqueId,
|
|
314
|
+
publicationUrl: input.publicationUrl,
|
|
315
|
+
status: "not-found",
|
|
316
|
+
deletedLocators: [],
|
|
317
|
+
clearedLocalSourcePath: null,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const locators = await activePublicationLocators(publication.activeArtifactLocator, input.contentStore);
|
|
321
|
+
await input.metadataStore.markRemoved(opaqueId);
|
|
322
|
+
await input.contentStore.delete(locators);
|
|
323
|
+
if (publication.localSourcePath) {
|
|
324
|
+
await input.stateStore.clear(publication.localSourcePath);
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
opaqueId,
|
|
328
|
+
publicationUrl: input.publicationUrl,
|
|
329
|
+
status: "removed",
|
|
330
|
+
deletedLocators: locators,
|
|
331
|
+
clearedLocalSourcePath: publication.localSourcePath,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
export async function removePublicationFromEnvironment(publicationUrl, options = {}) {
|
|
335
|
+
const token = await resolvePublisherToken({
|
|
336
|
+
env: options.env,
|
|
337
|
+
configDir: options.configDir,
|
|
338
|
+
});
|
|
339
|
+
if (!token.token)
|
|
340
|
+
throw new Error("A valid Publisher Token is required");
|
|
341
|
+
return removePublication({
|
|
342
|
+
publicationUrl,
|
|
343
|
+
publisherToken: token.token,
|
|
344
|
+
metadataStore: createNeonPublicationMetadataStore(),
|
|
345
|
+
contentStore: new VercelBlobArtifactContentStore(),
|
|
346
|
+
tokenStore: createNeonPublisherTokenStore(),
|
|
347
|
+
stateStore: new FilePublicationStateStore(options.configDir),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
export function removePublicationSummary(result) {
|
|
351
|
+
return result.status === "removed"
|
|
352
|
+
? `Removed ${result.publicationUrl}`
|
|
353
|
+
: `Publication not found or already removed: ${result.publicationUrl}`;
|
|
354
|
+
}
|
|
355
|
+
export function singleFilePublishSummary(result, options = {}) {
|
|
356
|
+
return publishArtifactSummary(result, options);
|
|
357
|
+
}
|
|
358
|
+
export function publishArtifactSummary(result, options = {}) {
|
|
359
|
+
const action = result.decision?.includes("update") ? "Updated" : "Published";
|
|
360
|
+
const suffix = result.revisionWindowExpiresAt
|
|
361
|
+
? ` (Revision Window until ${result.revisionWindowExpiresAt.toISOString()})`
|
|
362
|
+
: "";
|
|
363
|
+
const lines = [
|
|
364
|
+
`${action} ${basename(result.publicationUrlPath)} to ${result.publicationUrl}${suffix}`,
|
|
365
|
+
];
|
|
366
|
+
const excluded = result.excludedArtifactPaths ?? [];
|
|
367
|
+
if (excluded.length > 0) {
|
|
368
|
+
lines.push(`Warning: ${excluded.length} files or directories were excluded by packaging safety rules. Re-run with --verbose to see details.`);
|
|
369
|
+
if (options.verbose) {
|
|
370
|
+
lines.push("Excluded paths:", ...excluded.map((path) => `- ${path}`));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return lines.join("\n");
|
|
374
|
+
}
|
|
375
|
+
async function packageArtifactSource(input, localSourcePath) {
|
|
376
|
+
return {
|
|
377
|
+
manifestRef: "pending",
|
|
378
|
+
activeArtifactLocator: "pending",
|
|
379
|
+
artifactPaths: [],
|
|
380
|
+
excludedArtifactPaths: [],
|
|
381
|
+
async writeForPublication(publicationId) {
|
|
382
|
+
const sourceStats = await stat(localSourcePath);
|
|
383
|
+
if (sourceStats.isDirectory()) {
|
|
384
|
+
return writeDirectoryArtifactContent({
|
|
385
|
+
directoryPath: localSourcePath,
|
|
386
|
+
entryPage: input.entryPage,
|
|
387
|
+
contentStore: input.contentStore,
|
|
388
|
+
publicationId,
|
|
389
|
+
manifestRefFactory: input.manifestRefFactory,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return writeSingleFileArtifactContent({
|
|
393
|
+
filePath: localSourcePath,
|
|
394
|
+
contentStore: input.contentStore,
|
|
395
|
+
publicationId,
|
|
396
|
+
manifestRefFactory: input.manifestRefFactory,
|
|
397
|
+
});
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
async function createFreshPublication(input) {
|
|
402
|
+
const opaqueId = input.opaqueIdFactory?.() ?? createOpaqueId();
|
|
403
|
+
const packaged = await input.packaged.writeForPublication(opaqueId);
|
|
404
|
+
const publication = await input.metadataStore.create({
|
|
405
|
+
opaqueId,
|
|
406
|
+
publisherEmail: input.publisherEmail,
|
|
407
|
+
activeManifestRef: packaged.manifestRef,
|
|
408
|
+
activeArtifactLocator: packaged.activeArtifactLocator,
|
|
409
|
+
localSourcePath: input.localSourcePath,
|
|
410
|
+
revisionWindowExpiresAt: input.revisionWindowExpiresAt,
|
|
411
|
+
});
|
|
412
|
+
const publicationUrl = absolutePublicationUrl(input.publicBaseUrl, publication.publicationUrlPath);
|
|
413
|
+
await input.stateStore.set({
|
|
414
|
+
localSourcePath: input.localSourcePath,
|
|
415
|
+
opaqueId,
|
|
416
|
+
publicationUrl,
|
|
417
|
+
revisionWindowExpiresAt: input.revisionWindowExpiresAt,
|
|
418
|
+
});
|
|
419
|
+
return {
|
|
420
|
+
opaqueId,
|
|
421
|
+
publicationUrlPath: publication.publicationUrlPath,
|
|
422
|
+
publicationUrl,
|
|
423
|
+
manifestRef: packaged.manifestRef,
|
|
424
|
+
activeArtifactLocator: packaged.activeArtifactLocator,
|
|
425
|
+
revisionWindowExpiresAt: input.revisionWindowExpiresAt,
|
|
426
|
+
decision: input.decision,
|
|
427
|
+
artifactPaths: packaged.artifactPaths,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async function updateExistingPublication(input) {
|
|
431
|
+
const existing = await input.metadataStore.getByOpaqueId(input.opaqueId);
|
|
432
|
+
if (!existing || existing.status !== "active") {
|
|
433
|
+
throw new Error("Publication URL cannot be updated because it is not active.");
|
|
434
|
+
}
|
|
435
|
+
const oldLocators = await activePublicationLocators(existing.activeArtifactLocator, input.contentStore);
|
|
436
|
+
const packaged = await input.packaged.writeForPublication(input.opaqueId);
|
|
437
|
+
const publication = await input.metadataStore.update(input.opaqueId, {
|
|
438
|
+
activeManifestRef: packaged.manifestRef,
|
|
439
|
+
activeArtifactLocator: packaged.activeArtifactLocator,
|
|
440
|
+
localSourcePath: input.localSourcePath,
|
|
441
|
+
revisionWindowExpiresAt: input.revisionWindowExpiresAt,
|
|
442
|
+
});
|
|
443
|
+
if (!publication)
|
|
444
|
+
throw new Error("Publication URL cannot be updated.");
|
|
445
|
+
await input.contentStore.delete(oldLocators);
|
|
446
|
+
const publicationUrl = absolutePublicationUrl(input.publicBaseUrl, publication.publicationUrlPath);
|
|
447
|
+
await input.stateStore.set({
|
|
448
|
+
localSourcePath: input.localSourcePath,
|
|
449
|
+
opaqueId: input.opaqueId,
|
|
450
|
+
publicationUrl,
|
|
451
|
+
revisionWindowExpiresAt: input.revisionWindowExpiresAt,
|
|
452
|
+
});
|
|
453
|
+
return {
|
|
454
|
+
opaqueId: input.opaqueId,
|
|
455
|
+
publicationUrlPath: publication.publicationUrlPath,
|
|
456
|
+
publicationUrl,
|
|
457
|
+
manifestRef: packaged.manifestRef,
|
|
458
|
+
activeArtifactLocator: packaged.activeArtifactLocator,
|
|
459
|
+
revisionWindowExpiresAt: input.revisionWindowExpiresAt,
|
|
460
|
+
decision: input.decision,
|
|
461
|
+
artifactPaths: packaged.artifactPaths,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
async function writeSingleFileArtifactContent(input) {
|
|
465
|
+
const html = await readFile(input.filePath);
|
|
466
|
+
const manifestRef = input.manifestRefFactory?.(html) ?? createManifestRef(html);
|
|
467
|
+
const written = await input.contentStore.write({
|
|
468
|
+
publicationId: input.publicationId,
|
|
469
|
+
manifestRef,
|
|
470
|
+
artifactPath: singleFileEntryArtifactPath,
|
|
471
|
+
body: html,
|
|
472
|
+
contentType: "text/html; charset=utf-8",
|
|
473
|
+
});
|
|
474
|
+
return {
|
|
475
|
+
manifestRef,
|
|
476
|
+
activeArtifactLocator: written.url,
|
|
477
|
+
artifactPaths: [singleFileEntryArtifactPath],
|
|
478
|
+
excludedArtifactPaths: [],
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
async function writeDirectoryArtifactContent(input) {
|
|
482
|
+
const entryArtifactPath = normalizeEntryPage(input.entryPage ?? "index.html");
|
|
483
|
+
const { files: artifactFiles, excludedArtifactPaths: dirExcluded } = await collectDirectoryArtifactFiles(input.directoryPath);
|
|
484
|
+
const entryFile = artifactFiles.find((file) => file.artifactPath === entryArtifactPath);
|
|
485
|
+
if (!entryFile) {
|
|
486
|
+
if (!input.entryPage) {
|
|
487
|
+
throw new Error("Directory Artifacts require a root index.html by default. Pass --entry-page <file.html> to choose a different HTML Entry Page.");
|
|
488
|
+
}
|
|
489
|
+
throw new Error(`Entry Page not found in directory Artifact: ${entryArtifactPath}`);
|
|
490
|
+
}
|
|
491
|
+
if (!entryArtifactPath.endsWith(".html")) {
|
|
492
|
+
throw new Error("Directory Artifact Entry Page must be an HTML file.");
|
|
493
|
+
}
|
|
494
|
+
const manifestRef = input.manifestRefFactory?.(Buffer.from(input.directoryPath)) ??
|
|
495
|
+
createManifestRef(Buffer.from(input.directoryPath));
|
|
496
|
+
const manifest = {
|
|
497
|
+
kind: directoryManifestKind,
|
|
498
|
+
entryArtifactPath,
|
|
499
|
+
files: {},
|
|
500
|
+
};
|
|
501
|
+
for (const file of artifactFiles) {
|
|
502
|
+
const body = await readFile(file.absolutePath);
|
|
503
|
+
const contentType = contentTypeForArtifactPath(file.artifactPath);
|
|
504
|
+
const written = await input.contentStore.write({
|
|
505
|
+
publicationId: input.publicationId,
|
|
506
|
+
manifestRef,
|
|
507
|
+
artifactPath: file.artifactPath,
|
|
508
|
+
body,
|
|
509
|
+
contentType,
|
|
510
|
+
});
|
|
511
|
+
manifest.files[file.artifactPath] = { locator: written.url, contentType };
|
|
512
|
+
}
|
|
513
|
+
const manifestWritten = await input.contentStore.write({
|
|
514
|
+
publicationId: input.publicationId,
|
|
515
|
+
manifestRef,
|
|
516
|
+
artifactPath: directoryManifestArtifactPath,
|
|
517
|
+
body: JSON.stringify(manifest),
|
|
518
|
+
contentType: directoryManifestContentType,
|
|
519
|
+
});
|
|
520
|
+
return {
|
|
521
|
+
manifestRef,
|
|
522
|
+
activeArtifactLocator: manifestWritten.url,
|
|
523
|
+
artifactPaths: Object.keys(manifest.files),
|
|
524
|
+
excludedArtifactPaths: dirExcluded,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function shouldExcludeArtifactPath(artifactPath, isDirectory) {
|
|
528
|
+
const parts = artifactPath.split("/").filter(Boolean);
|
|
529
|
+
const name = parts.at(-1) ?? artifactPath;
|
|
530
|
+
if (parts[0] === ".well-known")
|
|
531
|
+
return false;
|
|
532
|
+
if (parts.some((part) => part.startsWith(".")))
|
|
533
|
+
return true;
|
|
534
|
+
if (excludedDirectoryNames.has(name))
|
|
535
|
+
return true;
|
|
536
|
+
if (!isDirectory && excludedFileNames.has(name))
|
|
537
|
+
return true;
|
|
538
|
+
if (!isDirectory && excludedFileExtensions.has(extname(name).toLowerCase())) {
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
function assertFileWithinSingleFileLimit(size, artifactPath) {
|
|
544
|
+
if (size > maxSingleFileBytes) {
|
|
545
|
+
throw new Error(`Artifact file ${artifactPath} exceeds the 25 MB single-file limit.`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function assertDirectoryArtifactWithinPreflightLimits(files) {
|
|
549
|
+
if (files.length > maxArtifactFileCount) {
|
|
550
|
+
throw new Error(`Directory Artifact has ${files.length} files and exceeds the 1,000 file limit.`);
|
|
551
|
+
}
|
|
552
|
+
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
|
553
|
+
if (totalSize > maxTotalArtifactBytes) {
|
|
554
|
+
throw new Error(`Directory Artifact total size exceeds the 100 MB total Artifact limit.`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const excludedDirectoryNames = new Set([
|
|
558
|
+
"node_modules",
|
|
559
|
+
"bower_components",
|
|
560
|
+
"vendor",
|
|
561
|
+
".git",
|
|
562
|
+
".hg",
|
|
563
|
+
".svn",
|
|
564
|
+
".next",
|
|
565
|
+
".nuxt",
|
|
566
|
+
".cache",
|
|
567
|
+
"cache",
|
|
568
|
+
".turbo",
|
|
569
|
+
"coverage",
|
|
570
|
+
"dist-cache",
|
|
571
|
+
"__pycache__",
|
|
572
|
+
]);
|
|
573
|
+
const excludedFileNames = new Set([
|
|
574
|
+
".env",
|
|
575
|
+
".env.local",
|
|
576
|
+
".env.production",
|
|
577
|
+
".npmrc",
|
|
578
|
+
".yarnrc",
|
|
579
|
+
"id_rsa",
|
|
580
|
+
"id_ed25519",
|
|
581
|
+
]);
|
|
582
|
+
const excludedFileExtensions = new Set([".pem", ".key", ".p12", ".pfx"]);
|
|
583
|
+
async function activePublicationLocators(activeArtifactLocator, contentStore) {
|
|
584
|
+
const activeContent = await contentStore.read(activeArtifactLocator);
|
|
585
|
+
if (!activeContent || !isDirectoryManifest(activeContent)) {
|
|
586
|
+
return [activeArtifactLocator];
|
|
587
|
+
}
|
|
588
|
+
const manifest = JSON.parse(activeContent.body.toString("utf8"));
|
|
589
|
+
return [
|
|
590
|
+
activeArtifactLocator,
|
|
591
|
+
...Object.values(manifest.files).map((file) => file.locator),
|
|
592
|
+
];
|
|
593
|
+
}
|
|
594
|
+
function addMinutes(date, minutes) {
|
|
595
|
+
return new Date(date.getTime() + minutes * 60_000);
|
|
596
|
+
}
|
|
597
|
+
async function collectDirectoryArtifactFiles(directoryPath) {
|
|
598
|
+
const files = [];
|
|
599
|
+
const excludedArtifactPaths = [];
|
|
600
|
+
async function visit(currentDirectory) {
|
|
601
|
+
const entries = await readdir(currentDirectory, { withFileTypes: true });
|
|
602
|
+
for (const entry of entries) {
|
|
603
|
+
const absolutePath = resolve(currentDirectory, entry.name);
|
|
604
|
+
const artifactPath = toArtifactPath(relative(directoryPath, absolutePath));
|
|
605
|
+
if (entry.isSymbolicLink()) {
|
|
606
|
+
throw new Error(`Symlinks are not supported in directory Artifacts: ${artifactPath}`);
|
|
607
|
+
}
|
|
608
|
+
if (shouldExcludeArtifactPath(artifactPath, entry.isDirectory())) {
|
|
609
|
+
excludedArtifactPaths.push(entry.isDirectory() ? `${artifactPath}/` : artifactPath);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (entry.isDirectory()) {
|
|
613
|
+
await visit(absolutePath);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (!entry.isFile())
|
|
617
|
+
continue;
|
|
618
|
+
const fileStats = await stat(absolutePath);
|
|
619
|
+
assertFileWithinSingleFileLimit(fileStats.size, artifactPath);
|
|
620
|
+
files.push({
|
|
621
|
+
absolutePath,
|
|
622
|
+
artifactPath,
|
|
623
|
+
size: fileStats.size,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
await visit(directoryPath);
|
|
628
|
+
assertDirectoryArtifactWithinPreflightLimits(files);
|
|
629
|
+
return {
|
|
630
|
+
files: files.sort((a, b) => a.artifactPath.localeCompare(b.artifactPath)),
|
|
631
|
+
excludedArtifactPaths: excludedArtifactPaths.sort((a, b) => a.localeCompare(b)),
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function normalizeEntryPage(entryPage) {
|
|
635
|
+
if (isAbsolute(entryPage)) {
|
|
636
|
+
throw new Error("Directory Artifact Entry Page must be inside the directory.");
|
|
637
|
+
}
|
|
638
|
+
const artifactPath = toArtifactPath(entryPage);
|
|
639
|
+
if (artifactPath.startsWith("../") || artifactPath === "..") {
|
|
640
|
+
throw new Error("Directory Artifact Entry Page must be inside the directory.");
|
|
641
|
+
}
|
|
642
|
+
return artifactPath;
|
|
643
|
+
}
|
|
644
|
+
function toArtifactPath(path) {
|
|
645
|
+
return path.split(sep).join("/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
646
|
+
}
|
|
647
|
+
async function readDirectoryArtifactContent(requestedArtifactPath, manifestContent, contentStore) {
|
|
648
|
+
const manifest = JSON.parse(manifestContent.body.toString("utf8"));
|
|
649
|
+
const artifactPath = resolveDirectoryArtifactPath(requestedArtifactPath, manifest);
|
|
650
|
+
if (!artifactPath)
|
|
651
|
+
return null;
|
|
652
|
+
const file = manifest.files[artifactPath];
|
|
653
|
+
if (!file)
|
|
654
|
+
return null;
|
|
655
|
+
return contentStore.read(file.locator);
|
|
656
|
+
}
|
|
657
|
+
function resolveDirectoryArtifactPath(requestedArtifactPath, manifest) {
|
|
658
|
+
const normalized = toArtifactPath(requestedArtifactPath);
|
|
659
|
+
if (normalized === "")
|
|
660
|
+
return manifest.entryArtifactPath;
|
|
661
|
+
if (normalized.endsWith("/"))
|
|
662
|
+
return `${normalized}index.html`;
|
|
663
|
+
if (manifest.files[normalized])
|
|
664
|
+
return normalized;
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
function isDirectoryManifest(content) {
|
|
668
|
+
return content.contentType?.startsWith(directoryManifestContentType) ?? false;
|
|
669
|
+
}
|
|
670
|
+
function contentTypeForArtifactPath(artifactPath) {
|
|
671
|
+
switch (extname(artifactPath).toLowerCase()) {
|
|
672
|
+
case ".html":
|
|
673
|
+
case ".htm":
|
|
674
|
+
return "text/html; charset=utf-8";
|
|
675
|
+
case ".css":
|
|
676
|
+
return "text/css; charset=utf-8";
|
|
677
|
+
case ".js":
|
|
678
|
+
case ".mjs":
|
|
679
|
+
return "text/javascript; charset=utf-8";
|
|
680
|
+
case ".json":
|
|
681
|
+
return "application/json; charset=utf-8";
|
|
682
|
+
case ".svg":
|
|
683
|
+
return "image/svg+xml";
|
|
684
|
+
case ".png":
|
|
685
|
+
return "image/png";
|
|
686
|
+
case ".jpg":
|
|
687
|
+
case ".jpeg":
|
|
688
|
+
return "image/jpeg";
|
|
689
|
+
case ".gif":
|
|
690
|
+
return "image/gif";
|
|
691
|
+
case ".webp":
|
|
692
|
+
return "image/webp";
|
|
693
|
+
default:
|
|
694
|
+
return "application/octet-stream";
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function requiredEnv(name) {
|
|
698
|
+
const value = process.env[name];
|
|
699
|
+
if (!value)
|
|
700
|
+
throw new Error(`${name} is required`);
|
|
701
|
+
return value;
|
|
702
|
+
}
|
|
703
|
+
//# sourceMappingURL=publication.js.map
|