@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.
- package/dist/api/store.mjs +121 -24
- package/dist/server.mjs +7 -7
- package/package.json +5 -5
package/dist/api/store.mjs
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
108
|
-
const
|
|
109
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
108
|
-
"@langchain/langgraph-
|
|
109
|
-
"@langchain/langgraph-
|
|
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/",
|