artifacty 0.1.0 → 0.1.2
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 +142 -33
- package/docs/artifact-schema-v1.md +26 -1
- package/docs/integrations.md +84 -8
- package/docs/network-sharing.md +41 -0
- package/docs/release-checklist.md +13 -0
- package/docs/sarif-csv-artifact-plan.md +65 -0
- package/package.json +15 -3
- package/scripts/smoke.sh +2 -1
- package/src/cli.js +77 -8
- package/src/client/editor.js +35 -2
- package/src/client/viewer.js +67 -0
- package/src/lib/background.js +344 -0
- package/src/lib/converters.js +448 -10
- package/src/lib/editor-assets.js +45 -2
- package/src/lib/installer.js +13 -4
- package/src/lib/render.js +348 -8
- package/src/lib/storage.js +60 -4
- package/src/lib/token.js +25 -0
- package/src/mcp-server.js +8 -6
- package/src/server.js +104 -25
package/src/mcp-server.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import readline from "node:readline";
|
|
3
3
|
import {
|
|
4
|
+
ARTIFACT_FORMATS,
|
|
5
|
+
ARTIFACT_TYPES,
|
|
4
6
|
archiveArtifact,
|
|
5
7
|
createArtifact,
|
|
6
8
|
createStore,
|
|
@@ -23,12 +25,12 @@ const nativeArtifactInputSchema = {
|
|
|
23
25
|
content: { type: "string", description: "Artifact content." },
|
|
24
26
|
format: {
|
|
25
27
|
type: "string",
|
|
26
|
-
enum:
|
|
28
|
+
enum: ARTIFACT_FORMATS,
|
|
27
29
|
description: "Content format."
|
|
28
30
|
},
|
|
29
31
|
artifactType: {
|
|
30
32
|
type: "string",
|
|
31
|
-
enum:
|
|
33
|
+
enum: ARTIFACT_TYPES
|
|
32
34
|
},
|
|
33
35
|
schemaVersion: {
|
|
34
36
|
type: "number",
|
|
@@ -109,11 +111,11 @@ const tools = [
|
|
|
109
111
|
payload: { type: "object", description: "Structured agent payload when available." },
|
|
110
112
|
format: {
|
|
111
113
|
type: "string",
|
|
112
|
-
enum:
|
|
114
|
+
enum: ARTIFACT_FORMATS
|
|
113
115
|
},
|
|
114
116
|
artifactType: {
|
|
115
117
|
type: "string",
|
|
116
|
-
enum:
|
|
118
|
+
enum: ARTIFACT_TYPES
|
|
117
119
|
},
|
|
118
120
|
schemaVersion: {
|
|
119
121
|
type: "number",
|
|
@@ -166,11 +168,11 @@ const tools = [
|
|
|
166
168
|
content: { type: "string" },
|
|
167
169
|
format: {
|
|
168
170
|
type: "string",
|
|
169
|
-
enum:
|
|
171
|
+
enum: ARTIFACT_FORMATS
|
|
170
172
|
},
|
|
171
173
|
artifactType: {
|
|
172
174
|
type: "string",
|
|
173
|
-
enum:
|
|
175
|
+
enum: ARTIFACT_TYPES
|
|
174
176
|
},
|
|
175
177
|
schemaVersion: {
|
|
176
178
|
type: "number",
|
package/src/server.js
CHANGED
|
@@ -17,13 +17,15 @@ import {
|
|
|
17
17
|
} from "./lib/storage.js";
|
|
18
18
|
import { convertAgentArtifact } from "./lib/converters.js";
|
|
19
19
|
import { createLineDiff } from "./lib/diff.js";
|
|
20
|
-
import { EDITOR_CLIENT_PATH, editorClientFilePath, editorVendorPath } from "./lib/editor-assets.js";
|
|
20
|
+
import { EDITOR_CLIENT_PATH, VIEWER_CLIENT_PATH, editorClientFilePath, editorVendorPath, viewerClientFilePath } from "./lib/editor-assets.js";
|
|
21
21
|
import { localeFromBodyOrUrl, localeFromUrl, localizedHref } from "./lib/i18n.js";
|
|
22
22
|
import { requireToken, securityConfig, validateServerExposure } from "./lib/security.js";
|
|
23
23
|
import { writeServerState } from "./lib/server-state.js";
|
|
24
|
+
import { generateToken } from "./lib/token.js";
|
|
24
25
|
import {
|
|
25
26
|
renderArtifactFormPage,
|
|
26
27
|
renderArtifactPage,
|
|
28
|
+
renderReactFramePage,
|
|
27
29
|
renderDashboard,
|
|
28
30
|
renderDiffPage,
|
|
29
31
|
renderImportArtifactPage,
|
|
@@ -146,7 +148,11 @@ export async function handleRequest({ request, response, store, host, port, secu
|
|
|
146
148
|
const method = headOnly ? "GET" : request.method;
|
|
147
149
|
|
|
148
150
|
if (method === "GET" && pathname === EDITOR_CLIENT_PATH) {
|
|
149
|
-
return sendJavaScriptFile(response, editorClientFilePath(PACKAGE_ROOT), headOnly);
|
|
151
|
+
return sendJavaScriptFile(response, editorClientFilePath(PACKAGE_ROOT), headOnly, request);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (method === "GET" && pathname === VIEWER_CLIENT_PATH) {
|
|
155
|
+
return sendJavaScriptFile(response, viewerClientFilePath(PACKAGE_ROOT), headOnly, request);
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
if (method === "GET" && pathname.startsWith("/vendor/npm/")) {
|
|
@@ -155,7 +161,7 @@ export async function handleRequest({ request, response, store, host, port, secu
|
|
|
155
161
|
if (!vendorPath) {
|
|
156
162
|
return sendJson(response, { error: "Not found" }, 404, headOnly);
|
|
157
163
|
}
|
|
158
|
-
return sendJavaScriptFile(response, vendorPath, headOnly);
|
|
164
|
+
return sendJavaScriptFile(response, vendorPath, headOnly, request);
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
if (pathname.startsWith("/api/")) {
|
|
@@ -375,6 +381,27 @@ export async function handleRequest({ request, response, store, host, port, secu
|
|
|
375
381
|
return sendJson(response, decorateArtifactUrls(artifact, baseUrl));
|
|
376
382
|
}
|
|
377
383
|
|
|
384
|
+
const reactFrameMatch = /^\/artifacts\/([^/]+)\/react-frame$/.exec(pathname);
|
|
385
|
+
if (reactFrameMatch && method === "GET") {
|
|
386
|
+
if (process.env.ARTIFACTY_ENABLE_REACT_RENDERER !== "true") {
|
|
387
|
+
return sendJson(response, { error: "React renderer is disabled" }, 403, headOnly);
|
|
388
|
+
}
|
|
389
|
+
const artifact = await getArtifact(store, reactFrameMatch[1], {
|
|
390
|
+
version: url.searchParams.get("version") || undefined,
|
|
391
|
+
audit: auditContext(request, "web-react-frame")
|
|
392
|
+
});
|
|
393
|
+
if (artifact.version.format !== "react") {
|
|
394
|
+
return sendJson(response, { error: "Artifact version is not a React artifact" }, 400, headOnly);
|
|
395
|
+
}
|
|
396
|
+
return sendHtml(
|
|
397
|
+
response,
|
|
398
|
+
renderReactFramePage({ title: artifact.title, content: artifact.content }),
|
|
399
|
+
200,
|
|
400
|
+
headOnly,
|
|
401
|
+
reactFrameContentSecurityPolicy()
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
378
405
|
const artifactMatch = /^\/artifacts\/([^/]+)(?:\/raw)?$/.exec(pathname);
|
|
379
406
|
if (artifactMatch && method === "GET") {
|
|
380
407
|
const artifact = await getArtifact(store, artifactMatch[1], {
|
|
@@ -477,35 +504,64 @@ export function sendJson(response, data, statusCode = 200, headOnly = false) {
|
|
|
477
504
|
response.end(headOnly ? undefined : `${JSON.stringify(data, null, 2)}\n`);
|
|
478
505
|
}
|
|
479
506
|
|
|
480
|
-
export function sendHtml(response, html, statusCode = 200, headOnly = false) {
|
|
507
|
+
export function sendHtml(response, html, statusCode = 200, headOnly = false, contentSecurityPolicy = defaultContentSecurityPolicy()) {
|
|
481
508
|
response.writeHead(statusCode, {
|
|
482
509
|
"content-type": "text/html; charset=utf-8",
|
|
483
510
|
"cache-control": "no-store",
|
|
484
511
|
"x-content-type-options": "nosniff",
|
|
485
|
-
"content-security-policy":
|
|
486
|
-
"default-src 'self' data: blob:",
|
|
487
|
-
"frame-src 'self' data: blob:",
|
|
488
|
-
"img-src 'self' data: blob:",
|
|
489
|
-
"style-src 'self' 'unsafe-inline'",
|
|
490
|
-
"script-src 'self' 'unsafe-inline'",
|
|
491
|
-
"object-src 'none'",
|
|
492
|
-
"base-uri 'none'",
|
|
493
|
-
"form-action 'self'"
|
|
494
|
-
].join("; ")
|
|
512
|
+
"content-security-policy": contentSecurityPolicy
|
|
495
513
|
});
|
|
496
514
|
response.end(headOnly ? undefined : html);
|
|
497
515
|
}
|
|
498
516
|
|
|
499
|
-
|
|
517
|
+
function defaultContentSecurityPolicy() {
|
|
518
|
+
return [
|
|
519
|
+
"default-src 'self' data: blob:",
|
|
520
|
+
"frame-src 'self' data: blob:",
|
|
521
|
+
"img-src 'self' data: blob:",
|
|
522
|
+
"style-src 'self' 'unsafe-inline'",
|
|
523
|
+
"script-src 'self' 'unsafe-inline'",
|
|
524
|
+
"object-src 'none'",
|
|
525
|
+
"base-uri 'none'",
|
|
526
|
+
"form-action 'self'"
|
|
527
|
+
].join("; ");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function reactFrameContentSecurityPolicy() {
|
|
531
|
+
return [
|
|
532
|
+
"default-src 'none'",
|
|
533
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
|
534
|
+
"style-src 'self' 'unsafe-inline'",
|
|
535
|
+
"img-src data: blob:",
|
|
536
|
+
"font-src data:",
|
|
537
|
+
"connect-src 'none'",
|
|
538
|
+
"object-src 'none'",
|
|
539
|
+
"base-uri 'none'",
|
|
540
|
+
"form-action 'none'"
|
|
541
|
+
].join("; ");
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export async function sendJavaScriptFile(response, filePath, headOnly = false, request = null) {
|
|
500
545
|
const content = headOnly ? "" : await readFile(filePath, "utf8");
|
|
501
546
|
response.writeHead(200, {
|
|
502
547
|
"content-type": "text/javascript; charset=utf-8",
|
|
503
548
|
"cache-control": "no-store",
|
|
549
|
+
...javascriptCorsHeaders(request),
|
|
504
550
|
"x-content-type-options": "nosniff"
|
|
505
551
|
});
|
|
506
552
|
response.end(headOnly ? undefined : content);
|
|
507
553
|
}
|
|
508
554
|
|
|
555
|
+
function javascriptCorsHeaders(request) {
|
|
556
|
+
if (request?.headers?.origin !== "null") {
|
|
557
|
+
return {};
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
"access-control-allow-origin": "null",
|
|
561
|
+
vary: "Origin"
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
509
565
|
export function sendRedirect(response, location) {
|
|
510
566
|
response.writeHead(303, {
|
|
511
567
|
location,
|
|
@@ -548,16 +604,29 @@ function isMain(metaUrl) {
|
|
|
548
604
|
}
|
|
549
605
|
|
|
550
606
|
if (isMain(import.meta.url)) {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
.
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
607
|
+
runServerMain(parseServerArgs(process.argv.slice(2))).catch((error) => {
|
|
608
|
+
process.stderr.write(`${error.stack || error.message}\n`);
|
|
609
|
+
process.exitCode = 1;
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function runServerMain(options) {
|
|
614
|
+
if (options.generateToken && options.apiToken) {
|
|
615
|
+
throw new Error("Use either --api-token or --generate-token, not both");
|
|
616
|
+
}
|
|
617
|
+
const generatedToken = options.generateToken ? generateToken(options) : null;
|
|
618
|
+
const { url, store } = await startServer({
|
|
619
|
+
...options,
|
|
620
|
+
apiToken: generatedToken?.token || options.apiToken
|
|
621
|
+
});
|
|
622
|
+
process.stderr.write(`Artifacty listening on ${url}\n`);
|
|
623
|
+
process.stderr.write(`Store: ${store.home}\n`);
|
|
624
|
+
if (generatedToken) {
|
|
625
|
+
process.stderr.write(`API token: ${generatedToken.token}\n`);
|
|
626
|
+
process.stderr.write(`HTTP header: ${generatedToken.header}\n`);
|
|
627
|
+
process.stderr.write(`Create URL: ${url}/new?token=${encodeURIComponent(generatedToken.token)}\n`);
|
|
628
|
+
process.stderr.write(`Import URL: ${url}/import?token=${encodeURIComponent(generatedToken.token)}\n`);
|
|
629
|
+
}
|
|
561
630
|
}
|
|
562
631
|
|
|
563
632
|
function parseServerArgs(args) {
|
|
@@ -570,6 +639,16 @@ function parseServerArgs(args) {
|
|
|
570
639
|
options.port = Number(args[++index]);
|
|
571
640
|
} else if (arg === "--home") {
|
|
572
641
|
options.home = args[++index];
|
|
642
|
+
} else if (arg === "--api-token") {
|
|
643
|
+
options.apiToken = args[++index];
|
|
644
|
+
} else if (arg === "--share-mode") {
|
|
645
|
+
options.shareMode = args[++index];
|
|
646
|
+
} else if (arg === "--bytes") {
|
|
647
|
+
options.bytes = Number(args[++index]);
|
|
648
|
+
} else if (arg === "--generate-token") {
|
|
649
|
+
options.generateToken = true;
|
|
650
|
+
} else if (arg === "--allow-secrets") {
|
|
651
|
+
options.allowSecrets = true;
|
|
573
652
|
}
|
|
574
653
|
}
|
|
575
654
|
return options;
|