artifacty 0.4.0 → 0.6.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/README.md CHANGED
@@ -27,37 +27,43 @@ artifacty --help
27
27
  Run it without a global install:
28
28
 
29
29
  ```bash
30
- npx artifacty@latest serve
30
+ npx artifacty@latest serve --foreground
31
31
  ```
32
32
 
33
- Start the local dashboard:
33
+ Start the local dashboard in the background:
34
34
 
35
35
  ```bash
36
36
  artifacty serve
37
37
  ```
38
38
 
39
- Open the URL printed by the server. Artifacty prefers `http://127.0.0.1:8787`; if that default port is busy and no explicit port was configured, it starts on the next available local port and records the actual URL for CLI and MCP responses.
39
+ Open the `url` printed in the JSON response. Artifacty prefers `http://127.0.0.1:8787`; if that default port is busy and no explicit port was configured, it starts on the next available local port and records the actual URL for CLI and MCP responses.
40
40
 
41
- Run it in the background and return to your prompt:
41
+ Manage the background server:
42
42
 
43
43
  ```bash
44
- artifacty start
45
44
  artifacty status
46
45
  artifacty stop
47
46
  ```
48
47
 
49
- `artifacty serve --detach` is equivalent to `artifacty start`. Logs are written under `~/.artifacty/logs/`.
48
+ `artifacty start` and `artifacty serve --detach` use the same lifecycle path as `artifacty serve`. Logs are written under `~/.artifacty/logs/`.
50
49
  These lifecycle commands use Node's detached process support and work on macOS, Linux, and Windows. `artifacty stop` uses Windows `taskkill` on Windows and process-group signals on macOS/Linux.
51
50
 
51
+ For foreground debugging, keep the process attached:
52
+
53
+ ```bash
54
+ artifacty serve --foreground
55
+ npm start
56
+ ```
57
+
52
58
  Generate an API token at startup when you want to protect HTTP API and browser write routes:
53
59
 
54
60
  ```bash
55
61
  artifacty serve --generate-token
56
62
  artifacty serve --host 0.0.0.0 --share-mode lan --generate-token
57
- npm start -- --generate-token
63
+ artifacty serve --foreground --generate-token
58
64
  ```
59
65
 
60
- The server prints the generated token plus `/new?token=...` and `/import?token=...` URLs. For scripts or background services that need a stable token, generate one first:
66
+ Background `serve` returns the generated token and ready-to-open `/new?token=...` and `/import?token=...` URLs in JSON. Foreground `serve` prints the same values to stderr. For scripts or long-running services that need a stable token, generate one first:
61
67
 
62
68
  ```bash
63
69
  artifacty token
@@ -216,11 +222,11 @@ artifacty integrity
216
222
 
217
223
  ## API Example
218
224
 
219
- Start a protected server in another terminal, or generate a reusable shell token first:
225
+ Start a protected server with a reusable shell token:
220
226
 
221
227
  ```bash
222
- artifacty serve --generate-token
223
228
  export ARTIFACTY_API_TOKEN="$(artifacty token --raw)"
229
+ artifacty serve --api-token "$ARTIFACTY_API_TOKEN"
224
230
  ```
225
231
 
226
232
  ```bash
@@ -285,9 +291,11 @@ Schema and storage:
285
291
  ## Security Model
286
292
 
287
293
  - The HTTP server binds to `127.0.0.1` by default.
288
- - If `ARTIFACTY_API_TOKEN` is set, HTTP API routes require `Authorization: Bearer <token>` or `x-artifacty-token`.
294
+ - If `ARTIFACTY_API_TOKEN` is set, HTTP API routes require `Authorization: Bearer <token>` or `x-artifacty-token`; scripts should prefer headers over `?token=...` URLs.
295
+ - API token checks use timing-safe digest comparison.
289
296
  - Binding outside localhost requires both `ARTIFACTY_SHARE_MODE=lan` or `team` and `ARTIFACTY_API_TOKEN`.
