chore(admin): remove the heap snapshot endpoint (#2814)

This commit is contained in:
Hampus
2026-09-16 18:56:01 +02:00
committed by GitHub
parent 4ef9c4c65b
commit 34b6ecfbd2
13 changed files with 10 additions and 209 deletions
+7 -62
View File
@@ -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"}},
-2
View File
@@ -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,
@@ -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;
}
},
);
}
@@ -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);
}
@@ -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,
@@ -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<AdminAuditCoverageCase> = [
{
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)},
},
};
},
},
];
@@ -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);
@@ -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;
@@ -342,7 +342,6 @@ An entry with any other action has `access` set to `write`.
| change_username | A username or discriminator was changed |
| clear_fields<sup>1</sup> | 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_pii<sup>6</sup> | 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 |
@@ -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
<RouteHeader method="POST" path="/v1/admin/system/heap-snapshots" auditReason />
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 |
| --- | --- | --- |
| 200<sup>1</sup> | heap snapshot file | The snapshot was written |
<sup>1</sup> 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.
-1
View File
@@ -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',
@@ -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']) {
@@ -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);