keryx 0.29.10 → 0.29.11

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.
@@ -62,10 +62,10 @@ export class Swagger implements Action {
62
62
  web = { route: "/swagger", method: HTTP_METHOD.GET };
63
63
 
64
64
  async run() {
65
- const paths: Record<string, any> = {};
65
+ const paths: Record<string, Record<string, unknown>> = {};
66
66
  const components: {
67
- schemas: Record<string, any>;
68
- securitySchemes?: Record<string, any>;
67
+ schemas: Record<string, unknown>;
68
+ securitySchemes?: Record<string, unknown>;
69
69
  } = {
70
70
  schemas: {},
71
71
  securitySchemes: {
@@ -94,21 +94,23 @@ export class Swagger implements Action {
94
94
  const description = action.description;
95
95
 
96
96
  // Extract path parameters from the original route
97
- const parameters: any[] = [];
97
+ const parameters: Array<Record<string, unknown>> = [];
98
98
  const pathParamMatches = action.web.route.match(/:\w+/g) || [];
99
99
  const pathParamNames = new Set<string>();
100
100
 
101
101
  // Pre-compute Zod JSON Schema for enriching path param types
102
- let zodProperties: Record<string, any> = {};
102
+ let zodProperties: Record<string, Record<string, unknown>> = {};
103
103
  let zodDescriptions: Record<string, string> = {};
104
104
  if (action.inputs && typeof action.inputs.parse === "function") {
105
105
  const jsonSchema = z.toJSONSchema(action.inputs, {
106
106
  io: "input",
107
107
  unrepresentable: "any",
108
- }) as any;
109
- zodProperties = jsonSchema.properties ?? {};
110
- for (const [name, propSchema] of Object.entries<any>(zodProperties)) {
111
- if (propSchema.description) {
108
+ }) as Record<string, unknown>;
109
+ zodProperties =
110
+ (jsonSchema.properties as Record<string, Record<string, unknown>>) ??
111
+ {};
112
+ for (const [name, propSchema] of Object.entries(zodProperties)) {
113
+ if (typeof propSchema.description === "string") {
112
114
  zodDescriptions[name] = propSchema.description;
113
115
  }
114
116
  }
@@ -135,16 +137,18 @@ export class Swagger implements Action {
135
137
  const fullSchema = z.toJSONSchema(action.inputs!, {
136
138
  io: "input",
137
139
  unrepresentable: "any",
138
- }) as any;
139
- const requiredFields = new Set<string>(fullSchema.required ?? []);
140
- for (const [name, propSchema] of Object.entries<any>(zodProperties)) {
140
+ }) as Record<string, unknown>;
141
+ const requiredFields = new Set<string>(
142
+ (fullSchema.required as string[] | undefined) ?? [],
143
+ );
144
+ for (const [name, propSchema] of Object.entries(zodProperties)) {
141
145
  if (pathParamNames.has(name)) continue; // already a path param
142
146
  parameters.push({
143
147
  name,
144
148
  in: "query",
145
149
  required: requiredFields.has(name),
146
150
  schema: propSchema,
147
- ...(propSchema.description
151
+ ...(typeof propSchema.description === "string"
148
152
  ? { description: propSchema.description }
149
153
  : {}),
150
154
  });
@@ -152,7 +156,7 @@ export class Swagger implements Action {
152
156
  }
153
157
 
154
158
  // Build requestBody if Zod inputs exist and method supports body
155
- let requestBody: any = undefined;
159
+ let requestBody: Record<string, unknown> | undefined = undefined;
156
160
  if (
157
161
  action.inputs &&
158
162
  typeof action.inputs.parse === "function" &&
@@ -166,9 +170,9 @@ export class Swagger implements Action {
166
170
  const jsonSchema = z.toJSONSchema(zodSchema, {
167
171
  io: "input",
168
172
  unrepresentable: "any",
169
- });
173
+ }) as Record<string, unknown>;
170
174
  // Remove $schema from component schemas (not needed in OpenAPI)
171
- const { $schema, ...schemaWithout$schema } = jsonSchema as any;
175
+ const { $schema, ...schemaWithout$schema } = jsonSchema;
172
176
  components.schemas[schemaName] = schemaWithout$schema;
173
177
  requestBody = {
174
178
  required: true,
@@ -181,7 +185,9 @@ export class Swagger implements Action {
181
185
  }
182
186
 
183
187
  // Build responses - use generated schema if available
184
- const responses = JSON.parse(JSON.stringify(swaggerResponses));
188
+ const responses: Record<string, unknown> = JSON.parse(
189
+ JSON.stringify(swaggerResponses),
190
+ );
185
191
 
186
192
  if (action.web?.streaming) {
187
193
  // Streaming endpoints return SSE
@@ -70,7 +70,7 @@ export const configServerWeb = {
70
70
  compression: {
71
71
  enabled: await loadFromEnvIfSet("WEB_COMPRESSION_ENABLED", true),
72
72
  threshold: await loadFromEnvIfSet("WEB_COMPRESSION_THRESHOLD", 1024),
73
- encodings: ["br", "gzip"] as ("br" | "gzip")[],
73
+ encodings: ["gzip"] as "gzip"[],
74
74
  },
75
75
  correlationId: {
76
76
  header: await loadFromEnvIfSet("WEB_CORRELATION_ID_HEADER", "X-Request-Id"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keryx",
3
- "version": "0.29.10",
3
+ "version": "0.29.11",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/util/config.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  type MergeMode = "overwrite" | "defaults";
2
2
 
3
- function mergeWith<T extends Record<string, any>>(
3
+ function mergeWith<T extends Record<string, unknown>>(
4
4
  target: T,
5
- source: Record<string, any>,
5
+ source: Record<string, unknown>,
6
6
  mode: MergeMode,
7
7
  ): T {
8
+ const writable = target as Record<string, unknown>;
8
9
  for (const key of Object.keys(source)) {
9
10
  const targetVal = target[key];
10
11
  const sourceVal = source[key];
@@ -17,9 +18,13 @@ function mergeWith<T extends Record<string, any>>(
17
18
  !Array.isArray(sourceVal);
18
19
 
19
20
  if (bothPlainObjects) {
20
- mergeWith(targetVal, sourceVal, mode);
21
+ mergeWith(
22
+ targetVal as Record<string, unknown>,
23
+ sourceVal as Record<string, unknown>,
24
+ mode,
25
+ );
21
26
  } else if (mode === "overwrite" || !(key in target)) {
22
- (target as any)[key] = sourceVal;
27
+ writable[key] = sourceVal;
23
28
  }
24
29
  }
25
30
 
@@ -30,9 +35,9 @@ function mergeWith<T extends Record<string, any>>(
30
35
  Deep-merges source into target, mutating target in place.
31
36
  Only plain objects are recursively merged; arrays and primitives are overwritten.
32
37
  */
33
- export function deepMerge<T extends Record<string, any>>(
38
+ export function deepMerge<T extends Record<string, unknown>>(
34
39
  target: T,
35
- source: Record<string, any>,
40
+ source: Record<string, unknown>,
36
41
  ): T {
37
42
  return mergeWith(target, source, "overwrite");
38
43
  }
@@ -41,9 +46,9 @@ export function deepMerge<T extends Record<string, any>>(
41
46
  * Like `deepMerge`, but only sets values that don't already exist in target.
42
47
  * Useful for applying plugin config defaults without overwriting user-set values.
43
48
  */
44
- export function deepMergeDefaults<T extends Record<string, any>>(
49
+ export function deepMergeDefaults<T extends Record<string, unknown>>(
45
50
  target: T,
46
- source: Record<string, any>,
51
+ source: Record<string, unknown>,
47
52
  ): T {
48
53
  return mergeWith(target, source, "defaults");
49
54
  }
package/util/mcpServer.ts CHANGED
@@ -3,6 +3,11 @@ import {
3
3
  ResourceTemplate,
4
4
  } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import type { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
6
+ import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
7
+ import type {
8
+ ServerNotification,
9
+ ServerRequest,
10
+ } from "@modelcontextprotocol/sdk/types.js";
6
11
  import { randomUUID } from "crypto";
7
12
  import * as z4mini from "zod/v4-mini";
8
13
  import { api, logger } from "../api";
@@ -159,7 +164,10 @@ function registerTools(mcpServer: McpServer) {
159
164
  mcpServer.registerTool(
160
165
  toolName,
161
166
  toolConfig,
162
- async (args: any, extra: any) => {
167
+ async (
168
+ args: Record<string, unknown>,
169
+ extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
170
+ ) => {
163
171
  const mcpSessionId = extra.sessionId || "";
164
172
  const connection = await createMcpConnection(extra);
165
173
 
package/util/oauth.ts CHANGED
@@ -40,23 +40,14 @@ export function validateRedirectUri(uri: string): {
40
40
  }
41
41
 
42
42
  /**
43
- * Compare two redirect URIs by origin and pathname (ignoring query params).
44
- * Returns `false` if either URI is malformed.
43
+ * Compare two redirect URIs with exact string matching, as required by
44
+ * RFC 6749 §3.1.2.3 and RFC 8252 §8.4.
45
45
  */
46
46
  export function redirectUrisMatch(
47
47
  registeredUri: string,
48
48
  requestedUri: string,
49
49
  ): boolean {
50
- try {
51
- const registered = new URL(registeredUri);
52
- const requested = new URL(requestedUri);
53
- return (
54
- registered.origin === requested.origin &&
55
- registered.pathname === requested.pathname
56
- );
57
- } catch {
58
- return false;
59
- }
50
+ return registeredUri === requestedUri;
60
51
  }
61
52
 
62
53
  /** Encode a byte array as a URL-safe base64 string (no padding). Used for PKCE code challenges. */
@@ -37,7 +37,7 @@ function parseAcceptEncoding(header: string): Set<string> {
37
37
  /**
38
38
  * Pick the best encoding based on server preference order and client support.
39
39
  */
40
- function selectEncoding(clientEncodings: Set<string>): "br" | "gzip" | null {
40
+ function selectEncoding(clientEncodings: Set<string>): "gzip" | null {
41
41
  for (const encoding of config.server.web.compression.encodings) {
42
42
  if (clientEncodings.has(encoding)) return encoding;
43
43
  }
@@ -53,6 +53,24 @@ function isIncompressible(contentType: string | null): boolean {
53
53
  return INCOMPRESSIBLE_TYPES.has(mimeType);
54
54
  }
55
55
 
56
+ /**
57
+ * Pipe a body through a gzip `CompressionStream` and build a new `Response` carrying the
58
+ * compression headers (`Content-Encoding`, appended `Vary`, removed `Content-Length`).
59
+ */
60
+ function compressBody(body: ReadableStream, response: Response): Response {
61
+ const compressionStream = new CompressionStream("gzip");
62
+ // @ts-ignore Bun's ReadableStream type is incompatible with Node/DOM ReadableStream
63
+ const stream = body.pipeThrough(compressionStream);
64
+
65
+ const headers = new Headers(response.headers);
66
+ headers.set("Content-Encoding", "gzip");
67
+ headers.append("Vary", "Accept-Encoding");
68
+ headers.delete("Content-Length");
69
+
70
+ // @ts-ignore Bun's ReadableStream type is incompatible with Node/DOM ReadableStream
71
+ return new Response(stream, { status: response.status, headers });
72
+ }
73
+
56
74
  /**
57
75
  * Conditionally compress an HTTP response based on the client's `Accept-Encoding` header,
58
76
  * the response content type, and the configured compression threshold.
@@ -86,8 +104,7 @@ export async function compressResponse(
86
104
  if (!acceptEncoding) return response;
87
105
 
88
106
  const clientEncodings = parseAcceptEncoding(acceptEncoding);
89
- const encoding = selectEncoding(clientEncodings);
90
- if (!encoding) return response;
107
+ if (!selectEncoding(clientEncodings)) return response;
91
108
 
92
109
  // Skip incompressible content types
93
110
  if (isIncompressible(response.headers.get("Content-Type"))) return response;
@@ -112,40 +129,9 @@ export async function compressResponse(
112
129
  });
113
130
  }
114
131
 
115
- // Compress the buffered body
116
- const format: Bun.CompressionFormat = encoding === "br" ? "brotli" : "gzip";
117
- // @ts-ignore Bun supports "brotli" as CompressionFormat but DOM lib does not
118
- const compressionStream = new CompressionStream(format);
119
- // @ts-ignore Bun's ReadableStream type is incompatible with Node/DOM ReadableStream
120
- const stream = new Blob([body]).stream().pipeThrough(compressionStream);
121
-
122
- const headers = new Headers(response.headers);
123
- headers.set("Content-Encoding", encoding);
124
- headers.append("Vary", "Accept-Encoding");
125
- headers.delete("Content-Length");
126
-
127
- // @ts-ignore Bun's ReadableStream type is incompatible with Node/DOM ReadableStream
128
- return new Response(stream, {
129
- status: response.status,
130
- headers,
131
- });
132
+ return compressBody(new Blob([body]).stream(), response);
132
133
  }
133
134
 
134
135
  // Content-Length is present and above threshold — stream-compress
135
- const format: Bun.CompressionFormat = encoding === "br" ? "brotli" : "gzip";
136
- // @ts-ignore Bun supports "brotli" as CompressionFormat but DOM lib does not
137
- const compressionStream = new CompressionStream(format);
138
- // @ts-ignore Bun's ReadableStream type is incompatible with Node/DOM ReadableStream
139
- const stream = response.body.pipeThrough(compressionStream);
140
-
141
- const headers = new Headers(response.headers);
142
- headers.set("Content-Encoding", encoding);
143
- headers.append("Vary", "Accept-Encoding");
144
- headers.delete("Content-Length");
145
-
146
- // @ts-ignore Bun's ReadableStream type is incompatible with Node/DOM ReadableStream
147
- return new Response(stream, {
148
- status: response.status,
149
- headers,
150
- });
136
+ return compressBody(response.body, response);
151
137
  }
package/util/zodMixins.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { eq } from "drizzle-orm";
1
+ import { type AnyColumn, eq, type Table } from "drizzle-orm";
2
2
  import { z } from "zod/v4";
3
3
  import { api } from "../api";
4
4
  import { ErrorType, TypedError } from "../classes/TypedError";
@@ -79,9 +79,6 @@ export function paginationInputs(options?: {
79
79
  });
80
80
  }
81
81
 
82
- // Type for Drizzle tables with an id column
83
- type TableWithId = { id: any; $inferSelect: any };
84
-
85
82
  /**
86
83
  * Generic factory to create a Zod schema that accepts either an ID or a model object.
87
84
  * If an ID is provided, it resolves to the full model via database lookup.
@@ -91,8 +88,8 @@ type TableWithId = { id: any; $inferSelect: any };
91
88
  * @param isModel - Type guard function to check if value is already a model
92
89
  * @param entityName - Human-readable name for error messages
93
90
  */
94
- export function zIdOrModel<TTable extends TableWithId, TModel>(
95
- table: TTable,
91
+ export function zIdOrModel<TModel>(
92
+ table: Table & { id: AnyColumn },
96
93
  modelSchema: z.ZodType<TModel>,
97
94
  isModel: (val: unknown) => val is TModel,
98
95
  entityName: string,
@@ -105,8 +102,8 @@ export function zIdOrModel<TTable extends TableWithId, TModel>(
105
102
  }
106
103
  const [record] = await api.db.db
107
104
  .select()
108
- .from(table as any)
109
- .where(eq((table as any).id, val))
105
+ .from(table)
106
+ .where(eq(table.id, val))
110
107
  .limit(1);
111
108
 
112
109
  if (!record) {