#191: feeds.enabled instance switch + feed-token log masking #242
@ -7,6 +7,7 @@ import { AuditModule } from './audit/audit.module';
|
|||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { BackupModule } from './backup/backup.module';
|
import { BackupModule } from './backup/backup.module';
|
||||||
import { ApiExceptionFilter } from './common/api-exception.filter';
|
import { ApiExceptionFilter } from './common/api-exception.filter';
|
||||||
|
import { maskTokenParam } from './common/mask-token-param';
|
||||||
import { CommentsModule } from './comments/comments.module';
|
import { CommentsModule } from './comments/comments.module';
|
||||||
import { CompactionModule } from './compaction/compaction.module';
|
import { CompactionModule } from './compaction/compaction.module';
|
||||||
import { AppConfig } from './config/app-config.service';
|
import { AppConfig } from './config/app-config.service';
|
||||||
@ -91,6 +92,11 @@ import { VersionsModule } from './versions/versions.module';
|
|||||||
autoLogging: config.env.NODE_ENV !== 'test',
|
autoLogging: config.env.NODE_ENV !== 'test',
|
||||||
// Request bodies are never logged (operations.md logging rules).
|
// Request bodies are never logged (operations.md logging rules).
|
||||||
redact: { paths: ['req.headers.authorization', 'req.headers.cookie'], remove: true },
|
redact: { paths: ['req.headers.authorization', 'req.headers.cookie'], remove: true },
|
||||||
|
// Feed tokens travel as `?token=` (issue #191) — mask them so the
|
||||||
|
// request log never stores the credential.
|
||||||
|
serializers: {
|
||||||
|
req: (req: { url?: string }) => ({ ...req, url: maskTokenParam(req.url) }),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
25
apps/api/src/common/mask-token-param.test.ts
Normal file
25
apps/api/src/common/mask-token-param.test.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { maskTokenParam } from './mask-token-param';
|
||||||
|
|
||||||
|
describe('maskTokenParam (issue #191)', () => {
|
||||||
|
it('masks a token as the only query parameter', () => {
|
||||||
|
expect(maskTokenParam('/api/v1/public/p/feed.xml?token=dt_feed_abc123')).toBe(
|
||||||
|
'/api/v1/public/p/feed.xml?token=[redacted]',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('masks a token between other parameters and stops at delimiters', () => {
|
||||||
|
expect(maskTokenParam('/x?a=1&token=secret&b=2')).toBe('/x?a=1&token=[redacted]&b=2');
|
||||||
|
expect(maskTokenParam('/x?token=secret#frag')).toBe('/x?token=[redacted]#frag');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves URLs without a token parameter untouched', () => {
|
||||||
|
expect(maskTokenParam('/api/v1/ponds?filter=token')).toBe('/api/v1/ponds?filter=token');
|
||||||
|
expect(maskTokenParam('/api/v1/readyz')).toBe('/api/v1/readyz');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes undefined through', () => {
|
||||||
|
expect(maskTokenParam(undefined)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
9
apps/api/src/common/mask-token-param.ts
Normal file
9
apps/api/src/common/mask-token-param.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Masks credential-bearing `token` query parameters before a URL reaches
|
||||||
|
* the request log (issue #191): feed tokens travel in the query string
|
||||||
|
* because feed readers cannot send headers, and the api's own log must
|
||||||
|
* not become the place where that long-lived credential is stored.
|
||||||
|
*/
|
||||||
|
export function maskTokenParam<T extends string | undefined>(url: T): T {
|
||||||
|
return url?.replace(/([?&]token=)[^&#]*/gi, '$1[redacted]') as T;
|
||||||
|
}
|
||||||
@ -1,4 +1,14 @@
|
|||||||
import { Body, Controller, Delete, Get, HttpCode, Param, Post, Req } from '@nestjs/common';
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
NotFoundException,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Req,
|
||||||
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
createFeedTokenInputSchema,
|
createFeedTokenInputSchema,
|
||||||
type CreateFeedTokenInput,
|
type CreateFeedTokenInput,
|
||||||
@ -9,6 +19,7 @@ import {
|
|||||||
import { AuthedRequest } from '../auth/auth.guard';
|
import { AuthedRequest } from '../auth/auth.guard';
|
||||||
import { ZodValidationPipe } from '../common/zod-validation.pipe';
|
import { ZodValidationPipe } from '../common/zod-validation.pipe';
|
||||||
import { AuthenticatedOnly } from '../permissions/permission.decorators';
|
import { AuthenticatedOnly } from '../permissions/permission.decorators';
|
||||||
|
import { InstanceSettingsService } from '../settings/instance-settings.service';
|
||||||
import { FeedTokensService } from './feed-tokens.service';
|
import { FeedTokensService } from './feed-tokens.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -18,24 +29,35 @@ import { FeedTokensService } from './feed-tokens.service';
|
|||||||
@Controller('users/me/feed-tokens')
|
@Controller('users/me/feed-tokens')
|
||||||
@AuthenticatedOnly()
|
@AuthenticatedOnly()
|
||||||
export class FeedTokensController {
|
export class FeedTokensController {
|
||||||
constructor(private readonly tokens: FeedTokensService) {}
|
constructor(
|
||||||
|
private readonly tokens: FeedTokensService,
|
||||||
|
private readonly settings: InstanceSettingsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** Feed master switch (issue #191): disabled ⇒ the surface answers 404. */
|
||||||
|
private async assertFeedsEnabled(): Promise<void> {
|
||||||
|
if (!(await this.settings.get('feeds.enabled'))) throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
list(@Req() request: AuthedRequest): Promise<FeedTokenView[]> {
|
async list(@Req() request: AuthedRequest): Promise<FeedTokenView[]> {
|
||||||
|
await this.assertFeedsEnabled();
|
||||||
return this.tokens.list(request.user!);
|
return this.tokens.list(request.user!);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
create(
|
async create(
|
||||||
@Body(new ZodValidationPipe(createFeedTokenInputSchema)) input: CreateFeedTokenInput,
|
@Body(new ZodValidationPipe(createFeedTokenInputSchema)) input: CreateFeedTokenInput,
|
||||||
@Req() request: AuthedRequest,
|
@Req() request: AuthedRequest,
|
||||||
): Promise<FeedTokenCreatedView> {
|
): Promise<FeedTokenCreatedView> {
|
||||||
|
await this.assertFeedsEnabled();
|
||||||
return this.tokens.create(request.user!, input);
|
return this.tokens.create(request.user!, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@HttpCode(204)
|
@HttpCode(204)
|
||||||
async remove(@Param('id') id: string, @Req() request: AuthedRequest): Promise<void> {
|
async remove(@Param('id') id: string, @Req() request: AuthedRequest): Promise<void> {
|
||||||
|
await this.assertFeedsEnabled();
|
||||||
await this.tokens.remove(request.user!, id);
|
await this.tokens.remove(request.user!, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { PrismaClient } from '@prisma/client';
|
|||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { InstanceSettingsService } from '../settings/instance-settings.service';
|
||||||
import { createTestApp, sessionCookieOf } from '../testing/test-app';
|
import { createTestApp, sessionCookieOf } from '../testing/test-app';
|
||||||
import { createTestPrisma, hasTestDb, uniqueSuffix } from '../testing/test-db';
|
import { createTestPrisma, hasTestDb, uniqueSuffix } from '../testing/test-db';
|
||||||
import { UsersService } from '../users/users.service';
|
import { UsersService } from '../users/users.service';
|
||||||
@ -119,6 +120,7 @@ describe.skipIf(!hasTestDb)('atom feeds (e2e, issue #149)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
await prisma.instanceSetting.deleteMany({ where: { key: 'feeds.enabled' } });
|
||||||
await prisma.feedToken.deleteMany({ where: { userId: ownerId } });
|
await prisma.feedToken.deleteMany({ where: { userId: ownerId } });
|
||||||
await prisma.roleGrant.deleteMany({ where: { pond: { ownerId } } });
|
await prisma.roleGrant.deleteMany({ where: { pond: { ownerId } } });
|
||||||
await prisma.pageVersion.deleteMany({ where: { page: { pond: { ownerId } } } });
|
await prisma.pageVersion.deleteMany({ where: { page: { pond: { ownerId } } } });
|
||||||
@ -195,4 +197,25 @@ describe.skipIf(!hasTestDb)('atom feeds (e2e, issue #149)', () => {
|
|||||||
expect(res.text).toContain('rel="alternate" type="application/atom+xml"');
|
expect(res.text).toContain('rel="alternate" type="application/atom+xml"');
|
||||||
expect(res.text).toContain(`/api/v1/public/${pondSlug}/feed.xml`);
|
expect(res.text).toContain(`/api/v1/public/${pondSlug}/feed.xml`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('answers 404 on the whole feed surface while feeds.enabled is off (issue #191)', async () => {
|
||||||
|
const settings = app.get(InstanceSettingsService);
|
||||||
|
await settings.set('feeds.enabled', false, ownerId);
|
||||||
|
try {
|
||||||
|
// Feed routes hide — even for a pond that serves anonymously above.
|
||||||
|
await api().get(`/api/v1/public/${pondSlug}/feed.xml`).expect(404);
|
||||||
|
await api().get(`/api/v1/public/${pondSlug}/newer-${suffix}/feed.xml`).expect(404);
|
||||||
|
// The token management surface hides with them.
|
||||||
|
await api().get('/api/v1/users/me/feed-tokens').set('Cookie', ownerCookie).expect(404);
|
||||||
|
await api()
|
||||||
|
.post('/api/v1/users/me/feed-tokens')
|
||||||
|
.set('Cookie', ownerCookie)
|
||||||
|
.send({ name: 'nope' })
|
||||||
|
.expect(404);
|
||||||
|
// The rest of the public surface is untouched.
|
||||||
|
await api().get(`/api/v1/public/${pondSlug}/newer-${suffix}`).expect(200);
|
||||||
|
} finally {
|
||||||
|
await settings.set('feeds.enabled', true, ownerId);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { Controller, Get, Param, Query, Req, Res } from '@nestjs/common';
|
import { Controller, Get, NotFoundException, Param, Query, Req, Res } from '@nestjs/common';
|
||||||
import type { PageCommentsView } from '@dorfteich/shared';
|
import type { PageCommentsView } from '@dorfteich/shared';
|
||||||
import type { Response } from 'express';
|
import type { Response } from 'express';
|
||||||
|
|
||||||
import { AuthedRequest, Public } from '../auth/auth.guard';
|
import { AuthedRequest, Public } from '../auth/auth.guard';
|
||||||
|
import { InstanceSettingsService } from '../settings/instance-settings.service';
|
||||||
import { FeedService } from './feed.service';
|
import { FeedService } from './feed.service';
|
||||||
import { PublicPageContent, PublicService } from './public.service';
|
import { PublicPageContent, PublicService } from './public.service';
|
||||||
|
|
||||||
@ -17,8 +18,14 @@ export class PublicController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly publicPages: PublicService,
|
private readonly publicPages: PublicService,
|
||||||
private readonly feeds: FeedService,
|
private readonly feeds: FeedService,
|
||||||
|
private readonly settings: InstanceSettingsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/** Feed master switch (issue #191): disabled ⇒ 404, existence hidden. */
|
||||||
|
private async assertFeedsEnabled(): Promise<void> {
|
||||||
|
if (!(await this.settings.get('feeds.enabled'))) throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
// The feed routes come FIRST: `:pondSlug/feed.xml` would otherwise be
|
// The feed routes come FIRST: `:pondSlug/feed.xml` would otherwise be
|
||||||
// swallowed by the `:pondSlug/:pageSlug` HTML route below (issue #149).
|
// swallowed by the `:pondSlug/:pageSlug` HTML route below (issue #149).
|
||||||
@Get(':pondSlug/feed.xml')
|
@Get(':pondSlug/feed.xml')
|
||||||
@ -29,6 +36,7 @@ export class PublicController {
|
|||||||
@Req() request: AuthedRequest,
|
@Req() request: AuthedRequest,
|
||||||
@Res({ passthrough: true }) response: Response,
|
@Res({ passthrough: true }) response: Response,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
await this.assertFeedsEnabled();
|
||||||
const viewer = await this.feeds.viewerFor(request.user ?? null, token);
|
const viewer = await this.feeds.viewerFor(request.user ?? null, token);
|
||||||
const xml = await this.feeds.pondFeed(viewer, pondSlug, baseUrlOf(request));
|
const xml = await this.feeds.pondFeed(viewer, pondSlug, baseUrlOf(request));
|
||||||
response.set('Content-Type', 'application/atom+xml; charset=utf-8');
|
response.set('Content-Type', 'application/atom+xml; charset=utf-8');
|
||||||
@ -44,6 +52,7 @@ export class PublicController {
|
|||||||
@Req() request: AuthedRequest,
|
@Req() request: AuthedRequest,
|
||||||
@Res({ passthrough: true }) response: Response,
|
@Res({ passthrough: true }) response: Response,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
await this.assertFeedsEnabled();
|
||||||
const viewer = await this.feeds.viewerFor(request.user ?? null, token);
|
const viewer = await this.feeds.viewerFor(request.user ?? null, token);
|
||||||
const xml = await this.feeds.pageFeed(viewer, pondSlug, pageSlug, baseUrlOf(request));
|
const xml = await this.feeds.pageFeed(viewer, pondSlug, pageSlug, baseUrlOf(request));
|
||||||
response.set('Content-Type', 'application/atom+xml; charset=utf-8');
|
response.set('Content-Type', 'application/atom+xml; charset=utf-8');
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { CommentsModule } from '../comments/comments.module';
|
import { CommentsModule } from '../comments/comments.module';
|
||||||
import { PagesModule } from '../pages/pages.module';
|
import { PagesModule } from '../pages/pages.module';
|
||||||
import { PluginsModule } from '../plugins/plugins.module';
|
import { PluginsModule } from '../plugins/plugins.module';
|
||||||
|
import { SettingsModule } from '../settings/settings.module';
|
||||||
|
|
||||||
import { FeedTokensController } from './feed-tokens.controller';
|
import { FeedTokensController } from './feed-tokens.controller';
|
||||||
import { FeedTokensService } from './feed-tokens.service';
|
import { FeedTokensService } from './feed-tokens.service';
|
||||||
@ -18,7 +19,7 @@ import { ReadContentController } from './read-content.controller';
|
|||||||
* marks `GET /media/:fileId` public too.
|
* marks `GET /media/:fileId` public too.
|
||||||
*/
|
*/
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PluginsModule, CommentsModule, PagesModule],
|
imports: [PluginsModule, CommentsModule, PagesModule, SettingsModule],
|
||||||
controllers: [PublicController, ReadContentController, FeedTokensController],
|
controllers: [PublicController, ReadContentController, FeedTokensController],
|
||||||
providers: [PublicService, FeedService, FeedTokensService],
|
providers: [PublicService, FeedService, FeedTokensService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -56,6 +56,12 @@ export const INSTANCE_SETTINGS = {
|
|||||||
// Built-in MCP endpoint master switch (issue #105, default off) —
|
// Built-in MCP endpoint master switch (issue #105, default off) —
|
||||||
// independent of the REST switch; ponds opt in via `mcpEnabled`.
|
// independent of the REST switch; ponds opt in via `mcpEnabled`.
|
||||||
'mcp.enabled': z.boolean().default(false),
|
'mcp.enabled': z.boolean().default(false),
|
||||||
|
// Atom feed master switch (issue #191). Default ON: feeds predate the
|
||||||
|
// switch, so existing instances and their subscribed readers keep
|
||||||
|
// working; the VS-NfD reference configuration (#227) turns it off.
|
||||||
|
// While disabled, the feed routes AND the feed-token management answer
|
||||||
|
// 404 (existence hidden, same semantics as the two switches above).
|
||||||
|
'feeds.enabled': z.boolean().default(true),
|
||||||
// Backup targets (ADR 0015, issue #103). The backup sidecar reads these
|
// Backup targets (ADR 0015, issue #103). The backup sidecar reads these
|
||||||
// rows directly (apps/backup settings.ts — keep the schemas in sync); the
|
// rows directly (apps/backup settings.ts — keep the schemas in sync); the
|
||||||
// Nextcloud app password is NOT here, it lives in the secret store
|
// Nextcloud app password is NOT here, it lives in the secret store
|
||||||
|
|||||||
@ -24,6 +24,7 @@ interface InstanceSettings {
|
|||||||
'quota.maxFileBytes': number;
|
'quota.maxFileBytes': number;
|
||||||
'api.enabled': boolean;
|
'api.enabled': boolean;
|
||||||
'mcp.enabled': boolean;
|
'mcp.enabled': boolean;
|
||||||
|
'feeds.enabled': boolean;
|
||||||
'upload.allowedExtensions': string[];
|
'upload.allowedExtensions': string[];
|
||||||
'upload.svgPolicy': 'reject' | 'sanitize';
|
'upload.svgPolicy': 'reject' | 'sanitize';
|
||||||
'legal.imprint': string;
|
'legal.imprint': string;
|
||||||
@ -239,6 +240,15 @@ function PublicApiSettingsForm({ settings }: { settings: InstanceSettings }): Re
|
|||||||
{t('admin.mcpLabel')}
|
{t('admin.mcpLabel')}
|
||||||
</label>
|
</label>
|
||||||
<p className="api-opt-in__hint">{t('admin.mcpHint')}</p>
|
<p className="api-opt-in__hint">{t('admin.mcpHint')}</p>
|
||||||
|
<label className="api-opt-in__label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings['feeds.enabled']}
|
||||||
|
onChange={(event) => void save({ 'feeds.enabled': event.target.checked })}
|
||||||
|
/>
|
||||||
|
{t('admin.feedsLabel')}
|
||||||
|
</label>
|
||||||
|
<p className="api-opt-in__hint">{t('admin.feedsHint')}</p>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,15 @@ or sloppy plugin authors, compromised dependencies.
|
|||||||
password reset via single-use hashed tokens; both rate-limited.
|
password reset via single-use hashed tokens; both rate-limited.
|
||||||
- Rate limiting (DB-backed) on login, signup, reset, and API; lockout
|
- Rate limiting (DB-backed) on login, signup, reset, and API; lockout
|
||||||
backoff on repeated failed logins per account+IP.
|
backoff on repeated failed logins per account+IP.
|
||||||
|
- Feed tokens (issue #149) authenticate feed URLs via `?token=` — feed
|
||||||
|
readers cannot send headers, which is why the credential lives in the
|
||||||
|
URL at all. Moving it into a path segment was rejected (issue #191): a
|
||||||
|
path lands in the same proxy and request logs as a query string.
|
||||||
|
Instead: the instance switch `feeds.enabled` hides the whole feed
|
||||||
|
surface with 404 semantics (the VS-NfD reference configuration turns
|
||||||
|
feeds off), tokens are stored hashed, and the api's request log masks
|
||||||
|
`?token=` values (`common/mask-token-param.ts`), so no code path logs
|
||||||
|
the credential.
|
||||||
- Self-registration can be disabled instance-wide; personal-pond quotas
|
- Self-registration can be disabled instance-wide; personal-pond quotas
|
||||||
(editors/readers/ponds/storage) bound the blast radius of spam accounts.
|
(editors/readers/ponds/storage) bound the blast radius of spam accounts.
|
||||||
|
|
||||||
|
|||||||
@ -95,7 +95,7 @@ chain`_
|
|||||||
durchgelassen · 1 AT · #189
|
durchgelassen · 1 AT · #189
|
||||||
- [x] **Session-Timeout konfigurierbar**, Default deutlich unter 30 Tagen,
|
- [x] **Session-Timeout konfigurierbar**, Default deutlich unter 30 Tagen,
|
||||||
separates Idle-Timeout · 1–2 AT · #190
|
separates Idle-Timeout · 1–2 AT · #190
|
||||||
- [ ] **Feed-Token raus aus dem Query-Parameter**, alternativ Feeds hart
|
- [x] **Feed-Token raus aus dem Query-Parameter**, alternativ Feeds hart
|
||||||
abschaltbar · 2 AT · #191
|
abschaltbar · 2 AT · #191
|
||||||
- [ ] **Backup-Ziele einschränkbar** — Allowlist, WebDAV/rsync per Deploy
|
- [ ] **Backup-Ziele einschränkbar** — Allowlist, WebDAV/rsync per Deploy
|
||||||
vollständig deaktivierbar · 2 AT · #192
|
vollständig deaktivierbar · 2 AT · #192
|
||||||
|
|||||||
@ -50,7 +50,9 @@
|
|||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"saved": "Gespeichert.",
|
"saved": "Gespeichert.",
|
||||||
"mcpLabel": "Eingebauten MCP-Endpoint aktivieren",
|
"mcpLabel": "Eingebauten MCP-Endpoint aktivieren",
|
||||||
"mcpHint": "Hauptschalter (standardmäßig aus), unabhängig von der REST-API. MCP-Clients verbinden sich mit einem API-Token auf /api/mcp; jeder Teich gibt sich zusätzlich über seine Teich-Einstellungen frei. Siehe docs/self-hosting/public-api.md."
|
"mcpHint": "Hauptschalter (standardmäßig aus), unabhängig von der REST-API. MCP-Clients verbinden sich mit einem API-Token auf /api/mcp; jeder Teich gibt sich zusätzlich über seine Teich-Einstellungen frei. Siehe docs/self-hosting/public-api.md.",
|
||||||
|
"feedsLabel": "Atom-Feeds aktivieren",
|
||||||
|
"feedsHint": "Hauptschalter (standardmäßig an). Ausgeschaltet antworten alle Feed-Adressen und die Feed-Token-Verwaltung mit 404 — für gehärtete Umgebungen, in denen Feed-Tokens als Lese-Zugangsdaten nicht in URLs auftauchen dürfen."
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
"title": "Feed-Tokens",
|
"title": "Feed-Tokens",
|
||||||
|
|||||||
@ -50,7 +50,9 @@
|
|||||||
"save": "Save",
|
"save": "Save",
|
||||||
"saved": "Saved.",
|
"saved": "Saved.",
|
||||||
"mcpLabel": "Enable the built-in MCP endpoint",
|
"mcpLabel": "Enable the built-in MCP endpoint",
|
||||||
"mcpHint": "Master switch (default off), independent of the REST API. MCP clients connect to /api/mcp with an API token; each pond additionally opts in via its pond settings. See docs/self-hosting/public-api.md."
|
"mcpHint": "Master switch (default off), independent of the REST API. MCP clients connect to /api/mcp with an API token; each pond additionally opts in via its pond settings. See docs/self-hosting/public-api.md.",
|
||||||
|
"feedsLabel": "Enable Atom feeds",
|
||||||
|
"feedsHint": "Master switch (default on). While off, every feed URL and the feed-token management answer 404 — for hardened environments where feed tokens must not appear in URLs as read credentials."
|
||||||
},
|
},
|
||||||
"feed": {
|
"feed": {
|
||||||
"title": "Feed tokens",
|
"title": "Feed tokens",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user