@gram-ai/elements 1.27.0 → 1.27.2
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/elements.cjs +1 -1
- package/dist/elements.js +1 -1
- package/dist/hooks/useAuth.d.ts +3 -1
- package/dist/hooks/useSession.d.ts +1 -0
- package/dist/{index-CP-wWZCV.cjs → index-CZ6AZT6Z.cjs} +49 -49
- package/dist/index-CZ6AZT6Z.cjs.map +1 -0
- package/dist/{index-oO5BAmPI.js → index-dPbw_95D.js} +6047 -5995
- package/dist/index-dPbw_95D.js.map +1 -0
- package/dist/lib/token.d.ts +12 -0
- package/dist/lib/token.test.d.ts +1 -0
- package/dist/{profiler-ECh1zoXF.js → profiler-D1ZeY9EE.js} +2 -2
- package/dist/{profiler-ECh1zoXF.js.map → profiler-D1ZeY9EE.js.map} +1 -1
- package/dist/{profiler-CEpc7O5Q.cjs → profiler-iUZI0bb_.cjs} +2 -2
- package/dist/{profiler-CEpc7O5Q.cjs.map → profiler-iUZI0bb_.cjs.map} +1 -1
- package/dist/server/bun.cjs +1 -1
- package/dist/server/bun.cjs.map +1 -1
- package/dist/server/bun.js +1 -1
- package/dist/server/core.cjs +2 -0
- package/dist/server/core.cjs.map +1 -0
- package/dist/{core-Cqad6-xW.js → server/core.js} +5 -5
- package/dist/server/core.js.map +1 -0
- package/dist/server/express.cjs +1 -1
- package/dist/server/express.cjs.map +1 -1
- package/dist/server/express.js +1 -1
- package/dist/server/fastify.cjs +1 -1
- package/dist/server/fastify.cjs.map +1 -1
- package/dist/server/fastify.js +6 -6
- package/dist/server/hono.cjs +1 -1
- package/dist/server/hono.cjs.map +1 -1
- package/dist/server/hono.js +3 -3
- package/dist/server/nextjs.cjs +1 -1
- package/dist/server/nextjs.cjs.map +1 -1
- package/dist/server/nextjs.js +1 -1
- package/dist/server/tanstack-start.cjs +1 -1
- package/dist/server/tanstack-start.cjs.map +1 -1
- package/dist/server/tanstack-start.d.ts +0 -14
- package/dist/server/tanstack-start.js +11 -24
- package/dist/server/tanstack-start.js.map +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/dist/{startRecording-qDCAu4Q0.cjs → startRecording-BssI1M_6.cjs} +2 -2
- package/dist/{startRecording-qDCAu4Q0.cjs.map → startRecording-BssI1M_6.cjs.map} +1 -1
- package/dist/{startRecording-CmZjjJoz.js → startRecording-RyH9ZWER.js} +2 -2
- package/dist/{startRecording-CmZjjJoz.js.map → startRecording-RyH9ZWER.js.map} +1 -1
- package/package.json +6 -1
- package/src/contexts/ElementsProvider.tsx +26 -7
- package/src/hooks/useAuth.ts +51 -3
- package/src/hooks/useSession.ts +7 -10
- package/src/lib/token.test.ts +79 -0
- package/src/lib/token.ts +39 -0
- package/src/server/tanstack-start.ts +14 -47
- package/dist/core-Cqad6-xW.js.map +0 -1
- package/dist/core-DBxmxwCi.cjs +0 -2
- package/dist/core-DBxmxwCi.cjs.map +0 -1
- package/dist/index-CP-wWZCV.cjs.map +0 -1
- package/dist/index-oO5BAmPI.js.map +0 -1
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@gram-ai/elements",
|
|
3
3
|
"description": "Gram Elements is a library of UI primitives for building chat-like experiences for MCP Servers.",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "1.27.
|
|
5
|
+
"version": "1.27.2",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"import": "./dist/server.js",
|
|
16
16
|
"require": "./dist/server.cjs"
|
|
17
17
|
},
|
|
18
|
+
"./server/core": {
|
|
19
|
+
"types": "./dist/server/core.d.ts",
|
|
20
|
+
"import": "./dist/server/core.js",
|
|
21
|
+
"require": "./dist/server/core.cjs"
|
|
22
|
+
},
|
|
18
23
|
"./server/express": {
|
|
19
24
|
"types": "./dist/server/express.d.ts",
|
|
20
25
|
"import": "./dist/server/express.js",
|
|
@@ -25,8 +25,8 @@ import { Plugin } from '@/types/plugins'
|
|
|
25
25
|
import {
|
|
26
26
|
AssistantRuntimeProvider,
|
|
27
27
|
AssistantTool,
|
|
28
|
-
unstable_useRemoteThreadListRuntime as useRemoteThreadListRuntime,
|
|
29
28
|
useAssistantState,
|
|
29
|
+
unstable_useRemoteThreadListRuntime as useRemoteThreadListRuntime,
|
|
30
30
|
} from '@assistant-ui/react'
|
|
31
31
|
import {
|
|
32
32
|
frontendTools as convertFrontendToolsToAISDKTools,
|
|
@@ -36,6 +36,7 @@ import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
|
|
36
36
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
37
37
|
import {
|
|
38
38
|
convertToModelMessages,
|
|
39
|
+
createUIMessageStream,
|
|
39
40
|
smoothStream,
|
|
40
41
|
stepCountIs,
|
|
41
42
|
streamText,
|
|
@@ -52,14 +53,14 @@ import {
|
|
|
52
53
|
useState,
|
|
53
54
|
} from 'react'
|
|
54
55
|
import { useAuth } from '../hooks/useAuth'
|
|
55
|
-
import {
|
|
56
|
-
import { ToolApprovalProvider } from './ToolApprovalContext'
|
|
56
|
+
import { ChatIdContext } from './ChatIdContext'
|
|
57
57
|
import {
|
|
58
58
|
ConnectionStatusProvider,
|
|
59
59
|
useConnectionStatusOptional,
|
|
60
60
|
} from './ConnectionStatusContext'
|
|
61
|
+
import { ElementsContext } from './contexts'
|
|
62
|
+
import { ToolApprovalProvider } from './ToolApprovalContext'
|
|
61
63
|
import { ToolExecutionProvider } from './ToolExecutionContext'
|
|
62
|
-
import { ChatIdContext } from './ChatIdContext'
|
|
63
64
|
|
|
64
65
|
/**
|
|
65
66
|
* Extracts executable tools from frontend tool definitions.
|
|
@@ -158,6 +159,10 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
158
159
|
auth: config.api,
|
|
159
160
|
projectSlug: config.projectSlug,
|
|
160
161
|
})
|
|
162
|
+
|
|
163
|
+
// Ref to access ensureValidHeaders in async transport without stale closures
|
|
164
|
+
const ensureValidHeadersRef = useRef(auth.ensureValidHeaders)
|
|
165
|
+
ensureValidHeadersRef.current = auth.ensureValidHeaders
|
|
161
166
|
const toolApproval = useToolApproval()
|
|
162
167
|
|
|
163
168
|
const [model, setModel] = useState<Model>(
|
|
@@ -266,6 +271,9 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
266
271
|
throw new Error('Session is loading')
|
|
267
272
|
}
|
|
268
273
|
|
|
274
|
+
// Ensure the session token is still valid; refresh if expired
|
|
275
|
+
const validHeaders = await ensureValidHeadersRef.current()
|
|
276
|
+
|
|
269
277
|
// Get chat ID - use the synced remoteId ref first (history mode),
|
|
270
278
|
// fall back to generated ID (non-history mode)
|
|
271
279
|
let chatId = currentRemoteIdRef.current
|
|
@@ -318,7 +326,7 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
318
326
|
|
|
319
327
|
// Include Gram-Chat-ID header for chat persistence and Gram-Environment for environment selection
|
|
320
328
|
const headersWithChatId = {
|
|
321
|
-
...
|
|
329
|
+
...validHeaders,
|
|
322
330
|
'Gram-Chat-ID': chatId,
|
|
323
331
|
'X-Gram-Source': 'elements',
|
|
324
332
|
...config.api?.headers, // We do this after X-Gram-Source so the playground can override it
|
|
@@ -327,6 +335,13 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
327
335
|
}),
|
|
328
336
|
}
|
|
329
337
|
|
|
338
|
+
// Update MCP headers with the (possibly refreshed) session token
|
|
339
|
+
// so mid-stream MCP tool calls use the fresh token
|
|
340
|
+
const freshSession = validHeaders['Gram-Chat-Session']
|
|
341
|
+
if (freshSession) {
|
|
342
|
+
mcpHeaders['Gram-Chat-Session'] = freshSession
|
|
343
|
+
}
|
|
344
|
+
|
|
330
345
|
// Create OpenRouter model (only needed when not using custom model)
|
|
331
346
|
const openRouterModel = usingCustomModel
|
|
332
347
|
? null
|
|
@@ -395,7 +410,12 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
395
410
|
// Mark as connected when stream starts successfully
|
|
396
411
|
connectionStatus?.markConnected()
|
|
397
412
|
|
|
398
|
-
|
|
413
|
+
// This weird construction is necessary to get errors to propagate properly to assistant-ui
|
|
414
|
+
return createUIMessageStream({
|
|
415
|
+
execute: ({ writer }) => {
|
|
416
|
+
writer.merge(result.toUIMessageStream())
|
|
417
|
+
},
|
|
418
|
+
})
|
|
399
419
|
} catch (error) {
|
|
400
420
|
console.error('Error creating stream:', error)
|
|
401
421
|
trackError(error, { source: 'stream-creation' })
|
|
@@ -430,7 +450,6 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
430
450
|
mcpTools,
|
|
431
451
|
getApprovalHelpers,
|
|
432
452
|
apiUrl,
|
|
433
|
-
auth.headers,
|
|
434
453
|
auth.isLoading,
|
|
435
454
|
connectionStatus,
|
|
436
455
|
]
|
package/src/hooks/useAuth.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { useReplayContext } from '@/contexts/ReplayContext'
|
|
2
2
|
import {
|
|
3
3
|
hasExplicitSessionAuth,
|
|
4
|
+
isAnyStaticSession,
|
|
4
5
|
isDangerousApiKeyAuth,
|
|
5
6
|
isStaticSessionAuth,
|
|
6
7
|
isUnifiedFunctionSession,
|
|
7
8
|
isUnifiedStaticSession,
|
|
8
9
|
} from '@/lib/auth'
|
|
9
|
-
import {
|
|
10
|
+
import { getTokenExpiry } from '@/lib/token'
|
|
11
|
+
import { useCallback, useMemo } from 'react'
|
|
10
12
|
import { ApiConfig, GetSessionFn } from '../types'
|
|
11
|
-
import { useSession } from './useSession'
|
|
13
|
+
import { getChatSessionQueryKey, useSession } from './useSession'
|
|
14
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
12
15
|
|
|
13
16
|
declare const __GRAM_API_URL__: string | undefined
|
|
14
17
|
|
|
@@ -16,10 +19,12 @@ export type Auth =
|
|
|
16
19
|
| {
|
|
17
20
|
headers: Record<string, string>
|
|
18
21
|
isLoading: false
|
|
22
|
+
ensureValidHeaders: () => Promise<Record<string, string>>
|
|
19
23
|
}
|
|
20
24
|
| {
|
|
21
25
|
headers?: Record<string, string>
|
|
22
26
|
isLoading: true
|
|
27
|
+
ensureValidHeaders: () => Promise<Record<string, string>>
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
async function defaultGetSession(init: {
|
|
@@ -62,7 +67,7 @@ function createDangerousApiKeySessionFn(
|
|
|
62
67
|
|
|
63
68
|
/**
|
|
64
69
|
* Hook to fetch or retrieve the session token for the chat.
|
|
65
|
-
* @returns
|
|
70
|
+
* @returns Auth object with headers and ensureValidHeaders for pre-request token refresh
|
|
66
71
|
*/
|
|
67
72
|
export const useAuth = ({
|
|
68
73
|
projectSlug,
|
|
@@ -73,6 +78,7 @@ export const useAuth = ({
|
|
|
73
78
|
}): Auth => {
|
|
74
79
|
const replayCtx = useReplayContext()
|
|
75
80
|
const isReplay = replayCtx?.isReplay ?? false
|
|
81
|
+
const queryClient = useQueryClient()
|
|
76
82
|
|
|
77
83
|
const apiUrl = useMemo(() => {
|
|
78
84
|
const envUrl =
|
|
@@ -117,17 +123,58 @@ export const useAuth = ({
|
|
|
117
123
|
projectSlug,
|
|
118
124
|
})
|
|
119
125
|
|
|
126
|
+
const shouldRefresh = !isAnyStaticSession(auth) && !isReplay
|
|
127
|
+
|
|
128
|
+
const ensureValidHeaders = useCallback(async (): Promise<
|
|
129
|
+
Record<string, string>
|
|
130
|
+
> => {
|
|
131
|
+
const queryKey = getChatSessionQueryKey(projectSlug)
|
|
132
|
+
const cachedToken = queryClient.getQueryData<string>(queryKey)
|
|
133
|
+
|
|
134
|
+
if (!shouldRefresh || !getSession) {
|
|
135
|
+
return {
|
|
136
|
+
'Gram-Project': projectSlug,
|
|
137
|
+
...(cachedToken && { 'Gram-Chat-Session': cachedToken }),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check if the cached token is expired (or within 30s of expiry).
|
|
142
|
+
// staleTime=0 forces a refetch; Infinity keeps the cached value.
|
|
143
|
+
const exp = cachedToken ? getTokenExpiry(cachedToken) : null
|
|
144
|
+
const isExpired = exp !== null && Date.now() >= exp * 1000 - 30_000
|
|
145
|
+
const staleTime = isExpired ? 0 : Infinity
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const token = await queryClient.fetchQuery({
|
|
149
|
+
queryKey,
|
|
150
|
+
queryFn: () => getSession({ projectSlug }),
|
|
151
|
+
staleTime,
|
|
152
|
+
})
|
|
153
|
+
return {
|
|
154
|
+
'Gram-Project': projectSlug,
|
|
155
|
+
...(token && { 'Gram-Chat-Session': token }),
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
return {
|
|
159
|
+
'Gram-Project': projectSlug,
|
|
160
|
+
...(cachedToken && { 'Gram-Chat-Session': cachedToken }),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}, [shouldRefresh, getSession, projectSlug, queryClient])
|
|
164
|
+
|
|
120
165
|
// In replay mode, return immediately without waiting for session
|
|
121
166
|
if (isReplay) {
|
|
122
167
|
return {
|
|
123
168
|
headers: {},
|
|
124
169
|
isLoading: false,
|
|
170
|
+
ensureValidHeaders: async () => ({}),
|
|
125
171
|
}
|
|
126
172
|
}
|
|
127
173
|
|
|
128
174
|
return !session
|
|
129
175
|
? {
|
|
130
176
|
isLoading: true,
|
|
177
|
+
ensureValidHeaders,
|
|
131
178
|
}
|
|
132
179
|
: {
|
|
133
180
|
headers: {
|
|
@@ -135,5 +182,6 @@ export const useAuth = ({
|
|
|
135
182
|
'Gram-Chat-Session': session,
|
|
136
183
|
},
|
|
137
184
|
isLoading: false,
|
|
185
|
+
ensureValidHeaders,
|
|
138
186
|
}
|
|
139
187
|
}
|
package/src/hooks/useSession.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { GetSessionFn } from '@/types'
|
|
2
2
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
3
3
|
|
|
4
|
+
export function getChatSessionQueryKey(projectSlug: string) {
|
|
5
|
+
return ['chatSession', projectSlug] as const
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
/**
|
|
5
9
|
* Hook to fetch or retrieve the session token for the chat.
|
|
6
10
|
* @returns The session token string or null
|
|
@@ -13,21 +17,14 @@ export const useSession = ({
|
|
|
13
17
|
projectSlug: string
|
|
14
18
|
}): string | null => {
|
|
15
19
|
const queryClient = useQueryClient()
|
|
16
|
-
const queryKey =
|
|
20
|
+
const queryKey = getChatSessionQueryKey(projectSlug)
|
|
17
21
|
|
|
18
|
-
const
|
|
19
|
-
const hasData = queryState?.data !== undefined
|
|
20
|
-
// Check if data is stale - with staleTime: Infinity, data never becomes stale
|
|
21
|
-
// but we check dataUpdatedAt to determine if we should refetch
|
|
22
|
-
const dataUpdatedAt = queryState?.dataUpdatedAt ?? 0
|
|
23
|
-
const staleTime = Infinity // Matches the staleTime in useQuery options
|
|
24
|
-
const isStale = hasData && Date.now() - dataUpdatedAt > staleTime
|
|
25
|
-
const shouldFetch = !hasData || isStale
|
|
22
|
+
const hasData = queryClient.getQueryState(queryKey)?.data !== undefined
|
|
26
23
|
|
|
27
24
|
const { data: fetchedSessionToken } = useQuery({
|
|
28
25
|
queryKey,
|
|
29
26
|
queryFn: () => getSession!({ projectSlug }),
|
|
30
|
-
enabled:
|
|
27
|
+
enabled: !hasData && getSession !== null,
|
|
31
28
|
staleTime: Infinity, // Session tokens don't need to be refetched
|
|
32
29
|
gcTime: Infinity, // Keep in cache indefinitely
|
|
33
30
|
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getTokenExpiry, isTokenExpired } from './token'
|
|
3
|
+
|
|
4
|
+
/** Helper: build a JWT with a given payload (no signature verification needed). */
|
|
5
|
+
function makeJwt(payload: Record<string, unknown>): string {
|
|
6
|
+
const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))
|
|
7
|
+
const body = btoa(JSON.stringify(payload))
|
|
8
|
+
return `${header}.${body}.fake-signature`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('getTokenExpiry', () => {
|
|
12
|
+
it('returns exp for a valid JWT', () => {
|
|
13
|
+
const exp = 1700000000
|
|
14
|
+
expect(getTokenExpiry(makeJwt({ exp }))).toBe(exp)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('returns null when exp is missing', () => {
|
|
18
|
+
expect(getTokenExpiry(makeJwt({ sub: 'user' }))).toBeNull()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns null for a non-JWT string', () => {
|
|
22
|
+
expect(getTokenExpiry('not-a-jwt')).toBeNull()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('returns null for an empty string', () => {
|
|
26
|
+
expect(getTokenExpiry('')).toBeNull()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns null when payload is not valid JSON', () => {
|
|
30
|
+
// Two dots but the middle segment is not valid base64 JSON
|
|
31
|
+
expect(getTokenExpiry('a.!!!.b')).toBeNull()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('handles base64url characters (- and _)', () => {
|
|
35
|
+
// Manually craft a payload with base64url-specific chars
|
|
36
|
+
const payload = { exp: 1700000000 }
|
|
37
|
+
const json = JSON.stringify(payload)
|
|
38
|
+
const b64 = btoa(json)
|
|
39
|
+
.replace(/\+/g, '-')
|
|
40
|
+
.replace(/\//g, '_')
|
|
41
|
+
.replace(/=+$/, '')
|
|
42
|
+
const token = `header.${b64}.sig`
|
|
43
|
+
expect(getTokenExpiry(token)).toBe(1700000000)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('isTokenExpired', () => {
|
|
48
|
+
it('returns true for an expired token', () => {
|
|
49
|
+
// exp = 1 second ago
|
|
50
|
+
const exp = Math.floor(Date.now() / 1000) - 1
|
|
51
|
+
expect(isTokenExpired(makeJwt({ exp }))).toBe(true)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('returns true when token is within the buffer window', () => {
|
|
55
|
+
// exp = 20 seconds from now (within 30s default buffer)
|
|
56
|
+
const exp = Math.floor(Date.now() / 1000) + 20
|
|
57
|
+
expect(isTokenExpired(makeJwt({ exp }))).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('returns false when token is well outside the buffer', () => {
|
|
61
|
+
// exp = 5 minutes from now
|
|
62
|
+
const exp = Math.floor(Date.now() / 1000) + 300
|
|
63
|
+
expect(isTokenExpired(makeJwt({ exp }))).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('respects a custom buffer', () => {
|
|
67
|
+
// exp = 20 seconds from now, buffer = 10s → should NOT be expired
|
|
68
|
+
const exp = Math.floor(Date.now() / 1000) + 20
|
|
69
|
+
expect(isTokenExpired(makeJwt({ exp }), 10_000)).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('returns false (fail-open) for a non-JWT string', () => {
|
|
73
|
+
expect(isTokenExpired('opaque-session-token')).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('returns false (fail-open) for an empty string', () => {
|
|
77
|
+
expect(isTokenExpired('')).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
})
|
package/src/lib/token.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts the `exp` claim from a JWT token without verifying the signature.
|
|
3
|
+
* Returns the expiry as a Unix timestamp (seconds), or null if the token
|
|
4
|
+
* is not a valid JWT or has no `exp` claim.
|
|
5
|
+
*/
|
|
6
|
+
export function getTokenExpiry(token: string): number | null {
|
|
7
|
+
try {
|
|
8
|
+
const parts = token.split('.')
|
|
9
|
+
if (parts.length !== 3) return null
|
|
10
|
+
|
|
11
|
+
// base64url → base64 → decode
|
|
12
|
+
let payload = parts[1].replace(/-/g, '+').replace(/_/g, '/')
|
|
13
|
+
while (payload.length % 4) payload += '='
|
|
14
|
+
|
|
15
|
+
const json = atob(payload)
|
|
16
|
+
const parsed = JSON.parse(json)
|
|
17
|
+
|
|
18
|
+
if (typeof parsed.exp === 'number') {
|
|
19
|
+
return parsed.exp
|
|
20
|
+
}
|
|
21
|
+
return null
|
|
22
|
+
} catch {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns true when the token is expired or within `bufferMs` milliseconds
|
|
29
|
+
* of expiry. Fails open (returns false) for non-JWT tokens or tokens
|
|
30
|
+
* without an `exp` claim so they pass through unchanged.
|
|
31
|
+
*/
|
|
32
|
+
export function isTokenExpired(
|
|
33
|
+
token: string,
|
|
34
|
+
bufferMs: number = 30_000
|
|
35
|
+
): boolean {
|
|
36
|
+
const exp = getTokenExpiry(token)
|
|
37
|
+
if (exp === null) return false // fail-open for non-JWT tokens
|
|
38
|
+
return Date.now() >= exp * 1000 - bufferMs
|
|
39
|
+
}
|
|
@@ -1,70 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TanStack Start adapter for Gram Elements server handlers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Use `createTanStackStartHandler` to create a handler for TanStack Start
|
|
5
|
+
* server routes that manages chat session creation.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
* a `createServerFn` that can be called directly from client code and passed
|
|
8
|
-
* to `session` in the Elements config.
|
|
9
|
-
*
|
|
10
|
-
* 2. **API Route** — use `createTanStackStartHandler` to create a handler for
|
|
11
|
-
* TanStack Start server routes (similar to the Next.js adapter).
|
|
12
|
-
*
|
|
13
|
-
* @example Server Function approach
|
|
7
|
+
* @example
|
|
14
8
|
* ```typescript
|
|
15
|
-
* // session.
|
|
16
|
-
* import {
|
|
9
|
+
* // routes/api/chat.session.ts
|
|
10
|
+
* import { createTanStackStartHandler } from '@gram-ai/elements/server/tanstack-start'
|
|
17
11
|
*
|
|
18
|
-
* export const
|
|
12
|
+
* export const POST = createTanStackStartHandler({
|
|
19
13
|
* embedOrigin: 'http://localhost:3000',
|
|
20
14
|
* userIdentifier: 'user-123',
|
|
21
15
|
* expiresAfter: 3600,
|
|
22
16
|
* })
|
|
23
17
|
* ```
|
|
24
18
|
*
|
|
25
|
-
* @example
|
|
19
|
+
* @example Dynamic configuration
|
|
26
20
|
* ```typescript
|
|
27
21
|
* // routes/api/chat.session.ts
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
22
|
+
* export const POST = createTanStackStartHandler(async (request) => {
|
|
23
|
+
* const user = await getUserFromRequest(request)
|
|
24
|
+
* return {
|
|
25
|
+
* embedOrigin: 'http://localhost:3000',
|
|
26
|
+
* userIdentifier: user.id,
|
|
27
|
+
* expiresAfter: 3600,
|
|
28
|
+
* }
|
|
34
29
|
* })
|
|
35
30
|
* ```
|
|
36
31
|
*/
|
|
37
32
|
|
|
38
|
-
import { createServerFn } from '@tanstack/react-start'
|
|
39
33
|
import { createChatSession, type SessionHandlerOptions } from './core'
|
|
40
34
|
|
|
41
|
-
/**
|
|
42
|
-
* Create a TanStack Start server function for session creation.
|
|
43
|
-
*
|
|
44
|
-
* The returned function can be called from client code (RPC-style) and passed
|
|
45
|
-
* to `session` in the Gram Elements config.
|
|
46
|
-
*
|
|
47
|
-
* @param options - Session configuration options
|
|
48
|
-
* @returns A `createServerFn` instance callable from the client
|
|
49
|
-
*/
|
|
50
|
-
export function createTanStackStartSessionFn(options: SessionHandlerOptions) {
|
|
51
|
-
return createServerFn({ method: 'POST' })
|
|
52
|
-
.inputValidator((data: { projectSlug: string }) => data)
|
|
53
|
-
.handler(async ({ data }) => {
|
|
54
|
-
const result = await createChatSession({
|
|
55
|
-
projectSlug: data.projectSlug,
|
|
56
|
-
options,
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
if (result.status !== 200) {
|
|
60
|
-
throw new Error(`Failed to create chat session: ${result.body}`)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const parsed = JSON.parse(result.body) as { client_token: string }
|
|
64
|
-
return parsed.client_token
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
|
|
68
35
|
/**
|
|
69
36
|
* Create a TanStack Start server route handler for the chat session endpoint.
|
|
70
37
|
*
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"core-Cqad6-xW.js","sources":["../src/server/core.ts"],"sourcesContent":null,"names":["createChatSession","request","base","response","body","error","errorMessage"],"mappings":"AA2CA,eAAsBA,EACpBC,GACgC;AAChC,QAAMC,IAAO,QAAQ,IAAI,gBAAgB;AAEzC,MAAI;AACF,UAAMC,IAAW,MAAM,MAAMD,IAAO,4BAA4B;AAAA,MAC9D,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU;AAAA,QACnB,cAAcD,EAAQ,QAAQ;AAAA,QAC9B,iBAAiBA,EAAQ,QAAQ;AAAA,QACjC,eAAeA,EAAQ,QAAQ;AAAA,MAAA,CAChC;AAAA,MACD,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,gBAAgBA,EAAQ;AAAA,QACxB,YAAYA,EAAQ,QAAQ,UAAU,QAAQ,IAAI,gBAAgB;AAAA,MAAA;AAAA,IACpE,CACD,GAEKG,IAAO,MAAMD,EAAS,KAAA;AAE5B,WAAO;AAAA,MACL,QAAQA,EAAS;AAAA,MACjB,MAAAC;AAAA,MACA,SAAS,EAAE,gBAAgB,mBAAA;AAAA,IAAmB;AAAA,EAElD,SAASC,GAAO;AACd,UAAMC,IACJD,aAAiB,QAAQA,EAAM,UAAU;AAC3C,mBAAQ,MAAM,kCAAkCA,CAAK,GAE9C;AAAA,MACL,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU;AAAA,QACnB,OAAO,oCAAoCC;AAAA,MAAA,CAC5C;AAAA,MACD,SAAS,EAAE,gBAAgB,mBAAA;AAAA,IAAmB;AAAA,EAElD;AACF;"}
|
package/dist/core-DBxmxwCi.cjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
"use strict";async function r(t){const o=process.env.GRAM_API_URL??"https://app.getgram.ai";try{const e=await fetch(o+"/rpc/chatSessions.create",{method:"POST",body:JSON.stringify({embed_origin:t.options.embedOrigin,user_identifier:t.options.userIdentifier,expires_after:t.options.expiresAfter}),headers:{"Content-Type":"application/json","Gram-Project":t.projectSlug,"Gram-Key":t.options.apiKey??process.env.GRAM_API_KEY??""}}),s=await e.text();return{status:e.status,body:s,headers:{"Content-Type":"application/json"}}}catch(e){const s=e instanceof Error?e.message:"Unknown error";return console.error("Failed to create chat session:",e),{status:500,body:JSON.stringify({error:"Failed to create chat session: "+s}),headers:{"Content-Type":"application/json"}}}}exports.createChatSession=r;
|
|
2
|
-
//# sourceMappingURL=core-DBxmxwCi.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"core-DBxmxwCi.cjs","sources":["../src/server/core.ts"],"sourcesContent":null,"names":["createChatSession","request","base","response","body","error","errorMessage"],"mappings":"aA2CA,eAAsBA,EACpBC,EACgC,CAChC,MAAMC,EAAO,QAAQ,IAAI,cAAgB,yBAEzC,GAAI,CACF,MAAMC,EAAW,MAAM,MAAMD,EAAO,2BAA4B,CAC9D,OAAQ,OACR,KAAM,KAAK,UAAU,CACnB,aAAcD,EAAQ,QAAQ,YAC9B,gBAAiBA,EAAQ,QAAQ,eACjC,cAAeA,EAAQ,QAAQ,YAAA,CAChC,EACD,QAAS,CACP,eAAgB,mBAChB,eAAgBA,EAAQ,YACxB,WAAYA,EAAQ,QAAQ,QAAU,QAAQ,IAAI,cAAgB,EAAA,CACpE,CACD,EAEKG,EAAO,MAAMD,EAAS,KAAA,EAE5B,MAAO,CACL,OAAQA,EAAS,OACjB,KAAAC,EACA,QAAS,CAAE,eAAgB,kBAAA,CAAmB,CAElD,OAASC,EAAO,CACd,MAAMC,EACJD,aAAiB,MAAQA,EAAM,QAAU,gBAC3C,eAAQ,MAAM,iCAAkCA,CAAK,EAE9C,CACL,OAAQ,IACR,KAAM,KAAK,UAAU,CACnB,MAAO,kCAAoCC,CAAA,CAC5C,EACD,QAAS,CAAE,eAAgB,kBAAA,CAAmB,CAElD,CACF"}
|