@aprovan/chat-backend 0.1.0-dev.99f9769 → 0.1.0-dev.9c336a0
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/.turbo/turbo-build.log +7 -7
- package/dist/index.d.ts +9 -0
- package/dist/index.js +583 -111
- package/dist/index.js.map +1 -1
- package/dist/lambda.js +583 -111
- package/dist/lambda.js.map +1 -1
- package/package.json +2 -1
- package/src/app.ts +23 -6
- package/src/env.ts +5 -0
- package/src/middleware/workspace.ts +38 -1
- package/src/routes/vfs.ts +369 -0
- package/src/routes/workspace.ts +41 -0
- package/src/routes/workspaces.ts +89 -0
- package/src/session.ts +80 -0
- package/test/app.test.ts +2 -0
- package/test/middleware/workspace.test.ts +79 -8
- package/test/routes/vfs.test.ts +436 -0
- package/test/routes/workspace.test.ts +107 -0
- package/test/routes/workspaces.test.ts +164 -0
- package/test/session.test.ts +56 -0
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { serve } from "@hono/node-server";
|
|
3
3
|
|
|
4
4
|
// src/app.ts
|
|
5
|
-
import { Hono as
|
|
5
|
+
import { Hono as Hono9 } from "hono";
|
|
6
6
|
|
|
7
7
|
// src/middleware/auth.ts
|
|
8
8
|
import { CognitoJwtVerifier } from "aws-jwt-verify";
|
|
@@ -32,11 +32,11 @@ var authMiddleware = async (c, next) => {
|
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
// src/middleware/
|
|
35
|
+
// src/middleware/plan.ts
|
|
36
36
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
37
|
-
import { DynamoDBDocumentClient,
|
|
38
|
-
var
|
|
39
|
-
var
|
|
37
|
+
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
|
|
38
|
+
var WORKSPACE_CACHE_TTL_MS = 6e4;
|
|
39
|
+
var workspaceCache = /* @__PURE__ */ new Map();
|
|
40
40
|
var ddbClient = null;
|
|
41
41
|
function getDdb() {
|
|
42
42
|
if (!ddbClient) {
|
|
@@ -46,57 +46,13 @@ function getDdb() {
|
|
|
46
46
|
}
|
|
47
47
|
return ddbClient;
|
|
48
48
|
}
|
|
49
|
-
async function resolveWorkspaceId(userSub) {
|
|
50
|
-
const now = Date.now();
|
|
51
|
-
const cached = membershipCache.get(userSub);
|
|
52
|
-
if (cached && now - cached.fetchedAt < MEMBERSHIP_CACHE_TTL_MS) {
|
|
53
|
-
return cached.workspaceId;
|
|
54
|
-
}
|
|
55
|
-
const result = await getDdb().send(
|
|
56
|
-
new QueryCommand({
|
|
57
|
-
TableName: process.env["MEMBERSHIPS_TABLE_NAME"],
|
|
58
|
-
IndexName: "ByUserSub",
|
|
59
|
-
KeyConditionExpression: "userSub = :sub",
|
|
60
|
-
ExpressionAttributeValues: { ":sub": userSub },
|
|
61
|
-
Limit: 1
|
|
62
|
-
})
|
|
63
|
-
);
|
|
64
|
-
const item = result.Items?.[0];
|
|
65
|
-
if (!item) return null;
|
|
66
|
-
membershipCache.set(userSub, { workspaceId: item.workspaceId, fetchedAt: now });
|
|
67
|
-
return item.workspaceId;
|
|
68
|
-
}
|
|
69
|
-
var workspaceMiddleware = async (c, next) => {
|
|
70
|
-
const claims = c.get("claims");
|
|
71
|
-
const workspaceId = await resolveWorkspaceId(claims.sub);
|
|
72
|
-
if (!workspaceId) {
|
|
73
|
-
return c.json({ error: "No workspace membership" }, 403);
|
|
74
|
-
}
|
|
75
|
-
c.set("workspaceId", workspaceId);
|
|
76
|
-
return next();
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// src/middleware/plan.ts
|
|
80
|
-
import { DynamoDBClient as DynamoDBClient2 } from "@aws-sdk/client-dynamodb";
|
|
81
|
-
import { DynamoDBDocumentClient as DynamoDBDocumentClient2, GetCommand } from "@aws-sdk/lib-dynamodb";
|
|
82
|
-
var WORKSPACE_CACHE_TTL_MS = 6e4;
|
|
83
|
-
var workspaceCache = /* @__PURE__ */ new Map();
|
|
84
|
-
var ddbClient2 = null;
|
|
85
|
-
function getDdb2() {
|
|
86
|
-
if (!ddbClient2) {
|
|
87
|
-
ddbClient2 = DynamoDBDocumentClient2.from(
|
|
88
|
-
new DynamoDBClient2({ region: process.env["AWS_REGION"] })
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
return ddbClient2;
|
|
92
|
-
}
|
|
93
49
|
async function getWorkspace(workspaceId) {
|
|
94
50
|
const now = Date.now();
|
|
95
51
|
const cached = workspaceCache.get(workspaceId);
|
|
96
52
|
if (cached && now - cached.fetchedAt < WORKSPACE_CACHE_TTL_MS) {
|
|
97
53
|
return cached.workspace;
|
|
98
54
|
}
|
|
99
|
-
const result = await
|
|
55
|
+
const result = await getDdb().send(
|
|
100
56
|
new GetCommand({
|
|
101
57
|
TableName: process.env["WORKSPACE_TABLE_NAME"],
|
|
102
58
|
Key: { workspaceId }
|
|
@@ -123,10 +79,75 @@ var planMiddleware = async (c, next) => {
|
|
|
123
79
|
return next();
|
|
124
80
|
};
|
|
125
81
|
|
|
126
|
-
// src/
|
|
127
|
-
import {
|
|
128
|
-
|
|
129
|
-
|
|
82
|
+
// src/middleware/workspace.ts
|
|
83
|
+
import { DynamoDBClient as DynamoDBClient2 } from "@aws-sdk/client-dynamodb";
|
|
84
|
+
import { DynamoDBDocumentClient as DynamoDBDocumentClient2, GetCommand as GetCommand2, QueryCommand } from "@aws-sdk/lib-dynamodb";
|
|
85
|
+
var MEMBERSHIP_CACHE_TTL_MS = 3e5;
|
|
86
|
+
var membershipCache = /* @__PURE__ */ new Map();
|
|
87
|
+
var ddbClient2 = null;
|
|
88
|
+
function getDdb2() {
|
|
89
|
+
if (!ddbClient2) {
|
|
90
|
+
ddbClient2 = DynamoDBDocumentClient2.from(
|
|
91
|
+
new DynamoDBClient2({ region: process.env["AWS_REGION"] })
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return ddbClient2;
|
|
95
|
+
}
|
|
96
|
+
function evictWorkspaceCache(userSub) {
|
|
97
|
+
membershipCache.delete(userSub);
|
|
98
|
+
}
|
|
99
|
+
async function resolveWorkspaceId(userSub) {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const cached = membershipCache.get(userSub);
|
|
102
|
+
if (cached && now - cached.fetchedAt < MEMBERSHIP_CACHE_TTL_MS) {
|
|
103
|
+
return cached.workspaceId;
|
|
104
|
+
}
|
|
105
|
+
const usersResult = await getDdb2().send(
|
|
106
|
+
new GetCommand2({
|
|
107
|
+
TableName: process.env["USERS_TABLE_NAME"],
|
|
108
|
+
Key: { sub: userSub },
|
|
109
|
+
ProjectionExpression: "activeWorkspaceId"
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
const userItem = usersResult.Item;
|
|
113
|
+
if (userItem?.activeWorkspaceId) {
|
|
114
|
+
membershipCache.set(userSub, { workspaceId: userItem.activeWorkspaceId, fetchedAt: now });
|
|
115
|
+
return userItem.activeWorkspaceId;
|
|
116
|
+
}
|
|
117
|
+
const result = await getDdb2().send(
|
|
118
|
+
new QueryCommand({
|
|
119
|
+
TableName: process.env["MEMBERSHIPS_TABLE_NAME"],
|
|
120
|
+
IndexName: "ByUserSub",
|
|
121
|
+
KeyConditionExpression: "userSub = :sub",
|
|
122
|
+
ExpressionAttributeValues: { ":sub": userSub },
|
|
123
|
+
Limit: 1
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
const item = result.Items?.[0];
|
|
127
|
+
if (!item) return null;
|
|
128
|
+
membershipCache.set(userSub, { workspaceId: item.workspaceId, fetchedAt: now });
|
|
129
|
+
return item.workspaceId;
|
|
130
|
+
}
|
|
131
|
+
async function listWorkspaceMemberships(userSub) {
|
|
132
|
+
const result = await getDdb2().send(
|
|
133
|
+
new QueryCommand({
|
|
134
|
+
TableName: process.env["MEMBERSHIPS_TABLE_NAME"],
|
|
135
|
+
IndexName: "ByUserSub",
|
|
136
|
+
KeyConditionExpression: "userSub = :sub",
|
|
137
|
+
ExpressionAttributeValues: { ":sub": userSub }
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
return (result.Items ?? []).map((item) => item.workspaceId);
|
|
141
|
+
}
|
|
142
|
+
var workspaceMiddleware = async (c, next) => {
|
|
143
|
+
const claims = c.get("claims");
|
|
144
|
+
const workspaceId = await resolveWorkspaceId(claims.sub);
|
|
145
|
+
if (!workspaceId) {
|
|
146
|
+
return c.json({ error: "No workspace membership" }, 403);
|
|
147
|
+
}
|
|
148
|
+
c.set("workspaceId", workspaceId);
|
|
149
|
+
return next();
|
|
150
|
+
};
|
|
130
151
|
|
|
131
152
|
// src/routes/chat.ts
|
|
132
153
|
import { withTracing } from "@posthog/ai";
|
|
@@ -137,7 +158,7 @@ import {
|
|
|
137
158
|
stepCountIs,
|
|
138
159
|
jsonSchema
|
|
139
160
|
} from "ai";
|
|
140
|
-
import { Hono
|
|
161
|
+
import { Hono } from "hono";
|
|
141
162
|
import { z } from "zod";
|
|
142
163
|
|
|
143
164
|
// src/fallback-prompts.ts
|
|
@@ -716,7 +737,7 @@ var retryAtStartMiddleware = {
|
|
|
716
737
|
}
|
|
717
738
|
}
|
|
718
739
|
};
|
|
719
|
-
var chatRoute = new
|
|
740
|
+
var chatRoute = new Hono();
|
|
720
741
|
async function fetchGatewayTools(gatewayUrl2, bearerToken) {
|
|
721
742
|
const res = await fetch(`${gatewayUrl2}/tools`, {
|
|
722
743
|
headers: { Authorization: `Bearer ${bearerToken}` }
|
|
@@ -844,14 +865,14 @@ chatRoute.post("/", async (c) => {
|
|
|
844
865
|
// src/routes/edit.ts
|
|
845
866
|
import { withTracing as withTracing2 } from "@posthog/ai";
|
|
846
867
|
import { streamText as streamText2 } from "ai";
|
|
847
|
-
import { Hono as
|
|
868
|
+
import { Hono as Hono2 } from "hono";
|
|
848
869
|
import { z as z2 } from "zod";
|
|
849
870
|
var editBodySchema = z2.object({
|
|
850
871
|
code: z2.string(),
|
|
851
872
|
prompt: z2.string()
|
|
852
873
|
});
|
|
853
874
|
var MODEL_ID = "openrouter/auto";
|
|
854
|
-
var editRoute = new
|
|
875
|
+
var editRoute = new Hono2();
|
|
855
876
|
editRoute.post("/", async (c) => {
|
|
856
877
|
const body = await c.req.json().catch(() => null);
|
|
857
878
|
const parsed = editBodySchema.safeParse(body);
|
|
@@ -881,12 +902,19 @@ editRoute.post("/", async (c) => {
|
|
|
881
902
|
return result.toTextStreamResponse();
|
|
882
903
|
});
|
|
883
904
|
|
|
884
|
-
// src/routes/
|
|
905
|
+
// src/routes/health.ts
|
|
906
|
+
import { Hono as Hono3 } from "hono";
|
|
907
|
+
var health = new Hono3();
|
|
908
|
+
health.get("/health", (c) => c.json({ status: "ok" }));
|
|
909
|
+
|
|
910
|
+
// src/routes/proxy.ts
|
|
885
911
|
import { Hono as Hono4 } from "hono";
|
|
886
|
-
var
|
|
887
|
-
|
|
912
|
+
var proxy = new Hono4();
|
|
913
|
+
proxy.post("/:ns/:proc{.*}", async (c) => {
|
|
888
914
|
const claims = c.get("claims");
|
|
889
915
|
const workspaceId = c.get("workspaceId");
|
|
916
|
+
const ns = c.req.param("ns");
|
|
917
|
+
const proc = c.req.param("proc");
|
|
890
918
|
const authHeader = c.req.header("Authorization");
|
|
891
919
|
const cognitoToken = authHeader.slice("Bearer ".length);
|
|
892
920
|
let sessionToken;
|
|
@@ -899,37 +927,343 @@ services.get("/", async (c) => {
|
|
|
899
927
|
}
|
|
900
928
|
return c.json({ error: "Failed to connect to gateway" }, 502);
|
|
901
929
|
}
|
|
930
|
+
let body = {};
|
|
931
|
+
try {
|
|
932
|
+
body = await c.req.json();
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
902
935
|
const gatewayUrl2 = process.env["GATEWAY_URL"].replace(/\/$/, "");
|
|
903
|
-
const res = await fetch(`${gatewayUrl2}/tools`, {
|
|
904
|
-
|
|
936
|
+
const res = await fetch(`${gatewayUrl2}/tools/${ns}/${proc}`, {
|
|
937
|
+
method: "POST",
|
|
938
|
+
headers: {
|
|
939
|
+
"Content-Type": "application/json",
|
|
940
|
+
Authorization: `Bearer ${sessionToken}`
|
|
941
|
+
},
|
|
942
|
+
body: JSON.stringify(body)
|
|
905
943
|
});
|
|
906
944
|
if (res.status === 401) {
|
|
907
945
|
evictGatewaySession(claims.sub);
|
|
908
946
|
return c.json({ error: "Gateway authentication failed" }, 502);
|
|
909
947
|
}
|
|
910
|
-
|
|
911
|
-
|
|
948
|
+
const responseData = await res.json();
|
|
949
|
+
return c.json(responseData, res.status);
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
// src/routes/vfs.ts
|
|
953
|
+
import { ConditionalCheckFailedException, DynamoDBClient as DynamoDBClient3 } from "@aws-sdk/client-dynamodb";
|
|
954
|
+
import {
|
|
955
|
+
DynamoDBDocumentClient as DynamoDBDocumentClient3,
|
|
956
|
+
DeleteCommand,
|
|
957
|
+
GetCommand as GetCommand3,
|
|
958
|
+
QueryCommand as QueryCommand2,
|
|
959
|
+
UpdateCommand
|
|
960
|
+
} from "@aws-sdk/lib-dynamodb";
|
|
961
|
+
import {
|
|
962
|
+
DeleteObjectCommand,
|
|
963
|
+
GetObjectCommand,
|
|
964
|
+
PutObjectCommand,
|
|
965
|
+
S3Client
|
|
966
|
+
} from "@aws-sdk/client-s3";
|
|
967
|
+
import { Hono as Hono5 } from "hono";
|
|
968
|
+
var ddbClient3 = null;
|
|
969
|
+
var s3Client = null;
|
|
970
|
+
function getDdb3() {
|
|
971
|
+
if (!ddbClient3) {
|
|
972
|
+
ddbClient3 = DynamoDBDocumentClient3.from(
|
|
973
|
+
new DynamoDBClient3({ region: process.env["AWS_REGION"] })
|
|
974
|
+
);
|
|
912
975
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
976
|
+
return ddbClient3;
|
|
977
|
+
}
|
|
978
|
+
function getS3() {
|
|
979
|
+
if (!s3Client) {
|
|
980
|
+
s3Client = new S3Client({ region: process.env["AWS_REGION"] });
|
|
981
|
+
}
|
|
982
|
+
return s3Client;
|
|
983
|
+
}
|
|
984
|
+
function vfsTableName() {
|
|
985
|
+
return process.env["VFS_TABLE_NAME"];
|
|
986
|
+
}
|
|
987
|
+
function vfsBucketName() {
|
|
988
|
+
return process.env["VFS_BUCKET_NAME"];
|
|
989
|
+
}
|
|
990
|
+
function pk(wsId) {
|
|
991
|
+
return `workspace#${wsId}`;
|
|
992
|
+
}
|
|
993
|
+
function sk(filePath) {
|
|
994
|
+
return `file#${filePath}`;
|
|
995
|
+
}
|
|
996
|
+
function s3Key(wsId, filePath) {
|
|
997
|
+
return `${wsId}/${filePath}`;
|
|
998
|
+
}
|
|
999
|
+
var vfsRoute = new Hono5();
|
|
1000
|
+
vfsRoute.get("/config", (c) => {
|
|
1001
|
+
return c.json({ usePaths: true });
|
|
1002
|
+
});
|
|
1003
|
+
vfsRoute.get("/", async (c) => {
|
|
1004
|
+
const since = c.req.query("since");
|
|
1005
|
+
if (!since) {
|
|
1006
|
+
return c.json({ error: "since query parameter is required" }, 400);
|
|
1007
|
+
}
|
|
1008
|
+
const sinceDate = new Date(since);
|
|
1009
|
+
if (isNaN(sinceDate.getTime())) {
|
|
1010
|
+
return c.json({ error: "since must be a valid RFC3339 timestamp" }, 400);
|
|
1011
|
+
}
|
|
1012
|
+
const tableName2 = vfsTableName();
|
|
1013
|
+
if (!tableName2) {
|
|
1014
|
+
return c.json([]);
|
|
1015
|
+
}
|
|
1016
|
+
const workspaceId = c.get("workspaceId");
|
|
1017
|
+
const result = await getDdb3().send(
|
|
1018
|
+
new QueryCommand2({
|
|
1019
|
+
TableName: tableName2,
|
|
1020
|
+
KeyConditionExpression: "PK = :pk",
|
|
1021
|
+
FilterExpression: "#mtime > :since",
|
|
1022
|
+
ExpressionAttributeNames: { "#mtime": "mtime" },
|
|
1023
|
+
ExpressionAttributeValues: {
|
|
1024
|
+
":pk": pk(workspaceId),
|
|
1025
|
+
":since": sinceDate.toISOString()
|
|
1026
|
+
}
|
|
1027
|
+
})
|
|
1028
|
+
);
|
|
1029
|
+
const items = result.Items ?? [];
|
|
1030
|
+
const changes = items.map((item) => ({
|
|
1031
|
+
path: item.SK.replace(/^file#/, ""),
|
|
1032
|
+
mtime: item.mtime,
|
|
1033
|
+
version: item.version ?? 0,
|
|
1034
|
+
size: item.size ?? 0
|
|
920
1035
|
}));
|
|
921
|
-
|
|
922
|
-
|
|
1036
|
+
return c.json(changes);
|
|
1037
|
+
});
|
|
1038
|
+
vfsRoute.on(["GET", "HEAD"], "/:path{.+}", async (c) => {
|
|
1039
|
+
const isHead = c.req.method === "HEAD";
|
|
1040
|
+
const filePath = c.req.param("path");
|
|
1041
|
+
const workspaceId = c.get("workspaceId");
|
|
1042
|
+
const tableName2 = vfsTableName();
|
|
1043
|
+
const bucketName = vfsBucketName();
|
|
1044
|
+
if (isHead) {
|
|
1045
|
+
if (!tableName2) return c.body(null, 404);
|
|
1046
|
+
const result = await getDdb3().send(
|
|
1047
|
+
new GetCommand3({
|
|
1048
|
+
TableName: tableName2,
|
|
1049
|
+
Key: { PK: pk(workspaceId), SK: sk(filePath) },
|
|
1050
|
+
ProjectionExpression: "SK"
|
|
1051
|
+
})
|
|
1052
|
+
);
|
|
1053
|
+
return result.Item ? c.body(null, 200) : c.body(null, 404);
|
|
1054
|
+
}
|
|
1055
|
+
if (!tableName2) {
|
|
1056
|
+
return c.json({ error: "VFS not configured" }, 503);
|
|
1057
|
+
}
|
|
1058
|
+
if (c.req.query("stat") === "true") {
|
|
1059
|
+
const result = await getDdb3().send(
|
|
1060
|
+
new GetCommand3({
|
|
1061
|
+
TableName: tableName2,
|
|
1062
|
+
Key: { PK: pk(workspaceId), SK: sk(filePath) },
|
|
1063
|
+
ProjectionExpression: "#sz, #mt, version",
|
|
1064
|
+
ExpressionAttributeNames: { "#sz": "size", "#mt": "mtime" }
|
|
1065
|
+
})
|
|
1066
|
+
);
|
|
1067
|
+
if (!result.Item) return c.json({ error: "ENOENT" }, 404);
|
|
1068
|
+
const item = result.Item;
|
|
1069
|
+
return c.json({
|
|
1070
|
+
size: item.size,
|
|
1071
|
+
mtime: item.mtime,
|
|
1072
|
+
isFile: true,
|
|
1073
|
+
isDirectory: false
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
if (c.req.query("readdir") === "true") {
|
|
1077
|
+
const prefix = filePath ? `file#${filePath}/` : "file#";
|
|
1078
|
+
const result = await getDdb3().send(
|
|
1079
|
+
new QueryCommand2({
|
|
1080
|
+
TableName: tableName2,
|
|
1081
|
+
KeyConditionExpression: "PK = :pk AND begins_with(SK, :prefix)",
|
|
1082
|
+
ExpressionAttributeValues: {
|
|
1083
|
+
":pk": pk(workspaceId),
|
|
1084
|
+
":prefix": prefix
|
|
1085
|
+
},
|
|
1086
|
+
ProjectionExpression: "SK"
|
|
1087
|
+
})
|
|
1088
|
+
);
|
|
1089
|
+
const items = result.Items ?? [];
|
|
1090
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1091
|
+
for (const item of items) {
|
|
1092
|
+
const relativePath = item.SK.slice(prefix.length);
|
|
1093
|
+
const slashIdx = relativePath.indexOf("/");
|
|
1094
|
+
if (slashIdx === -1) {
|
|
1095
|
+
seen.set(relativePath, false);
|
|
1096
|
+
} else {
|
|
1097
|
+
const dirName = relativePath.slice(0, slashIdx);
|
|
1098
|
+
if (!seen.has(dirName)) {
|
|
1099
|
+
seen.set(dirName, true);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const entries = Array.from(seen.entries()).map(([name, isDirectory]) => ({
|
|
1104
|
+
name,
|
|
1105
|
+
isDirectory
|
|
1106
|
+
}));
|
|
1107
|
+
return c.json(entries);
|
|
1108
|
+
}
|
|
1109
|
+
if (!bucketName) {
|
|
1110
|
+
return c.json({ error: "VFS storage not configured" }, 503);
|
|
1111
|
+
}
|
|
1112
|
+
const fileKey = s3Key(workspaceId, filePath);
|
|
1113
|
+
try {
|
|
1114
|
+
const obj = await getS3().send(
|
|
1115
|
+
new GetObjectCommand({ Bucket: bucketName, Key: fileKey })
|
|
1116
|
+
);
|
|
1117
|
+
const body = await obj.Body?.transformToString("utf-8");
|
|
1118
|
+
if (body === void 0) return c.json({ error: "ENOENT" }, 404);
|
|
1119
|
+
return c.text(body, 200);
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
const code = err.name;
|
|
1122
|
+
if (code === "NoSuchKey" || code === "NotFound") {
|
|
1123
|
+
return c.json({ error: "ENOENT" }, 404);
|
|
1124
|
+
}
|
|
1125
|
+
throw err;
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
vfsRoute.put("/:path{.+}", async (c) => {
|
|
1129
|
+
const tableName2 = vfsTableName();
|
|
1130
|
+
const bucketName = vfsBucketName();
|
|
1131
|
+
if (!tableName2 || !bucketName) {
|
|
1132
|
+
return c.json({ error: "VFS not configured" }, 503);
|
|
1133
|
+
}
|
|
1134
|
+
const filePath = c.req.param("path");
|
|
1135
|
+
const workspaceId = c.get("workspaceId");
|
|
1136
|
+
const content = await c.req.text();
|
|
1137
|
+
const expectedVersionHeader = c.req.header("X-Vfs-Expected-Version");
|
|
1138
|
+
const expectedVersion = expectedVersionHeader !== void 0 ? parseInt(expectedVersionHeader, 10) : void 0;
|
|
1139
|
+
const fileKey = s3Key(workspaceId, filePath);
|
|
1140
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1141
|
+
const byteSize = new TextEncoder().encode(content).length;
|
|
1142
|
+
await getS3().send(
|
|
1143
|
+
new PutObjectCommand({
|
|
1144
|
+
Bucket: bucketName,
|
|
1145
|
+
Key: fileKey,
|
|
1146
|
+
Body: content,
|
|
1147
|
+
ContentType: "text/plain; charset=utf-8"
|
|
1148
|
+
})
|
|
1149
|
+
);
|
|
1150
|
+
try {
|
|
1151
|
+
const condExpr = expectedVersion !== void 0 ? "attribute_not_exists(PK) OR version = :expected" : void 0;
|
|
1152
|
+
const exprAttrValues = {
|
|
1153
|
+
":mtime": now,
|
|
1154
|
+
":sz": byteSize,
|
|
1155
|
+
":s3Key": fileKey,
|
|
1156
|
+
":inc": 1,
|
|
1157
|
+
":zero": 0
|
|
1158
|
+
};
|
|
1159
|
+
if (expectedVersion !== void 0) {
|
|
1160
|
+
exprAttrValues[":expected"] = expectedVersion;
|
|
1161
|
+
}
|
|
1162
|
+
const result = await getDdb3().send(
|
|
1163
|
+
new UpdateCommand({
|
|
1164
|
+
TableName: tableName2,
|
|
1165
|
+
Key: { PK: pk(workspaceId), SK: sk(filePath) },
|
|
1166
|
+
UpdateExpression: "SET #mt = :mtime, #sz = :sz, s3Key = :s3Key, version = if_not_exists(version, :zero) + :inc",
|
|
1167
|
+
ExpressionAttributeNames: { "#mt": "mtime", "#sz": "size" },
|
|
1168
|
+
ExpressionAttributeValues: exprAttrValues,
|
|
1169
|
+
...condExpr ? { ConditionExpression: condExpr } : {},
|
|
1170
|
+
ReturnValues: "ALL_NEW"
|
|
1171
|
+
})
|
|
1172
|
+
);
|
|
1173
|
+
const newVersion = result.Attributes?.version ?? 1;
|
|
1174
|
+
return c.json({ ok: true, version: newVersion });
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
if (err instanceof ConditionalCheckFailedException) {
|
|
1177
|
+
const metaResult = await getDdb3().send(
|
|
1178
|
+
new GetCommand3({
|
|
1179
|
+
TableName: tableName2,
|
|
1180
|
+
Key: { PK: pk(workspaceId), SK: sk(filePath) },
|
|
1181
|
+
ProjectionExpression: "version, etag"
|
|
1182
|
+
})
|
|
1183
|
+
);
|
|
1184
|
+
const item = metaResult.Item;
|
|
1185
|
+
return c.json(
|
|
1186
|
+
{
|
|
1187
|
+
ok: false,
|
|
1188
|
+
conflict: {
|
|
1189
|
+
serverVersion: item?.version ?? 0,
|
|
1190
|
+
serverEtag: item?.etag
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
409
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
throw err;
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
vfsRoute.delete("/:path{.+}", async (c) => {
|
|
1200
|
+
const tableName2 = vfsTableName();
|
|
1201
|
+
const bucketName = vfsBucketName();
|
|
1202
|
+
if (!tableName2 || !bucketName) {
|
|
1203
|
+
return c.json({ error: "VFS not configured" }, 503);
|
|
1204
|
+
}
|
|
1205
|
+
const filePath = c.req.param("path");
|
|
1206
|
+
const workspaceId = c.get("workspaceId");
|
|
1207
|
+
const fileKey = s3Key(workspaceId, filePath);
|
|
1208
|
+
await Promise.all([
|
|
1209
|
+
getDdb3().send(
|
|
1210
|
+
new DeleteCommand({
|
|
1211
|
+
TableName: tableName2,
|
|
1212
|
+
Key: { PK: pk(workspaceId), SK: sk(filePath) }
|
|
1213
|
+
})
|
|
1214
|
+
),
|
|
1215
|
+
getS3().send(
|
|
1216
|
+
new DeleteObjectCommand({ Bucket: bucketName, Key: fileKey })
|
|
1217
|
+
)
|
|
1218
|
+
]);
|
|
1219
|
+
return c.body(null, 204);
|
|
1220
|
+
});
|
|
1221
|
+
vfsRoute.post("/:path{.+}", (c) => {
|
|
1222
|
+
if (c.req.query("mkdir") === "true") {
|
|
1223
|
+
return c.body(null, 204);
|
|
1224
|
+
}
|
|
1225
|
+
return c.json({ error: "unsupported operation" }, 400);
|
|
923
1226
|
});
|
|
924
1227
|
|
|
925
|
-
// src/routes/
|
|
926
|
-
import {
|
|
927
|
-
|
|
928
|
-
|
|
1228
|
+
// src/routes/workspace.ts
|
|
1229
|
+
import { DynamoDBClient as DynamoDBClient4 } from "@aws-sdk/client-dynamodb";
|
|
1230
|
+
import { DynamoDBDocumentClient as DynamoDBDocumentClient4, UpdateCommand as UpdateCommand2 } from "@aws-sdk/lib-dynamodb";
|
|
1231
|
+
import { Hono as Hono6 } from "hono";
|
|
1232
|
+
var ddbClient4 = null;
|
|
1233
|
+
function getDdb4() {
|
|
1234
|
+
if (!ddbClient4) {
|
|
1235
|
+
ddbClient4 = DynamoDBDocumentClient4.from(
|
|
1236
|
+
new DynamoDBClient4({ region: process.env["AWS_REGION"] })
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
return ddbClient4;
|
|
1240
|
+
}
|
|
1241
|
+
var workspaceRoute = new Hono6();
|
|
1242
|
+
workspaceRoute.post("/", async (c) => {
|
|
1243
|
+
const claims = c.get("claims");
|
|
1244
|
+
const body = await c.req.json();
|
|
1245
|
+
const workspaceId = body?.workspaceId;
|
|
1246
|
+
if (!workspaceId || typeof workspaceId !== "string") {
|
|
1247
|
+
return c.json({ error: "workspaceId is required" }, 400);
|
|
1248
|
+
}
|
|
1249
|
+
await getDdb4().send(
|
|
1250
|
+
new UpdateCommand2({
|
|
1251
|
+
TableName: process.env["USERS_TABLE_NAME"],
|
|
1252
|
+
Key: { sub: claims.sub },
|
|
1253
|
+
UpdateExpression: "SET activeWorkspaceId = :ws",
|
|
1254
|
+
ExpressionAttributeValues: { ":ws": workspaceId }
|
|
1255
|
+
})
|
|
1256
|
+
);
|
|
1257
|
+
evictWorkspaceCache(claims.sub);
|
|
1258
|
+
return c.json({ activeWorkspaceId: workspaceId });
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
// src/routes/services.ts
|
|
1262
|
+
import { Hono as Hono7 } from "hono";
|
|
1263
|
+
var services = new Hono7();
|
|
1264
|
+
services.get("/", async (c) => {
|
|
929
1265
|
const claims = c.get("claims");
|
|
930
1266
|
const workspaceId = c.get("workspaceId");
|
|
931
|
-
const ns = c.req.param("ns");
|
|
932
|
-
const proc = c.req.param("proc");
|
|
933
1267
|
const authHeader = c.req.header("Authorization");
|
|
934
1268
|
const cognitoToken = authHeader.slice("Bearer ".length);
|
|
935
1269
|
let sessionToken;
|
|
@@ -942,57 +1276,195 @@ proxy.post("/:ns/:proc{.*}", async (c) => {
|
|
|
942
1276
|
}
|
|
943
1277
|
return c.json({ error: "Failed to connect to gateway" }, 502);
|
|
944
1278
|
}
|
|
945
|
-
let body = {};
|
|
946
|
-
try {
|
|
947
|
-
body = await c.req.json();
|
|
948
|
-
} catch {
|
|
949
|
-
}
|
|
950
1279
|
const gatewayUrl2 = process.env["GATEWAY_URL"].replace(/\/$/, "");
|
|
951
|
-
const res = await fetch(`${gatewayUrl2}/tools
|
|
952
|
-
|
|
953
|
-
headers: {
|
|
954
|
-
"Content-Type": "application/json",
|
|
955
|
-
Authorization: `Bearer ${sessionToken}`
|
|
956
|
-
},
|
|
957
|
-
body: JSON.stringify(body)
|
|
1280
|
+
const res = await fetch(`${gatewayUrl2}/tools`, {
|
|
1281
|
+
headers: { Authorization: `Bearer ${sessionToken}` }
|
|
958
1282
|
});
|
|
959
1283
|
if (res.status === 401) {
|
|
960
1284
|
evictGatewaySession(claims.sub);
|
|
961
1285
|
return c.json({ error: "Gateway authentication failed" }, 502);
|
|
962
1286
|
}
|
|
963
|
-
|
|
964
|
-
|
|
1287
|
+
if (!res.ok) {
|
|
1288
|
+
return c.json({ error: "Gateway tools fetch failed" }, 502);
|
|
1289
|
+
}
|
|
1290
|
+
const data = await res.json();
|
|
1291
|
+
const serviceList = data.tools.map((t) => ({
|
|
1292
|
+
namespace: t.provider,
|
|
1293
|
+
name: t.name,
|
|
1294
|
+
procedure: t.operation,
|
|
1295
|
+
description: t.description ?? "",
|
|
1296
|
+
parameters: t.inputSchema
|
|
1297
|
+
}));
|
|
1298
|
+
const namespaces = Array.from(new Set(data.tools.map((t) => t.provider)));
|
|
1299
|
+
return c.json({ namespaces, services: serviceList });
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
// src/routes/workspaces.ts
|
|
1303
|
+
import { DynamoDBClient as DynamoDBClient6 } from "@aws-sdk/client-dynamodb";
|
|
1304
|
+
import { BatchGetCommand, DynamoDBDocumentClient as DynamoDBDocumentClient6 } from "@aws-sdk/lib-dynamodb";
|
|
1305
|
+
import { zValidator } from "@hono/zod-validator";
|
|
1306
|
+
import { Hono as Hono8 } from "hono";
|
|
1307
|
+
import { z as z3 } from "zod";
|
|
1308
|
+
|
|
1309
|
+
// src/session.ts
|
|
1310
|
+
import { DynamoDBClient as DynamoDBClient5 } from "@aws-sdk/client-dynamodb";
|
|
1311
|
+
import {
|
|
1312
|
+
DynamoDBDocumentClient as DynamoDBDocumentClient5,
|
|
1313
|
+
GetCommand as GetCommand4,
|
|
1314
|
+
PutCommand
|
|
1315
|
+
} from "@aws-sdk/lib-dynamodb";
|
|
1316
|
+
var SESSION_CACHE_TTL_MS = 15e3;
|
|
1317
|
+
var sessionCache2 = /* @__PURE__ */ new Map();
|
|
1318
|
+
var ddbClient5 = null;
|
|
1319
|
+
function getDdb5() {
|
|
1320
|
+
if (!ddbClient5) {
|
|
1321
|
+
ddbClient5 = DynamoDBDocumentClient5.from(
|
|
1322
|
+
new DynamoDBClient5({ region: process.env["AWS_REGION"] })
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
return ddbClient5;
|
|
1326
|
+
}
|
|
1327
|
+
function tableName() {
|
|
1328
|
+
return process.env["USER_SESSIONS_TABLE_NAME"];
|
|
1329
|
+
}
|
|
1330
|
+
async function getSessionWorkspaceId(userSub) {
|
|
1331
|
+
const now = Date.now();
|
|
1332
|
+
const cached = sessionCache2.get(userSub);
|
|
1333
|
+
if (cached && now - cached.fetchedAt < SESSION_CACHE_TTL_MS) {
|
|
1334
|
+
return cached.activeWorkspaceId;
|
|
1335
|
+
}
|
|
1336
|
+
const result = await getDdb5().send(
|
|
1337
|
+
new GetCommand4({
|
|
1338
|
+
TableName: tableName(),
|
|
1339
|
+
Key: { userSub }
|
|
1340
|
+
})
|
|
1341
|
+
);
|
|
1342
|
+
const activeWorkspaceId = result.Item?.activeWorkspaceId ?? null;
|
|
1343
|
+
sessionCache2.set(userSub, { activeWorkspaceId, fetchedAt: now });
|
|
1344
|
+
return activeWorkspaceId;
|
|
1345
|
+
}
|
|
1346
|
+
async function setSessionWorkspaceId(userSub, workspaceId) {
|
|
1347
|
+
await getDdb5().send(
|
|
1348
|
+
new PutCommand({
|
|
1349
|
+
TableName: tableName(),
|
|
1350
|
+
Item: {
|
|
1351
|
+
userSub,
|
|
1352
|
+
activeWorkspaceId: workspaceId,
|
|
1353
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1354
|
+
}
|
|
1355
|
+
})
|
|
1356
|
+
);
|
|
1357
|
+
sessionCache2.set(userSub, {
|
|
1358
|
+
activeWorkspaceId: workspaceId,
|
|
1359
|
+
fetchedAt: Date.now()
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/routes/workspaces.ts
|
|
1364
|
+
var ddbClient6 = null;
|
|
1365
|
+
function getDdb6() {
|
|
1366
|
+
if (!ddbClient6) {
|
|
1367
|
+
ddbClient6 = DynamoDBDocumentClient6.from(
|
|
1368
|
+
new DynamoDBClient6({ region: process.env["AWS_REGION"] })
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
return ddbClient6;
|
|
1372
|
+
}
|
|
1373
|
+
async function batchGetWorkspaces(workspaceIds) {
|
|
1374
|
+
if (workspaceIds.length === 0) return [];
|
|
1375
|
+
const tableName2 = process.env["WORKSPACE_TABLE_NAME"];
|
|
1376
|
+
const result = await getDdb6().send(
|
|
1377
|
+
new BatchGetCommand({
|
|
1378
|
+
RequestItems: {
|
|
1379
|
+
[tableName2]: { Keys: workspaceIds.map((id) => ({ workspaceId: id })) }
|
|
1380
|
+
}
|
|
1381
|
+
})
|
|
1382
|
+
);
|
|
1383
|
+
return result.Responses?.[tableName2] ?? [];
|
|
1384
|
+
}
|
|
1385
|
+
var workspacesRoute = new Hono8();
|
|
1386
|
+
workspacesRoute.get("/", async (c) => {
|
|
1387
|
+
const claims = c.get("claims");
|
|
1388
|
+
const userSub = claims.sub;
|
|
1389
|
+
const [workspaceIds, activeWorkspaceId] = await Promise.all([
|
|
1390
|
+
listWorkspaceMemberships(userSub),
|
|
1391
|
+
getSessionWorkspaceId(userSub)
|
|
1392
|
+
]);
|
|
1393
|
+
const workspaces = await batchGetWorkspaces(workspaceIds);
|
|
1394
|
+
const effectiveActiveId = activeWorkspaceId && workspaceIds.includes(activeWorkspaceId) ? activeWorkspaceId : workspaceIds[0] ?? null;
|
|
1395
|
+
const sorted = workspaceIds.map((id) => workspaces.find((w) => w.workspaceId === id)).filter((w) => w !== void 0);
|
|
1396
|
+
return c.json({
|
|
1397
|
+
workspaces: sorted.map((w) => ({
|
|
1398
|
+
workspaceId: w.workspaceId,
|
|
1399
|
+
name: w.name,
|
|
1400
|
+
plan: w.plan,
|
|
1401
|
+
active: w.workspaceId === effectiveActiveId
|
|
1402
|
+
})),
|
|
1403
|
+
activeWorkspaceId: effectiveActiveId
|
|
1404
|
+
});
|
|
965
1405
|
});
|
|
1406
|
+
var setActiveSchema = z3.object({
|
|
1407
|
+
workspaceId: z3.string().min(1)
|
|
1408
|
+
});
|
|
1409
|
+
workspacesRoute.put(
|
|
1410
|
+
"/active",
|
|
1411
|
+
zValidator("json", setActiveSchema),
|
|
1412
|
+
async (c) => {
|
|
1413
|
+
const claims = c.get("claims");
|
|
1414
|
+
const userSub = claims.sub;
|
|
1415
|
+
const { workspaceId } = c.req.valid("json");
|
|
1416
|
+
const workspaceIds = await listWorkspaceMemberships(userSub);
|
|
1417
|
+
if (!workspaceIds.includes(workspaceId)) {
|
|
1418
|
+
return c.json({ error: "Not a member of this workspace" }, 403);
|
|
1419
|
+
}
|
|
1420
|
+
await setSessionWorkspaceId(userSub, workspaceId);
|
|
1421
|
+
return c.json({ activeWorkspaceId: workspaceId });
|
|
1422
|
+
}
|
|
1423
|
+
);
|
|
966
1424
|
|
|
967
1425
|
// src/app.ts
|
|
968
1426
|
function createChatApp() {
|
|
969
|
-
const app2 = new
|
|
1427
|
+
const app2 = new Hono9();
|
|
970
1428
|
app2.route("/", health);
|
|
971
|
-
const
|
|
972
|
-
|
|
1429
|
+
const authOnly = new Hono9();
|
|
1430
|
+
authOnly.use("/*", authMiddleware);
|
|
1431
|
+
authOnly.route("/workspace", workspaceRoute);
|
|
1432
|
+
app2.route("/api", authOnly);
|
|
1433
|
+
const vfs = new Hono9();
|
|
1434
|
+
vfs.use("/*", authMiddleware, workspaceMiddleware);
|
|
1435
|
+
vfs.route("/", vfsRoute);
|
|
1436
|
+
app2.route("/vfs", vfs);
|
|
1437
|
+
const api = new Hono9();
|
|
1438
|
+
api.use("/*", authMiddleware, workspaceMiddleware, planMiddleware);
|
|
973
1439
|
api.route("/chat", chatRoute);
|
|
974
1440
|
api.route("/edit", editRoute);
|
|
975
1441
|
api.route("/services", services);
|
|
976
1442
|
api.route("/proxy", proxy);
|
|
1443
|
+
api.route("/workspaces", workspacesRoute);
|
|
1444
|
+
app2.route("/api", api);
|
|
977
1445
|
return app2;
|
|
978
1446
|
}
|
|
979
1447
|
|
|
980
1448
|
// src/env.ts
|
|
981
|
-
import { z as
|
|
982
|
-
var envSchema =
|
|
983
|
-
NODE_ENV:
|
|
984
|
-
PORT:
|
|
985
|
-
COGNITO_USER_POOL_ID:
|
|
986
|
-
COGNITO_CLIENT_ID:
|
|
987
|
-
AWS_REGION:
|
|
988
|
-
WORKSPACE_TABLE_NAME:
|
|
989
|
-
MEMBERSHIPS_TABLE_NAME:
|
|
990
|
-
|
|
991
|
-
|
|
1449
|
+
import { z as z4 } from "zod";
|
|
1450
|
+
var envSchema = z4.object({
|
|
1451
|
+
NODE_ENV: z4.enum(["development", "production", "test"]).default("development"),
|
|
1452
|
+
PORT: z4.coerce.number().default(3001),
|
|
1453
|
+
COGNITO_USER_POOL_ID: z4.string().min(1),
|
|
1454
|
+
COGNITO_CLIENT_ID: z4.string().min(1),
|
|
1455
|
+
AWS_REGION: z4.string().default("us-east-1"),
|
|
1456
|
+
WORKSPACE_TABLE_NAME: z4.string().min(1),
|
|
1457
|
+
MEMBERSHIPS_TABLE_NAME: z4.string().min(1),
|
|
1458
|
+
USERS_TABLE_NAME: z4.string().min(1),
|
|
1459
|
+
OPENROUTER_SECRET_ARN: z4.string().min(1),
|
|
1460
|
+
GATEWAY_URL: z4.string().url(),
|
|
1461
|
+
// VFS backing store (optional — routes degrade gracefully when unset)
|
|
1462
|
+
VFS_TABLE_NAME: z4.string().optional(),
|
|
1463
|
+
VFS_BUCKET_NAME: z4.string().optional(),
|
|
992
1464
|
// PostHog prompt management (optional — falls back to code prompts when absent)
|
|
993
|
-
POSTHOG_PROJECT_API_KEY:
|
|
994
|
-
POSTHOG_PERSONAL_API_KEY:
|
|
995
|
-
POSTHOG_HOST:
|
|
1465
|
+
POSTHOG_PROJECT_API_KEY: z4.string().optional(),
|
|
1466
|
+
POSTHOG_PERSONAL_API_KEY: z4.string().optional(),
|
|
1467
|
+
POSTHOG_HOST: z4.string().default("https://us.posthog.com")
|
|
996
1468
|
});
|
|
997
1469
|
function parseEnv(raw) {
|
|
998
1470
|
return envSchema.parse(raw);
|