@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/.turbo/turbo-build.log +7 -7
- package/dist/index.d.ts +9 -0
- package/dist/index.js +627 -119
- package/dist/index.js.map +1 -1
- package/dist/lambda.js +627 -119
- package/dist/lambda.js.map +1 -1
- package/package.json +2 -1
- package/src/app.ts +23 -6
- package/src/env.ts +5 -0
- package/src/gateway-session.ts +38 -1
- package/src/middleware/workspace.ts +38 -1
- package/src/routes/chat.ts +28 -8
- package/src/routes/vfs.ts +369 -0
- package/src/routes/workspace.ts +41 -0
- package/src/routes/workspaces.ts +89 -0
- package/src/session.ts +80 -0
- package/test/app.test.ts +2 -0
- package/test/gateway-session.test.ts +79 -0
- package/test/middleware/workspace.test.ts +79 -8
- package/test/routes/chat.test.ts +99 -0
- package/test/routes/vfs.test.ts +436 -0
- package/test/routes/workspace.test.ts +107 -0
- package/test/routes/workspaces.test.ts +164 -0
- package/test/session.test.ts +56 -0
package/dist/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
|
|
5
|
+
import { Hono as Hono9 } from "hono";
|
|
6
6
|
|
|
7
7
|
// src/middleware/auth.ts
|
|
8
8
|
import { CognitoJwtVerifier } from "aws-jwt-verify";
|
|
@@ -32,11 +32,11 @@ var authMiddleware = async (c, next) => {
|
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
// src/middleware/
|
|
35
|
+
// src/middleware/plan.ts
|
|
36
36
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
37
|
-
import { DynamoDBDocumentClient,
|
|
38
|
-
var
|
|
39
|
-
var
|
|
37
|
+
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
|
|
38
|
+
var WORKSPACE_CACHE_TTL_MS = 6e4;
|
|
39
|
+
var workspaceCache = /* @__PURE__ */ new Map();
|
|
40
40
|
var ddbClient = null;
|
|
41
41
|
function getDdb() {
|
|
42
42
|
if (!ddbClient) {
|
|
@@ -46,57 +46,13 @@ function getDdb() {
|
|
|
46
46
|
}
|
|
47
47
|
return ddbClient;
|
|
48
48
|
}
|
|
49
|
-
async function resolveWorkspaceId(userSub) {
|
|
50
|
-
const now = Date.now();
|
|
51
|
-
const cached = membershipCache.get(userSub);
|
|
52
|
-
if (cached && now - cached.fetchedAt < MEMBERSHIP_CACHE_TTL_MS) {
|
|
53
|
-
return cached.workspaceId;
|
|
54
|
-
}
|
|
55
|
-
const result = await getDdb().send(
|
|
56
|
-
new QueryCommand({
|
|
57
|
-
TableName: process.env["MEMBERSHIPS_TABLE_NAME"],
|
|
58
|
-
IndexName: "ByUserSub",
|
|
59
|
-
KeyConditionExpression: "userSub = :sub",
|
|
60
|
-
ExpressionAttributeValues: { ":sub": userSub },
|
|
61
|
-
Limit: 1
|
|
62
|
-
})
|
|
63
|
-
);
|
|
64
|
-
const item = result.Items?.[0];
|
|
65
|
-
if (!item) return null;
|
|
66
|
-
membershipCache.set(userSub, { workspaceId: item.workspaceId, fetchedAt: now });
|
|
67
|
-
return item.workspaceId;
|
|
68
|
-
}
|
|
69
|
-
var workspaceMiddleware = async (c, next) => {
|
|
70
|
-
const claims = c.get("claims");
|
|
71
|
-
const workspaceId = await resolveWorkspaceId(claims.sub);
|
|
72
|
-
if (!workspaceId) {
|
|
73
|
-
return c.json({ error: "No workspace membership" }, 403);
|
|
74
|
-
}
|
|
75
|
-
c.set("workspaceId", workspaceId);
|
|
76
|
-
return next();
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// src/middleware/plan.ts
|
|
80
|
-
import { DynamoDBClient as DynamoDBClient2 } from "@aws-sdk/client-dynamodb";
|
|
81
|
-
import { DynamoDBDocumentClient as DynamoDBDocumentClient2, GetCommand } from "@aws-sdk/lib-dynamodb";
|
|
82
|
-
var WORKSPACE_CACHE_TTL_MS = 6e4;
|
|
83
|
-
var workspaceCache = /* @__PURE__ */ new Map();
|
|
84
|
-
var ddbClient2 = null;
|
|
85
|
-
function getDdb2() {
|
|
86
|
-
if (!ddbClient2) {
|
|
87
|
-
ddbClient2 = DynamoDBDocumentClient2.from(
|
|
88
|
-
new DynamoDBClient2({ region: process.env["AWS_REGION"] })
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
return ddbClient2;
|
|
92
|
-
}
|
|
93
49
|
async function getWorkspace(workspaceId) {
|
|
94
50
|
const now = Date.now();
|
|
95
51
|
const cached = workspaceCache.get(workspaceId);
|
|
96
52
|
if (cached && now - cached.fetchedAt < WORKSPACE_CACHE_TTL_MS) {
|
|
97
53
|
return cached.workspace;
|
|
98
54
|
}
|
|
99
|
-
const result = await
|
|
55
|
+
const result = await getDdb().send(
|
|
100
56
|
new GetCommand({
|
|
101
57
|
TableName: process.env["WORKSPACE_TABLE_NAME"],
|
|
102
58
|
Key: { workspaceId }
|
|
@@ -123,10 +79,75 @@ var planMiddleware = async (c, next) => {
|
|
|
123
79
|
return next();
|
|
124
80
|
};
|
|
125
81
|
|
|
126
|
-
// src/
|
|
127
|
-
import {
|
|
128
|
-
|
|
129
|
-
|
|
82
|
+
// src/middleware/workspace.ts
|
|
83
|
+
import { DynamoDBClient as DynamoDBClient2 } from "@aws-sdk/client-dynamodb";
|
|
84
|
+
import { DynamoDBDocumentClient as DynamoDBDocumentClient2, GetCommand as GetCommand2, QueryCommand } from "@aws-sdk/lib-dynamodb";
|
|
85
|
+
var MEMBERSHIP_CACHE_TTL_MS = 3e5;
|
|
86
|
+
var membershipCache = /* @__PURE__ */ new Map();
|
|
87
|
+
var ddbClient2 = null;
|
|
88
|
+
function getDdb2() {
|
|
89
|
+
if (!ddbClient2) {
|
|
90
|
+
ddbClient2 = DynamoDBDocumentClient2.from(
|
|
91
|
+
new DynamoDBClient2({ region: process.env["AWS_REGION"] })
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return ddbClient2;
|
|
95
|
+
}
|
|
96
|
+
function evictWorkspaceCache(userSub) {
|
|
97
|
+
membershipCache.delete(userSub);
|
|
98
|
+
}
|
|
99
|
+
async function resolveWorkspaceId(userSub) {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const cached = membershipCache.get(userSub);
|
|
102
|
+
if (cached && now - cached.fetchedAt < MEMBERSHIP_CACHE_TTL_MS) {
|
|
103
|
+
return cached.workspaceId;
|
|
104
|
+
}
|
|
105
|
+
const usersResult = await getDdb2().send(
|
|
106
|
+
new GetCommand2({
|
|
107
|
+
TableName: process.env["USERS_TABLE_NAME"],
|
|
108
|
+
Key: { sub: userSub },
|
|
109
|
+
ProjectionExpression: "activeWorkspaceId"
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
const userItem = usersResult.Item;
|
|
113
|
+
if (userItem?.activeWorkspaceId) {
|
|
114
|
+
membershipCache.set(userSub, { workspaceId: userItem.activeWorkspaceId, fetchedAt: now });
|
|
115
|
+
return userItem.activeWorkspaceId;
|
|
116
|
+
}
|
|
117
|
+
const result = await getDdb2().send(
|
|
118
|
+
new QueryCommand({
|
|
119
|
+
TableName: process.env["MEMBERSHIPS_TABLE_NAME"],
|
|
120
|
+
IndexName: "ByUserSub",
|
|
121
|
+
KeyConditionExpression: "userSub = :sub",
|
|
122
|
+
ExpressionAttributeValues: { ":sub": userSub },
|
|
123
|
+
Limit: 1
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
const item = result.Items?.[0];
|
|
127
|
+
if (!item) return null;
|
|
128
|
+
membershipCache.set(userSub, { workspaceId: item.workspaceId, fetchedAt: now });
|
|
129
|
+
return item.workspaceId;
|
|
130
|
+
}
|
|
131
|
+
async function listWorkspaceMemberships(userSub) {
|
|
132
|
+
const result = await getDdb2().send(
|
|
133
|
+
new QueryCommand({
|
|
134
|
+
TableName: process.env["MEMBERSHIPS_TABLE_NAME"],
|
|
135
|
+
IndexName: "ByUserSub",
|
|
136
|
+
KeyConditionExpression: "userSub = :sub",
|
|
137
|
+
ExpressionAttributeValues: { ":sub": userSub }
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
return (result.Items ?? []).map((item) => item.workspaceId);
|
|
141
|
+
}
|
|
142
|
+
var workspaceMiddleware = async (c, next) => {
|
|
143
|
+
const claims = c.get("claims");
|
|
144
|
+
const workspaceId = await resolveWorkspaceId(claims.sub);
|
|
145
|
+
if (!workspaceId) {
|
|
146
|
+
return c.json({ error: "No workspace membership" }, 403);
|
|
147
|
+
}
|
|
148
|
+
c.set("workspaceId", workspaceId);
|
|
149
|
+
return next();
|
|
150
|
+
};
|
|
130
151
|
|
|
131
152
|
// src/routes/chat.ts
|
|
132
153
|
import { withTracing } from "@posthog/ai";
|
|
@@ -137,7 +158,7 @@ import {
|
|
|
137
158
|
stepCountIs,
|
|
138
159
|
jsonSchema
|
|
139
160
|
} from "ai";
|
|
140
|
-
import { Hono
|
|
161
|
+
import { Hono } from "hono";
|
|
141
162
|
import { z } from "zod";
|
|
142
163
|
|
|
143
164
|
// src/fallback-prompts.ts
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
|
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
|
|
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/
|
|
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
|
|
851
|
-
|
|
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
|
-
|
|
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
|
-
|
|
875
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
886
|
-
|
|
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/
|
|
890
|
-
import {
|
|
891
|
-
|
|
892
|
-
|
|
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
|
|
916
|
-
|
|
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
|
-
|
|
928
|
-
|
|
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
|
|
1427
|
+
const app2 = new Hono9();
|
|
934
1428
|
app2.route("/", health);
|
|
935
|
-
const
|
|
936
|
-
|
|
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
|
|
946
|
-
var envSchema =
|
|
947
|
-
NODE_ENV:
|
|
948
|
-
PORT:
|
|
949
|
-
COGNITO_USER_POOL_ID:
|
|
950
|
-
COGNITO_CLIENT_ID:
|
|
951
|
-
AWS_REGION:
|
|
952
|
-
WORKSPACE_TABLE_NAME:
|
|
953
|
-
MEMBERSHIPS_TABLE_NAME:
|
|
954
|
-
|
|
955
|
-
|
|
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:
|
|
958
|
-
POSTHOG_PERSONAL_API_KEY:
|
|
959
|
-
POSTHOG_HOST:
|
|
1465
|
+
POSTHOG_PROJECT_API_KEY: z4.string().optional(),
|
|
1466
|
+
POSTHOG_PERSONAL_API_KEY: z4.string().optional(),
|
|
1467
|
+
POSTHOG_HOST: z4.string().default("https://us.posthog.com")
|
|
960
1468
|
});
|
|
961
1469
|
function parseEnv(raw) {
|
|
962
1470
|
return envSchema.parse(raw);
|