Integrate reviewed least-privilege token guidance

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-04 06:46:12 +01:00
5 changed files with 372 additions and 62 deletions
+76 -31
View File
@@ -664,19 +664,36 @@ docker run --init -e HTTPS_ENABLED=true \
## 🔑 API Tokens
API tokens provide scoped, revocable access to Pulse. Manage tokens in **Settings → Security → API Tokens**.
API tokens provide scoped, revocable access to Pulse. Manage them in
**Settings → API Access**, in the **Security** group. An account without token
management permission can review the inventory but cannot create or revoke tokens.
The token shown during first-run setup is the primary automation API token for
that Pulse instance. It is separate from your web login password and is meant
for agents, scripts, integrations, kiosks, and temporary setup handoffs. Tokens
are shown once; later token rows show only identifying hints such as prefix,
suffix, label, scopes, and last-used metadata.
that Pulse instance, not your web login password. Do not reuse a full-access
setup token for every consumer. Give each agent, script, integration or display
only the scopes it needs, with a name that identifies its use. Tokens are shown
once; later rows show hints and metadata, not a recoverable secret. Store the
secret privately before dismissing it.
Revoking a token is safe for Pulse itself, but it immediately breaks any agent,
script, kiosk, or integration still using that token. When a consumer needs to
stay online, create and install a replacement token first, then revoke the old
one. An agent whose token has been revoked stops authenticating until it is
reinstalled or reconfigured with a valid token.
### Replace or revoke a token
Revoking a token stops consumers still using it from authenticating. For a
planned rotation, create a least-privilege replacement, install it privately
in each affected consumer and verify a fresh authenticated result before
revoking the old token. Last-used metadata alone does not account for every
consumer. Do not reinstall an agent or delete its saved identity just to
replace a credential; use its private token-file or managed configuration.
**If a token or kiosk link has been exposed, revoke it promptly**, even if that
interrupts monitoring or a display. Replace it through the same private setup
path. Do not leave a leaked token active while arranging a gradual rotation.
For a server-address change without credential exposure, follow
[agent retargeting](UNIFIED_AGENT.md#moving-pulse-to-a-new-address) instead.
Never put a token or session cookie in a diagnostic command, screenshot or
issue thread. For scripted API access, use the
[private header-file procedure](API.md#api-token-recommended), keep TLS
verification enabled and do not share verbose or trace output.
### Token Scopes
@@ -684,53 +701,81 @@ reinstalled or reconfigured with a valid token.
| ------- | ------------- |
| `*` (Full access) | All permissions (legacy, not recommended) |
| `monitoring:read` | View dashboards, metrics, alerts |
| `monitoring:write` | Acknowledge/silence alerts |
| `monitoring:write` | Acknowledge, silence and clear alerts |
| `docker:report` | Docker / Podman agent telemetry submission |
| `docker:manage` | Docker / Podman container lifecycle actions (restart, stop) |
| `kubernetes:report` | Kubernetes agent telemetry submission |
| `kubernetes:manage` | Kubernetes cluster management |
| `agent:report` | Agent host telemetry submission |
| `agent:config:read` | Read agent config payloads |
| `agent:manage` | Manage registered agents (unlink/delete/config) |
| `agent:exec` | Establish agent command WebSocket connections |
| `agent:manage` | Agent lifecycle/configuration changes and unregistering; not needed by the reporting preset |
| `agent:exec` | Establish agent command WebSocket connections; not a reporting permission |
| `ai:chat` | Use Pulse Assistant chat and read knowledge |
| `ai:execute` | Use governed Patrol plans, approvals, actions, and history |
| `settings:read` | Read configuration |
| `settings:write` | Modify configuration |
| `settings:write` | Modify configuration, manage tokens and trigger updates |
| `audit:read` | Read audit events, verification results, summaries, and exports |
### Presets
The UI offers quick presets for common use cases:
Use **New token**, enter a name under **Create token**, select a **Quick preset**
and choose **Generate**. The current presets are:
| Preset | Scopes | Use Case |
| -------- | -------- | ---------- |
| **Kiosk / Dashboard** | `monitoring:read` | Read-only dashboard displays |
| **Agent host** | `agent:report`, `agent:config:read`, `agent:manage` | Agent telemetry, configuration fetch, and uninstall cleanup |
| **Kiosk / Monitoring** | `monitoring:read` | Read-only dashboard displays |
| **Agent** | `agent:report`, `agent:config:read` | Host telemetry and the agent's bound configuration |
| **Docker / Podman report** | `docker:report` | Docker / Podman agent (read-only) |
| **Docker / Podman manage** | `docker:report`, `docker:manage` | Docker / Podman agent with actions |
| **Settings read** | `settings:read` | Read-only config access |
| **Settings admin** | `settings:read`, `settings:write` | Full config access |
| **Audit read** | `audit:read` | Audit events, verification history, summaries and exports |
**Reporting is not remote execution.** The Agent preset does not grant
`agent:manage` or `agent:exec`; Docker / Podman report does not grant
`docker:manage`. Add lifecycle or action authority only for the operation you
intend, not to fix a missing reading. A settings-read token can read sensitive
configuration and diagnostics; it is not a public-display credential. See
[agent security](AGENT_SECURITY.md) for the separate collector and action-runner
requirements.
If **Patrol external agent** is offered, its scopes come from the current Patrol
requirements. Use the displayed set rather than guessing or adding **Full access**.
The separate **Full access** choice grants the legacy `*` wildcard, not a
least-privilege preset. Hiding controls does not reduce a token's permissions.
### Kiosk Mode
For unattended displays (wall monitors, dashboards), use a kiosk token to avoid cookie persistence issues:
For a wall monitor, create a dedicated **Kiosk / Monitoring** token with only
`monitoring:read`. Never use **Full access**, a settings token or your
administrator's logged-in browser profile on an unattended display.
1. Go to **Settings → Security → API Tokens**
2. Click **New token** and select the **Kiosk / Dashboard** preset
3. Copy the generated token
4. Access Pulse via URL with token:
```text
https://your-pulse-url/?token=YOUR_TOKEN_HERE
```
1. Open Pulse at its trusted **HTTPS** address and go to **Settings → API Access**.
2. Use **New token**, name the display, select **Kiosk / Monitoring** and
choose **Generate**. Store the secret privately.
3. After closing the token-reveal dialog, the **Magic Kiosk Link** offers
**Copy Link** for this monitoring-only token. Transfer that link privately
to the display; do not put it in shared bookmarks, screenshots or reports.
4. Check the display using the link in a separate browser profile with no
administrator session. Confirm it shows the intended monitoring data and
does not offer settings or write actions. A hidden control alone is not
proof of restricted access.
**Kiosk tokens:**
- Grant read-only dashboard access (`monitoring:read` scope)
- Hide the Settings tab automatically
- Work without cookies (token in URL)
- Can be revoked anytime from the UI
The link is a **bearer credential**: anyone with it can read the permitted
infrastructure data until the token expires or is revoked. The `kiosk=1`
option only hides navigation and filters; it does not authenticate a browser
or change the token's scopes.
> **Security note**: URL tokens appear in browser history and server logs. Use only for read-only dashboard access on trusted networks.
Pulse removes the token from the page URL after loading and keeps it in that
tab's session storage. **The initial request already carried the token**;
HTTPS and the cleaned address bar do not remove copies from proxy/server logs,
browser history or the clipboard. Do not rely on this session surviving a
closed tab, browser restart or cleared site data. Keep the original link private
if the display needs it again, and clear clipboard copies after setup.
If the display or link is lost or exposed, revoke its dedicated token in
**API Access** and create a replacement. Revoking a display token should not
require rotating unrelated agents' credentials.
---
@@ -24,6 +24,7 @@ const targets: Record<string, string> = {
troubleshooting: '/docs/TROUBLESHOOTING#recovery-mode',
truenas: '/docs/TRUENAS',
installation: '/docs/INSTALL',
configuration: '/docs/CONFIGURATION#-api-tokens',
};
window.history.replaceState({}, '', targets[scenario] ?? targets.plain);
@@ -0,0 +1,119 @@
// Inspect the existing shipped help using the production Docs renderer.
// This creates no token, authenticates no client and performs no rotation.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { chromium, webkit } = require('playwright');
(async () => {
const root = '/workspace/frontend-modern';
const artifacts = path.join(root, 'node_modules', 'token-guidance-browser');
fs.mkdirSync(artifacts, { recursive: true });
const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js'));
const server = await createServer({
root,
configFile: path.join(root, 'vite.config.ts'),
cacheDir: path.join(root, 'node_modules', '.vite-token-guidance'),
server: { host: '127.0.0.1', port: 5253, strictPort: true },
optimizeDeps: { noDiscovery: true, include: [] },
});
const results = [];
let browser;
try {
await server.listen();
for (const [engine, width] of [['chromium', 1280], ['webkit', 390]]) {
browser = await { chromium, webkit }[engine].launch(engine === 'chromium'
? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
: { headless: true });
const page = await browser.newPage({ viewport: { width, height: 1000 },
...(engine === 'webkit' ? { isMobile: true, hasTouch: true } : {}) });
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
page.on('console', (message) => {
if (message.type() === 'error') errors.push(message.text());
});
await page.goto('http://127.0.0.1:5253/browser-tests/docs-fragment-navigation.html?scenario=configuration',
{ waitUntil: 'domcontentloaded', timeout: 60_000 });
const tokens = page.getByRole('heading', { name: '🔑 API Tokens', exact: true });
await tokens.waitFor();
await page.waitForFunction(() => document.activeElement?.id === '-api-tokens');
if (engine === 'webkit') {
await page.evaluate(async () => {
document.documentElement.classList.add('dark');
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
await Promise.all(document.getAnimations().map((animation) => animation.finished.catch(() => {})));
});
}
const presetTable = page.getByRole('table').filter({ has: page.getByRole('columnheader',
{ name: 'Preset', exact: true }) });
const agent = presetTable.getByRole('row').filter({ has: page.getByRole('cell',
{ name: 'Agent', exact: true }) });
assert.equal(await agent.getByRole('cell').nth(1).innerText(), 'agent:report, agent:config:read');
assert.equal(await presetTable.getByRole('row').count(), 8);
const scopesTable = page.getByRole('table').filter({ has: page.getByRole('columnheader',
{ name: 'Scope', exact: true }) });
assert.equal(await scopesTable.getByRole('row').count(), 17);
for (const table of [presetTable, scopesTable]) {
assert.ok(await table.evaluate((element) => {
const parent = element.parentElement;
return parent.dataset.docTableScroll !== undefined &&
getComputedStyle(parent).overflowX === 'auto' &&
[...element.querySelectorAll('thead th')].every((header) => header.scope === 'col');
}));
}
const body = (await page.locator('article').innerText()).replace(/\s+/g, ' ');
for (const text of ['Settings → API Access', 'Reporting is not remote execution',
'not to fix a missing reading', 'revoke it promptly', 'even if that interrupts monitoring',
'Magic Kiosk Link', 'Copy Link', 'no administrator session', 'bearer credential',
'initial request already carried the token', 'proxy/server logs',
'Do not rely on this session surviving']) assert.ok(body.includes(text), text);
assert.ok(!body.includes('?token=YOUR_TOKEN_HERE'));
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
for (const [name, heading] of [
['rotation', tokens],
['presets', page.getByRole('heading', { name: 'Presets', exact: true })],
['kiosk', page.getByRole('heading', { name: 'Kiosk Mode', exact: true })],
]) {
await heading.evaluate((element) => element.scrollIntoView({ block: 'start' }));
await page.screenshot({ path: path.join(artifacts, `${engine}-${name}.png`) });
}
for (const [name, href, heading, id] of [
['private header-file procedure', '/docs/API#api-token-recommended',
'API Token (Recommended)', 'api-token-recommended'],
['agent retargeting', '/docs/UNIFIED_AGENT#moving-pulse-to-a-new-address',
'Moving Pulse to a new address', 'moving-pulse-to-a-new-address'],
]) {
const link = page.getByRole('link', { name, exact: true });
assert.equal(await link.getAttribute('href'), href);
await link.focus();
await page.keyboard.press('Enter');
await page.waitForURL(`**${href}`);
await page.getByRole('heading', { name: heading, exact: true }).waitFor();
assert.equal(await page.evaluate(() => document.activeElement?.id), id);
await page.goBack();
await tokens.waitFor();
}
assert.deepEqual(errors, []);
results.push({ engine, version: browser.version(), width,
tokenAnchorFocus: true, leastPrivilegeAgentRow: true, actualPresetCount: 7,
scopeCount: 16, semanticContainedTables: true, noDocumentOverflow: true,
kioskCredentialWarning: true, keyboardApiHelp: true, keyboardAgentRetargeting: true, errors });
await browser.close();
browser = undefined;
}
const files = ['public/docs/CONFIGURATION.md', 'browser-tests/token-guidance.cjs',
'browser-tests/docs-fragment-navigation.tsx', 'src/features/docs/docMarkdown.ts',
'src/pages/Docs.tsx'].map((file) => ({ file,
sha256: crypto.createHash('sha256').update(fs.readFileSync(path.join(root, file))).digest('hex') }));
const result = { result: 'passed', playwright: require('playwright/package.json').version,
scope: 'shipped token guidance rendering and keyboard help links; no token/native operation performed',
dompurify: JSON.parse(fs.readFileSync(path.join(root, 'node_modules/dompurify/package.json'))).version,
files, results };
fs.writeFileSync(path.join(artifacts, 'result.json'), JSON.stringify(result, null, 2) + '\n');
console.log(JSON.stringify(result));
} finally {
if (browser) await browser.close();
await server.close();
}
})().catch((error) => { console.error(error); process.exitCode = 1; });
+76 -31
View File
@@ -664,19 +664,36 @@ docker run --init -e HTTPS_ENABLED=true \
## 🔑 API Tokens
API tokens provide scoped, revocable access to Pulse. Manage tokens in **Settings → Security → API Tokens**.
API tokens provide scoped, revocable access to Pulse. Manage them in
**Settings → API Access**, in the **Security** group. An account without token
management permission can review the inventory but cannot create or revoke tokens.
The token shown during first-run setup is the primary automation API token for
that Pulse instance. It is separate from your web login password and is meant
for agents, scripts, integrations, kiosks, and temporary setup handoffs. Tokens
are shown once; later token rows show only identifying hints such as prefix,
suffix, label, scopes, and last-used metadata.
that Pulse instance, not your web login password. Do not reuse a full-access
setup token for every consumer. Give each agent, script, integration or display
only the scopes it needs, with a name that identifies its use. Tokens are shown
once; later rows show hints and metadata, not a recoverable secret. Store the
secret privately before dismissing it.
Revoking a token is safe for Pulse itself, but it immediately breaks any agent,
script, kiosk, or integration still using that token. When a consumer needs to
stay online, create and install a replacement token first, then revoke the old
one. An agent whose token has been revoked stops authenticating until it is
reinstalled or reconfigured with a valid token.
### Replace or revoke a token
Revoking a token stops consumers still using it from authenticating. For a
planned rotation, create a least-privilege replacement, install it privately
in each affected consumer and verify a fresh authenticated result before
revoking the old token. Last-used metadata alone does not account for every
consumer. Do not reinstall an agent or delete its saved identity just to
replace a credential; use its private token-file or managed configuration.
**If a token or kiosk link has been exposed, revoke it promptly**, even if that
interrupts monitoring or a display. Replace it through the same private setup
path. Do not leave a leaked token active while arranging a gradual rotation.
For a server-address change without credential exposure, follow
[agent retargeting](UNIFIED_AGENT.md#moving-pulse-to-a-new-address) instead.
Never put a token or session cookie in a diagnostic command, screenshot or
issue thread. For scripted API access, use the
[private header-file procedure](API.md#api-token-recommended), keep TLS
verification enabled and do not share verbose or trace output.
### Token Scopes
@@ -684,53 +701,81 @@ reinstalled or reconfigured with a valid token.
| ------- | ------------- |
| `*` (Full access) | All permissions (legacy, not recommended) |
| `monitoring:read` | View dashboards, metrics, alerts |
| `monitoring:write` | Acknowledge/silence alerts |
| `monitoring:write` | Acknowledge, silence and clear alerts |
| `docker:report` | Docker / Podman agent telemetry submission |
| `docker:manage` | Docker / Podman container lifecycle actions (restart, stop) |
| `kubernetes:report` | Kubernetes agent telemetry submission |
| `kubernetes:manage` | Kubernetes cluster management |
| `agent:report` | Agent host telemetry submission |
| `agent:config:read` | Read agent config payloads |
| `agent:manage` | Manage registered agents (unlink/delete/config) |
| `agent:exec` | Establish agent command WebSocket connections |
| `agent:manage` | Agent lifecycle/configuration changes and unregistering; not needed by the reporting preset |
| `agent:exec` | Establish agent command WebSocket connections; not a reporting permission |
| `ai:chat` | Use Pulse Assistant chat and read knowledge |
| `ai:execute` | Use governed Patrol plans, approvals, actions, and history |
| `settings:read` | Read configuration |
| `settings:write` | Modify configuration |
| `settings:write` | Modify configuration, manage tokens and trigger updates |
| `audit:read` | Read audit events, verification results, summaries, and exports |
### Presets
The UI offers quick presets for common use cases:
Use **New token**, enter a name under **Create token**, select a **Quick preset**
and choose **Generate**. The current presets are:
| Preset | Scopes | Use Case |
| -------- | -------- | ---------- |
| **Kiosk / Dashboard** | `monitoring:read` | Read-only dashboard displays |
| **Agent host** | `agent:report`, `agent:config:read`, `agent:manage` | Agent telemetry, configuration fetch, and uninstall cleanup |
| **Kiosk / Monitoring** | `monitoring:read` | Read-only dashboard displays |
| **Agent** | `agent:report`, `agent:config:read` | Host telemetry and the agent's bound configuration |
| **Docker / Podman report** | `docker:report` | Docker / Podman agent (read-only) |
| **Docker / Podman manage** | `docker:report`, `docker:manage` | Docker / Podman agent with actions |
| **Settings read** | `settings:read` | Read-only config access |
| **Settings admin** | `settings:read`, `settings:write` | Full config access |
| **Audit read** | `audit:read` | Audit events, verification history, summaries and exports |
**Reporting is not remote execution.** The Agent preset does not grant
`agent:manage` or `agent:exec`; Docker / Podman report does not grant
`docker:manage`. Add lifecycle or action authority only for the operation you
intend, not to fix a missing reading. A settings-read token can read sensitive
configuration and diagnostics; it is not a public-display credential. See
[agent security](AGENT_SECURITY.md) for the separate collector and action-runner
requirements.
If **Patrol external agent** is offered, its scopes come from the current Patrol
requirements. Use the displayed set rather than guessing or adding **Full access**.
The separate **Full access** choice grants the legacy `*` wildcard, not a
least-privilege preset. Hiding controls does not reduce a token's permissions.
### Kiosk Mode
For unattended displays (wall monitors, dashboards), use a kiosk token to avoid cookie persistence issues:
For a wall monitor, create a dedicated **Kiosk / Monitoring** token with only
`monitoring:read`. Never use **Full access**, a settings token or your
administrator's logged-in browser profile on an unattended display.
1. Go to **Settings → Security → API Tokens**
2. Click **New token** and select the **Kiosk / Dashboard** preset
3. Copy the generated token
4. Access Pulse via URL with token:
```text
https://your-pulse-url/?token=YOUR_TOKEN_HERE
```
1. Open Pulse at its trusted **HTTPS** address and go to **Settings → API Access**.
2. Use **New token**, name the display, select **Kiosk / Monitoring** and
choose **Generate**. Store the secret privately.
3. After closing the token-reveal dialog, the **Magic Kiosk Link** offers
**Copy Link** for this monitoring-only token. Transfer that link privately
to the display; do not put it in shared bookmarks, screenshots or reports.
4. Check the display using the link in a separate browser profile with no
administrator session. Confirm it shows the intended monitoring data and
does not offer settings or write actions. A hidden control alone is not
proof of restricted access.
**Kiosk tokens:**
- Grant read-only dashboard access (`monitoring:read` scope)
- Hide the Settings tab automatically
- Work without cookies (token in URL)
- Can be revoked anytime from the UI
The link is a **bearer credential**: anyone with it can read the permitted
infrastructure data until the token expires or is revoked. The `kiosk=1`
option only hides navigation and filters; it does not authenticate a browser
or change the token's scopes.
> **Security note**: URL tokens appear in browser history and server logs. Use only for read-only dashboard access on trusted networks.
Pulse removes the token from the page URL after loading and keeps it in that
tab's session storage. **The initial request already carried the token**;
HTTPS and the cleaned address bar do not remove copies from proxy/server logs,
browser history or the clipboard. Do not rely on this session surviving a
closed tab, browser restart or cleared site data. Keep the original link private
if the display needs it again, and clear clipboard copies after setup.
If the display or link is lost or exposed, revoke its dedicated token in
**API Access** and create a replacement. Revoking a display token should not
require rotating unrelated agents' credentials.
---
@@ -0,0 +1,100 @@
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { API_SCOPE_OPTIONS } from '@/constants/apiScopes';
import { getAPITokenScopePresets } from '@/components/Settings/apiTokenManagerModel';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../..');
const configuration = readFileSync(path.join(repoRoot, 'docs/CONFIGURATION.md'), 'utf8');
const tokens = configuration.split('## 🔑 API Tokens')[1]?.split('\n## TrueNAS')[0] ?? '';
const normalized = tokens.replace(/\s+/g, ' ');
function tableRows(section: string) {
return section
.split('\n')
.filter((line) => line.startsWith('|') && !/^\|\s*-/.test(line))
.slice(1)
.map((line) =>
line
.split('|')
.slice(1, -1)
.map((cell) => cell.trim()),
);
}
describe('shipped API Access guidance', () => {
it('names the existing token controls rather than the retired navigation', () => {
expect(normalized).toContain('Settings → API Access');
expect(normalized).not.toContain('Security → API Tokens');
for (const control of ['New token', 'Create token', 'Quick preset', 'Generate']) {
expect(normalized).toContain(control);
}
expect(normalized).toContain('cannot create or revoke tokens');
});
it('documents every UI scope without inventing capabilities', () => {
const scopeSection = tokens.split('### Token Scopes')[1]?.split('### Presets')[0] ?? '';
const scopes = tableRows(scopeSection).map(([scope]) => scope.match(/`([^`]+)`/)?.[1]);
expect(scopes.sort()).toEqual(['*', ...API_SCOPE_OPTIONS.map((scope) => scope.value)].sort());
});
it('matches names and exact least-privilege scope sets to the actual preset model', () => {
const presetSection = tokens.split('### Presets')[1]?.split('### Kiosk Mode')[0] ?? '';
const documented = tableRows(presetSection).map(([label, scopes]) => ({
label: label.replace(/\*\*/g, ''),
scopes: [...scopes.matchAll(/`([^`]+)`/g)].map((match) => match[1]),
}));
expect(documented).toEqual(
getAPITokenScopePresets().map(({ label, scopes }) => ({ label, scopes })),
);
expect(normalized).toContain('Reporting is not remote execution');
expect(normalized).toContain('not to fix a missing reading');
expect(normalized).toContain('separate collector and action-runner');
});
it('distinguishes backend-required Patrol scopes and wildcard authority from presets', () => {
expect(normalized).toContain('its scopes come from the current Patrol requirements');
expect(normalized).toContain('legacy `*` wildcard, not a least-privilege preset');
expect(normalized).toContain("Hiding controls does not reduce a token's permissions");
});
it('keeps credential rotation separate from identity deletion and exposure response', () => {
const rotation =
tokens.split('### Replace or revoke a token')[1]?.split('### Token Scopes')[0] ?? '';
const text = rotation.replace(/\s+/g, ' ');
expect(text).toContain('fresh authenticated result before revoking the old token');
expect(text).toContain('Last-used metadata alone does not account for every consumer');
expect(text).toContain('Do not reinstall an agent or delete its saved identity');
expect(text).toContain('revoke it promptly');
expect(text).toContain('even if that interrupts monitoring or a display');
expect(text).toContain('Do not leave a leaked token active');
expect(text).toContain('[agent retargeting](UNIFIED_AGENT.md#moving-pulse-to-a-new-address)');
expect(text).toContain('[private header-file procedure](API.md#api-token-recommended)');
});
it('does not present a hidden dashboard or cleaned URL as a credential boundary', () => {
const kiosk = tokens.split('### Kiosk Mode')[1] ?? '';
const text = kiosk.replace(/\s+/g, ' ');
expect(text).toContain('Kiosk / Monitoring');
expect(text).toContain('only `monitoring:read`');
expect(text).toContain('Magic Kiosk Link');
expect(text).toContain('Copy Link');
expect(text).toContain('HTTPS');
expect(text).toContain('no administrator session');
expect(text).toContain('bearer credential');
expect(text).toContain('it does not authenticate a browser or change');
expect(text).toContain('initial request already carried the token');
expect(text).toContain('proxy/server logs');
expect(text).toContain('session storage');
expect(text).toContain('Do not rely on this session surviving');
expect(text).not.toContain('?token=YOUR_TOKEN_HERE');
expect(text).not.toContain('avoid cookie persistence issues');
});
it('ships the same scope guidance the UI links to', () => {
expect(
readFileSync(path.join(repoRoot, 'frontend-modern/public/docs/CONFIGURATION.md'), 'utf8'),
).toBe(configuration);
});
});