artifacty 0.11.1 → 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.
@@ -1,7 +1,7 @@
1
1
  # Threat Model
2
2
 
3
- Artifacty is a local artifact exchange for LLM agents. It is designed for one
4
- operator, one local store, and trusted local MCP clients by default.
3
+ Artifacty is a local artifact exchange for LLM agents. Its default deployment
4
+ uses one operator, one local store, and trusted local MCP clients.
5
5
 
6
6
  ## Assets
7
7
 
@@ -14,8 +14,8 @@ operator, one local store, and trusted local MCP clients by default.
14
14
 
15
15
  ## Trust Boundaries
16
16
 
17
- - **HTTP browser server**: local by default, optionally reachable on LAN/team
18
- networks when explicitly configured.
17
+ - **HTTP browser server**: local by default. An operator can configure access
18
+ from LAN or team networks.
19
19
  - **MCP stdio server**: local process launched by an MCP client. It inherits the
20
20
  local user account's filesystem permissions. In bridge mode, it forwards
21
21
  JSON-RPC to a configured central `/mcp` endpoint instead of touching local
@@ -58,8 +58,11 @@ Controls:
58
58
  - Personal API tokens are stored only as hashes.
59
59
  - Browser sessions use `HttpOnly` and `SameSite=Lax` cookies.
60
60
 
61
- Guidance: rotate tokens after sharing sessions, prefer header-based tokens for
62
- scripts, and use generated startup tokens only for temporary interactive shares.
61
+ Guidance:
62
+
63
+ - Rotate tokens after sharing sessions.
64
+ - Use token headers in scripts.
65
+ - Use generated startup tokens only for temporary interactive shares.
63
66
 
64
67
  ### Cross-Site Request Forgery
65
68
 
@@ -97,12 +100,12 @@ Risk: agents may accidentally publish API keys or private keys into artifacts.
97
100
 
98
101
  Controls:
99
102
 
100
- - Common API key and private key patterns are scanned before storage.
103
+ - Artifacty scans common API key and private key patterns before storage.
101
104
  - Writes fail unless `allowSecrets` or `ARTIFACTY_ALLOW_SECRETS=true` is set.
102
- - Stored scan status is recorded in version metadata.
105
+ - Artifacty records the scan status in version metadata.
103
106
 
104
- Limitations: pattern scanning is best-effort and does not prove content is free
105
- of sensitive data.
107
+ Limitation: pattern scanning can miss sensitive data. A successful scan does
108
+ not prove that content is safe to share.
106
109
 
107
110
  ### MCP Tool Abuse
108
111
 
@@ -120,23 +123,25 @@ Controls:
120
123
  - MCP writes go through the same secret scan and audit paths as CLI/HTTP writes.
121
124
  - MCP resources are read-only.
122
125
 
123
- Guidance: install Artifacty MCP only in clients and workspaces you trust. For
124
- central deployments, prefer TLS through a reverse proxy and rotate shared tokens
125
- after team changes.
126
+ Guidance:
127
+
128
+ - Install Artifacty MCP only in trusted clients and workspaces.
129
+ - For central deployments, use TLS through a reverse proxy.
130
+ - Rotate shared tokens after team changes.
126
131
 
127
132
  ### Personal API Token Scopes
128
133
 
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).
134
+ Risk: an attacker can use a leaked personal API token. A read-only reporting
135
+ token should not permit artifact changes or access to admin routes, such as
136
+ backup and webhooks.
132
137
 
133
138
  Controls:
134
139
 
135
140
  - Each personal token carries a `scopes_json` list drawn from `read`,
