@aprovan/chat-backend 0.1.0-dev.4d82df8 → 0.1.0-dev.78c0b14

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/test/app.test.ts CHANGED
@@ -20,8 +20,6 @@ vi.mock("@aws-sdk/lib-dynamodb", () => ({
20
20
  },
21
21
  QueryCommand: vi.fn((input) => input),
22
22
  GetCommand: vi.fn((input) => input),
23
- PutCommand: vi.fn((input) => input),
24
- BatchGetCommand: vi.fn((input) => input),
25
23
  }));
26
24
 
27
25
  beforeAll(() => {
@@ -30,7 +28,6 @@ beforeAll(() => {
30
28
  process.env["AWS_REGION"] = "us-east-1";
31
29
  process.env["WORKSPACE_TABLE_NAME"] = "test-workspaces";
32
30
  process.env["MEMBERSHIPS_TABLE_NAME"] = "test-memberships";
33
- process.env["USER_SESSIONS_TABLE_NAME"] = "test-user-sessions";
34
31
  });
35
32
 
36
33
  describe("chat app", () => {
@@ -3,9 +3,6 @@ import {
3
3
  getGatewaySession,
4
4
  evictGatewaySession,
5
5
  resetGatewaySessionCache,
6
- getCachedTools,
7
- setCachedTools,
8
- evictCachedTools,
9
6
  GatewaySessionError,
10
7
  } from "../src/gateway-session";
11
8
  import type { CognitoAccessTokenPayload } from "aws-jwt-verify/jwt-model";
@@ -132,80 +129,4 @@ describe("getGatewaySession", () => {
132
129
 
133
130
  expect(fetchSpy).toHaveBeenCalledTimes(2);
134
131
  });
135
-
136
- it("retries once after 200ms on 5xx and returns session if retry succeeds", async () => {
137
- const fetchSpy = vi.spyOn(globalThis, "fetch")
138
- .mockResolvedValueOnce(new Response("Service Unavailable", { status: 503 }))
139
- .mockResolvedValueOnce(
140
- new Response(JSON.stringify({ workspace_id: WORKSPACE_ID }), { status: 200 }),
141
- );
142
-
143
- const session = await getGatewaySession(makeClaims(), WORKSPACE_ID, COGNITO_TOKEN);
144
- expect(session.token).toBe(COGNITO_TOKEN);
145
- expect(fetchSpy).toHaveBeenCalledTimes(2);
146
- });
147
-
148
- it("throws GatewaySessionError on persistent 5xx after retry", async () => {
149
- vi.spyOn(globalThis, "fetch").mockResolvedValue(
150
- new Response("Service Unavailable", { status: 503 }),
151
- );
152
-
153
- await expect(
154
- getGatewaySession(makeClaims(), WORKSPACE_ID, COGNITO_TOKEN),
155
- ).rejects.toThrow(GatewaySessionError);
156
- });
157
-
158
- it("retries once after 200ms on network error and returns session if retry succeeds", async () => {
159
- const fetchSpy = vi.spyOn(globalThis, "fetch")
160
- .mockRejectedValueOnce(new TypeError("fetch failed"))
161
- .mockResolvedValueOnce(
162
- new Response(JSON.stringify({ workspace_id: WORKSPACE_ID }), { status: 200 }),
163
- );
164
-
165
- const session = await getGatewaySession(makeClaims(), WORKSPACE_ID, COGNITO_TOKEN);
166
- expect(session.token).toBe(COGNITO_TOKEN);
167
- expect(fetchSpy).toHaveBeenCalledTimes(2);
168
- });
169
- });
170
-
171
- describe("tool cache", () => {
172
- beforeEach(() => {
173
- resetGatewaySessionCache();
174
- });
175
-
176
- afterEach(() => {
177
- resetGatewaySessionCache();
178
- });
179
-
180
- it("getCachedTools returns undefined when nothing cached", () => {
181
- expect(getCachedTools("sub-123")).toBeUndefined();
182
- });
183
-
184
- it("setCachedTools stores tools and getCachedTools retrieves them", () => {
185
- const tools = [{ name: "github_repos_list" }];
186
- setCachedTools("sub-123", tools);
187
- expect(getCachedTools("sub-123")).toEqual(tools);
188
- });
189
-
190
- it("evictCachedTools removes tools for a sub", () => {
191
- setCachedTools("sub-123", [{ name: "github_repos_list" }]);
192
- evictCachedTools("sub-123");
193
- expect(getCachedTools("sub-123")).toBeUndefined();
194
- });
195
-
196
- it("evictGatewaySession also evicts the tool cache for that sub", async () => {
197
- vi.spyOn(globalThis, "fetch").mockResolvedValue(
198
- new Response(JSON.stringify({ workspace_id: WORKSPACE_ID }), { status: 200 }),
199
- );
200
- await getGatewaySession(makeClaims(), WORKSPACE_ID, COGNITO_TOKEN);
201
- setCachedTools(USER_SUB, [{ name: "tool" }]);
202
- evictGatewaySession(USER_SUB);
203
- expect(getCachedTools(USER_SUB)).toBeUndefined();
204
- });
205
-
206
- it("resetGatewaySessionCache also clears the tool cache", () => {
207
- setCachedTools("sub-a", [{ name: "tool" }]);
208
- resetGatewaySessionCache();
209
- expect(getCachedTools("sub-a")).toBeUndefined();
210
- });
211
132
  });
@@ -3,6 +3,7 @@ import { Hono } from "hono";
3
3
  import type { AppVariables } from "../../src/types";
4
4
  import type { CognitoAccessTokenPayload } from "aws-jwt-verify/jwt-model";
5
5
 
6
+ // Mock the DDB client before importing the middleware
6
7
  vi.mock("@aws-sdk/client-dynamodb", () => ({
7
8
  DynamoDBClient: vi.fn(() => ({})),
8
9
  }));
@@ -13,110 +14,88 @@ vi.mock("@aws-sdk/lib-dynamodb", () => ({
13
14
  from: vi.fn(() => ({ send: mockSend })),
14
15
  },
15
16
  QueryCommand: vi.fn((input) => input),
16
- GetCommand: vi.fn((input) => input),
17
- PutCommand: vi.fn((input) => input),
18
17
  }));
19
18
 
20
- const {
21
- workspaceMiddleware,
22
- listWorkspaceMemberships,
23
- resetMembershipCache,
24
- } = await import("../../src/middleware/workspace");
25
- const { resetSessionCache } = await import("../../src/session");
19
+ const { workspaceMiddleware, resolveWorkspaceId } = await import(
20
+ "../../src/middleware/workspace"
21
+ );
26
22
 
27
- const MEMBERSHIPS_TABLE = "gateway-prd-use1-memberships";
28
- const SESSIONS_TABLE = "test-user-sessions";
23
+ const fakeClaims = {
24
+ sub: "user-sub-123",
25
+ } as unknown as CognitoAccessTokenPayload;
29
26
 
30
- type MockCommand = { TableName?: string };
31
-
32
- function buildApp(userSub: string) {
33
- const fakeClaims = { sub: userSub } as unknown as CognitoAccessTokenPayload;
27
+ function buildApp() {
34
28
  const app = new Hono<{ Variables: AppVariables }>();
35
29
  app.use("/protected", async (c, next) => {
36
30
  c.set("claims", fakeClaims);
37
31
  await next();
38
32
  });
39
33
  app.use("/protected", workspaceMiddleware);
40
- app.get("/protected", (c) => c.json({ workspaceId: c.get("workspaceId") }));
34
+ app.get("/protected", (c) =>
35
+ c.json({ workspaceId: c.get("workspaceId") }),
36
+ );
41
37
  return app;
42
38
  }
43
39
 
44
40
  describe("workspaceMiddleware", () => {
45
41
  beforeEach(() => {
46
42
  vi.clearAllMocks();
47
- resetMembershipCache();
48
- resetSessionCache();
49
- process.env["MEMBERSHIPS_TABLE_NAME"] = MEMBERSHIPS_TABLE;
50
- process.env["USER_SESSIONS_TABLE_NAME"] = SESSIONS_TABLE;
43
+ process.env["MEMBERSHIPS_TABLE_NAME"] = "gateway-prd-use1-memberships";
51
44
  process.env["AWS_REGION"] = "us-east-1";
45
+ // Clear the module-level cache between tests by resetting the module state.
46
+ // resolveWorkspaceId memoizes per sub; tests use unique subs or clear cache.
52
47
  });
53
48
 
54
- it("uses session activeWorkspaceId when membership is valid", async () => {
55
- mockSend.mockImplementation((cmd: MockCommand) => {
56
- if (cmd.TableName === SESSIONS_TABLE) {
57
- return Promise.resolve({ Item: { activeWorkspaceId: "ws-b" } });
58
- }
59
- return Promise.resolve({ Items: [{ workspaceId: "ws-a" }, { workspaceId: "ws-b" }] });
49
+ it("resolves workspaceId from DDB and sets it on context", async () => {
50
+ mockSend.mockResolvedValueOnce({
51
+ Items: [{ workspaceId: "ws-abc", userSub: "user-sub-456" }],
60
52
  });
61
53
 
62
- const app = buildApp("user-session");
63
- const res = await app.request("/protected");
64
- expect(res.status).toBe(200);
65
- const body = await res.json();
66
- expect(body.workspaceId).toBe("ws-b");
67
- });
68
-
69
- it("falls back to first membership when no session preference exists", async () => {
70
- mockSend.mockImplementation((cmd: MockCommand) => {
71
- if (cmd.TableName === SESSIONS_TABLE) {
72
- return Promise.resolve({ Item: undefined });
73
- }
74
- return Promise.resolve({ Items: [{ workspaceId: "ws-first" }] });
54
+ const app = new Hono<{ Variables: AppVariables }>();
55
+ const localClaims = { sub: "user-sub-456" } as unknown as CognitoAccessTokenPayload;
56
+ app.use("/protected", async (c, next) => {
57
+ c.set("claims", localClaims);
58
+ await next();
75
59
  });
60
+ app.use("/protected", workspaceMiddleware);
61
+ app.get("/protected", (c) => c.json({ workspaceId: c.get("workspaceId") }));
76
62
 
77
- const app = buildApp("user-no-session");
78
63
  const res = await app.request("/protected");
79
64
  expect(res.status).toBe(200);
80
65
  const body = await res.json();
81
- expect(body.workspaceId).toBe("ws-first");
66
+ expect(body).toEqual({ workspaceId: "ws-abc" });
82
67
  });
83
68
 
84
- it("falls back to first membership when session workspace is not in memberships", async () => {
85
- mockSend.mockImplementation((cmd: MockCommand) => {
86
- if (cmd.TableName === SESSIONS_TABLE) {
87
- return Promise.resolve({ Item: { activeWorkspaceId: "ws-stale" } });
88
- }
89
- return Promise.resolve({ Items: [{ workspaceId: "ws-a" }] });
90
- });
91
-
92
- const app = buildApp("user-stale");
93
- const res = await app.request("/protected");
94
- expect(res.status).toBe(200);
95
- const body = await res.json();
96
- expect(body.workspaceId).toBe("ws-a");
97
- });
69
+ it("returns 403 when no membership exists", async () => {
70
+ mockSend.mockResolvedValueOnce({ Items: [] });
98
71
 
99
- it("returns 403 when user has no workspace memberships", async () => {
100
- mockSend.mockImplementation((cmd: MockCommand) => {
101
- if (cmd.TableName === SESSIONS_TABLE) {
102
- return Promise.resolve({ Item: undefined });
103
- }
104
- return Promise.resolve({ Items: [] });
72
+ const app = new Hono<{ Variables: AppVariables }>();
73
+ const localClaims = {
74
+ sub: "user-no-ws",
75
+ } as unknown as CognitoAccessTokenPayload;
76
+ app.use("/protected", async (c, next) => {
77
+ c.set("claims", localClaims);
78
+ await next();
105
79
  });
80
+ app.use("/protected", workspaceMiddleware);
81
+ app.get("/protected", (c) => c.json({ workspaceId: c.get("workspaceId") }));
106
82
 
107
- const app = buildApp("user-no-ws");
108
83
  const res = await app.request("/protected");
109
84
  expect(res.status).toBe(403);
110
85
  const body = await res.json();
111
86
  expect(body).toMatchObject({ error: "No workspace membership" });
112
87
  });
113
88
 
114
- it("uses membership cache on subsequent calls for the same sub", async () => {
115
- mockSend.mockResolvedValue({ Items: [{ workspaceId: "ws-cached" }] });
89
+ it("uses cache on subsequent calls for the same sub", async () => {
90
+ mockSend.mockResolvedValueOnce({
91
+ Items: [{ workspaceId: "ws-cached", userSub: "user-cached" }],
92
+ });
93
+
94
+ // First call — hits DDB
95
+ await resolveWorkspaceId("user-cached");
96
+ // Second call — should use cache
97
+ await resolveWorkspaceId("user-cached");
116
98
 
117
- await listWorkspaceMemberships("user-cached-q");
118
- await listWorkspaceMemberships("user-cached-q");
119
- // Only one DDB send for memberships (cache hit on second call)
120
99
  expect(mockSend).toHaveBeenCalledTimes(1);
121
100
  });
122
101
  });
@@ -12,8 +12,6 @@ const {
12
12
  mockProviderFactory,
13
13
  mockGetGatewaySession,
14
14
  mockEvictGatewaySession,
15
- mockGetCachedTools,
16
- mockSetCachedTools,
17
15
  mockGetPrompt,
18
16
  mockCompilePrompt,
19
17
  mockGetPostHogClient,
@@ -27,8 +25,6 @@ const {
27
25
  mockProviderFactory,
28
26
  mockGetGatewaySession: vi.fn(),
29
27
  mockEvictGatewaySession: vi.fn(),
30
- mockGetCachedTools: vi.fn().mockReturnValue(undefined),
31
- mockSetCachedTools: vi.fn(),
32
28
  mockGetPrompt: vi.fn().mockResolvedValue({
33
29
  source: "code_fallback",
34
30
  prompt: "System: {{compilers}} {{tool_docs}}",
@@ -55,8 +51,6 @@ vi.mock("../../src/providers/openrouter.js", () => ({
55
51
  vi.mock("../../src/gateway-session.js", () => ({
56
52
  getGatewaySession: mockGetGatewaySession,
57
53
  evictGatewaySession: mockEvictGatewaySession,
58
- getCachedTools: mockGetCachedTools,
59
- setCachedTools: mockSetCachedTools,
60
54
  GatewaySessionError: class GatewaySessionError extends Error {
61
55
  status: number;
62
56
  constructor(status: number, message: string) {
@@ -188,8 +182,6 @@ describe("POST /chat", () => {
188
182
  mockProviderFactory.mockReturnValue(mockModel);
189
183
  mockCreateOpenRouterProvider.mockReturnValue(mockProviderFactory);
190
184
  mockGetOpenRouterKey.mockResolvedValue("test-key");
191
- mockGetCachedTools.mockReturnValue(undefined);
192
- mockSetCachedTools.mockReset();
193
185
 
194
186
  // Reset prompt + tool-docs mocks to defaults
195
187
  mockGetPrompt.mockResolvedValue({
@@ -307,55 +299,6 @@ describe("POST /chat", () => {
307
299
  expect(mockDoStream).toHaveBeenCalledTimes(1);
308
300
  });
309
301
 
310
- it("returns 403 when requested model is not in workspace.limits.maxModels", async () => {
311
- const app = buildApp();
312
- const res = await app.request("/chat", {
313
- method: "POST",
314
- headers: validHeaders,
315
- body: JSON.stringify({
316
- id: "chat-1",
317
- messages: [{ role: "user", parts: [{ type: "text", text: "Hello" }], id: "msg-1" }],
318
- trigger: "submit-message",
319
- model: "anthropic/claude-opus-4",
320
- }),
321
- });
322
- expect(res.status).toBe(403);
323
- const body = await res.json();
324
- expect(body).toMatchObject({ error: expect.stringContaining("Model not allowed") });
325
- expect(mockDoStream).not.toHaveBeenCalled();
326
- });
327
-
328
- it("uses the requested model when it is in workspace.limits.maxModels", async () => {
329
- mockDoStream.mockResolvedValueOnce({
330
- stream: makeSuccessStream(),
331
- rawResponse: { headers: {} },
332
- });
333
-
334
- const proWorkspace: WorkspaceItem = {
335
- ...fakeWorkspace,
336
- plan: "pro",
337
- limits: {
338
- ...fakeWorkspace.limits,
339
- maxModels: ["openrouter/auto", "anthropic/claude-opus-4"],
340
- },
341
- };
342
-
343
- const app = buildApp(proWorkspace);
344
- const res = await app.request("/chat", {
345
- method: "POST",
346
- headers: validHeaders,
347
- body: JSON.stringify({
348
- id: "chat-1",
349
- messages: [{ role: "user", parts: [{ type: "text", text: "Hello" }], id: "msg-1" }],
350
- trigger: "submit-message",
351
- model: "anthropic/claude-opus-4",
352
- }),
353
- });
354
-
355
- expect(res.status).toBe(200);
356
- expect(mockProviderFactory).toHaveBeenCalledWith("anthropic/claude-opus-4");
357
- });
358
-
359
302
  it("selects model from workspace.limits.maxModels[0]", async () => {
360
303
  mockDoStream.mockResolvedValueOnce({
361
304
  stream: makeSuccessStream(),
@@ -541,47 +484,5 @@ describe("POST /chat", () => {
541
484
  expect.any(String),
542
485
  );
543
486
  });
544
-
545
- it("uses cached tools when available — skips gateway fetch", async () => {
546
- mockGetCachedTools.mockReturnValue(MOCK_TOOLS_RESPONSE.tools);
547
- mockDoStream.mockResolvedValueOnce({
548
- stream: makeSuccessStream(),
549
- rawResponse: { headers: {} },
550
- });
551
-
552
- const app = buildApp();
553
- const res = await app.request("/chat", {
554
- method: "POST",
555
- headers: validHeaders,
556
- body: validBody,
557
- });
558
-
559
- expect(res.status).toBe(200);
560
- // No gateway tools fetch should have occurred
561
- expect(mockFetch).not.toHaveBeenCalled();
562
- });
563
-
564
- it("caches fetched tools via setCachedTools", async () => {
565
- mockFetch.mockResolvedValueOnce(
566
- new Response(JSON.stringify(MOCK_TOOLS_RESPONSE), { status: 200 }),
567
- );
568
- mockDoStream.mockResolvedValueOnce({
569
- stream: makeSuccessStream(),
570
- rawResponse: { headers: {} },
571
- });
572
-
573
- const app = buildApp();
574
- await app.request("/chat", {
575
- method: "POST",
576
- headers: validHeaders,
577
- body: validBody,
578
- });
579
-
580
- expect(mockSetCachedTools).toHaveBeenCalledOnce();
581
- expect(mockSetCachedTools).toHaveBeenCalledWith(
582
- fakeClaims.sub,
583
- MOCK_TOOLS_RESPONSE.tools,
584
- );
585
- });
586
487
  });
587
488
  });
@@ -1,89 +0,0 @@
1
- import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
- import { BatchGetCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
3
- import { zValidator } from "@hono/zod-validator";
4
- import { Hono } from "hono";
5
- import { z } from "zod";
6
- import { listWorkspaceMemberships } from "../middleware/workspace.js";
7
- import { getSessionWorkspaceId, setSessionWorkspaceId } from "../session.js";
8
- import type { AppVariables, WorkspaceItem } from "../types.js";
9
-
10
- let ddbClient: DynamoDBDocumentClient | null = null;
11
-
12
- function getDdb() {
13
- if (!ddbClient) {
14
- ddbClient = DynamoDBDocumentClient.from(
15
- new DynamoDBClient({ region: process.env["AWS_REGION"] }),
16
- );
17
- }
18
- return ddbClient;
19
- }
20
-
21
- async function batchGetWorkspaces(
22
- workspaceIds: string[],
23
- ): Promise<WorkspaceItem[]> {
24
- if (workspaceIds.length === 0) return [];
25
- const tableName = process.env["WORKSPACE_TABLE_NAME"]!;
26
- const result = await getDdb().send(
27
- new BatchGetCommand({
28
- RequestItems: {
29
- [tableName]: { Keys: workspaceIds.map((id) => ({ workspaceId: id })) },
30
- },
31
- }),
32
- );
33
- return (result.Responses?.[tableName] ?? []) as WorkspaceItem[];
34
- }
35
-
36
- export const workspacesRoute = new Hono<{ Variables: AppVariables }>();
37
-
38
- workspacesRoute.get("/", async (c) => {
39
- const claims = c.get("claims");
40
- const userSub = claims.sub;
41
-
42
- const [workspaceIds, activeWorkspaceId] = await Promise.all([
43
- listWorkspaceMemberships(userSub),
44
- getSessionWorkspaceId(userSub),
45
- ]);
46
-
47
- const workspaces = await batchGetWorkspaces(workspaceIds);
48
-
49
- const effectiveActiveId =
50
- activeWorkspaceId && workspaceIds.includes(activeWorkspaceId)
51
- ? activeWorkspaceId
52
- : (workspaceIds[0] ?? null);
53
-
54
- const sorted = workspaceIds
55
- .map((id) => workspaces.find((w) => w.workspaceId === id))
56
- .filter((w): w is WorkspaceItem => w !== undefined);
57
-
58
- return c.json({
59
- workspaces: sorted.map((w) => ({
60
- workspaceId: w.workspaceId,
61
- name: w.name,
62
- plan: w.plan,
63
- active: w.workspaceId === effectiveActiveId,
64
- })),
65
- activeWorkspaceId: effectiveActiveId,
66
- });
67
- });
68
-
69
- const setActiveSchema = z.object({
70
- workspaceId: z.string().min(1),
71
- });
72
-
73
- workspacesRoute.put(
74
- "/active",
75
- zValidator("json", setActiveSchema),
76
- async (c) => {
77
- const claims = c.get("claims");
78
- const userSub = claims.sub;
79
- const { workspaceId } = c.req.valid("json");
80
-
81
- const workspaceIds = await listWorkspaceMemberships(userSub);
82
- if (!workspaceIds.includes(workspaceId)) {
83
- return c.json({ error: "Not a member of this workspace" }, 403);
84
- }
85
-
86
- await setSessionWorkspaceId(userSub, workspaceId);
87
- return c.json({ activeWorkspaceId: workspaceId });
88
- },
89
- );
package/src/session.ts DELETED
@@ -1,80 +0,0 @@
1
- import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
- import {
3
- DynamoDBDocumentClient,
4
- GetCommand,
5
- PutCommand,
6
- } from "@aws-sdk/lib-dynamodb";
7
-
8
- const SESSION_CACHE_TTL_MS = 15_000;
9
-
10
- interface SessionCacheEntry {
11
- activeWorkspaceId: string | null;
12
- fetchedAt: number;
13
- }
14
-
15
- const sessionCache = new Map<string, SessionCacheEntry>();
16
-
17
- let ddbClient: DynamoDBDocumentClient | null = null;
18
-
19
- function getDdb() {
20
- if (!ddbClient) {
21
- ddbClient = DynamoDBDocumentClient.from(
22
- new DynamoDBClient({ region: process.env["AWS_REGION"] }),
23
- );
24
- }
25
- return ddbClient;
26
- }
27
-
28
- function tableName(): string {
29
- return process.env["USER_SESSIONS_TABLE_NAME"]!;
30
- }
31
-
32
- export async function getSessionWorkspaceId(
33
- userSub: string,
34
- ): Promise<string | null> {
35
- const now = Date.now();
36
- const cached = sessionCache.get(userSub);
37
- if (cached && now - cached.fetchedAt < SESSION_CACHE_TTL_MS) {
38
- return cached.activeWorkspaceId;
39
- }
40
-
41
- const result = await getDdb().send(
42
- new GetCommand({
43
- TableName: tableName(),
44
- Key: { userSub },
45
- }),
46
- );
47
-
48
- const activeWorkspaceId =
49
- (result.Item?.activeWorkspaceId as string | undefined) ?? null;
50
- sessionCache.set(userSub, { activeWorkspaceId, fetchedAt: now });
51
- return activeWorkspaceId;
52
- }
53
-
54
- export async function setSessionWorkspaceId(
55
- userSub: string,
56
- workspaceId: string,
57
- ): Promise<void> {
58
- await getDdb().send(
59
- new PutCommand({
60
- TableName: tableName(),
61
- Item: {
62
- userSub,
63
- activeWorkspaceId: workspaceId,
64
- updatedAt: new Date().toISOString(),
65
- },
66
- }),
67
- );
68
- sessionCache.set(userSub, {
69
- activeWorkspaceId: workspaceId,
70
- fetchedAt: Date.now(),
71
- });
72
- }
73
-
74
- export function clearSessionCache(userSub: string): void {
75
- sessionCache.delete(userSub);
76
- }
77
-
78
- export function resetSessionCache(): void {
79
- sessionCache.clear();
80
- }