artifacty 0.8.0 → 0.10.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 +28 -0
- package/docs/central-team-deployment-design.md +212 -0
- package/docs/integrations.md +39 -1
- package/docs/mcp-public-api.md +13 -4
- package/docs/network-sharing.md +13 -0
- package/docs/threat-model.md +20 -6
- package/package.json +2 -1
- package/src/cli.js +28 -6
- package/src/lib/background.js +3 -0
- package/src/lib/installer.js +31 -0
- package/src/lib/render.js +371 -1
- package/src/lib/security.js +4 -0
- package/src/lib/service.js +4 -0
- package/src/lib/storage.js +565 -1
- package/src/mcp-server.js +225 -81
- package/src/server.js +412 -13
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,263 @@ 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
|
+
<a href="/account/password">Password</a>
|
|
325
|
+
<form class="nav-form" method="post" action="/logout"><button type="submit">Sign out</button></form>
|
|
326
|
+
${languageSwitcher(view)}
|
|
327
|
+
</nav>
|
|
328
|
+
</header>
|
|
329
|
+
<main class="artifact-view">
|
|
330
|
+
${createdToken ? `<section class="token-once">
|
|
331
|
+
<h2>New API token</h2>
|
|
332
|
+
<p>Copy this token now. Artifacty stores only its hash and cannot show it again.</p>
|
|
333
|
+
<pre class="artifact-code"><code>${escapeHtml(createdToken)}</code></pre>
|
|
334
|
+
</section>` : ""}
|
|
335
|
+
<section class="meta-card">
|
|
336
|
+
<h2>Profile</h2>
|
|
337
|
+
<p><strong>${escapeHtml(user.name)}</strong></p>
|
|
338
|
+
<p>${escapeHtml(user.email)}</p>
|
|
339
|
+
<p>Role: ${escapeHtml(user.role)}</p>
|
|
340
|
+
</section>
|
|
341
|
+
<section class="meta-card">
|
|
342
|
+
<h2>Create API token</h2>
|
|
343
|
+
<form class="inline-action" method="post" action="/account/tokens">
|
|
344
|
+
<input name="name" placeholder="Token name" autocomplete="off" required>
|
|
345
|
+
<button type="submit">Create token</button>
|
|
346
|
+
</form>
|
|
347
|
+
</section>
|
|
348
|
+
<section class="meta-card">
|
|
349
|
+
<h2>API tokens</h2>
|
|
350
|
+
<table class="data-table">
|
|
351
|
+
<thead><tr><th>Name</th><th>Created</th><th>Last used</th><th>Status</th><th></th></tr></thead>
|
|
352
|
+
<tbody>${rows || `<tr><td colspan="5">No API tokens.</td></tr>`}</tbody>
|
|
353
|
+
</table>
|
|
354
|
+
</section>
|
|
355
|
+
</main>
|
|
356
|
+
`,
|
|
357
|
+
locale: view.locale
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function renderPasswordPage({ baseUrl, user, required = false, error = "", success = "", locale = DEFAULT_LOCALE, currentPath = "/account/password" }) {
|
|
362
|
+
const view = viewContext(locale, currentPath);
|
|
363
|
+
return pageShell({
|
|
364
|
+
title: "Change Password",
|
|
365
|
+
body: `
|
|
366
|
+
<header class="topbar">
|
|
367
|
+
<div>
|
|
368
|
+
<h1>Change Password</h1>
|
|
369
|
+
<p>${escapeHtml(user.email)} · ${escapeHtml(baseUrl)}</p>
|
|
370
|
+
</div>
|
|
371
|
+
<nav>
|
|
372
|
+
<a href="${view.href("/")}">${view.text("nav.index")}</a>
|
|
373
|
+
${required ? "" : `<a href="/account">Account</a>`}
|
|
374
|
+
<form class="nav-form" method="post" action="/logout"><button type="submit">Sign out</button></form>
|
|
375
|
+
${languageSwitcher(view)}
|
|
376
|
+
</nav>
|
|
377
|
+
</header>
|
|
378
|
+
<main class="artifact-editor auth-panel">
|
|
379
|
+
${required ? `<p class="auth-warning">Password change is required before continuing.</p>` : ""}
|
|
380
|
+
${error ? `<p class="auth-error">${escapeHtml(error)}</p>` : ""}
|
|
381
|
+
${success ? `<p class="auth-success">${escapeHtml(success)}</p>` : ""}
|
|
382
|
+
<form class="editor-form auth-form" method="post" action="/account/password">
|
|
383
|
+
<section class="editor-fields">
|
|
384
|
+
<label class="field">
|
|
385
|
+
<span>Current password</span>
|
|
386
|
+
<input type="password" name="currentPassword" autocomplete="current-password" required>
|
|
387
|
+
</label>
|
|
388
|
+
<label class="field">
|
|
389
|
+
<span>New password</span>
|
|
390
|
+
<input type="password" name="newPassword" autocomplete="new-password" minlength="8" required>
|
|
391
|
+
</label>
|
|
392
|
+
<label class="field">
|
|
393
|
+
<span>Confirm password</span>
|
|
394
|
+
<input type="password" name="confirmPassword" autocomplete="new-password" minlength="8" required>
|
|
395
|
+
</label>
|
|
396
|
+
</section>
|
|
397
|
+
<footer class="editor-actions">
|
|
398
|
+
<button type="submit">Change password</button>
|
|
399
|
+
</footer>
|
|
400
|
+
</form>
|
|
401
|
+
</main>
|
|
402
|
+
`,
|
|
403
|
+
locale: view.locale
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function renderAdminUsersPage({ baseUrl, user, users = [], importResult = null, importError = "", locale = DEFAULT_LOCALE, currentPath = "/admin/users" }) {
|
|
408
|
+
const view = viewContext(locale, currentPath);
|
|
409
|
+
const rows = users.map((item) => `
|
|
410
|
+
<tr>
|
|
411
|
+
<td>${escapeHtml(item.email)}</td>
|
|
412
|
+
<td>${escapeHtml(item.name)}</td>
|
|
413
|
+
<td>${escapeHtml(item.role)}</td>
|
|
414
|
+
<td>${item.active ? "Active" : "Disabled"}${item.passwordResetRequired ? " · Reset required" : ""}</td>
|
|
415
|
+
<td>${escapeHtml(item.createdAt)}</td>
|
|
416
|
+
<td>
|
|
417
|
+
${item.id === user.id ? "" : `<form method="post" action="/admin/users/${encodeURIComponent(item.id)}/${item.active ? "disable" : "enable"}">
|
|
418
|
+
<button type="submit">${item.active ? "Disable" : "Enable"}</button>
|
|
419
|
+
</form>`}
|
|
420
|
+
</td>
|
|
421
|
+
</tr>
|
|
422
|
+
`).join("");
|
|
423
|
+
const createdRows = (importResult?.created || []).map((item) => `
|
|
424
|
+
<tr>
|
|
425
|
+
<td>${escapeHtml(item.user.email)}</td>
|
|
426
|
+
<td>${escapeHtml(item.user.role)}</td>
|
|
427
|
+
<td>${item.user.passwordResetRequired ? "Yes" : "No"}</td>
|
|
428
|
+
<td>${item.passwordGenerated ? `<code>${escapeHtml(item.temporaryPassword)}</code>` : "Provided in CSV"}</td>
|
|
429
|
+
</tr>
|
|
430
|
+
`).join("");
|
|
431
|
+
const skippedRows = (importResult?.skipped || []).map((item) => `
|
|
432
|
+
<tr><td>${escapeHtml(item.row)}</td><td>${escapeHtml(item.email)}</td><td>${escapeHtml(item.reason)}</td></tr>
|
|
433
|
+
`).join("");
|
|
434
|
+
const failedRows = (importResult?.failed || []).map((item) => `
|
|
435
|
+
<tr><td>${escapeHtml(item.row)}</td><td>${escapeHtml(item.email)}</td><td>${escapeHtml(item.error)}</td></tr>
|
|
436
|
+
`).join("");
|
|
437
|
+
const importSummary = importResult ? `
|
|
438
|
+
<section class="meta-card">
|
|
439
|
+
<h2>Import results</h2>
|
|
440
|
+
<p class="muted">${importResult.created.length} created, ${importResult.skipped.length} skipped, ${importResult.failed.length} failed.</p>
|
|
441
|
+
${createdRows ? `<table class="data-table">
|
|
442
|
+
<thead><tr><th>Email</th><th>Role</th><th>Reset required</th><th>Temporary password</th></tr></thead>
|
|
443
|
+
<tbody>${createdRows}</tbody>
|
|
444
|
+
</table>` : ""}
|
|
445
|
+
${skippedRows ? `<h3>Skipped</h3><table class="data-table">
|
|
446
|
+
<thead><tr><th>Row</th><th>Email</th><th>Reason</th></tr></thead>
|
|
447
|
+
<tbody>${skippedRows}</tbody>
|
|
448
|
+
</table>` : ""}
|
|
449
|
+
${failedRows ? `<h3>Failed</h3><table class="data-table">
|
|
450
|
+
<thead><tr><th>Row</th><th>Email</th><th>Error</th></tr></thead>
|
|
451
|
+
<tbody>${failedRows}</tbody>
|
|
452
|
+
</table>` : ""}
|
|
453
|
+
</section>
|
|
454
|
+
` : "";
|
|
455
|
+
|
|
456
|
+
return pageShell({
|
|
457
|
+
title: "Users",
|
|
458
|
+
body: `
|
|
459
|
+
<header class="topbar">
|
|
460
|
+
<div>
|
|
461
|
+
<h1>Users</h1>
|
|
462
|
+
<p>${escapeHtml(baseUrl)}</p>
|
|
463
|
+
</div>
|
|
464
|
+
<nav>
|
|
465
|
+
<a href="${view.href("/")}">${view.text("nav.index")}</a>
|
|
466
|
+
<a href="/account">Account</a>
|
|
467
|
+
${languageSwitcher(view)}
|
|
468
|
+
</nav>
|
|
469
|
+
</header>
|
|
470
|
+
<main class="artifact-view">
|
|
471
|
+
<section class="meta-card">
|
|
472
|
+
<h2>Create user</h2>
|
|
473
|
+
<form class="editor-form auth-form" method="post" action="/admin/users">
|
|
474
|
+
<section class="editor-fields">
|
|
475
|
+
<label class="field"><span>Email</span><input type="email" name="email" required></label>
|
|
476
|
+
<label class="field"><span>Name</span><input name="name"></label>
|
|
477
|
+
<label class="field"><span>Role</span><select name="role"><option value="user">user</option><option value="admin">admin</option></select></label>
|
|
478
|
+
<label class="field"><span>Password</span><input type="password" name="password" minlength="8" required></label>
|
|
479
|
+
<label class="check-field"><input type="checkbox" name="passwordResetRequired"> Require password change</label>
|
|
480
|
+
</section>
|
|
481
|
+
<footer class="editor-actions"><button type="submit">Create user</button></footer>
|
|
482
|
+
</form>
|
|
483
|
+
</section>
|
|
484
|
+
<section class="meta-card">
|
|
485
|
+
<h2>Import users from CSV</h2>
|
|
486
|
+
<p class="muted">Use headers: email, name, role, password, password_reset_required. Missing passwords are generated and must be changed at first sign-in.</p>
|
|
487
|
+
${importError ? `<p class="auth-error">${escapeHtml(importError)}</p>` : ""}
|
|
488
|
+
<form class="editor-form" method="post" action="/admin/users/import">
|
|
489
|
+
<label class="field content-field">
|
|
490
|
+
<span>CSV</span>
|
|
491
|
+
<textarea class="compact-textarea" name="csv" spellcheck="false" placeholder="email,name,role user@example.com,User,user" required></textarea>
|
|
492
|
+
</label>
|
|
493
|
+
<footer class="editor-actions"><button type="submit">Import users</button></footer>
|
|
494
|
+
</form>
|
|
495
|
+
</section>
|
|
496
|
+
${importSummary}
|
|
497
|
+
<section class="meta-card">
|
|
498
|
+
<h2>Existing users</h2>
|
|
499
|
+
<table class="data-table">
|
|
500
|
+
<thead><tr><th>Email</th><th>Name</th><th>Role</th><th>Status</th><th>Created</th><th></th></tr></thead>
|
|
501
|
+
<tbody>${rows}</tbody>
|
|
502
|
+
</table>
|
|
503
|
+
</section>
|
|
504
|
+
</main>
|
|
505
|
+
`,
|
|
506
|
+
locale: view.locale
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
252
510
|
export function renderArtifactPage({ artifact, version, content, baseUrl, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/" }) {
|
|
253
511
|
const view = viewContext(locale, currentPath);
|
|
254
512
|
const versionLinks = artifact.versions
|
|
@@ -726,6 +984,25 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
726
984
|
.inline-action {
|
|
727
985
|
margin-bottom: 12px;
|
|
728
986
|
}
|
|
987
|
+
.nav-form {
|
|
988
|
+
display: inline-flex;
|
|
989
|
+
margin: 0;
|
|
990
|
+
}
|
|
991
|
+
.nav-form button {
|
|
992
|
+
min-height: 0;
|
|
993
|
+
padding: 0;
|
|
994
|
+
border: 0;
|
|
995
|
+
background: transparent;
|
|
996
|
+
color: inherit;
|
|
997
|
+
font: inherit;
|
|
998
|
+
font-weight: inherit;
|
|
999
|
+
}
|
|
1000
|
+
.nav-form button:hover {
|
|
1001
|
+
background: transparent;
|
|
1002
|
+
border: 0;
|
|
1003
|
+
color: var(--text);
|
|
1004
|
+
text-decoration: underline;
|
|
1005
|
+
}
|
|
729
1006
|
.diff-form {
|
|
730
1007
|
grid-template-columns: 160px 160px auto;
|
|
731
1008
|
width: fit-content;
|
|
@@ -788,6 +1065,76 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
788
1065
|
display: grid;
|
|
789
1066
|
gap: 16px;
|
|
790
1067
|
}
|
|
1068
|
+
.auth-panel {
|
|
1069
|
+
max-width: 720px;
|
|
1070
|
+
margin: 0 auto;
|
|
1071
|
+
}
|
|
1072
|
+
.auth-form .editor-fields {
|
|
1073
|
+
grid-template-columns: minmax(220px, 1fr);
|
|
1074
|
+
}
|
|
1075
|
+
.auth-error,
|
|
1076
|
+
.auth-success,
|
|
1077
|
+
.auth-warning,
|
|
1078
|
+
.token-once {
|
|
1079
|
+
border: 1px solid var(--line);
|
|
1080
|
+
border-radius: 8px;
|
|
1081
|
+
padding: 12px 14px;
|
|
1082
|
+
background: var(--panel);
|
|
1083
|
+
}
|
|
1084
|
+
.auth-error {
|
|
1085
|
+
color: #991b1b;
|
|
1086
|
+
background: #fef2f2;
|
|
1087
|
+
border-color: #fecaca;
|
|
1088
|
+
}
|
|
1089
|
+
.auth-success {
|
|
1090
|
+
color: #166534;
|
|
1091
|
+
background: #f0fdf4;
|
|
1092
|
+
border-color: #bbf7d0;
|
|
1093
|
+
}
|
|
1094
|
+
.auth-warning {
|
|
1095
|
+
color: #92400e;
|
|
1096
|
+
background: #fffbeb;
|
|
1097
|
+
border-color: #fde68a;
|
|
1098
|
+
}
|
|
1099
|
+
.muted {
|
|
1100
|
+
color: var(--muted);
|
|
1101
|
+
}
|
|
1102
|
+
.meta-card {
|
|
1103
|
+
display: grid;
|
|
1104
|
+
gap: 10px;
|
|
1105
|
+
margin-bottom: 16px;
|
|
1106
|
+
border: 1px solid var(--line);
|
|
1107
|
+
border-radius: 8px;
|
|
1108
|
+
padding: 16px;
|
|
1109
|
+
background: var(--panel);
|
|
1110
|
+
}
|
|
1111
|
+
.meta-card h2,
|
|
1112
|
+
.token-once h2 {
|
|
1113
|
+
margin: 0;
|
|
1114
|
+
font-size: 18px;
|
|
1115
|
+
}
|
|
1116
|
+
.data-table {
|
|
1117
|
+
width: 100%;
|
|
1118
|
+
border-collapse: collapse;
|
|
1119
|
+
font-size: 13px;
|
|
1120
|
+
}
|
|
1121
|
+
.data-table th,
|
|
1122
|
+
.data-table td {
|
|
1123
|
+
padding: 9px 10px;
|
|
1124
|
+
border-bottom: 1px solid var(--line);
|
|
1125
|
+
text-align: left;
|
|
1126
|
+
vertical-align: middle;
|
|
1127
|
+
}
|
|
1128
|
+
.data-table th {
|
|
1129
|
+
color: var(--faint);
|
|
1130
|
+
font-family: var(--mono);
|
|
1131
|
+
font-size: 11.5px;
|
|
1132
|
+
letter-spacing: 0.06em;
|
|
1133
|
+
text-transform: uppercase;
|
|
1134
|
+
}
|
|
1135
|
+
.data-table form {
|
|
1136
|
+
margin: 0;
|
|
1137
|
+
}
|
|
791
1138
|
.editor-fields {
|
|
792
1139
|
display: grid;
|
|
793
1140
|
grid-template-columns: minmax(220px, 1fr) 180px 180px minmax(160px, 240px);
|
|
@@ -807,6 +1154,19 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
807
1154
|
text-transform: uppercase;
|
|
808
1155
|
color: var(--faint);
|
|
809
1156
|
}
|
|
1157
|
+
.check-field {
|
|
1158
|
+
display: flex;
|
|
1159
|
+
gap: 8px;
|
|
1160
|
+
align-items: center;
|
|
1161
|
+
min-height: 38px;
|
|
1162
|
+
color: var(--muted);
|
|
1163
|
+
font-size: 13px;
|
|
1164
|
+
}
|
|
1165
|
+
.check-field input {
|
|
1166
|
+
width: auto;
|
|
1167
|
+
min-height: 0;
|
|
1168
|
+
padding: 0;
|
|
1169
|
+
}
|
|
810
1170
|
input,
|
|
811
1171
|
select,
|
|
812
1172
|
textarea {
|
|
@@ -835,6 +1195,9 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
835
1195
|
white-space: pre;
|
|
836
1196
|
overflow: auto;
|
|
837
1197
|
}
|
|
1198
|
+
.compact-textarea {
|
|
1199
|
+
min-height: 160px;
|
|
1200
|
+
}
|
|
838
1201
|
.textarea-enhanced {
|
|
839
1202
|
display: none;
|
|
840
1203
|
}
|
|
@@ -1573,6 +1936,13 @@ function languageSwitcher(view) {
|
|
|
1573
1936
|
return `<span class="language-switcher">${english}${korean}</span>`;
|
|
1574
1937
|
}
|
|
1575
1938
|
|
|
1939
|
+
function authNav(user) {
|
|
1940
|
+
if (!user) {
|
|
1941
|
+
return `<a href="/login">Sign in</a>`;
|
|
1942
|
+
}
|
|
1943
|
+
return `${user.role === "admin" ? `<a href="/admin/users">Users</a>` : ""}<a href="/account">Account</a>`;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1576
1946
|
export function escapeHtml(value) {
|
|
1577
1947
|
return String(value)
|
|
1578
1948
|
.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
|
}
|