artifacty 0.10.3 → 0.10.5

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 CHANGED
@@ -163,6 +163,11 @@ the payload explicitly identifies the agent through `agent` or `sourceAgent`.
163
163
  Plain Markdown from these agents stays a normal
164
164
  `document` unless you pass an explicit `artifactType`.
165
165
 
166
+ When `format` is omitted, Artifacty infers common formats from filename,
167
+ content type, and content. HTML files and HTML fragments such as
168
+ `<section>...</section>` are stored as `html` so the browser viewer renders
169
+ them as HTML instead of plain text.
170
+
166
171
  ## Agent Handoff Example
167
172
 
168
173
  One agent can publish a continuation artifact, then another agent can discover it, read the context, and append the next version.
@@ -318,6 +323,7 @@ Schema and storage:
318
323
  - Archive hides artifacts from default lists without deleting versions.
319
324
  - Bundle artifacts store multiple files or base64 assets as portable JSON.
320
325
  - Supported formats are `html`, `markdown`, `text`, `json`, `code`, `svg`, `mermaid`, `react`, `sarif`, `csv`, `image`, and `video`.
326
+ - Native create/import paths infer `html` from HTML documents or fragments when no explicit format is supplied.
321
327
  - Diagram, component, source snippet, analysis report, table, and media assets use `diagram`, `component`, `snippet`, `analysis-report`, `table`, and `asset` artifact types.
322
328
  - Copilot/Cursor examples cover PR reviews, screenshots, demo recordings, and visual evidence bundles.
323
329
  - See [docs/artifact-schema-v1.md](docs/artifact-schema-v1.md).
@@ -319,7 +319,7 @@ Supported converter inputs:
319
319
  - GitHub Copilot: markdown/text/json outputs; Artifacty-compatible JSON payloads; structured handoff, review, diff, and verification JSON payloads with `agent` or `sourceAgent` set to `github-copilot` or `copilot`.
320
320
  - Cursor: markdown/text/json outputs; Artifacty-compatible JSON payloads; structured handoff, review, diff, verification, screenshot, demo/video, and visual evidence bundle JSON payloads with `agent` or `sourceAgent` set to `cursor`.
321
321
  - Gemini: `returnDisplay`, `llmContent`, text blocks, or local markdown/text/json files.
322
- - Generic: file extension, content type, HTML doctype, JSON shape, media data URLs, and markdown headings are used to infer format and title.
322
+ - Generic: file extension, content type, HTML doctype/fragments, JSON shape, media data URLs, and markdown headings are used to infer format and title. Generic `text/plain` uploads are upgraded to a more specific format, such as `html`, when filename or content makes that clear.
323
323
 
324
324
  The converter adds `imported` and source-agent tags, preserves the raw content as an immutable Artifacty version, and records source details under `metadata.artifactyImport`.
325
325
 
@@ -44,9 +44,9 @@ When Artifacty binds outside loopback, startup output includes a warning that th
44
44
 
45
45
  ## Browser Write Behavior
46
46
 
47
- Remote browsers can read shared pages, but write actions are intentionally conservative. Mutating browser routes reject non-local `Origin` headers to reduce CSRF risk. For LAN sharing, prefer API or MCP writes with an explicit token header.
47
+ Remote browsers can read shared pages and can submit browser forms when the request is same-origin with the Artifacty host, for example `Origin: http://10.0.0.50:8787` with `Host: 10.0.0.50:8787`. Mutating browser routes reject cross-origin requests to reduce CSRF risk. For scripts and agents, prefer API or MCP writes with an explicit token header.
48
48
 
49
- Do not relax the origin check just to make remote browser writes easier. A future team dashboard should use a dedicated policy that combines same-origin remote requests, explicit token validation, and clear operator intent.
49
+ Do not disable the origin check just to make remote browser writes easier. Same-origin central dashboard writes should pass; writes initiated from another website should fail with `NON_LOCAL_ORIGIN`.
50
50
 
51
51
  ## Renderer Guidance
52
52
 
@@ -68,9 +68,10 @@ reachable from the user's browser.
68
68
 
69
69
  Controls:
70
70
 
71
- - Mutating browser routes reject non-local `Origin` headers.
71
+ - Mutating browser routes allow loopback or same-origin requests and reject
72
+ cross-origin writes.
72
73
  - API routes require a token when configured.
73
- - LAN/team mode does not relax browser-origin checks.
74
+ - LAN/team mode does not allow arbitrary browser origins.
74
75
 
75
76
  ### Untrusted Artifact Rendering
