artifacty 0.11.0 → 0.12.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 +5 -3
- package/README.md +148 -22
- package/SECURITY.md +25 -16
- package/docs/artifact-schema-v1.md +73 -50
- package/docs/central-team-deployment-design.md +56 -52
- package/docs/integrations.md +25 -25
- package/docs/mcp-public-api.md +75 -57
- package/docs/network-sharing.md +8 -8
- package/docs/release-checklist.md +24 -13
- package/docs/roadmap-design.md +145 -133
- package/docs/sarif-csv-artifact-plan.md +25 -26
- package/docs/threat-model.md +121 -100
- package/docs/writing-style.md +56 -0
- package/package.json +3 -2
- package/src/cli.js +44 -3
- package/src/lib/backup.js +19 -6
- package/src/lib/i18n.js +142 -0
- package/src/lib/network-policy.js +234 -0
- package/src/lib/render.js +147 -29
- package/src/lib/sample-content.js +185 -0
- package/src/lib/samples.js +74 -0
- package/src/lib/share-links.js +136 -0
- package/src/lib/storage.js +455 -32
- package/src/mcp-server.js +18 -6
- package/src/server.js +194 -7
package/src/mcp-server.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ARTIFACT_TYPES,
|
|
11
11
|
archiveArtifact,
|
|
12
12
|
authenticateApiToken,
|
|
13
|
+
canReadArtifactEvent,
|
|
13
14
|
createArtifact,
|
|
14
15
|
createStore,
|
|
15
16
|
getArtifact,
|
|
@@ -34,7 +35,7 @@ import {
|
|
|
34
35
|
import { convertAgentArtifact } from "./lib/converters.js";
|
|
35
36
|
import { createStructuredDiff, diffFormatFor, renderUnifiedDiffText, resolveDiffView } from "./lib/diff.js";
|
|
36
37
|
import { createEmbeddingProvider } from "./lib/embeddings.js";
|
|
37
|
-
import { EVENT_TYPES,
|
|
38
|
+
import { EVENT_TYPES, sanitizeEventForDelivery, subscribe } from "./lib/events.js";
|
|
38
39
|
import { inlineSchema } from "./lib/schemas.js";
|
|
39
40
|
import { effectiveScopes, requireScope } from "./lib/security.js";
|
|
40
41
|
import { resolvePublicBaseUrl } from "./lib/server-state.js";
|
|
@@ -896,6 +897,7 @@ async function callTool(context, name, args) {
|
|
|
896
897
|
...converted,
|
|
897
898
|
allowSecrets: args.allowSecrets,
|
|
898
899
|
auditAction: "import",
|
|
900
|
+
access: await mcpAccessContext(context),
|
|
899
901
|
audit: mcpAuditContext(context)
|
|
900
902
|
});
|
|
901
903
|
return toolResult({
|
|
@@ -1081,6 +1083,7 @@ async function callTool(context, name, args) {
|
|
|
1081
1083
|
activeWaits += 1;
|
|
1082
1084
|
try {
|
|
1083
1085
|
return toolResult(await waitForEvent({
|
|
1086
|
+
store: context.store,
|
|
1084
1087
|
artifactId: args.artifactId,
|
|
1085
1088
|
tag: args.tag,
|
|
1086
1089
|
type: args.type,
|
|
@@ -1294,6 +1297,7 @@ async function createNativeArtifact(context, args) {
|
|
|
1294
1297
|
allowSecrets: args.allowSecrets,
|
|
1295
1298
|
visibility: args.visibility,
|
|
1296
1299
|
relations: args.relations,
|
|
1300
|
+
access: await mcpAccessContext(context),
|
|
1297
1301
|
audit: mcpAuditContext(context)
|
|
1298
1302
|
});
|
|
1299
1303
|
}
|
|
@@ -1468,7 +1472,7 @@ function parseArtifactResourceUri(uri) {
|
|
|
1468
1472
|
// (stdio or one streamable-HTTP request) because it never outlives the
|
|
1469
1473
|
// single tool call: it subscribes, waits, and always unsubscribes before
|
|
1470
1474
|
// returning, regardless of whether a match arrived.
|
|
1471
|
-
function waitForEvent({ artifactId, tag, type, timeoutMs, access } = {}) {
|
|
1475
|
+
function waitForEvent({ store, artifactId, tag, type, timeoutMs, access } = {}) {
|
|
1472
1476
|
const filter = {};
|
|
1473
1477
|
if (artifactId) filter.artifactId = artifactId;
|
|
1474
1478
|
if (tag) filter.tag = tag;
|
|
@@ -1489,9 +1493,14 @@ function waitForEvent({ artifactId, tag, type, timeoutMs, access } = {}) {
|
|
|
1489
1493
|
// A matching event for a private artifact the caller cannot see must
|
|
1490
1494
|
// not resolve or leak the wait: keep waiting (silently) until a visible
|
|
1491
1495
|
// match arrives or the timeout fires.
|
|
1492
|
-
const unsubscribe = subscribe(filter, (event) => {
|
|
1496
|
+
const unsubscribe = subscribe(filter, async (event) => {
|
|
1497
|
+
if (settled) return;
|
|
1498
|
+
try {
|
|
1499
|
+
if (!await canReadArtifactEvent(store, event, access)) return;
|
|
1500
|
+
} catch {
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1493
1503
|
if (settled) return;
|
|
1494
|
-
if (!eventVisibleTo(event, access)) return;
|
|
1495
1504
|
settled = true;
|
|
1496
1505
|
clearTimeout(timer);
|
|
1497
1506
|
unsubscribe();
|
|
@@ -1529,10 +1538,13 @@ async function subscribeResource(context, uri) {
|
|
|
1529
1538
|
}
|
|
1530
1539
|
if (!context.subscriptions.has(uri)) {
|
|
1531
1540
|
const access = await mcpAccessContext(context);
|
|
1532
|
-
const unsubscribe = subscribe(filter, (event) => {
|
|
1533
|
-
|
|
1541
|
+
const unsubscribe = subscribe(filter, async (event) => {
|
|
1542
|
+
try {
|
|
1543
|
+
if (!await canReadArtifactEvent(context.store, event, access)) return;
|
|
1544
|
+
} catch {
|
|
1534
1545
|
return;
|
|
1535
1546
|
}
|
|
1547
|
+
if (!context.subscriptions.has(uri)) return;
|
|
1536
1548
|
context.notify({
|
|
1537
1549
|
jsonrpc: "2.0",
|
|
1538
1550
|
method: "notifications/resources/updated",
|
package/src/server.js
CHANGED
|
@@ -25,6 +25,20 @@ import {
|
|
|
25
25
|
deleteSavedView,
|
|
26
26
|
deleteWebhook,
|
|
27
27
|
getArtifact,
|
|
28
|
+
getAccessMode,
|
|
29
|
+
canReadArtifactEvent,
|
|
30
|
+
setAccessMode,
|
|
31
|
+
listTeams,
|
|
32
|
+
createTeam,
|
|
33
|
+
setTeamMembers,
|
|
34
|
+
getArtifactSharing,
|
|
35
|
+
getArtifactAccessReport,
|
|
36
|
+
getArtifactPermission,
|
|
37
|
+
previewAccessMode,
|
|
38
|
+
listNotifications,
|
|
39
|
+
markNotificationRead,
|
|
40
|
+
markAllNotificationsRead,
|
|
41
|
+
setArtifactSharing,
|
|
28
42
|
getSessionUser,
|
|
29
43
|
getWebhook,
|
|
30
44
|
importUsersFromCsv,
|
|
@@ -59,12 +73,15 @@ import {
|
|
|
59
73
|
VersionConflictError,
|
|
60
74
|
verifyUserPassword
|
|
61
75
|
} from "./lib/storage.js";
|
|
76
|
+
import { assertNetworkAccess, getNetworkPolicy, stageNetworkPolicy, getPendingNetworkPolicy, confirmNetworkPolicy, rollbackNetworkPolicy } from "./lib/network-policy.js";
|
|
77
|
+
import { getShareLinkPolicy, setShareLinkPolicy, createShareLink, listShareLinks, revokeShareLink, resolveShareLink } from "./lib/share-links.js";
|
|
78
|
+
import { installSamples, removeSamples } from "./lib/samples.js";
|
|
62
79
|
import { MAX_BACKUP_BYTES, buildStoreBackup, exportStoreToString, importStoreBundle, importStoreFromString } from "./lib/backup.js";
|
|
63
80
|
import { convertAgentArtifact, decodeMediaContent, mediaContentType } from "./lib/converters.js";
|
|
64
81
|
import { createLineDiff, createStructuredDiff, diffFormatFor, resolveDiffView } from "./lib/diff.js";
|
|
65
82
|
import { EDITOR_CLIENT_PATH, VIEWER_CLIENT_PATH, editorClientFilePath, editorVendorPath, viewerClientFilePath } from "./lib/editor-assets.js";
|
|
66
83
|
import { createEmbeddingProvider } from "./lib/embeddings.js";
|
|
67
|
-
import { EVENT_TYPES,
|
|
84
|
+
import { EVENT_TYPES, publish, sanitizeEventForDelivery, subscribe } from "./lib/events.js";
|
|
68
85
|
import { localeFromBodyOrUrl, localeFromUrl, localizedHref } from "./lib/i18n.js";
|
|
69
86
|
import { paginationJson } from "./lib/listing.js";
|
|
70
87
|
import { buildOpenApiDocument, renderApiDocsHtml } from "./lib/openapi.js";
|
|
@@ -92,6 +109,10 @@ import {
|
|
|
92
109
|
renderArtifactPage,
|
|
93
110
|
renderAccountPage,
|
|
94
111
|
renderAdminBackupPage,
|
|
112
|
+
renderAdminAccessPage,
|
|
113
|
+
renderArtifactSharingPage,
|
|
114
|
+
renderSharedArtifactPage,
|
|
115
|
+
renderInboxPage,
|
|
95
116
|
renderAdminArtifactVersionsPage,
|
|
96
117
|
renderAdminRetentionPage,
|
|
97
118
|
renderAdminUsersPage,
|
|
@@ -283,6 +304,7 @@ function closeServer(server) {
|
|
|
283
304
|
}
|
|
284
305
|
|
|
285
306
|
export async function handleRequest({ request, response, store, host, port, security = securityConfig(), mcpHttp = false }) {
|
|
307
|
+
await assertNetworkAccess(store, request.socket.remoteAddress);
|
|
286
308
|
const url = new URL(request.url, `http://${request.headers.host || `${host}:${port}`}`);
|
|
287
309
|
const pathname = decodeURIComponent(url.pathname);
|
|
288
310
|
const baseUrl = `http://${host}:${port}`;
|
|
@@ -292,6 +314,35 @@ export async function handleRequest({ request, response, store, host, port, secu
|
|
|
292
314
|
const headOnly = request.method === "HEAD";
|
|
293
315
|
const method = headOnly ? "GET" : request.method;
|
|
294
316
|
|
|
317
|
+
const publicShareMatch = /^\/s\/([^/]+)(\/raw)?$/.exec(pathname);
|
|
318
|
+
if (publicShareMatch && method === "GET") {
|
|
319
|
+
const shared = await resolveShareLink(store, publicShareMatch[1]);
|
|
320
|
+
response.setHeader("referrer-policy", "no-referrer");
|
|
321
|
+
response.setHeader("cache-control", "no-store");
|
|
322
|
+
if (publicShareMatch[2]) {
|
|
323
|
+
const fileName = url.searchParams.get("file");
|
|
324
|
+
const bundleFile = fileName ? readBundleFile(shared.artifact, shared.content, fileName) : null;
|
|
325
|
+
if (fileName && !bundleFile) return sendJson(response, { error: "Not found" }, 404, headOnly);
|
|
326
|
+
const raw = bundleFile || rawArtifactResponse(shared.artifact);
|
|
327
|
+
response.writeHead(200, {
|
|
328
|
+
"content-type": raw.contentType,
|
|
329
|
+
"cache-control": "no-store",
|
|
330
|
+
"content-security-policy": "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:; frame-ancestors 'self'",
|
|
331
|
+
"x-content-type-options": "nosniff",
|
|
332
|
+
...(bundleFile ? { "content-disposition": buildContentDisposition(bundleFile.contentType === "application/pdf" ? "inline" : "attachment", bundleFile.fileName) } : {})
|
|
333
|
+
});
|
|
334
|
+
response.end(headOnly ? undefined : raw.body);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// Pass only display fields, never the source id, ownership, metadata, or history.
|
|
338
|
+
return sendHtml(response, renderSharedArtifactPage({
|
|
339
|
+
artifact: { title: shared.artifact.title, artifactType: shared.artifact.artifactType },
|
|
340
|
+
version: { version: shared.version.version, format: shared.version.format, contentType: shared.version.contentType },
|
|
341
|
+
content: shared.content, baseUrl, locale,
|
|
342
|
+
sharePath: `/s/${encodeURIComponent(publicShareMatch[1])}`
|
|
343
|
+
}), 200, headOnly);
|
|
344
|
+
}
|
|
345
|
+
|
|
295
346
|
if (method === "GET" && pathname === EDITOR_CLIENT_PATH) {
|
|
296
347
|
return sendJavaScriptFile(response, editorClientFilePath(PACKAGE_ROOT), headOnly, request);
|
|
297
348
|
}
|
|
@@ -490,6 +541,119 @@ export async function handleRequest({ request, response, store, host, port, secu
|
|
|
490
541
|
return sendRedirect(response, "/account");
|
|
491
542
|
}
|
|
492
543
|
|
|
544
|
+
const browserAccess = accessContext(request, currentUser);
|
|
545
|
+
const browserAudit = { surface: "web", actor: currentUser?.email, userId: currentUser?.id };
|
|
546
|
+
const adminAccessPage = async (extra = {}) => renderAdminAccessPage({
|
|
547
|
+
baseUrl, user: currentUser, mode: await getAccessMode(store),
|
|
548
|
+
teams: await listTeams(store), users: await listUsers(store),
|
|
549
|
+
networkPolicy: await getNetworkPolicy(store), pendingNetworkPolicy: await getPendingNetworkPolicy(store),
|
|
550
|
+
shareLinkPolicy: await getShareLinkPolicy(store), currentIp: request.socket.remoteAddress,
|
|
551
|
+
locale, currentPath, ...extra
|
|
552
|
+
});
|
|
553
|
+
const adminAccessPaths = ["/admin/access", "/admin/access/preview", "/admin/access/network", "/admin/access/network/confirm", "/admin/access/network/rollback", "/admin/access/share-links", "/admin/teams", "/admin/samples/install", "/admin/samples/remove"];
|
|
554
|
+
if (adminAccessPaths.includes(pathname) || /^\/admin\/teams\/[^/]+\/members$/.test(pathname)) {
|
|
555
|
+
if (!currentUser) return sendRedirect(response, "/login");
|
|
556
|
+
requireAdmin(currentUser);
|
|
557
|
+
if (method === "GET" && pathname === "/admin/access") {
|
|
558
|
+
return sendHtml(response, await adminAccessPage(), 200, headOnly);
|
|
559
|
+
}
|
|
560
|
+
if (method === "POST") {
|
|
561
|
+
assertLocalOrigin(request);
|
|
562
|
+
request.artifactyAuth = await requireBrowserWriteAuth({ store, request, url, config: security, currentUser });
|
|
563
|
+
await enforceRateLimit({ store, host, request, bucket: "write" });
|
|
564
|
+
const body = await readFormBody(request);
|
|
565
|
+
const options = { access: browserAccess, audit: browserAudit, remoteAddress: request.socket.remoteAddress };
|
|
566
|
+
if (pathname === "/admin/access/preview") {
|
|
567
|
+
return sendHtml(response, await adminAccessPage({ preview: await previewAccessMode(store, body.mode, options) }), 200, headOnly);
|
|
568
|
+
} else if (pathname === "/admin/access") {
|
|
569
|
+
await setAccessMode(store, body.mode, options);
|
|
570
|
+
} else if (pathname === "/admin/access/network") {
|
|
571
|
+
await stageNetworkPolicy(store, { allowedCidrs: body.allowedCidrs || "" }, options);
|
|
572
|
+
} else if (pathname === "/admin/access/network/confirm") {
|
|
573
|
+
await confirmNetworkPolicy(store, body.id, options);
|
|
574
|
+
} else if (pathname === "/admin/access/network/rollback") {
|
|
575
|
+
await rollbackNetworkPolicy(store, body.id, options);
|
|
576
|
+
} else if (pathname === "/admin/access/share-links") {
|
|
577
|
+
await setShareLinkPolicy(store, { enabledInPrivate: body.enabledInPrivate === "on" || body.enabledInPrivate === "true" }, options);
|
|
578
|
+
} else if (pathname === "/admin/samples/install") {
|
|
579
|
+
await installSamples(store, options);
|
|
580
|
+
} else if (pathname === "/admin/samples/remove") {
|
|
581
|
+
await removeSamples(store, options);
|
|
582
|
+
} else if (pathname === "/admin/teams") {
|
|
583
|
+
await createTeam(store, { name: body.name }, options);
|
|
584
|
+
} else {
|
|
585
|
+
await setTeamMembers(store, pathname.split("/")[3], body.userIds || [], options);
|
|
586
|
+
}
|
|
587
|
+
return sendRedirect(response, localizedHref("/admin/access", localeFromBodyOrUrl(body, url)));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const inboxReadMatch = /^\/inbox\/([^/]+)\/read$/.exec(pathname);
|
|
592
|
+
if (pathname === "/inbox" || pathname === "/inbox/read-all" || inboxReadMatch) {
|
|
593
|
+
if (!currentUser) return sendRedirect(response, "/login");
|
|
594
|
+
if (method === "GET" && pathname === "/inbox") {
|
|
595
|
+
return sendHtml(response, renderInboxPage({
|
|
596
|
+
baseUrl, user: currentUser, ...await listNotifications(store, { access: browserAccess }), locale, currentPath
|
|
597
|
+
}), 200, headOnly);
|
|
598
|
+
}
|
|
599
|
+
if (method === "POST" && (inboxReadMatch || pathname === "/inbox/read-all")) {
|
|
600
|
+
assertLocalOrigin(request);
|
|
601
|
+
request.artifactyAuth = await requireBrowserWriteAuth({ store, request, url, config: security, currentUser });
|
|
602
|
+
await enforceRateLimit({ store, host, request, bucket: "write" });
|
|
603
|
+
const body = await readFormBody(request);
|
|
604
|
+
if (inboxReadMatch) await markNotificationRead(store, inboxReadMatch[1], { access: browserAccess });
|
|
605
|
+
else await markAllNotificationsRead(store, { access: browserAccess });
|
|
606
|
+
return sendRedirect(response, localizedHref("/inbox", localeFromBodyOrUrl(body, url)));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const sharingMatch = /^\/artifacts\/([^/]+)\/(sharing|share-links)(?:\/([^/]+)\/revoke)?$/.exec(pathname);
|
|
611
|
+
if (sharingMatch && (method === "GET" || method === "POST")) {
|
|
612
|
+
if (!currentUser) return sendRedirect(response, "/login");
|
|
613
|
+
const access = browserAccess;
|
|
614
|
+
const artifact = await getArtifact(store, sharingMatch[1], { access });
|
|
615
|
+
if (!artifact) return sendJson(response, { error: "Not found" }, 404, headOnly);
|
|
616
|
+
if (currentUser.role !== "admin" && artifact.ownerUserId !== currentUser.id) {
|
|
617
|
+
throw Object.assign(new Error("Only the owner or an administrator can manage sharing"), { statusCode: 403 });
|
|
618
|
+
}
|
|
619
|
+
let createdShareLink = null;
|
|
620
|
+
if (method === "POST") {
|
|
621
|
+
assertLocalOrigin(request);
|
|
622
|
+
request.artifactyAuth = await requireBrowserWriteAuth({ store, request, url, config: security, currentUser });
|
|
623
|
+
await enforceRateLimit({ store, host, request, bucket: "write" });
|
|
624
|
+
const body = await readFormBody(request);
|
|
625
|
+
const options = { access, audit: browserAudit };
|
|
626
|
+
if (sharingMatch[2] === "sharing") {
|
|
627
|
+
const permissions = (prefix) => Object.fromEntries(Object.entries(body).filter(([key]) => key.startsWith(prefix)).map(([key, value]) => [key.slice(prefix.length), value]));
|
|
628
|
+
await setArtifactSharing(store, sharingMatch[1], {
|
|
629
|
+
userIds: body.userIds || [], teamIds: body.teamIds || [],
|
|
630
|
+
userPermissions: permissions("userPermission:"), teamPermissions: permissions("teamPermission:")
|
|
631
|
+
}, options);
|
|
632
|
+
} else if (sharingMatch[3]) {
|
|
633
|
+
await revokeShareLink(store, sharingMatch[1], sharingMatch[3], options);
|
|
634
|
+
} else {
|
|
635
|
+
const days = body.days === undefined ? 7 : Number(body.days);
|
|
636
|
+
if (!Number.isInteger(days) || days < 1 || days > 365) {
|
|
637
|
+
throw Object.assign(new Error("Expiry must be between 1 and 365 days"), { statusCode: 400 });
|
|
638
|
+
}
|
|
639
|
+
const version = !body.version || body.version === "latest" ? null : Number(body.version);
|
|
640
|
+
createdShareLink = await createShareLink(store, sharingMatch[1], {
|
|
641
|
+
version, expiresAt: body.expiresAt || new Date(Date.now() + days * 86400000).toISOString()
|
|
642
|
+
}, options);
|
|
643
|
+
}
|
|
644
|
+
if (!createdShareLink) return sendRedirect(response, localizedHref(`/artifacts/${encodeURIComponent(sharingMatch[1])}/sharing`, localeFromBodyOrUrl(body, url)));
|
|
645
|
+
}
|
|
646
|
+
return sendHtml(response, renderArtifactSharingPage({
|
|
647
|
+
baseUrl, user: currentUser, artifact,
|
|
648
|
+
sharing: await getArtifactSharing(store, sharingMatch[1], { access, includePermissions: true }),
|
|
649
|
+
accessReport: await getArtifactAccessReport(store, sharingMatch[1], { access }),
|
|
650
|
+
networkPolicy: await getNetworkPolicy(store),
|
|
651
|
+
shareLinks: await listShareLinks(store, sharingMatch[1], { access }),
|
|
652
|
+
createdShareLink, shareLinkPolicy: await getShareLinkPolicy(store), mode: await getAccessMode(store),
|
|
653
|
+
users: await listUsers(store), teams: await listTeams(store), locale, currentPath
|
|
654
|
+
}), 200, headOnly);
|
|
655
|
+
}
|
|
656
|
+
|
|
493
657
|
if (method === "GET" && pathname === "/admin/backup") {
|
|
494
658
|
if (!currentUser) {
|
|
495
659
|
return sendRedirect(response, "/login");
|
|
@@ -804,6 +968,11 @@ export async function handleRequest({ request, response, store, host, port, secu
|
|
|
804
968
|
return sendJson(response, { ok: true, name: "artifacty", store: store.home }, 200, headOnly);
|
|
805
969
|
}
|
|
806
970
|
|
|
971
|
+
if (!currentUser && await getAccessMode(store) === "private" &&
|
|
972
|
+
(["/", "/new", "/import"].includes(pathname) || pathname.startsWith("/artifacts/"))) {
|
|
973
|
+
return sendRedirect(response, localizedHref("/login", locale));
|
|
974
|
+
}
|
|
975
|
+
|
|
807
976
|
if (method === "GET" && pathname === "/") {
|
|
808
977
|
const access = accessContext(request, currentUser);
|
|
809
978
|
const resolved = await resolveListFilters(store, url, access);
|
|
@@ -885,6 +1054,7 @@ export async function handleRequest({ request, response, store, host, port, secu
|
|
|
885
1054
|
request.artifactyAuth = await requireBrowserWriteAuth({ store, request, url, body, config: security, currentUser });
|
|
886
1055
|
await enforceRateLimit({ store, host, request, bucket: "write" });
|
|
887
1056
|
const artifact = await createArtifact(store, {
|
|
1057
|
+
access: accessContext(request, currentUser),
|
|
888
1058
|
title: body.title,
|
|
889
1059
|
content: body.content,
|
|
890
1060
|
format: body.format,
|
|
@@ -921,6 +1091,7 @@ export async function handleRequest({ request, response, store, host, port, secu
|
|
|
921
1091
|
}
|
|
922
1092
|
});
|
|
923
1093
|
const artifact = await createArtifact(store, {
|
|
1094
|
+
access: accessContext(request, currentUser),
|
|
924
1095
|
...converted,
|
|
925
1096
|
visibility: body.visibility || undefined,
|
|
926
1097
|
auditAction: "import",
|
|
@@ -1135,6 +1306,7 @@ export async function handleRequest({ request, response, store, host, port, secu
|
|
|
1135
1306
|
await enforceRateLimit({ store, host, request, bucket: "write" });
|
|
1136
1307
|
const body = await readJsonBody(request);
|
|
1137
1308
|
const artifact = await createArtifact(store, {
|
|
1309
|
+
access: accessContext(request, currentUser),
|
|
1138
1310
|
...pickCreateArtifactInput(body),
|
|
1139
1311
|
audit: auditContext(request, "http-api")
|
|
1140
1312
|
});
|
|
@@ -1148,6 +1320,7 @@ export async function handleRequest({ request, response, store, host, port, secu
|
|
|
1148
1320
|
const body = await readJsonBody(request);
|
|
1149
1321
|
const converted = convertAgentArtifact(body);
|
|
1150
1322
|
const artifact = await createArtifact(store, {
|
|
1323
|
+
access: accessContext(request, currentUser),
|
|
1151
1324
|
...pickCreateArtifactInput(converted),
|
|
1152
1325
|
auditAction: "import",
|
|
1153
1326
|
audit: auditContext(request, "http-api")
|
|
@@ -1622,6 +1795,7 @@ export async function handleRequest({ request, response, store, host, port, secu
|
|
|
1622
1795
|
version: artifact.version,
|
|
1623
1796
|
content: artifact.content,
|
|
1624
1797
|
comments,
|
|
1798
|
+
permission: await getArtifactPermission(store, artifactMatch[1], { access: accessContext(request, currentUser) }),
|
|
1625
1799
|
baseUrl,
|
|
1626
1800
|
authToken,
|
|
1627
1801
|
locale,
|
|
@@ -1717,7 +1891,12 @@ export async function readJsonBody(request, limitBytes = MAX_ARTIFACT_BYTES + 10
|
|
|
1717
1891
|
export async function readFormBody(request, limitBytes = MAX_ARTIFACT_BYTES + 1024) {
|
|
1718
1892
|
const raw = await readBody(request, limitBytes);
|
|
1719
1893
|
const params = new URLSearchParams(raw);
|
|
1720
|
-
|
|
1894
|
+
const body = Object.fromEntries(params.entries());
|
|
1895
|
+
// Sharing checkboxes submit repeated keys; scalar form fields keep their existing shape.
|
|
1896
|
+
for (const key of ["userIds", "teamIds"]) {
|
|
1897
|
+
if (params.has(key)) body[key] = params.getAll(key);
|
|
1898
|
+
}
|
|
1899
|
+
return body;
|
|
1721
1900
|
}
|
|
1722
1901
|
|
|
1723
1902
|
export async function readBody(request, limitBytes) {
|
|
@@ -2215,14 +2394,22 @@ async function handleEventStream({ request, response, store, filter, access }) {
|
|
|
2215
2394
|
}
|
|
2216
2395
|
|
|
2217
2396
|
unsubscribe = subscribe(filter, (event) => {
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2397
|
+
// Recheck persisted permissions: sharing and membership can change while connected.
|
|
2398
|
+
const readable = canReadArtifactEvent(store, event, access).catch(() => false);
|
|
2399
|
+
readable.then(async (allowed) => {
|
|
2400
|
+
await assertNetworkAccess(store, request.socket.remoteAddress);
|
|
2401
|
+
if (allowed && !closed) writeSseEvent(response, event);
|
|
2402
|
+
}).catch(() => { cleanup(); response.end(); });
|
|
2222
2403
|
});
|
|
2223
2404
|
|
|
2224
2405
|
heartbeat = setInterval(() => {
|
|
2225
|
-
|
|
2406
|
+
try {
|
|
2407
|
+
assertNetworkAccess(store, request.socket.remoteAddress);
|
|
2408
|
+
if (!closed) response.write(": heartbeat\n\n");
|
|
2409
|
+
} catch {
|
|
2410
|
+
cleanup();
|
|
2411
|
+
response.end();
|
|
2412
|
+
}
|
|
2226
2413
|
}, SSE_HEARTBEAT_MS);
|
|
2227
2414
|
heartbeat.unref?.();
|
|
2228
2415
|
|