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.
@@ -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 `FormData` from the HTTP request or WebSocket message.
84
- * Validated and coerced against the action's `inputs` Zod schema.
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: FormData, // note: params are not constant for all connections - some are long-lived, like websockets
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: FormData, action: Action) {
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(rawParams);
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 = rawParams[key];
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 = (params: FormData, action: Action | undefined) => {
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
- params.forEach((v, k) => {
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
  };
@@ -391,20 +391,10 @@ function createMcpServer(): McpServer {
391
391
  await connection.updateSession({ userId: authInfo.extra.userId });
392
392
  }
393
393
 
394
- const params = new FormData();
395
- if (args && typeof args === "object") {
396
- for (const [key, value] of Object.entries(
397
- args as Record<string, unknown>,
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,
@@ -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 `FormData`, and runs the action via `connection.act()`. Handles
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 paramsAsFormData = new FormData();
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
- if (typeof params.entries === "function") {
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, paramsAsFormData);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keryx",
3
- "version": "0.11.6",
3
+ "version": "0.12.0",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "license": "MIT",
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 = new FormData();
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 };
@@ -252,11 +252,11 @@ export async function handleAuthorizePost(
252
252
  return renderAuthPage(oauthParams, templates, authActions);
253
253
  }
254
254
 
255
- // Build action FormData from all non-OAuth fields
256
- const actionParams = new FormData();
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.set(key, value);
259
+ actionParams[key] = value;
260
260
  }
261
261
  }
262
262
 
@@ -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 `FormData` instance.
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 `FormData` containing all merged parameters.
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<FormData> {
79
- // param load order: path params -> url params -> body params -> query params
80
- let params = new FormData();
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.set(key, String(value));
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
- if (Array.isArray(value)) {
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.append(key, value);
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
- for (const v of values) params.append(key, v);
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.append(key, values);
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 = new FormData();
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,