@genkit-ai/express 1.0.0-rc.9 → 1.0.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.
package/README.md CHANGED
@@ -6,17 +6,14 @@ This plugin provides utilities for conveninetly exposing Genkit flows and action
6
6
  import { expressHandler } from '@genkit-ai/express';
7
7
  import express from 'express';
8
8
 
9
- const simpleFlow = ai.defineFlow(
10
- 'simpleFlow',
11
- async (input, streamingCallback) => {
12
- const { text } = await ai.generate({
13
- model: gemini15Flash,
14
- prompt: input,
15
- streamingCallback,
16
- });
17
- return text;
18
- }
19
- );
9
+ const simpleFlow = ai.defineFlow('simpleFlow', async (input, { sendChunk }) => {
10
+ const { text } = await ai.generate({
11
+ model: gemini15Flash,
12
+ prompt: input,
13
+ onChunk: (c) => sendChunk(c.text),
14
+ });
15
+ return text;
16
+ });
20
17
 
21
18
  const app = express();
22
19
  app.use(express.json());
@@ -26,36 +23,34 @@ app.post('/simpleFlow', expressHandler(simpleFlow));
26
23
  app.listen(8080);
27
24
  ```
28
25
 
29
- You can also set auth policies:
26
+ You can also handle auth using context providers:
30
27
 
31
28
  ```ts
