pmxt-core 2.44.3 → 2.44.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/errors.d.ts +6 -0
  2. package/dist/errors.js +10 -1
  3. package/dist/exchanges/kalshi/api.d.ts +1 -1
  4. package/dist/exchanges/kalshi/api.js +1 -1
  5. package/dist/exchanges/limitless/api.d.ts +1 -1
  6. package/dist/exchanges/limitless/api.js +1 -1
  7. package/dist/exchanges/mock/index.d.ts +3 -2
  8. package/dist/exchanges/mock/index.js +14 -5
  9. package/dist/exchanges/myriad/api.d.ts +1 -1
  10. package/dist/exchanges/myriad/api.js +1 -1
  11. package/dist/exchanges/opinion/api.d.ts +1 -1
  12. package/dist/exchanges/opinion/api.js +1 -1
  13. package/dist/exchanges/polymarket/api-clob.d.ts +1 -1
  14. package/dist/exchanges/polymarket/api-clob.js +1 -1
  15. package/dist/exchanges/polymarket/api-data.d.ts +1 -1
  16. package/dist/exchanges/polymarket/api-data.js +1 -1
  17. package/dist/exchanges/polymarket/api-gamma.d.ts +1 -1
  18. package/dist/exchanges/polymarket/api-gamma.js +1 -1
  19. package/dist/exchanges/probable/api.d.ts +1 -1
  20. package/dist/exchanges/probable/api.js +1 -1
  21. package/dist/feeds/binance/binance-feed.d.ts +9 -0
  22. package/dist/feeds/binance/binance-feed.js +34 -7
  23. package/dist/feeds/chainlink/chainlink-feed.d.ts +14 -0
  24. package/dist/feeds/chainlink/chainlink-feed.js +62 -7
  25. package/dist/feeds/interfaces.d.ts +10 -0
  26. package/dist/router/Router.d.ts +9 -0
  27. package/dist/router/Router.js +153 -2
  28. package/dist/router/types.d.ts +5 -0
  29. package/dist/server/app.d.ts +26 -2
  30. package/dist/server/app.js +50 -9
  31. package/dist/server/feed-routes.js +34 -12
  32. package/dist/server/sql-route.d.ts +2 -0
  33. package/dist/server/sql-route.js +277 -0
  34. package/package.json +3 -3
