langgraph-api 0.1.0__tar.gz → 0.1.3__tar.gz

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.

Potentially problematic release.


This version of langgraph-api might be problematic. Click here for more details.

Files changed (103) hide show
  1. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/PKG-INFO +1 -2
  2. langgraph_api-0.1.3/langgraph_api/__init__.py +1 -0
  3. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/auth/custom.py +25 -4
  4. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/auth/middleware.py +1 -2
  5. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/config.py +10 -4
  6. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/graph.py +4 -12
  7. langgraph_api-0.1.3/langgraph_api/js/base.py +29 -0
  8. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/build.mts +3 -3
  9. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/client.mts +64 -3
  10. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/global.d.ts +1 -0
  11. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/package.json +4 -3
  12. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/remote.py +93 -2
  13. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/src/graph.mts +0 -6
  14. langgraph_api-0.1.3/langgraph_api/js/src/utils/files.mts +4 -0
  15. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/api.test.mts +83 -83
  16. langgraph_api-0.1.3/langgraph_api/js/tests/auth.test.mts +648 -0
  17. langgraph_api-0.1.3/langgraph_api/js/tests/compose-postgres.auth.yml +59 -0
  18. langgraph_api-0.1.3/langgraph_api/js/tests/graphs/agent_simple.mts +79 -0
  19. langgraph_api-0.1.3/langgraph_api/js/tests/graphs/auth.mts +106 -0
  20. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/graphs/package.json +3 -1
  21. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/graphs/yarn.lock +9 -4
  22. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/yarn.lock +18 -23
  23. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/metadata.py +6 -0
  24. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/serde.py +1 -1
  25. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/server.py +38 -0
  26. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/stream.py +2 -1
  27. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/worker.py +1 -22
  28. langgraph_api-0.1.3/langgraph_license/__init__.py +0 -0
  29. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/pyproject.toml +2 -2
  30. langgraph_api-0.1.0/langgraph_api/__init__.py +0 -1
  31. langgraph_api-0.1.0/langgraph_api/js/base.py +0 -12
  32. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/LICENSE +0 -0
  33. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/README.md +0 -0
  34. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/api/__init__.py +0 -0
  35. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/api/assistants.py +0 -0
  36. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/api/mcp.py +0 -0
  37. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/api/meta.py +0 -0
  38. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/api/openapi.py +0 -0
  39. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/api/runs.py +0 -0
  40. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/api/store.py +0 -0
  41. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/api/threads.py +0 -0
  42. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/api/ui.py +0 -0
  43. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/asyncio.py +0 -0
  44. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/auth/__init__.py +0 -0
  45. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/auth/langsmith/__init__.py +0 -0
  46. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/auth/langsmith/backend.py +0 -0
  47. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/auth/langsmith/client.py +0 -0
  48. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/auth/noop.py +0 -0
  49. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/auth/studio_user.py +0 -0
  50. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/cli.py +0 -0
  51. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/command.py +0 -0
  52. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/cron_scheduler.py +0 -0
  53. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/errors.py +0 -0
  54. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/http.py +0 -0
  55. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/.gitignore +0 -0
  56. {langgraph_api-0.1.0/langgraph_api/middleware → langgraph_api-0.1.3/langgraph_api/js}/__init__.py +0 -0
  57. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/errors.py +0 -0
  58. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/schema.py +0 -0
  59. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/src/hooks.mjs +0 -0
  60. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/src/parser/parser.mts +0 -0
  61. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/src/parser/parser.worker.mjs +0 -0
  62. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/src/schema/types.mts +0 -0
  63. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/src/schema/types.template.mts +0 -0
  64. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/src/utils/importMap.mts +0 -0
  65. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
  66. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/src/utils/serde.mts +0 -0
  67. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/sse.py +0 -0
  68. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/compose-postgres.yml +0 -0
  69. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/graphs/.gitignore +0 -0
  70. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/graphs/agent.css +0 -0
  71. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/graphs/agent.mts +0 -0
  72. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/graphs/agent.ui.tsx +0 -0
  73. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/graphs/command.mts +0 -0
  74. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/graphs/delay.mts +0 -0
  75. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/graphs/dynamic.mts +0 -0
  76. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/graphs/error.mts +0 -0
  77. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/graphs/langgraph.json +0 -0
  78. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/graphs/nested.mts +0 -0
  79. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/graphs/weather.mts +0 -0
  80. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/parser.test.mts +0 -0
  81. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/tests/utils.mts +0 -0
  82. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/js/ui.py +0 -0
  83. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/logging.py +0 -0
  84. {langgraph_api-0.1.0/langgraph_api/models → langgraph_api-0.1.3/langgraph_api/middleware}/__init__.py +0 -0
  85. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/middleware/http_logger.py +0 -0
  86. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/middleware/private_network.py +0 -0
  87. {langgraph_api-0.1.0/langgraph_license → langgraph_api-0.1.3/langgraph_api/models}/__init__.py +0 -0
  88. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/models/run.py +0 -0
  89. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/patch.py +0 -0
  90. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/queue_entrypoint.py +0 -0
  91. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/route.py +0 -0
  92. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/schema.py +0 -0
  93. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/sse.py +0 -0
  94. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/state.py +0 -0
  95. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/thread_ttl.py +0 -0
  96. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/utils.py +0 -0
  97. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/validation.py +0 -0
  98. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_api/webhook.py +0 -0
  99. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_license/middleware.py +0 -0
  100. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_license/validation.py +0 -0
  101. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/langgraph_runtime/__init__.py +0 -0
  102. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/logging.json +0 -0
  103. {langgraph_api-0.1.0 → langgraph_api-0.1.3}/openapi.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: langgraph-api