290
297
  - Non-local sharing is intended for trusted LAN or VPN sessions. Prefer a specific interface IP over `0.0.0.0`, keep React rendering disabled, and see [docs/network-sharing.md](docs/network-sharing.md).
298
+ - Non-local binding prints a startup warning because Artifacty does not terminate TLS.
291
299
  - Artifact content is scanned for common API keys and private keys before storage. Use `--allow-secrets` or `ARTIFACTY_ALLOW_SECRETS=true` only for intentional exceptions.
292
300
  - Creates, updates, reads, imports, archives, and restores write audit events to SQLite.
293
301
  - CodeMirror editor/viewer and renderer assets are served from local npm dependencies through a package allowlist, not from a public CDN. JavaScript asset routes answer `Origin: null` requests with `Access-Control-Allow-Origin: null` so sandboxed renderer iframes can import local ESM without `allow-same-origin`.
@@ -300,5 +308,6 @@ Schema and storage:
300
308
  - CSV artifacts render as an escaped, bounded table; `/raw` preserves the original text.
301
309
  - Image and video artifacts store base64 media inline, render safe previews, and decode bytes through `/raw`.
302
310
  - Artifact content should still be treated as untrusted; use the raw view when handing content back to an agent.
311
+ - npm releases are published with GitHub Actions OIDC Trusted Publishing after lint, test, and smoke checks pass.
303
312
 
304
- See [docs/release-checklist.md](docs/release-checklist.md) before publishing or running a shared instance.
313
+ See [SECURITY.md](SECURITY.md), [docs/threat-model.md](docs/threat-model.md), and [docs/release-checklist.md](docs/release-checklist.md) before publishing or running a shared instance.
package/SECURITY.md ADDED
@@ -0,0 +1,49 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ Artifacty is pre-1.0. Security fixes are provided for the latest published
6
+ minor release. Upgrade to the latest `artifacty` version before reporting a
7
+ suspected issue.
8
+
9
+ ## Reporting a Vulnerability
10
+
11
+ Use GitHub private vulnerability reporting if it is enabled for this repository.
12
+ If private reporting is unavailable, open a GitHub issue with a minimal summary
13
+ and avoid including exploit details, secrets, private artifacts, or sensitive
14
+ network information.
15
+
16
+ Please include:
17
+
18
+ - Artifacty version and install method.
19
+ - Node.js version and operating system.
20
+ - Whether the HTTP server was localhost-only, LAN/team mode, or behind a proxy.
21
+ - Whether `ARTIFACTY_API_TOKEN` and `ARTIFACTY_ENABLE_REACT_RENDERER` were set.
22
+ - Reproduction steps using non-sensitive sample content.
23
+
24
+ ## Security Model
25
+
26
+ Artifacty is local-first. The default HTTP server binds to `127.0.0.1`; binding
27
+ outside loopback requires `ARTIFACTY_SHARE_MODE=lan|team` and an API token.
28
+
29
+ The project treats artifact content as untrusted:
30
+
31
+ - HTML, SVG, Mermaid, and React rendering use sandboxed frames and scoped CSP.
32
+ - React execution is disabled unless `ARTIFACTY_ENABLE_REACT_RENDERER=true`.
33
+ - Mutating browser routes reject non-local `Origin` headers.
34
+ - API token checks use constant-time digest comparison.
35
+ - Artifact content is scanned for common API keys and private keys before
36
+ storage unless explicitly allowed.
37
+
38
+ Artifacty does not provide TLS termination, remote OAuth, multi-user access
39
+ control, or a hosted sync service. Do not expose the server directly to the
40
+ public internet.
41
+
42
+ ## Supply Chain
43
+
44
+ npm releases are published through GitHub Actions using npm Trusted Publishing
45
+ OIDC. The release workflow runs lint, tests, and smoke checks on supported Node
46
+ versions before publishing.
47
+
48
+ See [docs/threat-model.md](docs/threat-model.md) and
49
+ [docs/network-sharing.md](docs/network-sharing.md) for operational guidance.
@@ -13,22 +13,23 @@ The dashboard prefers `http://127.0.0.1:8787`. If that port is busy and no expli
13
13
  Use a generated startup token when running a protected foreground server:
14
14
 
15
15
  ```bash
16
- node src/cli.js serve --generate-token
17
- node src/cli.js serve --host 0.0.0.0 --share-mode lan --generate-token
16
+ node src/cli.js serve --foreground --generate-token
17
+ node src/cli.js serve --foreground --host 0.0.0.0 --share-mode lan --generate-token
18
18
  npm start -- --generate-token
19
19
  ```
20
20
 
21
- The generated token is printed with ready-to-open create and import URLs.
21
+ Foreground runs print the generated token with ready-to-open create and import URLs. Background runs return the same values in JSON.
22
22
 
23
23
  For prompt-friendly local background runs, use the lifecycle commands:
24
24
 
25
25
  ```bash
26
+ node src/cli.js serve --port 8787
26
27
  node src/cli.js start --port 8787
27
28
  node src/cli.js status
28
29
  node src/cli.js stop
29
30
  ```
30
31
 
31
- `serve --detach` uses the same detached-process path as `start`. It writes `server.pid`, `server.json`, and logs under `ARTIFACTY_HOME` (default `~/.artifacty`). Prefer `start --api-token "$(node src/cli.js token --raw)"` when a background server needs API protection, because generated startup tokens are only visible in the server log.
32
+ `serve`, `serve --detach`, and `start` use the same detached-process path. They write `server.pid`, `server.json`, and logs under `ARTIFACTY_HOME` (default `~/.artifacty`). `serve --generate-token` and `start --generate-token` generate the API token in the parent CLI process and return it in JSON along with ready-to-open create/import URLs. Use `serve --foreground` when you want attached logs for debugging.
32
33
 
33
34
  The lifecycle commands are intended to be cross-platform:
34
35
 
@@ -105,7 +106,7 @@ Generate a token for protected HTTP routes:
105
106
  node src/cli.js token
106
107
  node src/cli.js serve --generate-token
107
108
  npm start -- --generate-token
108
- ARTIFACTY_API_TOKEN="$(node src/cli.js token --raw)" node src/cli.js serve
109
+ ARTIFACTY_API_TOKEN="$(node src/cli.js token --raw)" node src/cli.js serve --foreground
109
110
  ```
110
111
 
111
112
  ## Claude Code
@@ -308,7 +309,7 @@ node src/cli.js service install
308
309
 
309
310
  The generated service runs `src/server.js` with explicit `--host` and `--home` arguments. It includes `--port` only when you configure a port, which keeps the default port fallback available. Load or unload it manually with the `launchctl` commands returned by `service install`.
310
311
 
311
- For background services, prefer a stable `ARTIFACTY_API_TOKEN` in the service environment. `serve --generate-token` is intended for foreground runs where the operator can read the generated token from startup output.
312
+ For background services, prefer a stable `ARTIFACTY_API_TOKEN` in the service environment. `serve --generate-token` is intended for temporary interactive sessions; the parent CLI returns the generated token in JSON.
312
313
 
313
314
  ## Backup and Audit
314
315
 
@@ -30,6 +30,8 @@ The generated token protects HTTP API routes and browser write forms. Prefer the
30
30
 
31
31
  Artifacty does not terminate TLS. Do not expose it directly on the public internet. If a shared instance must cross an untrusted network, put it behind a TLS reverse proxy or a private VPN.
32
32
 
33
+ When Artifacty binds outside loopback, startup output includes a warning that the server is reachable beyond the local machine and that TLS is not provided by Artifacty. Background `serve` returns this warning in JSON; foreground `serve` and `src/server.js` also write it to stderr.
34
+
33
35
  ## Browser Write Behavior
34
36
 
35
37
  Remote browsers can read shared pages, but write actions are intentionally conservative. Mutating browser routes reject non-local `Origin` headers to reduce CSRF risk. For LAN sharing, prefer API or MCP writes with an explicit token header.
@@ -39,3 +41,5 @@ Do not relax the origin check just to make remote browser writes easier. A futur
39
41
  ## Renderer Guidance
40
42
 
41
43
  Artifact content is untrusted. HTML, SVG, Mermaid, and React artifacts are rendered with sandboxing and CSP controls, but shared viewing still means content reaches another user's browser. Keep `ARTIFACTY_ENABLE_REACT_RENDERER` disabled for LAN sessions unless every viewer trusts the artifact source.
44
+
45
+ See [threat-model.md](threat-model.md) for the full trust-boundary summary.
@@ -8,7 +8,7 @@ Use this checklist before publishing or distributing Artifacty.
8
8
  npm run release:check
9
9
  ```