136
141
  `write`, `admin` (default `["read", "write"]` for both new tokens and
137
142
  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.
143
+ - Only a user with the admin role can own a token with the `admin` scope.
144
+ `createApiToken` rejects other requests for this scope.
140
145
  - `requireScope` (`src/lib/security.js`) is called on every `/api/*` HTTP
141
146
  route: read routes need `read`, mutating routes need `write`, and
142
147
  admin-only routes (`/api/webhooks*`, `/api/admin/backup*`) need `admin` in
@@ -145,22 +150,20 @@ Controls:
145
150
  throttled to one per token per minute.
146
151
  - The shared `ARTIFACTY_API_TOKEN` and browser sessions always have full
147
152
  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.
153
+ - For HTTP MCP, `tools/list` omits write tools when the token lacks `write`.
154
+ A direct call to one of these tools returns an `isError` result with
155
+ `structuredContent.code: "scope_denied"`. The tool does not execute.
156
+ - Local stdio is a trusted local process and does not enforce token scopes.
157
+ If its configured token identifies a personal account, artifact access rules
158
+ still apply to that account. Private site mode requires that personal identity.
159
+
160
+ Guidance: issue read-only tokens to reporting and analytics integrations.
161
+ Reserve tokens with the `admin` scope for operator tools.
159
162
 
160
163
  ### HTTP and MCP Rate Limiting
161
164
 
162
- Risk: a compromised or misbehaving token/client could flood the server with
163
- writes or searches, or brute-force `/login`.
165
+ Risk: a compromised or faulty client could send excessive writes or searches.
166
+ An attacker could also send repeated password guesses to `/login`.
164
167
 
165
168
  Controls:
166
169
 
@@ -184,29 +187,28 @@ Controls:
184
187
  - The limiter is in-memory and per-process; it resets on restart and does
185
188
  not coordinate across multiple server processes sharing one store.
186
189
 
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
+ Guidance: increase the `*_PER_MIN` environment variables for integrations that
191
+ need higher throughput. Keep rate limiting enabled on non-loopback interfaces.
190
192
 
191
193
  ### Request and Content Size Limits
192
194
 
193
- Artifacty enforces the following byte limits, all in-process and independent
194
- of any reverse proxy's own limits:
195
+ Artifacty enforces the following byte limits in its server process. These
196
+ limits are independent of reverse proxy limits:
195
197
 
196
198
  | Limit | Value | Constant / env |
197
199
  | --- | --- | --- |
198
200
  | Artifact version content | 16 MiB | `MAX_ARTIFACT_BYTES` (`src/lib/storage.js`) |
199
201
  | 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`) |
202
+ | Comment body | 16 KiB | `MAX_COMMENT_BYTES` (`src/lib/storage.js`) |
201
203
  | Generic JSON/form request body | `MAX_ARTIFACT_BYTES + 1024` | default `limitBytes` in `readJsonBody`/`readFormBody` (`src/server.js`) |
202
204
 
203
- A request body over its limit is rejected with `413` before being parsed.
205
+ Artifacty rejects a request body over its limit with `413` before parsing it.
204
206
 
205
207
  ### Webhook SSRF and Secret Handling
206
208
 
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.
209
+ Risk: an attacker who controls a webhook URL can influence outbound HTTP
210
+ requests from the Artifacty server. A leaked webhook secret lets an attacker
211
+ forge signed deliveries.
210
212
 
211
213
  Controls:
212
214
 
@@ -234,9 +236,8 @@ Controls:
234
236
  records changed after the webhook was created) is not caught by this
235
237
  control. Treat webhook creation as an admin-only, trusted-input operation,
236
238
  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.
239
+ - Deliveries use `redirect: "manual"` and never follow redirects. A webhook
240
+ target cannot use a 3xx response to send delivery to a blocked address.
240
241
  - The webhook secret is shown once, in the create response, and never stored
241
242
  or returned again in plaintext. What is persisted is `secretHash =
242
243
  sha256(secret)`, and every delivery is signed as
@@ -249,74 +250,97 @@ Controls:
249
250
  single-user mode (no accounts configured) it is available to whoever holds
250
251
  the configured API token, matching every other write route's trust
251
252
  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.
253
+ - After 20 consecutive delivery failures, Artifacty disables the webhook and
254
+ writes a `webhook-deliver-failed` audit row. The webhook stays disabled until
255
+ an administrator deletes and recreates it.
256
+
257
+ Guidance:
255
258
 
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).
259
+ - Register only webhook endpoints that you control or trust.
260
+ - Prefer HTTPS targets.
261
+ - If you suspect secret exposure, delete the webhook.
262
+ - Recreate the webhook with a new secret. This also rotates its signing
263
+ `secretHash`.
260
264
 
261
265
  ### Artifact Visibility and Ownership
262
266
 
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.
267
+ Risk: one user could read or change another user's artifacts without permission.
268
+ This includes sensitive drafts that the owner did not intend to share.
269
+
270
+ Controls:
271
+
272
+ - Administrators select public or private site mode at `/admin/access`.
273
+ - In public mode, every artifact has `visibility` (`private` or `team`, default
274
+ `team`) and an `ownerUserId`. Only the owner or an administrator can read or
275
+ write `private` artifacts through normal artifact routes. Anonymous browser
276
+ visitors can read `team` artifacts. Authenticated principals can write them.
277
+ API authentication requirements still apply. `ARTIFACTY_TEAM_WRITE=owner` restricts these writes to owners and
278
+ administrators.
279
+ - Private site mode requires a personal identity. Owners and administrators
280
+ retain access. Other users need a direct or team grant. Grants permit
281
+ reading, commenting, or editing. Artifacty applies the strongest matching
282
+ grant. Token scopes impose an additional restriction.
283
+ - Archiving, restoring, sharing, and changes to visibility or ownership require
284
+ the owner or an administrator. These rules apply in both site modes.
285
+ - If a caller cannot read an artifact, the server returns
286
+ `404`/`ARTIFACT_NOT_FOUND`. This prevents disclosure of the artifact's
287
+ existence. If the caller can read it but lacks the required write or management
288
+ permission, the server returns `403` with `code: "forbidden"`.
289
+ - List queries apply access checks in SQL for both metadata and FTS search.
290
+ Relations to unreadable targets return `{ restricted: true }` without the
291
+ target summary.
292
+ - A shared `ARTIFACTY_API_TOKEN` has no personal identity. In public mode, it can
293
+ read and write `team` artifacts but cannot see `private` artifacts. In private
294
+ mode, it cannot read or create artifacts. Its administrative privileges on
295
+ other server routes do not grant a personal artifact identity.
296
+ - In public mode with no user accounts, user access checks do not apply.
297
+
298
+ Guidance: use separate stores for mutually untrusted parties. Administrators can
299
+ read, write, and reassign any artifact in their store.
300
+
301
+ ### Public Share Links and Network Restrictions
302
+
303
+ Risk: a copied share link can disclose an artifact to anyone who holds the link.
304
+ An incorrect network policy can either expose the server or lock out its
305
+ administrator.
266
306
 
267
307
  Controls:
268
308
 
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.
309
+ - Share links provide read-only access. The owner or an administrator creates
310
+ and revokes them. A link can select the latest version or one fixed version.
311
+ - Links expire. Private site mode disables them unless an administrator enables
312
+ them. The server stores token hashes and shows the raw token only at creation.
313
+ - Share pages omit artifact ownership, tags, history, and internal artifact URLs.
314
+ They use `Referrer-Policy: no-referrer` and `Cache-Control: no-store`.
315
+ - All HTTP requests, including share links, obey the configured IP/CIDR allowlist.
316
+ The server checks the TCP peer address and ignores forwarding headers.
317
+ - A network change must include the administrator's current address. It remains
318
+ pending until confirmed. If it expires without confirmation, Artifacty restores
319
+ the previous policy. Administrators can also roll back a pending change.
320
+
321
+ Guidance: treat share links as credentials. Revoke a link if it reaches an
322
+ unintended recipient. When using a reverse proxy, account for its TCP peer
323
+ address in the network policy.
298
324
 
299
325
  ### Retention Purge Gate
300
326
 
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.
327
+ Risk: an incorrect retention policy can permanently delete artifacts and their
328
+ version files. For example, an operator could set `purgeArchivedAfterDays` too
329
+ low or apply it without reviewing the affected artifacts. Purge is the only
330
+ irreversible action that retention settings can trigger.
306
331
 
307
332
  Controls:
308
333
 
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`.
334
+ - Changing `archiveAfterDays`, `purgeArchivedAfterDays`, `auditRetentionDays`,
335
+ `eventRetentionRows`, or `keepTags` does not delete data. The new policy takes
336
+ effect on the next sweep or explicit `artifacty retention run`.
313
337
  - `artifacty retention run` and `POST /api/admin/retention/run` default to
314
338
  `dryRun: true`, returning a report of what would be archived/purged/pruned
315
339
  without changing the store. `/admin/retention` renders that same report so
316
340
  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`.
341
+ - Archiving removes an artifact from active lists without deleting content.
342
+ `artifacty restore` reverses it. The existing owner or administrator check
343
+ on `archiveArtifact` is the only access requirement.
320
344
  - Purging is irreversible and additionally requires
321
345
  `ARTIFACTY_RETENTION_ALLOW_PURGE=true` in the server's environment. A
322
346
  policy with purge candidates but no allow-purge gate set records them as
@@ -340,11 +364,8 @@ Controls:
340
364
  ## Out of Scope
341
365
 
342
366
  - Public internet hosting without a separate TLS/auth proxy.
343
- - Multi-user browser write access.
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).
367
+ - OAuth. Personal API token scopes and direct or team artifact grants are
368
+ supported, as described above.
348
369
  - Encrypted-at-rest storage.
349
370
  - Malware analysis of arbitrary artifact content.
350
371
 
@@ -0,0 +1,56 @@
1
+ # Documentation writing style
2
+
3
+ Artifacty uses an ASD-STE100-inspired style for English documentation. Korean documentation follows the same clarity principles with natural Korean phrasing. This is an editorial guide, not a claim of compliance with ASD-STE100.
4
+
5
+ Apply the sentence and procedure rules consistently. Use technical vocabulary where it makes the meaning more precise. Keep introductions natural. The project's “75%” target describes this balance; it is not a compliance score.
6
+
7
+ ## Sentences and terms
8
+
9
+ - Give each sentence one main idea. Split long sentences when this improves clarity.
10
+ - Name the actor when responsibility matters: the administrator, the artifact owner, the user, or the server.
11
+ - Prefer active voice. Use passive voice when the actor is unknown or does not matter.
12
+ - Use the same term for the same concept. Match UI labels, API fields, and CLI commands exactly.
13
+ - Keep established terms such as artifact, MCP, webhook, and CIDR. Explain unfamiliar terms on first use.
14
+ - Keep words that make a sentence complete. Do not turn prose into compressed notes.
15
+ - Do not enforce a fixed word limit or replace precise terms only to fit a controlled dictionary.
16
+
17
+ ## Instructions and behavior
18
+
19
+ - State prerequisites and conditions before the action.
20
+ - Give each numbered step one action. Show the command or UI control that performs it.
21
+ - State the expected result when readers need it to verify the action.
22
+ - Make defaults, permissions, exceptions, and recovery behavior explicit.
23
+ - Keep requirements and recommendations distinct. Preserve the meaning of MUST, SHOULD, and MAY in specifications.
24
+ - Preserve commands, identifiers, URLs, examples, and technical constraints when editing prose.
25
+ - Describe planned features as planned. Do not present a design proposal as current behavior.
26
+
27
+ ## Apply by document type
28
+
29
+ Use these rules most strictly in installation instructions, API references, and security documentation. Allow more connected prose in the README introduction, design rationale, and launch posts. Keep the explanation readers need; shorter text is not useful if it omits a condition or exception.
30
+
31
+ For example:
32
+
33
+ > By default, share links do not work in private mode. An administrator must enable them in Access settings.
34
+
35
+ This states the default, the responsible actor, and the setting location.
36
+
37
+ Refer to the [official ASD-STE100 overview](https://www.asd-ste100.org/about.html) for the specification's purpose and structure.
38
+
39
+ ## 한국어 문서 작성 기준
40
+
41
+ 한국어 문서에도 같은 명료성 원칙을 적용합니다. 다만 영어의 문장 구조나 어휘 규칙을 그대로 옮기지 않고, 자연스러운 한국어 어순과 흐름을 살립니다. ‘75% 강도’는 편집 방향을 뜻하며, 표준 준수율을 뜻하지 않습니다.
42
+
43
+ - 한 문장에는 하나의 핵심 내용을 담습니다. 서로 밀접한 내용은 자연스럽게 연결하고, 짧게 쓰기 위해 문장을 지나치게 끊지 않습니다.
44
+ - 조건이나 전제는 행동보다 먼저 설명합니다. 절차는 단계별로 나누고, 필요한 경우 예상 결과와 확인 방법을 덧붙입니다.
45
+ - 권한이나 책임을 구분해야 할 때는 주체를 밝힙니다. 문맥상 분명한 주어는 반복하지 않습니다.
46
+ - 번역투, 불필요한 수동 표현, 명사만 나열하는 문장을 피합니다. 예를 들어 ‘설정의 변경이 가능합니다’보다 ‘설정을 변경할 수 있습니다’로 씁니다.
47
+ - 같은 개념에는 같은 용어를 사용합니다. 화면 이름은 해당 언어의 실제 UI 표기와 맞추고, 명령어·API 필드·코드 식별자는 그대로 유지합니다.
48
+ - 익숙한 기술 용어는 억지로 번역하지 않습니다. 설명이 필요한 용어는 처음 나올 때 간단히 풀이합니다.
49
+ - 기본값, 예외, 제한, 복구 조건을 생략하지 않습니다. 필수 사항과 권장 사항도 명확히 구분합니다.
50
+ - 설명문은 ‘합니다’체를 기본으로 씁니다. 절차에서는 필요한 행동을 ‘선택하세요’, ‘실행하세요’처럼 직접 안내합니다.
51
+
52
+ 예를 들어 다음과 같이 씁니다.
53
+
54
+ > 비공개 모드에서는 기본적으로 공유 링크를 사용할 수 없습니다. 관리자가 접근 설정에서 공유 링크를 허용해야 합니다.
55
+
56
+ 설치·API·보안 문서에는 이 기준을 엄격하게 적용합니다. 소개 글과 설계 배경에서는 정확성을 유지하면서 문장이 자연스럽게 이어지도록 씁니다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifacty",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Local artifact exchange for heterogeneous LLM agents via HTTP and MCP.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -44,6 +44,7 @@
44
44
  "docs/roadmap-design.md",
45
45
  "docs/sarif-csv-artifact-plan.md",
46
46
  "docs/threat-model.md",
47
+ "docs/writing-style.md",
47
48
  "scripts/lint.mjs",
48
49
  "scripts/smoke.sh",
49
50
  "README.md",
package/src/cli.js CHANGED
@@ -71,6 +71,16 @@ async function main() {
71
71
  const { startServer } = await import("./server.js");
72
72
  const store = createStore({ home: options.home });
73
73
 
74
+ if (command === "samples") {
75
+ const { installSamples, removeSamples } = await import("./lib/samples.js");
76
+ const action = options._[0];
77
+ if (action !== "install" && action !== "remove") {
78
+ throw new Error("Usage: artifacty samples install|remove");
79
+ }
80
+ printJson(await (action === "install" ? installSamples : removeSamples)(store, { audit: cliAuditContext() }));
81
+ return;
82
+ }
83
+
74
84
  if (command === "serve") {
75
85
  if (options.detach && options.foreground) {
76
86
  throw new Error("Use either --foreground or --detach, not both");
@@ -699,7 +709,12 @@ async function runWatchCommand(store, options) {
699
709
  for (;;) {
700
710
  const matched = await watchOnce({ baseUrl, token, filter, lastEventId, options });
701
711
  if (matched.stop) {
702
- return;
712
+ // --once has delivered its event and run any --exec command, so the
713
+ // command is complete. Flush and exit rather than waiting for every
714
+ // handle to close on its own: a socket the runtime keeps pooled would
715
+ // otherwise leave the process running with nothing left to do.
716
+ await flushStdio();
717
+ process.exit(0);
703
718
  }
704
719
  if (matched.lastEventId) {
705
720
  lastEventId = matched.lastEventId;
@@ -721,9 +736,13 @@ async function watchOnce({ baseUrl, token, filter, lastEventId, options }) {
721
736
  headers["last-event-id"] = String(lastEventId);
722
737
  }
723
738
 
739
+ // An SSE response is a long-lived socket. Cancelling the reader alone left
740
+ // it in the connection pool on some platforms, so `watch --once` could
741
+ // finish its work and still not exit; abort the request instead.
742
+ const controller = new AbortController();
724
743
  let response;
725
744
  try {
726
- response = await fetch(url, { headers });
745
+ response = await fetch(url, { headers, signal: controller.signal });
727
746
  } catch (error) {
728
747
  process.stderr.write(`artifacty watch: connection failed: ${error.message}\n`);
729
748
  return {};
@@ -766,7 +785,12 @@ async function watchOnce({ baseUrl, token, filter, lastEventId, options }) {
766
785
  const event = JSON.parse(parsed.data);
767
786
  await emitWatchEvent(event, options);
768
787
  if (options.once) {
769
- await reader.cancel().catch(() => {});
788
+ // Abort first and do not await the cancel: cancelling a live SSE
789
+ // body can sit unresolved while the server is still writing, which
790
+ // left `watch --once` hanging after it had already printed its
791
+ // event. Aborting the request tears the socket down directly.
792
+ controller.abort();
793
+ reader.cancel().catch(() => {});
770
794
  return { stop: true };
771
795
  }
772
796
  }
@@ -777,6 +801,22 @@ async function watchOnce({ baseUrl, token, filter, lastEventId, options }) {
777
801
  return { lastEventId: seenLastEventId };
778
802
  }
779
803
 
804
+ // Resolves once buffered stdout/stderr have drained, so an explicit exit
805
+ // cannot truncate output that a caller is reading from a pipe. Bounded so a
806
+ // stuck pipe delays the exit rather than preventing it.
807
+ function flushStdio(timeoutMs = 1000) {
808
+ return Promise.race([
809
+ Promise.all([
810
+ new Promise((resolve) => process.stdout.write("", resolve)),
811
+ new Promise((resolve) => process.stderr.write("", resolve))
812
+ ]),
813
+ new Promise((resolve) => {
814
+ const timer = setTimeout(resolve, timeoutMs);
815
+ timer.unref?.();
816
+ })
817
+ ]);
818
+ }
819
+
780
820
  function printWatchReady(options) {
781
821
  if (options.json) {
782
822
  process.stderr.write(`${JSON.stringify({ ready: true })}\n`);
@@ -930,6 +970,7 @@ Usage:
930
970
  artifacty install claude|codex|gemini|copilot|cursor|all [--dry-run] [--config <path>] [--server-path <path>] [--url http://127.0.0.1:8787] [--mcp-url http://127.0.0.1:8787/mcp] [--api-token token] [--transport local|bridge] [--timeout 30000]
931
971
  artifacty check [--server-path <path>] [--timeout 5000]
932
972
  artifacty update <id> (--file <path> | --content <text>) [--title <title>] [--format html|markdown|text|json|code|svg|mermaid|react|notebook] [--expected-version N] [--visibility private|team]
973
+ artifacty samples install|remove
933
974
  artifacty archive <id>
934
975
  artifacty restore <id>
935
976
  artifacty visibility <id> private|team
package/src/lib/backup.js CHANGED
@@ -172,21 +172,21 @@ export async function importStoreBundle(store = createStore(), bundle, { confirm
172
172
  // artifact's comments/relations/embeddings (foreign keys, ON DELETE
173
173
  // CASCADE) — but an artifacts-scope bundle never carries or restores
174
174
  // those tables. Silently wiping a target store's comment/relation
175
- // history because someone ran an artifacts-only restore would be a
175
+ // history or sharing grants because someone ran an artifacts-only restore would be a
176
176
  // surprising, unrecoverable data loss, so refuse unless the caller
177
177
  // explicitly opts in the same way a full-scope restore does.
178
178
  const dependents = countArtifactDependents(store);
179
- if (dependents.comments > 0 || dependents.relations > 0) {
179
+ if (dependents.comments > 0 || dependents.relations > 0 || dependents.shares > 0) {
180
180
  if (confirm !== "replace-all") {
181
181
  throw Object.assign(
182
182
  new Error(
183
- 'Target store has comments and/or relations that this artifacts-scope restore would delete without restoring equivalents. Pass confirm: "replace-all" to proceed anyway, or use a full-scope backup.'
183
+ 'Target store has comments, relations, and/or sharing grants that this artifacts-scope restore would delete without restoring equivalents. Pass confirm: "replace-all" to proceed anyway, or use a full-scope backup.'
184
184
  ),
185
185
  { statusCode: 409, code: "dependents_exist" }
186
186
  );
187
187
  }
188
188
  warnings.push(
189
- `Target store's ${dependents.comments} comment(s) and ${dependents.relations} relation(s) were deleted by this artifacts-scope restore and are not restored (only a full-scope backup carries them).`
189
+ `Target store's ${dependents.comments} comment(s) and ${dependents.relations} relation(s), and ${dependents.shares} sharing grant(s) were deleted by this artifacts-scope restore and are not restored (only a full-scope backup carries them).`
190
190
  );