32
- // middleware for handling auth headers.
33
- const authMiddleware = async (req, resp, next) => {
34
- // parse auth headers and convert to auth object.
35
- (req as RequestWithAuth).auth = {
36
- user:
37
- req.header('authorization') === 'open sesame' ? 'Ali Baba' : '40 thieves',
29
+ import { UserFacingError } from 'genkit';
30
+ import { ContextProvider, RequestData } from 'genkit/context';
31
+
32
+ const context: ContextProvider<Context> = (req: RequestData) => {
33
+ if (req.headers['authorization'] !== 'open sesame') {
34
+ throw new UserFacingError('PERMISSION_DENIED', 'not authorized');
35
+ }
36
+ return {
37
+ auth: {
38
+ user: 'Ali Baba',
39
+ },
38
40
  };
39
- next();
40
41
  };
41
42
 
42
43
  app.post(
43
44
  '/simpleFlow',
44
45
  authMiddleware,
45
- expressHandler(simpleFlow, {
46
- authPolicy: ({ auth }) => {
47
- if (auth.user !== 'Ali Baba') {
48
- throw new Error('not authorized');
49
- }
50
- },
51
- })
46
+ expressHandler(simpleFlow, { context })
52
47
  );
53
48
  ```
54
49
 
55
- Flows and actions exposed using the `expressHandler` function can be accessed using `genkit/client` library:
50
+ Flows and actions exposed using the `expressHandler` function can be accessed using `genkit/beta/client` library:
56
51
 
57
52
  ```ts
58
- import { runFlow, streamFlow } from 'genkit/client';
53
+ import { runFlow, streamFlow } from 'genkit/beta/client';
59
54
 
60
55
  const result = await runFlow({
61
56
  url: `http://localhost:${port}/simpleFlow`,
@@ -80,10 +75,10 @@ const result = streamFlow({
80
75
  url: `http://localhost:${port}/simpleFlow`,
81
76
  input: 'say hello',
82
77
  });
83
- for await (const chunk of result.stream()) {
78
+ for await (const chunk of result.stream) {
84
79
  console.log(chunk);
85
80
  }
86
- console.log(await result.output());
81
+ console.log(await result.output);
87
82
  ```
88
83
 
89
84
  The sources for this package are in the main [Genkit](https://github.com/firebase/genkit) repo. Please file issues and pull requests against that repo.
package/lib/index.d.mts CHANGED
@@ -1,7 +1,8 @@
1
- import * as bodyParser from 'body-parser';
1
+ import bodyParser from 'body-parser';
2
2
  import { CorsOptions } from 'cors';
3
- import express, { RequestHandler } from 'express';
4
- import { z, Action, Flow } from 'genkit';
3
+ import express from 'express';
4
+ import { ActionContext, z, Action, Flow } from 'genkit';
5
+ import { ContextProvider } from 'genkit/context';
5
6
 
6
7
  /**
7
8
  * Copyright 2024 Google LLC
@@ -19,54 +20,29 @@ import { z, Action, Flow } from 'genkit';
19
20
  * limitations under the License.
20
21
  */
21
22
 
22
- /**
23
- * Auth policy context is an object passed to the auth policy providing details necessary for auth.
24
- */
25
- interface AuthPolicyContext<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> {
26
- action?: Action<I, O, S>;
27
- input: z.infer<I>;
28
- auth?: Record<string, any>;
29
- request: RequestWithAuth;
30
- }
31
- /**
32
- * Flow Auth policy. Consumes the authorization context of the flow and
33
- * performs checks before the flow runs. If this throws, the flow will not
34
- * be executed.
35
- */
36
- interface AuthPolicy<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> {
37
- (ctx: AuthPolicyContext<I, O, S>): void | Promise<void>;
38
- }
39
- /**
40
- * For express-based flows, req.auth should contain the value to bepassed into
41
- * the flow context.
42
- */
43
- interface RequestWithAuth extends express.Request {
44
- auth?: Record<string, any>;
45
- }
46
23
  /**
47
24
  * Exposes provided flow or an action as express handler.
48
25
  */
49
- declare function expressHandler<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(action: Action<I, O, S>, opts?: {
50
- authPolicy?: AuthPolicy<I, O, S>;
26
+ declare function expressHandler<C extends ActionContext = ActionContext, I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(action: Action<I, O, S>, opts?: {
27
+ contextProvider?: ContextProvider<C, I>;
51
28
  }): express.RequestHandler;
52
29
  /**
53
30
  * A wrapper object containing a flow with its associated auth policy.
54
31
  */
55
- type FlowWithAuthPolicy<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> = {
32
+ type FlowWithContextProvider<C extends ActionContext = ActionContext, I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> = {
56
33
  flow: Flow<I, O, S>;
57
- authProvider: RequestHandler;
58
- authPolicy: AuthPolicy;
34
+ context: ContextProvider<C, I>;
59
35
  };
60
36
  /**
61
37
  * Adds an auth policy to the flow.
62
38
  */
63
- declare function withAuth<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(flow: Flow<I, O, S>, authProvider: RequestHandler, authPolicy: AuthPolicy): FlowWithAuthPolicy<I, O, S>;
39
+ declare function withContextProvider<C extends ActionContext = ActionContext, I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(flow: Flow<I, O, S>, context: ContextProvider<C, I>): FlowWithContextProvider<C, I, O, S>;
64
40
  /**
65
41
  * Options to configure the flow server.
66
42
  */
67
43
  interface FlowServerOptions {
68
44
  /** List of flows to expose via the flow server. */
69
- flows: (Flow<any, any, any> | FlowWithAuthPolicy<any, any, any>)[];
45
+ flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];
70
46
  /** Port to run the server on. Defaults to env.PORT or 3400. */
71
47
  port?: number;
72
48
  /** CORS options for the server. */
@@ -111,4 +87,4 @@ declare class FlowServer {
111
87
  static stopAll(): Promise<void[]>;
112
88
  }
113
89
 
114
- export { type AuthPolicy, type AuthPolicyContext, FlowServer, type FlowServerOptions, type FlowWithAuthPolicy, type RequestWithAuth, expressHandler, startFlowServer, withAuth };
90
+ export { FlowServer, type FlowServerOptions, type FlowWithContextProvider, expressHandler, startFlowServer, withContextProvider };
package/lib/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import * as bodyParser from 'body-parser';
1
+ import bodyParser from 'body-parser';
2
2
  import { CorsOptions } from 'cors';
3
- import express, { RequestHandler } from 'express';
4
- import { z, Action, Flow } from 'genkit';
3
+ import express from 'express';
4
+ import { ActionContext, z, Action, Flow } from 'genkit';
5
+ import { ContextProvider } from 'genkit/context';
5
6
 
6
7
  /**
7
8
  * Copyright 2024 Google LLC
@@ -19,54 +20,29 @@ import { z, Action, Flow } from 'genkit';
19
20
  * limitations under the License.
20
21
  */
21
22
 
22
- /**
23
- * Auth policy context is an object passed to the auth policy providing details necessary for auth.
24
- */
25
- interface AuthPolicyContext<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> {
26
- action?: Action<I, O, S>;
27
- input: z.infer<I>;
28
- auth?: Record<string, any>;
29
- request: RequestWithAuth;
30
- }
31
- /**
32
- * Flow Auth policy. Consumes the authorization context of the flow and
33
- * performs checks before the flow runs. If this throws, the flow will not
34
- * be executed.
35
- */
36
- interface AuthPolicy<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> {
37
- (ctx: AuthPolicyContext<I, O, S>): void | Promise<void>;
38
- }
39
- /**
40
- * For express-based flows, req.auth should contain the value to bepassed into
41
- * the flow context.
42
- */
43
- interface RequestWithAuth extends express.Request {
44
- auth?: Record<string, any>;
45
- }
46
23
  /**
47
24
  * Exposes provided flow or an action as express handler.
48
25
  */
49
- declare function expressHandler<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(action: Action<I, O, S>, opts?: {
50
- authPolicy?: AuthPolicy<I, O, S>;
26
+ declare function expressHandler<C extends ActionContext = ActionContext, I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(action: Action<I, O, S>, opts?: {
27
+ contextProvider?: ContextProvider<C, I>;
51
28
  }): express.RequestHandler;
52
29
  /**
53
30
  * A wrapper object containing a flow with its associated auth policy.
54
31
  */
55
- type FlowWithAuthPolicy<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> = {
32
+ type FlowWithContextProvider<C extends ActionContext = ActionContext, I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> = {
56
33
  flow: Flow<I, O, S>;
57
- authProvider: RequestHandler;
58
- authPolicy: AuthPolicy;
34
+ context: ContextProvider<C, I>;
59
35
  };
60
36
  /**
61
37
  * Adds an auth policy to the flow.
62
38
  */
63
- declare function withAuth<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(flow: Flow<I, O, S>, authProvider: RequestHandler, authPolicy: AuthPolicy): FlowWithAuthPolicy<I, O, S>;
39
+ declare function withContextProvider<C extends ActionContext = ActionContext, I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(flow: Flow<I, O, S>, context: ContextProvider<C, I>): FlowWithContextProvider<C, I, O, S>;
64
40
  /**
65
41
  * Options to configure the flow server.
66
42
  */
67
43
  interface FlowServerOptions {
68
44
  /** List of flows to expose via the flow server. */
69
- flows: (Flow<any, any, any> | FlowWithAuthPolicy<any, any, any>)[];
45
+ flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];
70
46
  /** Port to run the server on. Defaults to env.PORT or 3400. */
71
47
  port?: number;
72
48
  /** CORS options for the server. */
@@ -111,4 +87,4 @@ declare class FlowServer {
111
87
  static stopAll(): Promise<void[]>;
112
88
  }
113
89
 
114
- export { type AuthPolicy, type AuthPolicyContext, FlowServer, type FlowServerOptions, type FlowWithAuthPolicy, type RequestWithAuth, expressHandler, startFlowServer, withAuth };
90
+ export { FlowServer, type FlowServerOptions, type FlowWithContextProvider, expressHandler, startFlowServer, withContextProvider };
package/lib/index.js CHANGED
@@ -31,37 +31,38 @@ __export(src_exports, {
31
31
  FlowServer: () => FlowServer,
32
32
  expressHandler: () => expressHandler,
33
33
  startFlowServer: () => startFlowServer,
34
- withAuth: () => withAuth
34
+ withContextProvider: () => withContextProvider
35
35
  });
36
36
  module.exports = __toCommonJS(src_exports);
37
- var bodyParser = __toESM(require("body-parser"));
37
+ var import_body_parser = __toESM(require("body-parser"));
38
38
  var import_cors = __toESM(require("cors"));
39
39
  var import_express = __toESM(require("express"));
40
40
  var import_genkit = require("genkit");
41
+ var import_context = require("genkit/context");
41
42
  var import_logging = require("genkit/logging");
42
- var import_utils = require("./utils");
43
43
  const streamDelimiter = "\n\n";
44
44
  function expressHandler(action, opts) {
45
45
  return async (request, response) => {
46
46
  const { stream } = request.query;
47
47
  let input = request.body.data;
48
- const auth = request.auth;
48
+ let context;
49
49
  try {
50
- await opts?.authPolicy?.({
51
- action,
52
- auth,
53
- input,
54
- request
55
- });
50
+ context = await opts?.contextProvider?.({
51
+ method: request.method,
52
+ headers: Object.fromEntries(
53
+ Object.entries(request.headers).map(([key, value]) => [
54
+ key.toLowerCase(),
55
+ Array.isArray(value) ? value.join(" ") : String(value)
56
+ ])
57
+ ),
58
+ input
59
+ }) || {};
56
60
  } catch (e) {
57
- import_logging.logger.debug(e);
58
- const respBody = {
59
- error: {
60
- status: "PERMISSION_DENIED",
61
- message: e.message || "Permission denied to resource"
62
- }
63
- };
64
- response.status(403).send(respBody).end();
61
+ import_logging.logger.error(
62
+ `Auth policy failed with error: ${e.message}
63
+ ${e.stack}`
64
+ );
65
+ response.status((0, import_context.getHttpStatus)(e)).json((0, import_context.getCallableJSON)(e)).end();
65
66
  return;
66
67
  }
67
68
  if (request.get("Accept") === "text/event-stream" || stream === "true") {
@@ -80,7 +81,7 @@ function expressHandler(action, opts) {
80
81
  onChunk,
81
82
  () => action.run(input, {
82
83
  onChunk,
83
- context: { auth }
84
+ context
84
85
  })
85
86
  );
86
87
  response.write(
@@ -88,43 +89,37 @@ function expressHandler(action, opts) {
88
89
  );
89
90
  response.end();
90
91
  } catch (e) {
91
- import_logging.logger.error(e);
92
+ import_logging.logger.error(
93
+ `Streaming request failed with error: ${e.message}
94
+ ${e.stack}`
95
+ );
92
96
  response.write(
93
- "data: " + JSON.stringify({
94
- error: {
95
- status: "INTERNAL",
96
- message: (0, import_utils.getErrorMessage)(e),
97
- details: (0, import_utils.getErrorStack)(e)
98
- }
99
- }) + streamDelimiter
97
+ `error: ${JSON.stringify({ error: (0, import_context.getCallableJSON)(e) })}${streamDelimiter}`
100
98
  );
101
99
  response.end();
102
100
  }
103
101
  } else {
104
102
  try {
105
- const result = await action.run(input, { context: { auth } });
103
+ const result = await action.run(input, { context });
106
104
  response.setHeader("x-genkit-trace-id", result.telemetry.traceId);
107
105
  response.setHeader("x-genkit-span-id", result.telemetry.spanId);
108
- response.status(200).send({
106
+ response.status(200).json({
109
107
  result: result.result
110
108
  }).end();
111
109
  } catch (e) {
112
- response.status(500).send({
113
- error: {
114
- status: "INTERNAL",
115
- message: (0, import_utils.getErrorMessage)(e),
116
- details: (0, import_utils.getErrorStack)(e)
117
- }
118
- }).end();
110
+ import_logging.logger.error(
111
+ `Non-streaming request failed with error: ${e.message}
112
+ ${e.stack}`
113
+ );
114
+ response.status((0, import_context.getHttpStatus)(e)).json((0, import_context.getCallableJSON)(e)).end();
119
115
  }
120
116
  }
121
117
  };
122
118
  }
123
- function withAuth(flow, authProvider, authPolicy) {
119
+ function withContextProvider(flow, context) {
124
120
  return {
125
121
  flow,
126
- authProvider,
127
- authPolicy
122
+ context
128
123
  };
129
124
  }
130
125
  function startFlowServer(options) {
@@ -151,27 +146,22 @@ class FlowServer {
151
146
  */
152
147
  async start() {
153
148
  const server = (0, import_express.default)();
154
- server.use(bodyParser.json(this.options.jsonParserOptions));
149
+ server.use(import_body_parser.default.json(this.options.jsonParserOptions));
155
150
  server.use((0, import_cors.default)(this.options.cors));
156
151
  import_logging.logger.debug("Running flow server with flow paths:");
157
152
  const pathPrefix = this.options.pathPrefix ?? "";
158
153
  this.options.flows?.forEach((flow) => {
159
- if (flow.authPolicy) {
160
- const flowWithPolicy = flow;
161
- const flowPath = `/${pathPrefix}${flowWithPolicy.flow.__action.name}`;
154
+ if ("context" in flow) {
155
+ const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;
162
156
  import_logging.logger.debug(` - ${flowPath}`);
163
157
  server.post(
164
158
  flowPath,
165
- flowWithPolicy.authProvider,
166
- expressHandler(flowWithPolicy.flow, {
167
- authPolicy: flowWithPolicy.authPolicy
168
- })
159
+ expressHandler(flow.flow, { contextProvider: flow.context })
169
160
  );
170
161
  } else {
171
- const resolvedFlow = flow;
172
- const flowPath = `/${pathPrefix}${resolvedFlow.__action.name}`;
162
+ const flowPath = `/${pathPrefix}${flow.__action.name}`;
173
163
  import_logging.logger.debug(` - ${flowPath}`);
174
- server.post(flowPath, expressHandler(resolvedFlow));
164
+ server.post(flowPath, expressHandler(flow));
175
165
  }
176
166
  });
177
167
  this.port = this.options?.port || (process.env.PORT ? parseInt(process.env.PORT) : 0) || 3400;
@@ -222,6 +212,6 @@ class FlowServer {
222
212
  FlowServer,
223
213
  expressHandler,
224
214
  startFlowServer,
225
- withAuth
215
+ withContextProvider
226
216
  });
227
217
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as bodyParser from 'body-parser';\nimport cors, { CorsOptions } from 'cors';\nimport express, { RequestHandler } from 'express';\nimport { Action, Flow, runWithStreamingCallback, z } from 'genkit';\nimport { logger } from 'genkit/logging';\nimport { Server } from 'http';\nimport { getErrorMessage, getErrorStack } from './utils';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Auth policy context is an object passed to the auth policy providing details necessary for auth.\n */\nexport interface AuthPolicyContext<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> {\n action?: Action<I, O, S>;\n input: z.infer<I>;\n auth?: Record<string, any>;\n request: RequestWithAuth;\n}\n\n/**\n * Flow Auth policy. Consumes the authorization context of the flow and\n * performs checks before the flow runs. If this throws, the flow will not\n * be executed.\n */\nexport interface AuthPolicy<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> {\n (ctx: AuthPolicyContext<I, O, S>): void | Promise<void>;\n}\n\n/**\n * For express-based flows, req.auth should contain the value to bepassed into\n * the flow context.\n */\nexport interface RequestWithAuth extends express.Request {\n auth?: Record<string, any>;\n}\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n authPolicy?: AuthPolicy<I, O, S>;\n }\n): express.RequestHandler {\n return async (\n request: RequestWithAuth,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n let input = request.body.data;\n const auth = request.auth;\n\n try {\n await opts?.authPolicy?.({\n action,\n auth,\n input,\n request,\n });\n } catch (e: any) {\n logger.debug(e);\n const respBody = {\n error: {\n status: 'PERMISSION_DENIED',\n message: e.message || 'Permission denied to resource',\n },\n };\n response.status(403).send(respBody).end();\n return;\n }\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n response.writeHead(200, {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n });\n try {\n const onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n const result = await runWithStreamingCallback(\n action.__registry,\n onChunk,\n () =>\n action.run(input, {\n onChunk,\n context: { auth },\n })\n );\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n logger.error(e);\n response.write(\n 'data: ' +\n JSON.stringify({\n error: {\n status: 'INTERNAL',\n message: getErrorMessage(e),\n details: getErrorStack(e),\n },\n }) +\n streamDelimiter\n );\n response.end();\n }\n } else {\n try {\n const result = await action.run(input, { context: { auth } });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .send({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n response\n .status(500)\n .send({\n error: {\n status: 'INTERNAL',\n message: getErrorMessage(e),\n details: getErrorStack(e),\n },\n })\n .end();\n }\n }\n };\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n */\nexport type FlowWithAuthPolicy<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n authProvider: RequestHandler;\n authPolicy: AuthPolicy;\n};\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withAuth<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n authProvider: RequestHandler,\n authPolicy: AuthPolicy\n): FlowWithAuthPolicy<I, O, S> {\n return {\n flow,\n authProvider,\n authPolicy,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (Flow<any, any, any> | FlowWithAuthPolicy<any, any, any>)[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ((flow as FlowWithAuthPolicy).authPolicy) {\n const flowWithPolicy = flow as FlowWithAuthPolicy;\n const flowPath = `/${pathPrefix}${flowWithPolicy.flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(\n flowPath,\n flowWithPolicy.authProvider,\n expressHandler(flowWithPolicy.flow, {\n authPolicy: flowWithPolicy.authPolicy,\n })\n );\n } else {\n const resolvedFlow = flow as Flow;\n const flowPath = `/${pathPrefix}${resolvedFlow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(resolvedFlow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":["express","cors"]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { CorsOptions } from 'cors';\nimport express from 'express';\nimport {\n Action,\n ActionContext,\n Flow,\n runWithStreamingCallback,\n z,\n} from 'genkit';\nimport {\n ContextProvider,\n RequestData,\n getCallableJSON,\n getHttpStatus,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n let input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers).map(([key, value]) => [\n key.toLowerCase(),\n Array.isArray(value) ? value.join(' ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n response.writeHead(200, {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n });\n try {\n const onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n const result = await runWithStreamingCallback(\n action.__registry,\n onChunk,\n () =>\n action.run(input, {\n onChunk,\n context,\n })\n );\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.write(\n `error: ${JSON.stringify({ error: getCallableJSON(e) })}${streamDelimiter}`\n );\n response.end();\n }\n } else {\n try {\n const result = await action.run(input, { context });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('context' in flow) {\n const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(\n flowPath,\n expressHandler(flow.flow, { contextProvider: flow.context })\n );\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":["express","bodyParser","cors"]}
package/lib/index.mjs CHANGED
@@ -1,31 +1,37 @@
1
- import * as bodyParser from "body-parser";
1
+ import bodyParser from "body-parser";
2
2
  import cors from "cors";
3
3
  import express from "express";
4
- import { runWithStreamingCallback } from "genkit";
4
+ import {
5
+ runWithStreamingCallback
6
+ } from "genkit";
7
+ import {
8
+ getCallableJSON,
9
+ getHttpStatus
10
+ } from "genkit/context";
5
11
  import { logger } from "genkit/logging";
6
- import { getErrorMessage, getErrorStack } from "./utils";
7
12
  const streamDelimiter = "\n\n";
8
13
  function expressHandler(action, opts) {
9
14
  return async (request, response) => {
10
15
  const { stream } = request.query;
11
16
  let input = request.body.data;
12
- const auth = request.auth;
17
+ let context;
13
18
  try {
14
- await opts?.authPolicy?.({
15
- action,
16
- auth,
17
- input,
18
- request
19
- });
19
+ context = await opts?.contextProvider?.({
20
+ method: request.method,
21
+ headers: Object.fromEntries(
22
+ Object.entries(request.headers).map(([key, value]) => [
23
+ key.toLowerCase(),
24
+ Array.isArray(value) ? value.join(" ") : String(value)
25
+ ])
26
+ ),
27
+ input
28
+ }) || {};
20
29
  } catch (e) {
21
- logger.debug(e);
22
- const respBody = {
23
- error: {
24
- status: "PERMISSION_DENIED",
25
- message: e.message || "Permission denied to resource"
26
- }
27
- };
28
- response.status(403).send(respBody).end();
30
+ logger.error(
31
+ `Auth policy failed with error: ${e.message}
32
+ ${e.stack}`
33
+ );
34
+ response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();
29
35
  return;
30
36
  }
31
37
  if (request.get("Accept") === "text/event-stream" || stream === "true") {
@@ -44,7 +50,7 @@ function expressHandler(action, opts) {
44
50
  onChunk,
45
51
  () => action.run(input, {
46
52
  onChunk,
47
- context: { auth }
53
+ context
48
54
  })
49
55
  );
50
56
  response.write(
@@ -52,43 +58,37 @@ function expressHandler(action, opts) {
52
58
  );
53
59
  response.end();
54
60
  } catch (e) {
55
- logger.error(e);
61
+ logger.error(
62
+ `Streaming request failed with error: ${e.message}
63
+ ${e.stack}`
64
+ );
56
65
  response.write(
57
- "data: " + JSON.stringify({
58
- error: {
59
- status: "INTERNAL",
60
- message: getErrorMessage(e),
61
- details: getErrorStack(e)
62
- }
63
- }) + streamDelimiter
66
+ `error: ${JSON.stringify({ error: getCallableJSON(e) })}${streamDelimiter}`
64
67
  );
65
68
  response.end();
66
69
  }
67
70
  } else {
68
71
  try {
69
- const result = await action.run(input, { context: { auth } });
72
+ const result = await action.run(input, { context });
70
73
  response.setHeader("x-genkit-trace-id", result.telemetry.traceId);
71
74
  response.setHeader("x-genkit-span-id", result.telemetry.spanId);
72
- response.status(200).send({
75
+ response.status(200).json({
73
76
  result: result.result
74
77
  }).end();
75
78
  } catch (e) {
76
- response.status(500).send({
77
- error: {
78
- status: "INTERNAL",
79
- message: getErrorMessage(e),
80
- details: getErrorStack(e)
81
- }
82
- }).end();
79
+ logger.error(
80
+ `Non-streaming request failed with error: ${e.message}
81
+ ${e.stack}`
82
+ );
83
+ response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();
83
84
  }
84
85
  }
85
86
  };
86
87
  }
87
- function withAuth(flow, authProvider, authPolicy) {
88
+ function withContextProvider(flow, context) {
88
89
  return {
89
90
  flow,
90
- authProvider,
91
- authPolicy
91
+ context
92
92
  };
93
93
  }
94
94
  function startFlowServer(options) {
@@ -120,22 +120,17 @@ class FlowServer {
120
120
  logger.debug("Running flow server with flow paths:");
121
121
  const pathPrefix = this.options.pathPrefix ?? "";
122
122
  this.options.flows?.forEach((flow) => {
123
- if (flow.authPolicy) {
124
- const flowWithPolicy = flow;
125
- const flowPath = `/${pathPrefix}${flowWithPolicy.flow.__action.name}`;
123
+ if ("context" in flow) {
124
+ const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;
126
125
  logger.debug(` - ${flowPath}`);
127
126
  server.post(
128
127
  flowPath,
129
- flowWithPolicy.authProvider,
130
- expressHandler(flowWithPolicy.flow, {
131
- authPolicy: flowWithPolicy.authPolicy
132
- })
128
+ expressHandler(flow.flow, { contextProvider: flow.context })
133
129
  );
134
130
  } else {
135
- const resolvedFlow = flow;
136
- const flowPath = `/${pathPrefix}${resolvedFlow.__action.name}`;
131
+ const flowPath = `/${pathPrefix}${flow.__action.name}`;
137
132
  logger.debug(` - ${flowPath}`);
138
- server.post(flowPath, expressHandler(resolvedFlow));
133
+ server.post(flowPath, expressHandler(flow));
139
134
  }
140
135
  });
141
136
  this.port = this.options?.port || (process.env.PORT ? parseInt(process.env.PORT) : 0) || 3400;
@@ -185,6 +180,6 @@ export {
185
180
  FlowServer,
186
181
  expressHandler,
187
182
  startFlowServer,
188
- withAuth
183
+ withContextProvider
189
184
  };
190
185
  //# sourceMappingURL=index.mjs.map
package/lib/index.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as bodyParser from 'body-parser';\nimport cors, { CorsOptions } from 'cors';\nimport express, { RequestHandler } from 'express';\nimport { Action, Flow, runWithStreamingCallback, z } from 'genkit';\nimport { logger } from 'genkit/logging';\nimport { Server } from 'http';\nimport { getErrorMessage, getErrorStack } from './utils';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Auth policy context is an object passed to the auth policy providing details necessary for auth.\n */\nexport interface AuthPolicyContext<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> {\n action?: Action<I, O, S>;\n input: z.infer<I>;\n auth?: Record<string, any>;\n request: RequestWithAuth;\n}\n\n/**\n * Flow Auth policy. Consumes the authorization context of the flow and\n * performs checks before the flow runs. If this throws, the flow will not\n * be executed.\n */\nexport interface AuthPolicy<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> {\n (ctx: AuthPolicyContext<I, O, S>): void | Promise<void>;\n}\n\n/**\n * For express-based flows, req.auth should contain the value to bepassed into\n * the flow context.\n */\nexport interface RequestWithAuth extends express.Request {\n auth?: Record<string, any>;\n}\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n authPolicy?: AuthPolicy<I, O, S>;\n }\n): express.RequestHandler {\n return async (\n request: RequestWithAuth,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n let input = request.body.data;\n const auth = request.auth;\n\n try {\n await opts?.authPolicy?.({\n action,\n auth,\n input,\n request,\n });\n } catch (e: any) {\n logger.debug(e);\n const respBody = {\n error: {\n status: 'PERMISSION_DENIED',\n message: e.message || 'Permission denied to resource',\n },\n };\n response.status(403).send(respBody).end();\n return;\n }\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n response.writeHead(200, {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n });\n try {\n const onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n const result = await runWithStreamingCallback(\n action.__registry,\n onChunk,\n () =>\n action.run(input, {\n onChunk,\n context: { auth },\n })\n );\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n logger.error(e);\n response.write(\n 'data: ' +\n JSON.stringify({\n error: {\n status: 'INTERNAL',\n message: getErrorMessage(e),\n details: getErrorStack(e),\n },\n }) +\n streamDelimiter\n );\n response.end();\n }\n } else {\n try {\n const result = await action.run(input, { context: { auth } });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .send({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n response\n .status(500)\n .send({\n error: {\n status: 'INTERNAL',\n message: getErrorMessage(e),\n details: getErrorStack(e),\n },\n })\n .end();\n }\n }\n };\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n */\nexport type FlowWithAuthPolicy<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n authProvider: RequestHandler;\n authPolicy: AuthPolicy;\n};\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withAuth<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n authProvider: RequestHandler,\n authPolicy: AuthPolicy\n): FlowWithAuthPolicy<I, O, S> {\n return {\n flow,\n authProvider,\n authPolicy,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (Flow<any, any, any> | FlowWithAuthPolicy<any, any, any>)[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ((flow as FlowWithAuthPolicy).authPolicy) {\n const flowWithPolicy = flow as FlowWithAuthPolicy;\n const flowPath = `/${pathPrefix}${flowWithPolicy.flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(\n flowPath,\n flowWithPolicy.authProvider,\n expressHandler(flowWithPolicy.flow, {\n authPolicy: flowWithPolicy.authPolicy,\n })\n );\n } else {\n const resolvedFlow = flow as Flow;\n const flowPath = `/${pathPrefix}${resolvedFlow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(resolvedFlow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"AAgBA,YAAY,gBAAgB;AAC5B,OAAO,UAA2B;AAClC,OAAO,aAAiC;AACxC,SAAuB,gCAAmC;AAC1D,SAAS,cAAc;AAEvB,SAAS,iBAAiB,qBAAqB;AAE/C,MAAM,kBAAkB;AAwCjB,SAAS,eAKd,QACA,MAGwB;AACxB,SAAO,OACL,SACA,aACkB;AAClB,UAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,QAAI,QAAQ,QAAQ,KAAK;AACzB,UAAM,OAAO,QAAQ;AAErB,QAAI;AACF,YAAM,MAAM,aAAa;AAAA,QACvB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,SAAS,GAAQ;AACf,aAAO,MAAM,CAAC;AACd,YAAM,WAAW;AAAA,QACf,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,SAAS,EAAE,WAAW;AAAA,QACxB;AAAA,MACF;AACA,eAAS,OAAO,GAAG,EAAE,KAAK,QAAQ,EAAE,IAAI;AACxC;AAAA,IACF;AAEA,QAAI,QAAQ,IAAI,QAAQ,MAAM,uBAAuB,WAAW,QAAQ;AACtE,eAAS,UAAU,KAAK;AAAA,QACtB,gBAAgB;AAAA,QAChB,qBAAqB;AAAA,MACvB,CAAC;AACD,UAAI;AACF,cAAM,UAAU,CAAC,UAAsB;AACrC,mBAAS;AAAA,YACP,WAAW,KAAK,UAAU,EAAE,SAAS,MAAM,CAAC,IAAI;AAAA,UAClD;AAAA,QACF;AACA,cAAM,SAAS,MAAM;AAAA,UACnB,OAAO;AAAA,UACP;AAAA,UACA,MACE,OAAO,IAAI,OAAO;AAAA,YAChB;AAAA,YACA,SAAS,EAAE,KAAK;AAAA,UAClB,CAAC;AAAA,QACL;AACA,iBAAS;AAAA,UACP,WAAW,KAAK,UAAU,EAAE,QAAQ,OAAO,OAAO,CAAC,IAAI;AAAA,QACzD;AACA,iBAAS,IAAI;AAAA,MACf,SAAS,GAAG;AACV,eAAO,MAAM,CAAC;AACd,iBAAS;AAAA,UACP,WACE,KAAK,UAAU;AAAA,YACb,OAAO;AAAA,cACL,QAAQ;AAAA,cACR,SAAS,gBAAgB,CAAC;AAAA,cAC1B,SAAS,cAAc,CAAC;AAAA,YAC1B;AAAA,UACF,CAAC,IACD;AAAA,QACJ;AACA,iBAAS,IAAI;AAAA,MACf;AAAA,IACF,OAAO;AACL,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,IAAI,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AAC5D,iBAAS,UAAU,qBAAqB,OAAO,UAAU,OAAO;AAChE,iBAAS,UAAU,oBAAoB,OAAO,UAAU,MAAM;AAE9D,iBACG,OAAO,GAAG,EACV,KAAK;AAAA,UACJ,QAAQ,OAAO;AAAA,QACjB,CAAC,EACA,IAAI;AAAA,MACT,SAAS,GAAG;AAEV,iBACG,OAAO,GAAG,EACV,KAAK;AAAA,UACJ,OAAO;AAAA,YACL,QAAQ;AAAA,YACR,SAAS,gBAAgB,CAAC;AAAA,YAC1B,SAAS,cAAc,CAAC;AAAA,UAC1B;AAAA,QACF,CAAC,EACA,IAAI;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;AAkBO,SAAS,SAKd,MACA,cACA,YAC6B;AAC7B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAqBO,SAAS,gBAAgB,SAAwC;AACtE,QAAM,SAAS,IAAI,WAAW,OAAO;AACrC,SAAO,MAAM;AACb,SAAO;AACT;AASO,MAAM,WAAW;AAAA;AAAA,EAEtB,OAAe,kBAAgC,CAAC;AAAA;AAAA,EAGxC;AAAA;AAAA,EAEA,OAAsB;AAAA;AAAA,EAEtB,SAAwB;AAAA,EAEhC,YAAY,SAA4B;AACtC,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,IACL;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAQ;AACZ,UAAM,SAAS,QAAQ;AAEvB,WAAO,IAAI,WAAW,KAAK,KAAK,QAAQ,iBAAiB,CAAC;AAC1D,WAAO,IAAI,KAAK,KAAK,QAAQ,IAAI,CAAC;AAElC,WAAO,MAAM,sCAAsC;AACnD,UAAM,aAAa,KAAK,QAAQ,cAAc;AAC9C,SAAK,QAAQ,OAAO,QAAQ,CAAC,SAAS;AACpC,UAAK,KAA4B,YAAY;AAC3C,cAAM,iBAAiB;AACvB,cAAM,WAAW,IAAI,UAAU,GAAG,eAAe,KAAK,SAAS,IAAI;AACnE,eAAO,MAAM,MAAM,QAAQ,EAAE;AAC7B,eAAO;AAAA,UACL;AAAA,UACA,eAAe;AAAA,UACf,eAAe,eAAe,MAAM;AAAA,YAClC,YAAY,eAAe;AAAA,UAC7B,CAAC;AAAA,QACH;AAAA,MACF,OAAO;AACL,cAAM,eAAe;AACrB,cAAM,WAAW,IAAI,UAAU,GAAG,aAAa,SAAS,IAAI;AAC5D,eAAO,MAAM,MAAM,QAAQ,EAAE;AAC7B,eAAO,KAAK,UAAU,eAAe,YAAY,CAAC;AAAA,MACpD;AAAA,IACF,CAAC;AACD,SAAK,OACH,KAAK,SAAS,SACb,QAAQ,IAAI,OAAO,SAAS,QAAQ,IAAI,IAAI,IAAI,MACjD;AACF,SAAK,SAAS,OAAO,OAAO,KAAK,MAAM,MAAM;AAC3C,aAAO,MAAM,2CAA2C,KAAK,IAAI,EAAE;AACnE,iBAAW,gBAAgB,KAAK,IAAI;AAAA,IACtC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAsB;AAC1B,QAAI,CAAC,KAAK,QAAQ;AAChB;AAAA,IACF;AACA,WAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,WAAK,OAAQ,MAAM,CAAC,QAAQ;AAC1B,YAAI,KAAK;AACP,iBAAO;AAAA,YACL,2CAA2C,KAAK,IAAI,KAAK,GAAG;AAAA,UAC9D;AACA,iBAAO,GAAG;AAAA,QACZ;AACA,cAAM,QAAQ,WAAW,gBAAgB,QAAQ,IAAI;AACrD,YAAI,QAAQ,IAAI;AACd,qBAAW,gBAAgB,OAAO,OAAO,CAAC;AAAA,QAC5C;AACA,eAAO;AAAA,UACL,uBAAuB,KAAK,IAAI;AAAA,QAClC;AACA,aAAK,OAAO;AACZ,aAAK,SAAS;AACd,gBAAQ;AAAA,MACV,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,UAAU;AACrB,WAAO,QAAQ;AAAA,MACb,WAAW,gBAAgB,IAAI,CAAC,WAAW,OAAO,KAAK,CAAC;AAAA,IAC1D;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { CorsOptions } from 'cors';\nimport express from 'express';\nimport {\n Action,\n ActionContext,\n Flow,\n runWithStreamingCallback,\n z,\n} from 'genkit';\nimport {\n ContextProvider,\n RequestData,\n getCallableJSON,\n getHttpStatus,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n let input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers).map(([key, value]) => [\n key.toLowerCase(),\n Array.isArray(value) ? value.join(' ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n response.writeHead(200, {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n });\n try {\n const onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n const result = await runWithStreamingCallback(\n action.__registry,\n onChunk,\n () =>\n action.run(input, {\n onChunk,\n context,\n })\n );\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.write(\n `error: ${JSON.stringify({ error: getCallableJSON(e) })}${streamDelimiter}`\n );\n response.end();\n }\n } else {\n try {\n const result = await action.run(input, { context });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('context' in flow) {\n const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(\n flowPath,\n expressHandler(flow.flow, { contextProvider: flow.context })\n );\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "genai",
10
10
  "generative-ai"
11
11
  ],
12
- "version": "1.0.0-rc.9",
12
+ "version": "1.0.5",
13
13
  "type": "commonjs",
14
14
  "repository": {
15
15
  "type": "git",
@@ -24,7 +24,8 @@
24
24
  },
25
25
  "peerDependencies": {
26
26
  "express": "^4.21.1",
27
- "genkit": "1.0.0-rc.9"
27
+ "@genkit-ai/core": "1.0.5",
28
+ "genkit": "^1.0.5"
28
29
  },
29
30
  "devDependencies": {
30
31
  "get-port": "^5.1.0",
package/src/index.ts CHANGED
@@ -14,88 +14,66 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import * as bodyParser from 'body-parser';
17
+ import bodyParser from 'body-parser';
18
18
  import cors, { CorsOptions } from 'cors';
19
- import express, { RequestHandler } from 'express';
20
- import { Action, Flow, runWithStreamingCallback, z } from 'genkit';
19
+ import express from 'express';
20
+ import {
21
+ Action,
22
+ ActionContext,
23
+ Flow,
24
+ runWithStreamingCallback,
25
+ z,
26
+ } from 'genkit';
27
+ import {
28
+ ContextProvider,
29
+ RequestData,
30
+ getCallableJSON,
31
+ getHttpStatus,
32
+ } from 'genkit/context';
21
33
  import { logger } from 'genkit/logging';
22
34
  import { Server } from 'http';
23
- import { getErrorMessage, getErrorStack } from './utils';
24
35
 
25
36
  const streamDelimiter = '\n\n';
26
37
 
27
- /**
28
- * Auth policy context is an object passed to the auth policy providing details necessary for auth.
29
- */
30
- export interface AuthPolicyContext<
31
- I extends z.ZodTypeAny = z.ZodTypeAny,
32
- O extends z.ZodTypeAny = z.ZodTypeAny,
33
- S extends z.ZodTypeAny = z.ZodTypeAny,
34
- > {
35
- action?: Action<I, O, S>;
36
- input: z.infer<I>;
37
- auth?: Record<string, any>;
38
- request: RequestWithAuth;
39
- }
40
-
41
- /**
42
- * Flow Auth policy. Consumes the authorization context of the flow and
43
- * performs checks before the flow runs. If this throws, the flow will not
44
- * be executed.
45
- */
46
- export interface AuthPolicy<
47
- I extends z.ZodTypeAny = z.ZodTypeAny,
48
- O extends z.ZodTypeAny = z.ZodTypeAny,
49
- S extends z.ZodTypeAny = z.ZodTypeAny,
50
- > {
51
- (ctx: AuthPolicyContext<I, O, S>): void | Promise<void>;
52
- }
53
-
54
- /**
55
- * For express-based flows, req.auth should contain the value to bepassed into
56
- * the flow context.
57
- */
58
- export interface RequestWithAuth extends express.Request {
59
- auth?: Record<string, any>;
60
- }
61
-
62
38
  /**
63
39
  * Exposes provided flow or an action as express handler.
64
40
  */
65
41
  export function expressHandler<
42
+ C extends ActionContext = ActionContext,
66
43
  I extends z.ZodTypeAny = z.ZodTypeAny,
67
44
  O extends z.ZodTypeAny = z.ZodTypeAny,
68
45
  S extends z.ZodTypeAny = z.ZodTypeAny,
69
46
  >(
70
47
  action: Action<I, O, S>,
71
48
  opts?: {
72
- authPolicy?: AuthPolicy<I, O, S>;
49
+ contextProvider?: ContextProvider<C, I>;
73
50
  }
74
51
  ): express.RequestHandler {
75
52
  return async (
76
- request: RequestWithAuth,
53
+ request: express.Request,
77
54
  response: express.Response
78
55
  ): Promise<void> => {
79
56
  const { stream } = request.query;
80
- let input = request.body.data;
81
- const auth = request.auth;
57
+ let input = request.body.data as z.infer<I>;
58
+ let context: Record<string, any>;
82
59
 
83
60
  try {
84
- await opts?.authPolicy?.({
85
- action,
86
- auth,
87
- input,
88
- request,
89
- });
61
+ context =
62
+ (await opts?.contextProvider?.({
63
+ method: request.method as RequestData['method'],
64
+ headers: Object.fromEntries(
65
+ Object.entries(request.headers).map(([key, value]) => [
66
+ key.toLowerCase(),
67
+ Array.isArray(value) ? value.join(' ') : String(value),
68
+ ])
69
+ ),
70
+ input,
71
+ })) || {};
90
72
  } catch (e: any) {
91
- logger.debug(e);
92
- const respBody = {
93
- error: {
94
- status: 'PERMISSION_DENIED',
95
- message: e.message || 'Permission denied to resource',
96
- },
97
- };
98
- response.status(403).send(respBody).end();
73
+ logger.error(
74
+ `Auth policy failed with error: ${(e as Error).message}\n${(e as Error).stack}`
75
+ );
76
+ response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();
99
77
  return;
100
78
  }
101
79
 
@@ -116,7 +94,7 @@ export function expressHandler<
116
94
  () =>
117
95
  action.run(input, {
118
96
  onChunk,
119
- context: { auth },
97
+ context,
120
98
  })
121
99
  );
122
100
  response.write(
@@ -124,44 +102,32 @@ export function expressHandler<
124
102
  );
125
103
  response.end();
126
104
  } catch (e) {
127
- logger.error(e);
105
+ logger.error(
106
+ `Streaming request failed with error: ${(e as Error).message}\n${(e as Error).stack}`
107
+ );
128
108
  response.write(
129
- 'data: ' +
130
- JSON.stringify({
131
- error: {
132
- status: 'INTERNAL',
133
- message: getErrorMessage(e),
134
- details: getErrorStack(e),
135
- },
136
- }) +
137
- streamDelimiter
109
+ `error: ${JSON.stringify({ error: getCallableJSON(e) })}${streamDelimiter}`
138
110
  );
139
111
  response.end();
140
112
  }
141
113
  } else {
142
114
  try {
143
- const result = await action.run(input, { context: { auth } });
115
+ const result = await action.run(input, { context });
144
116
  response.setHeader('x-genkit-trace-id', result.telemetry.traceId);
145
117
  response.setHeader('x-genkit-span-id', result.telemetry.spanId);
146
118
  // Responses for non-streaming flows are passed back with the flow result stored in a field called "result."
147
119
  response
148
120
  .status(200)
149
- .send({
121
+ .json({
150
122
  result: result.result,
151
123
  })
152
124
  .end();
153
125
  } catch (e) {
154
126
  // Errors for non-streaming flows are passed back as standard API errors.
155
- response
156
- .status(500)
157
- .send({
158
- error: {
159
- status: 'INTERNAL',
160
- message: getErrorMessage(e),
161
- details: getErrorStack(e),
162
- },
163
- })
164
- .end();
127
+ logger.error(
128
+ `Non-streaming request failed with error: ${(e as Error).message}\n${(e as Error).stack}`
129
+ );
130
+ response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();
165
131
  }
166
132
  }
167
133
  };
@@ -170,32 +136,31 @@ export function expressHandler<
170
136
  /**
171
137
  * A wrapper object containing a flow with its associated auth policy.
172
138
  */
173
- export type FlowWithAuthPolicy<
139
+ export type FlowWithContextProvider<
140
+ C extends ActionContext = ActionContext,
174
141
  I extends z.ZodTypeAny = z.ZodTypeAny,
175
142
  O extends z.ZodTypeAny = z.ZodTypeAny,
176
143
  S extends z.ZodTypeAny = z.ZodTypeAny,
177
144
  > = {
178
145
  flow: Flow<I, O, S>;
179
- authProvider: RequestHandler;
180
- authPolicy: AuthPolicy;
146
+ context: ContextProvider<C, I>;
181
147
  };
182
148
 
183
149
  /**
184
150
  * Adds an auth policy to the flow.
185
151
  */
186
- export function withAuth<
152
+ export function withContextProvider<
153
+ C extends ActionContext = ActionContext,
187
154
  I extends z.ZodTypeAny = z.ZodTypeAny,
188
155
  O extends z.ZodTypeAny = z.ZodTypeAny,
189
156
  S extends z.ZodTypeAny = z.ZodTypeAny,
190
157
  >(
191
158
  flow: Flow<I, O, S>,
192
- authProvider: RequestHandler,
193
- authPolicy: AuthPolicy
194
- ): FlowWithAuthPolicy<I, O, S> {
159
+ context: ContextProvider<C, I>
160
+ ): FlowWithContextProvider<C, I, O, S> {
195
161
  return {
196
162
  flow,
197
- authProvider,
198
- authPolicy,
163
+ context,
199
164
  };
200
165
  }
201
166
 
@@ -204,7 +169,7 @@ export function withAuth<
204
169
  */
205
170
  export interface FlowServerOptions {
206
171
  /** List of flows to expose via the flow server. */
207
- flows: (Flow<any, any, any> | FlowWithAuthPolicy<any, any, any>)[];
172
+ flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];
208
173
  /** Port to run the server on. Defaults to env.PORT or 3400. */
209
174
  port?: number;
210
175
  /** CORS options for the server. */
@@ -260,22 +225,17 @@ export class FlowServer {
260
225
  logger.debug('Running flow server with flow paths:');
261
226
  const pathPrefix = this.options.pathPrefix ?? '';
262
227
  this.options.flows?.forEach((flow) => {
263
- if ((flow as FlowWithAuthPolicy).authPolicy) {
264
- const flowWithPolicy = flow as FlowWithAuthPolicy;
265
- const flowPath = `/${pathPrefix}${flowWithPolicy.flow.__action.name}`;
228
+ if ('context' in flow) {
229
+ const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;
266
230
  logger.debug(` - ${flowPath}`);
267
231
  server.post(
268
232
  flowPath,
269
- flowWithPolicy.authProvider,
270
- expressHandler(flowWithPolicy.flow, {
271
- authPolicy: flowWithPolicy.authPolicy,
272
- })
233
+ expressHandler(flow.flow, { contextProvider: flow.context })
273
234
  );
274
235
  } else {
275
- const resolvedFlow = flow as Flow;
276
- const flowPath = `/${pathPrefix}${resolvedFlow.__action.name}`;
236
+ const flowPath = `/${pathPrefix}${flow.__action.name}`;
277
237
  logger.debug(` - ${flowPath}`);
278
- server.post(flowPath, expressHandler(resolvedFlow));
238
+ server.post(flowPath, expressHandler(flow));
279
239
  }
280
240
  });
281
241
  this.port =
@@ -14,22 +14,50 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
+ import { RequestData } from '@genkit-ai/core';
17
18
  import * as assert from 'assert';
18
19
  import express from 'express';
19
- import { GenerateResponseData, Genkit, genkit, z } from 'genkit';
20
- import { runFlow, streamFlow } from 'genkit/client';
20
+ import {
21
+ GenerateResponseData,
22
+ Genkit,
23
+ UserFacingError,
24
+ genkit,
25
+ z,
26
+ } from 'genkit';
27
+ import { runFlow, streamFlow } from 'genkit/beta/client';
28
+ import { ContextProvider } from 'genkit/context';
21
29
  import { GenerateResponseChunkData, ModelAction } from 'genkit/model';
22
30
  import getPort from 'get-port';
23
31
  import * as http from 'http';
24
32
  import { afterEach, beforeEach, describe, it } from 'node:test';
25
33
  import {
26
34
  FlowServer,
27
- RequestWithAuth,
28
35
  expressHandler,
29
36
  startFlowServer,
30
- withAuth,
37
+ withContextProvider,
31
38
  } from '../src/index.js';
32
39
 
40
+ interface Context {
41
+ auth: {
42
+ user: string;
43
+ };
44
+ }
45
+
46
+ const contextProvider: ContextProvider<Context> = (req: RequestData) => {
47
+ assert.ok(req.method, 'method must be set');
48
+ assert.ok(req.headers, 'headers must be set');
49
+ assert.ok(req.input, 'input must be set');
50
+
51
+ if (req.headers['authorization'] !== 'open sesame') {
52
+ throw new UserFacingError('PERMISSION_DENIED', 'not authorized');
53
+ }
54
+ return {
55
+ auth: {
56
+ user: 'Ali Baba',
57
+ },
58
+ };
59
+ };
60
+
33
61
  describe('expressHandler', async () => {
34
62
  let server: http.Server;
35
63
  let port;
@@ -82,7 +110,7 @@ describe('expressHandler', async () => {
82
110
  inputSchema: z.object({ question: z.string() }),
83
111
  },
84
112
  async (input, { context }) => {
85
- return `${input.question} - ${JSON.stringify(context.auth)}`;
113
+ return `${input.question} - ${JSON.stringify(context!.auth)}`;
86
114
  }
87
115
  );
88
116
 
@@ -96,54 +124,13 @@ describe('expressHandler', async () => {
96
124
  app.post('/streamingFlow', expressHandler(streamingFlow));
97
125
  app.post(
98
126
  '/flowWithAuth',
99
- async (req, resp, next) => {
100
- (req as RequestWithAuth).auth = {
101
- user:
102
- req.header('authorization') === 'open sesame'
103
- ? 'Ali Baba'
104
- : '40 thieves',
105
- };
106
- next();
107
- },
108
- expressHandler(flowWithAuth, {
109
- authPolicy: ({ auth, action, input, request }) => {
110
- assert.ok(auth, 'auth must be set');
111
- assert.ok(action, 'flow must be set');
112
- assert.ok(input, 'input must be set');
113
- assert.ok(request, 'request must be set');
114
-
115
- if (auth.user !== 'Ali Baba') {
116
- throw new Error('not authorized');
117
- }
118
- },
119
- })
127
+ expressHandler(flowWithAuth, { contextProvider })
120
128
  );
121
-
122
129
  // Can also expose any action.
123
130
  app.post('/echoModel', expressHandler(echoModel));
124
131
  app.post(
125
132
  '/echoModelWithAuth',
126
- async (req, resp, next) => {
127
- (req as RequestWithAuth).auth = {
128
- user:
129
- req.header('authorization') === 'open sesame'
130
- ? 'Ali Baba'
131
- : '40 thieves',
132
- };
133
- next();
134
- },
135
- expressHandler(echoModel, {
136
- authPolicy: ({ auth, action, input, request }) => {
137
- assert.ok(auth, 'auth must be set');
138
- assert.ok(action, 'flow must be set');
139
- assert.ok(input, 'input must be set');
140
- assert.ok(request, 'request must be set');
141
-
142
- if (auth.user !== 'Ali Baba') {
143
- throw new Error('not authorized');
144
- }
145
- },
146
- })
133
+ expressHandler(echoModel, { contextProvider })
147
134
  );
148
135
 
149
136
  server = app.listen(port, () => {
@@ -213,7 +200,7 @@ describe('expressHandler', async () => {
213
200
  question: 'hello',
214
201
  },
215
202
  headers: {
216
- Authorization: 'thieve #24',
203
+ Authorization: 'thief #24',
217
204
  },
218
205
  });
219
206
  await assert.rejects(result, (err) => {
@@ -264,7 +251,7 @@ describe('expressHandler', async () => {
264
251
  ],
265
252
  },
266
253
  headers: {
267
- Authorization: 'thieve #24',
254
+ Authorization: 'thief #24',
268
255
  },
269
256
  });
270
257
  await assert.rejects(result, (err) => {
@@ -283,7 +270,7 @@ describe('expressHandler', async () => {
283
270
  });
284
271
 
285
272
  const gotChunks: GenerateResponseChunkData[] = [];
286
- for await (const chunk of result.stream()) {
273
+ for await (const chunk of result.stream) {
287
274
  gotChunks.push(chunk);
288
275
  }
289
276
 
@@ -293,7 +280,7 @@ describe('expressHandler', async () => {
293
280
  { index: 0, role: 'model', content: [{ text: '1' }] },
294
281
  ]);
295
282
 
296
- assert.strictEqual(await result.output(), 'Echo: olleh');
283
+ assert.strictEqual(await result.output, 'Echo: olleh');
297
284
  });
298
285
 
299
286
  it('stream a model', async () => {
@@ -310,11 +297,11 @@ describe('expressHandler', async () => {
310
297
  });
311
298
 
312
299
  const gotChunks: any[] = [];
313
- for await (const chunk of result.stream()) {
300
+ for await (const chunk of result.stream) {
314
301
  gotChunks.push(chunk);
315
302
  }
316
303
 
317
- const output = await result.output();
304
+ const output = await result.output;
318
305
  assert.strictEqual(output.finishReason, 'stop');
319
306
  assert.deepStrictEqual(output.message, {
320
307
  role: 'model',
@@ -336,7 +323,7 @@ describe('startFlowServer', async () => {
336
323
 
337
324
  beforeEach(async () => {
338
325
  const ai = genkit({});
339
- const echoModel = defineEchoModel(ai);
326
+ defineEchoModel(ai);
340
327
 
341
328
  const voidInput = ai.defineFlow('voidInput', async () => {
342
329
  return 'banana';
@@ -382,7 +369,7 @@ describe('startFlowServer', async () => {
382
369
  inputSchema: z.object({ question: z.string() }),
383
370
  },
384
371
  async (input, { context }) => {
385
- return `${input.question} - ${JSON.stringify(context.auth)}`;
372
+ return `${input.question} - ${JSON.stringify(context!.auth)}`;
386
373
  }
387
374
  );
388
375
 
@@ -394,28 +381,7 @@ describe('startFlowServer', async () => {
394
381
  stringInput,
395
382
  objectInput,
396
383
  streamingFlow,
397
- withAuth(
398
- flowWithAuth,
399
- async (req, resp, next) => {
400
- (req as RequestWithAuth).auth = {
401
- user:
402
- req.header('authorization') === 'open sesame'
403
- ? 'Ali Baba'
404
- : '40 thieves',
405
- };
406
- return next();
407
- },
408
- ({ auth, action, input, request }) => {
409
- assert.ok(auth, 'auth must be set');
410
- assert.ok(action, 'flow must be set');
411
- assert.ok(input, 'input must be set');
412
- assert.ok(request, 'request must be set');
413
-
414
- if (auth.user !== 'Ali Baba') {
415
- throw new Error('not authorized');
416
- }
417
- }
418
- ),
384
+ withContextProvider(flowWithAuth, contextProvider),
419
385
  ],
420
386
  port,
421
387
  });
@@ -483,7 +449,7 @@ describe('startFlowServer', async () => {
483
449
  question: 'hello',
484
450
  },
485
451
  headers: {
486
- Authorization: 'thieve #24',
452
+ Authorization: 'thief #24',
487
453
  },
488
454
  });
489
455
  await assert.rejects(result, (err) => {
@@ -502,7 +468,7 @@ describe('startFlowServer', async () => {
502
468
  });
503
469
 
504
470
  const gotChunks: GenerateResponseChunkData[] = [];
505
- for await (const chunk of result.stream()) {
471
+ for await (const chunk of result.stream) {
506
472
  gotChunks.push(chunk);
507
473
  }
508
474
 
@@ -512,7 +478,7 @@ describe('startFlowServer', async () => {
512
478
  { index: 0, role: 'model', content: [{ text: '1' }] },
513
479
  ]);
514
480
 
515
- assert.strictEqual(await result.output(), 'Echo: olleh');
481
+ assert.strictEqual(await result.output, 'Echo: olleh');
516
482
  });
517
483
  });
518
484
  });