@adminforth/agent 1.44.1 → 1.45.0

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/build.log CHANGED
@@ -7,6 +7,7 @@ custom/
7
7
  custom/ChatFooter.vue
8
8
  custom/ChatHeader.vue
9
9
  custom/ChatSurface.vue
10
+ custom/ChatSurfaceSettings.vue
10
11
  custom/CustomAutoScrollContainer.vue
11
12
  custom/SessionsHistory.vue
12
13
  custom/chat.ts
@@ -62,5 +63,5 @@ custom/speech_recognition_frontend/voiceActivityDetection.ts
62
63
  custom/speech_recognition_frontend/types/
63
64
  custom/speech_recognition_frontend/types/voice-activity-detection.d.ts
64
65
 
65
- sent 1,667,285 bytes received 905 bytes 3,336,380.00 bytes/sec
66
- total size is 1,663,132 speedup is 1.00
66
+ sent 1,671,609 bytes received 940 bytes 3,345,098.00 bytes/sec
67
+ total size is 1,667,436 speedup is 1.00
@@ -106,7 +106,13 @@ onMounted(async () => {
106
106
  if( coreStore.isMobile ) {
107
107
  agentStore.setIsTeleportedToBody(false);
108
108
  } else {
109
- agentStore.setIsTeleportedToBody(isTeleportedToBodyFromLocalStorage || props.meta.stickByDefault);
109
+ const shouldTeleportToBody = isTeleportedToBodyFromLocalStorage || props.meta.stickByDefault;
110
+ const savedIsChatOpen = agentStore.getLocalStorageItem('isChatOpen');
111
+
112
+ agentStore.setIsTeleportedToBody(shouldTeleportToBody);
113
+ if (shouldTeleportToBody && savedIsChatOpen === null) {
114
+ agentStore.setIsChatOpen(true);
115
+ }
110
116
  }
111
117
  await agentStore.fetchSessionsList();
112
118
  });
