mcprigor 1.4.0 → 1.5.1
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 -4
- package/dist/audit.d.ts +44 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +157 -0
- package/dist/audit.js.map +1 -0
- package/dist/cli.js +243 -10
- package/dist/cli.js.map +1 -1
- package/dist/composition.d.ts +42 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +130 -0
- package/dist/composition.js.map +1 -0
- package/dist/coverage.d.ts +33 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +83 -0
- package/dist/coverage.js.map +1 -0
- package/dist/export.d.ts +4 -0
- package/dist/export.d.ts.map +1 -1
- package/dist/export.js +56 -0
- package/dist/export.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/language.js +1 -1
- package/dist/language.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +4 -1
- package/dist/loader.js.map +1 -1
- package/dist/monitor.d.ts +22 -0
- package/dist/monitor.d.ts.map +1 -0
- package/dist/monitor.js +71 -0
- package/dist/monitor.js.map +1 -0
- package/dist/oauth.d.ts +73 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +144 -0
- package/dist/oauth.js.map +1 -0
- package/dist/perf.d.ts +37 -0
- package/dist/perf.d.ts.map +1 -0
- package/dist/perf.js +90 -0
- package/dist/perf.js.map +1 -0
- package/dist/publish.d.ts +19 -0
- package/dist/publish.d.ts.map +1 -0
- package/dist/publish.js +58 -0
- package/dist/publish.js.map +1 -0
- package/dist/qa-language.d.ts.map +1 -1
- package/dist/qa-language.js +75 -3
- package/dist/qa-language.js.map +1 -1
- package/dist/reporters.d.ts +3 -1
- package/dist/reporters.d.ts.map +1 -1
- package/dist/reporters.js +21 -3
- package/dist/reporters.js.map +1 -1
- package/dist/runner.d.ts +7 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +53 -15
- package/dist/runner.js.map +1 -1
- package/dist/schema.d.ts +66 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +21 -1
- package/dist/schema.js.map +1 -1
- package/dist/session.d.ts +2 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +7 -3
- package/dist/session.js.map +1 -1
- package/dist/timeline.d.ts +18 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +59 -0
- package/dist/timeline.js.map +1 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +35 -3
- package/dist/workspace.js.map +1 -1
- package/docs/AUTHENTICATION.md +213 -0
- package/docs/CLI-REFERENCE.md +123 -2
- package/docs/COVERAGE.md +37 -0
- package/docs/ENGINEER-SETUP.md +1 -1
- package/docs/EVIDENCE.md +11 -0
- package/docs/GETTING-STARTED.md +1 -1
- package/docs/GITHUB-ACTION.md +82 -0
- package/docs/LANGUAGE-SPEC.md +64 -7
- package/docs/MCP-SERVER.md +1 -1
- package/docs/MONITORING.md +62 -0
- package/docs/MULTI-SERVER-COMPOSITIONS.md +103 -0
- package/docs/{PLAIN-LANGUAGE-COOKBOOK.md → NATURAL-LANGUAGE-COOKBOOK.md} +63 -4
- package/docs/PERFORMANCE-GOVERNANCE.md +85 -0
- package/docs/PUBLISHING.md +53 -0
- package/docs/QA-GUIDE.md +2 -2
- package/docs/QA-WORKSPACE.md +5 -1
- package/docs/README.md +10 -2
- package/docs/SECURITY-AUDIT.md +77 -0
- package/editors/mcpr-language.json +2 -2
- package/editors/vscode/package.json +1 -1
- package/editors/vscode/syntaxes/mcpr.tmLanguage.json +2 -2
- package/package.json +1 -2
- package/workspace-assets/app.js +35 -4
- package/workspace-assets/index.html +4 -1
- package/workspace-assets/style.css +2 -0
package/docs/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Choose the path that matches your role.
|
|
|
5
5
|
## QA authors
|
|
6
6
|
|
|
7
7
|
1. [Getting started](GETTING-STARTED.md) — install, connect, and run your first test.
|
|
8
|
-
2. [
|
|
8
|
+
2. [Natural-language cookbook](NATURAL-LANGUAGE-COOKBOOK.md) — copy-ready scenarios and assertions.
|
|
9
9
|
3. [QA workspace](QA-WORKSPACE.md) — edit and run tests in a browser.
|
|
10
10
|
4. [Data and reusable flows](DATA-AND-REUSE.md) — tables, files, functions, and shared flows.
|
|
11
11
|
5. [Troubleshooting](TROUBLESHOOTING.md) — understand errors and fix common failures.
|
|
@@ -13,11 +13,18 @@ Choose the path that matches your role.
|
|
|
13
13
|
## Test and platform engineers
|
|
14
14
|
|
|
15
15
|
- [Engineer setup](ENGINEER-SETUP.md) — targets, credentials, project layout, and CI.
|
|
16
|
+
- [Authentication and secrets](AUTHENTICATION.md) — test protected servers with bearer tokens, API keys, OAuth, and `${env.NAME}` placeholders.
|
|
16
17
|
- [CLI reference](CLI-REFERENCE.md) — commands, options, outputs, and exit codes.
|
|
17
|
-
- [Language reference](LANGUAGE-SPEC.md) — complete deterministic `.mcpr` syntax
|
|
18
|
+
- [Language reference](LANGUAGE-SPEC.md) — complete deterministic `.mcpr` syntax, in enforced parity with YAML via `mcprigor convert`.
|
|
18
19
|
- [State and dependencies](STATE-AND-DEPENDENCIES.md) — share outputs across tests and runs.
|
|
19
20
|
- [Data engineering](DATA-ENGINEERING.md) — types, filters, joins, samples, and caches.
|
|
20
21
|
- [Transport parity](TRANSPORT-PARITY.md) — compare stdio and Streamable HTTP.
|
|
22
|
+
- [Performance governance](PERFORMANCE-GOVERNANCE.md) — call limits, percentile budgets, and regression gates.
|
|
23
|
+
- [Multi-server compositions](MULTI-SERVER-COMPOSITIONS.md) — route tests across a fleet and gate combined drift.
|
|
24
|
+
- [Coverage](COVERAGE.md) — find untested MCP surfaces and input-schema branches.
|
|
25
|
+
- [Scheduled monitoring](MONITORING.md) — continuously test production HTTP endpoints and notify webhooks.
|
|
26
|
+
- [GitHub Action](GITHUB-ACTION.md) — test, drift, flaky warnings, and rich pull-request comments.
|
|
27
|
+
- [Shareable hosted reports](PUBLISHING.md) — `mcprigor publish` turns a run into a static report URL.
|
|
21
28
|
|
|
22
29
|
## Contracts and evidence
|
|
23
30
|
|
|
@@ -30,6 +37,7 @@ Choose the path that matches your role.
|
|
|
30
37
|
|
|
31
38
|
- [Extension SDK](EXTENSION-SDK.md)
|
|
32
39
|
- [Stable error model](ERROR-MODEL.md)
|
|
40
|
+
- [Deterministic security audit](SECURITY-AUDIT.md) — built-in probe pack, severity gate, and rich reports.
|
|
33
41
|
- [Security and retention](SECURITY-AND-RETENTION.md)
|
|
34
42
|
- [Compatibility policy](COMPATIBILITY.md)
|
|
35
43
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Deterministic security audit
|
|
2
|
+
|
|
3
|
+
> Available since `1.5.0`.
|
|
4
|
+
|
|
5
|
+
`mcprigor audit` runs a fixed security and permissions probe pack against an MCP server and produces an auditable score. No AI chooses payloads or interprets outcomes.
|
|
6
|
+
|
|
7
|
+
## Run the safe default pack
|
|
8
|
+
|
|
9
|
+
Use a suite's configured target:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
mcprigor audit tests/server.mcpr
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or supply a target directly:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
mcprigor audit --url https://qa.example.com/mcp
|
|
19
|
+
mcprigor audit --command "node dist/server.js"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The non-destructive default probes:
|
|
23
|
+
|
|
24
|
+
| Probe | What MCP Rigor verifies |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Malformed parameters | Invalid `tools/call` parameters are rejected |
|
|
27
|
+
| Tool-name spoofing | An unknown administrative-looking tool cannot be invoked |
|
|
28
|
+
| Oversized payload | A deterministic 1 MiB payload is rejected |
|
|
29
|
+
| Path traversal | Resource reads reject Unix, Windows, and relative traversal URIs |
|
|
30
|
+
| Tool inventory | Advertised tools are listed for opt-in injection/canary testing |
|
|
31
|
+
|
|
32
|
+
## Opt into reviewed tool calls
|
|
33
|
+
|
|
34
|
+
MCP Rigor never guesses that a tool is safe. Tool execution is disabled unless you allow an exact name:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
mcprigor audit tests/server.mcpr \
|
|
38
|
+
--allow-tool search \
|
|
39
|
+
--allow-tool summarize
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Only allow read-only, non-destructive tools in a disposable test environment. An allowed tool receives a fixed prompt-injection payload containing a deterministic canary. MCP Rigor checks whether the response follows or reflects the payload and whether it exposes the canary. Report evidence replaces the canary with `[CANARY REDACTED]`.
|
|
43
|
+
|
|
44
|
+
## CI severity gate
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
mcprigor audit tests/server.mcpr --fail-on high
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Values are `critical`, `high`, `medium`, `low`, and `none`; the default is `high`. The command exits nonzero when a failed finding meets or exceeds the selected severity.
|
|
51
|
+
|
|
52
|
+
Scoring starts at 100:
|
|
53
|
+
|
|
54
|
+
- critical: −35
|
|
55
|
+
- high: −20
|
|
56
|
+
- medium: −10
|
|
57
|
+
- low: −4
|
|
58
|
+
- skipped: no deduction
|
|
59
|
+
|
|
60
|
+
Skipped probes remain visible with the exact `--allow-tool` needed. They are not represented as passes.
|
|
61
|
+
|
|
62
|
+
## Reports
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
mcprigor audit tests/server.mcpr \
|
|
66
|
+
--pdf reports/security.pdf \
|
|
67
|
+
--json reports/security.json \
|
|
68
|
+
--csv reports/security.csv
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The rich PDF includes score, grade, severity cards, per-finding status, explanations, and bounded evidence. Markdown output is available with `--markdown`.
|
|
72
|
+
|
|
73
|
+
## Interpreting results
|
|
74
|
+
|
|
75
|
+
The audit is deterministic black-box evidence, not a certification or a replacement for threat modeling. A passing probe proves the observed server rejected that exact payload under that exact configuration. Review skipped probes, environment permissions, downstream model behavior, and business authorization separately.
|
|
76
|
+
|
|
77
|
+
Run intrusive tool probes only in an isolated environment. Do not expose production mutation tools through `--allow-tool`.
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"name": "keyword.control.version.mcpr"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
|
-
"match": "^\\s*(Suite|Server|Server options|MCP URL|Compare target|Target options for|Default timeout|Redact|Snapshots|Ignore snapshot paths|Client behavior|Import flows from|Flow|Inputs|Outputs|Test|Scenario|Id|Depends on|Require|Require protocol|Variables|Skip|Setup|Steps|Cleanup|Data source):?",
|
|
15
|
+
"match": "^\\s*(Suite|Server|Named server|Server options|Server options for|MCP URL|Compare target|Target options for|Default timeout|Budget|Redact|Snapshots|Ignore snapshot paths|Client behavior|Import flows from|Flow|Inputs|Outputs|Test|Scenario|Id|Depends on|Require|Require protocol|Variables|Skip|Setup|Steps|Cleanup|Data source):?",
|
|
16
16
|
"name": "keyword.control.mcpr"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
"match": "^\\s*(Call tool|When the server asks for input|When the server requests sampling|Read resource|Get prompt|Send|Expect|Save|Export|Set|Use flow|For each row|Wait up to|Wait for notification|Subscribe to resource|Unsubscribe from resource|Set log level|List all|Get task|List tasks|Cancel task|From CSV|From JSON|From YAML|From Excel|From REST|Column|Derive|Keep rows where|Sample|Cache this source)\\b",
|
|
19
|
+
"match": "^\\s*(Call tool|On server|When the server asks for input|When the server requests sampling|Read resource|Get prompt|Send|Expect|Save|Export|Set|Use flow|For each row|Wait up to|Wait for notification|Subscribe to resource|Unsubscribe from resource|Set log level|List all|Get task|List tasks|Cancel task|From CSV|From JSON|From YAML|From Excel|From REST|Column|Derive|Keep rows where|Sample|Cache this source)\\b",
|
|
20
20
|
"name": "keyword.other.mcpr"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "mcprigor",
|
|
3
3
|
"displayName": "MCP Rigor",
|
|
4
4
|
"description": "Syntax highlighting and inline validation for MCP Rigor (.mcpr) natural-language MCP test suites.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.4.0",
|
|
6
6
|
"publisher": "fusionone",
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
8
|
"repository": { "type": "git", "url": "https://github.com/FusionOnePlatform/mcprigor" },
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"name": "keyword.control.version.mcpr"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
|
-
"match": "^\\s*(Suite|Server|Server options|MCP URL|Compare target|Target options for|Default timeout|Redact|Snapshots|Ignore snapshot paths|Client behavior|Import flows from|Flow|Inputs|Outputs|Test|Scenario|Id|Depends on|Require|Require protocol|Variables|Skip|Setup|Steps|Cleanup|Data source):?",
|
|
15
|
+
"match": "^\\s*(Suite|Server|Named server|Server options|Server options for|MCP URL|Compare target|Target options for|Default timeout|Budget|Redact|Snapshots|Ignore snapshot paths|Client behavior|Import flows from|Flow|Inputs|Outputs|Test|Scenario|Id|Depends on|Require|Require protocol|Variables|Skip|Setup|Steps|Cleanup|Data source):?",
|
|
16
16
|
"name": "keyword.control.mcpr"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
"match": "^\\s*(Call tool|When the server asks for input|When the server requests sampling|Read resource|Get prompt|Send|Expect|Save|Export|Set|Use flow|For each row|Wait up to|Wait for notification|Subscribe to resource|Unsubscribe from resource|Set log level|List all|Get task|List tasks|Cancel task|From CSV|From JSON|From YAML|From Excel|From REST|Column|Derive|Keep rows where|Sample|Cache this source)\\b",
|
|
19
|
+
"match": "^\\s*(Call tool|On server|When the server asks for input|When the server requests sampling|Read resource|Get prompt|Send|Expect|Save|Export|Set|Use flow|For each row|Wait up to|Wait for notification|Subscribe to resource|Unsubscribe from resource|Set log level|List all|Get task|List tasks|Cancel task|From CSV|From JSON|From YAML|From Excel|From REST|Column|Derive|Keep rows where|Sample|Cache this source)\\b",
|
|
20
20
|
"name": "keyword.other.mcpr"
|
|
21
21
|
},
|
|
22
22
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcprigor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Plain-language MCP testing with isolated extensions and a local QA workspace",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "1.27.1",
|
|
49
49
|
"ajv": "^8.18.0",
|
|
50
|
-
"mcprigor": "^1.3.1",
|
|
51
50
|
"yaml": "^2.8.2"
|
|
52
51
|
},
|
|
53
52
|
"overrides": {
|
package/workspace-assets/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const state = { csrf: '', path: '', etag: '', dirty: false, files: [], selected: new Set(), run: null, history: [], tab: 'run', runSel: -1 };
|
|
1
|
+
const state = { csrf: '', path: '', etag: '', dirty: false, files: [], selected: new Set(), run: null, history: [], tab: 'run', runSel: -1, canPublish: false, publishing: false };
|
|
2
2
|
const $ = id => document.getElementById(id);
|
|
3
3
|
|
|
4
4
|
/* ---------- API ---------- */
|
|
@@ -14,6 +14,7 @@ async function api(path, options = {}) {
|
|
|
14
14
|
async function start() {
|
|
15
15
|
const boot = await api('/api/v1/bootstrap');
|
|
16
16
|
state.csrf = boot.csrf;
|
|
17
|
+
state.canPublish = (boot.capabilities || []).includes('publish');
|
|
17
18
|
$('version').textContent = `v${boot.version}`;
|
|
18
19
|
$('workspace').textContent = `Folder: ${boot.root}`;
|
|
19
20
|
$('connection').classList.add('ok');
|
|
@@ -613,16 +614,46 @@ start().catch(show);
|
|
|
613
614
|
function updateExportBar() {
|
|
614
615
|
const run = state.run;
|
|
615
616
|
const bar = $('export-run');
|
|
616
|
-
const index = state.runSel
|
|
617
|
+
const index = state.runSel >= 0 ? state.runSel : 0;
|
|
617
618
|
const item = run && run.items && run.items[index];
|
|
618
|
-
|
|
619
|
+
const ready = run && run.mode === 'test' && item && item.status !== 'running' && item.tests;
|
|
620
|
+
bar.hidden = !ready;
|
|
621
|
+
const publish = $('publish-run');
|
|
622
|
+
const link = $('published-link');
|
|
623
|
+
publish.hidden = !(ready && state.canPublish);
|
|
624
|
+
publish.disabled = state.publishing;
|
|
625
|
+
publish.textContent = state.publishing ? 'Publishing…' : (item && item.publishedUrl ? 'Publish again' : 'Publish');
|
|
626
|
+
link.hidden = !(ready && item.publishedUrl);
|
|
627
|
+
if (item && item.publishedUrl) link.href = item.publishedUrl;
|
|
619
628
|
}
|
|
620
629
|
function exportRun(format) {
|
|
621
630
|
const run = state.run;
|
|
622
631
|
if (!run) return;
|
|
623
|
-
const index = state.runSel
|
|
632
|
+
const index = state.runSel >= 0 ? state.runSel : 0;
|
|
624
633
|
window.open(`/api/v1/export/run?id=${encodeURIComponent(run.id)}&item=${index}&format=${format}`, '_blank');
|
|
625
634
|
}
|
|
635
|
+
async function publishRun() {
|
|
636
|
+
const run = state.run;
|
|
637
|
+
if (!run || state.publishing) return;
|
|
638
|
+
const index = state.runSel >= 0 ? state.runSel : 0;
|
|
639
|
+
state.publishing = true; updateExportBar();
|
|
640
|
+
try {
|
|
641
|
+
const value = await api('/api/v1/publish', { method: 'POST', body: JSON.stringify({ id: run.id, item: index }) });
|
|
642
|
+
const item = run.items[index];
|
|
643
|
+
if (item) item.publishedUrl = value.url;
|
|
644
|
+
window.open(value.url, '_blank', 'noopener');
|
|
645
|
+
} catch (error) {
|
|
646
|
+
toast(error.message || 'Publish failed', true);
|
|
647
|
+
} finally {
|
|
648
|
+
state.publishing = false; updateExportBar();
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
$('export-run-html').onclick = () => {
|
|
652
|
+
const run = state.run;
|
|
653
|
+
if (!run) return;
|
|
654
|
+
window.open(`/api/v1/report?id=${encodeURIComponent(run.id)}&item=${state.runSel >= 0 ? state.runSel : 0}`, '_blank');
|
|
655
|
+
};
|
|
656
|
+
$('publish-run').onclick = publishRun;
|
|
626
657
|
$('export-run-pdf').onclick = () => exportRun('pdf');
|
|
627
658
|
$('export-run-csv').onclick = () => exportRun('csv');
|
|
628
659
|
$('export-run-junit').onclick = () => exportRun('junit');
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
<button id="batch-clear" class="ghost small">Clear selection</button>
|
|
39
39
|
</div>
|
|
40
40
|
<div class="sidebar-foot">
|
|
41
|
-
<a href="https://mcprigor.com/docs/
|
|
41
|
+
<a href="https://mcprigor.com/docs/natural-language-cookbook.html" target="_blank" rel="noreferrer">📖 Cookbook — copy-ready examples</a>
|
|
42
42
|
</div>
|
|
43
43
|
</aside>
|
|
44
44
|
<div class="splitter" id="split-left" role="separator" aria-orientation="vertical" aria-label="Resize file list" tabindex="0"></div>
|
|
@@ -95,6 +95,9 @@
|
|
|
95
95
|
<button id="export-run-pdf" class="ghost small" type="button">PDF</button>
|
|
96
96
|
<button id="export-run-csv" class="ghost small" type="button">CSV</button>
|
|
97
97
|
<button id="export-run-junit" class="ghost small" type="button">JUnit XML</button>
|
|
98
|
+
<button id="export-run-html" class="ghost small" type="button">HTML report</button>
|
|
99
|
+
<button id="publish-run" class="ghost small" type="button" hidden>Publish</button>
|
|
100
|
+
<a id="published-link" class="published-link" target="_blank" rel="noopener" hidden>View published ↗</a>
|
|
98
101
|
</div>
|
|
99
102
|
<div id="run-list"></div>
|
|
100
103
|
<pre id="output" class="output"><span class="muted">Nothing has run yet.
|
|
@@ -232,3 +232,5 @@ nav .suite-row button .rn:hover{opacity:1}
|
|
|
232
232
|
.export-bar{display:flex;align-items:center;gap:6px;padding:8px 14px;border-bottom:1px solid var(--line-soft)}
|
|
233
233
|
.export-label{font-size:11px;color:var(--text-dim);margin-right:2px}
|
|
234
234
|
.export-bar .small{font-size:11px;padding:3px 10px}
|
|
235
|
+
.published-link{font-size:11px;margin-left:4px;color:var(--accent, #2563eb);text-decoration:none}
|
|
236
|
+
.published-link:hover{text-decoration:underline}
|