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/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: ["html", "markdown", "text", "json"],
28
+ enum: ARTIFACT_FORMATS,
27
29
  description: "Content format."
28
30
  },
29
31
  artifactType: {
30
32
  type: "string",
31
- enum: ["document", "html-page", "handoff", "code-review", "test-report", "dashboard", "design-option", "diff-walkthrough", "bundle", "asset", "unknown"]
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: ["html", "markdown", "text", "json"]
114
+ enum: ARTIFACT_FORMATS
113
115
  },
114
116
  artifactType: {
115
117
  type: "string",
116
- enum: ["document", "html-page", "handoff", "code-review", "test-report", "dashboard", "design-option", "diff-walkthrough", "bundle", "asset", "unknown"]
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: ["html", "markdown", "text", "json"]
171
+ enum: ARTIFACT_FORMATS
170
172
  },
171
173
  artifactType: {
172
174
  type: "string",
173
- enum: ["document", "html-page", "handoff", "code-review", "test-report", "dashboard", "design-option", "diff-walkthrough", "bundle", "asset", "unknown"]
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
- export async function sendJavaScriptFile(response, filePath, headOnly = false) {
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
- const options = parseServerArgs(process.argv.slice(2));
552
- startServer(options)
553
- .then(({ url, store }) => {
554
- process.stderr.write(`Artifacty listening on ${url}\n`);
555
- process.stderr.write(`Store: ${store.home}\n`);
556
- })
557
- .catch((error) => {
558
- process.stderr.write(`${error.stack || error.message}\n`);
559
- process.exitCode = 1;
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;