191
191
  }
192
192
  }
@@ -238,7 +238,8 @@ function countArtifactDependents(store) {
238
238
  try {
239
239
  return {
240
240
  comments: db.prepare("SELECT COUNT(*) AS count FROM artifact_comments").get().count,
241
- relations: db.prepare("SELECT COUNT(*) AS count FROM artifact_relations").get().count
241
+ relations: db.prepare("SELECT COUNT(*) AS count FROM artifact_relations").get().count,
242
+ shares: db.prepare("SELECT COUNT(*) AS count FROM artifact_shares").get().count
242
243
  };
243
244
  } finally {
244
245
  db.close();
@@ -254,7 +255,11 @@ const FULL_BACKUP_TABLE_ARRAY_KEYS = [
254
255
  "comments",
255
256
  "savedViews",
256
257
  "embeddings",
257
- "meta"
258
+ "meta",
259
+ "teams",
260
+ "teamMembers",
261
+ "artifactShares",
262
+ "notifications"
258
263
  ];
259
264
 
260
265
  // Partial, fail-fast validation of a full-scope backup's `full` object,
@@ -289,6 +294,14 @@ function assertValidFullBackupTables(full) {
289
294
  });
290
295
  }
291
296
  }
297
+ for (const row of full.artifactShares || []) {
298
+ if (row?.permission !== undefined && !["read", "comment", "edit"].includes(row.permission)) {
299
+ throw Object.assign(new Error("Invalid Artifacty backup: sharing permission must be read, comment, or edit"), {
300
+ statusCode: 400,
301
+ code: "INVALID_BACKUP"
302
+ });
303
+ }
304
+ }
292
305
  for (const row of full.apiTokens || []) {
293
306
  if (!row || typeof row !== "object" || !String(row.id || "").trim() || !String(row.userId || "").trim()) {
294
307
  throw Object.assign(new Error("Invalid Artifacty backup: each `full.apiTokens` entry needs an id and userId"), {