@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/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 Hono6 } from "hono";
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/workspace.ts
35
+ // src/middleware/plan.ts
36
36
  import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
37
- import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb";
38
- var MEMBERSHIP_CACHE_TTL_MS = 3e5;
39
- var membershipCache = /* @__PURE__ */ new Map();
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 getDdb2().send(
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/routes/health.ts
127
- import { Hono } from "hono";
128
- var health = new Hono();
129
- health.get("/health", (c) => c.json({ status: "ok" }));
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 as Hono2 } from "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 Hono2();
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 Hono3 } from "hono";
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 Hono3();
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/services.ts
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 services = new Hono4();
887
- services.get("/", async (c) => {
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
- headers: { Authorization: `Bearer ${sessionToken}` }
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
- if (!res.ok) {
911
- return c.json({ error: "Gateway tools fetch failed" }, 502);
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
- const data = await res.json();
914
- const serviceList = data.tools.map((t) => ({
915
- namespace: t.provider,
916
- name: t.name,
917
- procedure: t.operation,
918
- description: t.description ?? "",
919
- parameters: t.inputSchema
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
- const namespaces = Array.from(new Set(data.tools.map((t) => t.provider)));
922
- return c.json({ namespaces, services: serviceList });
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/proxy.ts
926
- import { Hono as Hono5 } from "hono";
927
- var proxy = new Hono5();
928
- proxy.post("/:ns/:proc{.*}", async (c) => {
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/${ns}/${proc}`, {
952
- method: "POST",
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
- const responseData = await res.json();
964
- return c.json(responseData, res.status);
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 Hono6();
1427
+ const app2 = new Hono9();
970
1428
  app2.route("/", health);
971
- const api = app2.basePath("/api");
972
- api.use(authMiddleware, workspaceMiddleware, planMiddleware);
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 z3 } from "zod";
982
- var envSchema = z3.object({
983
- NODE_ENV: z3.enum(["development", "production", "test"]).default("development"),
984
- PORT: z3.coerce.number().default(3001),
985
- COGNITO_USER_POOL_ID: z3.string().min(1),
986
- COGNITO_CLIENT_ID: z3.string().min(1),
987
- AWS_REGION: z3.string().default("us-east-1"),
988
- WORKSPACE_TABLE_NAME: z3.string().min(1),
989
- MEMBERSHIPS_TABLE_NAME: z3.string().min(1),
990
- OPENROUTER_SECRET_ARN: z3.string().min(1),
991
- GATEWAY_URL: z3.string().url(),
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: z3.string().optional(),
994
- POSTHOG_PERSONAL_API_KEY: z3.string().optional(),
995
- POSTHOG_HOST: z3.string().default("https://us.posthog.com")
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);