@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/.turbo/turbo-build.log +7 -7
- package/dist/index.d.ts +9 -3
- package/dist/index.js +405 -154
- package/dist/index.js.map +1 -1
- package/dist/lambda.js +405 -154
- package/dist/lambda.js.map +1 -1
- package/package.json +2 -1
- package/src/app.ts +18 -7
- package/src/env.ts +5 -1
- package/src/middleware/workspace.ts +55 -55
- package/src/routes/vfs.ts +369 -0
- package/src/routes/workspace.ts +41 -0
- package/test/app.test.ts +2 -3
- package/test/middleware/workspace.test.ts +104 -54
- package/test/routes/vfs.test.ts +436 -0
- package/test/routes/workspace.test.ts +107 -0
- package/src/routes/proxy.ts +0 -60
- package/src/routes/services.ts +0 -82
- package/test/routes/proxy.test.ts +0 -133
- package/test/routes/services.test.ts +0 -128
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
|
-
|
|
90
|
-
|
|
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
|
|
105
|
-
|
|
96
|
+
function evictWorkspaceCache(userSub) {
|
|
97
|
+
membershipCache.delete(userSub);
|
|
106
98
|
}
|
|
107
|
-
async function
|
|
99
|
+
async function resolveWorkspaceId(userSub) {
|
|
108
100
|
const now = Date.now();
|
|
109
|
-
const cached =
|
|
110
|
-
if (cached && now - cached.fetchedAt <
|
|
111
|
-
return cached.
|
|
101
|
+
const cached = membershipCache.get(userSub);
|
|
102
|
+
if (cached && now - cached.fetchedAt < MEMBERSHIP_CACHE_TTL_MS) {
|
|
103
|
+
return cached.workspaceId;
|
|
112
104
|
}
|
|
113
|
-
const
|
|
105
|
+
const usersResult = await getDdb2().send(
|
|
114
106
|
new GetCommand2({
|
|
115
|
-
TableName:
|
|
116
|
-
Key: { userSub }
|
|
107
|
+
TableName: process.env["USERS_TABLE_NAME"],
|
|
108
|
+
Key: { sub: userSub },
|
|
109
|
+
ProjectionExpression: "activeWorkspaceId"
|
|
117
110
|
})
|
|
118
111
|
);
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
await getDdb2().send(
|
|
125
|
-
new
|
|
126
|
-
TableName:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
954
|
-
|
|
955
|
-
|
|
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
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
-
|
|
1061
|
+
const entries = Array.from(seen.entries()).map(([name, isDirectory]) => ({
|
|
1062
|
+
name,
|
|
1063
|
+
isDirectory
|
|
1064
|
+
}));
|
|
1065
|
+
return c.json(entries);
|
|
970
1066
|
}
|
|
971
|
-
|
|
972
|
-
|
|
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
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
},
|
|
983
|
-
|
|
984
|
-
})
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
|
1000
|
-
const
|
|
1001
|
-
|
|
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
|
|
1004
|
-
|
|
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
|
|
1007
|
-
|
|
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
|
-
|
|
1154
|
+
throw err;
|
|
1010
1155
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
if (
|
|
1016
|
-
|
|
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
|
-
|
|
1020
|
-
|
|
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
|
-
|
|
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/
|
|
1186
|
+
// src/routes/workspace.ts
|
|
1035
1187
|
import { DynamoDBClient as DynamoDBClient4 } from "@aws-sdk/client-dynamodb";
|
|
1036
|
-
import {
|
|
1037
|
-
import {
|
|
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
|
|
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
|
|
1106
|
-
|
|
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
|
-
|
|
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(),
|