@aprovan/chat-backend 0.1.0-dev.93c7b6a → 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/lambda.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { streamHandle } from "hono/aws-lambda";
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
@@ -492,6 +513,13 @@ var EDIT_PROMPT_ID = "edit-patchwork-widget";
492
513
 
493
514
  // src/gateway-session.ts
494
515
  var sessionCache = /* @__PURE__ */ new Map();
516
+ var toolsCache = /* @__PURE__ */ new Map();
517
+ function getCachedTools(sub) {
518
+ return toolsCache.get(sub);
519
+ }
520
+ function setCachedTools(sub, tools) {
521
+ toolsCache.set(sub, tools);
522
+ }
495
523
  function gatewayUrl() {
496
524
  const url = process.env["GATEWAY_URL"];
497
525
  if (!url) throw new Error("GATEWAY_URL is not set");
@@ -509,7 +537,13 @@ async function getGatewaySession(claims, workspaceId, cognitoToken) {
509
537
  return entry;
510
538
  }
511
539
  async function exchangeSession(cognitoToken, workspaceId, exp) {
512
- const res = await callAuthSessions(cognitoToken, workspaceId);
540
+ let res;
541
+ try {
542
+ res = await callAuthSessions(cognitoToken, workspaceId);
543
+ } catch {
544
+ await new Promise((r) => setTimeout(r, 200));
545
+ res = await callAuthSessions(cognitoToken, workspaceId);
546
+ }
513
547
  if (res.status === 401) {
514
548
  const retryRes = await callAuthSessions(cognitoToken, workspaceId);
515
549
  if (!retryRes.ok) {
@@ -517,6 +551,14 @@ async function exchangeSession(cognitoToken, workspaceId, exp) {
517
551
  }
518
552
  return { token: cognitoToken, expires_at: exp };
519
553
  }
554
+ if (res.status >= 500) {
555
+ await new Promise((r) => setTimeout(r, 200));
556
+ const retryRes = await callAuthSessions(cognitoToken, workspaceId);
557
+ if (!retryRes.ok) {
558
+ throw new GatewaySessionError(retryRes.status, await retryRes.text());
559
+ }
560
+ return { token: cognitoToken, expires_at: exp };
561
+ }
520
562
  if (!res.ok) {
521
563
  throw new GatewaySessionError(res.status, await res.text());
522
564
  }
@@ -534,6 +576,7 @@ async function callAuthSessions(cognitoToken, workspaceId) {
534
576
  }
535
577
  function evictGatewaySession(sub) {
536
578
  sessionCache.delete(sub);
579
+ toolsCache.delete(sub);
537
580
  }
538
581
  var GatewaySessionError = class extends Error {
539
582
  constructor(status, message) {
@@ -674,6 +717,7 @@ var chatBodySchema = z.object({
674
717
  id: z.string(),
675
718
  messages: z.array(z.any()),
676
719
  trigger: z.string(),
720
+ model: z.string().optional(),
677
721
  metadata: z.unknown().optional(),
678
722
  prompt: z.object({
679
723
  id: z.string(),
@@ -693,7 +737,7 @@ var retryAtStartMiddleware = {
693
737
  }
694
738
  }
695
739
  };
696
- var chatRoute = new Hono2();
740
+ var chatRoute = new Hono();
697
741
  async function fetchGatewayTools(gatewayUrl2, bearerToken) {
698
742
  const res = await fetch(`${gatewayUrl2}/tools`, {
699
743
  headers: { Authorization: `Bearer ${bearerToken}` }
@@ -739,10 +783,16 @@ chatRoute.post("/", async (c) => {
739
783
  if (!parsed.success) {
740
784
  return c.json({ error: "Invalid request body" }, 400);
741
785
  }
742
- const { messages, prompt: promptBody } = parsed.data;
786
+ const { messages, model: requestedModel, prompt: promptBody } = parsed.data;
743
787
  const claims = c.get("claims");
744
788
  const workspaceId = c.get("workspaceId");
745
789
  const workspace = c.get("workspace");
790
+ if (requestedModel !== void 0 && !workspace.limits.maxModels.includes(requestedModel)) {
791
+ return c.json(
792
+ { error: "Model not allowed on your current plan", model: requestedModel },
793
+ 403
794
+ );
795
+ }
746
796
  const promptId = promptBody?.id ?? "chat-patchwork-widget";
747
797
  if (!CHAT_PROMPT_ALLOWLIST.has(promptId)) {
748
798
  return c.json({ error: "Unknown prompt id" }, 400);
@@ -760,11 +810,18 @@ chatRoute.post("/", async (c) => {
760
810
  }
761
811
  let gatewayTools = [];
762
812
  if (sessionToken && gatewayUrl2) {
763
- gatewayTools = await fetchGatewayTools(gatewayUrl2, sessionToken).catch(
764
- () => []
765
- );
766
- if (gatewayTools.length === 0) {
767
- evictGatewaySession(claims.sub);
813
+ const cached = getCachedTools(claims.sub);
814
+ if (cached) {
815
+ gatewayTools = cached;
816
+ } else {
817
+ gatewayTools = await fetchGatewayTools(gatewayUrl2, sessionToken).catch(
818
+ () => []
819
+ );
820
+ if (gatewayTools.length === 0) {
821
+ evictGatewaySession(claims.sub);
822
+ } else {
823
+ setCachedTools(claims.sub, gatewayTools);
824
+ }
768
825
  }
769
826
  }
770
827
  const tools = sessionToken && gatewayUrl2 ? buildTools(gatewayTools, gatewayUrl2, sessionToken) : {};
@@ -780,7 +837,7 @@ chatRoute.post("/", async (c) => {
780
837
  });
781
838
  const apiKey = await getOpenRouterKey();
782
839
  const provider = createOpenRouterProvider(apiKey);
783
- const modelId = workspace.limits.maxModels[0] ?? "openrouter/auto";
840
+ const modelId = requestedModel ?? workspace.limits.maxModels[0] ?? "openrouter/auto";
784
841
  const baseModel = provider(modelId);
785
842
  const phClient = getPostHogClient();
786
843
  const tracedModel = phClient && promptResult.source !== "code_fallback" ? withTracing(baseModel, phClient, {
@@ -808,14 +865,14 @@ chatRoute.post("/", async (c) => {
808
865
  // src/routes/edit.ts
809
866
  import { withTracing as withTracing2 } from "@posthog/ai";
810
867
  import { streamText as streamText2 } from "ai";
811
- import { Hono as Hono3 } from "hono";
868
+ import { Hono as Hono2 } from "hono";
812
869
  import { z as z2 } from "zod";
813
870
  var editBodySchema = z2.object({
814
871
  code: z2.string(),
815
872
  prompt: z2.string()
816
873
  });
817
874
  var MODEL_ID = "openrouter/auto";
818
- var editRoute = new Hono3();
875
+ var editRoute = new Hono2();
819
876
  editRoute.post("/", async (c) => {
820
877
  const body = await c.req.json().catch(() => null);
821
878
  const parsed = editBodySchema.safeParse(body);
@@ -845,12 +902,19 @@ editRoute.post("/", async (c) => {
845
902
  return result.toTextStreamResponse();
846
903
  });
847
904
 
848
- // 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
849
911
  import { Hono as Hono4 } from "hono";
850
- var services = new Hono4();
851
- services.get("/", async (c) => {
912
+ var proxy = new Hono4();
913
+ proxy.post("/:ns/:proc{.*}", async (c) => {
852
914
  const claims = c.get("claims");
853
915
  const workspaceId = c.get("workspaceId");
916
+ const ns = c.req.param("ns");
917
+ const proc = c.req.param("proc");
854
918
  const authHeader = c.req.header("Authorization");
855
919
  const cognitoToken = authHeader.slice("Bearer ".length);
856
920
  let sessionToken;
@@ -863,37 +927,343 @@ services.get("/", async (c) => {
863
927
  }
864
928
  return c.json({ error: "Failed to connect to gateway" }, 502);
865
929
  }
930
+ let body = {};
931
+ try {
932
+ body = await c.req.json();
933
+ } catch {
934
+ }
866
935
  const gatewayUrl2 = process.env["GATEWAY_URL"].replace(/\/$/, "");
867
- const res = await fetch(`${gatewayUrl2}/tools`, {
868
- 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)
869
943
  });
870
944
  if (res.status === 401) {
871
945
  evictGatewaySession(claims.sub);
872
946
  return c.json({ error: "Gateway authentication failed" }, 502);
873
947
  }
874
- if (!res.ok) {
875
- 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
+ );
876
975
  }
877
- const data = await res.json();
878
- const serviceList = data.tools.map((t) => ({
879
- namespace: t.provider,
880
- name: t.name,
881
- procedure: t.operation,
882
- description: t.description ?? "",
883
- 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
884
1035
  }));
885
- const namespaces = Array.from(new Set(data.tools.map((t) => t.provider)));
886
- 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);
887
1226
  });
888
1227
 
889
- // src/routes/proxy.ts
890
- import { Hono as Hono5 } from "hono";
891
- var proxy = new Hono5();
892
- 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) => {
893
1265
  const claims = c.get("claims");
894
1266
  const workspaceId = c.get("workspaceId");
895
- const ns = c.req.param("ns");
896
- const proc = c.req.param("proc");
897
1267
  const authHeader = c.req.header("Authorization");
898
1268
  const cognitoToken = authHeader.slice("Bearer ".length);
899
1269
  let sessionToken;
@@ -906,57 +1276,195 @@ proxy.post("/:ns/:proc{.*}", async (c) => {
906
1276
  }
907
1277
  return c.json({ error: "Failed to connect to gateway" }, 502);
908
1278
  }
909
- let body = {};
910
- try {
911
- body = await c.req.json();
912
- } catch {
913
- }
914
1279
  const gatewayUrl2 = process.env["GATEWAY_URL"].replace(/\/$/, "");
915
- const res = await fetch(`${gatewayUrl2}/tools/${ns}/${proc}`, {
916
- method: "POST",
917
- headers: {
918
- "Content-Type": "application/json",
919
- Authorization: `Bearer ${sessionToken}`
920
- },
921
- body: JSON.stringify(body)
1280
+ const res = await fetch(`${gatewayUrl2}/tools`, {
1281
+ headers: { Authorization: `Bearer ${sessionToken}` }
922
1282
  });
923
1283
  if (res.status === 401) {
924
1284
  evictGatewaySession(claims.sub);
925
1285
  return c.json({ error: "Gateway authentication failed" }, 502);
926
1286
  }
927
- const responseData = await res.json();
928
- 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
+ });
929
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
+ );
930
1424
 
931
1425
  // src/app.ts
932
1426
  function createChatApp() {
933
- const app2 = new Hono6();
1427
+ const app2 = new Hono9();
934
1428
  app2.route("/", health);
935
- const api = app2.basePath("/api");
936
- 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);
937
1439
  api.route("/chat", chatRoute);
938
1440
  api.route("/edit", editRoute);
939
1441
  api.route("/services", services);
940
1442
  api.route("/proxy", proxy);
1443
+ api.route("/workspaces", workspacesRoute);
1444
+ app2.route("/api", api);
941
1445
  return app2;
942
1446
  }
943
1447
 
944
1448
  // src/env.ts
945
- import { z as z3 } from "zod";
946
- var envSchema = z3.object({
947
- NODE_ENV: z3.enum(["development", "production", "test"]).default("development"),
948
- PORT: z3.coerce.number().default(3001),
949
- COGNITO_USER_POOL_ID: z3.string().min(1),
950
- COGNITO_CLIENT_ID: z3.string().min(1),
951
- AWS_REGION: z3.string().default("us-east-1"),
952
- WORKSPACE_TABLE_NAME: z3.string().min(1),
953
- MEMBERSHIPS_TABLE_NAME: z3.string().min(1),
954
- OPENROUTER_SECRET_ARN: z3.string().min(1),
955
- 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(),
956
1464
  // PostHog prompt management (optional — falls back to code prompts when absent)
957
- POSTHOG_PROJECT_API_KEY: z3.string().optional(),
958
- POSTHOG_PERSONAL_API_KEY: z3.string().optional(),
959
- 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")
960
1468
  });
961
1469
  function parseEnv(raw) {
962
1470
  return envSchema.parse(raw);