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.
@@ -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 generic CSV.
67
+ generic CSV, and real-world CodeQL/Semgrep/Trivy SARIF fixtures.
35
68
  - Server tests cover SARIF summary rendering, CSV escaping, `/raw` fidelity,
36
- and browser form options.
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
- - Add real-world fixtures from CodeQL, Semgrep, Trivy, and other scanners.
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.
@@ -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, scoped tokens, or per-user remote MCP authorization.
132
- - Per-artifact ACLs.
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.10.8",
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",