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.
- 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 +2 -1
- 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 +141 -27
- 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/docs/threat-model.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Threat Model
|
|
2
2
|
|
|
3
|
-
Artifacty is a local artifact exchange for LLM agents.
|
|
4
|
-
operator, one local store, and trusted local MCP clients
|
|
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
|
|
18
|
-
|
|
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:
|
|
62
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
105
|
+
- Artifacty records the scan status in version metadata.
|
|
103
106
|
|
|
104
|
-
|
|
105
|
-
|
|
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:
|
|
124
|
-
|
|
125
|
-
|
|
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:
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
-
|
|
139
|
-
|
|
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
|
-
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
163
|
-
|
|
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:
|
|
188
|
-
|
|
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
|
|
194
|
-
of
|
|
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
|
|
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
|
-
|
|
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
|
|
208
|
-
from the Artifacty server
|
|
209
|
-
|
|
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
|
|
238
|
-
|
|
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
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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:
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
-
|
|
310
|
-
`
|
|
311
|
-
|
|
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
|
|
318
|
-
`artifacty restore`
|
|
319
|
-
|
|
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
|
-
-
|
|
344
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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"), {
|