@@ -0,0 +1,125 @@
1
+ <template>
2
+ <div class="flex flex-col justify-center mr-6 md:mr-12">
3
+ <h2 class="flex items-start justify-start leading-none text-gray-800 dark:text-gray-50 text-3xl font-semibold">
4
+ {{ $t('Chat Surfaces') }}
5
+ </h2>
6
+ <p class="text-sm mt-3">
7
+ {{ $t('Connect external chat accounts to your AdminForth user') }}
8
+ </p>
9
+
10
+ <div class="mt-6 flex flex-wrap gap-4">
11
+ <div
12
+ v-for="surface in surfaces"
13
+ :key="surface.name"
14
+ class="flex flex-col w-full lg:w-72 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 shadow-sm"
15
+ >
16
+ <div class="flex items-center justify-between gap-3 mb-4">
17
+ <div class="min-w-0">
18
+ <p class="font-semibold text-gray-900 dark:text-white truncate">
19
+ {{ formatSurfaceName(surface.name) }}
20
+ </p>
21
+ <p class="text-xs text-gray-500 dark:text-gray-400">
22
+ {{ surface.externalUserId || $t('Not connected') }}
23
+ </p>
24
+ </div>
25
+ <span
26
+ class="shrink-0 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
27
+ :class="surface.externalUserId
28
+ ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
29
+ : 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'"
30
+ >
31
+ {{ surface.externalUserId ? $t('Active') : $t('Inactive') }}
32
+ </span>
33
+ </div>
34
+
35
+ <div class="grid gap-2 mt-auto" :class="surface.externalUserId ? 'grid-cols-2' : 'grid-cols-1'">
36
+ <Button
37
+ class="w-full"
38
+ :disabled="isSurfaceBusy(surface.name)"
39
+ :loader="connectingSurfaceName === surface.name"
40
+ @click="connectSurface(surface.name)"
41
+ >
42
+ {{ surface.externalUserId ? $t('Reconnect') : $t('Connect') }}
43
+ </Button>
44
+ <Button
45
+ v-if="surface.externalUserId"
46
+ class="w-full"
47
+ :disabled="isSurfaceBusy(surface.name)"
48
+ :loader="disconnectingSurfaceName === surface.name"
49
+ @click="disconnectSurface(surface.name)"
50
+ >
51
+ {{ $t('Disconnect') }}
52
+ </Button>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </template>
58
+
59
+ <script setup lang="ts">
60
+ import { onMounted, ref } from 'vue';
61
+ import { Button } from '@/afcl';
62
+ import { callAdminForthApi } from '@/utils';
63
+
64
+ type ChatSurface = {
65
+ name: string;
66
+ externalUserId: string | null;
67
+ };
68
+
69
+ const surfaces = ref<ChatSurface[]>([]);
70
+ const connectingSurfaceName = ref<string | null>(null);
71
+ const disconnectingSurfaceName = ref<string | null>(null);
72
+
73
+ onMounted(loadSurfaces);
74
+
75
+ async function loadSurfaces() {
76
+ const response = await callAdminForthApi({
77
+ method: 'POST',
78
+ path: '/agent/surfaces/connectable',
79
+ body: {},
80
+ });
81
+
82
+ surfaces.value = response.surfaces;
83
+ }
84
+
85
+ async function connectSurface(surfaceName: string) {
86
+ connectingSurfaceName.value = surfaceName;
87
+
88
+ try {
89
+ const response = await callAdminForthApi({
90
+ method: 'POST',
91
+ path: `/agent/surface/${surfaceName}/connect-action`,
92
+ body: {},
93
+ });
94
+
95
+ if (response.action.type === 'url') {
96
+ window.open(response.action.url, '_blank', 'noopener,noreferrer');
97
+ }
98
+ } finally {
99
+ connectingSurfaceName.value = null;
100
+ }
101
+ }
102
+
103
+ async function disconnectSurface(surfaceName: string) {
104
+ disconnectingSurfaceName.value = surfaceName;
105
+
106
+ try {
107
+ await callAdminForthApi({
108
+ method: 'POST',
109
+ path: `/agent/surface/${surfaceName}/disconnect`,
110
+ body: {},
111
+ });
112
+ await loadSurfaces();
113
+ } finally {
114
+ disconnectingSurfaceName.value = null;
115
+ }
116
+ }
117
+
118
+ function isSurfaceBusy(surfaceName: string) {
119
+ return connectingSurfaceName.value === surfaceName || disconnectingSurfaceName.value === surfaceName;
120
+ }
121
+
122
+ function formatSurfaceName(surfaceName: string) {
123
+ return surfaceName.charAt(0).toUpperCase() + surfaceName.slice(1);
124
+ }
125
+ </script>
@@ -174,7 +174,10 @@ export const useAgentStore = defineStore('agent', () => {
174
174
  if (!coreStore.isMobile) {
175
175
  const savedIsTeleportedToBody = getLocalStorageItem('isTeleportedToBody');
176
176
  const savedIsTeleportedToBodyBeforeFullScreen = getLocalStorageItem('isTeleportedToBodyBeforeFullScreen');
177
- const isTeleportedToBodyFromLocalStorage = savedIsTeleportedToBody === 'true' || savedIsTeleportedToBodyBeforeFullScreen === 'true';
177
+ let isTeleportedToBodyFromLocalStorage = true;
178
+ if (savedIsTeleportedToBody !== null || savedIsTeleportedToBodyBeforeFullScreen !== null) {
179
+ isTeleportedToBodyFromLocalStorage = savedIsTeleportedToBody === 'true' || savedIsTeleportedToBodyBeforeFullScreen === 'true';
180
+ }
178
181
  const savedIsChatOpen = getLocalStorageItem('isChatOpen');
179
182
 
180
183
  setIsTeleportedToBody(isTeleportedToBodyFromLocalStorage);
@@ -374,4 +377,4 @@ export const useAgentStore = defineStore('agent', () => {
374
377
  setCurrentChatStatus,
375
378
  updateLastAgentMessage
376
379
  }
377
- })
380
+ })
@@ -625,11 +625,11 @@ function sanitizeClassToken(value: string): string {
625
625
 
626
626
  function escapeHtml(value: string): string {
627
627
  return value
628
- .replaceAll('&', '&amp;')
629
- .replaceAll('<', '&lt;')
630
- .replaceAll('>', '&gt;')
631
- .replaceAll('"', '&quot;')
632
- .replaceAll("'", '&#39;');
628
+ .replace(/&/g, '&amp;')
629
+ .replace(/</g, '&lt;')
630
+ .replace(/>/g, '&gt;')
631
+ .replace(/"/g, '&quot;')
632
+ .replace(/'/g, '&#39;');
633
633
  }
634
634
 
635
635
  function escapeAttribute(value: string): string {
@@ -106,7 +106,13 @@ onMounted(async () => {
106
106
  if( coreStore.isMobile ) {
107
107
  agentStore.setIsTeleportedToBody(false);
108
108
  } else {
109
- agentStore.setIsTeleportedToBody(isTeleportedToBodyFromLocalStorage || props.meta.stickByDefault);
109
+ const shouldTeleportToBody = isTeleportedToBodyFromLocalStorage || props.meta.stickByDefault;
110
+ const savedIsChatOpen = agentStore.getLocalStorageItem('isChatOpen');
111
+
112
+ agentStore.setIsTeleportedToBody(shouldTeleportToBody);
113
+ if (shouldTeleportToBody && savedIsChatOpen === null) {
114
+ agentStore.setIsChatOpen(true);
115
+ }
110
116
  }
111
117
  await agentStore.fetchSessionsList();
112
118
  });
@@ -0,0 +1,125 @@
1
+ <template>
2
+ <div class="flex flex-col justify-center mr-6 md:mr-12">
3
+ <h2 class="flex items-start justify-start leading-none text-gray-800 dark:text-gray-50 text-3xl font-semibold">
4
+ {{ $t('Chat Surfaces') }}
5
+ </h2>
6
+ <p class="text-sm mt-3">
7
+ {{ $t('Connect external chat accounts to your AdminForth user') }}
8
+ </p>
9
+
10
+ <div class="mt-6 flex flex-wrap gap-4">
11
+ <div
12
+ v-for="surface in surfaces"
13
+ :key="surface.name"
14
+ class="flex flex-col w-full lg:w-72 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 shadow-sm"
15
+ >
16
+ <div class="flex items-center justify-between gap-3 mb-4">
17
+ <div class="min-w-0">
18
+ <p class="font-semibold text-gray-900 dark:text-white truncate">
19
+ {{ formatSurfaceName(surface.name) }}
20
+ </p>
21
+ <p class="text-xs text-gray-500 dark:text-gray-400">
22
+ {{ surface.externalUserId || $t('Not connected') }}
23
+ </p>
24
+ </div>
25
+ <span
26
+ class="shrink-0 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
27
+ :class="surface.externalUserId
28
+ ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
29
+ : 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'"
30
+ >
31
+ {{ surface.externalUserId ? $t('Active') : $t('Inactive') }}
32
+ </span>
33
+ </div>
34
+
35
+ <div class="grid gap-2 mt-auto" :class="surface.externalUserId ? 'grid-cols-2' : 'grid-cols-1'">
36
+ <Button
37
+ class="w-full"
38
+ :disabled="isSurfaceBusy(surface.name)"
39
+ :loader="connectingSurfaceName === surface.name"
40
+ @click="connectSurface(surface.name)"
41
+ >
42
+ {{ surface.externalUserId ? $t('Reconnect') : $t('Connect') }}
43
+ </Button>
44
+ <Button
45
+ v-if="surface.externalUserId"
46
+ class="w-full"
47
+ :disabled="isSurfaceBusy(surface.name)"
48
+ :loader="disconnectingSurfaceName === surface.name"
49
+ @click="disconnectSurface(surface.name)"
50
+ >
51
+ {{ $t('Disconnect') }}
52
+ </Button>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </template>
58
+
59
+ <script setup lang="ts">
60
+ import { onMounted, ref } from 'vue';
61
+ import { Button } from '@/afcl';
62
+ import { callAdminForthApi } from '@/utils';
63
+
64
+ type ChatSurface = {
65
+ name: string;
66
+ externalUserId: string | null;
67
+ };
68
+
69
+ const surfaces = ref<ChatSurface[]>([]);
70
+ const connectingSurfaceName = ref<string | null>(null);
71
+ const disconnectingSurfaceName = ref<string | null>(null);
72
+
73
+ onMounted(loadSurfaces);
74
+
75
+ async function loadSurfaces() {
76
+ const response = await callAdminForthApi({
77
+ method: 'POST',
78
+ path: '/agent/surfaces/connectable',
79
+ body: {},
80
+ });
81
+
82
+ surfaces.value = response.surfaces;
83
+ }
84
+
85
+ async function connectSurface(surfaceName: string) {
86
+ connectingSurfaceName.value = surfaceName;
87
+
88
+ try {
89
+ const response = await callAdminForthApi({
90
+ method: 'POST',
91
+ path: `/agent/surface/${surfaceName}/connect-action`,
92
+ body: {},
93
+ });
94
+
95
+ if (response.action.type === 'url') {
96
+ window.open(response.action.url, '_blank', 'noopener,noreferrer');
97
+ }
98
+ } finally {
99
+ connectingSurfaceName.value = null;
100
+ }
101
+ }
102
+
103
+ async function disconnectSurface(surfaceName: string) {
104
+ disconnectingSurfaceName.value = surfaceName;
105
+
106
+ try {
107
+ await callAdminForthApi({
108
+ method: 'POST',
109
+ path: `/agent/surface/${surfaceName}/disconnect`,
110
+ body: {},
111
+ });
112
+ await loadSurfaces();
113
+ } finally {
114
+ disconnectingSurfaceName.value = null;
115
+ }
116
+ }
117
+
118
+ function isSurfaceBusy(surfaceName: string) {
119
+ return connectingSurfaceName.value === surfaceName || disconnectingSurfaceName.value === surfaceName;
120
+ }
121
+
122
+ function formatSurfaceName(surfaceName: string) {
123
+ return surfaceName.charAt(0).toUpperCase() + surfaceName.slice(1);
124
+ }
125
+ </script>
@@ -174,7 +174,10 @@ export const useAgentStore = defineStore('agent', () => {
174
174
  if (!coreStore.isMobile) {
175
175
  const savedIsTeleportedToBody = getLocalStorageItem('isTeleportedToBody');
176
176
  const savedIsTeleportedToBodyBeforeFullScreen = getLocalStorageItem('isTeleportedToBodyBeforeFullScreen');
177
- const isTeleportedToBodyFromLocalStorage = savedIsTeleportedToBody === 'true' || savedIsTeleportedToBodyBeforeFullScreen === 'true';
177
+ let isTeleportedToBodyFromLocalStorage = true;
178
+ if (savedIsTeleportedToBody !== null || savedIsTeleportedToBodyBeforeFullScreen !== null) {
179
+ isTeleportedToBodyFromLocalStorage = savedIsTeleportedToBody === 'true' || savedIsTeleportedToBodyBeforeFullScreen === 'true';
180
+ }
178
181
  const savedIsChatOpen = getLocalStorageItem('isChatOpen');
179
182
 
180
183
  setIsTeleportedToBody(isTeleportedToBodyFromLocalStorage);
@@ -374,4 +377,4 @@ export const useAgentStore = defineStore('agent', () => {
374
377
  setCurrentChatStatus,
375
378
  updateLastAgentMessage
376
379
  }
377
- })
380
+ })
@@ -625,11 +625,11 @@ function sanitizeClassToken(value: string): string {
625
625
 
626
626
  function escapeHtml(value: string): string {
627
627
  return value
628
- .replaceAll('&', '&amp;')
629
- .replaceAll('<', '&lt;')
630
- .replaceAll('>', '&gt;')
631
- .replaceAll('"', '&quot;')
632
- .replaceAll("'", '&#39;');
628
+ .replace(/&/g, '&amp;')
629
+ .replace(/</g, '&lt;')
630
+ .replace(/>/g, '&gt;')
631
+ .replace(/"/g, '&quot;')
632
+ .replace(/'/g, '&#39;');
633
633
  }
634
634
 
635
635
  function escapeAttribute(value: string): string {
package/dist/index.d.ts CHANGED
@@ -32,6 +32,8 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
32
32
  options: PluginOptions;
33
33
  agentSystemPromptPromise: Promise<string>;
34
34
  private checkpointer;
35
+ private chatSurfaceLinkTokens;
36
+ private chatSurfaceSettingsPageRegistered;
35
37
  private parseBody;
36
38
  private createNewTurn;
37
39
  private getSessionTurns;
@@ -40,6 +42,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
40
42
  private getOrCreateChatSurfaceSession;
41
43
  private getCheckpointer;
42
44
  private getInternalAgentResourceIds;
45
+ private getChatSurfaceConnectActionAdapters;
43
46
  constructor(options: PluginOptions);
44
47
  modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource): Promise<void>;
45
48
  validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource): void;
@@ -53,6 +56,9 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
53
56
  failed: boolean;
54
57
  }>;
55
58
  private createChatSurfaceEventEmitter;
59
+ private createChatSurfaceLinkToken;
60
+ private consumeChatSurfaceLinkToken;
61
+ private handleChatSurfaceLink;
56
62
  private handleChatSurfaceMessage;
57
63
  setupEndpoints(server: IHttpServer): void;
58
64
  }
package/dist/index.js CHANGED
@@ -50,6 +50,8 @@ const createSessionBodySchema = z.object({
50
50
  }).strict();
51
51
  const VEGA_LITE_FENCE_START = "```vega-lite";
52
52
  const COMPLETE_VEGA_LITE_BLOCK_RE = /```vega-lite[\s\S]*?```/;
53
+ const DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD = "externalUserId";
54
+ const CHAT_SURFACE_LINK_TOKEN_TTL_MS = 60 * 1000;
53
55
  function isAbortError(error) {
54
56
  return (error instanceof DOMException && error.name === "AbortError") || (typeof error === "object" &&
55
57
  error !== null &&
@@ -59,12 +61,6 @@ function isAbortError(error) {
59
61
  function getErrorMessage(error) {
60
62
  return error instanceof Error ? error.message : String(error);
61
63
  }
62
- function requireAdminUser(adminUser) {
63
- if (!adminUser) {
64
- throw new Error("AdminForth Agent endpoint requires an authenticated admin user");
65
- }
66
- return adminUser;
67
- }
68
64
  export default class AdminForthAgentPlugin extends AdminForthPlugin {
69
65
  parseBody(schema, body, response) {
70
66
  const parsed = schema.safeParse(body !== null && body !== void 0 ? body : {});
@@ -141,9 +137,17 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
141
137
  (_a = this.options.checkpointResource) === null || _a === void 0 ? void 0 : _a.resourceId,
142
138
  ].filter((resourceId) => Boolean(resourceId));
143
139
  }
140
+ getChatSurfaceConnectActionAdapters() {
141
+ var _a;
142
+ return ((_a = this.options.chatSurfaceAdapters) !== null && _a !== void 0 ? _a : [])
143
+ .map((adapter) => adapter)
144
+ .filter((adapter) => adapter.createConnectAction);
145
+ }
144
146
  constructor(options) {
145
147
  super(options, import.meta.url);
146
148
  this.checkpointer = null;
149
+ this.chatSurfaceLinkTokens = new Map();
150
+ this.chatSurfaceSettingsPageRegistered = false;
147
151
  this.options = options;
148
152
  this.agentSystemPromptPromise = Promise.resolve(appendCustomSystemPrompt(DEFAULT_AGENT_SYSTEM_PROMPT, this.options.systemPrompt));
149
153
  this.shouldHaveSingleInstancePerWholeApp = () => false;
@@ -171,6 +175,19 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
171
175
  hasAudioAdapter: Boolean(this.options.audioAdapter),
172
176
  }
173
177
  });
178
+ if (this.getChatSurfaceConnectActionAdapters().length && !this.chatSurfaceSettingsPageRegistered) {
179
+ if (!this.adminforth.config.auth.userMenuSettingsPages) {
180
+ this.adminforth.config.auth.userMenuSettingsPages = [];
181
+ }
182
+ this.adminforth.config.auth.userMenuSettingsPages.push({
183
+ icon: "flowbite:link-outline",
184
+ pageLabel: "Chat Surfaces",
185
+ slug: "chat-surfaces",
186
+ component: this.componentPath("ChatSurfaceSettings.vue"),
187
+ isVisible: () => true,
188
+ });
189
+ this.chatSurfaceSettingsPageRegistered = true;
190
+ }
174
191
  if (!this.adminforth.config.customization.customHeadItems) {
175
192
  this.adminforth.config.customization.customHeadItems = [];
176
193
  }
@@ -485,25 +502,87 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
485
502
  }
486
503
  });
487
504
  }
488
- handleChatSurfaceMessage(adapter, incoming, sink) {
505
+ createChatSurfaceLinkToken(surface, adminUser) {
506
+ for (const [token, payload] of this.chatSurfaceLinkTokens) {
507
+ if (payload.expiresAt <= Date.now()) {
508
+ this.chatSurfaceLinkTokens.delete(token);
509
+ }
510
+ }
511
+ const token = randomUUID();
512
+ this.chatSurfaceLinkTokens.set(token, {
513
+ surface,
514
+ adminUserId: adminUser.pk,
515
+ expiresAt: Date.now() + CHAT_SURFACE_LINK_TOKEN_TTL_MS,
516
+ });
517
+ return token;
518
+ }
519
+ consumeChatSurfaceLinkToken(surface, token) {
520
+ const payload = this.chatSurfaceLinkTokens.get(token);
521
+ this.chatSurfaceLinkTokens.delete(token);
522
+ if (!payload || payload.surface !== surface || payload.expiresAt <= Date.now()) {
523
+ return null;
524
+ }
525
+ return payload;
526
+ }
527
+ handleChatSurfaceLink(incoming, sink) {
489
528
  return __awaiter(this, void 0, void 0, function* () {
490
- var _a;
491
- const adminUser = yield adapter.resolveAdminUser({
492
- adminforth: this.adminforth,
493
- incoming,
529
+ var _a, _b, _c;
530
+ if (typeof ((_a = incoming.metadata) === null || _a === void 0 ? void 0 : _a.startPayload) !== "string") {
531
+ return false;
532
+ }
533
+ const payload = this.consumeChatSurfaceLinkToken(incoming.surface, incoming.metadata.startPayload);
534
+ if (!payload) {
535
+ yield sink.emit({
536
+ type: "error",
537
+ message: "This chat surface link is expired or invalid. Please start linking again from AdminForth.",
538
+ });
539
+ return true;
540
+ }
541
+ const externalUserIdField = (_b = this.options.chatExternalIdsField) !== null && _b !== void 0 ? _b : DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
542
+ const authResourceId = this.adminforth.config.auth.usersResourceId;
543
+ const authResource = this.adminforth.config.resources.find((resource) => resource.resourceId === authResourceId);
544
+ const primaryKeyField = authResource.columns.find((column) => column.primaryKey).name;
545
+ const adminUserRecord = yield this.adminforth.resource(authResourceId).get([
546
+ Filters.EQ(primaryKeyField, payload.adminUserId),
547
+ ]);
548
+ yield this.adminforth.resource(authResourceId).update(payload.adminUserId, {
549
+ [externalUserIdField]: Object.assign(Object.assign({}, ((_c = adminUserRecord[externalUserIdField]) !== null && _c !== void 0 ? _c : {})), { [incoming.surface]: incoming.externalUserId }),
550
+ });
551
+ yield sink.emit({
552
+ type: "done",
553
+ text: `${incoming.surface} account connected to AdminForth.`,
494
554
  });
495
- if (!adminUser) {
555
+ return true;
556
+ });
557
+ }
558
+ handleChatSurfaceMessage(adapter, incoming, sink) {
559
+ return __awaiter(this, void 0, void 0, function* () {
560
+ var _a, _b;
561
+ if (yield this.handleChatSurfaceLink(incoming, sink)) {
562
+ return;
563
+ }
564
+ const authResourceId = this.adminforth.config.auth.usersResourceId;
565
+ const authResource = this.adminforth.config.resources.find((resource) => resource.resourceId === authResourceId);
566
+ const primaryKeyField = authResource.columns.find((column) => column.primaryKey).name;
567
+ const externalUserIdField = (_a = this.options.chatExternalIdsField) !== null && _a !== void 0 ? _a : DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
568
+ const adminUserRecord = (yield this.adminforth.resource(authResourceId).list(Filters.IS_NOT_EMPTY(externalUserIdField))).find((user) => { var _a; return ((_a = user[externalUserIdField]) === null || _a === void 0 ? void 0 : _a[adapter.name]) === incoming.externalUserId; });
569
+ if (!adminUserRecord) {
496
570
  yield sink.emit({
497
571
  type: "error",
498
572
  message: "This chat account is not authorized to use AdminForth Agent.",
499
573
  });
500
574
  return;
501
575
  }
576
+ const adminUser = {
577
+ pk: adminUserRecord[primaryKeyField],
578
+ username: adminUserRecord[this.adminforth.config.auth.usernameField],
579
+ dbUser: adminUserRecord,
580
+ };
502
581
  yield this.handleTurn({
503
582
  prompt: incoming.prompt,
504
583
  sessionId: yield this.getOrCreateChatSurfaceSession(incoming, adminUser),
505
584
  modeName: incoming.modeName,
506
- userTimeZone: (_a = incoming.userTimeZone) !== null && _a !== void 0 ? _a : "UTC",
585
+ userTimeZone: (_b = incoming.userTimeZone) !== null && _b !== void 0 ? _b : "UTC",
507
586
  adminUser,
508
587
  emit: this.createChatSurfaceEventEmitter(sink),
509
588
  failureLogMessage: `Agent ${incoming.surface} surface response failed`,
@@ -513,7 +592,57 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
513
592
  }
514
593
  setupEndpoints(server) {
515
594
  var _a;
595
+ if (this.getChatSurfaceConnectActionAdapters().length) {
596
+ server.endpoint({
597
+ method: "POST",
598
+ path: "/agent/surfaces/connectable",
599
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser }) {
600
+ var _b, _c;
601
+ const externalUserIdField = (_b = this.options.chatExternalIdsField) !== null && _b !== void 0 ? _b : DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
602
+ const externalIds = (_c = adminUser.dbUser[externalUserIdField]) !== null && _c !== void 0 ? _c : {};
603
+ return {
604
+ surfaces: this.getChatSurfaceConnectActionAdapters().map((adapter) => {
605
+ var _a;
606
+ return ({
607
+ name: adapter.name,
608
+ externalUserId: (_a = externalIds[adapter.name]) !== null && _a !== void 0 ? _a : null,
609
+ });
610
+ }),
611
+ };
612
+ }),
613
+ });
614
+ }
516
615
  for (const adapter of (_a = this.options.chatSurfaceAdapters) !== null && _a !== void 0 ? _a : []) {
616
+ const connectActionAdapter = adapter;
617
+ if (connectActionAdapter.createConnectAction) {
618
+ server.endpoint({
619
+ method: "POST",
620
+ path: `/agent/surface/${adapter.name}/connect-action`,
621
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser }) {
622
+ const token = this.createChatSurfaceLinkToken(adapter.name, adminUser);
623
+ const action = yield connectActionAdapter.createConnectAction({ token });
624
+ return {
625
+ action,
626
+ };
627
+ }),
628
+ });
629
+ server.endpoint({
630
+ method: "POST",
631
+ path: `/agent/surface/${adapter.name}/disconnect`,
632
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser }) {
633
+ var _b, _c;
634
+ const externalUserIdField = (_b = this.options.chatExternalIdsField) !== null && _b !== void 0 ? _b : DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
635
+ const externalIds = Object.assign({}, ((_c = adminUser.dbUser[externalUserIdField]) !== null && _c !== void 0 ? _c : {}));
636
+ delete externalIds[adapter.name];
637
+ yield this.adminforth.resource(this.adminforth.config.auth.usersResourceId).update(adminUser.pk, {
638
+ [externalUserIdField]: externalIds,
639
+ });
640
+ return {
641
+ ok: true,
642
+ };
643
+ }),
644
+ });
645
+ }
517
646
  server.endpoint({
518
647
  method: "POST",
519
648
  noAuth: true,
@@ -545,14 +674,13 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
545
674
  method: 'POST',
546
675
  path: `/agent/get-placeholder-messages`,
547
676
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ headers, adminUser }) {
548
- const currentAdminUser = requireAdminUser(adminUser);
549
677
  if (!this.options.placeholderMessages) {
550
678
  return {
551
679
  messages: [],
552
680
  };
553
681
  }
554
682
  const messages = yield this.options.placeholderMessages({
555
- adminUser: currentAdminUser,
683
+ adminUser: adminUser,
556
684
  headers,
557
685
  });
558
686
  return {
@@ -565,7 +693,6 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
565
693
  path: `/agent/response`,
566
694
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, _raw_express_res, abortSignal }) {
567
695
  var _b;
568
- const currentAdminUser = requireAdminUser(adminUser);
569
696
  const data = this.parseBody(agentResponseBodySchema, body, response);
570
697
  if (!data)
571
698
  return;
@@ -580,7 +707,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
580
707
  userTimeZone: (_b = data.timeZone) !== null && _b !== void 0 ? _b : 'UTC',
581
708
  currentPage: data.currentPage,
582
709
  abortSignal,
583
- adminUser: currentAdminUser,
710
+ adminUser: adminUser,
584
711
  emit,
585
712
  failureLogMessage: "Agent response streaming failed",
586
713
  abortLogMessage: "Agent response streaming aborted by the client",
@@ -594,7 +721,6 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
594
721
  target: 'upload',
595
722
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, _raw_express_req, _raw_express_res, abortSignal }) {
596
723
  var _b;
597
- const currentAdminUser = requireAdminUser(adminUser);
598
724
  const req = _raw_express_req;
599
725
  const audioAdapter = this.options.audioAdapter;
600
726
  if (!audioAdapter) {
@@ -660,7 +786,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
660
786
  userTimeZone: (_b = data.timeZone) !== null && _b !== void 0 ? _b : 'UTC',
661
787
  currentPage,
662
788
  abortSignal,
663
- adminUser: currentAdminUser,
789
+ adminUser: adminUser,
664
790
  emit: (event) => __awaiter(this, void 0, void 0, function* () {
665
791
  if (event.type === "tool-call") {
666
792
  yield emit(event);
@@ -762,11 +888,10 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
762
888
  path: `/agent/get-sessions`,
763
889
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
764
890
  var _b;
765
- const currentAdminUser = requireAdminUser(adminUser);
766
891
  const data = this.parseBody(getSessionsBodySchema, body, response);
767
892
  if (!data)
768
893
  return;
769
- const userId = currentAdminUser.pk;
894
+ const userId = adminUser.pk;
770
895
  const limit = (_b = data.limit) !== null && _b !== void 0 ? _b : 20;
771
896
  const sessions = yield this.adminforth.resource(this.options.sessionResource.resourceId).list([Filters.EQ(this.options.sessionResource.askerIdField, userId)], limit, undefined, [Sorts.DESC(this.options.sessionResource.createdAtField)]);
772
897
  return {
@@ -782,13 +907,12 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
782
907
  method: 'POST',
783
908
  path: `/agent/get-session-info`,
784
909
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
785
- const currentAdminUser = requireAdminUser(adminUser);
786
910
  const parsedBody = sessionIdBodySchema.safeParse(body);
787
911
  if (!parsedBody.success) {
788
912
  response.setStatus(422, parsedBody.error.message);
789
913
  return;
790
914
  }
791
- const userId = currentAdminUser.pk;
915
+ const userId = adminUser.pk;
792
916
  const sessionId = parsedBody.data.sessionId;
793
917
  const session = yield this.adminforth.resource(this.options.sessionResource.resourceId).get([Filters.EQ(this.options.sessionResource.idField, sessionId)]);
794
918
  if (!session) {
@@ -831,12 +955,11 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
831
955
  method: 'POST',
832
956
  path: `/agent/create-session`,
833
957
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
834
- const currentAdminUser = requireAdminUser(adminUser);
835
958
  const data = this.parseBody(createSessionBodySchema, body, response);
836
959
  if (!data)
837
960
  return;
838
961
  const triggerMessage = data.triggerMessage;
839
- const userId = currentAdminUser.pk;
962
+ const userId = adminUser.pk;
840
963
  const title = (triggerMessage === null || triggerMessage === void 0 ? void 0 : triggerMessage.slice(0, 40)) || "New Session";
841
964
  const newSession = {
842
965
  [this.options.sessionResource.idField]: randomUUID(),
@@ -856,12 +979,11 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
856
979
  method: 'POST',
857
980
  path: `/agent/delete-session`,
858
981
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
859
- const currentAdminUser = requireAdminUser(adminUser);
860
982
  const data = this.parseBody(sessionIdBodySchema, body, response);
861
983
  if (!data)
862
984
  return;
863
985
  const sessionId = data.sessionId;
864
- const userId = currentAdminUser.pk;
986
+ const userId = adminUser.pk;
865
987
  const session = yield this.adminforth.resource(this.options.sessionResource.resourceId).get([Filters.EQ(this.options.sessionResource.idField, sessionId)]);
866
988
  if (!session) {
867
989
  return {
package/dist/types.d.ts CHANGED
@@ -90,5 +90,9 @@ export interface PluginOptions extends PluginsCommonOptions {
90
90
  * Falls back to an in-memory MemorySaver when omitted.
91
91
  */
92
92
  checkpointResource?: ICheckpointResource;
93
+ /**
94
+ * Optional field for storing external chat IDs.
95
+ */
96
+ chatExternalIdsField?: string;
93
97
  }
94
98
  export {};
package/index.ts CHANGED
@@ -4,6 +4,7 @@ import type {
4
4
  ChatSurfaceAdapter,
5
5
  ChatSurfaceEventSink,
6
6
  ChatSurfaceIncomingMessage,
7
+ IAdminForthEndpointHandlerInput,
7
8
  IAdminForth,
8
9
  IHttpServer,
9
10
  } from "adminforth";
@@ -36,6 +37,24 @@ type MulterFile = {
36
37
 
37
38
  type ExpressMulterRequest = { file?: MulterFile };
38
39
 
40
+ type ChatSurfaceConnectAction = {
41
+ type: "url";
42
+ label: string;
43
+ url: string;
44
+ };
45
+
46
+ type ChatSurfaceAdapterWithConnectAction = ChatSurfaceAdapter & {
47
+ createConnectAction?(input: {
48
+ token: string;
49
+ }): ChatSurfaceConnectAction | Promise<ChatSurfaceConnectAction>;
50
+ };
51
+
52
+ type ChatSurfaceLinkTokenPayload = {
53
+ surface: string;
54
+ adminUserId: AdminUser["pk"];
55
+ expiresAt: number;
56
+ };
57
+
39
58
  type AgentTurnRunInput = {
40
59
  prompt: string;
41
60
  sessionId: string;
@@ -91,6 +110,8 @@ const createSessionBodySchema = z.object({
91
110
 
92
111
  const VEGA_LITE_FENCE_START = "```vega-lite";
93
112
  const COMPLETE_VEGA_LITE_BLOCK_RE = /```vega-lite[\s\S]*?```/;
113
+ const DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD = "externalUserId";
114
+ const CHAT_SURFACE_LINK_TOKEN_TTL_MS = 60 * 1000;
94
115
 
95
116
  function isAbortError(error: unknown): boolean {
96
117
  return (
@@ -107,18 +128,12 @@ function getErrorMessage(error: unknown): string {
107
128
  return error instanceof Error ? error.message : String(error);
108
129
  }
109
130
 
110
- function requireAdminUser(adminUser: AdminUser | undefined): AdminUser {
111
- if (!adminUser) {
112
- throw new Error("AdminForth Agent endpoint requires an authenticated admin user");
113
- }
114
-
115
- return adminUser;
116
- }
117
-
118
131
  export default class AdminForthAgentPlugin extends AdminForthPlugin {
119
132
  options: PluginOptions;
120
133
  agentSystemPromptPromise: Promise<string>;
121
134
  private checkpointer: BaseCheckpointSaver | null = null;
135
+ private chatSurfaceLinkTokens = new Map<string, ChatSurfaceLinkTokenPayload>();
136
+ private chatSurfaceSettingsPageRegistered = false;
122
137
  private parseBody<T>(
123
138
  schema: z.ZodType<T>,
124
139
  body: unknown,
@@ -215,6 +230,12 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
215
230
  ].filter((resourceId): resourceId is string => Boolean(resourceId));
216
231
  }
217
232
 
233
+ private getChatSurfaceConnectActionAdapters() {
234
+ return (this.options.chatSurfaceAdapters ?? [])
235
+ .map((adapter) => adapter as ChatSurfaceAdapterWithConnectAction)
236
+ .filter((adapter) => adapter.createConnectAction);
237
+ }
238
+
218
239
  constructor(options: PluginOptions) {
219
240
  super(options, import.meta.url);
220
241
  this.options = options;
@@ -242,6 +263,19 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
242
263
  hasAudioAdapter: Boolean(this.options.audioAdapter),
243
264
  }
244
265
  });
266
+ if (this.getChatSurfaceConnectActionAdapters().length && !this.chatSurfaceSettingsPageRegistered) {
267
+ if (!this.adminforth.config.auth!.userMenuSettingsPages) {
268
+ this.adminforth.config.auth!.userMenuSettingsPages = [];
269
+ }
270
+ this.adminforth.config.auth!.userMenuSettingsPages.push({
271
+ icon: "flowbite:link-outline",
272
+ pageLabel: "Chat Surfaces",
273
+ slug: "chat-surfaces",
274
+ component: this.componentPath("ChatSurfaceSettings.vue"),
275
+ isVisible: () => true,
276
+ });
277
+ this.chatSurfaceSettingsPageRegistered = true;
278
+ }
245
279
  if (!this.adminforth.config.customization.customHeadItems) {
246
280
  this.adminforth.config.customization.customHeadItems = [];
247
281
  }
@@ -576,17 +610,90 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
576
610
  };
577
611
  }
578
612
 
613
+ private createChatSurfaceLinkToken(surface: string, adminUser: AdminUser) {
614
+ for (const [token, payload] of this.chatSurfaceLinkTokens) {
615
+ if (payload.expiresAt <= Date.now()) {
616
+ this.chatSurfaceLinkTokens.delete(token);
617
+ }
618
+ }
619
+
620
+ const token = randomUUID();
621
+ this.chatSurfaceLinkTokens.set(token, {
622
+ surface,
623
+ adminUserId: adminUser.pk,
624
+ expiresAt: Date.now() + CHAT_SURFACE_LINK_TOKEN_TTL_MS,
625
+ });
626
+
627
+ return token;
628
+ }
629
+
630
+ private consumeChatSurfaceLinkToken(surface: string, token: string) {
631
+ const payload = this.chatSurfaceLinkTokens.get(token);
632
+ this.chatSurfaceLinkTokens.delete(token);
633
+
634
+ if (!payload || payload.surface !== surface || payload.expiresAt <= Date.now()) {
635
+ return null;
636
+ }
637
+
638
+ return payload;
639
+ }
640
+
641
+ private async handleChatSurfaceLink(
642
+ incoming: ChatSurfaceIncomingMessage,
643
+ sink: ChatSurfaceEventSink,
644
+ ) {
645
+ if (typeof incoming.metadata?.startPayload !== "string") {
646
+ return false;
647
+ }
648
+
649
+ const payload = this.consumeChatSurfaceLinkToken(incoming.surface, incoming.metadata.startPayload);
650
+ if (!payload) {
651
+ await sink.emit({
652
+ type: "error",
653
+ message: "This chat surface link is expired or invalid. Please start linking again from AdminForth.",
654
+ });
655
+ return true;
656
+ }
657
+ const externalUserIdField = this.options.chatExternalIdsField ?? DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
658
+ const authResourceId = this.adminforth.config.auth!.usersResourceId!;
659
+ const authResource = this.adminforth.config.resources.find((resource) => resource.resourceId === authResourceId)!;
660
+ const primaryKeyField = authResource.columns.find((column) => column.primaryKey)!.name!;
661
+ const adminUserRecord = await this.adminforth.resource(authResourceId).get([
662
+ Filters.EQ(primaryKeyField, payload.adminUserId),
663
+ ]);
664
+
665
+ await this.adminforth.resource(authResourceId).update(payload.adminUserId, {
666
+ [externalUserIdField]: {
667
+ ...(adminUserRecord[externalUserIdField] ?? {}),
668
+ [incoming.surface]: incoming.externalUserId,
669
+ },
670
+ });
671
+ await sink.emit({
672
+ type: "done",
673
+ text: `${incoming.surface} account connected to AdminForth.`,
674
+ });
675
+
676
+ return true;
677
+ }
678
+
579
679
  private async handleChatSurfaceMessage(
580
680
  adapter: ChatSurfaceAdapter,
581
681
  incoming: ChatSurfaceIncomingMessage,
582
682
  sink: ChatSurfaceEventSink,
583
683
  ) {
584
- const adminUser = await adapter.resolveAdminUser({
585
- adminforth: this.adminforth,
586
- incoming,
587
- });
684
+ if (await this.handleChatSurfaceLink(incoming, sink)) {
685
+ return;
686
+ }
588
687
 
589
- if (!adminUser) {
688
+ const authResourceId = this.adminforth.config.auth!.usersResourceId!;
689
+ const authResource = this.adminforth.config.resources.find((resource) => resource.resourceId === authResourceId)!;
690
+ const primaryKeyField = authResource.columns.find((column) => column.primaryKey)!.name!;
691
+ const externalUserIdField = this.options.chatExternalIdsField ?? DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
692
+ const adminUserRecord = (
693
+ await this.adminforth.resource(authResourceId).list(Filters.IS_NOT_EMPTY(externalUserIdField))
694
+ ).find((user) => user[externalUserIdField]?.[adapter.name] === incoming.externalUserId);
695
+
696
+ if (!adminUserRecord) {
590
697
  await sink.emit({
591
698
  type: "error",
592
699
  message: "This chat account is not authorized to use AdminForth Agent.",
@@ -594,6 +701,12 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
594
701
  return;
595
702
  }
596
703
 
704
+ const adminUser = {
705
+ pk: adminUserRecord[primaryKeyField],
706
+ username: adminUserRecord[this.adminforth.config.auth!.usernameField],
707
+ dbUser: adminUserRecord,
708
+ };
709
+
597
710
  await this.handleTurn({
598
711
  prompt: incoming.prompt,
599
712
  sessionId: await this.getOrCreateChatSurfaceSession(incoming, adminUser),
@@ -607,12 +720,66 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
607
720
  }
608
721
 
609
722
  setupEndpoints(server: IHttpServer) {
723
+ if (this.getChatSurfaceConnectActionAdapters().length) {
724
+ server.endpoint({
725
+ method: "POST",
726
+ path: "/agent/surfaces/connectable",
727
+ handler: async ({ adminUser }) => {
728
+ const externalUserIdField = this.options.chatExternalIdsField ?? DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
729
+ const externalIds = adminUser.dbUser[externalUserIdField] ?? {};
730
+
731
+ return {
732
+ surfaces: this.getChatSurfaceConnectActionAdapters().map((adapter) => ({
733
+ name: adapter.name,
734
+ externalUserId: externalIds[adapter.name] ?? null,
735
+ })),
736
+ };
737
+ },
738
+ });
739
+ }
740
+
610
741
  for (const adapter of this.options.chatSurfaceAdapters ?? []) {
742
+ const connectActionAdapter = adapter as ChatSurfaceAdapterWithConnectAction;
743
+ if (connectActionAdapter.createConnectAction) {
744
+ server.endpoint({
745
+ method: "POST",
746
+ path: `/agent/surface/${adapter.name}/connect-action`,
747
+ handler: async ({ adminUser }) => {
748
+ const token = this.createChatSurfaceLinkToken(adapter.name, adminUser);
749
+ const action = await connectActionAdapter.createConnectAction!({ token });
750
+
751
+ return {
752
+ action,
753
+ };
754
+ },
755
+ });
756
+ server.endpoint({
757
+ method: "POST",
758
+ path: `/agent/surface/${adapter.name}/disconnect`,
759
+ handler: async ({ adminUser }) => {
760
+ const externalUserIdField = this.options.chatExternalIdsField ?? DEFAULT_ADMIN_USER_EXTERNAL_USER_ID_FIELD;
761
+ const externalIds = {
762
+ ...(adminUser.dbUser[externalUserIdField] ?? {}),
763
+ };
764
+
765
+ delete externalIds[adapter.name];
766
+
767
+ await this.adminforth.resource(this.adminforth.config.auth!.usersResourceId!).update(adminUser.pk, {
768
+ [externalUserIdField]: externalIds,
769
+ });
770
+
771
+ return {
772
+ ok: true,
773
+ };
774
+ },
775
+ });
776
+ }
777
+
611
778
  server.endpoint({
612
779
  method: "POST",
613
780
  noAuth: true,
614
781
  path: `/agent/surface/${adapter.name}/webhook`,
615
- handler: async (ctx) => {
782
+ handler: async (ctx: IAdminForthEndpointHandlerInput) => {
616
783
  const surfaceContext = {
617
784
  body: ctx.body,
618
785
  headers: ctx.headers,
@@ -641,8 +808,6 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
641
808
  method: 'POST',
642
809
  path: `/agent/get-placeholder-messages`,
643
810
  handler: async ({ headers, adminUser }) => {
644
- const currentAdminUser = requireAdminUser(adminUser);
645
-
646
811
  if (!this.options.placeholderMessages) {
647
812
  return {
648
813
  messages: [],
@@ -650,7 +815,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
650
815
  }
651
816
 
652
817
  const messages = await this.options.placeholderMessages({
653
- adminUser: currentAdminUser,
818
+ adminUser: adminUser,
654
819
  headers,
655
820
  });
656
821
 
@@ -663,7 +828,6 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
663
828
  method: 'POST',
664
829
  path: `/agent/response`,
665
830
  handler: async ({ body, adminUser, response, _raw_express_res, abortSignal }) => {
666
- const currentAdminUser = requireAdminUser(adminUser);
667
831
  const data = this.parseBody(agentResponseBodySchema, body, response);
668
832
  if (!data) return;
669
833
  const emit = createSseEventEmitter(_raw_express_res, {
@@ -678,7 +842,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
678
842
  userTimeZone: data.timeZone ?? 'UTC',
679
843
  currentPage: data.currentPage,
680
844
  abortSignal,
681
- adminUser: currentAdminUser,
845
+ adminUser: adminUser,
682
846
  emit,
683
847
  failureLogMessage: "Agent response streaming failed",
684
848
  abortLogMessage: "Agent response streaming aborted by the client",
@@ -691,7 +855,6 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
691
855
  path: `/agent/speech-response`,
692
856
  target: 'upload',
693
857
  handler: async ({ body, adminUser, response, _raw_express_req, _raw_express_res, abortSignal }) => {
694
- const currentAdminUser = requireAdminUser(adminUser);
695
858
  const req = _raw_express_req as ExpressMulterRequest;
696
859
  const audioAdapter = this.options.audioAdapter;
697
860
  if (!audioAdapter) {
@@ -761,7 +924,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
761
924
  userTimeZone: data.timeZone ?? 'UTC',
762
925
  currentPage,
763
926
  abortSignal,
764
- adminUser: currentAdminUser,
927
+ adminUser: adminUser,
765
928
  emit: async (event) => {
766
929
  if (event.type === "tool-call") {
767
930
  await emit(event);
@@ -871,10 +1034,9 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
871
1034
  method: 'POST',
872
1035
  path: `/agent/get-sessions`,
873
1036
  handler: async ({body, adminUser, response }) => {
874
- const currentAdminUser = requireAdminUser(adminUser);
875
1037
  const data = this.parseBody(getSessionsBodySchema, body, response);
876
1038
  if (!data) return;
877
- const userId = currentAdminUser.pk;
1039
+ const userId = adminUser.pk;
878
1040
  const limit = data.limit ?? 20;
879
1041
  const sessions = await this.adminforth.resource(this.options.sessionResource.resourceId).list(
880
1042
  [Filters.EQ(this.options.sessionResource.askerIdField, userId)], limit, undefined, [Sorts.DESC(this.options.sessionResource.createdAtField)]
@@ -892,13 +1054,12 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
892
1054
  method: 'POST',
893
1055
  path: `/agent/get-session-info`,
894
1056
  handler: async ({body, adminUser, response }) => {
895
- const currentAdminUser = requireAdminUser(adminUser);
896
1057
  const parsedBody = sessionIdBodySchema.safeParse(body);
897
1058
  if (!parsedBody.success) {
898
1059
  response.setStatus(422, parsedBody.error.message);
899
1060
  return;
900
1061
  }
901
- const userId = currentAdminUser.pk;
1062
+ const userId = adminUser.pk;
902
1063
  const sessionId = parsedBody.data.sessionId;
903
1064
  const session = await this.adminforth.resource(this.options.sessionResource.resourceId).get(
904
1065
  [Filters.EQ(this.options.sessionResource.idField, sessionId)]
@@ -943,11 +1104,10 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
943
1104
  method: 'POST',
944
1105
  path: `/agent/create-session`,
945
1106
  handler: async ({body, adminUser, response }) => {
946
- const currentAdminUser = requireAdminUser(adminUser);
947
1107
  const data = this.parseBody(createSessionBodySchema, body, response);
948
1108
  if (!data) return;
949
1109
  const triggerMessage = data.triggerMessage;
950
- const userId = currentAdminUser.pk;
1110
+ const userId = adminUser.pk;
951
1111
  const title = triggerMessage?.slice(0, 40) || "New Session";
952
1112
  const newSession = {
953
1113
  [this.options.sessionResource.idField]: randomUUID(),
@@ -967,11 +1127,10 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
967
1127
  method: 'POST',
968
1128
  path: `/agent/delete-session`,
969
1129
  handler: async ({body, adminUser, response }) => {
970
- const currentAdminUser = requireAdminUser(adminUser);
971
1130
  const data = this.parseBody(sessionIdBodySchema, body, response);
972
1131
  if (!data) return;
973
1132
  const sessionId = data.sessionId;
974
- const userId = currentAdminUser.pk;
1133
+ const userId = adminUser.pk;
975
1134
  const session = await this.adminforth.resource(this.options.sessionResource.resourceId).get(
976
1135
  [Filters.EQ(this.options.sessionResource.idField, sessionId)]
977
1136
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/agent",
3
- "version": "1.44.1",
3
+ "version": "1.45.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "@langchain/core": "^1.1.40",
34
34
  "@langchain/langgraph": "^1.2.8",
35
35
  "@langchain/langgraph-checkpoint": "^1.0.1",
36
- "adminforth": ">=2.54.0",
36
+ "adminforth": "^2.60.0",
37
37
  "dayjs": "^1.11.20",
38
38
  "langchain": "^1.3.3",
39
39
  "multer": "^2.1.1",
package/types.ts CHANGED
@@ -110,4 +110,9 @@ export interface PluginOptions extends PluginsCommonOptions {
110
110
  * Falls back to an in-memory MemorySaver when omitted.
111
111
  */
112
112
  checkpointResource?: ICheckpointResource;
113
+
114
+ /**
115
+ * Optional field for storing external chat IDs.
116
+ */
117
+ chatExternalIdsField?: string;
113
118
  }