artifacty 0.10.9 → 0.11.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/CLAUDE.md +1 -1
- package/README.md +259 -14
- package/docs/artifact-schema-v1.md +187 -1
- package/docs/mcp-public-api.md +103 -9
- package/docs/release-checklist.md +1 -1
- package/docs/roadmap-design.md +977 -0
- package/docs/sarif-csv-artifact-plan.md +42 -7
- package/docs/threat-model.md +217 -2
- package/package.json +2 -1
- package/src/cli.js +550 -43
- package/src/client/viewer.js +377 -0
- package/src/lib/backup.js +201 -17
- package/src/lib/converters.js +99 -5
- package/src/lib/csv.js +51 -0
- package/src/lib/diff.js +656 -0
- package/src/lib/doctor.js +88 -2
- package/src/lib/embeddings.js +407 -0
- package/src/lib/events.js +183 -0
- package/src/lib/i18n.js +290 -2
- package/src/lib/listing.js +46 -0
- package/src/lib/openapi.js +614 -0
- package/src/lib/render.js +1534 -164
- package/src/lib/retention-form.js +31 -0
- package/src/lib/retention.js +499 -0
- package/src/lib/sarif-csv-export.js +290 -0
- package/src/lib/schemas.js +461 -0
- package/src/lib/security.js +226 -1
- package/src/lib/sse.js +52 -0
- package/src/lib/storage.js +2935 -305
- package/src/lib/webhooks.js +356 -0
- package/src/mcp-server.js +756 -25
- package/src/server.js +1426 -93
package/src/lib/converters.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { normalizeSourceAgent } from "./agents.js";
|
|
4
|
-
import { ARTIFACT_TYPES, contentTypeForFormat, normalizeFormat } from "./storage.js";
|
|
4
|
+
import { ARTIFACT_TYPES, BUNDLE_BINARY_CONTENT_TYPES, contentTypeForFormat, normalizeFormat } from "./storage.js";
|
|
5
5
|
|
|
6
6
|
const CONTINUATION_AGENTS = new Set(["codex", "copilot", "cursor"]);
|
|
7
7
|
|
|
@@ -119,6 +119,9 @@ export function detectFormat({ content = "", contentType = "", fileName = "" } =
|
|
|
119
119
|
if (type.includes("vnd.ant.react") || type.includes("jsx")) {
|
|
120
120
|
return "react";
|
|
121
121
|
}
|
|
122
|
+
if (type.includes("ipynb") || type.includes("notebook")) {
|
|
123
|
+
return "notebook";
|
|
124
|
+
}
|
|
122
125
|
if (type.includes("html")) {
|
|
123
126
|
return "html";
|
|
124
127
|
}
|
|
@@ -126,9 +129,12 @@ export function detectFormat({ content = "", contentType = "", fileName = "" } =
|
|
|
126
129
|
return "markdown";
|
|
127
130
|
}
|
|
128
131
|
if (type.includes("json")) {
|
|
129
|
-
return "json";
|
|
132
|
+
return looksLikeNotebook(content) ? "notebook" : "json";
|
|
130
133
|
}
|
|
131
134
|
const lowerName = optionalString(fileName).toLowerCase();
|
|
135
|
+
if (lowerName.endsWith(".ipynb")) {
|
|
136
|
+
return "notebook";
|
|
137
|
+
}
|
|
132
138
|
if (lowerName.endsWith(".sarif") || lowerName.endsWith(".sarif.json")) {
|
|
133
139
|
return "sarif";
|
|
134
140
|
}
|
|
@@ -161,6 +167,9 @@ export function detectFormat({ content = "", contentType = "", fileName = "" } =
|
|
|
161
167
|
if (extension === ".jsx" || extension === ".tsx") {
|
|
162
168
|
return "react";
|
|
163
169
|
}
|
|
170
|
+
if (extension === ".ipynb") {
|
|
171
|
+
return "notebook";
|
|
172
|
+
}
|
|
164
173
|
if (isCodeExtension(extension)) {
|
|
165
174
|
return "code";
|
|
166
175
|
}
|
|
@@ -190,6 +199,9 @@ export function detectFormat({ content = "", contentType = "", fileName = "" } =
|
|
|
190
199
|
if (looksLikeSarif(trimmed)) {
|
|
191
200
|
return "sarif";
|
|
192
201
|
}
|
|
202
|
+
if (looksLikeNotebook(trimmed)) {
|
|
203
|
+
return "notebook";
|
|
204
|
+
}
|
|
193
205
|
if (looksLikeJson(trimmed)) {
|
|
194
206
|
return "json";
|
|
195
207
|
}
|
|
@@ -248,6 +260,21 @@ function decodeObjectPayload(value, agent) {
|
|
|
248
260
|
};
|
|
249
261
|
}
|
|
250
262
|
|
|
263
|
+
if (isNotebookObject(artifactObject)) {
|
|
264
|
+
return {
|
|
265
|
+
content: JSON.stringify(artifactObject, null, 2),
|
|
266
|
+
format: "notebook",
|
|
267
|
+
contentType: "application/x-ipynb+json; charset=utf-8",
|
|
268
|
+
title: artifactObject.title || artifactObject.name || "Notebook",
|
|
269
|
+
sourceAgent: artifactObject.sourceAgent || artifactObject.source_agent || artifactObject.agent || (agent === "auto" ? undefined : agent),
|
|
270
|
+
artifactType: artifactObject.artifactType || artifactObject.artifact_type || "analysis-report",
|
|
271
|
+
tags: artifactObject.tags,
|
|
272
|
+
metadata: {
|
|
273
|
+
originalPayloadShape: "notebook"
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
251
278
|
if (typeof artifactObject.content === "string" && !Array.isArray(artifactObject.content)) {
|
|
252
279
|
const sourceContentType = artifactObject.contentType ||
|
|
253
280
|
artifactObject.content_type ||
|
|
@@ -440,13 +467,30 @@ function decodeBundlePayload(object, agent) {
|
|
|
440
467
|
? collectContinuationMetadata(continuationAgent, { agent: sourceAgent }, bundle, object)
|
|
441
468
|
: {};
|
|
442
469
|
const normalizedFiles = files.map((file, index) => {
|
|
470
|
+
const filePath = optionalString(file.path || file.name || `file-${index + 1}`);
|
|
471
|
+
const declaredContentType = optionalString(file.contentType || file.mimeType).toLowerCase().split(";")[0].trim();
|
|
472
|
+
const isBinary = file.encoding === "base64" || BUNDLE_BINARY_CONTENT_TYPES.has(declaredContentType);
|
|
473
|
+
|
|
474
|
+
if (isBinary) {
|
|
475
|
+
const base64 = normalizeBase64(typeof file.content === "string" ? file.content : "");
|
|
476
|
+
const decoded = base64 ? Buffer.from(base64, "base64") : Buffer.alloc(0);
|
|
477
|
+
return {
|
|
478
|
+
path: filePath,
|
|
479
|
+
content: base64,
|
|
480
|
+
encoding: "base64",
|
|
481
|
+
contentType: declaredContentType || "application/octet-stream",
|
|
482
|
+
sizeBytes: decoded.byteLength,
|
|
483
|
+
sha256: createHash("sha256").update(decoded).digest("hex")
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
443
487
|
const content = typeof file.content === "string" ? file.content : "";
|
|
444
488
|
return {
|
|
445
|
-
path:
|
|
489
|
+
path: filePath,
|
|
446
490
|
content,
|
|
447
491
|
contentType: optionalString(file.contentType || file.mimeType) || contentTypeForFormat(detectFormat({
|
|
448
492
|
content,
|
|
449
|
-
fileName:
|
|
493
|
+
fileName: filePath
|
|
450
494
|
})),
|
|
451
495
|
sizeBytes: Buffer.byteLength(content, "utf8"),
|
|
452
496
|
sha256: createHash("sha256").update(content).digest("hex")
|
|
@@ -1098,7 +1142,7 @@ function inferArtifactType({ format, content, fileName, metadata }) {
|
|
|
1098
1142
|
if (format === "code") {
|
|
1099
1143
|
return "snippet";
|
|
1100
1144
|
}
|
|
1101
|
-
if (format === "sarif") {
|
|
1145
|
+
if (format === "sarif" || format === "notebook") {
|
|
1102
1146
|
return "analysis-report";
|
|
1103
1147
|
}
|
|
1104
1148
|
if (format === "csv") {
|
|
@@ -1190,6 +1234,27 @@ function isSarifObject(value) {
|
|
|
1190
1234
|
(typeof value.version === "string" || optionalString(value.$schema).toLowerCase().includes("sarif"));
|
|
1191
1235
|
}
|
|
1192
1236
|
|
|
1237
|
+
function looksLikeNotebook(value) {
|
|
1238
|
+
if (!looksLikeJson(value)) {
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
try {
|
|
1242
|
+
return isNotebookObject(JSON.parse(optionalString(value)));
|
|
1243
|
+
} catch {
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
export function isNotebookObject(value) {
|
|
1249
|
+
return Boolean(
|
|
1250
|
+
value &&
|
|
1251
|
+
typeof value === "object" &&
|
|
1252
|
+
!Array.isArray(value) &&
|
|
1253
|
+
value.nbformat !== undefined &&
|
|
1254
|
+
Array.isArray(value.cells)
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1193
1258
|
function looksLikeHtml(value) {
|
|
1194
1259
|
const trimmed = optionalString(value);
|
|
1195
1260
|
if (/^<!doctype html/i.test(trimmed) || /^<html[\s>]/i.test(trimmed)) {
|
|
@@ -1420,3 +1485,32 @@ function optionalString(value) {
|
|
|
1420
1485
|
}
|
|
1421
1486
|
return String(value).trim();
|
|
1422
1487
|
}
|
|
1488
|
+
|
|
1489
|
+
// Resolves the best available image/video content type for a stored
|
|
1490
|
+
// artifact: an explicit media MIME type in version metadata, then the
|
|
1491
|
+
// version's own contentType if it is itself image/video, then a
|
|
1492
|
+
// format-based fallback. Used when serving raw media bytes (e.g. GET
|
|
1493
|
+
// /artifacts/:id/raw) so a stale or missing contentType still renders.
|
|
1494
|
+
export function mediaContentType(artifact) {
|
|
1495
|
+
const metadataType = String(artifact.version.metadata?.mimeType || "").toLowerCase();
|
|
1496
|
+
if (metadataType.startsWith("image/") || metadataType.startsWith("video/")) {
|
|
1497
|
+
return metadataType;
|
|
1498
|
+
}
|
|
1499
|
+
const versionType = String(artifact.version.contentType || "").toLowerCase().split(";")[0];
|
|
1500
|
+
if (versionType.startsWith("image/") || versionType.startsWith("video/")) {
|
|
1501
|
+
return versionType;
|
|
1502
|
+
}
|
|
1503
|
+
return artifact.version.format === "video" ? "video/mp4" : "image/png";
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// Decodes an image/video artifact's stored content (a data: URL or bare
|
|
1507
|
+
// base64 string) into raw bytes, or returns null if it isn't valid base64.
|
|
1508
|
+
export function decodeMediaContent(content) {
|
|
1509
|
+
const value = String(content || "").trim();
|
|
1510
|
+
const dataUrl = /^data:[^;,]+;base64,([A-Za-z0-9+/=_-\s]+)$/i.exec(value);
|
|
1511
|
+
const base64 = (dataUrl ? dataUrl[1] : value).replace(/\s+/g, "").replaceAll("-", "+").replaceAll("_", "/");
|
|
1512
|
+
if (!base64 || !/^[A-Za-z0-9+/]+={0,2}$/.test(base64)) {
|
|
1513
|
+
return null;
|
|
1514
|
+
}
|
|
1515
|
+
return Buffer.from(base64, "base64");
|
|
1516
|
+
}
|
package/src/lib/csv.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Single shared CSV parser used by the CSV renderer (render.js), the CSV
|
|
2
|
+
// diff (diff.js), and the SARIF/CSV export route (sarif-csv-export.js). All
|
|
3
|
+
// three previously carried byte-identical copies of this state machine that
|
|
4
|
+
// had already diverged in behavior on malformed input (an unterminated
|
|
5
|
+
// quoted field): the render.js copy failed closed while the diff.js copy
|
|
6
|
+
// silently emitted a bogus single-cell row for the same input, so the same
|
|
7
|
+
// artifact rendered as an error but diffed as data. This module is the one
|
|
8
|
+
// source of truth: it always fails closed on an unterminated quoted field.
|
|
9
|
+
export function parseCsv(content) {
|
|
10
|
+
const text = String(content || "");
|
|
11
|
+
const rows = [];
|
|
12
|
+
let row = [];
|
|
13
|
+
let cell = "";
|
|
14
|
+
let inQuotes = false;
|
|
15
|
+
|
|
16
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
17
|
+
const char = text[index];
|
|
18
|
+
if (char === "\"") {
|
|
19
|
+
if (inQuotes && text[index + 1] === "\"") {
|
|
20
|
+
cell += "\"";
|
|
21
|
+
index += 1;
|
|
22
|
+
} else {
|
|
23
|
+
inQuotes = !inQuotes;
|
|
24
|
+
}
|
|
25
|
+
} else if (char === "," && !inQuotes) {
|
|
26
|
+
row.push(cell);
|
|
27
|
+
cell = "";
|
|
28
|
+
} else if ((char === "\n" || char === "\r") && !inQuotes) {
|
|
29
|
+
if (char === "\r" && text[index + 1] === "\n") {
|
|
30
|
+
index += 1;
|
|
31
|
+
}
|
|
32
|
+
row.push(cell);
|
|
33
|
+
if (row.length > 1 || row[0] !== "") {
|
|
34
|
+
rows.push(row);
|
|
35
|
+
}
|
|
36
|
+
row = [];
|
|
37
|
+
cell = "";
|
|
38
|
+
} else {
|
|
39
|
+
cell += char;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (inQuotes) {
|
|
44
|
+
return { ok: false, error: "unterminated quoted field", rows: [] };
|
|
45
|
+
}
|
|
46
|
+
row.push(cell);
|
|
47
|
+
if (row.length > 1 || row[0] !== "") {
|
|
48
|
+
rows.push(row);
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, rows };
|
|
51
|
+
}
|