keryx 0.11.6 → 0.12.0
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.
- package/classes/Connection.ts +12 -24
- package/initializers/mcp.ts +4 -14
- package/initializers/resque.ts +11 -16
- package/package.json +1 -1
- package/util/cli.ts +1 -4
- package/util/oauthHandlers.ts +3 -3
- package/util/webRouting.ts +41 -23
- package/util/webSocket.ts +1 -4
package/classes/Connection.ts
CHANGED
|
@@ -80,8 +80,8 @@ export class Connection<
|
|
|
80
80
|
*
|
|
81
81
|
* @param actionName - The name of the action to run. If not found, throws
|
|
82
82
|
* `ErrorType.CONNECTION_ACTION_NOT_FOUND`.
|
|
83
|
-
* @param params - Raw
|
|
84
|
-
*
|
|
83
|
+
* @param params - Raw parameters as a plain object. Validated and coerced
|
|
84
|
+
* against the action's `inputs` Zod schema.
|
|
85
85
|
* @param method - The HTTP method of the incoming request (used for logging).
|
|
86
86
|
* @param url - The request URL (used for logging).
|
|
87
87
|
* @returns The action response and optional error.
|
|
@@ -89,7 +89,7 @@ export class Connection<
|
|
|
89
89
|
*/
|
|
90
90
|
async act(
|
|
91
91
|
actionName: string | undefined,
|
|
92
|
-
params:
|
|
92
|
+
params: Record<string, unknown>,
|
|
93
93
|
method: Request["method"] = "",
|
|
94
94
|
url: string = "",
|
|
95
95
|
): Promise<{ response: Object; error?: TypedError }> {
|
|
@@ -279,24 +279,9 @@ export class Connection<
|
|
|
279
279
|
return api.actions.actions.find((a: Action) => a.name === actionName);
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
private async formatParams(params:
|
|
282
|
+
private async formatParams(params: Record<string, unknown>, action: Action) {
|
|
283
283
|
if (!action.inputs) return {} as ActionParams<Action>;
|
|
284
284
|
|
|
285
|
-
// Convert FormData to a plain object for processing
|
|
286
|
-
const rawParams: Record<string, any> = {};
|
|
287
|
-
params.forEach((value, key) => {
|
|
288
|
-
if (rawParams[key] !== undefined) {
|
|
289
|
-
// If the key already exists, convert to array
|
|
290
|
-
if (Array.isArray(rawParams[key])) {
|
|
291
|
-
rawParams[key].push(value);
|
|
292
|
-
} else {
|
|
293
|
-
rawParams[key] = [rawParams[key], value];
|
|
294
|
-
}
|
|
295
|
-
} else {
|
|
296
|
-
rawParams[key] = value;
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
|
|
300
285
|
// Handle zod schema inputs
|
|
301
286
|
if (
|
|
302
287
|
typeof action.inputs === "object" &&
|
|
@@ -305,12 +290,12 @@ export class Connection<
|
|
|
305
290
|
) {
|
|
306
291
|
// This is a zod schema - use safeParseAsync to support both sync and async transforms
|
|
307
292
|
try {
|
|
308
|
-
const result = await (action.inputs as any).safeParseAsync(
|
|
293
|
+
const result = await (action.inputs as any).safeParseAsync(params);
|
|
309
294
|
if (!result.success) {
|
|
310
295
|
// Get the first validation error (Zod v4 uses .issues instead of .errors)
|
|
311
296
|
const firstError = result.error.issues[0];
|
|
312
297
|
const key = firstError.path[0];
|
|
313
|
-
const value =
|
|
298
|
+
const value = params[key];
|
|
314
299
|
let message = firstError.message;
|
|
315
300
|
// Zod v4: detect missing required param (code: "invalid_type" with undefined input)
|
|
316
301
|
const isMissingRequired =
|
|
@@ -405,7 +390,10 @@ function logAction(opts: {
|
|
|
405
390
|
|
|
406
391
|
const REDACTED = "[[secret]]" as const;
|
|
407
392
|
|
|
408
|
-
const sanitizeParams = (
|
|
393
|
+
const sanitizeParams = (
|
|
394
|
+
params: Record<string, unknown>,
|
|
395
|
+
action: Action | undefined,
|
|
396
|
+
) => {
|
|
409
397
|
const sanitizedParams: Record<string, any> = {};
|
|
410
398
|
|
|
411
399
|
// Get secret fields from the action's zod schema if it exists
|
|
@@ -422,13 +410,13 @@ const sanitizeParams = (params: FormData, action: Action | undefined) => {
|
|
|
422
410
|
}
|
|
423
411
|
}
|
|
424
412
|
|
|
425
|
-
|
|
413
|
+
for (const [k, v] of Object.entries(params)) {
|
|
426
414
|
if (secretFields.has(k)) {
|
|
427
415
|
sanitizedParams[k] = REDACTED;
|
|
428
416
|
} else {
|
|
429
417
|
sanitizedParams[k] = v;
|
|
430
418
|
}
|
|
431
|
-
}
|
|
419
|
+
}
|
|
432
420
|
|
|
433
421
|
return sanitizedParams;
|
|
434
422
|
};
|
package/initializers/mcp.ts
CHANGED
|
@@ -391,20 +391,10 @@ function createMcpServer(): McpServer {
|
|
|
391
391
|
await connection.updateSession({ userId: authInfo.extra.userId });
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
-
const params =
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
)) {
|
|
399
|
-
if (Array.isArray(value)) {
|
|
400
|
-
for (const item of value) {
|
|
401
|
-
params.append(key, String(item));
|
|
402
|
-
}
|
|
403
|
-
} else if (value !== undefined && value !== null) {
|
|
404
|
-
params.set(key, String(value));
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
394
|
+
const params =
|
|
395
|
+
args && typeof args === "object"
|
|
396
|
+
? (args as Record<string, unknown>)
|
|
397
|
+
: {};
|
|
408
398
|
|
|
409
399
|
const { response, error } = await connection.act(
|
|
410
400
|
action.name,
|
package/initializers/resque.ts
CHANGED
|
@@ -279,7 +279,7 @@ export class Resque extends Initializer {
|
|
|
279
279
|
|
|
280
280
|
/**
|
|
281
281
|
* Wrap an action as a node-resque job. Creates a temporary `Connection` with type `"resque"`,
|
|
282
|
-
* converts inputs to
|
|
282
|
+
* converts inputs to a plain object, and runs the action via `connection.act()`. Handles
|
|
283
283
|
* fan-out result/error collection and recurring task re-enqueue.
|
|
284
284
|
*/
|
|
285
285
|
wrapActionAsJob = (
|
|
@@ -302,26 +302,21 @@ export class Resque extends Initializer {
|
|
|
302
302
|
connection.correlationId = propagatedCorrelationId;
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
const
|
|
305
|
+
const plainParams: Record<string, unknown> =
|
|
306
|
+
typeof params === "object" && params !== null
|
|
307
|
+
? Object.fromEntries(
|
|
308
|
+
typeof params.entries === "function"
|
|
309
|
+
? params.entries()
|
|
310
|
+
: Object.entries(params),
|
|
311
|
+
)
|
|
312
|
+
: {};
|
|
306
313
|
|
|
307
|
-
|
|
308
|
-
for (const [key, value] of params.entries()) {
|
|
309
|
-
paramsAsFormData.append(key, value);
|
|
310
|
-
}
|
|
311
|
-
} else if (typeof params === "object" && params !== null) {
|
|
312
|
-
for (const [key, value] of Object.entries(params)) {
|
|
313
|
-
if (value !== undefined && value !== null) {
|
|
314
|
-
paramsAsFormData.append(key, String(value));
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const fanOutId = params._fanOutId as string | undefined;
|
|
314
|
+
const fanOutId = plainParams._fanOutId as string | undefined;
|
|
320
315
|
|
|
321
316
|
let response: Awaited<ReturnType<(typeof action)["run"]>>;
|
|
322
317
|
let error: TypedError | undefined;
|
|
323
318
|
try {
|
|
324
|
-
const payload = await connection.act(action.name,
|
|
319
|
+
const payload = await connection.act(action.name, plainParams);
|
|
325
320
|
response = payload.response;
|
|
326
321
|
error = payload.error;
|
|
327
322
|
|
package/package.json
CHANGED
package/util/cli.ts
CHANGED
|
@@ -241,10 +241,7 @@ async function runActionViaCLI(options: Record<string, string>, command: any) {
|
|
|
241
241
|
|
|
242
242
|
const id = "cli:" + os.userInfo().username;
|
|
243
243
|
const connection = new Connection("cli", id);
|
|
244
|
-
const params =
|
|
245
|
-
for (const [key, value] of Object.entries(options)) {
|
|
246
|
-
params.append(key, value);
|
|
247
|
-
}
|
|
244
|
+
const params: Record<string, unknown> = { ...options };
|
|
248
245
|
|
|
249
246
|
const { response, error } = await connection.act(actionName, params);
|
|
250
247
|
const payload: { response: any; error?: any } = { response };
|
package/util/oauthHandlers.ts
CHANGED
|
@@ -252,11 +252,11 @@ export async function handleAuthorizePost(
|
|
|
252
252
|
return renderAuthPage(oauthParams, templates, authActions);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
// Build action
|
|
256
|
-
const actionParams =
|
|
255
|
+
// Build action params from all non-OAuth fields
|
|
256
|
+
const actionParams: Record<string, unknown> = {};
|
|
257
257
|
for (const [key, value] of Object.entries(fields)) {
|
|
258
258
|
if (!OAUTH_FIELDS.has(key)) {
|
|
259
|
-
actionParams
|
|
259
|
+
actionParams[key] = value;
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
|
package/util/webRouting.ts
CHANGED
|
@@ -64,25 +64,30 @@ export async function determineActionName(
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Parse request parameters from path params, request body (JSON or form-data),
|
|
67
|
-
* and query string into a single
|
|
67
|
+
* and query string into a single plain object.
|
|
68
|
+
*
|
|
69
|
+
* JSON bodies are preserved with full type fidelity (nested objects, arrays,
|
|
70
|
+
* booleans, numbers). FormData bodies (multipart/form-data and
|
|
71
|
+
* application/x-www-form-urlencoded) are converted to a plain object where
|
|
72
|
+
* repeated keys become arrays and `File` values are preserved.
|
|
68
73
|
*
|
|
69
74
|
* @param req - The incoming HTTP request.
|
|
70
75
|
* @param url - The parsed URL (for query string).
|
|
71
76
|
* @param pathParams - Path parameters extracted by route matching.
|
|
72
|
-
* @returns A
|
|
77
|
+
* @returns A plain object containing all merged parameters.
|
|
73
78
|
*/
|
|
74
79
|
export async function parseRequestParams(
|
|
75
80
|
req: Request,
|
|
76
81
|
url: ReturnType<typeof parse>,
|
|
77
82
|
pathParams?: Record<string, string>,
|
|
78
|
-
): Promise<
|
|
79
|
-
// param load order: path params ->
|
|
80
|
-
|
|
83
|
+
): Promise<Record<string, unknown>> {
|
|
84
|
+
// param load order: path params -> body params -> query params
|
|
85
|
+
const params: Record<string, unknown> = {};
|
|
81
86
|
|
|
82
|
-
// Add path parameters
|
|
87
|
+
// Add path parameters (always strings from URL segments)
|
|
83
88
|
if (pathParams) {
|
|
84
89
|
for (const [key, value] of Object.entries(pathParams)) {
|
|
85
|
-
params
|
|
90
|
+
params[key] = String(value);
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
|
|
@@ -92,20 +97,9 @@ export async function parseRequestParams(
|
|
|
92
97
|
) {
|
|
93
98
|
try {
|
|
94
99
|
const bodyContent = (await req.json()) as Record<string, unknown>;
|
|
100
|
+
// Merge JSON body directly — preserves types (objects, arrays, booleans, numbers)
|
|
95
101
|
for (const [key, value] of Object.entries(bodyContent)) {
|
|
96
|
-
|
|
97
|
-
// Handle arrays by appending each element
|
|
98
|
-
if (value.length === 0) {
|
|
99
|
-
// For empty arrays, set an empty string to indicate the field exists
|
|
100
|
-
params.set(key, "");
|
|
101
|
-
} else {
|
|
102
|
-
for (const item of value) {
|
|
103
|
-
params.append(key, item);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
} else {
|
|
107
|
-
params.set(key, value as any);
|
|
108
|
-
}
|
|
102
|
+
params[key] = value;
|
|
109
103
|
}
|
|
110
104
|
} catch (e) {
|
|
111
105
|
throw new TypedError({
|
|
@@ -123,7 +117,15 @@ export async function parseRequestParams(
|
|
|
123
117
|
) {
|
|
124
118
|
const f = await req.formData();
|
|
125
119
|
f.forEach((value, key) => {
|
|
126
|
-
params
|
|
120
|
+
if (params[key] !== undefined) {
|
|
121
|
+
if (Array.isArray(params[key])) {
|
|
122
|
+
(params[key] as unknown[]).push(value);
|
|
123
|
+
} else {
|
|
124
|
+
params[key] = [params[key], value];
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
params[key] = value;
|
|
128
|
+
}
|
|
127
129
|
});
|
|
128
130
|
}
|
|
129
131
|
|
|
@@ -131,9 +133,25 @@ export async function parseRequestParams(
|
|
|
131
133
|
for (const [key, values] of Object.entries(url.query)) {
|
|
132
134
|
if (values !== undefined) {
|
|
133
135
|
if (Array.isArray(values)) {
|
|
134
|
-
|
|
136
|
+
if (params[key] !== undefined) {
|
|
137
|
+
if (Array.isArray(params[key])) {
|
|
138
|
+
(params[key] as unknown[]).push(...values);
|
|
139
|
+
} else {
|
|
140
|
+
params[key] = [params[key], ...values];
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
params[key] = values;
|
|
144
|
+
}
|
|
135
145
|
} else {
|
|
136
|
-
params
|
|
146
|
+
if (params[key] !== undefined) {
|
|
147
|
+
if (Array.isArray(params[key])) {
|
|
148
|
+
(params[key] as unknown[]).push(values);
|
|
149
|
+
} else {
|
|
150
|
+
params[key] = [params[key], values];
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
params[key] = values;
|
|
154
|
+
}
|
|
137
155
|
}
|
|
138
156
|
}
|
|
139
157
|
}
|
package/util/webSocket.ts
CHANGED
|
@@ -25,10 +25,7 @@ export async function handleWebsocketAction(
|
|
|
25
25
|
ws: ServerWebSocket,
|
|
26
26
|
formattedMessage: ActionParams<any>,
|
|
27
27
|
) {
|
|
28
|
-
const params =
|
|
29
|
-
for (const [key, value] of Object.entries(formattedMessage.params)) {
|
|
30
|
-
params.append(key, value as string);
|
|
31
|
-
}
|
|
28
|
+
const params = (formattedMessage.params ?? {}) as Record<string, unknown>;
|
|
32
29
|
|
|
33
30
|
const { response, error } = await connection.act(
|
|
34
31
|
formattedMessage.action,
|