10
10
 
11
- This runs syntax checks, the full Node test suite, and a local smoke test that starts the HTTP server with token auth enabled, creates an artifact, verifies secret blocking, reads audit logs, writes a backup, and checks MCP tool discovery.
11
+ This runs syntax checks, the full Node test suite, and a local smoke test that starts the HTTP server with token auth enabled, creates an artifact, verifies secret blocking, reads audit logs, writes a backup, and checks MCP tool/resource/prompt discovery.
12
12
 
13
13
  ## Packaging
14
14
 
@@ -23,7 +23,11 @@ This runs syntax checks, the full Node test suite, and a local smoke test that s
23
23
 
24
24
  - Keep the default HTTP bind address at `127.0.0.1`.
25
25
  - Require `ARTIFACTY_API_TOKEN` and `ARTIFACTY_SHARE_MODE=lan` or `team` before binding to `0.0.0.0`.
26
+ - Confirm non-loopback startup output includes the LAN/team warning.
27
+ - Confirm `artifacty serve` starts a managed background server and returns prompt-friendly JSON; use `artifacty serve --foreground` for attached log checks.
28
+ - Prefer `x-artifacty-token` or `Authorization: Bearer <token>` over query tokens in scripts.
26
29
  - Review secret-scan bypasses. `--allow-secrets` and `ARTIFACTY_ALLOW_SECRETS=true` should be deliberate and temporary.
30
+ - Review [../SECURITY.md](../SECURITY.md) and [threat-model.md](threat-model.md) when changing auth, rendering, MCP, or network-sharing behavior.
27
31
  - Treat artifact HTML and imported agent payloads as untrusted content.
28
32
  - Confirm scripted artifact iframes never include `allow-same-origin`.
29
33
  - Confirm `/assets/*` and `/vendor/npm/*` JavaScript responses return
@@ -35,6 +39,8 @@ This runs syntax checks, the full Node test suite, and a local smoke test that s
35
39
  not prove Mermaid or React rendered inside the iframe.
36
40
  - Keep `ARTIFACTY_ENABLE_REACT_RENDERER` disabled by default. Enable it only when the operator accepts arbitrary component execution risk.
37
41
  - Confirm parent app CSP does not include `unsafe-eval`; it should appear only on the React frame response CSP.
42
+ - Confirm CodeQL, Scorecard, and Dependabot configuration changes are intentional.
43
+ - Confirm npm publish still uses OIDC Trusted Publishing rather than a long-lived npm token.
38
44
 
39
45
  ## Operations
40
46
 
