@aprovan/chat-backend 0.1.0-dev.4d82df8 → 0.1.0-dev.67fbce2

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/lambda.js CHANGED
@@ -80,18 +80,10 @@ var planMiddleware = async (c, next) => {
80
80
  };
81
81
 
82
82
  // src/middleware/workspace.ts
83
- import { DynamoDBClient as DynamoDBClient3 } from "@aws-sdk/client-dynamodb";
84
- import { DynamoDBDocumentClient as DynamoDBDocumentClient3, QueryCommand } from "@aws-sdk/lib-dynamodb";
85
-
86
- // src/session.ts
87
83
  import { DynamoDBClient as DynamoDBClient2 } from "@aws-sdk/client-dynamodb";
88
- import {
89
- DynamoDBDocumentClient as DynamoDBDocumentClient2,
90
- GetCommand as GetCommand2,
91
- PutCommand
92
- } from "@aws-sdk/lib-dynamodb";
93
- var SESSION_CACHE_TTL_MS = 15e3;
94
- var sessionCache = /* @__PURE__ */ new Map();
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();
95
87
  var ddbClient2 = null;
96
88
  function getDdb2() {
97
89
  if (!ddbClient2) {
@@ -101,61 +93,43 @@ function getDdb2() {
101
93
  }
102
94
  return ddbClient2;
103
95
  }
104
- function tableName() {
105
- return process.env["USER_SESSIONS_TABLE_NAME"];
96
+ function evictWorkspaceCache(userSub) {
97
+ membershipCache.delete(userSub);
106
98
  }
107
- async function getSessionWorkspaceId(userSub) {
99
+ async function resolveWorkspaceId(userSub) {
108
100
  const now = Date.now();
109
- const cached = sessionCache.get(userSub);
110
- if (cached && now - cached.fetchedAt < SESSION_CACHE_TTL_MS) {
111
- return cached.activeWorkspaceId;
101
+ const cached = membershipCache.get(userSub);
102
+ if (cached && now - cached.fetchedAt < MEMBERSHIP_CACHE_TTL_MS) {
103
+ return cached.workspaceId;
112
104
  }
113
- const result = await getDdb2().send(
105
+ const usersResult = await getDdb2().send(
114
106
  new GetCommand2({
115
- TableName: tableName(),
116
- Key: { userSub }
107
+ TableName: process.env["USERS_TABLE_NAME"],
108
+ Key: { sub: userSub },
109
+ ProjectionExpression: "activeWorkspaceId"
117
110
  })
118
111
  );
119
- const activeWorkspaceId = result.Item?.activeWorkspaceId ?? null;
120
- sessionCache.set(userSub, { activeWorkspaceId, fetchedAt: now });
121
- return activeWorkspaceId;
122
- }
123
- async function setSessionWorkspaceId(userSub, workspaceId) {
124
- await getDdb2().send(
125
- new PutCommand({
126
- TableName: tableName(),
127
- Item: {
128
- userSub,
129
- activeWorkspaceId: workspaceId,
130
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
131
- }
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
132
124
  })
133
125
  );
134
- sessionCache.set(userSub, {
135
- activeWorkspaceId: workspaceId,
136
- fetchedAt: Date.now()
137
- });
138
- }
139
-
140
- // src/middleware/workspace.ts
141
- var MEMBERSHIP_CACHE_TTL_MS = 3e5;
142
- var membershipCache = /* @__PURE__ */ new Map();
143
- var ddbClient3 = null;
144
- function getDdb3() {
145
- if (!ddbClient3) {
146
- ddbClient3 = DynamoDBDocumentClient3.from(
147
- new DynamoDBClient3({ region: process.env["AWS_REGION"] })
148
- );
149
- }
150
- return ddbClient3;
126
+ const item = result.Items?.[0];
127
+ if (!item) return null;
128
+ membershipCache.set(userSub, { workspaceId: item.workspaceId, fetchedAt: now });
129
+ return item.workspaceId;
151
130
  }
152
131
  async function listWorkspaceMemberships(userSub) {
153
- const now = Date.now();
154
- const cached = membershipCache.get(userSub);
155
- if (cached && now - cached.fetchedAt < MEMBERSHIP_CACHE_TTL_MS) {
156
- return cached.workspaceIds;
157
- }
158
- const result = await getDdb3().send(
132
+ const result = await getDdb2().send(
159
133
  new QueryCommand({
160
134
  TableName: process.env["MEMBERSHIPS_TABLE_NAME"],
161
135
  IndexName: "ByUserSub",
@@ -163,22 +137,7 @@ async function listWorkspaceMemberships(userSub) {
163
137
  ExpressionAttributeValues: { ":sub": userSub }
164
138
  })
165
139
  );
166
- const workspaceIds = (result.Items ?? []).map(
167
- (item) => item.workspaceId
168
- );
169
- membershipCache.set(userSub, { workspaceIds, fetchedAt: now });
170
- return workspaceIds;
171
- }
172
- async function resolveWorkspaceId(userSub) {
173
- const [sessionWsId, workspaceIds] = await Promise.all([
174
- getSessionWorkspaceId(userSub),
175
- listWorkspaceMemberships(userSub)
176
- ]);
177
- if (workspaceIds.length === 0) return null;
178
- if (sessionWsId && workspaceIds.includes(sessionWsId)) {
179
- return sessionWsId;
180
- }
181
- return workspaceIds[0] ?? null;
140
+ return (result.Items ?? []).map((item) => item.workspaceId);
182
141
  }
183
142
  var workspaceMiddleware = async (c, next) => {
184
143
  const claims = c.get("claims");
@@ -553,7 +512,7 @@ var CHAT_PROMPT_ALLOWLIST = /* @__PURE__ */ new Set(["chat-patchwork-widget", "c
553
512
  var EDIT_PROMPT_ID = "edit-patchwork-widget";
554
513
 
555
514
  // src/gateway-session.ts
556
- var sessionCache2 = /* @__PURE__ */ new Map();
515
+ var sessionCache = /* @__PURE__ */ new Map();
557
516
  var toolsCache = /* @__PURE__ */ new Map();
558
517
  function getCachedTools(sub) {
559
518
  return toolsCache.get(sub);
@@ -569,12 +528,12 @@ function gatewayUrl() {
569
528
  async function getGatewaySession(claims, workspaceId, cognitoToken) {
570
529
  const sub = claims.sub;
571
530
  const now = Math.floor(Date.now() / 1e3);
572
- const cached = sessionCache2.get(sub);
531
+ const cached = sessionCache.get(sub);
573
532
  if (cached && cached.expires_at > now + 60) {
574
533
  return cached;
575
534
  }
576
535
  const entry = await exchangeSession(cognitoToken, workspaceId, claims.exp);
577
- sessionCache2.set(sub, entry);
536
+ sessionCache.set(sub, entry);
578
537
  return entry;
579
538
  }
580
539
  async function exchangeSession(cognitoToken, workspaceId, exp) {
@@ -616,7 +575,7 @@ async function callAuthSessions(cognitoToken, workspaceId) {
616
575
  });
617
576
  }
618
577
  function evictGatewaySession(sub) {
619
- sessionCache2.delete(sub);
578
+ sessionCache.delete(sub);
620
579
  toolsCache.delete(sub);
621
580
  }
622
581
  var GatewaySessionError = class extends Error {
@@ -948,95 +907,286 @@ import { Hono as Hono3 } from "hono";
948
907
  var health = new Hono3();
949
908
  health.get("/health", (c) => c.json({ status: "ok" }));
950
909
 
951
- // src/routes/proxy.ts
910
+ // src/routes/vfs.ts
911
+ import { ConditionalCheckFailedException, DynamoDBClient as DynamoDBClient3 } from "@aws-sdk/client-dynamodb";
912
+ import {
913
+ DynamoDBDocumentClient as DynamoDBDocumentClient3,
914
+ DeleteCommand,
915
+ GetCommand as GetCommand3,
916
+ QueryCommand as QueryCommand2,
917
+ UpdateCommand
918
+ } from "@aws-sdk/lib-dynamodb";
919
+ import {
920
+ DeleteObjectCommand,
921
+ GetObjectCommand,
922
+ PutObjectCommand,
923
+ S3Client
924
+ } from "@aws-sdk/client-s3";
952
925
  import { Hono as Hono4 } from "hono";
953
- var proxy = new Hono4();
954
- proxy.post("/:ns/:proc{.*}", async (c) => {
955
- const claims = c.get("claims");
926
+ var ddbClient3 = null;
927
+ var s3Client = null;
928
+ function getDdb3() {
929
+ if (!ddbClient3) {
930
+ ddbClient3 = DynamoDBDocumentClient3.from(
931
+ new DynamoDBClient3({ region: process.env["AWS_REGION"] })
932
+ );
933
+ }
934
+ return ddbClient3;
935
+ }
936
+ function getS3() {
937
+ if (!s3Client) {
938
+ s3Client = new S3Client({ region: process.env["AWS_REGION"] });
939
+ }
940
+ return s3Client;
941
+ }
942
+ function vfsTableName() {
943
+ return process.env["VFS_TABLE_NAME"];
944
+ }
945
+ function vfsBucketName() {
946
+ return process.env["VFS_BUCKET_NAME"];
947
+ }
948
+ function pk(wsId) {
949
+ return `workspace#${wsId}`;
950
+ }
951
+ function sk(filePath) {
952
+ return `file#${filePath}`;
953
+ }
954
+ function s3Key(wsId, filePath) {
955
+ return `${wsId}/${filePath}`;
956
+ }
957
+ var vfsRoute = new Hono4();
958
+ vfsRoute.get("/config", (c) => {
959
+ return c.json({ usePaths: true });
960
+ });
961
+ vfsRoute.get("/", async (c) => {
962
+ const since = c.req.query("since");
963
+ if (!since) {
964
+ return c.json({ error: "since query parameter is required" }, 400);
965
+ }
966
+ const sinceDate = new Date(since);
967
+ if (isNaN(sinceDate.getTime())) {
968
+ return c.json({ error: "since must be a valid RFC3339 timestamp" }, 400);
969
+ }
970
+ const tableName2 = vfsTableName();
971
+ if (!tableName2) {
972
+ return c.json([]);
973
+ }
956
974
  const workspaceId = c.get("workspaceId");
957
- const ns = c.req.param("ns");
958
- const proc = c.req.param("proc");
959
- const authHeader = c.req.header("Authorization");
960
- const cognitoToken = authHeader.slice("Bearer ".length);
961
- let sessionToken;
962
- try {
963
- const session = await getGatewaySession(claims, workspaceId, cognitoToken);
964
- sessionToken = session.token;
965
- } catch (err) {
966
- if (err instanceof GatewaySessionError && err.status === 401) {
967
- return c.json({ error: "Gateway session setup failed" }, 401);
975
+ const result = await getDdb3().send(
976
+ new QueryCommand2({
977
+ TableName: tableName2,
978
+ KeyConditionExpression: "PK = :pk",
979
+ FilterExpression: "#mtime > :since",
980
+ ExpressionAttributeNames: { "#mtime": "mtime" },
981
+ ExpressionAttributeValues: {
982
+ ":pk": pk(workspaceId),
983
+ ":since": sinceDate.toISOString()
984
+ }
985
+ })
986
+ );
987
+ const items = result.Items ?? [];
988
+ const changes = items.map((item) => ({
989
+ path: item.SK.replace(/^file#/, ""),
990
+ mtime: item.mtime,
991
+ version: item.version ?? 0,
992
+ size: item.size ?? 0
993
+ }));
994
+ return c.json(changes);
995
+ });
996
+ vfsRoute.on(["GET", "HEAD"], "/:path{.+}", async (c) => {
997
+ const isHead = c.req.method === "HEAD";
998
+ const filePath = c.req.param("path");
999
+ const workspaceId = c.get("workspaceId");
1000
+ const tableName2 = vfsTableName();
1001
+ const bucketName = vfsBucketName();
1002
+ if (isHead) {
1003
+ if (!tableName2) return c.body(null, 404);
1004
+ const result = await getDdb3().send(
1005
+ new GetCommand3({
1006
+ TableName: tableName2,
1007
+ Key: { PK: pk(workspaceId), SK: sk(filePath) },
1008
+ ProjectionExpression: "SK"
1009
+ })
1010
+ );
1011
+ return result.Item ? c.body(null, 200) : c.body(null, 404);
1012
+ }
1013
+ if (!tableName2) {
1014
+ return c.json({ error: "VFS not configured" }, 503);
1015
+ }
1016
+ if (c.req.query("stat") === "true") {
1017
+ const result = await getDdb3().send(
1018
+ new GetCommand3({
1019
+ TableName: tableName2,
1020
+ Key: { PK: pk(workspaceId), SK: sk(filePath) },
1021
+ ProjectionExpression: "#sz, #mt, version",
1022
+ ExpressionAttributeNames: { "#sz": "size", "#mt": "mtime" }
1023
+ })
1024
+ );
1025
+ if (!result.Item) return c.json({ error: "ENOENT" }, 404);
1026
+ const item = result.Item;
1027
+ return c.json({
1028
+ size: item.size,
1029
+ mtime: item.mtime,
1030
+ isFile: true,
1031
+ isDirectory: false
1032
+ });
1033
+ }
1034
+ if (c.req.query("readdir") === "true") {
1035
+ const prefix = filePath ? `file#${filePath}/` : "file#";
1036
+ const result = await getDdb3().send(
1037
+ new QueryCommand2({
1038
+ TableName: tableName2,
1039
+ KeyConditionExpression: "PK = :pk AND begins_with(SK, :prefix)",
1040
+ ExpressionAttributeValues: {
1041
+ ":pk": pk(workspaceId),
1042
+ ":prefix": prefix
1043
+ },
1044
+ ProjectionExpression: "SK"
1045
+ })
1046
+ );
1047
+ const items = result.Items ?? [];
1048
+ const seen = /* @__PURE__ */ new Map();
1049
+ for (const item of items) {
1050
+ const relativePath = item.SK.slice(prefix.length);
1051
+ const slashIdx = relativePath.indexOf("/");
1052
+ if (slashIdx === -1) {
1053
+ seen.set(relativePath, false);
1054
+ } else {
1055
+ const dirName = relativePath.slice(0, slashIdx);
1056
+ if (!seen.has(dirName)) {
1057
+ seen.set(dirName, true);
1058
+ }
1059
+ }
968
1060
  }
969
- return c.json({ error: "Failed to connect to gateway" }, 502);
1061
+ const entries = Array.from(seen.entries()).map(([name, isDirectory]) => ({
1062
+ name,
1063
+ isDirectory
1064
+ }));
1065
+ return c.json(entries);
970
1066
  }
971
- let body = {};
972
- try {
973
- body = await c.req.json();
974
- } catch {
1067
+ if (!bucketName) {
1068
+ return c.json({ error: "VFS storage not configured" }, 503);
975
1069
  }
976
- const gatewayUrl2 = process.env["GATEWAY_URL"].replace(/\/$/, "");
977
- const res = await fetch(`${gatewayUrl2}/tools/${ns}/${proc}`, {
978
- method: "POST",
979
- headers: {
980
- "Content-Type": "application/json",
981
- Authorization: `Bearer ${sessionToken}`
982
- },
983
- body: JSON.stringify(body)
984
- });
985
- if (res.status === 401) {
986
- evictGatewaySession(claims.sub);
987
- return c.json({ error: "Gateway authentication failed" }, 502);
1070
+ const fileKey = s3Key(workspaceId, filePath);
1071
+ try {
1072
+ const obj = await getS3().send(
1073
+ new GetObjectCommand({ Bucket: bucketName, Key: fileKey })
1074
+ );
1075
+ const body = await obj.Body?.transformToString("utf-8");
1076
+ if (body === void 0) return c.json({ error: "ENOENT" }, 404);
1077
+ return c.text(body, 200);
1078
+ } catch (err) {
1079
+ const code = err.name;
1080
+ if (code === "NoSuchKey" || code === "NotFound") {
1081
+ return c.json({ error: "ENOENT" }, 404);
1082
+ }
1083
+ throw err;
988
1084
  }
989
- const responseData = await res.json();
990
- return c.json(responseData, res.status);
991
1085
  });
992
-
993
- // src/routes/services.ts
994
- import { Hono as Hono5 } from "hono";
995
- var services = new Hono5();
996
- services.get("/", async (c) => {
997
- const claims = c.get("claims");
1086
+ vfsRoute.put("/:path{.+}", async (c) => {
1087
+ const tableName2 = vfsTableName();
1088
+ const bucketName = vfsBucketName();
1089
+ if (!tableName2 || !bucketName) {
1090
+ return c.json({ error: "VFS not configured" }, 503);
1091
+ }
1092
+ const filePath = c.req.param("path");
998
1093
  const workspaceId = c.get("workspaceId");
999
- const authHeader = c.req.header("Authorization");
1000
- const cognitoToken = authHeader.slice("Bearer ".length);
1001
- let sessionToken;
1094
+ const content = await c.req.text();
1095
+ const expectedVersionHeader = c.req.header("X-Vfs-Expected-Version");
1096
+ const expectedVersion = expectedVersionHeader !== void 0 ? parseInt(expectedVersionHeader, 10) : void 0;
1097
+ const fileKey = s3Key(workspaceId, filePath);
1098
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1099
+ const byteSize = new TextEncoder().encode(content).length;
1100
+ await getS3().send(
1101
+ new PutObjectCommand({
1102
+ Bucket: bucketName,
1103
+ Key: fileKey,
1104
+ Body: content,
1105
+ ContentType: "text/plain; charset=utf-8"
1106
+ })
1107
+ );
1002
1108
  try {
1003
- const session = await getGatewaySession(claims, workspaceId, cognitoToken);
1004
- sessionToken = session.token;
1109
+ const condExpr = expectedVersion !== void 0 ? "attribute_not_exists(PK) OR version = :expected" : void 0;
1110
+ const exprAttrValues = {
1111
+ ":mtime": now,
1112
+ ":sz": byteSize,
1113
+ ":s3Key": fileKey,
1114
+ ":inc": 1,
1115
+ ":zero": 0
1116
+ };
1117
+ if (expectedVersion !== void 0) {
1118
+ exprAttrValues[":expected"] = expectedVersion;
1119
+ }
1120
+ const result = await getDdb3().send(
1121
+ new UpdateCommand({
1122
+ TableName: tableName2,
1123
+ Key: { PK: pk(workspaceId), SK: sk(filePath) },
1124
+ UpdateExpression: "SET #mt = :mtime, #sz = :sz, s3Key = :s3Key, version = if_not_exists(version, :zero) + :inc",
1125
+ ExpressionAttributeNames: { "#mt": "mtime", "#sz": "size" },
1126
+ ExpressionAttributeValues: exprAttrValues,
1127
+ ...condExpr ? { ConditionExpression: condExpr } : {},
1128
+ ReturnValues: "ALL_NEW"
1129
+ })
1130
+ );
1131
+ const newVersion = result.Attributes?.version ?? 1;
1132
+ return c.json({ ok: true, version: newVersion });
1005
1133
  } catch (err) {
1006
- if (err instanceof GatewaySessionError && err.status === 401) {
1007
- return c.json({ error: "Gateway session setup failed" }, 401);
1134
+ if (err instanceof ConditionalCheckFailedException) {
1135
+ const metaResult = await getDdb3().send(
1136
+ new GetCommand3({
1137
+ TableName: tableName2,
1138
+ Key: { PK: pk(workspaceId), SK: sk(filePath) },
1139
+ ProjectionExpression: "version, etag"
1140
+ })
1141
+ );
1142
+ const item = metaResult.Item;
1143
+ return c.json(
1144
+ {
1145
+ ok: false,
1146
+ conflict: {
1147
+ serverVersion: item?.version ?? 0,
1148
+ serverEtag: item?.etag
1149
+ }
1150
+ },
1151
+ 409
1152
+ );
1008
1153
  }
1009
- return c.json({ error: "Failed to connect to gateway" }, 502);
1154
+ throw err;
1010
1155
  }
1011
- const gatewayUrl2 = process.env["GATEWAY_URL"].replace(/\/$/, "");
1012
- const res = await fetch(`${gatewayUrl2}/tools`, {
1013
- headers: { Authorization: `Bearer ${sessionToken}` }
1014
- });
1015
- if (res.status === 401) {
1016
- evictGatewaySession(claims.sub);
1017
- return c.json({ error: "Gateway authentication failed" }, 502);
1156
+ });
1157
+ vfsRoute.delete("/:path{.+}", async (c) => {
1158
+ const tableName2 = vfsTableName();
1159
+ const bucketName = vfsBucketName();
1160
+ if (!tableName2 || !bucketName) {
1161
+ return c.json({ error: "VFS not configured" }, 503);
1018
1162
  }
1019
- if (!res.ok) {
1020
- return c.json({ error: "Gateway tools fetch failed" }, 502);
1163
+ const filePath = c.req.param("path");
1164
+ const workspaceId = c.get("workspaceId");
1165
+ const fileKey = s3Key(workspaceId, filePath);
1166
+ await Promise.all([
1167
+ getDdb3().send(
1168
+ new DeleteCommand({
1169
+ TableName: tableName2,
1170
+ Key: { PK: pk(workspaceId), SK: sk(filePath) }
1171
+ })
1172
+ ),
1173
+ getS3().send(
1174
+ new DeleteObjectCommand({ Bucket: bucketName, Key: fileKey })
1175
+ )
1176
+ ]);
1177
+ return c.body(null, 204);
1178
+ });
1179
+ vfsRoute.post("/:path{.+}", (c) => {
1180
+ if (c.req.query("mkdir") === "true") {
1181
+ return c.body(null, 204);
1021
1182
  }
1022
- const data = await res.json();
1023
- const serviceList = data.tools.map((t) => ({
1024
- namespace: t.provider,
1025
- name: t.name,
1026
- procedure: t.operation,
1027
- description: t.description ?? "",
1028
- parameters: t.inputSchema
1029
- }));
1030
- const namespaces = Array.from(new Set(data.tools.map((t) => t.provider)));
1031
- return c.json({ namespaces, services: serviceList });
1183
+ return c.json({ error: "unsupported operation" }, 400);
1032
1184
  });
1033
1185
 
1034
- // src/routes/workspaces.ts
1186
+ // src/routes/workspace.ts
1035
1187
  import { DynamoDBClient as DynamoDBClient4 } from "@aws-sdk/client-dynamodb";
1036
- import { BatchGetCommand, DynamoDBDocumentClient as DynamoDBDocumentClient4 } from "@aws-sdk/lib-dynamodb";
1037
- import { zValidator } from "@hono/zod-validator";
1038
- import { Hono as Hono6 } from "hono";
1039
- import { z as z3 } from "zod";
1188
+ import { DynamoDBDocumentClient as DynamoDBDocumentClient4, UpdateCommand as UpdateCommand2 } from "@aws-sdk/lib-dynamodb";
1189
+ import { Hono as Hono5 } from "hono";
1040
1190
  var ddbClient4 = null;
1041
1191
  function getDdb4() {
1042
1192
  if (!ddbClient4) {
@@ -1046,10 +1196,101 @@ function getDdb4() {
1046
1196
  }
1047
1197
  return ddbClient4;
1048
1198
  }
1199
+ var workspaceRoute = new Hono5();
1200
+ workspaceRoute.post("/", async (c) => {
1201
+ const claims = c.get("claims");
1202
+ const body = await c.req.json();
1203
+ const workspaceId = body?.workspaceId;
1204
+ if (!workspaceId || typeof workspaceId !== "string") {
1205
+ return c.json({ error: "workspaceId is required" }, 400);
1206
+ }
1207
+ await getDdb4().send(
1208
+ new UpdateCommand2({
1209
+ TableName: process.env["USERS_TABLE_NAME"],
1210
+ Key: { sub: claims.sub },
1211
+ UpdateExpression: "SET activeWorkspaceId = :ws",
1212
+ ExpressionAttributeValues: { ":ws": workspaceId }
1213
+ })
1214
+ );
1215
+ evictWorkspaceCache(claims.sub);
1216
+ return c.json({ activeWorkspaceId: workspaceId });
1217
+ });
1218
+
1219
+ // src/routes/workspaces.ts
1220
+ import { DynamoDBClient as DynamoDBClient6 } from "@aws-sdk/client-dynamodb";
1221
+ import { BatchGetCommand, DynamoDBDocumentClient as DynamoDBDocumentClient6 } from "@aws-sdk/lib-dynamodb";
1222
+ import { zValidator } from "@hono/zod-validator";
1223
+ import { Hono as Hono6 } from "hono";
1224
+ import { z as z3 } from "zod";
1225
+
1226
+ // src/session.ts
1227
+ import { DynamoDBClient as DynamoDBClient5 } from "@aws-sdk/client-dynamodb";
1228
+ import {
1229
+ DynamoDBDocumentClient as DynamoDBDocumentClient5,
1230
+ GetCommand as GetCommand4,
1231
+ PutCommand
1232
+ } from "@aws-sdk/lib-dynamodb";
1233
+ var SESSION_CACHE_TTL_MS = 15e3;
1234
+ var sessionCache2 = /* @__PURE__ */ new Map();
1235
+ var ddbClient5 = null;
1236
+ function getDdb5() {
1237
+ if (!ddbClient5) {
1238
+ ddbClient5 = DynamoDBDocumentClient5.from(
1239
+ new DynamoDBClient5({ region: process.env["AWS_REGION"] })
1240
+ );
1241
+ }
1242
+ return ddbClient5;
1243
+ }
1244
+ function tableName() {
1245
+ return process.env["USER_SESSIONS_TABLE_NAME"];
1246
+ }
1247
+ async function getSessionWorkspaceId(userSub) {
1248
+ const now = Date.now();
1249
+ const cached = sessionCache2.get(userSub);
1250
+ if (cached && now - cached.fetchedAt < SESSION_CACHE_TTL_MS) {
1251
+ return cached.activeWorkspaceId;
1252
+ }
1253
+ const result = await getDdb5().send(
1254
+ new GetCommand4({
1255
+ TableName: tableName(),
1256
+ Key: { userSub }
1257
+ })
1258
+ );
1259
+ const activeWorkspaceId = result.Item?.activeWorkspaceId ?? null;
1260
+ sessionCache2.set(userSub, { activeWorkspaceId, fetchedAt: now });
1261
+ return activeWorkspaceId;
1262
+ }
1263
+ async function setSessionWorkspaceId(userSub, workspaceId) {
1264
+ await getDdb5().send(
1265
+ new PutCommand({
1266
+ TableName: tableName(),
1267
+ Item: {
1268
+ userSub,
1269
+ activeWorkspaceId: workspaceId,
1270
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1271
+ }
1272
+ })
1273
+ );
1274
+ sessionCache2.set(userSub, {
1275
+ activeWorkspaceId: workspaceId,
1276
+ fetchedAt: Date.now()
1277
+ });
1278
+ }
1279
+
1280
+ // src/routes/workspaces.ts
1281
+ var ddbClient6 = null;
1282
+ function getDdb6() {
1283
+ if (!ddbClient6) {
1284
+ ddbClient6 = DynamoDBDocumentClient6.from(
1285
+ new DynamoDBClient6({ region: process.env["AWS_REGION"] })
1286
+ );
1287
+ }
1288
+ return ddbClient6;
1289
+ }
1049
1290
  async function batchGetWorkspaces(workspaceIds) {
1050
1291
  if (workspaceIds.length === 0) return [];
1051
1292
  const tableName2 = process.env["WORKSPACE_TABLE_NAME"];
1052
- const result = await getDdb4().send(
1293
+ const result = await getDdb6().send(
1053
1294
  new BatchGetCommand({
1054
1295
  RequestItems: {
1055
1296
  [tableName2]: { Keys: workspaceIds.map((id) => ({ workspaceId: id })) }
@@ -1102,13 +1343,20 @@ workspacesRoute.put(
1102
1343
  function createChatApp() {
1103
1344
  const app2 = new Hono7();
1104
1345
  app2.route("/", health);
1105
- const api = app2.basePath("/api");
1106
- api.use(authMiddleware, workspaceMiddleware, planMiddleware);
1346
+ const authOnly = new Hono7();
1347
+ authOnly.use("/*", authMiddleware);
1348
+ authOnly.route("/workspace", workspaceRoute);
1349
+ app2.route("/api", authOnly);
1350
+ const vfs = new Hono7();
1351
+ vfs.use("/*", authMiddleware, workspaceMiddleware);
1352
+ vfs.route("/", vfsRoute);
1353
+ app2.route("/vfs", vfs);
1354
+ const api = new Hono7();
1355
+ api.use("/*", authMiddleware, workspaceMiddleware, planMiddleware);
1107
1356
  api.route("/chat", chatRoute);
1108
1357
  api.route("/edit", editRoute);
1109
- api.route("/services", services);
1110
- api.route("/proxy", proxy);
1111
1358
  api.route("/workspaces", workspacesRoute);
1359
+ app2.route("/api", api);
1112
1360
  return app2;
1113
1361
  }
1114
1362
 
@@ -1122,9 +1370,12 @@ var envSchema = z4.object({
1122
1370
  AWS_REGION: z4.string().default("us-east-1"),
1123
1371
  WORKSPACE_TABLE_NAME: z4.string().min(1),
1124
1372
  MEMBERSHIPS_TABLE_NAME: z4.string().min(1),
1125
- USER_SESSIONS_TABLE_NAME: z4.string().min(1),
1373
+ USERS_TABLE_NAME: z4.string().min(1),
1126
1374
  OPENROUTER_SECRET_ARN: z4.string().min(1),
1127
1375
  GATEWAY_URL: z4.string().url(),
1376
+ // VFS backing store (optional — routes degrade gracefully when unset)
1377
+ VFS_TABLE_NAME: z4.string().optional(),
1378
+ VFS_BUCKET_NAME: z4.string().optional(),
1128
1379
  // PostHog prompt management (optional — falls back to code prompts when absent)
1129
1380
  POSTHOG_PROJECT_API_KEY: z4.string().optional(),
1130
1381
  POSTHOG_PERSONAL_API_KEY: z4.string().optional(),