artifacty 0.10.1 → 0.10.3
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 +3 -3
- package/docs/artifact-schema-v1.md +10 -0
- package/docs/central-team-deployment-design.md +5 -4
- package/docs/integrations.md +21 -1
- package/docs/mcp-public-api.md +3 -2
- package/docs/threat-model.md +2 -2
- package/package.json +1 -1
- package/src/lib/i18n.js +4 -0
- package/src/lib/installer.js +109 -18
- package/src/lib/render.js +26 -2
- package/src/lib/storage.js +172 -12
- package/src/server.js +3 -2
package/README.md
CHANGED
|
@@ -314,7 +314,7 @@ The browser UI defaults to English. Add `?lang=ko` to any browser route to use K
|
|
|
314
314
|
|
|
315
315
|
Schema and storage:
|
|
316
316
|
|
|
317
|
-
- Metadata lives in SQLite with `schemaVersion: 1`, `artifactType`, and `archivedAt`.
|
|
317
|
+
- Metadata lives in SQLite with `schemaVersion: 1`, `artifactType`, `publisherId`, and `archivedAt`.
|
|
318
318
|
- Archive hides artifacts from default lists without deleting versions.
|
|
319
319
|
- Bundle artifacts store multiple files or base64 assets as portable JSON.
|
|
320
320
|
- Supported formats are `html`, `markdown`, `text`, `json`, `code`, `svg`, `mermaid`, `react`, `sarif`, `csv`, `image`, and `video`.
|
|
@@ -329,13 +329,13 @@ Schema and storage:
|
|
|
329
329
|
|
|
330
330
|
- The HTTP server binds to `127.0.0.1` by default.
|
|
331
331
|
- If `ARTIFACTY_API_TOKEN` is set, HTTP API routes require `Authorization: Bearer <token>` or `x-artifacty-token`; scripts should prefer headers over `?token=...` URLs.
|
|
332
|
-
- When users exist, personal API tokens issued from `/account` also authenticate HTTP API and MCP requests
|
|
332
|
+
- When users exist, personal API tokens issued from `/account` also authenticate HTTP API and MCP requests. Created artifacts record the token owner's email as `publisherId`, and audit logs record the same identity as `actor`.
|
|
333
333
|
- API token checks use timing-safe digest comparison.
|
|
334
334
|
- Binding outside localhost requires both `ARTIFACTY_SHARE_MODE=lan` or `team` and `ARTIFACTY_API_TOKEN`.
|
|
335
335
|
- 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).
|
|
336
336
|
- Non-local binding prints a startup warning because Artifacty does not terminate TLS.
|
|
337
337
|
- 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.
|
|
338
|
-
- Creates, updates, reads, imports, archives, and restores write audit events to SQLite.
|
|
338
|
+
- Creates, updates, reads, imports, archives, and restores write audit events to SQLite. Legacy artifacts without a stored publisher are best-effort backfilled from their first `create` or `import` audit actor.
|
|
339
339
|
- 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`.
|
|
340
340
|
- Mutating HTTP routes reject non-local browser origins.
|
|
341
341
|
- HTML artifacts render in a sandboxed iframe.
|
|
@@ -11,6 +11,9 @@ Artifacty schema v1 defines the stable envelope shared by HTTP, CLI, MCP, conver
|
|
|
11
11
|
"artifactType": "document",
|
|
12
12
|
"title": "Readable title",
|
|
13
13
|
"sourceAgent": "codex",
|
|
14
|
+
"publisherId": "user@example.com",
|
|
15
|
+
"publisherName": "User Name",
|
|
16
|
+
"publisherUserId": "user-record-id",
|
|
14
17
|
"tags": ["handoff"],
|
|
15
18
|
"createdAt": "2026-06-24T00:00:00.000Z",
|
|
16
19
|
"updatedAt": "2026-06-24T00:00:00.000Z",
|
|
@@ -41,6 +44,13 @@ Allowed `artifactType` values:
|
|
|
41
44
|
|
|
42
45
|
Unknown legacy or external types should be mapped to `unknown`, not rejected during conversion. Native create/update rejects unsupported explicit types.
|
|
43
46
|
|
|
47
|
+
`sourceAgent` identifies the tool or model surface that produced the artifact,
|
|
48
|
+
such as `codex` or `claude`. `publisherId` identifies who published it to the
|
|
49
|
+
Artifacty store. For authenticated central servers this is the personal token
|
|
50
|
+
owner's email; `publisherName` and `publisherUserId` are included when the
|
|
51
|
+
server can map the request to a local user record. Legacy artifacts are
|
|
52
|
+
best-effort backfilled from the first `create` or `import` audit actor.
|
|
53
|
+
|
|
44
54
|
## Version Record
|
|
45
55
|
|
|
46
56
|
Each version is immutable and points at one content file.
|
|
@@ -126,7 +126,8 @@ revoke personal API tokens from `/account`. CSV imports support
|
|
|
126
126
|
`email,name,role,password,password_reset_required`; blank passwords are
|
|
127
127
|
generated once and require a password change on first sign-in. Use personal
|
|
128
128
|
tokens for `artifacty install ... --api-token` so MCP and API audit logs record
|
|
129
|
-
the user's email as `actor
|
|
129
|
+
the user's email as `actor` and created artifacts record the same email as
|
|
130
|
+
`publisherId`.
|
|
130
131
|
|
|
131
132
|
Production-like internal deployments should run Artifacty behind a TLS reverse
|
|
132
133
|
proxy, keep `ARTIFACTY_ENABLE_REACT_RENDERER` disabled unless the team trusts
|
|
@@ -162,9 +163,9 @@ x-artifacty-actor: user@example.com
|
|
|
162
163
|
x-artifacty-host: IRAE-MACBOOK
|
|
163
164
|
```
|
|
164
165
|
|
|
165
|
-
The server records the authenticated user's email as the
|
|
166
|
-
personal token is used. The `x-artifacty-actor` header
|
|
167
|
-
fallback for the bootstrap/global token path.
|
|
166
|
+
The server records the authenticated user's email as the artifact `publisherId`
|
|
167
|
+
and audit `actor` when a personal token is used. The `x-artifacty-actor` header
|
|
168
|
+
remains a compatibility fallback for the bootstrap/global token path.
|
|
168
169
|
|
|
169
170
|
## Security Model
|
|
170
171
|
|
package/docs/integrations.md
CHANGED
|
@@ -130,9 +130,29 @@ their password before they can continue to `/account` and issue MCP/API tokens.
|
|
|
130
130
|
- `--api-token <token>` is written into the generated MCP environment for bridge mode. For central servers, prefer a personal token issued from `/account`.
|
|
131
131
|
- `--url <url>` remains a browser-link override and does not enable central MCP by itself.
|
|
132
132
|
- `--timeout <ms>` adjusts Codex `startup_timeout_sec` and Gemini `timeout`. It does not change Claude Code startup behavior; set `MCP_TIMEOUT` before launching Claude Code if you need a larger value there.
|
|
133
|
+
- Do not use `artifacty install all --config <path>`. Each MCP client uses a
|
|
134
|
+
different config file shape, so shared config overrides are rejected. Use
|
|
135
|
+
`artifacty install <agent> --config <path>` only when targeting one agent.
|
|
136
|
+
- JSON-based installers preserve unrelated servers and replace only the
|
|
137
|
+
`artifacty` entry. If `mcpServers` or `servers` already exists but is not a
|
|
138
|
+
JSON object, Artifacty stops with an error instead of rewriting the file.
|
|
133
139
|
- `check` starts the local MCP server and verifies required tools, resources, and prompts through MCP discovery methods.
|
|
134
140
|
- `doctor` combines MCP discovery with runtime, storage, server, and service diagnostics.
|
|
135
141
|
|
|
142
|
+
Codex troubleshooting:
|
|
143
|
+
|
|
144
|
+
- If Codex reports a duplicate `mcp_servers.artifacty.env` or `artifacty.env`
|
|
145
|
+
key, update Artifacty and rerun the Codex installer. Older installs could
|
|
146
|
+
leave a legacy `[mcp_servers.artifacty.env]` child table behind while adding a
|
|
147
|
+
new inline `env = { ... }` value.
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npm install -g artifacty@latest
|
|
151
|
+
artifacty install codex \
|
|
152
|
+
--mcp-url http://10.0.0.50:8787/mcp \
|
|
153
|
+
--api-token "$ARTIFACTY_PERSONAL_TOKEN"
|
|
154
|
+
```
|
|
155
|
+
|
|
136
156
|
## Client Compatibility Matrix
|
|
137
157
|
|
|
138
158
|
| Client | Config shape | Scope | Timeout behavior | Restart requirement | Notes |
|
|
@@ -333,7 +353,7 @@ tests.
|
|
|
333
353
|
- `GET /artifacts/:id/diff`: compare two versions.
|
|
334
354
|
- `GET /artifacts/:id/raw?version=n`: raw content.
|
|
335
355
|
|
|
336
|
-
When `ARTIFACTY_API_TOKEN` is configured, `/api/*` routes require either `Authorization: Bearer <token>` or `x-artifacty-token: <token>`. When users exist, personal tokens issued from `/account` also authenticate API and MCP requests
|
|
356
|
+
When `ARTIFACTY_API_TOKEN` is configured, `/api/*` routes require either `Authorization: Bearer <token>` or `x-artifacty-token: <token>`. When users exist, personal tokens issued from `/account` also authenticate API and MCP requests, set created artifacts' `publisherId` to the token owner's email, and map audit `actor` to the same identity. Browser forms can also carry `?token=<token>` in the URL, which is copied to hidden form fields for local team workflows.
|
|
337
357
|
|
|
338
358
|
Renderer notes:
|
|
339
359
|
|
package/docs/mcp-public-api.md
CHANGED
|
@@ -66,8 +66,9 @@ Prompts accept optional context arguments such as `artifactId`, `goal`, `scope`,
|
|
|
66
66
|
- Local stdio remains the default. Enable the central HTTP endpoint with
|
|
67
67
|
`artifacty serve --mcp-http` and install bridge mode with
|
|
68
68
|
`artifacty install <agent> --mcp-url http://host:8787/mcp --api-token <token>`.
|
|
69
|
-
- On central servers, use a personal token from `/account` so
|
|
70
|
-
the token owner's email as
|
|
69
|
+
- On central servers, use a personal token from `/account` so created artifacts
|
|
70
|
+
include the token owner's email as `publisherId` and audit logs record the
|
|
71
|
+
same identity as `actor`.
|
|
71
72
|
- Remote MCP currently uses bearer/header token auth. OAuth and per-user scoped
|
|
72
73
|
tokens are future hardening work.
|
|
73
74
|
- Binary media resources return stored base64 text through MCP; browser `/raw`
|
package/docs/threat-model.md
CHANGED
|
@@ -114,8 +114,8 @@ Controls:
|
|
|
114
114
|
- The HTTP MCP endpoint is disabled unless explicitly enabled.
|
|
115
115
|
- Remote MCP requests require the configured API token.
|
|
116
116
|
- Stdio bridge mode sends tokens in headers, not URLs.
|
|
117
|
-
- Personal tokens map requests to a server-side user record for
|
|
118
|
-
attribution.
|
|
117
|
+
- Personal tokens map requests to a server-side user record for artifact
|
|
118
|
+
`publisherId` and audit `actor` attribution.
|
|
119
119
|
- MCP writes go through the same secret scan and audit paths as CLI/HTTP writes.
|
|
120
120
|
- MCP resources are read-only.
|
|
121
121
|
|
package/package.json
CHANGED
package/src/lib/i18n.js
CHANGED
|
@@ -45,6 +45,8 @@ const TRANSLATIONS = {
|
|
|
45
45
|
"artifact.schema": "schema v{version}",
|
|
46
46
|
"artifact.archived": "archived {date}",
|
|
47
47
|
"artifact.bytes": "{size} bytes",
|
|
48
|
+
"artifact.publisher": "publisher {publisher}",
|
|
49
|
+
"artifact.publisherUnknown": "unknown publisher",
|
|
48
50
|
"artifact.archive": "Archive",
|
|
49
51
|
"artifact.restore": "Restore",
|
|
50
52
|
"diff.title": "{title} Diff",
|
|
@@ -103,6 +105,8 @@ const TRANSLATIONS = {
|
|
|
103
105
|
"artifact.schema": "스키마 v{version}",
|
|
104
106
|
"artifact.archived": "보관됨 {date}",
|
|
105
107
|
"artifact.bytes": "{size} 바이트",
|
|
108
|
+
"artifact.publisher": "게시자 {publisher}",
|
|
109
|
+
"artifact.publisherUnknown": "게시자 알 수 없음",
|
|
106
110
|
"artifact.archive": "보관",
|
|
107
111
|
"artifact.restore": "복원",
|
|
108
112
|
"diff.title": "{title} 비교",
|
package/src/lib/installer.js
CHANGED
|
@@ -14,6 +14,9 @@ export async function installAgent(agent, options = {}) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
if (normalized === "all") {
|
|
17
|
+
if (options.configPath) {
|
|
18
|
+
throw new Error("install all does not support --config because each MCP client uses a different config file. Run install per agent when overriding config paths.");
|
|
19
|
+
}
|
|
17
20
|
const results = [];
|
|
18
21
|
for (const target of INSTALL_TARGETS) {
|
|
19
22
|
results.push(await installAgent(target, options));
|
|
@@ -73,11 +76,12 @@ export function createMcpServerConfig(options = {}) {
|
|
|
73
76
|
export async function installClaude(options = {}) {
|
|
74
77
|
const projectDir = path.resolve(options.projectDir || process.cwd());
|
|
75
78
|
const targetPath = path.resolve(options.configPath || path.join(projectDir, ".mcp.json"));
|
|
76
|
-
const existing = await
|
|
79
|
+
const existing = await readJsonConfigFile(targetPath);
|
|
80
|
+
const mcpServers = jsonObjectSection(existing, "mcpServers", targetPath);
|
|
77
81
|
const next = {
|
|
78
82
|
...existing,
|
|
79
83
|
mcpServers: {
|
|
80
|
-
...
|
|
84
|
+
...mcpServers,
|
|
81
85
|
artifacty: createMcpServerConfig(options)
|
|
82
86
|
}
|
|
83
87
|
};
|
|
@@ -93,11 +97,12 @@ export async function installClaude(options = {}) {
|
|
|
93
97
|
export async function installGemini(options = {}) {
|
|
94
98
|
const projectDir = path.resolve(options.projectDir || process.cwd());
|
|
95
99
|
const targetPath = path.resolve(options.configPath || path.join(projectDir, ".gemini", "settings.json"));
|
|
96
|
-
const existing = await
|
|
100
|
+
const existing = await readJsonConfigFile(targetPath);
|
|
101
|
+
const mcpServers = jsonObjectSection(existing, "mcpServers", targetPath);
|
|
97
102
|
const next = {
|
|
98
103
|
...existing,
|
|
99
104
|
mcpServers: {
|
|
100
|
-
...
|
|
105
|
+
...mcpServers,
|
|
101
106
|
artifacty: {
|
|
102
107
|
...createMcpServerConfig(options),
|
|
103
108
|
timeout: normalizeTimeoutMs(options.timeout),
|
|
@@ -117,11 +122,12 @@ export async function installGemini(options = {}) {
|
|
|
117
122
|
export async function installCopilot(options = {}) {
|
|
118
123
|
const projectDir = path.resolve(options.projectDir || process.cwd());
|
|
119
124
|
const targetPath = path.resolve(options.configPath || path.join(projectDir, ".vscode", "mcp.json"));
|
|
120
|
-
const existing = await
|
|
125
|
+
const existing = await readJsonConfigFile(targetPath);
|
|
126
|
+
const servers = jsonObjectSection(existing, "servers", targetPath);
|
|
121
127
|
const next = {
|
|
122
128
|
...existing,
|
|
123
129
|
servers: {
|
|
124
|
-
...
|
|
130
|
+
...servers,
|
|
125
131
|
artifacty: {
|
|
126
132
|
type: "stdio",
|
|
127
133
|
...createMcpServerConfig(options)
|
|
@@ -140,11 +146,12 @@ export async function installCopilot(options = {}) {
|
|
|
140
146
|
export async function installCursor(options = {}) {
|
|
141
147
|
const projectDir = path.resolve(options.projectDir || process.cwd());
|
|
142
148
|
const targetPath = path.resolve(options.configPath || path.join(projectDir, ".cursor", "mcp.json"));
|
|
143
|
-
const existing = await
|
|
149
|
+
const existing = await readJsonConfigFile(targetPath);
|
|
150
|
+
const mcpServers = jsonObjectSection(existing, "mcpServers", targetPath);
|
|
144
151
|
const next = {
|
|
145
152
|
...existing,
|
|
146
153
|
mcpServers: {
|
|
147
|
-
...
|
|
154
|
+
...mcpServers,
|
|
148
155
|
artifacty: createMcpServerConfig(options)
|
|
149
156
|
}
|
|
150
157
|
};
|
|
@@ -190,11 +197,35 @@ export function codexTomlBlock(config, options = {}) {
|
|
|
190
197
|
}
|
|
191
198
|
|
|
192
199
|
export function replaceTomlBlock(existing, dottedName, block) {
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
200
|
+
const trimmedBlock = block.trimEnd();
|
|
201
|
+
const lines = existing.replace(/\r\n/g, "\n").split("\n");
|
|
202
|
+
const target = normalizeTomlDottedName(dottedName);
|
|
203
|
+
const start = lines.findIndex((line) => normalizeTomlDottedName(parseTomlTableHeader(line)) === target);
|
|
204
|
+
|
|
205
|
+
if (start !== -1) {
|
|
206
|
+
let end = lines.length;
|
|
207
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
208
|
+
const tableName = normalizeTomlDottedName(parseTomlTableHeader(lines[index]));
|
|
209
|
+
if (!tableName) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (tableName === target || tableName.startsWith(`${target}.`)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
end = index;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const prefix = lines.slice(0, start).join("\n").trimEnd();
|
|
220
|
+
const suffix = lines.slice(end).join("\n").trimStart();
|
|
221
|
+
return [
|
|
222
|
+
prefix,
|
|
223
|
+
trimmedBlock,
|
|
224
|
+
suffix
|
|
225
|
+
].filter(Boolean).join("\n").trimEnd() + "\n";
|
|
197
226
|
}
|
|
227
|
+
|
|
228
|
+
const trimmed = existing.trimEnd();
|
|
198
229
|
return `${trimmed}${trimmed ? "\n\n" : ""}${block}`;
|
|
199
230
|
}
|
|
200
231
|
|
|
@@ -216,20 +247,25 @@ async function writeInstallFile({ agent, path: targetPath, dryRun, content }) {
|
|
|
216
247
|
};
|
|
217
248
|
}
|
|
218
249
|
|
|
219
|
-
async function
|
|
250
|
+
async function readJsonConfigFile(filePath) {
|
|
220
251
|
if (!existsSync(filePath)) {
|
|
221
|
-
return
|
|
252
|
+
return {};
|
|
222
253
|
}
|
|
223
254
|
|
|
224
255
|
const raw = await readTextFile(filePath, "");
|
|
225
256
|
if (!raw.trim()) {
|
|
226
|
-
return
|
|
257
|
+
return {};
|
|
227
258
|
}
|
|
259
|
+
let parsed;
|
|
228
260
|
try {
|
|
229
|
-
|
|
261
|
+
parsed = JSON.parse(raw);
|
|
230
262
|
} catch (error) {
|
|
231
263
|
throw new Error(`Failed to parse ${filePath}: ${error.message}`);
|
|
232
264
|
}
|
|
265
|
+
if (!isPlainObject(parsed)) {
|
|
266
|
+
throw new Error(`Invalid MCP config at ${filePath}: expected a JSON object`);
|
|
267
|
+
}
|
|
268
|
+
return parsed;
|
|
233
269
|
}
|
|
234
270
|
|
|
235
271
|
async function readTextFile(filePath, fallback) {
|
|
@@ -247,6 +283,20 @@ function normalizeAgent(agent) {
|
|
|
247
283
|
return normalized;
|
|
248
284
|
}
|
|
249
285
|
|
|
286
|
+
function jsonObjectSection(config, key, filePath) {
|
|
287
|
+
if (config[key] === undefined) {
|
|
288
|
+
return {};
|
|
289
|
+
}
|
|
290
|
+
if (!isPlainObject(config[key])) {
|
|
291
|
+
throw new Error(`Invalid MCP config at ${filePath}: ${key} must be a JSON object`);
|
|
292
|
+
}
|
|
293
|
+
return config[key];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function isPlainObject(value) {
|
|
297
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
298
|
+
}
|
|
299
|
+
|
|
250
300
|
function normalizeTimeoutMs(value) {
|
|
251
301
|
const timeout = Number(value ?? DEFAULT_MCP_TIMEOUT_MS);
|
|
252
302
|
return Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_MCP_TIMEOUT_MS;
|
|
@@ -276,6 +326,47 @@ function quoteTomlString(value) {
|
|
|
276
326
|
return JSON.stringify(String(value));
|
|
277
327
|
}
|
|
278
328
|
|
|
279
|
-
function
|
|
280
|
-
|
|
329
|
+
function parseTomlTableHeader(line) {
|
|
330
|
+
const match = /^\s*\[([^\][\r\n]+)]\s*(?:#.*)?$/.exec(line);
|
|
331
|
+
return match ? match[1] : "";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function normalizeTomlDottedName(value) {
|
|
335
|
+
return splitTomlDottedName(value).join(".");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function splitTomlDottedName(value) {
|
|
339
|
+
const parts = [];
|
|
340
|
+
let current = "";
|
|
341
|
+
let quoted = false;
|
|
342
|
+
let quote = "";
|
|
343
|
+
let escaped = false;
|
|
344
|
+
for (const char of String(value || "").trim()) {
|
|
345
|
+
if (quoted) {
|
|
346
|
+
if (escaped) {
|
|
347
|
+
current += char;
|
|
348
|
+
escaped = false;
|
|
349
|
+
} else if (char === "\\" && quote === "\"") {
|
|
350
|
+
escaped = true;
|
|
351
|
+
} else if (char === quote) {
|
|
352
|
+
quoted = false;
|
|
353
|
+
} else {
|
|
354
|
+
current += char;
|
|
355
|
+
}
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (char === "\"" || char === "'") {
|
|
359
|
+
quoted = true;
|
|
360
|
+
quote = char;
|
|
361
|
+
} else if (char === ".") {
|
|
362
|
+
parts.push(current.trim());
|
|
363
|
+
current = "";
|
|
364
|
+
} else if (!/\s/.test(char)) {
|
|
365
|
+
current += char;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (current || parts.length > 0) {
|
|
369
|
+
parts.push(current.trim());
|
|
370
|
+
}
|
|
371
|
+
return parts.filter(Boolean);
|
|
281
372
|
}
|
package/src/lib/render.js
CHANGED
|
@@ -13,6 +13,7 @@ export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination,
|
|
|
13
13
|
.map((artifact) => {
|
|
14
14
|
const tags = artifact.tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("");
|
|
15
15
|
const status = artifact.archivedAt ? statusBadge("archived") : "";
|
|
16
|
+
const publisher = publisherDisplay(artifact, view);
|
|
16
17
|
const snippet = artifact.searchSnippet
|
|
17
18
|
? `<span class="row-snippet">${escapeHtml(artifact.searchSnippet)}</span>`
|
|
18
19
|
: "";
|
|
@@ -24,6 +25,7 @@ export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination,
|
|
|
24
25
|
${snippet}
|
|
25
26
|
</span>
|
|
26
27
|
<span>${escapeHtml(artifact.sourceAgent)}</span>
|
|
28
|
+
<span class="publisher-cell" title="${escapeAttribute(publisherTitle(artifact, view))}">${escapeHtml(publisher)}</span>
|
|
27
29
|
${typeBadge(artifact.artifactType || "document")}
|
|
28
30
|
${formatBadge(artifact.format || "text")}
|
|
29
31
|
<span>v${artifact.latestVersion}</span>
|
|
@@ -527,6 +529,7 @@ export function renderArtifactPage({ artifact, version, content, baseUrl, authTo
|
|
|
527
529
|
const rawUrl = `/artifacts/${encodeURIComponent(artifact.id)}/raw?version=${version.version}`;
|
|
528
530
|
const archiveAction = artifact.archivedAt ? "restore" : "archive";
|
|
529
531
|
const archiveLabel = artifact.archivedAt ? view.text("artifact.restore") : view.text("artifact.archive");
|
|
532
|
+
const publisher = publisherDisplay(artifact, view);
|
|
530
533
|
|
|
531
534
|
return pageShell({
|
|
532
535
|
title: artifact.title,
|
|
@@ -550,6 +553,7 @@ export function renderArtifactPage({ artifact, version, content, baseUrl, authTo
|
|
|
550
553
|
${typeBadge(artifact.artifactType || "document")}
|
|
551
554
|
<span>${view.text("artifact.schema", { version: artifact.schemaVersion || 1 })}</span>
|
|
552
555
|
${artifact.archivedAt ? statusBadge(view.text("artifact.archived", { date: artifact.archivedAt })) : ""}
|
|
556
|
+
<span title="${escapeAttribute(publisherTitle(artifact, view))}">${view.text("artifact.publisher", { publisher })}</span>
|
|
553
557
|
<span>${view.text("artifact.bytes", { size: version.sizeBytes })}</span>
|
|
554
558
|
<span>${escapeHtml(version.createdAt)}</span>
|
|
555
559
|
<span>${escapeHtml(version.sha256.slice(0, 12))}</span>
|
|
@@ -1295,8 +1299,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
1295
1299
|
}
|
|
1296
1300
|
.artifact-row {
|
|
1297
1301
|
display: grid;
|
|
1298
|
-
grid-template-columns: minmax(220px, 1fr)
|
|
1299
|
-
gap:
|
|
1302
|
+
grid-template-columns: minmax(220px, 1fr) 110px minmax(140px, 200px) 110px 84px 56px minmax(100px, 180px);
|
|
1303
|
+
gap: 12px;
|
|
1300
1304
|
align-items: center;
|
|
1301
1305
|
min-height: 64px;
|
|
1302
1306
|
padding: 14px 18px;
|
|
@@ -1343,6 +1347,13 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
1343
1347
|
font-size: 12.5px;
|
|
1344
1348
|
color: var(--muted);
|
|
1345
1349
|
}
|
|
1350
|
+
.artifact-row .publisher-cell {
|
|
1351
|
+
min-width: 0;
|
|
1352
|
+
overflow: hidden;
|
|
1353
|
+
text-overflow: ellipsis;
|
|
1354
|
+
white-space: nowrap;
|
|
1355
|
+
color: var(--text);
|
|
1356
|
+
}
|
|
1346
1357
|
.empty {
|
|
1347
1358
|
color: var(--muted);
|
|
1348
1359
|
font-size: 13px;
|
|
@@ -1828,6 +1839,19 @@ function formatBadge(value) {
|
|
|
1828
1839
|
return `<span class="badge f-${escapeAttribute(slug)}">${escapeHtml(format)}</span>`;
|
|
1829
1840
|
}
|
|
1830
1841
|
|
|
1842
|
+
function publisherDisplay(artifact, view) {
|
|
1843
|
+
return artifact.publisherId || artifact.publisherName || view.text("artifact.publisherUnknown");
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function publisherTitle(artifact, view) {
|
|
1847
|
+
const id = artifact.publisherId || "";
|
|
1848
|
+
const name = artifact.publisherName || "";
|
|
1849
|
+
if (id && name && id !== name) {
|
|
1850
|
+
return `${name} <${id}>`;
|
|
1851
|
+
}
|
|
1852
|
+
return id || name || view.text("artifact.publisherUnknown");
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1831
1855
|
function statusBadge(label) {
|
|
1832
1856
|
return `<span class="badge s-archived">${label}</span>`;
|
|
1833
1857
|
}
|
package/src/lib/storage.js
CHANGED
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { DatabaseSync } from "node:sqlite";
|
|
7
7
|
import { assertNoSecrets, securityConfig } from "./security.js";
|
|
8
8
|
|
|
9
|
-
export const STORE_VERSION =
|
|
9
|
+
export const STORE_VERSION = 4;
|
|
10
10
|
export const ARTIFACT_SCHEMA_VERSION = 1;
|
|
11
11
|
export const MAX_ARTIFACT_BYTES = 16 * 1024 * 1024;
|
|
12
12
|
export const USER_ROLES = ["admin", "user"];
|
|
@@ -141,6 +141,7 @@ export async function createArtifact(store = createStore(), input = {}) {
|
|
|
141
141
|
const now = new Date().toISOString();
|
|
142
142
|
const id = makeArtifactId(normalized.title);
|
|
143
143
|
const version = writeVersionFile(store, id, 1, normalized, now);
|
|
144
|
+
const publisher = publisherFromAudit(input.audit);
|
|
144
145
|
|
|
145
146
|
artifact = {
|
|
146
147
|
id,
|
|
@@ -148,6 +149,9 @@ export async function createArtifact(store = createStore(), input = {}) {
|
|
|
148
149
|
artifactType: normalized.artifactType,
|
|
149
150
|
schemaVersion: normalized.schemaVersion,
|
|
150
151
|
sourceAgent: normalized.sourceAgent,
|
|
152
|
+
publisherId: publisher.publisherId,
|
|
153
|
+
publisherName: publisher.publisherName,
|
|
154
|
+
publisherUserId: publisher.publisherUserId,
|
|
151
155
|
tags: normalized.tags,
|
|
152
156
|
createdAt: now,
|
|
153
157
|
updatedAt: now,
|
|
@@ -461,15 +465,17 @@ function listArtifactsPageWithSql(db, filters) {
|
|
|
461
465
|
LOWER(id) LIKE ? ESCAPE '\\' OR
|
|
462
466
|
LOWER(title) LIKE ? ESCAPE '\\' OR
|
|
463
467
|
LOWER(source_agent) LIKE ? ESCAPE '\\' OR
|
|
468
|
+
LOWER(COALESCE(publisher_id, '')) LIKE ? ESCAPE '\\' OR
|
|
469
|
+
LOWER(COALESCE(publisher_name, '')) LIKE ? ESCAPE '\\' OR
|
|
464
470
|
LOWER(tags_json) LIKE ? ESCAPE '\\'
|
|
465
471
|
)`);
|
|
466
|
-
params.push(like, like, like, like);
|
|
472
|
+
params.push(like, like, like, like, like, like);
|
|
467
473
|
}
|
|
468
474
|
|
|
469
475
|
const whereSql = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
470
476
|
const total = db.prepare(`SELECT COUNT(*) AS total FROM artifacts ${whereSql}`).get(...params).total;
|
|
471
477
|
const rows = db.prepare(`
|
|
472
|
-
SELECT id, title, artifact_type, schema_version, source_agent, tags_json, created_at, updated_at, latest_version, archived_at
|
|
478
|
+
SELECT id, title, artifact_type, schema_version, source_agent, publisher_id, publisher_name, publisher_user_id, tags_json, created_at, updated_at, latest_version, archived_at
|
|
473
479
|
FROM artifacts
|
|
474
480
|
${whereSql}
|
|
475
481
|
ORDER BY updated_at DESC, created_at DESC
|
|
@@ -503,13 +509,16 @@ function listArtifactsPageWithFts(db, filters) {
|
|
|
503
509
|
a.artifact_type,
|
|
504
510
|
a.schema_version,
|
|
505
511
|
a.source_agent,
|
|
512
|
+
a.publisher_id,
|
|
513
|
+
a.publisher_name,
|
|
514
|
+
a.publisher_user_id,
|
|
506
515
|
a.tags_json,
|
|
507
516
|
a.created_at,
|
|
508
517
|
a.updated_at,
|
|
509
518
|
a.latest_version,
|
|
510
519
|
a.archived_at,
|
|
511
520
|
bm25(artifact_search) AS search_rank,
|
|
512
|
-
snippet(artifact_search,
|
|
521
|
+
snippet(artifact_search, -1, '', '', '...', 24) AS search_snippet
|
|
513
522
|
FROM artifact_search
|
|
514
523
|
JOIN artifacts a ON a.id = artifact_search.artifact_id
|
|
515
524
|
${whereSql}
|
|
@@ -572,6 +581,9 @@ function artifactFromRow(db, row) {
|
|
|
572
581
|
artifactType: row.artifact_type,
|
|
573
582
|
schemaVersion: row.schema_version,
|
|
574
583
|
sourceAgent: row.source_agent,
|
|
584
|
+
publisherId: row.publisher_id || null,
|
|
585
|
+
publisherName: row.publisher_name || null,
|
|
586
|
+
publisherUserId: row.publisher_user_id || null,
|
|
575
587
|
tags: parseJson(row.tags_json, []),
|
|
576
588
|
createdAt: row.created_at,
|
|
577
589
|
updatedAt: row.updated_at,
|
|
@@ -589,6 +601,15 @@ function normalizeWhitespace(value) {
|
|
|
589
601
|
return normalizeOptionalString(value).replace(/\s+/g, " ");
|
|
590
602
|
}
|
|
591
603
|
|
|
604
|
+
function publisherFromAudit(audit = {}) {
|
|
605
|
+
const publisherId = normalizeOptionalString(audit.publisherId || audit.actor) || null;
|
|
606
|
+
return {
|
|
607
|
+
publisherId,
|
|
608
|
+
publisherName: normalizeOptionalString(audit.publisherName || audit.userName) || null,
|
|
609
|
+
publisherUserId: normalizeOptionalString(audit.publisherUserId || audit.userId) || null
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
592
613
|
function toFtsQuery(value) {
|
|
593
614
|
const tokens = normalizeOptionalString(value).match(/[\p{L}\p{N}_-]+/gu) || [];
|
|
594
615
|
return tokens
|
|
@@ -986,6 +1007,9 @@ export function toArtifactSummary(artifact) {
|
|
|
986
1007
|
artifactType: artifact.artifactType,
|
|
987
1008
|
schemaVersion: artifact.schemaVersion,
|
|
988
1009
|
sourceAgent: artifact.sourceAgent,
|
|
1010
|
+
publisherId: artifact.publisherId || null,
|
|
1011
|
+
publisherName: artifact.publisherName || null,
|
|
1012
|
+
publisherUserId: artifact.publisherUserId || null,
|
|
989
1013
|
tags: artifact.tags,
|
|
990
1014
|
createdAt: artifact.createdAt,
|
|
991
1015
|
updatedAt: artifact.updatedAt,
|
|
@@ -1075,6 +1099,9 @@ function initializeSchema(db) {
|
|
|
1075
1099
|
artifact_type TEXT NOT NULL DEFAULT 'document',
|
|
1076
1100
|
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
1077
1101
|
source_agent TEXT NOT NULL,
|
|
1102
|
+
publisher_id TEXT,
|
|
1103
|
+
publisher_name TEXT,
|
|
1104
|
+
publisher_user_id TEXT,
|
|
1078
1105
|
tags_json TEXT NOT NULL,
|
|
1079
1106
|
created_at TEXT NOT NULL,
|
|
1080
1107
|
updated_at TEXT NOT NULL,
|
|
@@ -1152,11 +1179,118 @@ function initializeSchema(db) {
|
|
|
1152
1179
|
ensureColumn(db, "artifacts", "artifact_type", "TEXT NOT NULL DEFAULT 'document'");
|
|
1153
1180
|
ensureColumn(db, "artifacts", "schema_version", "INTEGER NOT NULL DEFAULT 1");
|
|
1154
1181
|
ensureColumn(db, "artifacts", "archived_at", "TEXT");
|
|
1182
|
+
ensureColumn(db, "artifacts", "publisher_id", "TEXT");
|
|
1183
|
+
ensureColumn(db, "artifacts", "publisher_name", "TEXT");
|
|
1184
|
+
ensureColumn(db, "artifacts", "publisher_user_id", "TEXT");
|
|
1155
1185
|
ensureColumn(db, "users", "password_reset_required", "INTEGER NOT NULL DEFAULT 0");
|
|
1186
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_artifacts_publisher_id ON artifacts(publisher_id)");
|
|
1187
|
+
backfillArtifactPublishers(db);
|
|
1156
1188
|
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('store_version', ?)").run(String(STORE_VERSION));
|
|
1157
1189
|
ensureSearchTable(db);
|
|
1158
1190
|
}
|
|
1159
1191
|
|
|
1192
|
+
function backfillArtifactPublishers(db) {
|
|
1193
|
+
const rows = db.prepare(`
|
|
1194
|
+
SELECT
|
|
1195
|
+
id,
|
|
1196
|
+
publisher_id,
|
|
1197
|
+
(
|
|
1198
|
+
SELECT actor
|
|
1199
|
+
FROM audit_log
|
|
1200
|
+
WHERE audit_log.artifact_id = artifacts.id
|
|
1201
|
+
AND action IN ('create', 'import')
|
|
1202
|
+
AND actor IS NOT NULL
|
|
1203
|
+
AND TRIM(actor) != ''
|
|
1204
|
+
ORDER BY created_at ASC
|
|
1205
|
+
LIMIT 1
|
|
1206
|
+
) AS actor
|
|
1207
|
+
FROM artifacts
|
|
1208
|
+
WHERE publisher_id IS NULL
|
|
1209
|
+
OR TRIM(publisher_id) = ''
|
|
1210
|
+
OR publisher_id LIKE 'curl/%'
|
|
1211
|
+
OR publisher_id LIKE 'Wget/%'
|
|
1212
|
+
OR publisher_id LIKE 'HTTPie/%'
|
|
1213
|
+
OR publisher_id LIKE 'PostmanRuntime/%'
|
|
1214
|
+
OR publisher_id LIKE 'undici%'
|
|
1215
|
+
OR publisher_id LIKE 'node-fetch%'
|
|
1216
|
+
OR publisher_id LIKE 'Mozilla/%'
|
|
1217
|
+
`).all();
|
|
1218
|
+
|
|
1219
|
+
if (rows.length === 0) {
|
|
1220
|
+
enrichPublisherUsers(db);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const update = db.prepare(`
|
|
1225
|
+
UPDATE artifacts
|
|
1226
|
+
SET publisher_id = ?, publisher_name = ?, publisher_user_id = ?
|
|
1227
|
+
WHERE id = ?
|
|
1228
|
+
`);
|
|
1229
|
+
|
|
1230
|
+
transaction(db, () => {
|
|
1231
|
+
for (const row of rows) {
|
|
1232
|
+
const existingPublisherId = normalizeOptionalString(row.publisher_id);
|
|
1233
|
+
if (existingPublisherId && !isLikelyUserAgentActor(existingPublisherId)) {
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
const actor = normalizeOptionalString(row.actor);
|
|
1237
|
+
const publisher = publisherForActor(db, actor);
|
|
1238
|
+
if (!publisher.publisherId && !existingPublisherId) {
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
update.run(publisher.publisherId, publisher.publisherName, publisher.publisherUserId, row.id);
|
|
1242
|
+
}
|
|
1243
|
+
enrichPublisherUsers(db);
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function enrichPublisherUsers(db) {
|
|
1248
|
+
db.prepare(`
|
|
1249
|
+
UPDATE artifacts
|
|
1250
|
+
SET
|
|
1251
|
+
publisher_user_id = COALESCE(
|
|
1252
|
+
publisher_user_id,
|
|
1253
|
+
(SELECT id FROM users WHERE LOWER(users.email) = LOWER(artifacts.publisher_id) LIMIT 1)
|
|
1254
|
+
),
|
|
1255
|
+
publisher_name = COALESCE(
|
|
1256
|
+
NULLIF(publisher_name, ''),
|
|
1257
|
+
(SELECT name FROM users WHERE LOWER(users.email) = LOWER(artifacts.publisher_id) LIMIT 1)
|
|
1258
|
+
)
|
|
1259
|
+
WHERE publisher_id IS NOT NULL AND TRIM(publisher_id) != ''
|
|
1260
|
+
`).run();
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function publisherForActor(db, actor) {
|
|
1264
|
+
const publisherId = normalizeOptionalString(actor);
|
|
1265
|
+
if (!publisherId || isLikelyUserAgentActor(publisherId)) {
|
|
1266
|
+
return {
|
|
1267
|
+
publisherId: null,
|
|
1268
|
+
publisherName: null,
|
|
1269
|
+
publisherUserId: null
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const user = db.prepare("SELECT id, email, name FROM users WHERE LOWER(email) = LOWER(?)").get(publisherId);
|
|
1274
|
+
return {
|
|
1275
|
+
publisherId: user?.email || publisherId,
|
|
1276
|
+
publisherName: user?.name || null,
|
|
1277
|
+
publisherUserId: user?.id || null
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function isLikelyUserAgentActor(value) {
|
|
1282
|
+
if (/^(curl|Wget|HTTPie|PostmanRuntime)\//i.test(value)) {
|
|
1283
|
+
return true;
|
|
1284
|
+
}
|
|
1285
|
+
if (/^(undici|node-fetch)(\/|$)/i.test(value)) {
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
if (value.length < 24) {
|
|
1289
|
+
return false;
|
|
1290
|
+
}
|
|
1291
|
+
return /\b(Mozilla|AppleWebKit|Chrome|Safari|Firefox|Edg)\b/i.test(value);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1160
1294
|
function migrateJsonIndex(db, store) {
|
|
1161
1295
|
if (!existsSync(store.indexPath)) {
|
|
1162
1296
|
return;
|
|
@@ -1192,11 +1326,17 @@ function migrateJsonIndex(db, store) {
|
|
|
1192
1326
|
|
|
1193
1327
|
function ensureSearchTable(db) {
|
|
1194
1328
|
try {
|
|
1329
|
+
const existing = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'artifact_search'").get();
|
|
1330
|
+
if (existing && !searchTableHasPublisherColumns(db)) {
|
|
1331
|
+
db.exec("DROP TABLE artifact_search");
|
|
1332
|
+
}
|
|
1195
1333
|
db.exec(`
|
|
1196
1334
|
CREATE VIRTUAL TABLE IF NOT EXISTS artifact_search USING fts5(
|
|
1197
1335
|
artifact_id UNINDEXED,
|
|
1198
1336
|
title,
|
|
1199
1337
|
source_agent,
|
|
1338
|
+
publisher_id,
|
|
1339
|
+
publisher_name,
|
|
1200
1340
|
artifact_type,
|
|
1201
1341
|
tags,
|
|
1202
1342
|
format,
|
|
@@ -1212,9 +1352,14 @@ function ensureSearchTable(db) {
|
|
|
1212
1352
|
}
|
|
1213
1353
|
}
|
|
1214
1354
|
|
|
1355
|
+
function searchTableHasPublisherColumns(db) {
|
|
1356
|
+
const columns = db.prepare("PRAGMA table_info(artifact_search)").all().map((row) => row.name);
|
|
1357
|
+
return columns.includes("publisher_id") && columns.includes("publisher_name");
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1215
1360
|
function searchIndexAvailable(db) {
|
|
1216
1361
|
const row = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'artifact_search'").get();
|
|
1217
|
-
return Boolean(row) || ensureSearchTable(db);
|
|
1362
|
+
return (Boolean(row) && searchTableHasPublisherColumns(db)) || ensureSearchTable(db);
|
|
1218
1363
|
}
|
|
1219
1364
|
|
|
1220
1365
|
function syncSearchIndexIfEmpty(db, store) {
|
|
@@ -1280,17 +1425,23 @@ function upsertSearchIndex(db, artifact, version, content) {
|
|
|
1280
1425
|
db.prepare("DELETE FROM artifact_search WHERE artifact_id = ?").run(artifact.id);
|
|
1281
1426
|
db.prepare(`
|
|
1282
1427
|
INSERT INTO artifact_search (
|
|
1283
|
-
artifact_id, title, source_agent, artifact_type, tags, format, metadata, content
|
|
1428
|
+
artifact_id, title, source_agent, publisher_id, publisher_name, artifact_type, tags, format, metadata, content
|
|
1284
1429
|
)
|
|
1285
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1430
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1286
1431
|
`).run(
|
|
1287
1432
|
artifact.id,
|
|
1288
1433
|
artifact.title,
|
|
1289
1434
|
artifact.sourceAgent,
|
|
1435
|
+
artifact.publisherId || "",
|
|
1436
|
+
artifact.publisherName || "",
|
|
1290
1437
|
artifact.artifactType,
|
|
1291
1438
|
artifact.tags.join(" "),
|
|
1292
1439
|
version.format,
|
|
1293
|
-
metadataSearchText(
|
|
1440
|
+
metadataSearchText({
|
|
1441
|
+
...(version.metadata || {}),
|
|
1442
|
+
publisherId: artifact.publisherId || undefined,
|
|
1443
|
+
publisherName: artifact.publisherName || undefined
|
|
1444
|
+
}),
|
|
1294
1445
|
content
|
|
1295
1446
|
);
|
|
1296
1447
|
}
|
|
@@ -1321,7 +1472,7 @@ function listStoreFiles(root) {
|
|
|
1321
1472
|
|
|
1322
1473
|
function loadArtifacts(db) {
|
|
1323
1474
|
const rows = db.prepare(`
|
|
1324
|
-
SELECT id, title, artifact_type, schema_version, source_agent, tags_json, created_at, updated_at, latest_version, archived_at
|
|
1475
|
+
SELECT id, title, artifact_type, schema_version, source_agent, publisher_id, publisher_name, publisher_user_id, tags_json, created_at, updated_at, latest_version, archived_at
|
|
1325
1476
|
FROM artifacts
|
|
1326
1477
|
ORDER BY updated_at DESC, created_at DESC
|
|
1327
1478
|
`).all();
|
|
@@ -1332,6 +1483,9 @@ function loadArtifacts(db) {
|
|
|
1332
1483
|
artifactType: row.artifact_type,
|
|
1333
1484
|
schemaVersion: row.schema_version,
|
|
1334
1485
|
sourceAgent: row.source_agent,
|
|
1486
|
+
publisherId: row.publisher_id || null,
|
|
1487
|
+
publisherName: row.publisher_name || null,
|
|
1488
|
+
publisherUserId: row.publisher_user_id || null,
|
|
1335
1489
|
tags: parseJson(row.tags_json, []),
|
|
1336
1490
|
createdAt: row.created_at,
|
|
1337
1491
|
updatedAt: row.updated_at,
|
|
@@ -1352,7 +1506,7 @@ function loadVersions(db, artifactId) {
|
|
|
1352
1506
|
|
|
1353
1507
|
function findArtifactById(db, id) {
|
|
1354
1508
|
const row = db.prepare(`
|
|
1355
|
-
SELECT id, title, artifact_type, schema_version, source_agent, tags_json, created_at, updated_at, latest_version, archived_at
|
|
1509
|
+
SELECT id, title, artifact_type, schema_version, source_agent, publisher_id, publisher_name, publisher_user_id, tags_json, created_at, updated_at, latest_version, archived_at
|
|
1356
1510
|
FROM artifacts
|
|
1357
1511
|
WHERE id = ?
|
|
1358
1512
|
`).get(id);
|
|
@@ -1370,6 +1524,9 @@ function findArtifactById(db, id) {
|
|
|
1370
1524
|
artifactType: row.artifact_type,
|
|
1371
1525
|
schemaVersion: row.schema_version,
|
|
1372
1526
|
sourceAgent: row.source_agent,
|
|
1527
|
+
publisherId: row.publisher_id || null,
|
|
1528
|
+
publisherName: row.publisher_name || null,
|
|
1529
|
+
publisherUserId: row.publisher_user_id || null,
|
|
1373
1530
|
tags: parseJson(row.tags_json, []),
|
|
1374
1531
|
createdAt: row.created_at,
|
|
1375
1532
|
updatedAt: row.updated_at,
|
|
@@ -1382,15 +1539,18 @@ function findArtifactById(db, id) {
|
|
|
1382
1539
|
function insertArtifactRecord(db, artifact) {
|
|
1383
1540
|
db.prepare(`
|
|
1384
1541
|
INSERT INTO artifacts (
|
|
1385
|
-
id, title, artifact_type, schema_version, source_agent, tags_json, created_at, updated_at, latest_version, archived_at
|
|
1542
|
+
id, title, artifact_type, schema_version, source_agent, publisher_id, publisher_name, publisher_user_id, tags_json, created_at, updated_at, latest_version, archived_at
|
|
1386
1543
|
)
|
|
1387
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1544
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1388
1545
|
`).run(
|
|
1389
1546
|
artifact.id,
|
|
1390
1547
|
artifact.title,
|
|
1391
1548
|
normalizeArtifactType(artifact.artifactType || artifact.artifact_type || "document"),
|
|
1392
1549
|
normalizeSchemaVersion(artifact.schemaVersion || artifact.schema_version),
|
|
1393
1550
|
artifact.sourceAgent || artifact.source_agent || "unknown",
|
|
1551
|
+
normalizeOptionalString(artifact.publisherId || artifact.publisher_id) || null,
|
|
1552
|
+
normalizeOptionalString(artifact.publisherName || artifact.publisher_name) || null,
|
|
1553
|
+
normalizeOptionalString(artifact.publisherUserId || artifact.publisher_user_id) || null,
|
|
1394
1554
|
JSON.stringify(artifact.tags || []),
|
|
1395
1555
|
artifact.createdAt || artifact.created_at,
|
|
1396
1556
|
artifact.updatedAt || artifact.updated_at,
|
package/src/server.js
CHANGED
|
@@ -967,8 +967,9 @@ function auditContext(request, surface) {
|
|
|
967
967
|
const auth = request.artifactyAuth;
|
|
968
968
|
return {
|
|
969
969
|
surface,
|
|
970
|
-
actor: auth?.actor || request.headers["x-artifacty-actor"] ||
|
|
970
|
+
actor: auth?.actor || request.headers["x-artifacty-actor"] || "unknown",
|
|
971
971
|
userId: auth?.user?.id || null,
|
|
972
|
+
publisherName: auth?.user?.name || null,
|
|
972
973
|
tokenId: auth?.tokenId || null
|
|
973
974
|
};
|
|
974
975
|
}
|
|
@@ -992,7 +993,7 @@ async function requireRequestAuth({ store, request, url, body = {}, config }) {
|
|
|
992
993
|
if (!authRequired) {
|
|
993
994
|
return {
|
|
994
995
|
type: "anonymous",
|
|
995
|
-
actor: request.headers["x-artifacty-actor"] ||
|
|
996
|
+
actor: request.headers["x-artifacty-actor"] || "anonymous"
|
|
996
997
|
};
|
|
997
998
|
}
|
|
998
999
|
|