@@ -0,0 +1,128 @@
1
+ # Threat Model
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.
5
+
6
+ ## Assets
7
+
8
+ - Artifact content, including generated code, reports, screenshots, and media.
9
+ - Artifact metadata, tags, audit records, and version history.
10
+ - API tokens and generated startup tokens.
11
+ - Local MCP client configuration files.
12
+ - The Artifacty SQLite database and immutable version files.
13
+
14
+ ## Trust Boundaries
15
+
16
+ - **HTTP browser server**: local by default, optionally reachable on LAN/team
17
+ networks when explicitly configured.
18
+ - **MCP stdio server**: local process launched by an MCP client. It inherits the
19
+ local user account's filesystem permissions.
20
+ - **Artifact renderers**: untrusted content is rendered inside browser sandbox
21
+ boundaries where practical.
22
+ - **Storage**: Artifacty stores content under `ARTIFACTY_HOME`; anyone with
23
+ filesystem access to that directory can read artifacts.
24
+ - **npm package**: published through GitHub Actions OIDC after automated checks.
25
+
26
+ ## Primary Threats and Controls
27
+
28
+ ### Accidental Network Exposure
29
+
30
+ Risk: binding to `0.0.0.0` exposes Artifacty on every interface, including VPNs
31
+ or cloud VM public interfaces.
32
+
33
+ Controls:
34
+
35
+ - Default host is `127.0.0.1`.
36
+ - Non-loopback binding requires `ARTIFACTY_SHARE_MODE=lan|team`.
37
+ - Non-loopback binding also requires `ARTIFACTY_API_TOKEN`.
38
+ - Startup logs warn when the server binds outside loopback.
39
+
40
+ Guidance: prefer a specific private interface IP over `0.0.0.0`. Do not expose
41
+ Artifacty directly to the public internet.
42
+
43
+ ### Token Leakage
44
+
45
+ Risk: query-string tokens can appear in browser history, shell history, logs, or
46
+ referrers.
47
+
48
+ Controls:
49
+
50
+ - Scripts should use `x-artifacty-token` or `Authorization: Bearer <token>`.
51
+ - Browser form token URLs exist only for local convenience.
52
+ - Token comparisons use timing-safe digest comparison.
53
+
54
+ Guidance: rotate tokens after sharing sessions, prefer header-based tokens for
55
+ scripts, and use generated startup tokens only for temporary interactive shares.
56
+
57
+ ### Cross-Site Request Forgery
58
+
59
+ Risk: a remote website could attempt to submit writes to an Artifacty server
60
+ reachable from the user's browser.
61
+
62
+ Controls:
63
+
64
+ - Mutating browser routes reject non-local `Origin` headers.
65
+ - API routes require a token when configured.
66
+ - LAN/team mode does not relax browser-origin checks.
67
+
68
+ ### Untrusted Artifact Rendering
69
+
70
+ Risk: HTML, SVG, Mermaid, or React content could execute code in a viewer's
71
+ browser.
72
+
73
+ Controls:
74
+
75
+ - HTML artifacts render in sandboxed iframes.
76
+ - SVG artifacts render in scriptless sandboxed iframes after viewer-side
77
+ sanitization.
78
+ - Mermaid renders in a sandboxed iframe without `allow-same-origin`.
79
+ - React is source-only unless `ARTIFACTY_ENABLE_REACT_RENDERER=true`.
80
+ - React rendering, when enabled, runs in a separate sandboxed frame with
81
+ frame-scoped CSP.
82
+
83
+ Guidance: keep React rendering disabled for shared sessions unless all viewers
84
+ trust the source.
85
+
86
+ ### Secret Storage
87
+
88
+ Risk: agents may accidentally publish API keys or private keys into artifacts.
89
+
90
+ Controls:
91
+
92
+ - Common API key and private key patterns are scanned before storage.
93
+ - Writes fail unless `allowSecrets` or `ARTIFACTY_ALLOW_SECRETS=true` is set.
94
+ - Stored scan status is recorded in version metadata.
95
+
96
+ Limitations: pattern scanning is best-effort and does not prove content is free
97
+ of sensitive data.
98
+
99
+ ### MCP Tool Abuse
100
+
101
+ Risk: an MCP client can create, update, import, archive, restore, and read local
102
+ artifacts through stdio.
103
+
104
+ Controls:
105
+
106
+ - MCP is local stdio only.
107
+ - MCP writes go through the same secret scan and audit paths as CLI/HTTP writes.
108
+ - MCP resources are read-only.
109
+
110
+ Guidance: install Artifacty MCP only in clients and workspaces you trust.
111
+
112
+ ## Out of Scope
113
+
114
+ - Public internet hosting without a separate TLS/auth proxy.
115
+ - Multi-user browser write access.
116
+ - OAuth or remote MCP authorization.
117
+ - Per-artifact ACLs.
118
+ - Encrypted-at-rest storage.
119
+ - Malware analysis of arbitrary artifact content.
120
+
121
+ ## Security Review Checklist
122
+
123
+ - Run `npm run release:check`.
124
+ - Run `artifacty integrity` against important stores.
125
+ - Confirm non-local sharing uses a token and trusted LAN/VPN.
126
+ - Keep `ARTIFACTY_ENABLE_REACT_RENDERER` disabled unless required.
127
+ - Prefer token headers over query tokens in scripts.
128
+ - Review generated MCP configs before committing workspace files.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifacty",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Local artifact exchange for heterogeneous LLM agents via HTTP and MCP.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -41,9 +41,11 @@
41
41
  "docs/network-sharing.md",