3
- Version: 0.1.0
3
+ Version: 0.1.3
4
4
  Summary:
5
5
  License: Elastic-2.0
6
6
  Author: Nuno Campos
@@ -11,7 +11,6 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
- Requires-Dist: blockbuster (>=1.5.24,<2.0.0)
15
14
  Requires-Dist: cloudpickle (>=3.0.0,<4.0.0)
16
15
  Requires-Dist: cryptography (>=42.0.0,<45.0)
17
16
  Requires-Dist: httpx (>=0.25.0)
@@ -0,0 +1 @@
1
+ __version__ = "0.1.3"
@@ -7,7 +7,7 @@ import os
7
7
  import sys
8
8
  from collections.abc import Awaitable, Callable, Mapping
9
9
  from contextlib import AsyncExitStack
10
- from typing import Any, get_args
10
+ from typing import Any, Literal, get_args
11
11
 
12
12
  import structlog
13
13
  from langgraph_sdk import Auth
@@ -26,6 +26,7 @@ from starlette.responses import Response
26
26
  from langgraph_api.auth.langsmith.backend import LangsmithAuthBackend
27
27
  from langgraph_api.auth.studio_user import StudioUser
28
28
  from langgraph_api.config import LANGGRAPH_AUTH, LANGGRAPH_AUTH_TYPE
29
+ from langgraph_api.js.base import is_js_path
29
30
 
30
31
  logger = structlog.stdlib.get_logger(__name__)
31
32
 
@@ -56,7 +57,7 @@ def get_custom_auth_middleware() -> AuthenticationBackend:
56
57
 
57
58
 
58
59
  @functools.lru_cache(maxsize=1)
59
- def get_auth_instance() -> Auth | None:
60
+ def get_auth_instance() -> Auth | Literal["js"] | None:
60
61
  logger.info(
61
62
  f"Getting auth instance: {LANGGRAPH_AUTH}", langgraph_auth=str(LANGGRAPH_AUTH)
62
63
  )
