@langchain/langgraph-api 0.0.19 → 0.0.21

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.
@@ -0,0 +1,120 @@
1
+ import { HTTPException } from "hono/http-exception";
2
+ import * as url from "node:url";
3
+ import * as path from "path";
4
+ let CUSTOM_AUTH;
5
+ let DISABLE_STUDIO_AUTH = false;
6
+ export const isAuthRegistered = () => CUSTOM_AUTH != null;
7
+ export const isStudioAuthDisabled = () => DISABLE_STUDIO_AUTH;
8
+ function convertError(error) {
9
+ const isHTTPAuthException = (error) => {
10
+ return (typeof error === "object" &&
11
+ error != null &&
12
+ "status" in error &&
13
+ "headers" in error);
14
+ };
15
+ if (isHTTPAuthException(error)) {
16
+ throw new HTTPException(error.status, {
17
+ message: error.message,
18
+ res: new Response(error.message || "Unauthorized", {
19
+ status: error.status,
20
+ headers: error.headers,
21
+ }),
22
+ });
23
+ }
24
+ throw error;
25
+ }
26
+ export async function authorize(payload) {
27
+ // find filters and execute them
28
+ const handlers = CUSTOM_AUTH?.["~handlerCache"];
29
+ if (!handlers)
30
+ return { filters: undefined, value: payload.value };
31
+ const cbKey = [
32
+ `${payload.resource}:${payload.action}`,
33
+ `${payload.resource}`,
34
+ `*:${payload.action}`,
35
+ `*`,
36
+ ].find((priority) => handlers.callbacks?.[priority]);
37
+ const handler = cbKey ? handlers.callbacks?.[cbKey] : undefined;
38
+ if (!handler || !payload.context) {
39
+ return { filters: undefined, value: payload.value };
40
+ }
41
+ try {
42
+ const result = await handler({
43
+ event: `${payload.resource}:${payload.action}`,
44
+ resource: payload.resource,
45
+ action: payload.action,
46
+ value: payload.value,
47
+ permissions: payload.context?.scopes,
48
+ user: payload.context?.user,
49
+ });
50
+ if (result == null || result == true) {
51
+ return { filters: undefined, value: payload.value };
52
+ }
53
+ if (result === false)
54
+ throw new HTTPException(403);
55
+ if (typeof result !== "object") {
56
+ throw new HTTPException(500, {
57
+ message: `Auth handler returned invalid result. Expected filter object, null, undefined or boolean. Got "${typeof result}" instead.`,
58
+ });
59
+ }
60
+ return { filters: result, value: payload.value };
61
+ }
62
+ catch (error) {
63
+ throw convertError(error);
64
+ }
65
+ }
66
+ export async function authenticate(request) {
67
+ const handlers = CUSTOM_AUTH?.["~handlerCache"];
68
+ if (!handlers?.authenticate)
69
+ return undefined;
70
+ try {
71
+ const response = await handlers.authenticate(request);
72
+ // normalize auth response
73
+ const { scopes, user } = (() => {
74
+ if (typeof response === "string") {
75
+ return {
76
+ scopes: [],
77
+ user: {
78
+ permissions: [],
79
+ identity: response,
80
+ display_name: response,
81
+ is_authenticated: true,
82
+ },
83
+ };
84
+ }
85
+ if ("identity" in response && typeof response.identity === "string") {
86
+ const scopes = "permissions" in response && Array.isArray(response.permissions)
87
+ ? response.permissions
88
+ : [];
89
+ return {
90
+ scopes,
91
+ user: {
92
+ ...response,
93
+ permissions: scopes,
94
+ is_authenticated: response.is_authenticated ?? true,
95
+ display_name: response.display_name ?? response.identity,
96
+ },
97
+ };
98
+ }
99
+ throw new Error("Invalid auth response received. Make sure to either return a `string` or an object with `identity` property.");
100
+ })();
101
+ return { scopes, user };
102
+ }
103
+ catch (error) {
104
+ throw convertError(error);
105
+ }
106
+ }
107
+ export async function registerAuth(auth, options) {
108
+ if (!auth.path)
109
+ return;
110
+ // TODO: handle options.auth.disable_studio_auth
111
+ const [userFile, exportSymbol] = auth.path.split(":", 2);
112
+ const sourceFile = path.resolve(options.cwd, userFile);
113
+ const module = (await import(url.pathToFileURL(sourceFile).toString()).then((module) => module[exportSymbol || "default"]));
114
+ if (!module)
115
+ throw new Error(`Failed to load auth: ${auth.path}`);
116
+ if (!("~handlerCache" in module))
117
+ throw new Error(`Auth must be an instance of Auth: ${auth.path}`);
118
+ CUSTOM_AUTH = module;
119
+ DISABLE_STUDIO_AUTH = auth.disable_studio_auth ?? false;
120
+ }
@@ -9,6 +9,10 @@ export declare function spawnServer(args: {
9
9
  ui_config?: {
10
10
  shared?: string[];
11
11
  };
12
+ auth?: {
13
+ path?: string;
14
+ disable_studio_auth?: boolean;
15
+ };
12
16
  };
13
17
  env: NodeJS.ProcessEnv;
14
18
  hostUrl: string;
@@ -30,6 +30,7 @@ For production use, please use LangGraph Cloud.
30
30
  nWorkers: Number.parseInt(args.nJobsPerWorker, 10),
31
31
  host: args.host,
32
32
  graphs: context.config.graphs,
33
+ auth: context.config.auth,
33
34
  ui: context.config.ui,
34
35
  ui_config: context.config.ui_config,
35
36
  cwd: options.projectCwd,
@@ -10,7 +10,7 @@ export const GRAPHS = {};
10
10
  export const GRAPH_SPEC = {};
11
11
  export const GRAPH_SCHEMA = {};
12
12
  export const NAMESPACE_GRAPH = uuid.parse("6ba7b821-9dad-11d1-80b4-00c04fd430c8");
13
- const ConfigSchema = z.record(z.unknown());
13
+ const ConfigSchema = z.record(z.record(z.unknown()));
14
14
  export const getAssistantId = (graphId) => {
15
15
  if (graphId in GRAPHS)
16
16
  return uuid.v5(graphId, NAMESPACE_GRAPH);
@@ -37,7 +37,7 @@ export async function registerFromEnv(specs, options) {
37
37
  config: config ?? {},
38
38
  if_exists: "do_nothing",
39
39
  name: graphId,
40
- });
40
+ }, undefined);
41
41
  return resolved;