42
42
  "docs/release-checklist.md",
43
43
  "docs/sarif-csv-artifact-plan.md",
44
+ "docs/threat-model.md",
44
45
  "scripts/smoke.sh",
45
46
  "README.md",
46
47
  "LICENSE",
48
+ "SECURITY.md",
47
49
  "THIRD_PARTY_NOTICES.md",
48
50
  "AGENTS.md",
49
51
  "CLAUDE.md"
package/src/cli.js CHANGED
@@ -47,7 +47,10 @@ async function main() {
47
47
  }
48
48
 
49
49
  if (command === "serve") {
50
- if (options.detach) {
50
+ if (options.detach && options.foreground) {
51
+ throw new Error("Use either --foreground or --detach, not both");
52
+ }
53
+ if (!options.foreground) {
51
54
  printJson(await startBackgroundServer({
52
55
  ...serverOptions(options),
53
56
  serverPath: path.join(PACKAGE_ROOT, "src", "server.js")
@@ -68,6 +71,9 @@ async function main() {
68
71
  });
69
72
  process.stderr.write(`Artifacty listening on ${server.url}\n`);
70
73
  process.stderr.write(`Store: ${server.store.home}\n`);
74
+ if (server.securityWarning) {
75
+ process.stderr.write(`${server.securityWarning}\n`);
76
+ }
71
77
  if (generatedToken) {
72
78
  process.stderr.write(`API token: ${generatedToken.token}\n`);
73
79
  process.stderr.write(`HTTP header: ${generatedToken.header}\n`);
@@ -311,7 +317,7 @@ function parseArgs(args) {
311
317
  }
312
318
 
313
319
  const key = arg.slice(2);
314
- if (key === "raw" || key === "dry-run" || key === "trust" || key === "include-archived" || key === "allow-secrets" || key === "generate-token" || key === "detach" || key === "force") {
320
+ if (key === "raw" || key === "dry-run" || key === "trust" || key === "include-archived" || key === "allow-secrets" || key === "generate-token" || key === "detach" || key === "foreground" || key === "force") {
315
321
  options[toCamelCase(key)] = true;
316
322
  continue;
317
323
  }
@@ -394,10 +400,11 @@ function printHelp() {
394
400
 
395
401
  Usage:
396
402
  artifacty token [--bytes 32] [--raw]
397
- artifacty serve [--host 127.0.0.1] [--port 8787] [--home ~/.artifacty] [--api-token token] [--generate-token] [--bytes 32] [--detach]
398
- artifacty start [--host 127.0.0.1] [--port 8787] [--home ~/.artifacty] [--api-token token] [--timeout 5000]
403
+ artifacty serve [--host 127.0.0.1] [--port 8787] [--home ~/.artifacty] [--api-token token] [--generate-token] [--bytes 32] [--foreground]
404
+ artifacty serve --foreground [--generate-token]
405
+ artifacty start [--host 127.0.0.1] [--port 8787] [--home ~/.artifacty] [--api-token token] [--generate-token] [--timeout 30000]
399
406
  artifacty status [--home ~/.artifacty]
400
- artifacty stop [--home ~/.artifacty] [--timeout 5000] [--force]
407
+ artifacty stop [--home ~/.artifacty] [--timeout 30000] [--force]
401
408
  artifacty publish --title <title> (--file <path> | --content <text>) [--format html|markdown|text|json|code|svg|mermaid|react] [--source agent] [--tag tag]
402
409
  artifacty import --agent claude|codex|gemini|copilot|cursor|auto (--file <path> | --content <text>) [--title <title>] [--format html|markdown|text|json|code|svg|mermaid|react] [--tag tag]
403
410
  artifacty install claude|codex|gemini|copilot|cursor|all [--dry-run] [--config <path>] [--server-path <path>] [--url http://127.0.0.1:8787] [--timeout 30000]
@@ -4,14 +4,23 @@ import { readFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { createStore } from "./storage.js";
6
6
  import { readServerState, serverStatePath } from "./server-state.js";
7
+ import { exposureWarning, securityConfig } from "./security.js";
8
+ import { generateToken } from "./token.js";
7
9
 
8
- const DEFAULT_READY_TIMEOUT_MS = 5000;
10
+ const DEFAULT_READY_TIMEOUT_MS = 30000;
11
+ const DEFAULT_HOST = "127.0.0.1";
9
12
 
10
13
  export async function startBackgroundServer(options = {}) {
11
14
  if (options.generateToken && options.apiToken) {
12
15
  throw new Error("Use either --api-token or --generate-token, not both");
13
16
  }
14
17
 
18
+ const generatedToken = options.generateToken ? generateToken(options) : null;
19
+ const serverOptions = {
20
+ ...options,
21
+ apiToken: generatedToken?.token || options.apiToken,
22
+ generateToken: false
23
+ };
15
24
  const store = createStore({ home: options.home });
16
25
  const paths = backgroundPaths(store);
17
26
  const current = await backgroundStatus({ home: store.home });
@@ -22,7 +31,7 @@ export async function startBackgroundServer(options = {}) {
22
31
  mkdirSync(paths.logDir, { recursive: true });
23
32
  mkdirSync(store.home, { recursive: true });
24
33
 
25
- const child = spawnDetachedServer(options, store, paths);
34
+ const child = spawnDetachedServer(serverOptions, store, paths);
26
35
 
27
36
  writeFileSync(paths.pidFile, `${child.pid}\n`, "utf8");
28
37
 
@@ -39,6 +48,11 @@ export async function startBackgroundServer(options = {}) {
39
48
  pid: child.pid,
40
49
  url: ready.url,
41
50
  home: store.home,
51
+ auth: authResponse(generatedToken, ready.url),
52
+ securityWarning: exposureWarning({
53
+ host: serverOptions.host || process.env.ARTIFACTY_HOST || DEFAULT_HOST,
54
+ config: securityConfig(serverOptions)
55
+ }) || undefined,
42
56
  logs: {
43
57
  stdout: paths.stdoutLog,
44
58
  stderr: paths.stderrLog
@@ -58,6 +72,20 @@ export async function startBackgroundServer(options = {}) {
58
72
  }
59
73
  }
60
74
 
75
+ function authResponse(generatedToken, url) {
76
+ if (!generatedToken) {
77
+ return null;
78
+ }
79
+ return {
80
+ token: generatedToken.token,
81
+ bytes: generatedToken.bytes,
82
+ header: generatedToken.header,
83
+ authorization: generatedToken.authorization,
84
+ createUrl: `${url}/new?token=${encodeURIComponent(generatedToken.token)}`,
85
+ importUrl: `${url}/import?token=${encodeURIComponent(generatedToken.token)}`
86
+ };
87
+ }
88
+
61
89
  export async function stopBackgroundServer(options = {}) {
62
90
  const store = createStore({ home: options.home });
63
91
  const paths = backgroundPaths(store);
@@ -68,7 +96,7 @@ export async function stopBackgroundServer(options = {}) {
68
96
  action: "stop",
69
97
  stopped: false,
70
98
  running: status.running,
71
- reason: status.pid ? "server was not started by artifacty start" : "server is not running",
99
+ reason: status.pid ? "server was not started by artifacty serve/start" : "server is not running",
72
100
  pid: status.pid || null,
73
101
  home: store.home
74
102
  };
@@ -1,3 +1,5 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+
1
3
  const TOKEN_HEADER = "x-artifacty-token";
2
4
 
3
5
  const SECRET_PATTERNS = [
@@ -31,12 +33,23 @@ export function validateServerExposure({ host, config = securityConfig() }) {
31
33
  }
32
34
  }
33
35
 
36
+ export function exposureWarning({ host, config = securityConfig() }) {
37
+ if (isLoopbackHost(host)) {
38
+ return "";
39
+ }
40
+ return [
41
+ `Warning: Artifacty is listening on ${host} in ${config.shareMode} share mode.`,
42
+ "HTTP is not encrypted by Artifacty; use only a trusted LAN/VPN or place it behind TLS.",
43
+ "Prefer x-artifacty-token or Authorization headers for scripts, and keep React rendering disabled unless every viewer trusts the artifact source."
44
+ ].join(" ");
45
+ }
46
+
34
47
  export function requireToken({ request, url, body = {}, config = securityConfig() }) {
35
48
  if (!config.apiToken) {
36
49
  return;
37
50
  }
38
51
  const provided = extractToken({ request, url, body });
39
- if (provided !== config.apiToken) {
52
+ if (!tokensEqual(provided, config.apiToken)) {
40
53
  throw Object.assign(new Error("Artifacty API token required"), {
41
54
  code: "AUTH_REQUIRED",
42
55
  statusCode: 401
@@ -87,6 +100,15 @@ export function assertNoSecrets(input = {}, options = {}) {
87
100
  };
88
101
  }
89
102
 
103
+ export function tokensEqual(provided, expected) {
104
+ const providedText = String(provided ?? "");
105
+ const expectedText = String(expected ?? "");
106
+ const providedDigest = createHash("sha256").update(providedText).digest();
107
+ const expectedDigest = createHash("sha256").update(expectedText).digest();
108
+ return timingSafeEqual(providedDigest, expectedDigest) &&
109
+ Buffer.byteLength(providedText) === Buffer.byteLength(expectedText);
110
+ }
111
+
90
112
  export function isLoopbackHost(host) {
91
113
  const normalized = String(host || "").toLowerCase();
92
114
  return normalized === "localhost" ||
package/src/server.js CHANGED
@@ -19,7 +19,7 @@ import { convertAgentArtifact } from "./lib/converters.js";
19
19
  import { createLineDiff } from "./lib/diff.js";
20
20
  import { EDITOR_CLIENT_PATH, VIEWER_CLIENT_PATH, editorClientFilePath, editorVendorPath, viewerClientFilePath } from "./lib/editor-assets.js";
21
21
  import { localeFromBodyOrUrl, localeFromUrl, localizedHref } from "./lib/i18n.js";
22
- import { requireToken, securityConfig, validateServerExposure } from "./lib/security.js";
22
+ import { exposureWarning, requireToken, securityConfig, validateServerExposure } from "./lib/security.js";
23
23
  import { writeServerState } from "./lib/server-state.js";
24
24
  import { generateToken } from "./lib/token.js";
25
25
  import {
@@ -78,6 +78,7 @@ export async function startServer(options = {}) {
78
78
  requestedPort,
79
79
  port: actualPort,
80
80
  portFallback: usedPortFallback,
81
+ securityWarning: exposureWarning({ host, config: security }),
81
82
  close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())))
82
83
  };
83
84
  }
@@ -674,12 +675,15 @@ async function runServerMain(options) {
674
675
  throw new Error("Use either --api-token or --generate-token, not both");
675
676
  }
676
677
  const generatedToken = options.generateToken ? generateToken(options) : null;
677
- const { url, store } = await startServer({
678
+ const { url, store, securityWarning } = await startServer({
678
679
  ...options,
679
680
  apiToken: generatedToken?.token || options.apiToken
680
681
  });
681
682
  process.stderr.write(`Artifacty listening on ${url}\n`);
682
683
  process.stderr.write(`Store: ${store.home}\n`);
684
+ if (securityWarning) {
685
+ process.stderr.write(`${securityWarning}\n`);
686
+ }
683
687
  if (generatedToken) {
684
688
  process.stderr.write(`API token: ${generatedToken.token}\n`);
685
689
  process.stderr.write(`HTTP header: ${generatedToken.header}\n`);