@langchain/langgraph-api 1.4.2 → 1.4.3

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.
@@ -3,8 +3,21 @@ import { zValidator } from "@hono/zod-validator";
3
3
  import * as schemas from "../schemas.mjs";
4
4
  import { HTTPException } from "hono/http-exception";
5
5
  import { store as storageStore } from "../storage/store.mjs";
6
- import { handleAuthEvent } from "../auth/index.mjs";
6
+ import { handleAuthEvent, isAuthMatching, } from "../auth/index.mjs";
7
7
  const api = new Hono();
8
+ const AUTH_FILTER_SCAN_PAGE_LIMIT = 1000;
9
+ const AUTH_FILTER_MAX_SCAN_LIMIT = 10_000;
10
+ const throwStoreAuthScanLimitError = () => {
11
+ throw new HTTPException(400, {
12
+ message: "Authenticated store query exceeded maximum scan limit. Narrow the namespace or filters.",
13
+ });
14
+ };
15
+ const getStoreAuthMetadata = (namespace, key, value) => ({
16
+ ...(value ?? {}),
17
+ namespace: namespace ?? [],
18
+ ...(key == null ? {} : { key }),
19
+ });
20
+ const isStoreAuthMatching = (namespace, key, value, filters) => isAuthMatching(getStoreAuthMetadata(namespace, key, value), filters);
8
21
  const validateNamespace = (namespace) => {
9
22
  if (!namespace || namespace.length === 0) {
10
23
  throw new HTTPException(400, { message: "Namespace is required" });
@@ -35,41 +48,99 @@ api.post("/store/namespaces", zValidator("json", schemas.StoreListNamespaces), a
35
48
  validateNamespace(payload.prefix);
36
49
  if (payload.suffix)
37
50
  validateNamespace(payload.suffix);
38
- await handleAuthEvent(c.var.auth, "store:list_namespaces", {
51
+ const [filters] = await handleAuthEvent(c.var.auth, "store:list_namespaces", {
39
52
  namespace: payload.prefix,
40
53
  suffix: payload.suffix,
41
54
  max_depth: payload.max_depth,
42
55
  limit: payload.limit,
43
56
  offset: payload.offset,
44
57
  });
45
- return c.json({
46
- namespaces: await storageStore.listNamespaces({
47
- limit: payload.limit ?? 100,
48
- offset: payload.offset ?? 0,
58
+ const limit = payload.limit ?? 100;
59
+ const offset = payload.offset ?? 0;
60
+ let namespaces;
61
+ if (filters == null) {
62
+ namespaces = await storageStore.listNamespaces({
63
+ limit,
64
+ offset,
49
65
  prefix: payload.prefix,
50
66
  suffix: payload.suffix,
51
67
  maxDepth: payload.max_depth,
52
- }),
53
- });
68
+ });
69
+ }
70
+ else {
71
+ const authorizedNamespaces = [];
72
+ const requiredCount = offset + limit;
73
+ let scannedCount = 0;
74
+ while (authorizedNamespaces.length < requiredCount &&
75
+ scannedCount < AUTH_FILTER_MAX_SCAN_LIMIT) {
76
+ const scanLimit = Math.min(AUTH_FILTER_SCAN_PAGE_LIMIT, AUTH_FILTER_MAX_SCAN_LIMIT - scannedCount);
77
+ const candidates = await storageStore.listNamespaces({
78
+ limit: scanLimit,
79
+ offset: scannedCount,
80
+ prefix: payload.prefix,
81
+ suffix: payload.suffix,
82
+ maxDepth: payload.max_depth,
83
+ });
84
+ scannedCount += candidates.length;
85
+ authorizedNamespaces.push(...candidates.filter((namespace) => isStoreAuthMatching(namespace, undefined, undefined, filters)));
86
+ if (candidates.length < scanLimit)
87
+ break;
88
+ }
89
+ if (authorizedNamespaces.length < requiredCount &&
90
+ scannedCount >= AUTH_FILTER_MAX_SCAN_LIMIT) {
91
+ throwStoreAuthScanLimitError();
92
+ }
93
+ namespaces = authorizedNamespaces.slice(offset, requiredCount);
94
+ }
95
+ return c.json({ namespaces });
54
96
  });
55
97
  api.post("/store/items/search", zValidator("json", schemas.StoreSearchItems), async (c) => {
56
98
  // Search Items
57
99
  const payload = c.req.valid("json");
58
- if (payload.namespace_prefix)
100
+ if (payload.namespace_prefix.length)
59
101
  validateNamespace(payload.namespace_prefix);
60
- await handleAuthEvent(c.var.auth, "store:search", {
102
+ const [filters] = await handleAuthEvent(c.var.auth, "store:search", {
61
103
  namespace: payload.namespace_prefix,
62
104
  filter: payload.filter,
63
105
  limit: payload.limit,
64
106
  offset: payload.offset,
65
107
  query: payload.query,
66
108
  });
67
- const items = await storageStore.search(payload.namespace_prefix, {
68
- filter: payload.filter,
69
- limit: payload.limit ?? 10,
70
- offset: payload.offset ?? 0,
71
- query: payload.query,
72
- });
109
+ const limit = payload.limit ?? 10;
110
+ const offset = payload.offset ?? 0;
111
+ let items;
112
+ if (filters == null) {
113
+ items = await storageStore.search(payload.namespace_prefix, {
114
+ filter: payload.filter,
115
+ limit,
116
+ offset,
117
+ query: payload.query,
118
+ });
119
+ }
120
+ else {
121
+ const authorizedItems = [];
122
+ const requiredCount = offset + limit;
123
+ let scannedCount = 0;
124
+ while (authorizedItems.length < requiredCount &&
125
+ scannedCount < AUTH_FILTER_MAX_SCAN_LIMIT) {
126
+ const scanLimit = Math.min(AUTH_FILTER_SCAN_PAGE_LIMIT, AUTH_FILTER_MAX_SCAN_LIMIT - scannedCount);
127
+ const candidates = await storageStore.search(payload.namespace_prefix, {
128
+ filter: payload.filter,
129
+ limit: scanLimit,
130
+ offset: scannedCount,
131
+ query: payload.query,
132
+ });
133
+ scannedCount += candidates.length;
134
+ authorizedItems.push(...candidates.filter((item) => isStoreAuthMatching(item.namespace, item.key, item.value, filters)));
135
+ if (candidates.length < scanLimit)
136
+ break;
137
+ }
138
+ if (authorizedItems.length < requiredCount &&
139
+ scannedCount >= AUTH_FILTER_MAX_SCAN_LIMIT) {
140
+ throwStoreAuthScanLimitError();
141
+ }
142
+ items = authorizedItems.slice(offset, requiredCount);
143
+ }
73
144
  return c.json({ items: items.map(mapItemsToApi) });
74
145
  });
75
146
  api.put("/store/items", zValidator("json", schemas.StorePutItem), async (c) => {
@@ -77,12 +148,22 @@ api.put("/store/items", zValidator("json", schemas.StorePutItem), async (c) => {
77
148
  const payload = c.req.valid("json");
78
149
  if (payload.namespace)
79
150
  validateNamespace(payload.namespace);
80
- await handleAuthEvent(c.var.auth, "store:put", {
151
+ const [filters, mutable] = await handleAuthEvent(c.var.auth, "store:put", {
81
152
  namespace: payload.namespace,
82
153
  key: payload.key,
83
154
  value: payload.value,
84
155
  });
85
- await storageStore.put(payload.namespace, payload.key, payload.value);
156
+ if (mutable.namespace)
157
+ validateNamespace(mutable.namespace);
158
+ const existingItem = await storageStore.get(mutable.namespace, mutable.key);
159
+ if (existingItem != null &&
160
+ !isStoreAuthMatching(existingItem.namespace, existingItem.key, existingItem.value, filters)) {
161
+ throw new HTTPException(404, { message: "Item not found" });
162
+ }
163
+ if (!isStoreAuthMatching(mutable.namespace, mutable.key, mutable.value, filters)) {
164
+ throw new HTTPException(403, { message: "Not authorized" });
165
+ }
166
+ await storageStore.put(mutable.namespace, mutable.key, mutable.value);
86
167
  return c.body(null, 204);
87
168
  });
88
169
  api.delete("/store/items", zValidator("json", schemas.StoreDeleteItem), async (c) => {
@@ -90,22 +171,38 @@ api.delete("/store/items", zValidator("json", schemas.StoreDeleteItem), async (c
90
171
  const payload = c.req.valid("json");
91
172
  if (payload.namespace)
92
173
  validateNamespace(payload.namespace);
93
- await handleAuthEvent(c.var.auth, "store:delete", {
174
+ const [filters, mutable] = await handleAuthEvent(c.var.auth, "store:delete", {
94
175
  namespace: payload.namespace,
95
176
  key: payload.key,
96
177
  });
97
- await storageStore.delete(payload.namespace ?? [], payload.key);
178
+ const namespace = mutable.namespace ?? [];
179
+ if (namespace.length)
180
+ validateNamespace(namespace);
181
+ const existingItem = await storageStore.get(namespace, mutable.key);
182
+ if (existingItem != null) {
183
+ if (!isStoreAuthMatching(existingItem.namespace, existingItem.key, existingItem.value, filters)) {
184
+ throw new HTTPException(404, { message: "Item not found" });
185
+ }
186
+ }
187
+ else if (!isStoreAuthMatching(namespace, mutable.key, undefined, filters)) {
188
+ throw new HTTPException(403, { message: "Not authorized" });
189
+ }
190
+ await storageStore.delete(namespace, mutable.key);
98
191
  return c.body(null, 204);
99
192
  });
100
193
  api.get("/store/items", zValidator("query", schemas.StoreGetItem), async (c) => {
101
194
  // Get Item
102
195
  const payload = c.req.valid("query");
103
- await handleAuthEvent(c.var.auth, "store:get", {
196
+ const [filters, mutable] = await handleAuthEvent(c.var.auth, "store:get", {
104
197
  namespace: payload.namespace,
105
198
  key: payload.key,
106
199
  });
107
- const key = payload.key;
108
- const namespace = payload.namespace;
109
- return c.json(mapItemsToApi(await storageStore.get(namespace, key)));
200
+ const namespace = mutable.namespace ?? [];
201
+ const item = await storageStore.get(namespace, mutable.key);
202
+ if (item != null &&
203
+ !isStoreAuthMatching(item.namespace, item.key, item.value, filters)) {
204
+ throw new HTTPException(404, { message: "Item not found" });
205
+ }
206
+ return c.json(mapItemsToApi(item));
110
207
  });
111
208
  export default api;
package/dist/server.mjs CHANGED
@@ -132,6 +132,13 @@ export async function startServer(options, storage) {
132
132
  });
133
133
  // Loopback fetch used by webhooks and custom routes
134
134
  bindLoopbackFetch(app);
135
+ app.use(cors(options.http?.cors));
136
+ app.use(requestLogger());
137
+ if (options.auth?.path) {
138
+ logger.info(`Loading auth from ${options.auth.path}`);
139
+ await registerAuth(options.auth, { cwd: options.cwd });
140
+ app.use(auth());
141
+ }
135
142
  app.post("/internal/truncate", zValidator("json", z.object({
136
143
  runs: z.boolean().optional(),
137
144
  threads: z.boolean().optional(),
@@ -143,13 +150,6 @@ export async function startServer(options, storage) {
143
150
  ops.truncate({ runs, threads, assistants, checkpointer, store });
144
151
  return c.json({ ok: true });
145
152
  });
146
- app.use(cors(options.http?.cors));
147
- app.use(requestLogger());
148
- if (options.auth?.path) {
149
- logger.info(`Loading auth from ${options.auth.path}`);
150
- await registerAuth(options.auth, { cwd: options.cwd });
151
- app.use(auth());
152
- }
153
153
  if (options.http?.app) {
154
154
  logger.info(`Loading HTTP app from ${options.http.app}`);
155
155
  const { api } = await registerHttp(options.http.app, { cwd: options.cwd });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/langgraph-api",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": "^18.19.0 || >=20.16.0"
@@ -71,7 +71,7 @@
71
71
  "winston": "^3.17.0",
72
72
  "winston-console-format": "^1.0.8",
73
73
  "zod": "^3.25.76 || ^4",
74
- "@langchain/langgraph-ui": "1.4.2"
74
+ "@langchain/langgraph-ui": "1.4.3"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "@langchain/core": "^1.1.48",
@@ -104,9 +104,9 @@
104
104
  "typescript": "^4.9.5 || ^5.4.5",
105
105
  "vitest": "^4.1.9",
106
106
  "wait-port": "^1.1.0",
107
- "@langchain/langgraph": "1.4.7",
108
- "@langchain/langgraph-sdk": "1.9.25",
109
- "@langchain/langgraph-checkpoint": "1.1.3"
107
+ "@langchain/langgraph": "1.4.8",
108
+ "@langchain/langgraph-checkpoint": "1.1.3",
109
+ "@langchain/langgraph-sdk": "1.9.26"
110
110
  },
111
111
  "scripts": {
112
112
  "clean": "rm -rf dist/ .turbo/ ./tests/graphs/.langgraph_api/ ./tests/protocol-v2/graphs/.langgraph_api/",