@@ -89,6 +90,12 @@ async def handle_event(
89
90
  auth = get_auth_instance()
90
91
  if auth is None:
91
92
  return
93
+
94
+ if auth == "js":
95
+ from langgraph_api.js.remote import handle_js_auth_event
96
+
97
+ return await handle_js_auth_event(ctx, value)
98
+
92
99
  handler = _get_handler(auth, ctx)
93
100
  if not handler:
94
101
  return
@@ -195,6 +202,7 @@ def _get_custom_auth_middleware(
195
202
  else:
196
203
  path = config.get("path")
197
204
  disable_studio_auth = config.get("disable_studio_auth", disable_studio_auth)
205
+
198
206
  auth_instance = _get_auth_instance(path)
199
207
  if auth_instance is None:
200
208
  raise ValueError(
@@ -204,6 +212,12 @@ def _get_custom_auth_middleware(
204
212
  "from langgraph_sdk import Auth\n"
205
213
  "auth = Auth()"
206
214
  )
215
+
216
+ if auth_instance == "js":
217
+ from langgraph_api.js.remote import CustomJsAuthBackend
218
+
219
+ return CustomJsAuthBackend(disable_studio_auth=disable_studio_auth)
220
+
207
221
  if auth_instance._authenticate_handler is None:
208
222
  raise ValueError(
209
223
  f"Custom Auth object at path: {path} does not have an authenticate handler."
@@ -214,6 +228,7 @@ def _get_custom_auth_middleware(
214
228
  "async def authenticate(request):\n"
215
229
  ' return "my-user-id"'
216
230
  )
231
+
217
232
  result = CustomAuthBackend(
218
233
  auth_instance._authenticate_handler,
219
234
  disable_studio_auth,
@@ -223,12 +238,15 @@ def _get_custom_auth_middleware(
223
238
 
224
239
 
225
240
  @functools.lru_cache(maxsize=1)
226
- def _get_auth_instance(path: str | None = None) -> Auth | None:
241
+ def _get_auth_instance(path: str | None = None) -> Auth | Literal["js"] | None:
227
242
  if path is not None:
228
243
  auth_instance = _load_auth_obj(path)
229
244
  else:
230
245
  auth_instance = None
231
246
 
247
+ if auth_instance == "js":
248
+ return auth_instance
249
+
232
250
  if auth_instance is not None and (
233
251
  deps := _get_dependencies(auth_instance._authenticate_handler)
234
252
  ):
@@ -544,7 +562,7 @@ def normalize_user(user: Any) -> BaseUser:
544
562
  )
545
563
 
546
564
 
547
- def _load_auth_obj(path: str) -> Auth:
565
+ def _load_auth_obj(path: str) -> Auth | Literal["js"]:
548
566
  """Load an object from a path string."""
549
567
  if ":" not in path:
550
568
  raise ValueError(
@@ -555,6 +573,9 @@ def _load_auth_obj(path: str) -> Auth:
555
573
  module_name, callable_name = path.rsplit(":", 1)
556
574
  module_name = module_name.rstrip(":")
557
575
 
576
+ if is_js_path(module_name):
577
+ return "js"
578
+
558
579
  try:
559
580
  if "/" in module_name or ".py" in module_name:
560
581
  # Load from file path
@@ -36,7 +36,7 @@ def on_error(conn: HTTPConnection, exc: AuthenticationError):
36
36
 
37
37
  class ConditionalAuthenticationMiddleware(AuthenticationMiddleware):
38
38
  async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
39
- if scope["root_path"] == "/noauth":
39
+ if (root_path := scope.get("root_path")) and root_path.startswith("/noauth"):
40
40
  # disable auth for requests originating from SDK ASGI transport
41
41
  # root_path cannot be set from a request, so safe to use as auth bypass
42
42
  await self.app(scope, receive, send)
@@ -46,7 +46,6 @@ class ConditionalAuthenticationMiddleware(AuthenticationMiddleware):
46
46
  # disable auth for UI asset requests
47
47
  await self.app(scope, receive, send)
48
48
  return
49
-
50
49
  return await super().__call__(scope, receive, send)
51
50
 
52
51
 
@@ -37,6 +37,8 @@ class HttpConfig(TypedDict, total=False):
37
37
  """Disable /ui routes"""
38
38
  disable_mcp: bool
39
39
  """Disable /mcp routes"""
40
+ mount_prefix: str
41
+ """Prefix for mounted routes. E.g., "/my-deployment/api"."""
40
42
 
41
43
 
42
44
  class ThreadTTLConfig(TypedDict, total=False):
@@ -169,11 +171,15 @@ HTTP_CONFIG: HttpConfig | None = env("LANGGRAPH_HTTP", cast=_parse_json, default
169
171
  STORE_CONFIG: StoreConfig | None = env(
170
172
  "LANGGRAPH_STORE", cast=_parse_json, default=None
171
173
  )
174
+
175
+ MOUNT_PREFIX: str | None = env("MOUNT_PREFIX", cast=str, default=None) or (
176
+ HTTP_CONFIG.get("mount_prefix") if HTTP_CONFIG else None
177
+ )
178
+
172
179
  CORS_ALLOW_ORIGINS = env("CORS_ALLOW_ORIGINS", cast=CommaSeparatedStrings, default="*")
173
- if HTTP_CONFIG and HTTP_CONFIG.get("cors"):
174
- CORS_CONFIG = HTTP_CONFIG["cors"]
175
- else:
176
- CORS_CONFIG: CorsConfig | None = env("CORS_CONFIG", cast=_parse_json, default=None)
180
+ CORS_CONFIG: CorsConfig | None = env("CORS_CONFIG", cast=_parse_json, default=None) or (
181
+ HTTP_CONFIG.get("cors") if HTTP_CONFIG else None
182
+ )
177
183
  """
178
184
  {
179
185
  "type": "object",
@@ -24,7 +24,7 @@ from starlette.exceptions import HTTPException
24
24
 
25
25
  from langgraph_api import asyncio as lg_asyncio
26
26
  from langgraph_api import config
27
- from langgraph_api.js.base import BaseRemotePregel
27
+ from langgraph_api.js.base import BaseRemotePregel, is_js_path
28
28
  from langgraph_api.schema import Config
29
29
 
30
30
  if TYPE_CHECKING:
@@ -181,17 +181,6 @@ class GraphSpec(NamedTuple):
181
181
  js_bg_tasks: set[asyncio.Task] = set()
182
182
 
183
183
 
184
- def is_js_spec(spec: GraphSpec) -> bool:
185
- return spec.path is not None and os.path.splitext(spec.path)[1] in (
186
- ".ts",
187
- ".mts",
188
- ".cts",
189
- ".js",
190
- ".mjs",
191
- ".cjs",
192
- )
193
-
194
-
195
184
  def _load_graph_config_from_env() -> dict | None:
196
185
  """Return graph config from env."""
197
186
  config_str = os.getenv("LANGGRAPH_CONFIG")
@@ -283,6 +272,9 @@ async def collect_graphs_from_env(register: bool = False) -> None:
283
272
  for graph_path in glob.glob("/graphs/*.py")
284
273
  ]
285
274
 
275
+ def is_js_spec(x: GraphSpec) -> bool:
276
+ return is_js_path(x.path)
277
+
286
278
  js_specs = list(filter(is_js_spec, specs))
287
279
  py_specs = list(filterfalse(is_js_spec, specs))
288
280
 
@@ -0,0 +1,29 @@
1
+ import os
2
+
3
+ from langchain_core.runnables import Runnable
4
+
5
+ from langgraph_api.schema import Config
6
+
7
+ JS_EXTENSIONS = (
8
+ ".ts",
9
+ ".mts",
10
+ ".cts",
11
+ ".js",
12
+ ".mjs",
13
+ ".cjs",
14
+ )
15
+
16
+
17
+ def is_js_path(path: str | None) -> bool:
18
+ if path is None:
19
+ return False
20
+ return os.path.splitext(path)[1] in JS_EXTENSIONS
21
+
22
+
23
+ class BaseRemotePregel(Runnable):
24
+ name: str = "LangGraph"
25
+
26
+ graph_id: str
27
+
28
+ # Config passed from get_graph()
29
+ config: Config
@@ -4,19 +4,19 @@ import { z } from "zod";
4
4
  import * as fs from "node:fs/promises";
5
5
  import * as path from "node:path";
6
6
  import {
7
- filterValidGraphSpecs,
8
7
  GraphSchema,
9
8
  resolveGraph,
10
9
  runGraphSchemaWorker,
11
10
  } from "./src/graph.mts";
12
11
  import { build } from "@langchain/langgraph-ui";
12
+ import { filterValidExportPath } from "./src/utils/files.mts";
13
13
 
14
14
  const __dirname = new URL(".", import.meta.url).pathname;
15
15
 
16
16
  async function main() {
17
- const specs = filterValidGraphSpecs(
17
+ const specs = Object.entries(
18
18
  z.record(z.string()).parse(JSON.parse(process.env.LANGSERVE_GRAPHS))
19
- );
19
+ ).filter(([_, spec]) => filterValidExportPath(spec));
20
20
 
21
21
  const GRAPH_SCHEMAS: Record<string, Record<string, GraphSchema> | false> = {};
22
22
  let failed = false;
@@ -42,11 +42,17 @@ import {
42
42
  GraphSchema,
43
43
  resolveGraph,
44
44
  GraphSpec,
45
- filterValidGraphSpecs,
46
45
  type CompiledGraphFactory,
47
46
  } from "./src/graph.mts";
48
47
  import { asyncExitHook, gracefulExit } from "exit-hook";
49
48
  import { awaitAllCallbacks } from "@langchain/core/callbacks/promises";
49
+ import { StatusCode } from "hono/utils/http-status";
50
+ import {
51
+ authenticate,
52
+ authorize,
53
+ registerAuth,
54
+ } from "@langchain/langgraph-api/auth";
55
+ import { filterValidExportPath } from "./src/utils/files.mts";
50
56
 
51
57
  const logger = createLogger({
52
58
  level: "debug",
@@ -872,9 +878,9 @@ async function main() {
872
878
  store: new RemoteStore(),
873
879
  };
874
880
 
875
- const specs = filterValidGraphSpecs(
881
+ const specs = Object.entries(
876
882
  z.record(z.string()).parse(JSON.parse(process.env.LANGSERVE_GRAPHS ?? "{}"))
877
- );
883
+ ).filter(([_, spec]) => filterValidExportPath(spec));
878
884
 
879
885
  if (!process.argv.includes("--skip-schema-cache")) {
880
886
  try {
@@ -944,6 +950,61 @@ async function main() {
944
950
  )
945
951
  );
946
952
 
953
+ // Load LANGGRAPH_AUTH
954
+ const auth = z
955
+ .object({
956
+ path: z.string().optional(),
957
+ disable_studio_auth: z.boolean().optional(),
958
+ })
959
+ .parse(JSON.parse(process.env.LANGGRAPH_AUTH ?? "{}"));
960
+
961
+ if (filterValidExportPath(auth.path)) {
962
+ await registerAuth(auth, { cwd: process.cwd() });
963
+
964
+ app.post("/auth/authenticate", async (c) => {
965
+ try {
966
+ const rawHeaders = c.req.raw.headers;
967
+ const authUrl = rawHeaders.get("x-langgraph-auth-url") as string;
968
+ const method = rawHeaders.get("x-langgraph-auth-method") as string;
969
+
970
+ const headers = new Headers(rawHeaders);
971
+ headers.delete("x-langgraph-auth-url");
972
+ headers.delete("x-langgraph-auth-method");
973
+
974
+ const context = await authenticate(
975
+ new Request(authUrl, { headers, method })
976
+ );
977
+
978
+ return c.json(context);
979
+ } catch (error) {
980
+ if (error instanceof HTTPException) {
981
+ return c.json(
982
+ {
983
+ ...serializeError(error),
984
+ status: error.res?.status ?? error.status,
985
+ headers: error.res?.headers,
986
+ },
987
+ error.status as StatusCode
988
+ );
989
+ }
990
+
991
+ return c.json(serializeError(error), 403);
992
+ }
993
+ });
994
+
995
+ app.post("/auth/authorize", async (c) => {
996
+ try {
997
+ return c.json(await authorize(await c.req.json()));
998
+ } catch (error) {
999
+ if (error instanceof HTTPException) {
1000
+ return c.json(serializeError(error), error.status);
1001
+ }
1002
+
1003
+ return c.json(serializeError(error), 500);
1004
+ }
1005
+ });
1006
+ }
1007
+
947
1008
  app.get("/ok", (c) => c.json({ ok: true }));
948
1009
 
949
1010
  app.onError((err, c) => {
@@ -3,6 +3,7 @@ declare namespace NodeJS {
3
3
  LANGSERVE_GRAPHS: string;
4
4
  LANGGRAPH_UI?: string;
5
5
  LANGGRAPH_UI_CONFIG?: string;
6
+ LANGGRAPH_AUTH?: string;
6
7
  PORT: string;
7
8
  }
8
9
  }
@@ -24,15 +24,16 @@
24
24
  "undici": "^6.21.1",
25
25
  "uuid": "^10.0.0",
26
26
  "winston": "^3.17.0",
27
- "@langchain/langgraph-api": "~0.0.20",
28
- "@langchain/langgraph-ui": "~0.0.20",
27
+ "@langchain/langgraph-api": "~0.0.21",
28
+ "@langchain/langgraph-ui": "~0.0.21",
29
29
  "zod": "^3.23.8"
30
30
  },
31
31
  "resolutions": {
32
32
  "esbuild": "^0.25.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@langchain/langgraph-sdk": "^0.0.60",
35
+ "jose": "^6.0.10",
36
+ "@langchain/langgraph-sdk": "^0.0.66",
36
37
  "@types/react": "^19.0.8",
37
38
  "@types/react-dom": "^19.0.3",
38
39
  "@types/node": "^22.2.0",
@@ -5,7 +5,7 @@ import shutil
5
5
  import ssl
6
6
  from collections.abc import AsyncIterator
7
7
  from contextlib import AbstractContextManager
8
- from typing import Any, Literal, Self
8
+ from typing import Any, Literal, Self, cast
9
9
 
10
10
  import certifi
11
11
  import httpx
@@ -24,12 +24,20 @@ from langgraph.checkpoint.serde.base import SerializerProtocol
24
24
  from langgraph.pregel.types import PregelTask, StateSnapshot
25
25
  from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
26
26
  from langgraph.types import Command, Interrupt, Send
27
+ from langgraph_sdk import Auth
27
28
  from pydantic import BaseModel
28
29
  from starlette.applications import Starlette
30
+ from starlette.authentication import (
31
+ AuthCredentials,
32
+ AuthenticationBackend,
33
+ BaseUser,
34
+ )
29
35
  from starlette.exceptions import HTTPException
30
- from starlette.requests import Request
36
+ from starlette.requests import HTTPConnection, Request
31
37
  from starlette.routing import Route
32
38
 
39
+ from langgraph_api.auth.custom import DotDict, ProxyUser
40
+ from langgraph_api.config import LANGGRAPH_AUTH_TYPE
33
41
  from langgraph_api.js.base import BaseRemotePregel
34
42
  from langgraph_api.js.errors import RemoteException
35
43
  from langgraph_api.js.sse import SSEDecoder, aiter_lines_raw
@@ -729,3 +737,86 @@ async def js_healthcheck():
729
737
  status_code=500,
730
738
  detail="JS healthcheck failed. Either the JS server is not running or the event loop is blocked by a CPU-intensive task.",
731
739
  ) from exc
740
+
741
+
742
+ class CustomJsAuthBackend(AuthenticationBackend):
743
+ ls_auth: AuthenticationBackend | None
744
+
745
+ def __init__(self, disable_studio_auth: bool = False):
746
+ self.ls_auth = None
747
+ if not disable_studio_auth and LANGGRAPH_AUTH_TYPE == "langsmith":
748
+ from langgraph_api.auth.langsmith.backend import LangsmithAuthBackend
749
+
750
+ self.ls_auth = LangsmithAuthBackend()
751
+
752
+ async def authenticate(
753
+ self, conn: HTTPConnection
754
+ ) -> tuple[AuthCredentials, BaseUser] | None:
755
+ if self.ls_auth is not None and (
756
+ (auth_scheme := conn.headers.get("x-auth-scheme"))
757
+ and auth_scheme == "langsmith"
758
+ ):
759
+ return await self.ls_auth.authenticate(conn)
760
+
761
+ headers = dict(conn.headers)
762
+ # need to remove content-length to prevent confusing the HTTP client
763
+ headers.pop("content-length", None)
764
+ headers["x-langgraph-auth-url"] = str(conn.url)
765
+ headers["x-langgraph-auth-method"] = conn.scope.get("method")
766
+
767
+ res = await _client.post("/auth/authenticate", headers=headers)
768
+ data = res.json()
769
+
770
+ if data.get("error"):
771
+ status = data.get("status") or 403
772
+ headers = data.get("headers")
773
+ message = data.get("message") or "Unauthorized"
774
+
775
+ raise HTTPException(status_code=status, detail=message, headers=headers)
776
+
777
+ return AuthCredentials(data["scopes"]), ProxyUser(DotDict(data["user"]))
778
+
779
+
780
+ async def handle_js_auth_event(
781
+ ctx: Auth.types.AuthContext | None,
782
+ value: dict,
783
+ ) -> Auth.types.FilterType | None:
784
+ res = await _client.post(
785
+ "/auth/authorize",
786
+ headers={"Content-Type": "application/json"},
787
+ data=json_dumpb(
788
+ {
789
+ "resource": ctx.resource,
790
+ "action": ctx.action,
791
+ "value": value,
792
+ "context": {
793
+ "user": cast(DotDict, ctx.user).dict(),
794
+ "scopes": ctx.permissions,
795
+ }
796
+ if ctx
797
+ else None,
798
+ }
799
+ ),
800
+ )
801
+
802
+ response = res.json()
803
+
804
+ if response.get("error"):
805
+ status = response.get("status") or 403
806
+ headers = response.get("headers")
807
+ message = response.get("message") or "Unauthorized"
808
+
809
+ raise HTTPException(status_code=status, detail=message, headers=headers)
810
+
811
+ filters = cast(Auth.types.FilterType | None, response.get("filters"))
812
+
813
+ # mutate metadata in value if applicable
814
+ # we need to preserve the identity of the object, so cannot create a new
815
+ # dictionary, otherwise the changes will not persist
816
+ if isinstance(value, dict) and (updated_value := response.get("value")):
817
+ if isinstance(value.get("metadata"), dict) and (
818
+ metadata := updated_value.get("metadata")
819
+ ):
820
+ value["metadata"].update(metadata)
821
+
822
+ return filters
@@ -20,12 +20,6 @@ export interface GraphSpec {
20
20
  exportSymbol: string;
21
21
  }
22
22
 
23
- export function filterValidGraphSpecs(specs: Record<string, string>) {
24
- return Object.entries(specs).filter(
25
- ([_, spec]) => !spec.split(":")[0].endsWith(".py")
26
- );
27
- }
28
-
29
23
  export type CompiledGraphFactory<T extends string> = (config: {
30
24
  configurable?: Record<string, unknown>;
31
25
  }) => Promise<CompiledGraph<T>>;
@@ -0,0 +1,4 @@
1
+ export function filterValidExportPath(path: string | undefined) {
2
+ if (!path) return false;
3
+ return !path.split(":")[0].endsWith(".py");
4
+ }