@@ -0,0 +1,277 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSqlRouter = createSqlRouter;
4
+ const express_1 = require("express");
5
+ const MAX_QUERY_LENGTH = 10_000;
6
+ const ALLOWED_FIRST_KEYWORDS = new Set([
7
+ "SELECT",
8
+ "WITH",
9
+ "SHOW",
10
+ "DESCRIBE",
11
+ "DESC",
12
+ "EXISTS",
13
+ "EXPLAIN",
14
+ ]);
15
+ const WRITE_KEYWORDS = [
16
+ "INSERT",
17
+ "CREATE",
18
+ "DROP",
19
+ "ALTER",
20
+ "DELETE",
21
+ "TRUNCATE",
22
+ "ATTACH",
23
+ "DETACH",
24
+ "GRANT",
25
+ "REVOKE",
26
+ "KILL",
27
+ "RENAME",
28
+ ];
29
+ const BLOCKED_FUNCTIONS = new Set([
30
+ "hostname",
31
+ "fqdn",
32
+ "displayname",
33
+ "version",
34
+ "buildid",
35
+ "serveruuid",
36
+ "uptime",
37
+ "tcpport",
38
+ "currentuser",
39
+ "user",
40
+ "currentdatabase",
41
+ "currentprofiles",
42
+ "enabledprofiles",
43
+ "defaultprofiles",
44
+ "currentroles",
45
+ "enabledroles",
46
+ "defaultroles",
47
+ "filesystemavailable",
48
+ "filesystemcapacity",
49
+ "filesystemunreserved",
50
+ "getsetting",
51
+ "getmacro",
52
+ ]);
53
+ const BLOCKED_DB_PREFIXES = ["system.", "information_schema."];
54
+ function stripComments(sql) {
55
+ return sql
56
+ .replace(/\/\*[\s\S]*?\*\//g, " ")
57
+ .replace(/--[^\n]*/g, " ")
58
+ .trim();
59
+ }
60
+ function stripStrings(sql) {
61
+ return sql.replace(/'(?:[^'\\]|\\.)*'/g, "''");
62
+ }
63
+ function validateSqlQuery(sql) {
64
+ if (typeof sql !== "string") {
65
+ return { valid: false, error: "query is required" };
66
+ }
67
+ const trimmed = sql.trim();
68
+ if (trimmed.length === 0) {
69
+ return { valid: false, error: "query is required" };
70
+ }
71
+ if (trimmed.length > MAX_QUERY_LENGTH) {
72
+ return {
73
+ valid: false,
74
+ error: `query exceeds maximum length (${MAX_QUERY_LENGTH} chars)`,
75
+ };
76
+ }
77
+ const withoutStrings = stripStrings(trimmed);
78
+ if (withoutStrings.includes(";")) {
79
+ return { valid: false, error: "multi-statement queries are not allowed" };
80
+ }
81
+ const stripped = stripComments(trimmed);
82
+ const firstWord = (stripped.split(/\s+/)[0] || "").toUpperCase();
83
+ if (!ALLOWED_FIRST_KEYWORDS.has(firstWord)) {
84
+ return {
85
+ valid: false,
86
+ error: `query type "${firstWord}" is not allowed - only SELECT, WITH, ` +
87
+ "SHOW, DESCRIBE, and EXPLAIN are permitted",
88
+ };
89
+ }
90
+ if (firstWord === "SHOW") {
91
+ const upper = stripped.toUpperCase();
92
+ if (/^SHOW\s+(GRANTS|CREATE|ACCESS|PROCESSLIST|SETTINGS)/.test(upper)) {
93
+ return { valid: false, error: "that SHOW command is not allowed" };
94
+ }
95
+ }
96
+ const safeText = stripStrings(stripped);
97
+ for (const keyword of WRITE_KEYWORDS) {
98
+ if (new RegExp(`\\b${keyword}\\b`, "i").test(safeText)) {
99
+ return { valid: false, error: `"${keyword}" is not allowed in queries` };
100
+ }
101
+ }
102
+ const lower = safeText.toLowerCase();
103
+ for (const prefix of BLOCKED_DB_PREFIXES) {
104
+ if (lower.includes(prefix)) {
105
+ return {
106
+ valid: false,
107
+ error: "queries against system tables are not allowed",
108
+ };
109
+ }
110
+ }
111
+ const funcCalls = lower.matchAll(/\b([a-z_][a-z0-9_]*)\s*\(/g);
112
+ for (const match of funcCalls) {
113
+ if (BLOCKED_FUNCTIONS.has(match[1])) {
114
+ return { valid: false, error: `function "${match[1]}" is not allowed` };
115
+ }
116
+ }
117
+ return { valid: true, query: trimmed };
118
+ }
119
+ function allowedPlans() {
120
+ return new Set((process.env.SQL_ALLOWED_PLANS || "Enterprise")
121
+ .split(",")
122
+ .map((plan) => plan.trim().toLowerCase())
123
+ .filter(Boolean));
124
+ }
125
+ function hasSqlAccess(req) {
126
+ if (!req.account)
127
+ return true;
128
+ const plan = req.account.plan_name ?? req.account.plan ?? "";
129
+ return typeof plan === "string" && allowedPlans().has(plan.toLowerCase());
130
+ }
131
+ function isConfigured() {
132
+ return Boolean(process.env.CLICKHOUSE_HTTP_URL);
133
+ }
134
+ function scrubErrorMessage(message) {
135
+ const user = process.env.CLICKHOUSE_SQL_USER || "readonly";
136
+ const escapedUser = user.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
137
+ return message
138
+ .replace(/\s*\(version\s+[\d.]+ \(official build\)\)/gi, "")
139
+ .replace(new RegExp(`${escapedUser}:\\s*`, "g"), "");
140
+ }
141
+ function extractErrorMessage(body) {
142
+ if (typeof body !== "string")
143
+ return String(body);
144
+ const trimmed = body.trim();
145
+ if (trimmed.startsWith("{")) {
146
+ try {
147
+ const parsed = JSON.parse(trimmed);
148
+ return scrubErrorMessage(parsed.exception || parsed.error || trimmed);
149
+ }
150
+ catch {
151
+ // Fall through to plain-text handling.
152
+ }
153
+ }
154
+ return scrubErrorMessage(trimmed.split("\n")[0] || "ClickHouse query failed");
155
+ }
156
+ async function queryClickHouse(sql) {
157
+ const baseUrl = process.env.CLICKHOUSE_HTTP_URL;
158
+ if (!baseUrl) {
159
+ throw Object.assign(new Error("SQL query service is not available"), {
160
+ chStatusCode: 503,
161
+ });
162
+ }
163
+ let url;
164
+ try {
165
+ url = new URL("/", baseUrl);
166
+ }
167
+ catch {
168
+ throw Object.assign(new Error("CLICKHOUSE_HTTP_URL is not a valid URL"), {
169
+ chStatusCode: 503,
170
+ });
171
+ }
172
+ url.searchParams.set("default_format", "JSON");
173
+ url.searchParams.set("readonly", "1");
174
+ url.searchParams.set("allow_ddl", "0");
175
+ url.searchParams.set("allow_introspection_functions", "0");
176
+ url.searchParams.set("max_execution_time", "5");
177
+ url.searchParams.set("max_result_rows", "10000");
178
+ const user = process.env.CLICKHOUSE_SQL_USER || "readonly";
179
+ const password = process.env.CLICKHOUSE_SQL_PASSWORD || "";
180
+ const auth = Buffer.from(`${user}:${password}`).toString("base64");
181
+ const response = await fetch(url, {
182
+ method: "POST",
183
+ headers: {
184
+ Authorization: `Basic ${auth}`,
185
+ "Content-Type": "text/plain",
186
+ },
187
+ body: sql,
188
+ signal: AbortSignal.timeout(6_000),
189
+ });
190
+ if (!response.ok) {
191
+ const body = await response.text();
192
+ const error = new Error(extractErrorMessage(body));
193
+ error.chStatusCode = response.status;
194
+ throw error;
195
+ }
196
+ const result = (await response.json());
197
+ if (result.exception) {
198
+ const error = new Error(extractErrorMessage(result.exception));
199
+ error.chStatusCode = 400;
200
+ throw error;
201
+ }
202
+ return result;
203
+ }
204
+ function extractQuery(req) {
205
+ return req.body?.query ?? req.query?.query;
206
+ }
207
+ async function handleQuery(req, res, next) {
208
+ try {
209
+ const validation = validateSqlQuery(extractQuery(req));
210
+ if (!validation.valid) {
211
+ res.status(400).json({ error: validation.error });
212
+ return;
213
+ }
214
+ if (!hasSqlAccess(req)) {
215
+ res.status(403).json({
216
+ error: "sql_access_denied",
217
+ message: "SQL query access requires an Enterprise plan",
218
+ });
219
+ return;
220
+ }
221
+ if (!isConfigured()) {
222
+ res.status(503).json({
223
+ error: "service_unavailable",
224
+ message: "SQL query service is not available. Configure CLICKHOUSE_HTTP_URL " +
225
+ "or use the hosted PMXT Enterprise SQL endpoint.",
226
+ });
227
+ return;
228
+ }
229
+ const result = await queryClickHouse(validation.query);
230
+ res.json({
231
+ data: result.data || [],
232
+ meta: {
233
+ columns: result.meta || [],
234
+ rows: result.rows ?? result.data?.length ?? 0,
235
+ statistics: result.statistics || {},
236
+ },
237
+ });
238
+ }
239
+ catch (error) {
240
+ const err = error;
241
+ if (err.chStatusCode === 503) {
242
+ res.status(503).json({
243
+ error: "service_unavailable",
244
+ message: err.message,
245
+ });
246
+ return;
247
+ }
248
+ if (err.chStatusCode === 401 || err.chStatusCode === 403) {
249
+ res.status(502).json({
250
+ error: "database_error",
251
+ message: "Database connection failed",
252
+ });
253
+ return;
254
+ }
255
+ if (err.chStatusCode) {
256
+ res.status(400).json({
257
+ error: "query_error",
258
+ message: err.message,
259
+ });
260
+ return;
261
+ }
262
+ if (err.name === "TimeoutError" || err.name === "AbortError") {
263
+ res.status(408).json({
264
+ error: "query_timeout",
265
+ message: "Query exceeded the maximum execution time (5s)",
266
+ });
267
+ return;
268
+ }
269
+ next(error);
270
+ }
271
+ }
272
+ function createSqlRouter() {
273
+ const router = (0, express_1.Router)();
274
+ router.post("/", handleQuery);
275
+ router.get("/", handleQuery);
276
+ return router;
277
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmxt-core",
3
- "version": "2.44.3",
3
+ "version": "2.44.5",
4
4
  "description": "pmxt is a unified prediction market data API. The ccxt for prediction markets.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -29,8 +29,8 @@
29
29
  "test": "jest -c jest.config.js",
30
30
  "server": "tsx watch src/server/index.ts",
31
31
  "server:prod": "node dist/server/index.js",
32
- "generate:sdk:python": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g python -o ../sdks/python/generated --package-name pmxt_internal --additional-properties=projectName=pmxt-internal,packageVersion=2.44.3,library=urllib3",
33
- "generate:sdk:typescript": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g typescript-fetch -o ../sdks/typescript/generated --additional-properties=npmName=pmxtjs,npmVersion=2.44.3,supportsES6=true,typescriptThreePlus=true && node ../sdks/typescript/scripts/fix-generated.js",
32
+ "generate:sdk:python": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g python -o ../sdks/python/generated --package-name pmxt_internal --additional-properties=projectName=pmxt-internal,packageVersion=2.44.5,library=urllib3",
33
+ "generate:sdk:typescript": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g typescript-fetch -o ../sdks/typescript/generated --additional-properties=npmName=pmxtjs,npmVersion=2.44.5,supportsES6=true,typescriptThreePlus=true && node ../sdks/typescript/scripts/fix-generated.js",
34
34
  "fetch:openapi": "node scripts/fetch-openapi-specs.js",
35
35
  "extract:jsdoc": "node ../scripts/extract-jsdoc.js",
36
36
  "generate:docs": "npm run extract:jsdoc && node ../scripts/generate-api-docs.js",