artifacty 0.7.0 → 0.9.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 +21 -0
- package/docs/central-team-deployment-design.md +208 -0
- package/docs/integrations.md +32 -1
- package/docs/mcp-public-api.md +13 -4
- package/docs/network-sharing.md +13 -0
- package/docs/release-checklist.md +7 -0
- package/docs/threat-model.md +20 -6
- package/package.json +2 -1
- package/src/cli.js +37 -6
- package/src/lib/background.js +3 -0
- package/src/lib/doctor.js +214 -0
- package/src/lib/installer.js +31 -0
- package/src/lib/render.js +250 -1
- package/src/lib/security.js +4 -0
- package/src/lib/service.js +4 -0
- package/src/lib/storage.js +332 -1
- package/src/mcp-server.js +225 -81
- package/src/server.js +317 -13
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { arch, platform } from "node:os";
|
|
4
|
+
import { backgroundStatus } from "./background.js";
|
|
5
|
+
import { checkMcpTools } from "./check.js";
|
|
6
|
+
import { securityConfig, validateServerExposure, exposureWarning } from "./security.js";
|
|
7
|
+
import { serviceCommand } from "./service.js";
|
|
8
|
+
import { checkStoreIntegrity, createStore } from "./storage.js";
|
|
9
|
+
|
|
10
|
+
const MIN_NODE_VERSION = "22.5.0";
|
|
11
|
+
|
|
12
|
+
export async function runDoctor(options = {}) {
|
|
13
|
+
const packageRoot = path.resolve(options.packageRoot || process.cwd());
|
|
14
|
+
const store = createStore({ home: options.home });
|
|
15
|
+
const checks = [];
|
|
16
|
+
|
|
17
|
+
await collect(checks, "runtime", () => runtimeCheck(packageRoot));
|
|
18
|
+
await collect(checks, "security", () => securityCheck(options));
|
|
19
|
+
await collect(checks, "storage", () => storageCheck(store));
|
|
20
|
+
await collect(checks, "server", () => serverCheck(store));
|
|
21
|
+
await collect(checks, "service", () => serviceCheck(options));
|
|
22
|
+
if (options.skipMcp) {
|
|
23
|
+
checks.push({
|
|
24
|
+
name: "mcp",
|
|
25
|
+
status: "skip",
|
|
26
|
+
message: "MCP discovery skipped by --skip-mcp"
|
|
27
|
+
});
|
|
28
|
+
} else {
|
|
29
|
+
await collect(checks, "mcp", () => mcpCheck(packageRoot, options));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const failures = checks.filter((check) => check.status === "fail");
|
|
33
|
+
const warnings = checks.filter((check) => check.status === "warn");
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
ok: failures.length === 0,
|
|
37
|
+
checkedAt: new Date().toISOString(),
|
|
38
|
+
version: await packageVersion(packageRoot),
|
|
39
|
+
home: store.home,
|
|
40
|
+
platform: process.platform,
|
|
41
|
+
checks,
|
|
42
|
+
failures: failures.map(({ name, message }) => ({ name, message })),
|
|
43
|
+
warnings: warnings.map(({ name, message }) => ({ name, message }))
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function collect(checks, name, fn) {
|
|
48
|
+
try {
|
|
49
|
+
checks.push({ name, ...(await fn()) });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
checks.push({
|
|
52
|
+
name,
|
|
53
|
+
status: "fail",
|
|
54
|
+
message: error.message,
|
|
55
|
+
error: {
|
|
56
|
+
name: error.name,
|
|
57
|
+
code: error.code
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function runtimeCheck(packageRoot) {
|
|
64
|
+
const version = await packageVersion(packageRoot);
|
|
65
|
+
const supported = compareVersions(process.versions.node, MIN_NODE_VERSION) >= 0;
|
|
66
|
+
return {
|
|
67
|
+
status: supported ? "pass" : "fail",
|
|
68
|
+
message: supported
|
|
69
|
+
? `Node ${process.versions.node} satisfies >=${MIN_NODE_VERSION}`
|
|
70
|
+
: `Node ${process.versions.node} is below required >=${MIN_NODE_VERSION}`,
|
|
71
|
+
data: {
|
|
72
|
+
artifactyVersion: version,
|
|
73
|
+
nodeVersion: process.versions.node,
|
|
74
|
+
requiredNodeVersion: `>=${MIN_NODE_VERSION}`,
|
|
75
|
+
platform: platform(),
|
|
76
|
+
arch: arch()
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function securityCheck(options = {}) {
|
|
82
|
+
const host = options.host || process.env.ARTIFACTY_HOST || "127.0.0.1";
|
|
83
|
+
const config = securityConfig(options);
|
|
84
|
+
validateServerExposure({ host, config });
|
|
85
|
+
const warning = exposureWarning({ host, config });
|
|
86
|
+
if (warning) {
|
|
87
|
+
return {
|
|
88
|
+
status: "warn",
|
|
89
|
+
message: warning,
|
|
90
|
+
data: {
|
|
91
|
+
host,
|
|
92
|
+
shareMode: config.shareMode,
|
|
93
|
+
hasApiToken: Boolean(config.apiToken)
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
status: "pass",
|
|
99
|
+
message: "Server exposure settings are local-first",
|
|
100
|
+
data: {
|
|
101
|
+
host,
|
|
102
|
+
shareMode: config.shareMode,
|
|
103
|
+
hasApiToken: Boolean(config.apiToken)
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function storageCheck(store) {
|
|
109
|
+
const integrity = await checkStoreIntegrity(store);
|
|
110
|
+
return {
|
|
111
|
+
status: integrity.ok ? "pass" : "fail",
|
|
112
|
+
message: integrity.ok
|
|
113
|
+
? `Store is consistent with ${integrity.artifactCount} artifacts and ${integrity.versionCount} versions`
|
|
114
|
+
: "Store integrity check found missing, changed, orphaned, or inconsistent files",
|
|
115
|
+
data: integrity
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function serverCheck(store) {
|
|
120
|
+
const status = await backgroundStatus({ home: store.home });
|
|
121
|
+
if (status.running) {
|
|
122
|
+
return {
|
|
123
|
+
status: "pass",
|
|
124
|
+
message: `Managed server is healthy at ${status.url}`,
|
|
125
|
+
data: status
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (status.processRunning || status.pidFileExists) {
|
|
129
|
+
return {
|
|
130
|
+
status: "fail",
|
|
131
|
+
message: "Recorded server process or pid file exists but health check is not passing",
|
|
132
|
+
data: status
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
status: "warn",
|
|
137
|
+
message: "No managed Artifacty server is currently running",
|
|
138
|
+
data: status
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function serviceCheck(options = {}) {
|
|
143
|
+
const definitions = [];
|
|
144
|
+
for (const action of ["plist", "unit", "task"]) {
|
|
145
|
+
const result = await serviceCommand(action, {
|
|
146
|
+
projectDir: options.packageRoot || process.cwd(),
|
|
147
|
+
serverPath: options.serverPath,
|
|
148
|
+
host: options.host,
|
|
149
|
+
port: options.port,
|
|
150
|
+
home: options.home,
|
|
151
|
+
apiToken: options.apiToken,
|
|
152
|
+
shareMode: options.shareMode,
|
|
153
|
+
allowSecrets: options.allowSecrets,
|
|
154
|
+
dryRun: true
|
|
155
|
+
});
|
|
156
|
+
definitions.push({
|
|
157
|
+
action,
|
|
158
|
+
platform: result.platform,
|
|
159
|
+
path: result.path,
|
|
160
|
+
contentBytes: Buffer.byteLength(result.content || "", "utf8")
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
status: "pass",
|
|
165
|
+
message: "Service definitions render for macOS, Linux, and Windows",
|
|
166
|
+
data: { definitions }
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function mcpCheck(packageRoot, options = {}) {
|
|
171
|
+
const result = await checkMcpTools({
|
|
172
|
+
projectDir: packageRoot,
|
|
173
|
+
serverPath: options.serverPath,
|
|
174
|
+
url: options.url,
|
|
175
|
+
home: options.home,
|
|
176
|
+
timeout: options.timeout
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
status: result.ok ? "pass" : "fail",
|
|
180
|
+
message: result.ok
|
|
181
|
+
? `MCP discovery found ${result.toolCount} tools, ${result.resourceCount} resources, and ${result.promptCount} prompts`
|
|
182
|
+
: "MCP discovery is missing required tools, resources, or prompts",
|
|
183
|
+
data: result
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function packageVersion(packageRoot) {
|
|
188
|
+
const file = path.join(packageRoot, "package.json");
|
|
189
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
190
|
+
return parsed.version;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function compareVersions(left, right) {
|
|
194
|
+
const leftParts = versionParts(left);
|
|
195
|
+
const rightParts = versionParts(right);
|
|
196
|
+
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
|
|
197
|
+
const leftPart = leftParts[index] || 0;
|
|
198
|
+
const rightPart = rightParts[index] || 0;
|
|
199
|
+
if (leftPart > rightPart) {
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
202
|
+
if (leftPart < rightPart) {
|
|
203
|
+
return -1;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function versionParts(value) {
|
|
210
|
+
return String(value)
|
|
211
|
+
.split(".")
|
|
212
|
+
.map((part) => Number.parseInt(part, 10))
|
|
213
|
+
.filter((part) => Number.isFinite(part));
|
|
214
|
+
}
|
package/src/lib/installer.js
CHANGED
|
@@ -48,6 +48,17 @@ export function createMcpServerConfig(options = {}) {
|
|
|
48
48
|
env.ARTIFACTY_URL = options.url || process.env.ARTIFACTY_URL;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
if (options.mcpUrl || process.env.ARTIFACTY_MCP_URL) {
|
|
52
|
+
env.ARTIFACTY_MCP_MODE = normalizeMcpMode(options.transport || "bridge");
|
|
53
|
+
env.ARTIFACTY_MCP_URL = normalizeMcpEndpoint(options.mcpUrl || process.env.ARTIFACTY_MCP_URL);
|
|
54
|
+
} else if (options.transport || process.env.ARTIFACTY_MCP_MODE) {
|
|
55
|
+
env.ARTIFACTY_MCP_MODE = normalizeMcpMode(options.transport || process.env.ARTIFACTY_MCP_MODE);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (options.apiToken || process.env.ARTIFACTY_API_TOKEN) {
|
|
59
|
+
env.ARTIFACTY_API_TOKEN = options.apiToken || process.env.ARTIFACTY_API_TOKEN;
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
if (options.home || process.env.ARTIFACTY_HOME) {
|
|
52
63
|
env.ARTIFACTY_HOME = path.resolve(options.home || process.env.ARTIFACTY_HOME);
|
|
53
64
|
}
|
|
@@ -241,6 +252,26 @@ function normalizeTimeoutMs(value) {
|
|
|
241
252
|
return Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_MCP_TIMEOUT_MS;
|
|
242
253
|
}
|
|
243
254
|
|
|
255
|
+
function normalizeMcpMode(value) {
|
|
256
|
+
const mode = String(value || "bridge").trim().toLowerCase();
|
|
257
|
+
if (mode === "remote") {
|
|
258
|
+
return "bridge";
|
|
259
|
+
}
|
|
260
|
+
if (!["local", "bridge"].includes(mode)) {
|
|
261
|
+
throw new Error("MCP transport must be local or bridge");
|
|
262
|
+
}
|
|
263
|
+
return mode;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function normalizeMcpEndpoint(value) {
|
|
267
|
+
const parsed = new URL(value);
|
|
268
|
+
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
269
|
+
parsed.pathname = pathname || "/mcp";
|
|
270
|
+
parsed.search = "";
|
|
271
|
+
parsed.hash = "";
|
|
272
|
+
return parsed.toString();
|
|
273
|
+
}
|
|
274
|
+
|
|
244
275
|
function quoteTomlString(value) {
|
|
245
276
|
return JSON.stringify(String(value));
|
|
246
277
|
}
|
package/src/lib/render.js
CHANGED
|
@@ -2,7 +2,7 @@ import { EDITOR_CLIENT_PATH, VIEWER_CLIENT_PATH, editorImportMapJson } from "./e
|
|
|
2
2
|
import { createI18n, DEFAULT_LOCALE, editorMessages, localizedHref, switchLocaleHref } from "./i18n.js";
|
|
3
3
|
import { ARTIFACT_FORMATS, ARTIFACT_TYPES } from "./storage.js";
|
|
4
4
|
|
|
5
|
-
export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination, locale = DEFAULT_LOCALE, currentPath = "/" }) {
|
|
5
|
+
export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination, locale = DEFAULT_LOCALE, currentPath = "/", user = null }) {
|
|
6
6
|
const view = viewContext(locale, currentPath);
|
|
7
7
|
const total = pagination?.total ?? artifacts.length;
|
|
8
8
|
const start = artifacts.length ? (pagination?.offset ?? 0) + 1 : 0;
|
|
@@ -48,6 +48,7 @@ export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination,
|
|
|
48
48
|
<a href="${view.href("/new")}">${view.text("nav.new")}</a>
|
|
49
49
|
<a href="${view.href("/import")}">${view.text("nav.import")}</a>
|
|
50
50
|
<a href="/api/artifacts">${view.text("nav.api")}</a>
|
|
51
|
+
${authNav(user)}
|
|
51
52
|
${languageSwitcher(view)}
|
|
52
53
|
</nav>
|
|
53
54
|
</header>
|
|
@@ -249,6 +250,170 @@ export function renderImportArtifactPage({ baseUrl, authToken = "", locale = DEF
|
|
|
249
250
|
});
|
|
250
251
|
}
|
|
251
252
|
|
|
253
|
+
export function renderLoginPage({ baseUrl, setup = false, error = "", locale = DEFAULT_LOCALE, currentPath = "/login" }) {
|
|
254
|
+
const view = viewContext(locale, currentPath);
|
|
255
|
+
const title = setup ? "Create admin account" : "Sign in";
|
|
256
|
+
return pageShell({
|
|
257
|
+
title,
|
|
258
|
+
body: `
|
|
259
|
+
<header class="topbar">
|
|
260
|
+
<div>
|
|
261
|
+
<h1>${escapeHtml(title)}</h1>
|
|
262
|
+
<p>${escapeHtml(baseUrl)}</p>
|
|
263
|
+
</div>
|
|
264
|
+
<nav>
|
|
265
|
+
<a href="${view.href("/")}">${view.text("nav.index")}</a>
|
|
266
|
+
${languageSwitcher(view)}
|
|
267
|
+
</nav>
|
|
268
|
+
</header>
|
|
269
|
+
<main class="artifact-editor auth-panel">
|
|
270
|
+
${error ? `<p class="auth-error">${escapeHtml(error)}</p>` : ""}
|
|
271
|
+
${setup ? `<p class="muted">No users exist yet. The first account becomes an administrator.</p>` : ""}
|
|
272
|
+
<form class="editor-form auth-form" method="post" action="/login">
|
|
273
|
+
<section class="editor-fields">
|
|
274
|
+
<label class="field">
|
|
275
|
+
<span>Email</span>
|
|
276
|
+
<input type="email" name="email" autocomplete="username" required>
|
|
277
|
+
</label>
|
|
278
|
+
${setup ? `<label class="field">
|
|
279
|
+
<span>Name</span>
|
|
280
|
+
<input name="name" autocomplete="name">
|
|
281
|
+
</label>` : ""}
|
|
282
|
+
<label class="field">
|
|
283
|
+
<span>Password</span>
|
|
284
|
+
<input type="password" name="password" autocomplete="${setup ? "new-password" : "current-password"}" minlength="8" required>
|
|
285
|
+
</label>
|
|
286
|
+
</section>
|
|
287
|
+
<footer class="editor-actions">
|
|
288
|
+
<button type="submit">${setup ? "Create admin" : "Sign in"}</button>
|
|
289
|
+
</footer>
|
|
290
|
+
</form>
|
|
291
|
+
</main>
|
|
292
|
+
`,
|
|
293
|
+
locale: view.locale
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function renderAccountPage({ baseUrl, user, tokens = [], createdToken = "", locale = DEFAULT_LOCALE, currentPath = "/account" }) {
|
|
298
|
+
const view = viewContext(locale, currentPath);
|
|
299
|
+
const rows = tokens.map((token) => `
|
|
300
|
+
<tr>
|
|
301
|
+
<td>${escapeHtml(token.name)}</td>
|
|
302
|
+
<td>${escapeHtml(token.createdAt)}</td>
|
|
303
|
+
<td>${token.lastUsedAt ? escapeHtml(token.lastUsedAt) : "Never"}</td>
|
|
304
|
+
<td>${token.revokedAt ? escapeHtml(token.revokedAt) : "Active"}</td>
|
|
305
|
+
<td>
|
|
306
|
+
${token.revokedAt ? "" : `<form method="post" action="/account/tokens/${encodeURIComponent(token.id)}/revoke">
|
|
307
|
+
<button type="submit">Revoke</button>
|
|
308
|
+
</form>`}
|
|
309
|
+
</td>
|
|
310
|
+
</tr>
|
|
311
|
+
`).join("");
|
|
312
|
+
|
|
313
|
+
return pageShell({
|
|
314
|
+
title: "Account",
|
|
315
|
+
body: `
|
|
316
|
+
<header class="topbar">
|
|
317
|
+
<div>
|
|
318
|
+
<h1>Account</h1>
|
|
319
|
+
<p>${escapeHtml(user.email)} · ${escapeHtml(user.role)} · ${escapeHtml(baseUrl)}</p>
|
|
320
|
+
</div>
|
|
321
|
+
<nav>
|
|
322
|
+
<a href="${view.href("/")}">${view.text("nav.index")}</a>
|
|
323
|
+
${user.role === "admin" ? `<a href="/admin/users">Users</a>` : ""}
|
|
324
|
+
<form class="nav-form" method="post" action="/logout"><button type="submit">Sign out</button></form>
|
|
325
|
+
${languageSwitcher(view)}
|
|
326
|
+
</nav>
|
|
327
|
+
</header>
|
|
328
|
+
<main class="artifact-view">
|
|
329
|
+
${createdToken ? `<section class="token-once">
|
|
330
|
+
<h2>New API token</h2>
|
|
331
|
+
<p>Copy this token now. Artifacty stores only its hash and cannot show it again.</p>
|
|
332
|
+
<pre class="artifact-code"><code>${escapeHtml(createdToken)}</code></pre>
|
|
333
|
+
</section>` : ""}
|
|
334
|
+
<section class="meta-card">
|
|
335
|
+
<h2>Profile</h2>
|
|
336
|
+
<p><strong>${escapeHtml(user.name)}</strong></p>
|
|
337
|
+
<p>${escapeHtml(user.email)}</p>
|
|
338
|
+
<p>Role: ${escapeHtml(user.role)}</p>
|
|
339
|
+
</section>
|
|
340
|
+
<section class="meta-card">
|
|
341
|
+
<h2>Create API token</h2>
|
|
342
|
+
<form class="inline-action" method="post" action="/account/tokens">
|
|
343
|
+
<input name="name" placeholder="Token name" autocomplete="off" required>
|
|
344
|
+
<button type="submit">Create token</button>
|
|
345
|
+
</form>
|
|
346
|
+
</section>
|
|
347
|
+
<section class="meta-card">
|
|
348
|
+
<h2>API tokens</h2>
|
|
349
|
+
<table class="data-table">
|
|
350
|
+
<thead><tr><th>Name</th><th>Created</th><th>Last used</th><th>Status</th><th></th></tr></thead>
|
|
351
|
+
<tbody>${rows || `<tr><td colspan="5">No API tokens.</td></tr>`}</tbody>
|
|
352
|
+
</table>
|
|
353
|
+
</section>
|
|
354
|
+
</main>
|
|
355
|
+
`,
|
|
356
|
+
locale: view.locale
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function renderAdminUsersPage({ baseUrl, user, users = [], locale = DEFAULT_LOCALE, currentPath = "/admin/users" }) {
|
|
361
|
+
const view = viewContext(locale, currentPath);
|
|
362
|
+
const rows = users.map((item) => `
|
|
363
|
+
<tr>
|
|
364
|
+
<td>${escapeHtml(item.email)}</td>
|
|
365
|
+
<td>${escapeHtml(item.name)}</td>
|
|
366
|
+
<td>${escapeHtml(item.role)}</td>
|
|
367
|
+
<td>${item.active ? "Active" : "Disabled"}</td>
|
|
368
|
+
<td>${escapeHtml(item.createdAt)}</td>
|
|
369
|
+
<td>
|
|
370
|
+
${item.id === user.id ? "" : `<form method="post" action="/admin/users/${encodeURIComponent(item.id)}/${item.active ? "disable" : "enable"}">
|
|
371
|
+
<button type="submit">${item.active ? "Disable" : "Enable"}</button>
|
|
372
|
+
</form>`}
|
|
373
|
+
</td>
|
|
374
|
+
</tr>
|
|
375
|
+
`).join("");
|
|
376
|
+
|
|
377
|
+
return pageShell({
|
|
378
|
+
title: "Users",
|
|
379
|
+
body: `
|
|
380
|
+
<header class="topbar">
|
|
381
|
+
<div>
|
|
382
|
+
<h1>Users</h1>
|
|
383
|
+
<p>${escapeHtml(baseUrl)}</p>
|
|
384
|
+
</div>
|
|
385
|
+
<nav>
|
|
386
|
+
<a href="${view.href("/")}">${view.text("nav.index")}</a>
|
|
387
|
+
<a href="/account">Account</a>
|
|
388
|
+
${languageSwitcher(view)}
|
|
389
|
+
</nav>
|
|
390
|
+
</header>
|
|
391
|
+
<main class="artifact-view">
|
|
392
|
+
<section class="meta-card">
|
|
393
|
+
<h2>Create user</h2>
|
|
394
|
+
<form class="editor-form auth-form" method="post" action="/admin/users">
|
|
395
|
+
<section class="editor-fields">
|
|
396
|
+
<label class="field"><span>Email</span><input type="email" name="email" required></label>
|
|
397
|
+
<label class="field"><span>Name</span><input name="name"></label>
|
|
398
|
+
<label class="field"><span>Role</span><select name="role"><option value="user">user</option><option value="admin">admin</option></select></label>
|
|
399
|
+
<label class="field"><span>Password</span><input type="password" name="password" minlength="8" required></label>
|
|
400
|
+
</section>
|
|
401
|
+
<footer class="editor-actions"><button type="submit">Create user</button></footer>
|
|
402
|
+
</form>
|
|
403
|
+
</section>
|
|
404
|
+
<section class="meta-card">
|
|
405
|
+
<h2>Existing users</h2>
|
|
406
|
+
<table class="data-table">
|
|
407
|
+
<thead><tr><th>Email</th><th>Name</th><th>Role</th><th>Status</th><th>Created</th><th></th></tr></thead>
|
|
408
|
+
<tbody>${rows}</tbody>
|
|
409
|
+
</table>
|
|
410
|
+
</section>
|
|
411
|
+
</main>
|
|
412
|
+
`,
|
|
413
|
+
locale: view.locale
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
252
417
|
export function renderArtifactPage({ artifact, version, content, baseUrl, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/" }) {
|
|
253
418
|
const view = viewContext(locale, currentPath);
|
|
254
419
|
const versionLinks = artifact.versions
|
|
@@ -726,6 +891,25 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
726
891
|
.inline-action {
|
|
727
892
|
margin-bottom: 12px;
|
|
728
893
|
}
|
|
894
|
+
.nav-form {
|
|
895
|
+
display: inline-flex;
|
|
896
|
+
margin: 0;
|
|
897
|
+
}
|
|
898
|
+
.nav-form button {
|
|
899
|
+
min-height: 0;
|
|
900
|
+
padding: 0;
|
|
901
|
+
border: 0;
|
|
902
|
+
background: transparent;
|
|
903
|
+
color: inherit;
|
|
904
|
+
font: inherit;
|
|
905
|
+
font-weight: inherit;
|
|
906
|
+
}
|
|
907
|
+
.nav-form button:hover {
|
|
908
|
+
background: transparent;
|
|
909
|
+
border: 0;
|
|
910
|
+
color: var(--text);
|
|
911
|
+
text-decoration: underline;
|
|
912
|
+
}
|
|
729
913
|
.diff-form {
|
|
730
914
|
grid-template-columns: 160px 160px auto;
|
|
731
915
|
width: fit-content;
|
|
@@ -788,6 +972,64 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
788
972
|
display: grid;
|
|
789
973
|
gap: 16px;
|
|
790
974
|
}
|
|
975
|
+
.auth-panel {
|
|
976
|
+
max-width: 720px;
|
|
977
|
+
margin: 0 auto;
|
|
978
|
+
}
|
|
979
|
+
.auth-form .editor-fields {
|
|
980
|
+
grid-template-columns: minmax(220px, 1fr);
|
|
981
|
+
}
|
|
982
|
+
.auth-error,
|
|
983
|
+
.token-once {
|
|
984
|
+
border: 1px solid var(--line);
|
|
985
|
+
border-radius: 8px;
|
|
986
|
+
padding: 12px 14px;
|
|
987
|
+
background: var(--panel);
|
|
988
|
+
}
|
|
989
|
+
.auth-error {
|
|
990
|
+
color: #991b1b;
|
|
991
|
+
background: #fef2f2;
|
|
992
|
+
border-color: #fecaca;
|
|
993
|
+
}
|
|
994
|
+
.muted {
|
|
995
|
+
color: var(--muted);
|
|
996
|
+
}
|
|
997
|
+
.meta-card {
|
|
998
|
+
display: grid;
|
|
999
|
+
gap: 10px;
|
|
1000
|
+
margin-bottom: 16px;
|
|
1001
|
+
border: 1px solid var(--line);
|
|
1002
|
+
border-radius: 8px;
|
|
1003
|
+
padding: 16px;
|
|
1004
|
+
background: var(--panel);
|
|
1005
|
+
}
|
|
1006
|
+
.meta-card h2,
|
|
1007
|
+
.token-once h2 {
|
|
1008
|
+
margin: 0;
|
|
1009
|
+
font-size: 18px;
|
|
1010
|
+
}
|
|
1011
|
+
.data-table {
|
|
1012
|
+
width: 100%;
|
|
1013
|
+
border-collapse: collapse;
|
|
1014
|
+
font-size: 13px;
|
|
1015
|
+
}
|
|
1016
|
+
.data-table th,
|
|
1017
|
+
.data-table td {
|
|
1018
|
+
padding: 9px 10px;
|
|
1019
|
+
border-bottom: 1px solid var(--line);
|
|
1020
|
+
text-align: left;
|
|
1021
|
+
vertical-align: middle;
|
|
1022
|
+
}
|
|
1023
|
+
.data-table th {
|
|
1024
|
+
color: var(--faint);
|
|
1025
|
+
font-family: var(--mono);
|
|
1026
|
+
font-size: 11.5px;
|
|
1027
|
+
letter-spacing: 0.06em;
|
|
1028
|
+
text-transform: uppercase;
|
|
1029
|
+
}
|
|
1030
|
+
.data-table form {
|
|
1031
|
+
margin: 0;
|
|
1032
|
+
}
|
|
791
1033
|
.editor-fields {
|
|
792
1034
|
display: grid;
|
|
793
1035
|
grid-template-columns: minmax(220px, 1fr) 180px 180px minmax(160px, 240px);
|
|
@@ -1573,6 +1815,13 @@ function languageSwitcher(view) {
|
|
|
1573
1815
|
return `<span class="language-switcher">${english}${korean}</span>`;
|
|
1574
1816
|
}
|
|
1575
1817
|
|
|
1818
|
+
function authNav(user) {
|
|
1819
|
+
if (!user) {
|
|
1820
|
+
return `<a href="/login">Sign in</a>`;
|
|
1821
|
+
}
|
|
1822
|
+
return `${user.role === "admin" ? `<a href="/admin/users">Users</a>` : ""}<a href="/account">Account</a>`;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1576
1825
|
export function escapeHtml(value) {
|
|
1577
1826
|
return String(value)
|
|
1578
1827
|
.replaceAll("&", "&")
|
package/src/lib/security.js
CHANGED
|
@@ -57,6 +57,10 @@ export function requireToken({ request, url, body = {}, config = securityConfig(
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
export function requestToken({ request, url, body = {} }) {
|
|
61
|
+
return extractToken({ request, url, body });
|
|
62
|
+
}
|
|
63
|
+
|
|
60
64
|
export function scanForSecrets(content) {
|
|
61
65
|
const text = String(content ?? "");
|
|
62
66
|
const findings = [];
|
package/src/lib/service.js
CHANGED
|
@@ -230,6 +230,7 @@ function serviceConfig(options = {}) {
|
|
|
230
230
|
apiToken: options.apiToken || "",
|
|
231
231
|
shareMode: options.shareMode || "",
|
|
232
232
|
allowSecrets: Boolean(options.allowSecrets),
|
|
233
|
+
mcpHttp: Boolean(options.mcpHttp),
|
|
233
234
|
taskName: options.taskName || DEFAULT_TASK_NAME
|
|
234
235
|
};
|
|
235
236
|
}
|
|
@@ -251,6 +252,9 @@ function serverArgs(config, options = {}) {
|
|
|
251
252
|
if (config.allowSecrets) {
|
|
252
253
|
args.push("--allow-secrets");
|
|
253
254
|
}
|
|
255
|
+
if (config.mcpHttp) {
|
|
256
|
+
args.push("--mcp-http");
|
|
257
|
+
}
|
|
254
258
|
if (options.includeApiTokenArg && config.apiToken) {
|
|
255
259
|
args.push("--api-token", config.apiToken);
|
|
256
260
|
}
|