feat: plugins v1.5
This commit is contained in:
@@ -25,6 +25,15 @@ export class ServerPluginRequirementEntity {
|
||||
@Column('text', { nullable: true })
|
||||
reason!: string | null;
|
||||
|
||||
@Column('text', { nullable: true })
|
||||
installUrl!: string | null;
|
||||
|
||||
@Column('text', { nullable: true })
|
||||
sourceUrl!: string | null;
|
||||
|
||||
@Column('text', { nullable: true })
|
||||
manifestJson!: string | null;
|
||||
|
||||
@Column('text', { nullable: true })
|
||||
configuredBy!: string | null;
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class ServerPluginInstallMetadata1000000000008 implements MigrationInterface {
|
||||
name = 'ServerPluginInstallMetadata1000000000008';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "server_plugin_requirements" ADD COLUMN "installUrl" TEXT`);
|
||||
await queryRunner.query(`ALTER TABLE "server_plugin_requirements" ADD COLUMN "sourceUrl" TEXT`);
|
||||
await queryRunner.query(`ALTER TABLE "server_plugin_requirements" ADD COLUMN "manifestJson" TEXT`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "temporary_server_plugin_requirements" (
|
||||
"serverId" TEXT NOT NULL,
|
||||
"pluginId" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"versionRange" TEXT,
|
||||
"reason" TEXT,
|
||||
"configuredBy" TEXT,
|
||||
"createdAt" INTEGER NOT NULL,
|
||||
"updatedAt" INTEGER NOT NULL,
|
||||
PRIMARY KEY ("serverId", "pluginId")
|
||||
)`);
|
||||
await queryRunner.query(`INSERT INTO "temporary_server_plugin_requirements" ("serverId", "pluginId", "status", "versionRange", "reason", "configuredBy", "createdAt", "updatedAt")
|
||||
SELECT "serverId", "pluginId", "status", "versionRange", "reason", "configuredBy", "createdAt", "updatedAt" FROM "server_plugin_requirements"`);
|
||||
await queryRunner.query(`DROP TABLE "server_plugin_requirements"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_server_plugin_requirements" RENAME TO "server_plugin_requirements"`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_server_plugin_requirements_status" ON "server_plugin_requirements" ("status")`);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { NormalizeServerArrays1000000000004 } from './1000000000004-NormalizeSer
|
||||
import { ServerRoleAccessControl1000000000005 } from './1000000000005-ServerRoleAccessControl';
|
||||
import { GameMatchMisses1000000000006 } from './1000000000006-GameMatchMisses';
|
||||
import { PluginSupport1000000000007 } from './1000000000007-PluginSupport';
|
||||
import { ServerPluginInstallMetadata1000000000008 } from './1000000000008-ServerPluginInstallMetadata';
|
||||
|
||||
export const serverMigrations = [
|
||||
InitialSchema1000000000000,
|
||||
@@ -15,5 +16,6 @@ export const serverMigrations = [
|
||||
NormalizeServerArrays1000000000004,
|
||||
ServerRoleAccessControl1000000000005,
|
||||
GameMatchMisses1000000000006,
|
||||
PluginSupport1000000000007
|
||||
PluginSupport1000000000007,
|
||||
ServerPluginInstallMetadata1000000000008
|
||||
];
|
||||
|
||||
@@ -9,7 +9,7 @@ function createOpenApiDocument(baseUrl: string) {
|
||||
info: {
|
||||
title: 'MetoYou Plugin Support API',
|
||||
version: '1.0.0',
|
||||
description: 'Official HTTP endpoints for plugin metadata, event definitions, and plugin data. '
|
||||
description: 'Official HTTP endpoints for plugin install metadata and event definitions. '
|
||||
+ 'Plugin code is never executed by the signal server.'
|
||||
},
|
||||
servers: [{ url: `${baseUrl}/api` }],
|
||||
@@ -43,18 +43,18 @@ function createOpenApiDocument(baseUrl: string) {
|
||||
},
|
||||
'/servers/{serverId}/plugins/{pluginId}/data': {
|
||||
get: {
|
||||
summary: 'List plugin data records',
|
||||
responses: { '200': { description: 'Plugin data records' }, '403': { description: 'Not a server member' } }
|
||||
summary: 'Plugin data persistence disabled',
|
||||
responses: { '410': { description: 'Plugin data persistence is disabled on the signal server' } }
|
||||
}
|
||||
},
|
||||
'/servers/{serverId}/plugins/{pluginId}/data/{key}': {
|
||||
put: {
|
||||
summary: 'Write plugin data',
|
||||
responses: { '200': { description: 'Plugin data saved' }, '403': { description: 'Not a server member' } }
|
||||
summary: 'Plugin data persistence disabled',
|
||||
responses: { '410': { description: 'Plugin data persistence is disabled on the signal server' } }
|
||||
},
|
||||
delete: {
|
||||
summary: 'Delete plugin data',
|
||||
responses: { '200': { description: 'Plugin data deleted' }, '403': { description: 'Not a server member' } }
|
||||
summary: 'Plugin data persistence disabled',
|
||||
responses: { '410': { description: 'Plugin data persistence is disabled on the signal server' } }
|
||||
}
|
||||
},
|
||||
'/openapi/settings': {
|
||||
@@ -98,7 +98,7 @@ router.get('/docs', (_req, res) => {
|
||||
<body style="font-family:system-ui;margin:2rem;line-height:1.5">
|
||||
<h1>MetoYou Plugin Support API</h1>
|
||||
<p>Plugin support endpoints are available at <a href="/api/openapi.json">/api/openapi.json</a>.</p>
|
||||
<p>The signal server stores metadata, data, and event definitions only. It never executes plugin code.</p>
|
||||
<p>The signal server stores plugin install metadata and event definitions only. It never executes plugin code or stores arbitrary plugin data.</p>
|
||||
</body>
|
||||
</html>`);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Response, Router } from 'express';
|
||||
import {
|
||||
deletePluginData,
|
||||
deletePluginEventDefinition,
|
||||
deletePluginRequirement,
|
||||
getPluginRequirementsSnapshot,
|
||||
listPluginData,
|
||||
PluginSupportError,
|
||||
upsertPluginData,
|
||||
upsertPluginEventDefinition,
|
||||
upsertPluginRequirement
|
||||
} from '../services/plugin-support.service';
|
||||
@@ -52,9 +49,12 @@ router.put('/:serverId/plugins/:pluginId/requirement', async (req, res) => {
|
||||
try {
|
||||
const requirement = await upsertPluginRequirement({
|
||||
actorUserId: readActorUserId(req.body.actorUserId),
|
||||
installUrl: req.body.installUrl,
|
||||
manifest: req.body.manifest,
|
||||
pluginId,
|
||||
reason: req.body.reason,
|
||||
serverId,
|
||||
sourceUrl: req.body.sourceUrl,
|
||||
status: req.body.status,
|
||||
versionRange: req.body.versionRange
|
||||
});
|
||||
@@ -124,85 +124,25 @@ router.delete('/:serverId/plugins/:pluginId/events/:eventName', async (req, res)
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:serverId/plugins/:pluginId/data', async (req, res) => {
|
||||
const { serverId, pluginId } = req.params;
|
||||
|
||||
try {
|
||||
const records = await listPluginData({
|
||||
actorUserId: readActorUserId(req.query.userId),
|
||||
key: req.query.key,
|
||||
ownerId: req.query.ownerId,
|
||||
pluginId,
|
||||
scope: req.query.scope,
|
||||
serverId
|
||||
});
|
||||
|
||||
res.json({ records });
|
||||
} catch (error) {
|
||||
sendPluginSupportError(error, res);
|
||||
}
|
||||
router.get('/:serverId/plugins/:pluginId/data', (_req, res) => {
|
||||
res.status(410).json({
|
||||
error: 'Plugin data persistence is disabled on the signal server',
|
||||
errorCode: 'PLUGIN_DATA_DISABLED'
|
||||
});
|
||||
});
|
||||
|
||||
router.put('/:serverId/plugins/:pluginId/data/:key', async (req, res) => {
|
||||
const { serverId, pluginId, key } = req.params;
|
||||
|
||||
try {
|
||||
const record = await upsertPluginData({
|
||||
actorUserId: readActorUserId(req.body.actorUserId),
|
||||
key,
|
||||
ownerId: req.body.ownerId,
|
||||
pluginId,
|
||||
schemaVersion: req.body.schemaVersion,
|
||||
scope: req.body.scope,
|
||||
serverId,
|
||||
value: req.body.value
|
||||
});
|
||||
|
||||
broadcastToServer(serverId, {
|
||||
type: 'plugin_data_changed',
|
||||
serverId,
|
||||
pluginId: record.pluginId,
|
||||
scope: record.scope,
|
||||
ownerId: record.ownerId,
|
||||
key: record.key,
|
||||
updatedAt: record.updatedAt
|
||||
});
|
||||
|
||||
res.json({ record });
|
||||
} catch (error) {
|
||||
sendPluginSupportError(error, res);
|
||||
}
|
||||
router.put('/:serverId/plugins/:pluginId/data/:key', (_req, res) => {
|
||||
res.status(410).json({
|
||||
error: 'Plugin data persistence is disabled on the signal server',
|
||||
errorCode: 'PLUGIN_DATA_DISABLED'
|
||||
});
|
||||
});
|
||||
|
||||
router.delete('/:serverId/plugins/:pluginId/data/:key', async (req, res) => {
|
||||
const { serverId, pluginId, key } = req.params;
|
||||
const scope = req.body.scope ?? req.query.scope;
|
||||
const ownerId = req.body.ownerId ?? req.query.ownerId;
|
||||
|
||||
try {
|
||||
await deletePluginData({
|
||||
actorUserId: readActorUserId(req.body.actorUserId),
|
||||
key,
|
||||
ownerId,
|
||||
pluginId,
|
||||
scope,
|
||||
serverId
|
||||
});
|
||||
|
||||
broadcastToServer(serverId, {
|
||||
type: 'plugin_data_changed',
|
||||
serverId,
|
||||
pluginId,
|
||||
scope: typeof scope === 'string' ? scope : 'server',
|
||||
ownerId: typeof ownerId === 'string' && ownerId.trim() ? ownerId.trim() : undefined,
|
||||
key,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (error) {
|
||||
sendPluginSupportError(error, res);
|
||||
}
|
||||
router.delete('/:serverId/plugins/:pluginId/data/:key', (_req, res) => {
|
||||
res.status(410).json({
|
||||
error: 'Plugin data persistence is disabled on the signal server',
|
||||
errorCode: 'PLUGIN_DATA_DISABLED'
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -37,8 +37,11 @@ const DATA_KEY_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
|
||||
const DATA_SCOPE_PATTERN = /^[a-zA-Z][a-zA-Z0-9._:-]{0,63}$/;
|
||||
|
||||
export interface PluginRequirementSummary {
|
||||
installUrl?: string;
|
||||
manifest?: unknown;
|
||||
pluginId: string;
|
||||
reason?: string;
|
||||
sourceUrl?: string;
|
||||
status: ServerPluginRequirementStatus;
|
||||
updatedAt: number;
|
||||
versionRange?: string;
|
||||
@@ -174,6 +177,10 @@ function parseJsonValue(valueJson: string): unknown {
|
||||
}
|
||||
}
|
||||
|
||||
function parseOptionalJsonValue(valueJson: string | null): unknown {
|
||||
return valueJson ? parseJsonValue(valueJson) : undefined;
|
||||
}
|
||||
|
||||
function serializeJsonValue(value: unknown, code: string): string {
|
||||
try {
|
||||
return JSON.stringify(value ?? null);
|
||||
@@ -184,8 +191,11 @@ function serializeJsonValue(value: unknown, code: string): string {
|
||||
|
||||
function toRequirementSummary(entity: ServerPluginRequirementEntity): PluginRequirementSummary {
|
||||
return {
|
||||
installUrl: entity.installUrl ?? undefined,
|
||||
manifest: parseOptionalJsonValue(entity.manifestJson),
|
||||
pluginId: entity.pluginId,
|
||||
reason: entity.reason ?? undefined,
|
||||
sourceUrl: entity.sourceUrl ?? undefined,
|
||||
status: entity.status,
|
||||
updatedAt: entity.updatedAt,
|
||||
versionRange: entity.versionRange ?? undefined
|
||||
@@ -282,9 +292,12 @@ export async function getPluginRequirementsSnapshot(serverId: string): Promise<P
|
||||
|
||||
export async function upsertPluginRequirement(options: {
|
||||
actorUserId: string;
|
||||
installUrl?: unknown;
|
||||
manifest?: unknown;
|
||||
pluginId: string;
|
||||
reason?: unknown;
|
||||
serverId: string;
|
||||
sourceUrl?: unknown;
|
||||
status: unknown;
|
||||
versionRange?: unknown;
|
||||
}): Promise<PluginRequirementSummary> {
|
||||
@@ -306,6 +319,9 @@ export async function upsertPluginRequirement(options: {
|
||||
status: status as ServerPluginRequirementStatus,
|
||||
versionRange: normalizeOptionalString(options.versionRange, 128),
|
||||
reason: normalizeOptionalString(options.reason, 512),
|
||||
installUrl: normalizeOptionalString(options.installUrl, 2_000),
|
||||
sourceUrl: normalizeOptionalString(options.sourceUrl, 2_000),
|
||||
manifestJson: options.manifest === undefined ? null : serializeJsonValue(options.manifest, 'INVALID_PLUGIN_MANIFEST_METADATA'),
|
||||
configuredBy: options.actorUserId,
|
||||
createdAt: existing?.createdAt ?? now,
|
||||
updatedAt: now
|
||||
|
||||
Reference in New Issue
Block a user