76
77
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifacty",
3
- "version": "0.10.3",
3
+ "version": "0.10.5",
4
4
  "description": "Local artifact exchange for heterogeneous LLM agents via HTTP and MCP.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -36,7 +36,12 @@ export function convertAgentArtifact(input = {}) {
36
36
  optionalString(decoded.title) ||
37
37
  inferTitle({ content, format, fileName, sourceAgent });
38
38
 
39
- const contentType = explicitContentType || decoded.contentType || media?.mimeType || contentTypeForFormat(format);
39
+ const contentType = resolvedContentType({
40
+ format,
41
+ explicitContentType,
42
+ decodedContentType: decoded.contentType,
43
+ mediaMimeType: media?.mimeType
44
+ });
40
45
  const artifactType = normalizeArtifactType(
41
46
  input.artifactType ||
42
47
  input.artifact_type ||
@@ -77,6 +82,14 @@ export function convertAgentArtifact(input = {}) {
77
82
  };
78
83
  }
79
84
 
85
+ function resolvedContentType({ format, explicitContentType, decodedContentType, mediaMimeType }) {
86
+ const candidate = explicitContentType || decodedContentType || mediaMimeType;
87
+ if (candidate && !(isGenericTextContentType(candidate) && format !== "text")) {
88
+ return candidate;
89
+ }
90
+ return mediaMimeType || contentTypeForFormat(format);
91
+ }
92
+
80
93
  export function detectFormat({ content = "", contentType = "", fileName = "" } = {}) {
81
94
  const type = optionalString(contentType).toLowerCase();
82
95
  if (type.includes("vnd.ant.code") || type.includes("source-code")) {
@@ -112,10 +125,6 @@ export function detectFormat({ content = "", contentType = "", fileName = "" } =
112
125
  if (type.includes("json")) {
113
126
  return "json";
114
127
  }
115
- if (type.startsWith("text/")) {
116
- return "text";
117
- }
118
-
119
128
  const lowerName = optionalString(fileName).toLowerCase();
120
129
  if (lowerName.endsWith(".sarif") || lowerName.endsWith(".sarif.json")) {
121
130
  return "sarif";
@@ -172,7 +181,7 @@ export function detectFormat({ content = "", contentType = "", fileName = "" } =
172
181
  if (looksLikeMediaDataUrl(trimmed, "video")) {
173
182
  return "video";
174
183
  }
175
- if (/^<!doctype html/i.test(trimmed) || /^<html[\s>]/i.test(trimmed)) {
184
+ if (looksLikeHtml(trimmed)) {
176
185
  return "html";
177
186
  }
178
187
  if (looksLikeSarif(trimmed)) {
@@ -187,6 +196,9 @@ export function detectFormat({ content = "", contentType = "", fileName = "" } =
187
196
  if (/^#{1,3}\s+\S/m.test(trimmed) || /^[-*]\s+\S/m.test(trimmed)) {
188
197
  return "markdown";
189
198
  }
199
+ if (type.startsWith("text/")) {
200
+ return "text";
201
+ }
190
202
  return "text";
191
203
  }
192
204
 
@@ -1175,6 +1187,19 @@ function isSarifObject(value) {
1175
1187
  (typeof value.version === "string" || optionalString(value.$schema).toLowerCase().includes("sarif"));
1176
1188
  }
1177
1189
 
1190
+ function looksLikeHtml(value) {
1191
+ const trimmed = optionalString(value);
1192
+ if (/^<!doctype html/i.test(trimmed) || /^<html[\s>]/i.test(trimmed)) {
1193
+ return true;
1194
+ }
1195
+ const withoutLeadingComment = trimmed.replace(/^<!--[\s\S]*?-->\s*/, "");
1196
+ return htmlTagPattern().test(withoutLeadingComment);
1197
+ }
1198
+
1199
+ function htmlTagPattern() {
1200
+ return /^<\/?(?:a|article|aside|body|br|button|canvas|code|div|fieldset|figcaption|figure|footer|form|h[1-6]|head|header|hr|iframe|img|input|label|li|link|main|meta|nav|ol|option|p|pre|script|section|select|span|style|table|tbody|td|textarea|tfoot|th|thead|title|tr|ul|video|audio)(?:\s|>|\/)/i;
1201
+ }
1202
+
1178
1203
  function looksLikeCsv(value) {
1179
1204
  const lines = optionalString(value)
1180
1205
  .split(/\r?\n/)
@@ -1307,6 +1332,10 @@ function isSupportedVideoMime(value) {
1307
1332
  return new Set(["video/mp4", "video/webm"]).has(optionalString(value).toLowerCase().split(";")[0]);
1308
1333
  }
1309
1334
 
1335
+ function isGenericTextContentType(value) {
1336
+ return optionalString(value).toLowerCase().split(";")[0] === "text/plain";
1337
+ }
1338
+
1310
1339
  function looksLikeMermaid(value) {
1311
1340
  const firstMeaningfulLine = optionalString(value)
1312
1341
  .split(/\r?\n/)
@@ -1721,7 +1721,7 @@ function normalizeArtifactInput(input, options) {
1721
1721
  return {
1722
1722
  title,
1723
1723
  content,
1724
- format: normalizeFormat(input.format || inferFormat(input.contentType)),
1724
+ format: normalizeFormat(input.format || inferFormat(input.contentType, content)),
1725
1725
  contentType: normalizeOptionalString(input.contentType),
1726
1726
  artifactType: normalizeArtifactType(input.artifactType || input.artifact_type || inferArtifactType(input)),
1727
1727
  schemaVersion: normalizeSchemaVersion(input.schemaVersion || input.schema_version),
@@ -1758,9 +1758,9 @@ function normalizeSchemaVersion(value) {
1758
1758
  function inferArtifactType(input) {
1759
1759
  let format;
1760
1760
  try {
1761
- format = normalizeFormat(input.format || inferFormat(input.contentType));
1761
+ format = normalizeFormat(input.format || inferFormat(input.contentType, input.content));
1762
1762
  } catch {
1763
- format = normalizeOptionalString(input.format || inferFormat(input.contentType));
1763
+ format = normalizeOptionalString(input.format || inferFormat(input.contentType, input.content));
1764
1764
  }
1765
1765
  if (format === "html") {
1766
1766
  return "html-page";
@@ -2011,7 +2011,7 @@ function verifyPassword(password, stored) {
2011
2011
  return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
2012
2012
  }
2013
2013
 
2014
- function inferFormat(contentType) {
2014
+ function inferFormat(contentType, content = "") {
2015
2015
  const value = normalizeOptionalString(contentType).toLowerCase();
2016
2016
  if (value.includes("vnd.ant.code") || value.includes("source-code")) {
2017
2017
  return "code";
@@ -2046,9 +2046,21 @@ function inferFormat(contentType) {
2046
2046
  if (value.includes("json")) {
2047
2047
  return "json";
2048
2048
  }
2049
+ const trimmed = normalizeOptionalString(content);
2050
+ if (looksLikeHtml(trimmed)) {
2051
+ return "html";
2052
+ }
2049
2053
  return "text";
2050
2054
  }
2051
2055
 
2056
+ function looksLikeHtml(value) {
2057
+ if (/^<!doctype html/i.test(value) || /^<html[\s>]/i.test(value)) {
2058
+ return true;
2059
+ }
2060
+ const withoutLeadingComment = value.replace(/^<!--[\s\S]*?-->\s*/, "");
2061
+ return /^<\/?(?:a|article|aside|body|br|button|canvas|code|div|fieldset|figcaption|figure|footer|form|h[1-6]|head|header|hr|iframe|img|input|label|li|link|main|meta|nav|ol|option|p|pre|script|section|select|span|style|table|tbody|td|textarea|tfoot|th|thead|title|tr|ul|video|audio)(?:\s|>|\/)/i.test(withoutLeadingComment);
2062
+ }
2063
+
2052
2064
  function makeArtifactId(title) {
2053
2065
  const slug = title
2054
2066
  .toLowerCase()
package/src/server.js CHANGED
@@ -816,11 +816,25 @@ export function assertLocalOrigin(request) {
816
816
  return;
817
817
  }
818
818
 
819
- const parsed = new URL(origin);
819
+ let parsed;
820
+ try {
821
+ parsed = new URL(origin);
822
+ } catch {
823
+ throw Object.assign(new Error(`Rejected invalid origin: ${origin}`), {
824
+ statusCode: 403,
825
+ code: "NON_LOCAL_ORIGIN"
826
+ });
827
+ }
828
+
820
829
  const hostname = parsed.hostname.toLowerCase();
821
- const allowed = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
830
+ const host = String(request.headers.host || "").toLowerCase();
831
+ const allowed =
832
+ hostname === "localhost" ||
833
+ hostname === "127.0.0.1" ||
834
+ hostname === "::1" ||
835
+ (host && parsed.host.toLowerCase() === host && (parsed.protocol === "http:" || parsed.protocol === "https:"));
822
836
  if (!allowed) {
823
- throw Object.assign(new Error(`Rejected non-local origin: ${origin}`), {
837
+ throw Object.assign(new Error(`Rejected untrusted origin: ${origin}`), {
824
838
  statusCode: 403,
825
839
  code: "NON_LOCAL_ORIGIN"
826
840
  });