42
42
  }));
43
43
  }
package/dist/queue.mjs CHANGED
@@ -84,7 +84,7 @@ const worker = async (run, attempt, abortSignal) => {
84
84
  }
85
85
  finally {
86
86
  if (temporary) {
87
- await Threads.delete(run.thread_id);
87
+ await Threads.delete(run.thread_id, undefined);
88
88
  }
89
89
  else {
90
90
  await Threads.setStatus(run.thread_id, { checkpoint, exception });
package/dist/schemas.mjs CHANGED
@@ -45,6 +45,7 @@ export const AssistantPatch = z
45
45
  .object({
46
46
  graph_id: z.string().describe("The graph to use.").optional(),
47
47
  config: AssistantConfig.optional(),
48
+ name: z.string().optional(),
48
49
  metadata: z
49
50
  .object({})
50
51
  .catchall(z.any())
package/dist/server.mjs CHANGED
@@ -13,6 +13,8 @@ import { queue } from "./queue.mjs";
13
13
  import { logger, requestLogger } from "./logging.mjs";
14
14
  import { checkpointer } from "./storage/checkpoint.mjs";
15
15
  import { store as graphStore } from "./storage/store.mjs";
16
+ import { auth } from "./auth/custom.mjs";
17
+ import { registerAuth } from "./auth/index.mjs";
16
18
  const app = new Hono();
17
19
  // This is used to match the behavior of the original LangGraph API
18
20
  // where the content-type is not being validated. Might be nice
@@ -27,10 +29,6 @@ app.use(async (c, next) => {
27
29
  });
28
30
  app.use(cors());
29
31
  app.use(requestLogger());
30
- app.route("/", assistants);
31
- app.route("/", runs);
32
- app.route("/", threads);
33
- app.route("/", store);
34
32
  app.get("/info", (c) => c.json({ flags: { assistants: true, crons: false } }));
35
33
  app.post("/internal/truncate", zValidator("json", z.object({
36
34
  runs: z.boolean().optional(),
@@ -43,12 +41,23 @@ app.post("/internal/truncate", zValidator("json", z.object({
43
41
  truncate({ runs, threads, assistants, checkpointer, store });
44
42
  return c.json({ ok: true });
45
43
  });
44
+ app.use(auth());
45
+ app.route("/", assistants);
46
+ app.route("/", runs);
47
+ app.route("/", threads);
48
+ app.route("/", store);
46
49
  export const StartServerSchema = z.object({
47
50
  port: z.number(),
48
51
  nWorkers: z.number(),
49
52
  host: z.string(),
50
53
  cwd: z.string(),
51
54
  graphs: z.record(z.string()),
55
+ auth: z
56
+ .object({
57
+ path: z.string().optional(),
58
+ disable_studio_auth: z.boolean().default(false),
59
+ })
60
+ .optional(),
52
61
  ui: z.record(z.string()).optional(),
53
62
  ui_config: z.object({ shared: z.array(z.string()).optional() }).optional(),
54
63
  });
@@ -65,6 +74,10 @@ export async function startServer(options) {
65
74
  };
66
75
  logger.info(`Registering graphs from ${options.cwd}`);
67
76
  await registerFromEnv(options.graphs, { cwd: options.cwd });
77
+ if (options.auth?.path) {
78
+ logger.info(`Loading auth from ${options.auth.path}`);
79
+ await registerAuth(options.auth, { cwd: options.cwd });
80
+ }
68
81
  if (options.ui) {
69
82
  logger.info(`Loading UI`);
70
83
  const { api, registerGraphUi } = await import("./ui/load.mjs");