diff --git a/fluxer_admin/openapi-admin.json b/fluxer_admin/openapi-admin.json index c7ce00df7..ead78390c 100644 --- a/fluxer_admin/openapi-admin.json +++ b/fluxer_admin/openapi-admin.json @@ -5443,59 +5443,6 @@ } } }, - "/admin/system/heap-snapshots": { - "post": { - "operationId": "create_admin_system_heap_snapshot", - "summary": "Create a V8 heap snapshot", - "tags": ["Admin"], - "responses": { - "200": { - "description": "Success", - "content": {"application/octet-stream": {"schema": {"$ref": "#/components/schemas/HeapSnapshotResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ThrottledError"}}}, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "description": "Writes a V8 heap snapshot of the current process and returns the snapshot file. Used for diagnosing memory leaks. Requires SYSTEM_HEAP_SNAPSHOT permission.", - "security": [{"adminApiKey": []}] - } - }, "/admin/users": { "get": { "operationId": "list_admin_users", @@ -9890,7 +9837,7 @@ "type": "object", "properties": { "acls": { - "maxItems": 112, + "maxItems": 111, "type": "array", "items": {"$ref": "#/components/schemas/AdminAclType"}, "description": "List of access control permissions to assign" @@ -9920,7 +9867,6 @@ "required": ["users", "total"], "additionalProperties": false }, - "HeapSnapshotResponse": {"type": "string", "format": "binary", "description": "V8 heap snapshot file"}, "SendSystemDmRequest": { "type": "object", "properties": { @@ -12759,7 +12705,7 @@ }, "acls": { "description": "Replacement list of access control permissions for the key", - "maxItems": 112, + "maxItems": 111, "type": "array", "items": {"$ref": "#/components/schemas/AdminAclType"} } @@ -12777,7 +12723,7 @@ "type": "string" }, "acls": { - "maxItems": 112, + "maxItems": 111, "type": "array", "items": {"type": "string"}, "description": "List of access control permissions for the key" @@ -12807,7 +12753,7 @@ "maximum": 365 }, "acls": { - "maxItems": 112, + "maxItems": 111, "type": "array", "items": {"$ref": "#/components/schemas/AdminAclType"}, "description": "List of access control permissions for the key" @@ -12828,7 +12774,7 @@ "type": "string" }, "acls": { - "maxItems": 112, + "maxItems": 111, "type": "array", "items": {"type": "string"}, "description": "List of access control permissions for the key" @@ -12841,7 +12787,7 @@ "type": "object", "properties": { "acls": { - "maxItems": 112, + "maxItems": 111, "type": "array", "items": {"type": "string", "minLength": 1, "maxLength": 64}, "description": "Every admin access control permission the admin API recognises" @@ -12932,7 +12878,6 @@ "report:view", "report:view:reporter_pii", "system_dm:send", - "system:heap_snapshot", "user:cancel:bulk_message_deletion", "user:delete", "user:disable:suspicious", @@ -15471,7 +15416,7 @@ "pending_bulk_message_deletion_at": {"nullable": true, "type": "string"}, "deletion_reason_code": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]}, "deletion_public_reason": {"nullable": true, "type": "string"}, - "acls": {"maxItems": 112, "type": "array", "items": {"type": "string"}}, + "acls": {"maxItems": 111, "type": "array", "items": {"type": "string"}}, "traits": {"maxItems": 100, "type": "array", "items": {"type": "string"}}, "has_totp": {"type": "boolean"}, "authenticator_types": {"maxItems": 10, "type": "array", "items": {"$ref": "#/components/schemas/Int32Type"}}, diff --git a/fluxer_admin/src/acl.rs b/fluxer_admin/src/acl.rs index 80e132689..7c4fa0e05 100644 --- a/fluxer_admin/src/acl.rs +++ b/fluxer_admin/src/acl.rs @@ -79,7 +79,6 @@ pub const REPORT_RESOLVE: &str = "report:resolve"; pub const REPORT_VIEW: &str = "report:view"; pub const REPORT_VIEW_REPORTER_PII: &str = "report:view:reporter_pii"; pub const SYSTEM_DM_SEND: &str = "system_dm:send"; -pub const SYSTEM_HEAP_SNAPSHOT: &str = "system:heap_snapshot"; pub const USER_CANCEL_BULK_MESSAGE_DELETION: &str = "user:cancel:bulk_message_deletion"; pub const USER_DELETE: &str = "user:delete"; pub const USER_DISABLE_SUSPICIOUS: &str = "user:disable:suspicious"; @@ -192,7 +191,6 @@ pub const ALL_ACLS: &[&str] = &[ REPORT_VIEW, REPORT_VIEW_REPORTER_PII, SYSTEM_DM_SEND, - SYSTEM_HEAP_SNAPSHOT, USER_CANCEL_BULK_MESSAGE_DELETION, USER_DELETE, USER_DISABLE_SUSPICIOUS, diff --git a/fluxer_api/src/api/admin/controllers/SystemAdminController.ts b/fluxer_api/src/api/admin/controllers/SystemAdminController.ts deleted file mode 100644 index 2cd043d49..000000000 --- a/fluxer_api/src/api/admin/controllers/SystemAdminController.ts +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import {Readable} from 'node:stream'; -import * as v8 from 'node:v8'; -import {recordAdminWrite} from '@app/api/admin/AdminAuditRecorder'; -import {requireAdminACL} from '@app/api/middleware/AdminMiddleware'; -import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware'; -import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware'; -import {RateLimitConfigs} from '@app/api/RateLimitConfig'; -import type {HonoApp} from '@app/api/types/HonoEnv'; -import {AdminACLs} from '@fluxer/constants/src/AdminACLs'; -import {HeapSnapshotResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas'; - -export function SystemAdminController(app: HonoApp) { - app.post( - '/admin/system/heap-snapshots', - RateLimitMiddleware(RateLimitConfigs.ADMIN_SYSTEM_HEAP_SNAPSHOT), - requireAdminACL(AdminACLs.SYSTEM_HEAP_SNAPSHOT), - OpenAPI({ - operationId: 'create_admin_system_heap_snapshot', - summary: 'Create a V8 heap snapshot', - description: - 'Writes a V8 heap snapshot of the current process and returns the snapshot file. Used for diagnosing memory leaks. Requires SYSTEM_HEAP_SNAPSHOT permission.', - responseSchema: HeapSnapshotResponse, - responseContentType: 'application/octet-stream', - statusCode: 200, - security: 'adminApiKey', - tags: 'Admin', - }), - async (ctx) => { - const snapshotPath = path.join('/tmp', `heap-${Date.now()}.heapsnapshot`); - try { - v8.writeHeapSnapshot(snapshotPath); - const stat = fs.statSync(snapshotPath); - await recordAdminWrite(ctx, { - targetType: 'system', - targetId: 0n, - action: 'create_heap_snapshot', - metadata: {size_bytes: stat.size}, - }); - const nodeStream = fs.createReadStream(snapshotPath); - const body = Readable.toWeb(nodeStream) as ReadableStream; - nodeStream.on('close', () => { - fs.unlink(snapshotPath, () => {}); - }); - return new Response(body, { - status: 200, - headers: { - 'Content-Type': 'application/octet-stream', - 'Content-Disposition': `attachment; filename="${path.basename(snapshotPath)}"`, - 'Content-Length': String(stat.size), - }, - }); - } catch (error) { - fs.unlink(snapshotPath, () => {}); - throw error; - } - }, - ); -} diff --git a/fluxer_api/src/api/admin/controllers/index.ts b/fluxer_api/src/api/admin/controllers/index.ts index 7e995a68a..460bd565d 100644 --- a/fluxer_api/src/api/admin/controllers/index.ts +++ b/fluxer_api/src/api/admin/controllers/index.ts @@ -17,7 +17,6 @@ import {LimitConfigAdminController} from '@app/api/admin/controllers/LimitConfig import {MessageAdminController} from '@app/api/admin/controllers/MessageAdminController'; import {ReportAdminController} from '@app/api/admin/controllers/ReportAdminController'; import {SearchAdminController} from '@app/api/admin/controllers/SearchAdminController'; -import {SystemAdminController} from '@app/api/admin/controllers/SystemAdminController'; import {SystemDmAdminController} from '@app/api/admin/controllers/SystemDmAdminController'; import {UserAdminController} from '@app/api/admin/controllers/UserAdminController'; import {VoiceAdminController} from '@app/api/admin/controllers/VoiceAdminController'; @@ -43,6 +42,5 @@ export function registerAdminControllers(app: HonoApp) { SearchAdminController(app); DiscoveryAdminController(app); SystemDmAdminController(app); - SystemAdminController(app); JobsAdminController(app); } diff --git a/fluxer_api/src/api/admin/tests/audit_coverage/AdminAuditRouteCoverage.test.ts b/fluxer_api/src/api/admin/tests/audit_coverage/AdminAuditRouteCoverage.test.ts index 39facfb35..e84112c45 100644 --- a/fluxer_api/src/api/admin/tests/audit_coverage/AdminAuditRouteCoverage.test.ts +++ b/fluxer_api/src/api/admin/tests/audit_coverage/AdminAuditRouteCoverage.test.ts @@ -19,7 +19,6 @@ import {LimitConfigAdminAuditCases} from '@app/api/admin/tests/audit_coverage/Li import {MessageAdminAuditCases} from '@app/api/admin/tests/audit_coverage/MessageAdminAuditCases'; import {ReportAdminAuditCases} from '@app/api/admin/tests/audit_coverage/ReportAdminAuditCases'; import {SearchAdminAuditCases} from '@app/api/admin/tests/audit_coverage/SearchAdminAuditCases'; -import {SystemAdminAuditCases} from '@app/api/admin/tests/audit_coverage/SystemAdminAuditCases'; import {SystemDmAdminAuditCases} from '@app/api/admin/tests/audit_coverage/SystemDmAdminAuditCases'; import {UserAdminAuditCases} from '@app/api/admin/tests/audit_coverage/UserAdminAuditCases'; import {UserWriteAdminAuditCases} from '@app/api/admin/tests/audit_coverage/UserWriteAdminAuditCases'; @@ -46,7 +45,6 @@ const ALL_CASES = [ ...MessageAdminAuditCases, ...ReportAdminAuditCases, ...SearchAdminAuditCases, - ...SystemAdminAuditCases, ...SystemDmAdminAuditCases, ...UserAdminAuditCases, ...UserWriteAdminAuditCases, diff --git a/fluxer_api/src/api/admin/tests/audit_coverage/SystemAdminAuditCases.ts b/fluxer_api/src/api/admin/tests/audit_coverage/SystemAdminAuditCases.ts deleted file mode 100644 index 289f0549e..000000000 --- a/fluxer_api/src/api/admin/tests/audit_coverage/SystemAdminAuditCases.ts +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {AdminAuditCoverageCase} from '@app/api/admin/tests/audit_coverage/AdminAuditCoverage'; -import {expect} from 'vitest'; - -export const SystemAdminAuditCases: ReadonlyArray = [ - { - method: 'POST', - route: '/admin/system/heap-snapshots', - async prepare() { - return { - request: {path: '/admin/system/heap-snapshots'}, - expected: { - action: 'create_heap_snapshot', - targetType: 'system', - targetId: '0', - metadata: {size_bytes: expect.any(String)}, - }, - }; - }, - }, -]; diff --git a/fluxer_api/src/api/admin/tests/audit_coverage/SystemAdminAuditCoverage.test.ts b/fluxer_api/src/api/admin/tests/audit_coverage/SystemAdminAuditCoverage.test.ts deleted file mode 100644 index 32de2d6dd..000000000 --- a/fluxer_api/src/api/admin/tests/audit_coverage/SystemAdminAuditCoverage.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {describeAdminAuditCoverage} from '@app/api/admin/tests/audit_coverage/AdminAuditCoverage'; -import {SystemAdminAuditCases} from '@app/api/admin/tests/audit_coverage/SystemAdminAuditCases'; - -describeAdminAuditCoverage('SystemAdminController', SystemAdminAuditCases); diff --git a/fluxer_api/src/api/rate_limit_configs/AdminRateLimitConfig.ts b/fluxer_api/src/api/rate_limit_configs/AdminRateLimitConfig.ts index d9196ab53..856e11640 100644 --- a/fluxer_api/src/api/rate_limit_configs/AdminRateLimitConfig.ts +++ b/fluxer_api/src/api/rate_limit_configs/AdminRateLimitConfig.ts @@ -48,8 +48,4 @@ export const AdminRateLimitConfigs = { bucket: 'admin:general', config: {limit: 200, windowMs: ms('1 minute')}, } as RouteRateLimitConfig, - ADMIN_SYSTEM_HEAP_SNAPSHOT: { - bucket: 'admin:system:heap_snapshot', - config: {limit: 2, windowMs: ms('5 minutes')}, - } as RouteRateLimitConfig, } as const; diff --git a/fluxer_docs/src/content/docs/admin-api/index.mdx b/fluxer_docs/src/content/docs/admin-api/index.mdx index ae0ef5b88..52f397c03 100644 --- a/fluxer_docs/src/content/docs/admin-api/index.mdx +++ b/fluxer_docs/src/content/docs/admin-api/index.mdx @@ -342,7 +342,6 @@ An entry with any other action has `access` set to `write`. | change_username | A username or discriminator was changed | | clear_fields1 | Profile fields of an account, or fields of a guild, were cleared | | create_admin_api_key | An Admin API key was created | -| create_heap_snapshot | A heap snapshot of the API process was written | | create_registration_url | A registration URL was issued | | create_voice_region | A voice region was created | | create_voice_server | A voice server was registered | @@ -593,7 +592,6 @@ The registry is returned in this order by [List ACLs](#list-acls). A value outsi | report:view | Reads and searches reports | | report:view:reporter_pii6 | Unredacts the contact fields a reporter supplied | | system_dm:send | Sends an official direct message from the instance to one or many accounts | -| system:heap_snapshot | Captures a heap snapshot of a running process | | user:cancel:bulk_message_deletion | Cancels the bulk message deletion an account scheduled for itself | | user:delete | Schedules or cancels account deletion | | user:disable:suspicious | Disables a user for suspicious activity | diff --git a/fluxer_docs/src/content/docs/admin-api/instance.mdx b/fluxer_docs/src/content/docs/admin-api/instance.mdx index 043be6b50..97264af3a 100644 --- a/fluxer_docs/src/content/docs/admin-api/instance.mdx +++ b/fluxer_docs/src/content/docs/admin-api/instance.mdx @@ -10,13 +10,13 @@ Instance configuration is everything an operator can change at runtime. It cover Every write is a merge over the stored configuration, and an omitted key leaves the stored value unchanged. The limit configuration write is the one exception and replaces the stored document. -Reading configuration requires the [Admin ACL](/admin-api/#acl-registry) `instance:config:view`, and writing it requires `instance:config:update`. The limit configuration has its own pair, `instance:limit_config:view` and `instance:limit_config:update`. The heap snapshot requires `system:heap_snapshot`. +Reading configuration requires the [Admin ACL](/admin-api/#acl-registry) `instance:config:view`, and writing it requires `instance:config:update`. The limit configuration has its own pair, `instance:limit_config:view` and `instance:limit_config:update`. :::note[Initial setup accepts a plain session credential] Until setup is marked complete, [Get instance configuration](#get-instance-configuration), [Update instance configuration](#update-instance-configuration), [Create branding asset](#create-branding-asset), and [Create SMTP test](#create-smtp-test) also accept a session credential holding no Admin ACL. ::: -That relaxation applies only to a session credential, so an Admin API key and a bearer token are evaluated normally even before setup is complete. Completing setup grants the acting session the wildcard ACL. Registration URLs, pending registrations, limit configuration, and the heap snapshot always need their own ACL. +That relaxation applies only to a session credential, so an Admin API key and a bearer token are evaluated normally even before setup is complete. Completing setup grants the acting session the wildcard ACL. Registration URLs, pending registrations, and limit configuration always need their own ACL. :::caution[No read returns a stored secret] A secret is reported by a companion boolean such as `client_secret_set`. Except for the single sign-on client secret, that boolean is true whether the value is stored here or supplied by deployment configuration. @@ -24,7 +24,7 @@ A secret is reported by a companion boolean such as `client_secret_set`. Except ## Instance configuration object -The complete runtime configuration of the deployment. Every configuration operation here except the limit configuration and the heap snapshot returns it. +The complete runtime configuration of the deployment. Every configuration operation here except the limit configuration returns it. Missing settings use the defaults documented below. Invalid stored configuration causes an error rather than silently resetting a policy. Operators can find recovery guidance under [stored instance policy](/operator/configuration/#stored-instance-policy). @@ -840,37 +840,3 @@ The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-obje ### Rate limit 100 requests per minute for each authenticated user, on the `admin:user:modify` bucket. - -## Create heap snapshot - - - -Writes a V8 heap snapshot of the API process that serves the request and returns the file. Requires `system:heap_snapshot`. - -### Response - -| Status | Body | Condition | -| --- | --- | --- | -| 2001 | heap snapshot file | The snapshot was written | - -1 The body is the raw `.heapsnapshot` file streamed as an attachment. The filename is `heap-` followed by the Unix time in milliseconds at which the snapshot was taken - -The 200 has `Content-Type: application/octet-stream`, `Content-Disposition: attachment`, and `Content-Length`. - -A client MUST NOT decode the body as JSON. The published OpenAPI document declares a JSON object of `success`, `filename`, and `size_bytes` here, so a generated client has to be overridden. - -:::danger[A snapshot stalls the process and exposes memory] -Writing a snapshot blocks the serving process until the dump finishes. The file contains whatever that process held in memory, including message content, tokens, and personal data. -::: - -:::caution[Only the serving node is captured] -A deployment behind a load balancer needs several attempts to reach a particular node, and the file grows with that process heap. -::: - -### Side effects - -No configuration is changed. The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-object) with action `create_heap_snapshot`, target type `system`, target ID `0`, and metadata key `size_bytes`, after the snapshot is written and before the file is sent. A snapshot that fails to write records no entry. - -### Rate limit - -2 requests per five minutes for each authenticated user, on the `admin:system:heap_snapshot` bucket. diff --git a/packages/constants/src/AdminACLs.ts b/packages/constants/src/AdminACLs.ts index c729c1c57..6c7c6e786 100644 --- a/packages/constants/src/AdminACLs.ts +++ b/packages/constants/src/AdminACLs.ts @@ -80,7 +80,6 @@ export const AdminACLs = { REPORT_VIEW: 'report:view', REPORT_VIEW_REPORTER_PII: 'report:view:reporter_pii', SYSTEM_DM_SEND: 'system_dm:send', - SYSTEM_HEAP_SNAPSHOT: 'system:heap_snapshot', USER_CANCEL_BULK_MESSAGE_DELETION: 'user:cancel:bulk_message_deletion', USER_DELETE: 'user:delete', USER_DISABLE_SUSPICIOUS: 'user:disable:suspicious', diff --git a/packages/openapi/src/__tests__/OpenAPIGenerator.test.ts b/packages/openapi/src/__tests__/OpenAPIGenerator.test.ts index f1c3bf491..15d75aa16 100644 --- a/packages/openapi/src/__tests__/OpenAPIGenerator.test.ts +++ b/packages/openapi/src/__tests__/OpenAPIGenerator.test.ts @@ -54,12 +54,6 @@ describe('OpenAPI generation from API controllers', () => { expect(operation.responses).toHaveProperty('401'); }); - it('publishes the real heap-snapshot media type', () => { - expect(document.paths['/admin/system/heap-snapshots'].post.responses['200'].content).toEqual({ - 'application/octet-stream': {schema: {$ref: '#/components/schemas/HeapSnapshotResponse'}}, - }); - }); - it('documents full and partial harvest archive downloads as binary ZIP responses', () => { const responses = document.paths['/harvest-downloads/{harvestId}'].get.responses; for (const status of ['200', '206']) { diff --git a/packages/schema/src/domains/admin/AdminSchemas.ts b/packages/schema/src/domains/admin/AdminSchemas.ts index 9a7c3c860..649ddd251 100644 --- a/packages/schema/src/domains/admin/AdminSchemas.ts +++ b/packages/schema/src/domains/admin/AdminSchemas.ts @@ -1489,6 +1489,5 @@ export const LimitConfigGetResponse = z.object({ export const DeleteApiKeyResponse = z.object({ success: z.literal(true), }); -export const HeapSnapshotResponse = z.file().describe('V8 heap snapshot file'); export const AdminApiKeyListResponse = z.array(ListAdminApiKeyResponse);