@app-connect/core 1.7.24 → 1.7.26
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/.env.test +5 -5
- package/README.md +441 -441
- package/connector/developerPortal.js +31 -42
- package/connector/mock.js +84 -77
- package/connector/proxy/engine.js +164 -163
- package/connector/proxy/index.js +500 -500
- package/connector/registry.js +252 -252
- package/docs/README.md +50 -50
- package/docs/architecture.md +93 -93
- package/docs/connectors.md +116 -117
- package/docs/handlers.md +125 -125
- package/docs/libraries.md +101 -101
- package/docs/models.md +144 -144
- package/docs/routes.md +115 -115
- package/docs/tests.md +73 -73
- package/handlers/admin.js +523 -523
- package/handlers/appointment.js +193 -0
- package/handlers/auth.js +296 -296
- package/handlers/calldown.js +99 -99
- package/handlers/contact.js +280 -280
- package/handlers/disposition.js +82 -80
- package/handlers/log.js +984 -973
- package/handlers/managedAuth.js +446 -446
- package/handlers/plugin.js +208 -208
- package/handlers/user.js +142 -142
- package/index.js +3140 -2652
- package/jest.config.js +56 -56
- package/lib/analytics.js +54 -54
- package/lib/authSession.js +109 -109
- package/lib/cacheCleanup.js +21 -0
- package/lib/callLogComposer.js +898 -898
- package/lib/callLogLookup.js +34 -0
- package/lib/constants.js +8 -8
- package/lib/debugTracer.js +177 -177
- package/lib/encode.js +30 -30
- package/lib/errorHandler.js +218 -206
- package/lib/generalErrorMessage.js +41 -41
- package/lib/jwt.js +18 -18
- package/lib/logger.js +190 -190
- package/lib/migrateCallLogsSchema.js +116 -0
- package/lib/ringcentral.js +266 -266
- package/lib/s3ErrorLogReport.js +65 -65
- package/lib/sharedSMSComposer.js +471 -471
- package/lib/util.js +67 -67
- package/mcp/README.md +412 -395
- package/mcp/lib/validator.js +91 -91
- package/mcp/mcpHandler.js +425 -425
- package/mcp/tools/cancelAppointment.js +101 -0
- package/mcp/tools/checkAuthStatus.js +105 -105
- package/mcp/tools/confirmAppointment.js +101 -0
- package/mcp/tools/createAppointment.js +157 -0
- package/mcp/tools/createCallLog.js +327 -316
- package/mcp/tools/createContact.js +117 -117
- package/mcp/tools/createMessageLog.js +287 -287
- package/mcp/tools/doAuth.js +60 -60
- package/mcp/tools/findContactByName.js +93 -93
- package/mcp/tools/findContactByPhone.js +101 -101
- package/mcp/tools/getCallLog.js +111 -102
- package/mcp/tools/getGoogleFilePicker.js +99 -99
- package/mcp/tools/getHelp.js +43 -43
- package/mcp/tools/getPublicConnectors.js +94 -94
- package/mcp/tools/getSessionInfo.js +90 -90
- package/mcp/tools/index.js +51 -41
- package/mcp/tools/listAppointments.js +163 -0
- package/mcp/tools/logout.js +96 -96
- package/mcp/tools/rcGetCallLogs.js +65 -65
- package/mcp/tools/updateAppointment.js +154 -0
- package/mcp/tools/updateCallLog.js +130 -126
- package/mcp/ui/App/App.tsx +358 -358
- package/mcp/ui/App/components/AuthInfoForm.tsx +113 -113
- package/mcp/ui/App/components/AuthSuccess.tsx +22 -22
- package/mcp/ui/App/components/ConnectorList.tsx +82 -82
- package/mcp/ui/App/components/DebugPanel.tsx +43 -43
- package/mcp/ui/App/components/OAuthConnect.tsx +270 -270
- package/mcp/ui/App/lib/callTool.ts +130 -130
- package/mcp/ui/App/lib/debugLog.ts +41 -41
- package/mcp/ui/App/lib/developerPortal.ts +111 -111
- package/mcp/ui/App/main.css +5 -5
- package/mcp/ui/App/root.tsx +13 -13
- package/mcp/ui/index.html +13 -13
- package/mcp/ui/package-lock.json +6356 -6356
- package/mcp/ui/package.json +25 -25
- package/mcp/ui/tsconfig.json +26 -26
- package/mcp/ui/vite.config.ts +16 -16
- package/models/accountDataModel.js +33 -33
- package/models/adminConfigModel.js +35 -35
- package/models/cacheModel.js +30 -26
- package/models/callDownListModel.js +34 -34
- package/models/callLogModel.js +33 -27
- package/models/dynamo/connectorSchema.js +146 -146
- package/models/dynamo/lockSchema.js +24 -24
- package/models/dynamo/noteCacheSchema.js +29 -29
- package/models/llmSessionModel.js +17 -17
- package/models/messageLogModel.js +25 -25
- package/models/sequelize.js +16 -16
- package/models/userModel.js +45 -45
- package/package.json +72 -72
- package/releaseNotes.json +1093 -1073
- package/test/connector/proxy/engine.test.js +126 -93
- package/test/connector/proxy/index.test.js +279 -279
- package/test/connector/proxy/sample.json +161 -161
- package/test/connector/registry.test.js +415 -415
- package/test/handlers/admin.test.js +616 -616
- package/test/handlers/auth.test.js +1018 -1015
- package/test/handlers/contact.test.js +1014 -1014
- package/test/handlers/log.test.js +1298 -1160
- package/test/handlers/managedAuth.test.js +458 -458
- package/test/handlers/plugin.test.js +380 -380
- package/test/index.test.js +105 -105
- package/test/lib/cacheCleanup.test.js +42 -0
- package/test/lib/callLogComposer.test.js +1231 -1231
- package/test/lib/debugTracer.test.js +328 -328
- package/test/lib/jwt.test.js +176 -176
- package/test/lib/logger.test.js +206 -206
- package/test/lib/oauth.test.js +359 -359
- package/test/lib/ringcentral.test.js +467 -467
- package/test/lib/sharedSMSComposer.test.js +1084 -1084
- package/test/lib/util.test.js +329 -329
- package/test/mcp/tools/checkAuthStatus.test.js +83 -82
- package/test/mcp/tools/createCallLog.test.js +436 -436
- package/test/mcp/tools/createContact.test.js +58 -58
- package/test/mcp/tools/createMessageLog.test.js +595 -595
- package/test/mcp/tools/doAuth.test.js +113 -113
- package/test/mcp/tools/findContactByName.test.js +275 -275
- package/test/mcp/tools/findContactByPhone.test.js +296 -296
- package/test/mcp/tools/getCallLog.test.js +298 -298
- package/test/mcp/tools/getGoogleFilePicker.test.js +281 -281
- package/test/mcp/tools/getPublicConnectors.test.js +107 -107
- package/test/mcp/tools/getSessionInfo.test.js +127 -127
- package/test/mcp/tools/logout.test.js +233 -233
- package/test/mcp/tools/rcGetCallLogs.test.js +56 -56
- package/test/mcp/tools/updateCallLog.test.js +360 -360
- package/test/models/accountDataModel.test.js +98 -98
- package/test/models/dynamo/connectorSchema.test.js +189 -189
- package/test/models/models.test.js +568 -539
- package/test/routes/managedAuthRoutes.test.js +104 -129
- package/test/setup.js +178 -178
|
@@ -1,270 +1,270 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { Button } from '@openai/apps-sdk-ui/components/Button'
|
|
3
|
-
import { callTool, getServerUrl, updateModelContext } from '../lib/callTool'
|
|
4
|
-
import { dbg } from '../lib/debugLog'
|
|
5
|
-
|
|
6
|
-
type AuthStatus = 'ready' | 'creatingSession' | 'polling' | 'error'
|
|
7
|
-
|
|
8
|
-
interface OAuthConnectProps {
|
|
9
|
-
connectorManifest: any
|
|
10
|
-
connectorName: string
|
|
11
|
-
connectorDisplayName: string
|
|
12
|
-
hostname: string
|
|
13
|
-
openaiSessionId?: string | null
|
|
14
|
-
rcExtensionId?: string | null
|
|
15
|
-
onSuccess: (data: { jwtToken: string; userInfo?: any }) => void
|
|
16
|
-
onError: (error: string) => void
|
|
17
|
-
onBack: () => void
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Compose the OAuth authorization URL entirely client-side.
|
|
22
|
-
* The widget already has the full manifest and serverUrl — no server round-trip needed.
|
|
23
|
-
*/
|
|
24
|
-
function buildAuthUri(platform: any, sessionId: string, hostname: string, serverUrl: string): string {
|
|
25
|
-
const oauth = platform.auth.oauth
|
|
26
|
-
let stateParam = `sessionId=${sessionId}&platform=${platform.name}&hostname=${hostname}`
|
|
27
|
-
if (oauth.customState) stateParam += `&${oauth.customState}`
|
|
28
|
-
|
|
29
|
-
return `${oauth.authUrl}?` +
|
|
30
|
-
`response_type=code` +
|
|
31
|
-
`&client_id=${oauth.clientId}` +
|
|
32
|
-
`${oauth.scope ? `&${oauth.scope}` : ''}` +
|
|
33
|
-
`&state=${encodeURIComponent(stateParam)}` +
|
|
34
|
-
`&redirect_uri=${serverUrl}/oauth-callback`
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const POLL_INTERVAL_MS = 5_000;
|
|
38
|
-
const MAX_POLL_ATTEMPTS = 100; // ~5 minutes
|
|
39
|
-
|
|
40
|
-
export function OAuthConnect({
|
|
41
|
-
connectorManifest,
|
|
42
|
-
connectorName,
|
|
43
|
-
connectorDisplayName,
|
|
44
|
-
hostname,
|
|
45
|
-
openaiSessionId,
|
|
46
|
-
rcExtensionId,
|
|
47
|
-
onSuccess,
|
|
48
|
-
onError,
|
|
49
|
-
onBack,
|
|
50
|
-
}: OAuthConnectProps) {
|
|
51
|
-
const [status, setStatus] = useState<AuthStatus>('ready')
|
|
52
|
-
const [authUri, setAuthUri] = useState<string | null>(null)
|
|
53
|
-
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
|
54
|
-
const [sessionReady, setSessionReady] = useState(false)
|
|
55
|
-
|
|
56
|
-
const sessionIdRef = useRef<string | null>(null)
|
|
57
|
-
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
58
|
-
const pollCountRef = useRef(0)
|
|
59
|
-
const unmountedRef = useRef(false)
|
|
60
|
-
|
|
61
|
-
// Cleanup on unmount
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
return () => {
|
|
64
|
-
unmountedRef.current = true
|
|
65
|
-
if (pollTimerRef.current) clearInterval(pollTimerRef.current)
|
|
66
|
-
}
|
|
67
|
-
}, [])
|
|
68
|
-
|
|
69
|
-
// Build authUri client-side immediately — no server round-trip needed.
|
|
70
|
-
// Simultaneously call doAuth in the background to create the DB session.
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
let cancelled = false
|
|
73
|
-
const platform = connectorManifest?.platforms?.[connectorName]
|
|
74
|
-
|
|
75
|
-
if (!platform) {
|
|
76
|
-
setStatus('error')
|
|
77
|
-
setErrorMsg('Invalid connector configuration')
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Use openaiSessionId as the primary session key so the OAuth callback
|
|
82
|
-
// can be correlated with the ChatGPT conversation. Fall back to a random
|
|
83
|
-
// UUID when the widget is loaded outside ChatGPT.
|
|
84
|
-
const sessionId = openaiSessionId ?? crypto.randomUUID()
|
|
85
|
-
sessionIdRef.current = sessionId
|
|
86
|
-
const serverUrl = getServerUrl() ?? ''
|
|
87
|
-
const uri = buildAuthUri(platform, sessionId, hostname, serverUrl)
|
|
88
|
-
setAuthUri(uri)
|
|
89
|
-
dbg.info('authUri composed client-side, sessionId:', sessionId)
|
|
90
|
-
|
|
91
|
-
// Create the server-side session in background (fast DB write only)
|
|
92
|
-
callTool('doAuth', { connectorName, hostname, sessionId }).then((result) => {
|
|
93
|
-
if (cancelled || unmountedRef.current) return
|
|
94
|
-
if (!result?.success) {
|
|
95
|
-
dbg.warn('doAuth session creation failed:', result?.error)
|
|
96
|
-
setStatus('error')
|
|
97
|
-
setErrorMsg(result?.error || 'Failed to prepare authentication session')
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
dbg.info('doAuth session created successfully')
|
|
101
|
-
setSessionReady(true)
|
|
102
|
-
}).catch((err: any) => {
|
|
103
|
-
if (cancelled || unmountedRef.current) return
|
|
104
|
-
dbg.error('doAuth error:', err.message)
|
|
105
|
-
setStatus('error')
|
|
106
|
-
setErrorMsg(err.message || 'Failed to prepare authentication session')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
return () => { cancelled = true }
|
|
110
|
-
}, [connectorManifest, connectorName, hostname])
|
|
111
|
-
|
|
112
|
-
const startPolling = () => {
|
|
113
|
-
setStatus('polling')
|
|
114
|
-
pollCountRef.current = 0
|
|
115
|
-
|
|
116
|
-
pollTimerRef.current = setInterval(async () => {
|
|
117
|
-
if (unmountedRef.current) {
|
|
118
|
-
if (pollTimerRef.current) clearInterval(pollTimerRef.current)
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
pollCountRef.current += 1
|
|
123
|
-
if (pollCountRef.current > MAX_POLL_ATTEMPTS) {
|
|
124
|
-
if (pollTimerRef.current) clearInterval(pollTimerRef.current)
|
|
125
|
-
setStatus('error')
|
|
126
|
-
setErrorMsg('Authorization timed out. Please try again.')
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
const res = await callTool('checkAuthStatus', {
|
|
132
|
-
sessionId: sessionIdRef.current,
|
|
133
|
-
...(rcExtensionId ? { rcExtensionId } : {}),
|
|
134
|
-
})
|
|
135
|
-
const sc = res?.data ?? null
|
|
136
|
-
|
|
137
|
-
if (unmountedRef.current) return
|
|
138
|
-
|
|
139
|
-
const dataStatus = sc?.status
|
|
140
|
-
if (dataStatus === 'completed' && sc?.jwtToken) {
|
|
141
|
-
if (pollTimerRef.current) clearInterval(pollTimerRef.current)
|
|
142
|
-
|
|
143
|
-
// Push the JWT into ChatGPT's model context so it can be used
|
|
144
|
-
// in future tool calls. updateModelContext tries ui/update-model-context
|
|
145
|
-
// first (invisible to user, visible to model), then falls back to ui/message.
|
|
146
|
-
const userLabel = sc.userInfo?.name ? ` as ${sc.userInfo.name}` : ''
|
|
147
|
-
await updateModelContext(
|
|
148
|
-
`CRM authentication with ${connectorDisplayName} completed${userLabel}. ` +
|
|
149
|
-
`Platform: ${connectorName}. ` +
|
|
150
|
-
`jwtToken: ${sc.jwtToken}. ` +
|
|
151
|
-
`IMPORTANT: Store this jwtToken and pass it as a parameter to all future CRM tool calls.`
|
|
152
|
-
)
|
|
153
|
-
dbg.info('updateModelContext sent with jwtToken')
|
|
154
|
-
|
|
155
|
-
onSuccess({
|
|
156
|
-
jwtToken: sc.jwtToken,
|
|
157
|
-
userInfo: sc.userInfo,
|
|
158
|
-
})
|
|
159
|
-
} else if (dataStatus === 'failed') {
|
|
160
|
-
if (pollTimerRef.current) clearInterval(pollTimerRef.current)
|
|
161
|
-
setStatus('error')
|
|
162
|
-
setErrorMsg(sc?.errorMessage || 'Authorization failed')
|
|
163
|
-
} else if (dataStatus === 'expired') {
|
|
164
|
-
if (pollTimerRef.current) clearInterval(pollTimerRef.current)
|
|
165
|
-
setStatus('error')
|
|
166
|
-
setErrorMsg('Authorization session expired. Please try again.')
|
|
167
|
-
}
|
|
168
|
-
// 'pending' -> keep polling
|
|
169
|
-
} catch {
|
|
170
|
-
// Transient errors during polling are acceptable; keep trying
|
|
171
|
-
}
|
|
172
|
-
}, POLL_INTERVAL_MS)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const handleAuthorizeClick = () => {
|
|
176
|
-
if (authUri && sessionReady) {
|
|
177
|
-
window.open(authUri, '_blank', 'noopener,noreferrer')
|
|
178
|
-
startPolling()
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const handleRetry = () => {
|
|
183
|
-
setStatus('error')
|
|
184
|
-
setErrorMsg(null)
|
|
185
|
-
onError('retry')
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Error state
|
|
189
|
-
if (status === 'error') {
|
|
190
|
-
return (
|
|
191
|
-
<div className="w-full max-w-md">
|
|
192
|
-
<div className="mb-4">
|
|
193
|
-
<button
|
|
194
|
-
type="button"
|
|
195
|
-
onClick={onBack}
|
|
196
|
-
className="text-sm text-secondary hover:text-primary transition-colors mb-2 cursor-pointer"
|
|
197
|
-
>
|
|
198
|
-
← Back
|
|
199
|
-
</button>
|
|
200
|
-
<h2 className="heading-lg">Authentication Error</h2>
|
|
201
|
-
</div>
|
|
202
|
-
<div className="rounded-lg border border-red-200 bg-red-50 p-4 mb-4">
|
|
203
|
-
<p className="text-red-700 text-sm">{errorMsg}</p>
|
|
204
|
-
</div>
|
|
205
|
-
<Button color="primary" size="sm" onClick={handleRetry}>
|
|
206
|
-
Try Again
|
|
207
|
-
</Button>
|
|
208
|
-
</div>
|
|
209
|
-
)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Ready to authorize or polling
|
|
213
|
-
return (
|
|
214
|
-
<div className="w-full max-w-md">
|
|
215
|
-
<div className="mb-4">
|
|
216
|
-
<button
|
|
217
|
-
type="button"
|
|
218
|
-
onClick={onBack}
|
|
219
|
-
className="text-sm text-secondary hover:text-primary transition-colors mb-2 cursor-pointer"
|
|
220
|
-
>
|
|
221
|
-
← Back
|
|
222
|
-
</button>
|
|
223
|
-
<h2 className="heading-lg">Authorize {connectorDisplayName}</h2>
|
|
224
|
-
</div>
|
|
225
|
-
|
|
226
|
-
<div className="rounded-xl border border-default bg-surface p-4 space-y-4">
|
|
227
|
-
{status === 'ready' && (
|
|
228
|
-
<>
|
|
229
|
-
<p className="text-sm text-secondary">
|
|
230
|
-
Click the button below to open {connectorDisplayName}'s authorization
|
|
231
|
-
page. After you approve access, return here to continue.
|
|
232
|
-
</p>
|
|
233
|
-
<Button
|
|
234
|
-
color="primary"
|
|
235
|
-
size="md"
|
|
236
|
-
onClick={handleAuthorizeClick}
|
|
237
|
-
disabled={!sessionReady}
|
|
238
|
-
>
|
|
239
|
-
{sessionReady
|
|
240
|
-
? `Authorize in ${connectorDisplayName}`
|
|
241
|
-
: 'Preparing…'}
|
|
242
|
-
</Button>
|
|
243
|
-
</>
|
|
244
|
-
)}
|
|
245
|
-
|
|
246
|
-
{status === 'polling' && (
|
|
247
|
-
<div className="text-center space-y-3">
|
|
248
|
-
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-primary mx-auto" />
|
|
249
|
-
<p className="text-sm text-secondary">
|
|
250
|
-
Waiting for you to authorize in {connectorDisplayName}...
|
|
251
|
-
</p>
|
|
252
|
-
<p className="text-xs text-secondary opacity-60">
|
|
253
|
-
Complete the authorization in the opened tab, then come back here.
|
|
254
|
-
</p>
|
|
255
|
-
{authUri && (
|
|
256
|
-
<a
|
|
257
|
-
href={authUri}
|
|
258
|
-
target="_blank"
|
|
259
|
-
rel="noopener noreferrer"
|
|
260
|
-
className="text-xs text-primary hover:underline"
|
|
261
|
-
>
|
|
262
|
-
Open authorization page again
|
|
263
|
-
</a>
|
|
264
|
-
)}
|
|
265
|
-
</div>
|
|
266
|
-
)}
|
|
267
|
-
</div>
|
|
268
|
-
</div>
|
|
269
|
-
)
|
|
270
|
-
}
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Button } from '@openai/apps-sdk-ui/components/Button'
|
|
3
|
+
import { callTool, getServerUrl, updateModelContext } from '../lib/callTool'
|
|
4
|
+
import { dbg } from '../lib/debugLog'
|
|
5
|
+
|
|
6
|
+
type AuthStatus = 'ready' | 'creatingSession' | 'polling' | 'error'
|
|
7
|
+
|
|
8
|
+
interface OAuthConnectProps {
|
|
9
|
+
connectorManifest: any
|
|
10
|
+
connectorName: string
|
|
11
|
+
connectorDisplayName: string
|
|
12
|
+
hostname: string
|
|
13
|
+
openaiSessionId?: string | null
|
|
14
|
+
rcExtensionId?: string | null
|
|
15
|
+
onSuccess: (data: { jwtToken: string; userInfo?: any }) => void
|
|
16
|
+
onError: (error: string) => void
|
|
17
|
+
onBack: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Compose the OAuth authorization URL entirely client-side.
|
|
22
|
+
* The widget already has the full manifest and serverUrl — no server round-trip needed.
|
|
23
|
+
*/
|
|
24
|
+
function buildAuthUri(platform: any, sessionId: string, hostname: string, serverUrl: string): string {
|
|
25
|
+
const oauth = platform.auth.oauth
|
|
26
|
+
let stateParam = `sessionId=${sessionId}&platform=${platform.name}&hostname=${hostname}`
|
|
27
|
+
if (oauth.customState) stateParam += `&${oauth.customState}`
|
|
28
|
+
|
|
29
|
+
return `${oauth.authUrl}?` +
|
|
30
|
+
`response_type=code` +
|
|
31
|
+
`&client_id=${oauth.clientId}` +
|
|
32
|
+
`${oauth.scope ? `&${oauth.scope}` : ''}` +
|
|
33
|
+
`&state=${encodeURIComponent(stateParam)}` +
|
|
34
|
+
`&redirect_uri=${serverUrl}/oauth-callback`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const POLL_INTERVAL_MS = 5_000;
|
|
38
|
+
const MAX_POLL_ATTEMPTS = 100; // ~5 minutes
|
|
39
|
+
|
|
40
|
+
export function OAuthConnect({
|
|
41
|
+
connectorManifest,
|
|
42
|
+
connectorName,
|
|
43
|
+
connectorDisplayName,
|
|
44
|
+
hostname,
|
|
45
|
+
openaiSessionId,
|
|
46
|
+
rcExtensionId,
|
|
47
|
+
onSuccess,
|
|
48
|
+
onError,
|
|
49
|
+
onBack,
|
|
50
|
+
}: OAuthConnectProps) {
|
|
51
|
+
const [status, setStatus] = useState<AuthStatus>('ready')
|
|
52
|
+
const [authUri, setAuthUri] = useState<string | null>(null)
|
|
53
|
+
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
|
54
|
+
const [sessionReady, setSessionReady] = useState(false)
|
|
55
|
+
|
|
56
|
+
const sessionIdRef = useRef<string | null>(null)
|
|
57
|
+
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
58
|
+
const pollCountRef = useRef(0)
|
|
59
|
+
const unmountedRef = useRef(false)
|
|
60
|
+
|
|
61
|
+
// Cleanup on unmount
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
return () => {
|
|
64
|
+
unmountedRef.current = true
|
|
65
|
+
if (pollTimerRef.current) clearInterval(pollTimerRef.current)
|
|
66
|
+
}
|
|
67
|
+
}, [])
|
|
68
|
+
|
|
69
|
+
// Build authUri client-side immediately — no server round-trip needed.
|
|
70
|
+
// Simultaneously call doAuth in the background to create the DB session.
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
let cancelled = false
|
|
73
|
+
const platform = connectorManifest?.platforms?.[connectorName]
|
|
74
|
+
|
|
75
|
+
if (!platform) {
|
|
76
|
+
setStatus('error')
|
|
77
|
+
setErrorMsg('Invalid connector configuration')
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Use openaiSessionId as the primary session key so the OAuth callback
|
|
82
|
+
// can be correlated with the ChatGPT conversation. Fall back to a random
|
|
83
|
+
// UUID when the widget is loaded outside ChatGPT.
|
|
84
|
+
const sessionId = openaiSessionId ?? crypto.randomUUID()
|
|
85
|
+
sessionIdRef.current = sessionId
|
|
86
|
+
const serverUrl = getServerUrl() ?? ''
|
|
87
|
+
const uri = buildAuthUri(platform, sessionId, hostname, serverUrl)
|
|
88
|
+
setAuthUri(uri)
|
|
89
|
+
dbg.info('authUri composed client-side, sessionId:', sessionId)
|
|
90
|
+
|
|
91
|
+
// Create the server-side session in background (fast DB write only)
|
|
92
|
+
callTool('doAuth', { connectorName, hostname, sessionId }).then((result) => {
|
|
93
|
+
if (cancelled || unmountedRef.current) return
|
|
94
|
+
if (!result?.success) {
|
|
95
|
+
dbg.warn('doAuth session creation failed:', result?.error)
|
|
96
|
+
setStatus('error')
|
|
97
|
+
setErrorMsg(result?.error || 'Failed to prepare authentication session')
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
dbg.info('doAuth session created successfully')
|
|
101
|
+
setSessionReady(true)
|
|
102
|
+
}).catch((err: any) => {
|
|
103
|
+
if (cancelled || unmountedRef.current) return
|
|
104
|
+
dbg.error('doAuth error:', err.message)
|
|
105
|
+
setStatus('error')
|
|
106
|
+
setErrorMsg(err.message || 'Failed to prepare authentication session')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
return () => { cancelled = true }
|
|
110
|
+
}, [connectorManifest, connectorName, hostname])
|
|
111
|
+
|
|
112
|
+
const startPolling = () => {
|
|
113
|
+
setStatus('polling')
|
|
114
|
+
pollCountRef.current = 0
|
|
115
|
+
|
|
116
|
+
pollTimerRef.current = setInterval(async () => {
|
|
117
|
+
if (unmountedRef.current) {
|
|
118
|
+
if (pollTimerRef.current) clearInterval(pollTimerRef.current)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pollCountRef.current += 1
|
|
123
|
+
if (pollCountRef.current > MAX_POLL_ATTEMPTS) {
|
|
124
|
+
if (pollTimerRef.current) clearInterval(pollTimerRef.current)
|
|
125
|
+
setStatus('error')
|
|
126
|
+
setErrorMsg('Authorization timed out. Please try again.')
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const res = await callTool('checkAuthStatus', {
|
|
132
|
+
sessionId: sessionIdRef.current,
|
|
133
|
+
...(rcExtensionId ? { rcExtensionId } : {}),
|
|
134
|
+
})
|
|
135
|
+
const sc = res?.data ?? null
|
|
136
|
+
|
|
137
|
+
if (unmountedRef.current) return
|
|
138
|
+
|
|
139
|
+
const dataStatus = sc?.status
|
|
140
|
+
if (dataStatus === 'completed' && sc?.jwtToken) {
|
|
141
|
+
if (pollTimerRef.current) clearInterval(pollTimerRef.current)
|
|
142
|
+
|
|
143
|
+
// Push the JWT into ChatGPT's model context so it can be used
|
|
144
|
+
// in future tool calls. updateModelContext tries ui/update-model-context
|
|
145
|
+
// first (invisible to user, visible to model), then falls back to ui/message.
|
|
146
|
+
const userLabel = sc.userInfo?.name ? ` as ${sc.userInfo.name}` : ''
|
|
147
|
+
await updateModelContext(
|
|
148
|
+
`CRM authentication with ${connectorDisplayName} completed${userLabel}. ` +
|
|
149
|
+
`Platform: ${connectorName}. ` +
|
|
150
|
+
`jwtToken: ${sc.jwtToken}. ` +
|
|
151
|
+
`IMPORTANT: Store this jwtToken and pass it as a parameter to all future CRM tool calls.`
|
|
152
|
+
)
|
|
153
|
+
dbg.info('updateModelContext sent with jwtToken')
|
|
154
|
+
|
|
155
|
+
onSuccess({
|
|
156
|
+
jwtToken: sc.jwtToken,
|
|
157
|
+
userInfo: sc.userInfo,
|
|
158
|
+
})
|
|
159
|
+
} else if (dataStatus === 'failed') {
|
|
160
|
+
if (pollTimerRef.current) clearInterval(pollTimerRef.current)
|
|
161
|
+
setStatus('error')
|
|
162
|
+
setErrorMsg(sc?.errorMessage || 'Authorization failed')
|
|
163
|
+
} else if (dataStatus === 'expired') {
|
|
164
|
+
if (pollTimerRef.current) clearInterval(pollTimerRef.current)
|
|
165
|
+
setStatus('error')
|
|
166
|
+
setErrorMsg('Authorization session expired. Please try again.')
|
|
167
|
+
}
|
|
168
|
+
// 'pending' -> keep polling
|
|
169
|
+
} catch {
|
|
170
|
+
// Transient errors during polling are acceptable; keep trying
|
|
171
|
+
}
|
|
172
|
+
}, POLL_INTERVAL_MS)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const handleAuthorizeClick = () => {
|
|
176
|
+
if (authUri && sessionReady) {
|
|
177
|
+
window.open(authUri, '_blank', 'noopener,noreferrer')
|
|
178
|
+
startPolling()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const handleRetry = () => {
|
|
183
|
+
setStatus('error')
|
|
184
|
+
setErrorMsg(null)
|
|
185
|
+
onError('retry')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Error state
|
|
189
|
+
if (status === 'error') {
|
|
190
|
+
return (
|
|
191
|
+
<div className="w-full max-w-md">
|
|
192
|
+
<div className="mb-4">
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
onClick={onBack}
|
|
196
|
+
className="text-sm text-secondary hover:text-primary transition-colors mb-2 cursor-pointer"
|
|
197
|
+
>
|
|
198
|
+
← Back
|
|
199
|
+
</button>
|
|
200
|
+
<h2 className="heading-lg">Authentication Error</h2>
|
|
201
|
+
</div>
|
|
202
|
+
<div className="rounded-lg border border-red-200 bg-red-50 p-4 mb-4">
|
|
203
|
+
<p className="text-red-700 text-sm">{errorMsg}</p>
|
|
204
|
+
</div>
|
|
205
|
+
<Button color="primary" size="sm" onClick={handleRetry}>
|
|
206
|
+
Try Again
|
|
207
|
+
</Button>
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Ready to authorize or polling
|
|
213
|
+
return (
|
|
214
|
+
<div className="w-full max-w-md">
|
|
215
|
+
<div className="mb-4">
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
onClick={onBack}
|
|
219
|
+
className="text-sm text-secondary hover:text-primary transition-colors mb-2 cursor-pointer"
|
|
220
|
+
>
|
|
221
|
+
← Back
|
|
222
|
+
</button>
|
|
223
|
+
<h2 className="heading-lg">Authorize {connectorDisplayName}</h2>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div className="rounded-xl border border-default bg-surface p-4 space-y-4">
|
|
227
|
+
{status === 'ready' && (
|
|
228
|
+
<>
|
|
229
|
+
<p className="text-sm text-secondary">
|
|
230
|
+
Click the button below to open {connectorDisplayName}'s authorization
|
|
231
|
+
page. After you approve access, return here to continue.
|
|
232
|
+
</p>
|
|
233
|
+
<Button
|
|
234
|
+
color="primary"
|
|
235
|
+
size="md"
|
|
236
|
+
onClick={handleAuthorizeClick}
|
|
237
|
+
disabled={!sessionReady}
|
|
238
|
+
>
|
|
239
|
+
{sessionReady
|
|
240
|
+
? `Authorize in ${connectorDisplayName}`
|
|
241
|
+
: 'Preparing…'}
|
|
242
|
+
</Button>
|
|
243
|
+
</>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{status === 'polling' && (
|
|
247
|
+
<div className="text-center space-y-3">
|
|
248
|
+
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-primary mx-auto" />
|
|
249
|
+
<p className="text-sm text-secondary">
|
|
250
|
+
Waiting for you to authorize in {connectorDisplayName}...
|
|
251
|
+
</p>
|
|
252
|
+
<p className="text-xs text-secondary opacity-60">
|
|
253
|
+
Complete the authorization in the opened tab, then come back here.
|
|
254
|
+
</p>
|
|
255
|
+
{authUri && (
|
|
256
|
+
<a
|
|
257
|
+
href={authUri}
|
|
258
|
+
target="_blank"
|
|
259
|
+
rel="noopener noreferrer"
|
|
260
|
+
className="text-xs text-primary hover:underline"
|
|
261
|
+
>
|
|
262
|
+
Open authorization page again
|
|
263
|
+
</a>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
)
|
|
270
|
+
}
|