mirror of
https://github.com/PreMiD/PreMiD.git
synced 2026-04-06 04:41:58 +02:00
Compare commits
13 Commits
api-master
...
api-master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cf3f93889 | ||
|
|
0e30a0d250 | ||
|
|
4dc941bb91 | ||
|
|
3a78c6529e | ||
|
|
d4673720a0 | ||
|
|
dc859448bd | ||
|
|
9cbb88beda | ||
|
|
09bcfe703f | ||
|
|
d24eda8957 | ||
|
|
bfffcb94ee | ||
|
|
4db6a78816 | ||
|
|
666838874f | ||
|
|
697f3660c2 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@premid/api-master",
|
||||
"type": "module",
|
||||
"version": "0.0.31",
|
||||
"version": "0.0.36",
|
||||
"private": true,
|
||||
"description": "PreMiD's api master",
|
||||
"license": "MPL-2.0",
|
||||
@@ -19,9 +19,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@envelop/sentry": "^9.0.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.52.1",
|
||||
"@opentelemetry/node": "^0.24.0",
|
||||
"@sentry/node": "^8.17.0",
|
||||
"cron": "^3.1.7",
|
||||
"debug": "^4.3.6",
|
||||
@@ -30,7 +27,8 @@
|
||||
"ip-location-api": "^1.0.0",
|
||||
"ky": "^1.7.2",
|
||||
"p-limit": "^6.1.0",
|
||||
"postgres": "^3.4.4"
|
||||
"postgres": "^3.4.4",
|
||||
"prom-client": "^15.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.12",
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import type { ServerResponse } from "node:http";
|
||||
import type { Attributes } from "@opentelemetry/api";
|
||||
import { ValueType, diag } from "@opentelemetry/api";
|
||||
import type { PrometheusExporter, PrometheusSerializer } from "@opentelemetry/exporter-prometheus";
|
||||
import { AggregationTemporality, DataPointType, type GaugeMetricData, InstrumentType } from "@opentelemetry/sdk-metrics";
|
||||
|
||||
const registeredMetrics = new Map<string, ClearableGaugeMetric>();
|
||||
|
||||
//* Custom gauge metric class
|
||||
export class ClearableGaugeMetric {
|
||||
private data = new Map<string, { value: number; attributes: Attributes }>();
|
||||
|
||||
constructor(private readonly name: string, private readonly description: string) {
|
||||
registeredMetrics.set(name, this);
|
||||
}
|
||||
|
||||
set(key: string, value: number, attributes: Attributes) {
|
||||
this.data.set(key, { value, attributes });
|
||||
}
|
||||
|
||||
clear({ except }: { except?: string[] }) {
|
||||
for (const key of this.data.keys()) {
|
||||
if (except && except.includes(key))
|
||||
continue;
|
||||
|
||||
this.data.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
toMetricData(): GaugeMetricData {
|
||||
return {
|
||||
descriptor: {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
unit: "",
|
||||
type: InstrumentType.GAUGE,
|
||||
valueType: ValueType.INT,
|
||||
},
|
||||
dataPointType: DataPointType.GAUGE,
|
||||
dataPoints: Array.from(this.data.values()).map(({ value, attributes }) => ({
|
||||
value,
|
||||
attributes,
|
||||
startTime: [0, 0],
|
||||
endTime: [0, 0],
|
||||
})),
|
||||
aggregationTemporality: AggregationTemporality.CUMULATIVE,
|
||||
};
|
||||
}
|
||||
|
||||
get hasData() {
|
||||
return this.data.size > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function updatePrometheusMetrics(prometheusExporter: PrometheusExporter) {
|
||||
// @ts-expect-error We are modifying a private method
|
||||
prometheusExporter._exportMetrics = function (this: PrometheusExporter, response: ServerResponse) {
|
||||
response.statusCode = 200;
|
||||
response.setHeader("content-type", "text/plain");
|
||||
this.collect().then(
|
||||
(collectionResult) => {
|
||||
const { resourceMetrics, errors } = collectionResult;
|
||||
if (errors.length) {
|
||||
diag.error(
|
||||
"PrometheusExporter: metrics collection errors",
|
||||
...errors,
|
||||
);
|
||||
}
|
||||
|
||||
for (const metric of registeredMetrics.values()) {
|
||||
if (metric.hasData) {
|
||||
resourceMetrics.scopeMetrics[0]!.metrics.push(metric.toMetricData());
|
||||
}
|
||||
}
|
||||
|
||||
response.end((this as unknown as { _serializer: PrometheusSerializer })._serializer.serialize(resourceMetrics));
|
||||
},
|
||||
(err) => {
|
||||
response.end(`# failed to export metrics: ${err}`);
|
||||
},
|
||||
);
|
||||
}.bind(prometheusExporter);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { redis } from "../index.js";
|
||||
import { activeSessionsCounter } from "../tracing.js";
|
||||
|
||||
let activeActivities = 0;
|
||||
activeSessionsCounter.add(0);
|
||||
export async function setSessionCounter() {
|
||||
const length = await redis.hlen("pmd-api.sessions");
|
||||
if (length === activeActivities)
|
||||
return;
|
||||
const diff = length - activeActivities;
|
||||
activeActivities = length;
|
||||
activeSessionsCounter.add(diff);
|
||||
}
|
||||
17
apps/api-master/src/functions/setupServer.ts
Normal file
17
apps/api-master/src/functions/setupServer.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import http from "node:http";
|
||||
import { mainLog } from "../index.js";
|
||||
import { register } from "../tracing.js";
|
||||
|
||||
export function setupServer() {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
//* Basic routing logic
|
||||
res.writeHead(200, { "Content-Type": "text/plain" });
|
||||
res.end(await register.metrics());
|
||||
});
|
||||
|
||||
server.listen(9464, () => {
|
||||
mainLog("Server running");
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
@@ -1,67 +1,77 @@
|
||||
import process from "node:process";
|
||||
import pLimit from "p-limit";
|
||||
import type { Gauge } from "prom-client";
|
||||
import { mainLog, redis } from "../index.js";
|
||||
import { activePresenceGauge } from "../tracing.js";
|
||||
import { insertIpData } from "./insertIpData.js";
|
||||
|
||||
export const updateActivePresenceGaugeLimit = pLimit(1);
|
||||
let log: debug.Debugger | undefined;
|
||||
//* Function to update the gauge with per-service counts
|
||||
export async function updateActivePresenceGauge() {
|
||||
|
||||
const scanCount = Number.parseInt(process.env.SCAN_COUNT || "1000", 10);
|
||||
|
||||
export async function updateActivePresenceGauge(gauge: Gauge) {
|
||||
await updateActivePresenceGaugeLimit(async () => {
|
||||
log ??= mainLog.extend("Heartbeat-Updates");
|
||||
log?.("Starting active presence gauge update");
|
||||
|
||||
const pattern = "pmd-api.heartbeatUpdates.*";
|
||||
let cursor: string = "0";
|
||||
const serviceCounts = new Map<string, number>();
|
||||
const ips = new Map<string, {
|
||||
presences: string[];
|
||||
presences: Set<string>;
|
||||
sessions: number;
|
||||
}>();
|
||||
|
||||
do {
|
||||
const [newCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000);
|
||||
const [newCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", scanCount);
|
||||
cursor = newCursor;
|
||||
|
||||
const hashes = await Promise.all(keys.map(key => redis.hmget(key, "service", "version", "ip_address")));
|
||||
for (const hash of hashes) {
|
||||
const service = hash[0];
|
||||
const version = hash[1];
|
||||
const ip = hash[2];
|
||||
if (service && version) {
|
||||
serviceCounts.set(`${service}:${version}`, (serviceCounts.get(`${service}:${version}`) || 0) + 1);
|
||||
}
|
||||
else {
|
||||
serviceCounts.set("none", (serviceCounts.get("none") || 0) + 1);
|
||||
}
|
||||
//* Use pipelining for batch Redis operations
|
||||
const pipeline = redis.pipeline();
|
||||
keys.forEach(key => pipeline.hmget(key, "service", "version", "ip_address"));
|
||||
const hashes = await pipeline.exec();
|
||||
|
||||
if (!hashes) {
|
||||
log?.("No hashes found");
|
||||
return;
|
||||
}
|
||||
|
||||
hashes.forEach(([err, hash]) => {
|
||||
if (err || !Array.isArray(hash))
|
||||
return;
|
||||
|
||||
const [service, version, ip] = hash;
|
||||
const serviceVersion = service && version ? `${service}:${version}` : "none";
|
||||
serviceCounts.set(serviceVersion, (serviceCounts.get(serviceVersion) || 0) + 1);
|
||||
|
||||
if (ip) {
|
||||
const presenceName = service && version ? `${service}:${version}` : undefined;
|
||||
|
||||
const ipData = ips.get(ip) || { presences: [], sessions: 0 };
|
||||
if (presenceName) {
|
||||
ipData.presences.push(presenceName);
|
||||
ipData.presences = Array.from(new Set(ipData.presences.filter(Boolean)));
|
||||
}
|
||||
|
||||
const ipData = ips.get(ip) || { presences: new Set(), sessions: 0 };
|
||||
if (serviceVersion !== "none")
|
||||
ipData.presences.add(serviceVersion);
|
||||
ipData.sessions++;
|
||||
ips.set(ip, ipData);
|
||||
}
|
||||
}
|
||||
});
|
||||
} while (cursor !== "0");
|
||||
|
||||
log?.("Updating active presence gauge");
|
||||
|
||||
// Clear previous data
|
||||
activePresenceGauge.clear({ except: [...serviceCounts.keys()] });
|
||||
|
||||
// Set new data
|
||||
for (const [serviceVersion, count] of serviceCounts.entries()) {
|
||||
//* Batch update the gauge
|
||||
gauge.reset();
|
||||
for (const [serviceVersion, count] of serviceCounts) {
|
||||
const [presence_name, version] = serviceVersion.split(":");
|
||||
activePresenceGauge.set(serviceVersion, count, {
|
||||
presence_name,
|
||||
version,
|
||||
});
|
||||
gauge.set({ presence_name, version }, count);
|
||||
}
|
||||
|
||||
insertIpData(ips);
|
||||
//* Convert IP data for insertion
|
||||
const ipDataForInsertion = new Map(
|
||||
Array.from(ips, ([ip, data]) => [ip, {
|
||||
presences: Array.from(data.presences),
|
||||
sessions: data.sessions,
|
||||
}]),
|
||||
);
|
||||
|
||||
await insertIpData(ipDataForInsertion);
|
||||
log?.("Active presence gauge update completed");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,14 +3,15 @@ import { CronJob } from "cron";
|
||||
import debug from "debug";
|
||||
import { clearOldSessions } from "./functions/clearOldSessions.js";
|
||||
import createRedis from "./functions/createRedis.js";
|
||||
import { setSessionCounter } from "./functions/setSessionCounter.js";
|
||||
import "./tracing.js";
|
||||
import { updateActivePresenceGauge, updateActivePresenceGaugeLimit } from "./functions/updateActivePresenceGauge.js";
|
||||
// import { reloadIpLocationApi } from "./functions/lookupIp.js";
|
||||
import { cleanupOldUserData } from "./functions/cleanupOldUserData.js";
|
||||
import { setupServer } from "./functions/setupServer.js";
|
||||
|
||||
export const redis = createRedis();
|
||||
|
||||
export const server = setupServer();
|
||||
|
||||
export const mainLog = debug("api-master");
|
||||
|
||||
debug("Starting cron jobs");
|
||||
@@ -22,13 +23,6 @@ void new CronJob(
|
||||
if (process.env.DISABLE_CLEAR_OLD_SESSIONS !== "true") {
|
||||
clearOldSessions();
|
||||
}
|
||||
if (process.env.DISABLE_SET_SESSION_COUNTER !== "true") {
|
||||
setSessionCounter();
|
||||
}
|
||||
if (process.env.DISABLE_ACTIVE_PRESENCE_GAUGE !== "true") {
|
||||
updateActivePresenceGaugeLimit.clearQueue();
|
||||
updateActivePresenceGauge();
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
import { ValueType } from "@opentelemetry/api";
|
||||
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
import { ClearableGaugeMetric, updatePrometheusMetrics } from "./functions/clearableGaugeMetric.js";
|
||||
import process from "node:process";
|
||||
import { Counter, Gauge, Registry, collectDefaultMetrics } from "prom-client";
|
||||
import { updateActivePresenceGauge, updateActivePresenceGaugeLimit } from "./functions/updateActivePresenceGauge.js";
|
||||
import { redis } from "./index.js";
|
||||
|
||||
const prometheusExporter = new PrometheusExporter();
|
||||
const scanCount = Number.parseInt(process.env.SCAN_COUNT || "1000", 10);
|
||||
|
||||
const provider = new MeterProvider({
|
||||
readers: [prometheusExporter],
|
||||
export const register = new Registry();
|
||||
collectDefaultMetrics({ register });
|
||||
|
||||
export const activeSessionsCounter = new Counter({
|
||||
name: "active_sessions",
|
||||
help: "Number of active sessions",
|
||||
async collect() {
|
||||
this.reset();
|
||||
let length = 0;
|
||||
let cursor = "0";
|
||||
do {
|
||||
const reply = await redis.scan(cursor, "MATCH", "pmd-api.sessions.*", "COUNT", scanCount);
|
||||
cursor = reply[0];
|
||||
length += reply[1].length;
|
||||
} while (cursor !== "0");
|
||||
this.inc(length);
|
||||
},
|
||||
});
|
||||
|
||||
const meter = provider.getMeter("nice");
|
||||
|
||||
export const activeSessionsCounter = meter.createUpDownCounter("active_sessions", {
|
||||
description: "Number of active sessions",
|
||||
valueType: ValueType.INT,
|
||||
export const activePresencesCounter = new Gauge({
|
||||
name: "active_presences",
|
||||
help: "Number of active presences",
|
||||
labelNames: ["presence_name", "version"],
|
||||
async collect() {
|
||||
if (process.env.DISABLE_ACTIVE_PRESENCE_GAUGE !== "true") {
|
||||
this.reset();
|
||||
updateActivePresenceGaugeLimit.clearQueue();
|
||||
await updateActivePresenceGauge(this);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const activePresenceGauge = new ClearableGaugeMetric(
|
||||
"active_presences",
|
||||
"Per presence name+version, active number of users",
|
||||
);
|
||||
|
||||
updatePrometheusMetrics(prometheusExporter);
|
||||
|
||||
prometheusExporter.startServer();
|
||||
register.registerMetric(activeSessionsCounter);
|
||||
register.registerMetric(activePresencesCounter);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@premid/schema-server",
|
||||
"type": "module",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"private": true,
|
||||
"description": "A small service to serve the JSON schemas for PreMiD",
|
||||
"license": "MPL-2.0",
|
||||
|
||||
260
apps/schema-server/schemas/metadata/1.11.json
Normal file
260
apps/schema-server/schemas/metadata/1.11.json
Normal file
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://schemas.premid.app/metadata/1.11",
|
||||
"title": "Metadata",
|
||||
"type": "object",
|
||||
"description": "Metadata that describes a presence.",
|
||||
"definitions": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"description": "User information.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the user."
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The Discord snowflake of the user.",
|
||||
"pattern": "^\\d+$"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
|
||||
"type": "string",
|
||||
"description": "The metadata schema URL."
|
||||
},
|
||||
"author": {
|
||||
"$ref": "#/definitions/user",
|
||||
"description": "The author of this presence."
|
||||
},
|
||||
"contributors": {
|
||||
"type": "array",
|
||||
"description": "Any extra contributors to this presence.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/user"
|
||||
}
|
||||
},
|
||||
"service": {
|
||||
"type": "string",
|
||||
"description": "The service this presence is for."
|
||||
},
|
||||
"altnames": {
|
||||
"type": "array",
|
||||
"description": "Alternative names for the service.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "An alternative name."
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "object",
|
||||
"description": "A description of the presence in multiple languages.",
|
||||
"propertyNames": {
|
||||
"type": "string",
|
||||
"description": "The language key. The key must be languagecode(_REGIONCODE).",
|
||||
"pattern": "^[a-z]{2}(?:_(?:[A-Z]{2}|[0-9]{1,3}))?$"
|
||||
},
|
||||
"patternProperties": {
|
||||
"^[a-z]{2}(?:_(?:[A-Z]{2}|[0-9]{1,3}))?$": {
|
||||
"type": "string",
|
||||
"description": "The description of the presence in the key's language."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"en"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": [
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
|
||||
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "One of the service's website URLs.",
|
||||
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
|
||||
},
|
||||
"minItems": 2
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "The SemVer version of the presence. Must just be major.minor.patch.",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+$"
|
||||
},
|
||||
"apiVersion": {
|
||||
"type": "integer",
|
||||
"description": "The Presence System version this Presence supports.",
|
||||
"minimum": 1,
|
||||
"maximum": 2
|
||||
},
|
||||
"logo": {
|
||||
"type": "string",
|
||||
"description": "The logo of the service this presence is for.",
|
||||
"pattern": "^https?://.+\\.(png|jpe?g|gif|webp)$"
|
||||
},
|
||||
"thumbnail": {
|
||||
"type": "string",
|
||||
"description": "A thumbnail of the service this presence is for.",
|
||||
"pattern": "^https?://.+\\.(png|jpe?g|gif|webp)$"
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"description": "The theme color of the service this presence is for. Must be either a 6 digit or a 3 digit hex code.",
|
||||
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
|
||||
},
|
||||
"tags": {
|
||||
"type": [
|
||||
"array"
|
||||
],
|
||||
"description": "The tags for the presence.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "A tag.",
|
||||
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "The category the presence falls under.",
|
||||
"enum": [
|
||||
"anime",
|
||||
"games",
|
||||
"music",
|
||||
"socials",
|
||||
"videos",
|
||||
"other"
|
||||
]
|
||||
},
|
||||
"iframe": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not the presence should run in IFrames."
|
||||
},
|
||||
"readLogs": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not the extension should be reading logs."
|
||||
},
|
||||
"regExp": {
|
||||
"type": "string",
|
||||
"description": "A regular expression used to match URLs for the presence to inject into."
|
||||
},
|
||||
"iFrameRegExp": {
|
||||
"type": "string",
|
||||
"description": "A regular expression used to match IFrames for the presence to inject into."
|
||||
},
|
||||
"button": {
|
||||
"type": "boolean",
|
||||
"description": "Controls whether the presence is automatically added when the extension is installed. For partner presences only."
|
||||
},
|
||||
"warning": {
|
||||
"type": "boolean",
|
||||
"description": "Shows a warning saying that it requires additional steps for the presence to function correctly."
|
||||
},
|
||||
"settings": {
|
||||
"type": "array",
|
||||
"description": "An array of settings the user can change in the presence.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "A setting.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The ID of the setting."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
|
||||
"pattern": "^fa([bsdrlt]|([-](brands|solid|duotone|regular|light|thin))) fa-[0-9a-z-]+$"
|
||||
},
|
||||
"if": {
|
||||
"type": "object",
|
||||
"description": "Restrict showing this setting if another setting is the defined value.",
|
||||
"propertyNames": {
|
||||
"type": "string",
|
||||
"description": "The ID of the setting."
|
||||
},
|
||||
"patternProperties": {
|
||||
"": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
],
|
||||
"description": "The value of the setting."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string",
|
||||
"description": "The placeholder for settings that require input. Shown when the input is empty."
|
||||
},
|
||||
"value": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
],
|
||||
"description": "The default value of the setting. Not compatible with `values`."
|
||||
},
|
||||
"values": {
|
||||
"type": "array",
|
||||
"description": "The default values of the setting. Not compatible with `value`.",
|
||||
"items": {
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
],
|
||||
"description": "The value of the setting."
|
||||
}
|
||||
},
|
||||
"multiLanguage": {
|
||||
"type": [
|
||||
"string",
|
||||
"boolean",
|
||||
"array"
|
||||
],
|
||||
"description": "When false, multi-localization is disabled. When true, strings from the `general.json` file are available for use. When a string, it is the name of a file (excluding .json) of a used language from the localization GitHub repo. When an array of strings, it is all of the file names (excluding .json) of used languages from the localization GitHub repo.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "The name of a file from the localization GitHub repository."
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"author",
|
||||
"service",
|
||||
"description",
|
||||
"url",
|
||||
"version",
|
||||
"apiVersion",
|
||||
"logo",
|
||||
"thumbnail",
|
||||
"color",
|
||||
"tags",
|
||||
"category"
|
||||
]
|
||||
}
|
||||
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
@@ -61,15 +61,6 @@ importers:
|
||||
'@envelop/sentry':
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0(@envelop/core@5.0.2)(@sentry/node@8.30.0)(graphql@16.9.0)
|
||||
'@opentelemetry/api':
|
||||
specifier: ^1.9.0
|
||||
version: 1.9.0
|
||||
'@opentelemetry/exporter-prometheus':
|
||||
specifier: ^0.52.1
|
||||
version: 0.52.1(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/node':
|
||||
specifier: ^0.24.0
|
||||
version: 0.24.0(@opentelemetry/api@1.9.0)
|
||||
'@sentry/node':
|
||||
specifier: ^8.17.0
|
||||
version: 8.30.0
|
||||
@@ -97,6 +88,9 @@ importers:
|
||||
postgres:
|
||||
specifier: ^3.4.4
|
||||
version: 3.4.4
|
||||
prom-client:
|
||||
specifier: ^15.1.3
|
||||
version: 15.1.3
|
||||
devDependencies:
|
||||
'@types/debug':
|
||||
specifier: ^4.1.12
|
||||
@@ -3123,7 +3117,6 @@ packages:
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.21.2':
|
||||
resolution: {integrity: sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.18.1':
|
||||
@@ -4246,6 +4239,9 @@ packages:
|
||||
bindings@1.5.0:
|
||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||
|
||||
bintrees@1.0.2:
|
||||
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
|
||||
|
||||
birpc@0.2.17:
|
||||
resolution: {integrity: sha512-+hkTxhot+dWsLpp3gia5AkVHIsKlZybNT5gIYiDlNzJrmYPcTM9k5/w2uaj3IPpd7LlEYpmCj4Jj1nC41VhDFg==}
|
||||
|
||||
@@ -7657,6 +7653,10 @@ packages:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
prom-client@15.1.3:
|
||||
resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==}
|
||||
engines: {node: ^16 || ^18 || >=20}
|
||||
|
||||
promise@7.3.1:
|
||||
resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
|
||||
|
||||
@@ -8326,6 +8326,9 @@ packages:
|
||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
tdigest@0.1.2:
|
||||
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
|
||||
|
||||
terser-webpack-plugin@5.3.10:
|
||||
resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
@@ -13954,7 +13957,7 @@ snapshots:
|
||||
pathe: 1.1.2
|
||||
sirv: 2.0.4
|
||||
tinyrainbow: 1.2.0
|
||||
vitest: 2.0.5(@types/node@20.16.5)(@vitest/ui@2.0.5)(happy-dom@15.7.4)(sass@1.78.0)(terser@5.32.0)
|
||||
vitest: 2.0.5(@types/node@22.5.4)(@vitest/ui@2.0.5)(happy-dom@15.0.0)(sass@1.78.0)(terser@5.32.0)
|
||||
|
||||
'@vitest/utils@2.0.5':
|
||||
dependencies:
|
||||
@@ -14654,6 +14657,8 @@ snapshots:
|
||||
dependencies:
|
||||
file-uri-to-path: 1.0.0
|
||||
|
||||
bintrees@1.0.2: {}
|
||||
|
||||
birpc@0.2.17: {}
|
||||
|
||||
bl@4.1.0:
|
||||
@@ -18889,6 +18894,11 @@ snapshots:
|
||||
|
||||
process@0.11.10: {}
|
||||
|
||||
prom-client@15.1.3:
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
tdigest: 0.1.2
|
||||
|
||||
promise@7.3.1:
|
||||
dependencies:
|
||||
asap: 2.0.6
|
||||
@@ -19639,6 +19649,10 @@ snapshots:
|
||||
mkdirp: 1.0.4
|
||||
yallist: 4.0.0
|
||||
|
||||
tdigest@0.1.2:
|
||||
dependencies:
|
||||
bintrees: 1.0.2
|
||||
|
||||
terser-webpack-plugin@5.3.10(esbuild@0.23.1)(webpack@5.94.0(esbuild@0.23.1)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
|
||||
Reference in New Issue
Block a user