@@ -6,23 +6,16 @@ import type {
66 IAdminForth ,
77} from "adminforth" ;
88import { Filters , logger } from "adminforth" ;
9- import { randomUUID } from "crypto" ;
109import type { AgentEventEmitter } from "./agentEvents.js" ;
1110import type {
1211 HandleTurnInput ,
1312 RunAndPersistAgentResponseInput ,
1413 RunAndPersistAgentResponseResult ,
1514} from "./agentTurnService.js" ;
16- import type { PluginOptions } from "./types.js" ;
17- import type { AgentSessionStore } from "./sessionStore.js" ;
1815import { getErrorMessage , isAbortError } from "./errors.js" ;
16+ import type { AgentSessionStore } from "./sessionStore.js" ;
1917import { sanitizeSpeechText } from "./sanitizeSpeechText.js" ;
20-
21- type ChatSurfaceConnectAction = {
22- type : "url" ;
23- label : string ;
24- url : string ;
25- } ;
18+ import type { PluginOptions } from "./types.js" ;
2619
2720type ChatSurfaceIncomingMessageWithAudio = ChatSurfaceIncomingMessage & {
2821 audio ?: {
@@ -41,24 +34,7 @@ type ChatSurfaceEventSinkWithAudio = ChatSurfaceEventSink & {
4134 } ) : void | Promise < void > ;
4235} ;
4336
44- export type ChatSurfaceAdapterWithConnectAction = ChatSurfaceAdapter & {
45- createConnectAction ?( input : {
46- token : string ;
47- } ) : ChatSurfaceConnectAction | Promise < ChatSurfaceConnectAction > ;
48- } ;
49-
50- type ChatSurfaceLinkTokenPayload = {
51- surface : string ;
52- adminUserId : AdminUser [ "pk" ] ;
53- expiresAt : number ;
54- } ;
55-
56- const DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD = "externalUserId" ;
57- const CHAT_SURFACE_LINK_TOKEN_TTL_MS = 60 * 1000 ;
58-
5937export class ChatSurfaceService {
60- private linkTokens = new Map < string , ChatSurfaceLinkTokenPayload > ( ) ;
61-
6238 constructor (
6339 private getAdminforth : ( ) => IAdminForth ,
6440 private options : PluginOptions ,
@@ -69,40 +45,6 @@ export class ChatSurfaceService {
6945 ) => Promise < RunAndPersistAgentResponseResult > ,
7046 ) { }
7147
72- getConnectActionAdapters ( ) {
73- return ( this . options . chatSurfaceAdapters ?? [ ] )
74- . map ( ( adapter ) => adapter as ChatSurfaceAdapterWithConnectAction )
75- . filter ( ( adapter ) => adapter . createConnectAction ) ;
76- }
77-
78- createLinkToken ( surface : string , adminUser : AdminUser ) {
79- for ( const [ token , payload ] of this . linkTokens ) {
80- if ( payload . expiresAt <= Date . now ( ) ) {
81- this . linkTokens . delete ( token ) ;
82- }
83- }
84-
85- const token = randomUUID ( ) ;
86- this . linkTokens . set ( token , {
87- surface,
88- adminUserId : adminUser . pk ,
89- expiresAt : Date . now ( ) + CHAT_SURFACE_LINK_TOKEN_TTL_MS ,
90- } ) ;
91-
92- return token ;
93- }
94-
95- private consumeLinkToken ( surface : string , token : string ) {
96- const payload = this . linkTokens . get ( token ) ;
97- this . linkTokens . delete ( token ) ;
98-
99- if ( ! payload || payload . surface !== surface || payload . expiresAt <= Date . now ( ) ) {
100- return null ;
101- }
102-
103- return payload ;
104- }
105-
10648 private createEventEmitter ( sink : ChatSurfaceEventSink ) : AgentEventEmitter {
10749 return async ( event ) => {
10850 if ( event . type === "text-delta" ) {
@@ -138,55 +80,10 @@ export class ChatSurfaceService {
13880 return false ;
13981 }
14082
141- const externalUserIdField = this . options . chatExternalIdsField ?? DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD ;
142- const adminforth = this . getAdminforth ( ) ;
143- const authResourceId = adminforth . config . auth ! . usersResourceId ! ;
144- const authResource = adminforth . config . resources . find ( ( resource ) => resource . resourceId === authResourceId ) ! ;
145- const primaryKeyField = authResource . columns . find ( ( column ) => column . primaryKey ) ! . name ! ;
146- const linkedAdminUserRecord = (
147- await adminforth . resource ( authResourceId ) . list ( Filters . IS_NOT_EMPTY ( externalUserIdField ) )
148- ) . find ( ( user ) => user [ externalUserIdField ] ?. [ incoming . surface ] === incoming . externalUserId ) ;
149-
150- if ( linkedAdminUserRecord ) {
151- await sink . emit ( {
152- type : "done" ,
153- text : `${ incoming . surface } account is already connected to AdminForth.` ,
154- } ) ;
155- return true ;
156- }
157-
158- if ( typeof incoming . metadata ?. startPayload !== "string" ) {
159- await sink . emit ( {
160- type : "done" ,
161- text : `Open AdminForth and connect your ${ incoming . surface } account from Chat Surfaces settings.` ,
162- } ) ;
163- return true ;
164- }
165-
166- const payload = this . consumeLinkToken ( incoming . surface , incoming . metadata . startPayload ) ;
167- if ( ! payload ) {
168- await sink . emit ( {
169- type : "error" ,
170- message : "This chat surface link is expired or invalid. Please start linking again from AdminForth." ,
171- } ) ;
172- return true ;
173- }
174-
175- const adminUserRecord = await adminforth . resource ( authResourceId ) . get ( [
176- Filters . EQ ( primaryKeyField , payload . adminUserId ) ,
177- ] ) ;
178-
179- await adminforth . resource ( authResourceId ) . update ( payload . adminUserId , {
180- [ externalUserIdField ] : {
181- ...( adminUserRecord [ externalUserIdField ] ?? { } ) ,
182- [ incoming . surface ] : incoming . externalUserId ,
183- } ,
184- } ) ;
18583 await sink . emit ( {
18684 type : "done" ,
187- text : `${ incoming . surface } account connected to AdminForth .` ,
85+ text : `Open AdminForth and connect your ${ incoming . surface } account from Connected Accounts settings .` ,
18886 } ) ;
189-
19087 return true ;
19188 }
19289
@@ -327,6 +224,54 @@ export class ChatSurfaceService {
327224 return agentResponse ;
328225 }
329226
227+ private async getAdminUserRecordForChatSurface (
228+ adapter : ChatSurfaceAdapter ,
229+ incoming : ChatSurfaceIncomingMessage ,
230+ ) {
231+ const adminforth = this . getAdminforth ( ) ;
232+ const authResourceId = adminforth . config . auth ! . usersResourceId ! ;
233+ const externalIdentityResource = this . options . chatExternalIdentityResource ;
234+ if ( ! externalIdentityResource ) {
235+ return null ;
236+ }
237+
238+ const surfaceIdentityConfig = externalIdentityResource . surfaces [ adapter . name ] ;
239+ if ( ! surfaceIdentityConfig ) {
240+ return null ;
241+ }
242+
243+ const providerField = externalIdentityResource . providerField ?? 'provider' ;
244+ const subjectField = externalIdentityResource . subjectField ?? 'subject' ;
245+ const adminUserIdField = externalIdentityResource . adminUserIdField ?? 'adminUserId' ;
246+ const externalUserIdField = externalIdentityResource . externalUserIdField ?? 'externalUserId' ;
247+ const identityFilters = [
248+ Filters . EQ ( providerField , surfaceIdentityConfig . provider ) ,
249+ Filters . EQ ( externalUserIdField , incoming . externalUserId ) ,
250+ ] ;
251+ const identities = await adminforth . resource ( externalIdentityResource . resourceId ) . list ( identityFilters ) ;
252+ const identity = identities . find ( ( identity ) => {
253+ if ( String ( identity [ externalUserIdField ] ) === incoming . externalUserId ) {
254+ return true ;
255+ }
256+
257+ if ( String ( identity [ subjectField ] ) === incoming . externalUserId ) {
258+ return true ;
259+ }
260+
261+ return false ;
262+ } ) ;
263+
264+ if ( ! identity ) {
265+ return null ;
266+ }
267+
268+ const authResource = adminforth . config . resources . find ( ( resource ) => resource . resourceId === authResourceId ) ! ;
269+ const primaryKeyField = authResource . columns . find ( ( column ) => column . primaryKey ) ! . name ! ;
270+ return adminforth . resource ( authResourceId ) . get ( [
271+ Filters . EQ ( primaryKeyField , identity [ adminUserIdField ] ) ,
272+ ] ) ;
273+ }
274+
330275 async handleMessage (
331276 adapter : ChatSurfaceAdapter ,
332277 incoming : ChatSurfaceIncomingMessage ,
@@ -340,10 +285,7 @@ export class ChatSurfaceService {
340285 const authResourceId = adminforth . config . auth ! . usersResourceId ! ;
341286 const authResource = adminforth . config . resources . find ( ( resource ) => resource . resourceId === authResourceId ) ! ;
342287 const primaryKeyField = authResource . columns . find ( ( column ) => column . primaryKey ) ! . name ! ;
343- const externalUserIdField = this . options . chatExternalIdsField ?? DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD ;
344- const adminUserRecord = (
345- await adminforth . resource ( authResourceId ) . list ( Filters . IS_NOT_EMPTY ( externalUserIdField ) )
346- ) . find ( ( user ) => user [ externalUserIdField ] ?. [ adapter . name ] === incoming . externalUserId ) ;
288+ const adminUserRecord = await this . getAdminUserRecordForChatSurface ( adapter , incoming ) ;
347289
348290 if ( ! adminUserRecord ) {
349291 await sink . emit ( {
0 commit comments