@langchain/langgraph-api 1.1.8 → 1.1.10
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 +3 -3
- package/dist/api/assistants.d.mts +3 -0
- package/dist/api/assistants.mjs +193 -0
- package/dist/api/meta.d.mts +3 -0
- package/dist/api/meta.mjs +65 -0
- package/dist/api/runs.d.mts +3 -0
- package/dist/api/runs.mjs +324 -0
- package/dist/api/store.d.mts +3 -0
- package/dist/api/store.mjs +111 -0
- package/dist/api/threads.d.mts +3 -0
- package/dist/api/threads.mjs +143 -0
- package/dist/auth/custom.d.mts +9 -0
- package/dist/auth/custom.mjs +32 -0
- package/dist/auth/index.d.mts +43 -0
- package/dist/auth/index.mjs +163 -0
- package/dist/cli/entrypoint.d.mts +1 -0
- package/dist/cli/entrypoint.mjs +41 -0
- package/dist/cli/spawn.d.mts +42 -0
- package/dist/cli/spawn.mjs +47 -0
- package/dist/cli/utils/ipc/client.d.mts +5 -0
- package/dist/cli/utils/ipc/client.mjs +47 -0
- package/dist/cli/utils/ipc/utils/get-pipe-path.d.mts +1 -0
- package/dist/cli/utils/ipc/utils/get-pipe-path.mjs +29 -0
- package/dist/cli/utils/ipc/utils/temporary-directory.d.mts +5 -0
- package/dist/cli/utils/ipc/utils/temporary-directory.mjs +40 -0
- package/dist/command.d.mts +11 -0
- package/dist/command.mjs +15 -0
- package/dist/experimental/embed.d.mts +42 -0
- package/dist/experimental/embed.mjs +299 -0
- package/dist/graph/api.d.mts +1 -0
- package/dist/graph/api.mjs +2 -0
- package/dist/graph/load.d.mts +19 -0
- package/dist/graph/load.hooks.d.mts +2 -0
- package/dist/graph/load.hooks.mjs +52 -0
- package/dist/graph/load.mjs +96 -0
- package/dist/graph/load.utils.d.mts +22 -0
- package/dist/graph/load.utils.mjs +49 -0
- package/dist/graph/parser/index.d.mts +23 -0
- package/dist/graph/parser/index.mjs +58 -0
- package/dist/graph/parser/parser.d.mts +77 -0
- package/dist/graph/parser/parser.mjs +429 -0
- package/dist/graph/parser/parser.worker.d.mts +1 -0
- package/dist/graph/parser/parser.worker.mjs +7 -0
- package/dist/graph/parser/schema/types.d.mts +154 -0
- package/dist/graph/parser/schema/types.mjs +1496 -0
- package/dist/graph/parser/schema/types.template.d.mts +1 -0
- package/dist/graph/parser/schema/types.template.mts +92 -0
- package/dist/http/custom.d.mts +6 -0
- package/dist/http/custom.mjs +10 -0
- package/dist/http/middleware.d.mts +11 -0
- package/dist/http/middleware.mjs +57 -0
- package/dist/logging.d.mts +10 -0
- package/dist/logging.mjs +115 -0
- package/dist/loopback.d.mts +4 -0
- package/dist/loopback.mjs +10 -0
- package/dist/preload.d.mts +1 -0
- package/dist/preload.mjs +29 -0
- package/dist/queue.d.mts +2 -0
- package/dist/queue.mjs +119 -0
- package/dist/schemas.d.mts +1552 -0
- package/dist/schemas.mjs +492 -0
- package/dist/semver/index.d.mts +15 -0
- package/dist/semver/index.mjs +46 -0
- package/dist/server.d.mts +175 -0
- package/dist/server.mjs +181 -0
- package/dist/state.d.mts +3 -0
- package/dist/state.mjs +30 -0
- package/dist/storage/checkpoint.d.mts +19 -0
- package/dist/storage/checkpoint.mjs +127 -0
- package/dist/storage/context.d.mts +3 -0
- package/dist/storage/context.mjs +11 -0
- package/dist/storage/importMap.d.mts +55 -0
- package/dist/storage/importMap.mjs +55 -0
- package/dist/storage/ops.d.mts +169 -0
- package/dist/storage/ops.mjs +1262 -0
- package/dist/storage/persist.d.mts +18 -0
- package/dist/storage/persist.mjs +81 -0
- package/dist/storage/store.d.mts +17 -0
- package/dist/storage/store.mjs +41 -0
- package/dist/storage/types.d.mts +301 -0
- package/dist/storage/types.mjs +1 -0
- package/dist/stream.d.mts +43 -0
- package/dist/stream.mjs +235 -0
- package/dist/ui/load.d.mts +8 -0
- package/dist/ui/load.mjs +53 -0
- package/dist/utils/abort.d.mts +1 -0
- package/dist/utils/abort.mjs +8 -0
- package/dist/utils/hono.d.mts +5 -0
- package/dist/utils/hono.mjs +24 -0
- package/dist/utils/importMap.d.mts +55 -0
- package/dist/utils/importMap.mjs +55 -0
- package/dist/utils/runnableConfig.d.mts +3 -0
- package/dist/utils/runnableConfig.mjs +45 -0
- package/dist/utils/serde.d.mts +5 -0
- package/dist/utils/serde.mjs +20 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +11 -0
- package/dist/webhook.d.mts +11 -0
- package/dist/webhook.mjs +30 -0
- package/package.json +19 -19
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { zValidator } from "@hono/zod-validator";
|
|
3
|
+
import * as schemas from "../schemas.mjs";
|
|
4
|
+
import { HTTPException } from "hono/http-exception";
|
|
5
|
+
import { store as storageStore } from "../storage/store.mjs";
|
|
6
|
+
import { handleAuthEvent } from "../auth/index.mjs";
|
|
7
|
+
const api = new Hono();
|
|
8
|
+
const validateNamespace = (namespace) => {
|
|
9
|
+
if (!namespace || namespace.length === 0) {
|
|
10
|
+
throw new HTTPException(400, { message: "Namespace is required" });
|
|
11
|
+
}
|
|
12
|
+
for (const label of namespace) {
|
|
13
|
+
if (!label || label.includes(".")) {
|
|
14
|
+
throw new HTTPException(422, {
|
|
15
|
+
message: "Namespace labels cannot be empty or contain periods. Received: " +
|
|
16
|
+
namespace.join("."),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const mapItemsToApi = (item) => {
|
|
22
|
+
if (item == null)
|
|
23
|
+
return null;
|
|
24
|
+
const clonedItem = { ...item };
|
|
25
|
+
delete clonedItem.createdAt;
|
|
26
|
+
delete clonedItem.updatedAt;
|
|
27
|
+
clonedItem.created_at = item.createdAt;
|
|
28
|
+
clonedItem.updated_at = item.updatedAt;
|
|
29
|
+
return clonedItem;
|
|
30
|
+
};
|
|
31
|
+
api.post("/store/namespaces", zValidator("json", schemas.StoreListNamespaces), async (c) => {
|
|
32
|
+
// List Namespaces
|
|
33
|
+
const payload = c.req.valid("json");
|
|
34
|
+
if (payload.prefix)
|
|
35
|
+
validateNamespace(payload.prefix);
|
|
36
|
+
if (payload.suffix)
|
|
37
|
+
validateNamespace(payload.suffix);
|
|
38
|
+
await handleAuthEvent(c.var.auth, "store:list_namespaces", {
|
|
39
|
+
namespace: payload.prefix,
|
|
40
|
+
suffix: payload.suffix,
|
|
41
|
+
max_depth: payload.max_depth,
|
|
42
|
+
limit: payload.limit,
|
|
43
|
+
offset: payload.offset,
|
|
44
|
+
});
|
|
45
|
+
return c.json({
|
|
46
|
+
namespaces: await storageStore.listNamespaces({
|
|
47
|
+
limit: payload.limit ?? 100,
|
|
48
|
+
offset: payload.offset ?? 0,
|
|
49
|
+
prefix: payload.prefix,
|
|
50
|
+
suffix: payload.suffix,
|
|
51
|
+
maxDepth: payload.max_depth,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
api.post("/store/items/search", zValidator("json", schemas.StoreSearchItems), async (c) => {
|
|
56
|
+
// Search Items
|
|
57
|
+
const payload = c.req.valid("json");
|
|
58
|
+
if (payload.namespace_prefix)
|
|
59
|
+
validateNamespace(payload.namespace_prefix);
|
|
60
|
+
await handleAuthEvent(c.var.auth, "store:search", {
|
|
61
|
+
namespace: payload.namespace_prefix,
|
|
62
|
+
filter: payload.filter,
|
|
63
|
+
limit: payload.limit,
|
|
64
|
+
offset: payload.offset,
|
|
65
|
+
query: payload.query,
|
|
66
|
+
});
|
|
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
|
+
});
|
|
73
|
+
return c.json({ items: items.map(mapItemsToApi) });
|
|
74
|
+
});
|
|
75
|
+
api.put("/store/items", zValidator("json", schemas.StorePutItem), async (c) => {
|
|
76
|
+
// Put Item
|
|
77
|
+
const payload = c.req.valid("json");
|
|
78
|
+
if (payload.namespace)
|
|
79
|
+
validateNamespace(payload.namespace);
|
|
80
|
+
await handleAuthEvent(c.var.auth, "store:put", {
|
|
81
|
+
namespace: payload.namespace,
|
|
82
|
+
key: payload.key,
|
|
83
|
+
value: payload.value,
|
|
84
|
+
});
|
|
85
|
+
await storageStore.put(payload.namespace, payload.key, payload.value);
|
|
86
|
+
return c.body(null, 204);
|
|
87
|
+
});
|
|
88
|
+
api.delete("/store/items", zValidator("json", schemas.StoreDeleteItem), async (c) => {
|
|
89
|
+
// Delete Item
|
|
90
|
+
const payload = c.req.valid("json");
|
|
91
|
+
if (payload.namespace)
|
|
92
|
+
validateNamespace(payload.namespace);
|
|
93
|
+
await handleAuthEvent(c.var.auth, "store:delete", {
|
|
94
|
+
namespace: payload.namespace,
|
|
95
|
+
key: payload.key,
|
|
96
|
+
});
|
|
97
|
+
await storageStore.delete(payload.namespace ?? [], payload.key);
|
|
98
|
+
return c.body(null, 204);
|
|
99
|
+
});
|
|
100
|
+
api.get("/store/items", zValidator("query", schemas.StoreGetItem), async (c) => {
|
|
101
|
+
// Get Item
|
|
102
|
+
const payload = c.req.valid("query");
|
|
103
|
+
await handleAuthEvent(c.var.auth, "store:get", {
|
|
104
|
+
namespace: payload.namespace,
|
|
105
|
+
key: payload.key,
|
|
106
|
+
});
|
|
107
|
+
const key = payload.key;
|
|
108
|
+
const namespace = payload.namespace;
|
|
109
|
+
return c.json(mapItemsToApi(await storageStore.get(namespace, key)));
|
|
110
|
+
});
|
|
111
|
+
export default api;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { v4 as uuid4 } from "uuid";
|
|
4
|
+
import { z } from "zod/v3";
|
|
5
|
+
import * as schemas from "../schemas.mjs";
|
|
6
|
+
import { stateSnapshotToThreadState } from "../state.mjs";
|
|
7
|
+
import { threads } from "../storage/context.mjs";
|
|
8
|
+
import { jsonExtra } from "../utils/hono.mjs";
|
|
9
|
+
const api = new Hono();
|
|
10
|
+
// Threads Routes
|
|
11
|
+
api.post("/threads", zValidator("json", schemas.ThreadCreate), async (c) => {
|
|
12
|
+
// Create Thread
|
|
13
|
+
const payload = c.req.valid("json");
|
|
14
|
+
const thread = await threads().put(payload.thread_id || uuid4(), { metadata: payload.metadata, if_exists: payload.if_exists ?? "raise" }, c.var.auth);
|
|
15
|
+
if (payload.supersteps?.length) {
|
|
16
|
+
await threads().state.bulk({ configurable: { thread_id: thread.thread_id } }, payload.supersteps, c.var.auth);
|
|
17
|
+
}
|
|
18
|
+
return jsonExtra(c, thread);
|
|
19
|
+
});
|
|
20
|
+
api.post("/threads/search", zValidator("json", schemas.ThreadSearchRequest), async (c) => {
|
|
21
|
+
// Search Threads
|
|
22
|
+
const payload = c.req.valid("json");
|
|
23
|
+
const result = [];
|
|
24
|
+
let total = 0;
|
|
25
|
+
for await (const item of threads().search({
|
|
26
|
+
status: payload.status,
|
|
27
|
+
values: payload.values,
|
|
28
|
+
metadata: payload.metadata,
|
|
29
|
+
ids: payload.ids,
|
|
30
|
+
limit: payload.limit ?? 10,
|
|
31
|
+
offset: payload.offset ?? 0,
|
|
32
|
+
sort_by: payload.sort_by ?? "created_at",
|
|
33
|
+
sort_order: payload.sort_order ?? "desc",
|
|
34
|
+
select: payload.select,
|
|
35
|
+
}, c.var.auth)) {
|
|
36
|
+
result.push(Object.fromEntries(Object.entries(item.thread).filter(([k]) => !payload.select || payload.select.includes(k))));
|
|
37
|
+
// Only set total if it's the first item
|
|
38
|
+
if (total === 0) {
|
|
39
|
+
total = item.total;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const nextOffset = (payload.offset ?? 0) + total;
|
|
43
|
+
if (total === payload.limit) {
|
|
44
|
+
c.res.headers.set("X-Pagination-Next", nextOffset.toString());
|
|
45
|
+
c.res.headers.set("X-Pagination-Total", (nextOffset + 1).toString());
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
c.res.headers.set("X-Pagination-Total", nextOffset.toString());
|
|
49
|
+
}
|
|
50
|
+
return jsonExtra(c, result);
|
|
51
|
+
});
|
|
52
|
+
api.post("/threads/count", zValidator("json", schemas.ThreadCountRequest), async (c) => {
|
|
53
|
+
const payload = c.req.valid("json");
|
|
54
|
+
const total = await threads().count(payload, c.var.auth);
|
|
55
|
+
return c.json(total);
|
|
56
|
+
});
|
|
57
|
+
api.get("/threads/:thread_id/state", zValidator("param", z.object({ thread_id: z.string().uuid() })), zValidator("query", z.object({ subgraphs: schemas.coercedBoolean.optional() })), async (c) => {
|
|
58
|
+
// Get Latest Thread State
|
|
59
|
+
const { thread_id } = c.req.valid("param");
|
|
60
|
+
const { subgraphs } = c.req.valid("query");
|
|
61
|
+
const state = stateSnapshotToThreadState(await threads().state.get({ configurable: { thread_id } }, { subgraphs }, c.var.auth));
|
|
62
|
+
return jsonExtra(c, state);
|
|
63
|
+
});
|
|
64
|
+
api.post("/threads/:thread_id/state", zValidator("param", z.object({ thread_id: z.string().uuid() })), zValidator("json", schemas.ThreadStateUpdate), async (c) => {
|
|
65
|
+
// Update Thread State
|
|
66
|
+
const { thread_id } = c.req.valid("param");
|
|
67
|
+
const payload = c.req.valid("json");
|
|
68
|
+
const config = { configurable: { thread_id } };
|
|
69
|
+
if (payload.checkpoint_id) {
|
|
70
|
+
config.configurable ??= {};
|
|
71
|
+
config.configurable.checkpoint_id = payload.checkpoint_id;
|
|
72
|
+
}
|
|
73
|
+
if (payload.checkpoint) {
|
|
74
|
+
config.configurable ??= {};
|
|
75
|
+
Object.assign(config.configurable, payload.checkpoint);
|
|
76
|
+
}
|
|
77
|
+
const inserted = await threads().state.post(config, payload.values, payload.as_node, c.var.auth);
|
|
78
|
+
return jsonExtra(c, inserted);
|
|
79
|
+
});
|
|
80
|
+
api.get("/threads/:thread_id/state/:checkpoint_id", zValidator("param", z.object({
|
|
81
|
+
thread_id: z.string().uuid(),
|
|
82
|
+
checkpoint_id: z.string().uuid(),
|
|
83
|
+
})), zValidator("query", z.object({ subgraphs: schemas.coercedBoolean.optional() })), async (c) => {
|
|
84
|
+
// Get Thread State At Checkpoint
|
|
85
|
+
const { thread_id, checkpoint_id } = c.req.valid("param");
|
|
86
|
+
const { subgraphs } = c.req.valid("query");
|
|
87
|
+
const state = stateSnapshotToThreadState(await threads().state.get({ configurable: { thread_id, checkpoint_id } }, { subgraphs }, c.var.auth));
|
|
88
|
+
return jsonExtra(c, state);
|
|
89
|
+
});
|
|
90
|
+
api.post("/threads/:thread_id/state/checkpoint", zValidator("param", z.object({ thread_id: z.string().uuid() })), zValidator("json", z.object({
|
|
91
|
+
subgraphs: schemas.coercedBoolean.optional(),
|
|
92
|
+
checkpoint: schemas.CheckpointSchema.nullish(),
|
|
93
|
+
})), async (c) => {
|
|
94
|
+
// Get Thread State At Checkpoint Post
|
|
95
|
+
const { thread_id } = c.req.valid("param");
|
|
96
|
+
const { checkpoint, subgraphs } = c.req.valid("json");
|
|
97
|
+
const state = stateSnapshotToThreadState(await threads().state.get({ configurable: { thread_id, ...checkpoint } }, { subgraphs }, c.var.auth));
|
|
98
|
+
return jsonExtra(c, state);
|
|
99
|
+
});
|
|
100
|
+
api.get("/threads/:thread_id/history", zValidator("param", z.object({ thread_id: z.string().uuid() })), zValidator("query", z.object({
|
|
101
|
+
limit: z
|
|
102
|
+
.string()
|
|
103
|
+
.optional()
|
|
104
|
+
.default("10")
|
|
105
|
+
.transform((value) => parseInt(value, 10)),
|
|
106
|
+
before: z.string().optional(),
|
|
107
|
+
})), async (c) => {
|
|
108
|
+
// Get Thread History
|
|
109
|
+
const { thread_id } = c.req.valid("param");
|
|
110
|
+
const { limit, before } = c.req.valid("query");
|
|
111
|
+
const states = await threads().state.list({ configurable: { thread_id, checkpoint_ns: "" } }, { limit, before }, c.var.auth);
|
|
112
|
+
return jsonExtra(c, states.map(stateSnapshotToThreadState));
|
|
113
|
+
});
|
|
114
|
+
api.post("/threads/:thread_id/history", zValidator("param", z.object({ thread_id: z.string().uuid() })), zValidator("json", schemas.ThreadHistoryRequest), async (c) => {
|
|
115
|
+
// Get Thread History Post
|
|
116
|
+
const { thread_id } = c.req.valid("param");
|
|
117
|
+
const { limit, before, metadata, checkpoint } = c.req.valid("json");
|
|
118
|
+
const states = await threads().state.list({ configurable: { thread_id, checkpoint_ns: "", ...checkpoint } }, { limit, before, metadata }, c.var.auth);
|
|
119
|
+
return jsonExtra(c, states.map(stateSnapshotToThreadState));
|
|
120
|
+
});
|
|
121
|
+
api.get("/threads/:thread_id", zValidator("param", z.object({ thread_id: z.string().uuid() })), async (c) => {
|
|
122
|
+
// Get Thread
|
|
123
|
+
const { thread_id } = c.req.valid("param");
|
|
124
|
+
return jsonExtra(c, await threads().get(thread_id, c.var.auth));
|
|
125
|
+
});
|
|
126
|
+
api.delete("/threads/:thread_id", zValidator("param", z.object({ thread_id: z.string().uuid() })), async (c) => {
|
|
127
|
+
// Delete Thread
|
|
128
|
+
const { thread_id } = c.req.valid("param");
|
|
129
|
+
await threads().delete(thread_id, c.var.auth);
|
|
130
|
+
return new Response(null, { status: 204 });
|
|
131
|
+
});
|
|
132
|
+
api.patch("/threads/:thread_id", zValidator("param", z.object({ thread_id: z.string().uuid() })), zValidator("json", schemas.ThreadPatchRequest), async (c) => {
|
|
133
|
+
// Patch Thread
|
|
134
|
+
const { thread_id } = c.req.valid("param");
|
|
135
|
+
const { metadata } = c.req.valid("json");
|
|
136
|
+
return jsonExtra(c, await threads().patch(thread_id, { metadata }, c.var.auth));
|
|
137
|
+
});
|
|
138
|
+
api.post("/threads/:thread_id/copy", zValidator("param", z.object({ thread_id: z.string().uuid() })), async (c) => {
|
|
139
|
+
// Copy Thread
|
|
140
|
+
const { thread_id } = c.req.valid("param");
|
|
141
|
+
return jsonExtra(c, await threads().copy(thread_id, c.var.auth));
|
|
142
|
+
});
|
|
143
|
+
export default api;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from "hono";
|
|
2
|
+
import { type AuthContext } from "./index.mjs";
|
|
3
|
+
declare module "hono" {
|
|
4
|
+
interface ContextVariableMap {
|
|
5
|
+
auth?: AuthContext | undefined;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export declare const auth: () => MiddlewareHandler;
|
|
9
|
+
export { registerAuth } from "./index.mjs";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { authenticate, isAuthRegistered, isStudioAuthDisabled, } from "./index.mjs";
|
|
2
|
+
const STUDIO_USER = {
|
|
3
|
+
kind: "StudioUser",
|
|
4
|
+
display_name: "langgraph-studio-user",
|
|
5
|
+
identity: "langgraph-studio-user",
|
|
6
|
+
permissions: [],
|
|
7
|
+
is_authenticated: true,
|
|
8
|
+
};
|
|
9
|
+
export const auth = () => {
|
|
10
|
+
return async (c, next) => {
|
|
11
|
+
if (!isAuthRegistered())
|
|
12
|
+
return next();
|
|
13
|
+
// skip for /info
|
|
14
|
+
if (c.req.path === "/info")
|
|
15
|
+
return next();
|
|
16
|
+
// skip for UI asset requests
|
|
17
|
+
if (c.req.path.startsWith("/ui") && c.req.method === "GET")
|
|
18
|
+
return next();
|
|
19
|
+
if (!isStudioAuthDisabled() &&
|
|
20
|
+
c.req.header("x-auth-scheme") === "langsmith") {
|
|
21
|
+
c.set("auth", {
|
|
22
|
+
user: STUDIO_USER,
|
|
23
|
+
scopes: STUDIO_USER.permissions.slice(),
|
|
24
|
+
});
|
|
25
|
+
return next();
|
|
26
|
+
}
|
|
27
|
+
const auth = await authenticate(c.req.raw);
|
|
28
|
+
c.set("auth", auth);
|
|
29
|
+
return next();
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export { registerAuth } from "./index.mjs";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AuthEventValueMap } from "@langchain/langgraph-sdk/auth";
|
|
2
|
+
export declare const isAuthRegistered: () => boolean;
|
|
3
|
+
export declare const isStudioAuthDisabled: () => boolean;
|
|
4
|
+
export type AuthFilters = Record<string, string | {
|
|
5
|
+
$eq?: string;
|
|
6
|
+
$contains?: string | string[];
|
|
7
|
+
}> | undefined;
|
|
8
|
+
export interface AuthContext {
|
|
9
|
+
user: {
|
|
10
|
+
identity: string;
|
|
11
|
+
permissions: string[];
|
|
12
|
+
display_name: string;
|
|
13
|
+
is_authenticated: boolean;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
scopes: string[];
|
|
17
|
+
}
|
|
18
|
+
export declare function authorize(payload: {
|
|
19
|
+
resource: string;
|
|
20
|
+
action: string;
|
|
21
|
+
value: unknown;
|
|
22
|
+
context: AuthContext | undefined | null;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
filters: AuthFilters;
|
|
25
|
+
value: unknown;
|
|
26
|
+
}>;
|
|
27
|
+
export declare function authenticate(request: Request): Promise<{
|
|
28
|
+
scopes: string[];
|
|
29
|
+
user: {
|
|
30
|
+
permissions: string[];
|
|
31
|
+
is_authenticated: boolean;
|
|
32
|
+
display_name: string;
|
|
33
|
+
identity: string;
|
|
34
|
+
};
|
|
35
|
+
} | undefined>;
|
|
36
|
+
export declare function registerAuth(auth: {
|
|
37
|
+
path?: string;
|
|
38
|
+
disable_studio_auth?: boolean;
|
|
39
|
+
}, options: {
|
|
40
|
+
cwd: string;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
export declare const handleAuthEvent: <T extends keyof AuthEventValueMap>(context: AuthContext | undefined, event: T, value: AuthEventValueMap[T]) => Promise<[AuthFilters | undefined, value: AuthEventValueMap[T]]>;
|
|
43
|
+
export declare function isAuthMatching(metadata: Record<string, unknown> | undefined, filters: AuthFilters): boolean;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception";
|
|
2
|
+
import * as url from "node:url";
|
|
3
|
+
import * as path from "node: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
|
+
}
|
|
121
|
+
export const handleAuthEvent = async (context, event, value) => {
|
|
122
|
+
const [resource, action] = event.split(":");
|
|
123
|
+
const result = await authorize({
|
|
124
|
+
resource,
|
|
125
|
+
action,
|
|
126
|
+
context,
|
|
127
|
+
value,
|
|
128
|
+
});
|
|
129
|
+
return [result.filters, result.value];
|
|
130
|
+
};
|
|
131
|
+
export function isAuthMatching(metadata, filters) {
|
|
132
|
+
if (filters == null)
|
|
133
|
+
return true;
|
|
134
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
135
|
+
if (typeof value === "object" && value != null) {
|
|
136
|
+
if (value.$eq) {
|
|
137
|
+
if (metadata?.[key] !== value.$eq)
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
else if (value.$contains) {
|
|
141
|
+
if (!Array.isArray(metadata?.[key])) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (Array.isArray(value.$contains)) {
|
|
145
|
+
// Match Postgres list containment semantics (at the top level).
|
|
146
|
+
if (!value.$contains.every((v) => (metadata?.[key]).includes(v))) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
if (!metadata?.[key].includes(value.$contains)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
if (metadata?.[key] !== value)
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { asyncExitHook } from "exit-hook";
|
|
2
|
+
import * as process from "node:process";
|
|
3
|
+
import { startServer, StartServerSchema } from "../server.mjs";
|
|
4
|
+
import { connectToServer } from "./utils/ipc/client.mjs";
|
|
5
|
+
import { Client as LangSmithClient } from "langsmith";
|
|
6
|
+
import { logger } from "../logging.mjs";
|
|
7
|
+
logger.info(`Starting server...`);
|
|
8
|
+
const [ppid, payload] = process.argv.slice(-2);
|
|
9
|
+
const sendToParent = await connectToServer(+ppid);
|
|
10
|
+
// TODO: re-export langsmith/isTracingEnabled
|
|
11
|
+
const isTracingEnabled = () => {
|
|
12
|
+
const value = process.env?.LANGSMITH_TRACING_V2 ||
|
|
13
|
+
process.env?.LANGCHAIN_TRACING_V2 ||
|
|
14
|
+
process.env?.LANGSMITH_TRACING ||
|
|
15
|
+
process.env?.LANGCHAIN_TRACING;
|
|
16
|
+
return value === "true";
|
|
17
|
+
};
|
|
18
|
+
const options = StartServerSchema.parse(JSON.parse(payload));
|
|
19
|
+
// Export PORT to the environment
|
|
20
|
+
process.env.PORT = options.port.toString();
|
|
21
|
+
const [{ host, cleanup }, organizationId] = await Promise.all([
|
|
22
|
+
startServer(options),
|
|
23
|
+
(async () => {
|
|
24
|
+
if (isTracingEnabled()) {
|
|
25
|
+
try {
|
|
26
|
+
// @ts-expect-error Private method
|
|
27
|
+
return await new LangSmithClient()._getTenantId();
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
logger.warn("Failed to get organization ID. Tracing to LangSmith will not work.", { error });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
})(),
|
|
35
|
+
]);
|
|
36
|
+
logger.info(`Server running at ${host}`);
|
|
37
|
+
let queryParams = `?baseUrl=http://${options.host}:${options.port}`;
|
|
38
|
+
if (organizationId)
|
|
39
|
+
queryParams += `&organizationId=${organizationId}`;
|
|
40
|
+
asyncExitHook(cleanup, { wait: 3_000 });
|
|
41
|
+
sendToParent?.({ queryParams });
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare function spawnServer(args: {
|
|
2
|
+
host: string;
|
|
3
|
+
port: string;
|
|
4
|
+
nJobsPerWorker: string;
|
|
5
|
+
}, context: {
|
|
6
|
+
config: {
|
|
7
|
+
graphs: Record<string, string | {
|
|
8
|
+
path: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}>;
|
|
11
|
+
ui?: Record<string, string>;
|
|
12
|
+
ui_config?: {
|
|
13
|
+
shared?: string[];
|
|
14
|
+
};
|
|
15
|
+
auth?: {
|
|
16
|
+
path?: string;
|
|
17
|
+
disable_studio_auth?: boolean;
|
|
18
|
+
};
|
|
19
|
+
http?: {
|
|
20
|
+
app?: string;
|
|
21
|
+
disable_assistants?: boolean;
|
|
22
|
+
disable_threads?: boolean;
|
|
23
|
+
disable_runs?: boolean;
|
|
24
|
+
disable_store?: boolean;
|
|
25
|
+
disable_meta?: boolean;
|
|
26
|
+
cors?: {
|
|
27
|
+
allow_origins?: string[];
|
|
28
|
+
allow_methods?: string[];
|
|
29
|
+
allow_headers?: string[];
|
|
30
|
+
allow_credentials?: boolean;
|
|
31
|
+
allow_origin_regex?: string;
|
|
32
|
+
expose_headers?: string[];
|
|
33
|
+
max_age?: number;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
env: NodeJS.ProcessEnv;
|
|
38
|
+
hostUrl: string;
|
|
39
|
+
}, options: {
|
|
40
|
+
pid: number;
|
|
41
|
+
projectCwd: string;
|
|
42
|
+
}): Promise<import("child_process").ChildProcess>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
export async function spawnServer(args, context, options) {
|
|
4
|
+
const localUrl = `http://${args.host}:${args.port}`;
|
|
5
|
+
const studioUrl = `${context.hostUrl}/studio?baseUrl=${localUrl}`;
|
|
6
|
+
console.log(`
|
|
7
|
+
Welcome to
|
|
8
|
+
|
|
9
|
+
╦ ┌─┐┌┐┌┌─┐╔═╗┬─┐┌─┐┌─┐┬ ┬
|
|
10
|
+
║ ├─┤││││ ┬║ ╦├┬┘├─┤├─┘├─┤
|
|
11
|
+
╩═╝┴ ┴┘└┘└─┘╚═╝┴└─┴ ┴┴ ┴ ┴.js
|
|
12
|
+
|
|
13
|
+
- 🚀 API: \x1b[36m${localUrl}\x1b[0m
|
|
14
|
+
- 🎨 Studio UI: \x1b[36m${studioUrl}\x1b[0m
|
|
15
|
+
|
|
16
|
+
This in-memory server is designed for development and testing.
|
|
17
|
+
For production use, please use LangSmith Deployment.
|
|
18
|
+
|
|
19
|
+
`);
|
|
20
|
+
return spawn(process.execPath, [
|
|
21
|
+
fileURLToPath(new URL("../../cli.mjs", import.meta.resolve("tsx/esm/api"))),
|
|
22
|
+
"watch",
|
|
23
|
+
"--clear-screen=false",
|
|
24
|
+
"--import",
|
|
25
|
+
new URL(import.meta.resolve("../preload.mjs")).toString(),
|
|
26
|
+
fileURLToPath(new URL(import.meta.resolve("./entrypoint.mjs"))),
|
|
27
|
+
options.pid.toString(),
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
port: Number.parseInt(args.port, 10),
|
|
30
|
+
nWorkers: Number.parseInt(args.nJobsPerWorker, 10),
|
|
31
|
+
host: args.host,
|
|
32
|
+
graphs: context.config.graphs,
|
|
33
|
+
auth: context.config.auth,
|
|
34
|
+
ui: context.config.ui,
|
|
35
|
+
ui_config: context.config.ui_config,
|
|
36
|
+
cwd: options.projectCwd,
|
|
37
|
+
http: context.config.http,
|
|
38
|
+
}),
|
|
39
|
+
], {
|
|
40
|
+
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
|
41
|
+
env: {
|
|
42
|
+
...context.env,
|
|
43
|
+
NODE_ENV: "development",
|
|
44
|
+
LANGGRAPH_API_URL: localUrl,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|