artifacty 0.10.8 → 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 +260 -5
- package/docs/artifact-schema-v1.md +187 -1
- package/docs/central-team-deployment-design.md +3 -1
- package/docs/integrations.md +9 -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 +326 -15
- 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 +1604 -160
- 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 +1529 -98
|
@@ -14,7 +14,8 @@ context, appshots, thread state, and client-specific UI state.
|
|
|
14
14
|
`analysis-report` artifacts.
|
|
15
15
|
- CSV inputs default to `table`; CSV files that look like security or review
|
|
16
16
|
findings infer `analysis-report`.
|
|
17
|
-
- `/raw` always returns the original stored source
|
|
17
|
+
- `/raw` always returns the original stored source, unaffected by rendering,
|
|
18
|
+
sort/filter, or export.
|
|
18
19
|
|
|
19
20
|
## Browser Rendering
|
|
20
21
|
|
|
@@ -26,19 +27,53 @@ context, appshots, thread state, and client-specific UI state.
|
|
|
26
27
|
- Malformed CSV or non-SARIF JSON fails closed to escaped source or formatted
|
|
27
28
|
JSON fallback.
|
|
28
29
|
|
|
30
|
+
## Sort, Filter, and Download (roadmap section 7)
|
|
31
|
+
|
|
32
|
+
- The rendered SARIF and CSV tables are progressively enhanced client-side in
|
|
33
|
+
`src/client/viewer.js`, over the data already in the bounded server-rendered
|
|
34
|
+
set; the server output is fully correct and readable with JavaScript
|
|
35
|
+
disabled.
|
|
36
|
+
- SARIF: level filter chips (`error`, `warning`, `note`), a rule id text
|
|
37
|
+
filter, and click-to-sort on the Level/Rule/Location columns, plus a
|
|
38
|
+
visible result count.
|
|
39
|
+
- CSV: click-to-sort column headers (numeric-aware), a per-column contains
|
|
40
|
+
filter, and a visible row count.
|
|
41
|
+
- `GET /artifacts/:id/export` downloads a filtered/sorted copy without
|
|
42
|
+
touching immutable storage or `/raw`:
|
|
43
|
+
- `?format=csv&sort=<col>&dir=asc|desc&filter=<col>:<text>[,...]` — `col`
|
|
44
|
+
is a header name or 0-based index; `filter` accepts comma-separated
|
|
45
|
+
`col:text` clauses, matched case-insensitively as a substring.
|
|
46
|
+
- `?format=sarif&level=error,warning&rule=<text>` — `level` is a
|
|
47
|
+
comma-separated subset of `error`, `warning`, `note`, `none`; `rule` is a
|
|
48
|
+
case-insensitive rule id substring match. The exported document keeps
|
|
49
|
+
only the matching results per run and stays structurally valid SARIF.
|
|
50
|
+
- The route re-parses the stored original content (never `/raw` itself),
|
|
51
|
+
streams the result with `Content-Disposition: attachment` and the
|
|
52
|
+
matching content type, and is capped at the store's max artifact byte
|
|
53
|
+
size (trailing rows/results are dropped deterministically once the cap
|
|
54
|
+
would be exceeded).
|
|
55
|
+
- Invalid parameters (unknown format, unknown sort/filter column, bad
|
|
56
|
+
`dir`, unknown SARIF level, or an artifact whose stored format doesn't
|
|
57
|
+
match `format`) return HTTP 400 with `code: "invalid_export"`.
|
|
58
|
+
- Shared filter/sort/serialization logic lives in
|
|
59
|
+
`src/lib/sarif-csv-export.js`, independent of the presentation-focused
|
|
60
|
+
parsing in `src/lib/render.js`.
|
|
61
|
+
|
|
29
62
|
## Verification Coverage
|
|
30
63
|
|
|
31
64
|
- Storage round trips cover format enums, content types, extensions, and type
|
|
32
65
|
inference.
|
|
33
66
|
- Converter tests cover SARIF extension/MIME/object detection, findings CSV,
|
|
34
|
-
and
|
|
67
|
+
generic CSV, and real-world CodeQL/Semgrep/Trivy SARIF fixtures.
|
|
35
68
|
- Server tests cover SARIF summary rendering, CSV escaping, `/raw` fidelity,
|
|
36
|
-
|
|
69
|
+
browser form options, the export route's content type/disposition/byte cap,
|
|
70
|
+
export parameter validation, and that the viewer script has no inline
|
|
71
|
+
event handlers.
|
|
72
|
+
- `test/sarif-csv-export.test.js` unit-tests CSV parsing/serialization,
|
|
73
|
+
filter/sort/cap behavior, and SARIF filter/cap behavior directly, including
|
|
74
|
+
against the real-world fixtures under `test/fixtures/sarif/`.
|
|
37
75
|
- MCP tests assert the new format and artifact type enums are exposed.
|
|
38
76
|
|
|
39
77
|
## Future Extensions
|
|
40
78
|
|
|
41
|
-
-
|
|
42
|
-
- Add sorting/filtering for SARIF levels and CSV columns.
|
|
43
|
-
- Add optional download helpers for filtered CSV/SARIF views without changing
|
|
44
|
-
immutable source storage.
|
|
79
|
+
- None currently planned; see roadmap-design.md for the broader roadmap.
|
package/docs/threat-model.md
CHANGED
|
@@ -124,12 +124,227 @@ Guidance: install Artifacty MCP only in clients and workspaces you trust. For
|
|
|
124
124
|
central deployments, prefer TLS through a reverse proxy and rotate shared tokens
|
|
125
125
|
after team changes.
|
|
126
126
|
|
|
127
|
+
### Personal API Token Scopes
|
|
128
|
+
|
|
129
|
+
Risk: a personal API token leaked from one integration (e.g. a read-only
|
|
130
|
+
reporting bot) should not also let the leaker create, modify, or delete
|
|
131
|
+
artifacts or reach admin-only routes (backup, webhooks).
|
|
132
|
+
|
|
133
|
+
Controls:
|
|
134
|
+
|
|
135
|
+
- Each personal token carries a `scopes_json` list drawn from `read`,
|
|
136
|
+
`write`, `admin` (default `["read", "write"]` for both new tokens and
|
|
137
|
+
tokens created before scopes existed).
|
|
138
|
+
- `admin` can only be requested for a token owned by a user with the admin
|
|
139
|
+
role; `createApiToken` rejects the request otherwise.
|
|
140
|
+
- `requireScope` (`src/lib/security.js`) is called on every `/api/*` HTTP
|
|
141
|
+
route: read routes need `read`, mutating routes need `write`, and
|
|
142
|
+
admin-only routes (`/api/webhooks*`, `/api/admin/backup*`) need `admin` in
|
|
143
|
+
addition to the existing admin-role check. A denial returns `403` with
|
|
144
|
+
`code: "scope_denied"` and writes a `token-scope-denied` audit row,
|
|
145
|
+
throttled to one per token per minute.
|
|
146
|
+
- The shared `ARTIFACTY_API_TOKEN` and browser sessions always have full
|
|
147
|
+
scopes — scoping only narrows a *personal* token.
|
|
148
|
+
- Over MCP, `tools/list` omits every tool without a `readOnlyHint`
|
|
149
|
+
annotation (create, publish, import, update, archive, restore, link,
|
|
150
|
+
unlink) when the connection's token lacks `write`; calling one of those
|
|
151
|
+
tools directly anyway returns an `isError` tool result with
|
|
152
|
+
`structuredContent.code: "scope_denied"` instead of executing it. This
|
|
153
|
+
only applies to the streamable-HTTP MCP transport, where the connection is
|
|
154
|
+
tied to an authenticated token — local stdio MCP connections are treated
|
|
155
|
+
as fully trusted, matching every other local write path.
|
|
156
|
+
|
|
157
|
+
Guidance: issue read-only tokens to reporting/analytics integrations, and
|
|
158
|
+
reserve `admin` scope tokens for operator tooling only.
|
|
159
|
+
|
|
160
|
+
### HTTP and MCP Rate Limiting
|
|
161
|
+
|
|
162
|
+
Risk: a compromised or misbehaving token/client could flood the server with
|
|
163
|
+
writes or searches, or brute-force `/login`.
|
|
164
|
+
|
|
165
|
+
Controls:
|
|
166
|
+
|
|
167
|
+
- A fixed one-minute-window in-memory limiter, keyed by
|
|
168
|
+
`(principal, bucket)` where principal is the token id, else the
|
|
169
|
+
authenticated user id, else the remote address.
|
|
170
|
+
- Buckets: `write` (mutating `/api/*` routes and browser write posts;
|
|
171
|
+
default 120/min, `ARTIFACTY_RATE_WRITE_PER_MIN`), `auth` (`POST /login`;
|
|
172
|
+
default 10/min per address, `ARTIFACTY_RATE_AUTH_PER_MIN`), `search`
|
|
173
|
+
(`GET /api/artifacts` with a `q` query; default 300/min,
|
|
174
|
+
`ARTIFACTY_RATE_SEARCH_PER_MIN`).
|
|
175
|
+
- Disabled by default on a loopback bind (single-user local use is not
|
|
176
|
+
throttled) unless `ARTIFACTY_RATE_LIMIT=always`; `ARTIFACTY_RATE_LIMIT=off`
|
|
177
|
+
disables it unconditionally.
|
|
178
|
+
- Exceeding a bucket's limit returns `429` with a `Retry-After` header (in
|
|
179
|
+
seconds) and `code: "rate_limited"`, and writes a `rate-limited` audit
|
|
180
|
+
row, throttled to one per `(bucket, principal)` window. A mutating MCP
|
|
181
|
+
tool call over the streamable-HTTP transport that exceeds the `write`
|
|
182
|
+
bucket returns an `isError` tool result with
|
|
183
|
+
`structuredContent.code: "rate_limited"` instead of a hard HTTP error.
|
|
184
|
+
- The limiter is in-memory and per-process; it resets on restart and does
|
|
185
|
+
not coordinate across multiple server processes sharing one store.
|
|
186
|
+
|
|
187
|
+
Guidance: raise the `*_PER_MIN` env vars for legitimate high-throughput
|
|
188
|
+
integrations rather than disabling rate limiting outright on a
|
|
189
|
+
non-loopback bind.
|
|
190
|
+
|
|
191
|
+
### Request and Content Size Limits
|
|
192
|
+
|
|
193
|
+
Artifacty enforces the following byte limits, all in-process and independent
|
|
194
|
+
of any reverse proxy's own limits:
|
|
195
|
+
|
|
196
|
+
| Limit | Value | Constant / env |
|
|
197
|
+
| --- | --- | --- |
|
|
198
|
+
| Artifact version content | 16 MiB | `MAX_ARTIFACT_BYTES` (`src/lib/storage.js`) |
|
|
199
|
+
| Backup import body | see `src/lib/backup.js` | `MAX_BACKUP_BYTES` |
|
|
200
|
+
| Comment body (reserved; no comments feature yet) | 16 KiB | `MAX_COMMENT_BYTES` (`src/lib/storage.js`) |
|
|
201
|
+
| Generic JSON/form request body | `MAX_ARTIFACT_BYTES + 1024` | default `limitBytes` in `readJsonBody`/`readFormBody` (`src/server.js`) |
|
|
202
|
+
|
|
203
|
+
A request body over its limit is rejected with `413` before being parsed.
|
|
204
|
+
|
|
205
|
+
### Webhook SSRF and Secret Handling
|
|
206
|
+
|
|
207
|
+
Risk: a webhook target URL is an attacker-influenceable outbound HTTP request
|
|
208
|
+
from the Artifacty server's network position; a leaked webhook secret would let
|
|
209
|
+
an attacker forge signed deliveries.
|
|
210
|
+
|
|
211
|
+
Controls:
|
|
212
|
+
|
|
213
|
+
- Webhook targets must be `http` or `https`.
|
|
214
|
+
- Loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`),
|
|
215
|
+
private ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`,
|
|
216
|
+
`fc00::/7`), CGNAT (`100.64.0.0/10`), IETF protocol assignments
|
|
217
|
+
(`192.0.0.0/24`), benchmarking (`198.18.0.0/15`), multicast
|
|
218
|
+
(`224.0.0.0/4`), `0.0.0.0/8`, and IPv4-mapped IPv6 equivalents of all of
|
|
219
|
+
the above (`::ffff:0:0/96`) are rejected as webhook target hosts unless
|
|
220
|
+
`ARTIFACTY_WEBHOOK_ALLOW_PRIVATE=true` (intended for local development and
|
|
221
|
+
tests only — never set it on a shared or internet-reachable server). The
|
|
222
|
+
IPv4 check parses every legacy `inet_aton` literal form (decimal, octal,
|
|
223
|
+
hex, and 1-3 part shorthand, e.g. `2130706433`, `0177.0.0.1`, `0x7f000001`,
|
|
224
|
+
`127.1`) into its 32-bit value before range-checking it, not just
|
|
225
|
+
four-part dotted-decimal, and the IPv6 check fully expands `::` and zone
|
|
226
|
+
ids before comparing groups, so an equivalent, differently-written form of
|
|
227
|
+
a blocked address (e.g. `[0:0:0:0:0:0:0:1]` for `::1`) is rejected the
|
|
228
|
+
same as its canonical form.
|
|
229
|
+
- The guard is a literal-address check on the URL's hostname, evaluated only
|
|
230
|
+
at webhook creation time (`assertPublicWebhookUrl`, also exercised by
|
|
231
|
+
tests) — it does not resolve DNS, and it does not run again at delivery
|
|
232
|
+
time. A hostname that resolves to a private/loopback address only when the
|
|
233
|
+
outbound HTTP request is actually made (DNS rebinding, or a name whose
|
|
234
|
+
records changed after the webhook was created) is not caught by this
|
|
235
|
+
control. Treat webhook creation as an admin-only, trusted-input operation,
|
|
236
|
+
not as safe to expose to untrusted callers.
|
|
237
|
+
- Deliveries use `redirect: "manual"` and never follow a redirect, so a
|
|
238
|
+
webhook target cannot use a 3xx response to retarget delivery at an
|
|
239
|
+
otherwise-blocked address.
|
|
240
|
+
- The webhook secret is shown once, in the create response, and never stored
|
|
241
|
+
or returned again in plaintext. What is persisted is `secretHash =
|
|
242
|
+
sha256(secret)`, and every delivery is signed as
|
|
243
|
+
`X-Artifacty-Signature: sha256=HMAC-SHA256(key = secretHash, body =
|
|
244
|
+
<raw request body>)`. A receiver that kept the raw secret from the create
|
|
245
|
+
response verifies a delivery by computing `sha256(secret)` locally to
|
|
246
|
+
recover the same key before checking the HMAC — the server never needs to
|
|
247
|
+
read the raw secret again to sign future deliveries.
|
|
248
|
+
- Webhook creation is admin-only once any user account exists; in
|
|
249
|
+
single-user mode (no accounts configured) it is available to whoever holds
|
|
250
|
+
the configured API token, matching every other write route's trust
|
|
251
|
+
boundary.
|
|
252
|
+
- After 20 consecutive delivery failures a webhook is automatically disabled
|
|
253
|
+
and a `webhook-deliver-failed` audit row is written; it stays disabled
|
|
254
|
+
until deleted and recreated.
|
|
255
|
+
|
|
256
|
+
Guidance: only register webhook endpoints you control or trust, prefer HTTPS
|
|
257
|
+
targets, and treat a leaked webhook secret as a credential (delete and
|
|
258
|
+
recreate the webhook if you suspect exposure — that also rotates the
|
|
259
|
+
`secretHash` used for signing).
|
|
260
|
+
|
|
261
|
+
### Artifact Visibility and Ownership
|
|
262
|
+
|
|
263
|
+
Risk: in team mode, any authenticated user could read or modify any other
|
|
264
|
+
user's artifacts, including sensitive drafts or work-in-progress content
|
|
265
|
+
that was never meant to be shared server-wide.
|
|
266
|
+
|
|
267
|
+
Controls:
|
|
268
|
+
|
|
269
|
+
- Every artifact carries a `visibility` (`private` or `team`, default
|
|
270
|
+
`team`) and an `ownerUserId`. `private` artifacts are readable and
|
|
271
|
+
writable only by their owner or an admin; `team` artifacts are readable
|
|
272
|
+
and writable by any authenticated principal, or restricted to the owner
|
|
273
|
+
and admins for writes with `ARTIFACTY_TEAM_WRITE=owner`.
|
|
274
|
+
- Archiving, restoring, and changing visibility or ownership always require
|
|
275
|
+
the artifact's owner or an admin, independent of the write rule above.
|
|
276
|
+
- A read of a `private` artifact by anyone other than its owner or an admin
|
|
277
|
+
returns `404`/`ARTIFACT_NOT_FOUND` rather than `403`, so the artifact's
|
|
278
|
+
existence is not leaked to callers who cannot see it. Writes and manage
|
|
279
|
+
actions on an artifact the caller can see but does not own return `403`
|
|
280
|
+
with `code: "forbidden"`.
|
|
281
|
+
- The access predicate is applied at the SQL layer for list queries (both
|
|
282
|
+
the metadata and FTS search paths), not filtered after the fact, and
|
|
283
|
+
relation entries pointing at a target the caller cannot see are returned
|
|
284
|
+
as `{ restricted: true }` instead of the target's summary.
|
|
285
|
+
- A shared `ARTIFACTY_API_TOKEN` (or any request before the first user
|
|
286
|
+
account exists) has no personal identity and is treated as an anonymous
|
|
287
|
+
team principal: it can read and write `team` artifacts but never sees
|
|
288
|
+
`private` ones, even though it is admin-equivalent for server
|
|
289
|
+
administration routes elsewhere.
|
|
290
|
+
- In single-user mode (no user accounts configured at all) every check is
|
|
291
|
+
bypassed, matching pre-Section-10 behavior, since there is no second
|
|
292
|
+
identity to protect against.
|
|
293
|
+
|
|
294
|
+
Guidance: treat `private` as a convenience boundary between cooperating,
|
|
295
|
+
already-authenticated users on one server, not a substitute for running
|
|
296
|
+
separate stores for genuinely untrusted parties — an admin account can
|
|
297
|
+
always read, write, and reassign ownership of any artifact.
|
|
298
|
+
|
|
299
|
+
### Retention Purge Gate
|
|
300
|
+
|
|
301
|
+
Risk: a misconfigured or overly broad retention policy (e.g. a low
|
|
302
|
+
`purgeArchivedAfterDays` set by mistake, or applied before reviewing what it
|
|
303
|
+
would affect) could hard-delete artifacts and their version files with no
|
|
304
|
+
way to recover them — the only irreversible action any retention setting
|
|
305
|
+
can trigger.
|
|
306
|
+
|
|
307
|
+
Controls:
|
|
308
|
+
|
|
309
|
+
- Retention policy changes (`archiveAfterDays`, `purgeArchivedAfterDays`,
|
|
310
|
+
`auditRetentionDays`, `eventRetentionRows`, `keepTags`) themselves never
|
|
311
|
+
delete anything; they only take effect on the next sweep or explicit
|
|
312
|
+
`artifacty retention run`.
|
|
313
|
+
- `artifacty retention run` and `POST /api/admin/retention/run` default to
|
|
314
|
+
`dryRun: true`, returning a report of what would be archived/purged/pruned
|
|
315
|
+
without changing the store. `/admin/retention` renders that same report so
|
|
316
|
+
an admin can review it before applying anything.
|
|
317
|
+
- Archiving (moving an artifact out of active listings) is reversible via
|
|
318
|
+
`artifacty restore` and never deletes content, so it carries no
|
|
319
|
+
additional gate beyond the existing owner/admin check on `archiveArtifact`.
|
|
320
|
+
- Purging is irreversible and additionally requires
|
|
321
|
+
`ARTIFACTY_RETENTION_ALLOW_PURGE=true` in the server's environment. A
|
|
322
|
+
policy with purge candidates but no allow-purge gate set records them as
|
|
323
|
+
`purgeSkipped: true` in the sweep result instead of silently no-op'ing or
|
|
324
|
+
deleting anyway; the CLI's `--allow-purge` flag and the browser's "Allow
|
|
325
|
+
purge" checkbox both map to the same environment-variable check rather
|
|
326
|
+
than establishing an independent bypass.
|
|
327
|
+
- Every applied sweep writes one `retention-sweep` audit summary row
|
|
328
|
+
(actor `system:retention`), and every purged artifact gets its own
|
|
329
|
+
`retention-purge` audit row before deletion — `retention-purge` rows are
|
|
330
|
+
themselves exempt from `auditRetentionDays` pruning, so a purge always
|
|
331
|
+
remains traceable in the audit log even after the artifact it removed is
|
|
332
|
+
long gone.
|
|
333
|
+
- A purge deletes the artifact's dependent rows that cascade via foreign
|
|
334
|
+
keys (comments, relations, embeddings) plus its `events` rows and search
|
|
335
|
+
index entries (neither of which has a foreign key to the artifact, so
|
|
336
|
+
those are cleared explicitly rather than relying on `ON DELETE CASCADE`).
|
|
337
|
+
Its audit log rows are the one thing that deliberately survive a purge —
|
|
338
|
+
they are the record that the purge happened.
|
|
339
|
+
|
|
127
340
|
## Out of Scope
|
|
128
341
|
|
|
129
342
|
- Public internet hosting without a separate TLS/auth proxy.
|
|
130
343
|
- Multi-user browser write access.
|
|
131
|
-
- OAuth
|
|
132
|
-
|
|
344
|
+
- OAuth or per-user remote MCP authorization beyond the `read`/`write`/`admin`
|
|
345
|
+
personal API token scopes described below.
|
|
346
|
+
- Group- or team-scoped ACLs beyond the single owner + team/private model
|
|
347
|
+
(see Artifact Visibility and Ownership above).
|
|
133
348
|
- Encrypted-at-rest storage.
|
|
134
349
|
- Malware analysis of arbitrary artifact content.
|
|
135
350
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "artifacty",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Local artifact exchange for heterogeneous LLM agents via HTTP and MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"docs/mcp-public-api.md",
|
|
42
42
|
"docs/network-sharing.md",
|
|
43
43
|
"docs/release-checklist.md",
|
|
44
|
+
"docs/roadmap-design.md",
|
|
44
45
|
"docs/sarif-csv-artifact-plan.md",
|
|
45
46
|
"docs/threat-model.md",
|
|
46
47
|
"scripts/lint.mjs",
|