@devicai/ui 0.40.3 → 0.41.1
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/README.md +74 -1
- package/dist/cjs/api/client.js +127 -6
- package/dist/cjs/api/client.js.map +1 -1
- package/dist/cjs/components/AICommandBar/AICommandBar.js +4 -2
- package/dist/cjs/components/AICommandBar/AICommandBar.js.map +1 -1
- package/dist/cjs/components/AICommandBar/useAICommandBar.js +4 -2
- package/dist/cjs/components/AICommandBar/useAICommandBar.js.map +1 -1
- package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js +4 -2
- package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
- package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js +4 -2
- package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js +18 -9
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatInput.js +9 -3
- package/dist/cjs/components/ChatDrawer/ChatInput.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatMessages.js +9 -3
- package/dist/cjs/components/ChatDrawer/ChatMessages.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ConversationSelector.js +9 -7
- package/dist/cjs/components/ChatDrawer/ConversationSelector.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js +10 -3
- package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/UsageBar.js +8 -2
- package/dist/cjs/components/ChatDrawer/UsageBar.js.map +1 -1
- package/dist/cjs/components/CoreMemoryModal/CoreMemoryModal.js +7 -3
- package/dist/cjs/components/CoreMemoryModal/CoreMemoryModal.js.map +1 -1
- package/dist/cjs/components/IntegrationsModal/useIntegrations.js +12 -5
- package/dist/cjs/components/IntegrationsModal/useIntegrations.js.map +1 -1
- package/dist/cjs/components/ThreadStateTag/ThreadStateTag.js +10 -3
- package/dist/cjs/components/ThreadStateTag/ThreadStateTag.js.map +1 -1
- package/dist/cjs/hooks/useDevicChat.js +4 -2
- package/dist/cjs/hooks/useDevicChat.js.map +1 -1
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/provider/DevicProvider.js +31 -3
- package/dist/cjs/provider/DevicProvider.js.map +1 -1
- package/dist/cjs/provider/sharedSession.js +71 -0
- package/dist/cjs/provider/sharedSession.js.map +1 -0
- package/dist/esm/api/client.d.ts +68 -3
- package/dist/esm/api/client.js +127 -6
- package/dist/esm/api/client.js.map +1 -1
- package/dist/esm/components/AICommandBar/AICommandBar.js +4 -2
- package/dist/esm/components/AICommandBar/AICommandBar.js.map +1 -1
- package/dist/esm/components/AICommandBar/useAICommandBar.js +4 -2
- package/dist/esm/components/AICommandBar/useAICommandBar.js.map +1 -1
- package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js +4 -2
- package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
- package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js +4 -2
- package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatDrawer.js +18 -9
- package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatInput.js +9 -3
- package/dist/esm/components/ChatDrawer/ChatInput.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatMessages.js +9 -3
- package/dist/esm/components/ChatDrawer/ChatMessages.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ConversationSelector.js +9 -7
- package/dist/esm/components/ChatDrawer/ConversationSelector.js.map +1 -1
- package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js +10 -3
- package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
- package/dist/esm/components/ChatDrawer/UsageBar.js +8 -2
- package/dist/esm/components/ChatDrawer/UsageBar.js.map +1 -1
- package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.js +7 -3
- package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.js.map +1 -1
- package/dist/esm/components/IntegrationsModal/useIntegrations.js +12 -5
- package/dist/esm/components/IntegrationsModal/useIntegrations.js.map +1 -1
- package/dist/esm/components/ThreadStateTag/ThreadStateTag.js +10 -3
- package/dist/esm/components/ThreadStateTag/ThreadStateTag.js.map +1 -1
- package/dist/esm/hooks/useDevicChat.js +4 -2
- package/dist/esm/hooks/useDevicChat.js.map +1 -1
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/provider/DevicProvider.d.ts +1 -1
- package/dist/esm/provider/DevicProvider.js +31 -3
- package/dist/esm/provider/DevicProvider.js.map +1 -1
- package/dist/esm/provider/sharedSession.d.ts +18 -0
- package/dist/esm/provider/sharedSession.js +68 -0
- package/dist/esm/provider/sharedSession.js.map +1 -0
- package/dist/esm/provider/types.d.ts +62 -5
- package/package.json +1 -1
package/dist/cjs/index.js
CHANGED
|
@@ -18,6 +18,7 @@ var IntegrationsLauncher = require('./components/IntegrationsModal/IntegrationsL
|
|
|
18
18
|
var IntegrationsHint = require('./components/IntegrationsModal/IntegrationsHint.js');
|
|
19
19
|
var IntegrationLogo = require('./components/IntegrationsModal/IntegrationLogo.js');
|
|
20
20
|
var useIntegrations = require('./components/IntegrationsModal/useIntegrations.js');
|
|
21
|
+
var sharedSession = require('./provider/sharedSession.js');
|
|
21
22
|
var theme = require('./components/theme.js');
|
|
22
23
|
var ThreadStateTag = require('./components/ThreadStateTag/ThreadStateTag.js');
|
|
23
24
|
var AICommandBar = require('./components/AICommandBar/AICommandBar.js');
|
|
@@ -59,6 +60,7 @@ exports.IntegrationsLauncher = IntegrationsLauncher.IntegrationsLauncher;
|
|
|
59
60
|
exports.IntegrationsHint = IntegrationsHint.IntegrationsHint;
|
|
60
61
|
exports.IntegrationLogo = IntegrationLogo.IntegrationLogo;
|
|
61
62
|
exports.useIntegrations = useIntegrations.useIntegrations;
|
|
63
|
+
exports.createSharedSession = sharedSession.createSharedSession;
|
|
62
64
|
exports.isDarkTheme = theme.isDarkTheme;
|
|
63
65
|
exports.themeVars = theme.themeVars;
|
|
64
66
|
exports.ThreadStateTag = ThreadStateTag.ThreadStateTag;
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -4,6 +4,7 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
4
4
|
var React = require('react');
|
|
5
5
|
var DevicContext = require('./DevicContext.js');
|
|
6
6
|
var client = require('../api/client.js');
|
|
7
|
+
var sharedSession = require('./sharedSession.js');
|
|
7
8
|
var index = require('../utils/index.js');
|
|
8
9
|
|
|
9
10
|
const DEFAULT_BASE_URL = 'https://api.devic.ai';
|
|
@@ -22,7 +23,7 @@ const DEFAULT_BASE_URL = 'https://api.devic.ai';
|
|
|
22
23
|
* </DevicProvider>
|
|
23
24
|
* ```
|
|
24
25
|
*/
|
|
25
|
-
function DevicProvider({ apiKey, baseUrl = DEFAULT_BASE_URL, tenantId, tenantMetadata, subtenantId, subtenantMetadata, tags, debug, children, }) {
|
|
26
|
+
function DevicProvider({ apiKey, getTenantSession, onSessionExpired, baseUrl = DEFAULT_BASE_URL, tenantId, tenantMetadata, subtenantId, subtenantMetadata, tags, debug, children, }) {
|
|
26
27
|
const [references, setReferences] = React.useState([]);
|
|
27
28
|
const drawerRef = React.useRef(null);
|
|
28
29
|
const addReference = React.useCallback((ref) => {
|
|
@@ -47,17 +48,41 @@ function DevicProvider({ apiKey, baseUrl = DEFAULT_BASE_URL, tenantId, tenantMet
|
|
|
47
48
|
const openDrawer = React.useCallback(() => {
|
|
48
49
|
drawerRef.current?.open();
|
|
49
50
|
}, []);
|
|
50
|
-
|
|
51
|
+
// Both callbacks are read through a ref and handed on as stable wrappers.
|
|
52
|
+
// An inline `getTenantSession={() => …}` is a new function on every render,
|
|
53
|
+
// and passing it straight through would rebuild the client — throwing away
|
|
54
|
+
// the session it holds, so the page fetches a token per render — and rebuild
|
|
55
|
+
// the context, so every component below it would do the same.
|
|
56
|
+
const sessionSourceRef = React.useRef(getTenantSession);
|
|
57
|
+
sessionSourceRef.current = getTenantSession;
|
|
58
|
+
const expiredRef = React.useRef(onSessionExpired);
|
|
59
|
+
expiredRef.current = onSessionExpired;
|
|
60
|
+
const usesSessions = !!getTenantSession;
|
|
61
|
+
// One session for the whole tree: every component below builds its own API
|
|
62
|
+
// client, and each dedupes only its own renewals — so without this, N clients
|
|
63
|
+
// ask for N tokens. See `createSharedSession`.
|
|
64
|
+
const tenantSession = React.useMemo(() => usesSessions
|
|
65
|
+
? sharedSession.createSharedSession(() => sessionSourceRef.current())
|
|
66
|
+
: undefined, [usesSessions]);
|
|
67
|
+
const sessionExpired = React.useMemo(() => () => expiredRef.current?.(), []);
|
|
68
|
+
const client$1 = React.useMemo(() => new client.DevicApiClient({
|
|
69
|
+
apiKey,
|
|
70
|
+
baseUrl,
|
|
71
|
+
getTenantSession: tenantSession,
|
|
72
|
+
onSessionExpired: sessionExpired,
|
|
73
|
+
}), [apiKey, baseUrl, tenantSession, sessionExpired]);
|
|
51
74
|
const contextValue = React.useMemo(() => ({
|
|
52
75
|
client: client$1,
|
|
53
76
|
apiKey,
|
|
77
|
+
getTenantSession: tenantSession,
|
|
78
|
+
onSessionExpired: sessionExpired,
|
|
54
79
|
baseUrl,
|
|
55
80
|
tenantId,
|
|
56
81
|
tenantMetadata,
|
|
57
82
|
subtenantId,
|
|
58
83
|
subtenantMetadata,
|
|
59
84
|
tags,
|
|
60
|
-
isConfigured: !!apiKey,
|
|
85
|
+
isConfigured: !!apiKey || usesSessions,
|
|
61
86
|
debug,
|
|
62
87
|
references,
|
|
63
88
|
addReference,
|
|
@@ -68,6 +93,9 @@ function DevicProvider({ apiKey, baseUrl = DEFAULT_BASE_URL, tenantId, tenantMet
|
|
|
68
93
|
}), [
|
|
69
94
|
client$1,
|
|
70
95
|
apiKey,
|
|
96
|
+
tenantSession,
|
|
97
|
+
sessionExpired,
|
|
98
|
+
usesSessions,
|
|
71
99
|
baseUrl,
|
|
72
100
|
tenantId,
|
|
73
101
|
tenantMetadata,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DevicProvider.js","sources":["../../../../src/provider/DevicProvider.tsx"],"sourcesContent":["import React, { useMemo, useState, useRef, useCallback } from 'react';\nimport { DevicContext } from './DevicContext';\nimport { DevicApiClient } from '../api/client';\nimport { generateId } from '../utils';\nimport type {\n DevicProviderProps,\n DevicContextValue,\n AIReference,\n DrawerRegistration,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://api.devic.ai';\n\n/**\n * Provider component for Devic UI configuration\n *\n * @example\n * ```tsx\n * <DevicProvider\n * apiKey=\"devic-xxx\"\n * baseUrl=\"https://api.devic.ai\"\n * tenantId=\"tenant-123\"\n * tenantMetadata={{ userId: '456' }}\n * >\n * <App />\n * </DevicProvider>\n * ```\n */\nexport function DevicProvider({\n apiKey,\n baseUrl = DEFAULT_BASE_URL,\n tenantId,\n tenantMetadata,\n subtenantId,\n subtenantMetadata,\n tags,\n debug,\n children,\n}: DevicProviderProps): JSX.Element {\n const [references, setReferences] = useState<AIReference[]>([]);\n const drawerRef = useRef<DrawerRegistration | null>(null);\n\n const addReference = useCallback((ref: Omit<AIReference, 'id'>): string => {\n const id = generateId();\n setReferences((prev) => [...prev, { ...ref, id }]);\n return id;\n }, []);\n\n const removeReference = useCallback((id: string) => {\n setReferences((prev) => prev.filter((r) => r.id !== id));\n }, []);\n\n const clearReferences = useCallback(() => {\n setReferences([]);\n }, []);\n\n const registerDrawer = useCallback((handle: DrawerRegistration) => {\n drawerRef.current = handle;\n return () => {\n if (drawerRef.current === handle) {\n drawerRef.current = null;\n }\n };\n }, []);\n\n const openDrawer = useCallback(() => {\n drawerRef.current?.open();\n }, []);\n\n const client = useMemo(\n () => new DevicApiClient({
|
|
1
|
+
{"version":3,"file":"DevicProvider.js","sources":["../../../../src/provider/DevicProvider.tsx"],"sourcesContent":["import React, { useMemo, useState, useRef, useCallback } from 'react';\nimport { DevicContext } from './DevicContext';\nimport { DevicApiClient } from '../api/client';\nimport { createSharedSession } from './sharedSession';\nimport { generateId } from '../utils';\nimport type {\n DevicProviderProps,\n DevicContextValue,\n AIReference,\n DrawerRegistration,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://api.devic.ai';\n\n/**\n * Provider component for Devic UI configuration\n *\n * @example\n * ```tsx\n * <DevicProvider\n * apiKey=\"devic-xxx\"\n * baseUrl=\"https://api.devic.ai\"\n * tenantId=\"tenant-123\"\n * tenantMetadata={{ userId: '456' }}\n * >\n * <App />\n * </DevicProvider>\n * ```\n */\nexport function DevicProvider({\n apiKey,\n getTenantSession,\n onSessionExpired,\n baseUrl = DEFAULT_BASE_URL,\n tenantId,\n tenantMetadata,\n subtenantId,\n subtenantMetadata,\n tags,\n debug,\n children,\n}: DevicProviderProps): JSX.Element {\n const [references, setReferences] = useState<AIReference[]>([]);\n const drawerRef = useRef<DrawerRegistration | null>(null);\n\n const addReference = useCallback((ref: Omit<AIReference, 'id'>): string => {\n const id = generateId();\n setReferences((prev) => [...prev, { ...ref, id }]);\n return id;\n }, []);\n\n const removeReference = useCallback((id: string) => {\n setReferences((prev) => prev.filter((r) => r.id !== id));\n }, []);\n\n const clearReferences = useCallback(() => {\n setReferences([]);\n }, []);\n\n const registerDrawer = useCallback((handle: DrawerRegistration) => {\n drawerRef.current = handle;\n return () => {\n if (drawerRef.current === handle) {\n drawerRef.current = null;\n }\n };\n }, []);\n\n const openDrawer = useCallback(() => {\n drawerRef.current?.open();\n }, []);\n\n // Both callbacks are read through a ref and handed on as stable wrappers.\n // An inline `getTenantSession={() => …}` is a new function on every render,\n // and passing it straight through would rebuild the client — throwing away\n // the session it holds, so the page fetches a token per render — and rebuild\n // the context, so every component below it would do the same.\n const sessionSourceRef = useRef(getTenantSession);\n sessionSourceRef.current = getTenantSession;\n const expiredRef = useRef(onSessionExpired);\n expiredRef.current = onSessionExpired;\n const usesSessions = !!getTenantSession;\n\n // One session for the whole tree: every component below builds its own API\n // client, and each dedupes only its own renewals — so without this, N clients\n // ask for N tokens. See `createSharedSession`.\n const tenantSession = useMemo(\n () =>\n usesSessions\n ? createSharedSession(() => sessionSourceRef.current!())\n : undefined,\n [usesSessions]\n );\n const sessionExpired = useMemo(() => () => expiredRef.current?.(), []);\n\n const client = useMemo(\n () =>\n new DevicApiClient({\n apiKey,\n baseUrl,\n getTenantSession: tenantSession,\n onSessionExpired: sessionExpired,\n }),\n [apiKey, baseUrl, tenantSession, sessionExpired]\n );\n\n const contextValue = useMemo<DevicContextValue>(\n () => ({\n client,\n apiKey,\n getTenantSession: tenantSession,\n onSessionExpired: sessionExpired,\n baseUrl,\n tenantId,\n tenantMetadata,\n subtenantId,\n subtenantMetadata,\n tags,\n isConfigured: !!apiKey || usesSessions,\n debug,\n references,\n addReference,\n removeReference,\n clearReferences,\n registerDrawer,\n openDrawer,\n }),\n [\n client,\n apiKey,\n tenantSession,\n sessionExpired,\n usesSessions,\n baseUrl,\n tenantId,\n tenantMetadata,\n subtenantId,\n subtenantMetadata,\n tags,\n debug,\n references,\n addReference,\n removeReference,\n clearReferences,\n registerDrawer,\n openDrawer,\n ]\n );\n\n return (\n <DevicContext.Provider value={contextValue}>\n {children}\n </DevicContext.Provider>\n );\n}\n"],"names":["useState","useRef","useCallback","generateId","useMemo","createSharedSession","client","DevicApiClient","_jsx","DevicContext"],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renew this long before expiry. A little wider than the client's own margin,
|
|
5
|
+
* so the sharing decides when to renew and the clients find a live token rather
|
|
6
|
+
* than each reaching for one.
|
|
7
|
+
*/
|
|
8
|
+
const RENEWAL_MARGIN_MS = 90000;
|
|
9
|
+
/** The `exp` inside a JWT, in epoch ms. Unknown when it cannot be read. */
|
|
10
|
+
function expiryOf(token) {
|
|
11
|
+
try {
|
|
12
|
+
const payload = token.split('.')[1];
|
|
13
|
+
if (!payload)
|
|
14
|
+
return undefined;
|
|
15
|
+
const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
|
|
16
|
+
const exp = JSON.parse(json)?.exp;
|
|
17
|
+
return typeof exp === 'number' ? exp * 1000 : undefined;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Whatever the integrator returned, as a token with an expiry we can compare. */
|
|
24
|
+
function normaliseSession(result) {
|
|
25
|
+
if (typeof result === 'string') {
|
|
26
|
+
return { token: result, expiresAt: expiryOf(result) };
|
|
27
|
+
}
|
|
28
|
+
const declared = result.expiresAt ??
|
|
29
|
+
(result.expiresIn ? Date.now() + result.expiresIn * 1000 : undefined);
|
|
30
|
+
return { ...result, expiresAt: declared ?? expiryOf(result.token) };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* One session for a whole tree of components.
|
|
34
|
+
*
|
|
35
|
+
* Every component in this library builds its own API client, and each of those
|
|
36
|
+
* dedupes only its own renewals — so N clients ask for N tokens. Opening the
|
|
37
|
+
* drawer fired three requests at the integrator's backend before anyone had
|
|
38
|
+
* said a word. The sharing belongs where there is exactly one of something,
|
|
39
|
+
* which is the provider, and not in each client, where there is not.
|
|
40
|
+
*
|
|
41
|
+
* `force` is what a client passes after the API refused the token it just used.
|
|
42
|
+
* The cached answer is then known to be dead, so it is skipped: without that, a
|
|
43
|
+
* shared cache would hand the refused token straight back and turn a
|
|
44
|
+
* recoverable 401 into a widget that never speaks again.
|
|
45
|
+
*/
|
|
46
|
+
function createSharedSession(source) {
|
|
47
|
+
let value;
|
|
48
|
+
let inFlight;
|
|
49
|
+
return (force = false) => {
|
|
50
|
+
const stillGood = value &&
|
|
51
|
+
(value.expiresAt === undefined ||
|
|
52
|
+
value.expiresAt - Date.now() > RENEWAL_MARGIN_MS);
|
|
53
|
+
if (stillGood && !force)
|
|
54
|
+
return Promise.resolve(value);
|
|
55
|
+
if (!inFlight) {
|
|
56
|
+
inFlight = Promise.resolve(source())
|
|
57
|
+
.then((result) => {
|
|
58
|
+
value = normaliseSession(result);
|
|
59
|
+
return value;
|
|
60
|
+
})
|
|
61
|
+
.finally(() => {
|
|
62
|
+
inFlight = undefined;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return inFlight;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
exports.createSharedSession = createSharedSession;
|
|
70
|
+
exports.normaliseSession = normaliseSession;
|
|
71
|
+
//# sourceMappingURL=sharedSession.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sharedSession.js","sources":["../../../../src/provider/sharedSession.ts"],"sourcesContent":["import type { TenantSessionToken } from '../api/client';\n\n/**\n * Renew this long before expiry. A little wider than the client's own margin,\n * so the sharing decides when to renew and the clients find a live token rather\n * than each reaching for one.\n */\nconst RENEWAL_MARGIN_MS = 90_000;\n\n/** The `exp` inside a JWT, in epoch ms. Unknown when it cannot be read. */\nfunction expiryOf(token: string): number | undefined {\n try {\n const payload = token.split('.')[1];\n if (!payload) return undefined;\n const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));\n const exp = JSON.parse(json)?.exp;\n return typeof exp === 'number' ? exp * 1000 : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Whatever the integrator returned, as a token with an expiry we can compare. */\nexport function normaliseSession(\n result: string | TenantSessionToken,\n): TenantSessionToken {\n if (typeof result === 'string') {\n return { token: result, expiresAt: expiryOf(result) };\n }\n const declared =\n result.expiresAt ??\n (result.expiresIn ? Date.now() + result.expiresIn * 1000 : undefined);\n return { ...result, expiresAt: declared ?? expiryOf(result.token) };\n}\n\n/**\n * One session for a whole tree of components.\n *\n * Every component in this library builds its own API client, and each of those\n * dedupes only its own renewals — so N clients ask for N tokens. Opening the\n * drawer fired three requests at the integrator's backend before anyone had\n * said a word. The sharing belongs where there is exactly one of something,\n * which is the provider, and not in each client, where there is not.\n *\n * `force` is what a client passes after the API refused the token it just used.\n * The cached answer is then known to be dead, so it is skipped: without that, a\n * shared cache would hand the refused token straight back and turn a\n * recoverable 401 into a widget that never speaks again.\n */\nexport function createSharedSession(\n source: () => Promise<string | TenantSessionToken>,\n): (force?: boolean) => Promise<TenantSessionToken> {\n let value: TenantSessionToken | undefined;\n let inFlight: Promise<TenantSessionToken> | undefined;\n\n return (force = false) => {\n const stillGood =\n value &&\n (value.expiresAt === undefined ||\n value.expiresAt - Date.now() > RENEWAL_MARGIN_MS);\n if (stillGood && !force) return Promise.resolve(value!);\n\n if (!inFlight) {\n inFlight = Promise.resolve(source())\n .then((result) => {\n value = normaliseSession(result);\n return value;\n })\n .finally(() => {\n inFlight = undefined;\n });\n }\n return inFlight;\n };\n}\n"],"names":[],"mappings":";;AAEA;;;;AAIG;AACH,MAAM,iBAAiB,GAAG,KAAM;AAEhC;AACA,SAAS,QAAQ,CAAC,KAAa,EAAA;AAC7B,IAAA,IAAI;QACF,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACnC,QAAA,IAAI,CAAC,OAAO;AAAE,YAAA,OAAO,SAAS;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAChE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG;AACjC,QAAA,OAAO,OAAO,GAAG,KAAK,QAAQ,GAAG,GAAG,GAAG,IAAI,GAAG,SAAS;IACzD;AAAE,IAAA,MAAM;AACN,QAAA,OAAO,SAAS;IAClB;AACF;AAEA;AACM,SAAU,gBAAgB,CAC9B,MAAmC,EAAA;AAEnC,IAAA,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;AAC9B,QAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE;IACvD;AACA,IAAA,MAAM,QAAQ,GACZ,MAAM,CAAC,SAAS;SACf,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,SAAS,GAAG,IAAI,GAAG,SAAS,CAAC;AACvE,IAAA,OAAO,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,QAAQ,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;AACrE;AAEA;;;;;;;;;;;;;AAaG;AACG,SAAU,mBAAmB,CACjC,MAAkD,EAAA;AAElD,IAAA,IAAI,KAAqC;AACzC,IAAA,IAAI,QAAiD;AAErD,IAAA,OAAO,CAAC,KAAK,GAAG,KAAK,KAAI;QACvB,MAAM,SAAS,GACb,KAAK;AACL,aAAC,KAAK,CAAC,SAAS,KAAK,SAAS;gBAC5B,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,iBAAiB,CAAC;QACrD,IAAI,SAAS,IAAI,CAAC,KAAK;AAAE,YAAA,OAAO,OAAO,CAAC,OAAO,CAAC,KAAM,CAAC;QAEvD,IAAI,CAAC,QAAQ,EAAE;AACb,YAAA,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE;AAChC,iBAAA,IAAI,CAAC,CAAC,MAAM,KAAI;AACf,gBAAA,KAAK,GAAG,gBAAgB,CAAC,MAAM,CAAC;AAChC,gBAAA,OAAO,KAAK;AACd,YAAA,CAAC;iBACA,OAAO,CAAC,MAAK;gBACZ,QAAQ,GAAG,SAAS;AACtB,YAAA,CAAC,CAAC;QACN;AACA,QAAA,OAAO,QAAQ;AACjB,IAAA,CAAC;AACH;;;;;"}
|
package/dist/esm/api/client.d.ts
CHANGED
|
@@ -1,18 +1,83 @@
|
|
|
1
1
|
import type { ProcessMessageDto, ChatMessage, AsyncResponse, RealtimeChatHistory, ChatHistory, AssistantSpecialization, ApiError, ToolCallResponse, ListConversationsResponse, FeedbackSubmission, FeedbackEntry, AgentThreadDto, AgentDto, WhisperTranscriptionResponse, TenantUsage, TenantUsageHistoryRow, TenantUsageHistoryQuery, CoreMemoryList, CoreMemoryEntry, Integration } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* A tenant session, as the integrator's backend hands it over.
|
|
4
|
+
*
|
|
5
|
+
* A bare string is accepted too — the expiry is then read out of the token
|
|
6
|
+
* itself, so the simplest possible source still gets proactive renewal.
|
|
7
|
+
*/
|
|
8
|
+
export interface TenantSessionToken {
|
|
9
|
+
token: string;
|
|
10
|
+
/** Epoch milliseconds. */
|
|
11
|
+
expiresAt?: number;
|
|
12
|
+
/** Seconds from now. Used when `expiresAt` is absent. */
|
|
13
|
+
expiresIn?: number;
|
|
14
|
+
}
|
|
2
15
|
export interface DevicApiClientConfig {
|
|
3
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The public API key. Optional when `getTenantSession` is supplied — a page
|
|
18
|
+
* using tenant sessions has no reason to carry one.
|
|
19
|
+
*/
|
|
20
|
+
apiKey?: string;
|
|
4
21
|
baseUrl: string;
|
|
22
|
+
/**
|
|
23
|
+
* Where the tenant session comes from.
|
|
24
|
+
*
|
|
25
|
+
* Supplying this changes what the tenant IS: with an API key alone the tenant
|
|
26
|
+
* is whatever the page says it is, and the page can say anything. With a
|
|
27
|
+
* session it is what your server signed.
|
|
28
|
+
*
|
|
29
|
+
* A function rather than a value because it is called again on its own —
|
|
30
|
+
* before the token expires and after the API rejects one. It does not have to
|
|
31
|
+
* reach your backend: reading a cookie your login already set is a perfectly
|
|
32
|
+
* good answer, and then the whole thing is `async () => readCookie(…)`.
|
|
33
|
+
*
|
|
34
|
+
* It is called with `force: true` when the API has just rejected the token in
|
|
35
|
+
* hand, meaning a cached answer is known to be dead. Your own function can
|
|
36
|
+
* ignore the argument — `DevicProvider` uses it to share one session across
|
|
37
|
+
* every component without ever serving a refused token back.
|
|
38
|
+
*/
|
|
39
|
+
getTenantSession?: (force?: boolean) => Promise<string | TenantSessionToken>;
|
|
40
|
+
/**
|
|
41
|
+
* Called when the session is dead and cannot be replaced — the API rejected
|
|
42
|
+
* it and `getTenantSession` handed back the same expired token.
|
|
43
|
+
*
|
|
44
|
+
* Matters most when the session comes from a cookie with no way to renew it:
|
|
45
|
+
* without this the widget simply stops answering, at the exact moment the
|
|
46
|
+
* user's own login has also expired. Refresh the page, send them to log in,
|
|
47
|
+
* or say something — but say it.
|
|
48
|
+
*/
|
|
49
|
+
onSessionExpired?: () => void;
|
|
5
50
|
}
|
|
6
51
|
/**
|
|
7
52
|
* Devic API client using native fetch
|
|
8
53
|
*/
|
|
9
54
|
export declare class DevicApiClient {
|
|
10
55
|
private config;
|
|
56
|
+
private session?;
|
|
57
|
+
/** In flight renewal, shared so a burst of calls fetches one token. */
|
|
58
|
+
private renewing?;
|
|
59
|
+
/** So a page full of widgets reports one expiry, not one per widget. */
|
|
60
|
+
private expiryReported;
|
|
11
61
|
constructor(config: DevicApiClientConfig);
|
|
12
62
|
/**
|
|
13
63
|
* Update client configuration
|
|
14
64
|
*/
|
|
15
65
|
setConfig(config: Partial<DevicApiClientConfig>): void;
|
|
66
|
+
/** The credential for the next request, renewed if it is about to expire. */
|
|
67
|
+
private authorization;
|
|
68
|
+
private renew;
|
|
69
|
+
/**
|
|
70
|
+
* Whether a rejected request is worth retrying, having got a live session.
|
|
71
|
+
*
|
|
72
|
+
* Takes the token the request actually went out with, not the current one:
|
|
73
|
+
* with several requests in flight another may already have replaced it, and
|
|
74
|
+
* comparing against the current one would read that success as a failure.
|
|
75
|
+
*
|
|
76
|
+
* False means the session is dead and cannot be replaced — a cookie set at
|
|
77
|
+
* login, now expired, with nowhere to renew it from. That is worth telling
|
|
78
|
+
* the host application about, once.
|
|
79
|
+
*/
|
|
80
|
+
private recoverSession;
|
|
16
81
|
/**
|
|
17
82
|
* Make an authenticated request to the API
|
|
18
83
|
*/
|
|
@@ -113,7 +178,7 @@ export declare class DevicApiClient {
|
|
|
113
178
|
/**
|
|
114
179
|
* Upload a file and get a download URL
|
|
115
180
|
*/
|
|
116
|
-
uploadFile(file: File): Promise<{
|
|
181
|
+
uploadFile(file: File, isRetry?: boolean): Promise<{
|
|
117
182
|
name: string;
|
|
118
183
|
downloadUrl: string;
|
|
119
184
|
fileType: string;
|
|
@@ -131,7 +196,7 @@ export declare class DevicApiClient {
|
|
|
131
196
|
chatUid?: string;
|
|
132
197
|
tenantId?: string;
|
|
133
198
|
fileName?: string;
|
|
134
|
-
}): Promise<WhisperTranscriptionResponse>;
|
|
199
|
+
}, isRetry?: boolean): Promise<WhisperTranscriptionResponse>;
|
|
135
200
|
/**
|
|
136
201
|
* Fetch a single speech-to-text transcript by its id (the `transcriptId`
|
|
137
202
|
* stored on a message). Returns the transcribed text and the download URL of
|
package/dist/esm/api/client.js
CHANGED
|
@@ -1,24 +1,124 @@
|
|
|
1
|
+
/** Renew this long before expiry, so a request never leaves with a dead token. */
|
|
2
|
+
const RENEWAL_MARGIN_MS = 60000;
|
|
3
|
+
/**
|
|
4
|
+
* The expiry inside a JWT, in epoch milliseconds.
|
|
5
|
+
*
|
|
6
|
+
* Read rather than required, so the session source can return just the string.
|
|
7
|
+
* Any failure means "no idea", and the token is then renewed only when the API
|
|
8
|
+
* rejects it — correct, just less graceful.
|
|
9
|
+
*/
|
|
10
|
+
function expiryOf(token) {
|
|
11
|
+
try {
|
|
12
|
+
const payload = token.split(".")[1];
|
|
13
|
+
if (!payload)
|
|
14
|
+
return undefined;
|
|
15
|
+
const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
|
|
16
|
+
const exp = JSON.parse(json)?.exp;
|
|
17
|
+
return typeof exp === "number" ? exp * 1000 : undefined;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
1
23
|
/**
|
|
2
24
|
* Devic API client using native fetch
|
|
3
25
|
*/
|
|
4
26
|
class DevicApiClient {
|
|
5
27
|
constructor(config) {
|
|
28
|
+
/** So a page full of widgets reports one expiry, not one per widget. */
|
|
29
|
+
this.expiryReported = false;
|
|
6
30
|
this.config = config;
|
|
7
31
|
}
|
|
8
32
|
/**
|
|
9
33
|
* Update client configuration
|
|
10
34
|
*/
|
|
11
35
|
setConfig(config) {
|
|
36
|
+
const previous = this.config;
|
|
12
37
|
this.config = { ...this.config, ...config };
|
|
38
|
+
// A new session source speaks for a different end user; keeping the old
|
|
39
|
+
// token would show them the previous one's conversations.
|
|
40
|
+
if (config.getTenantSession &&
|
|
41
|
+
config.getTenantSession !== previous.getTenantSession) {
|
|
42
|
+
this.session = undefined;
|
|
43
|
+
this.expiryReported = false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** The credential for the next request, renewed if it is about to expire. */
|
|
47
|
+
async authorization(force = false) {
|
|
48
|
+
if (!this.config.getTenantSession)
|
|
49
|
+
return this.config.apiKey ?? "";
|
|
50
|
+
const current = this.session;
|
|
51
|
+
const stillGood = current &&
|
|
52
|
+
(current.expiresAt === undefined ||
|
|
53
|
+
current.expiresAt - Date.now() > RENEWAL_MARGIN_MS);
|
|
54
|
+
if (stillGood && !force)
|
|
55
|
+
return current.token;
|
|
56
|
+
return this.renew(force);
|
|
57
|
+
}
|
|
58
|
+
renew(force = false) {
|
|
59
|
+
if (!this.renewing) {
|
|
60
|
+
this.renewing = Promise.resolve(this.config.getTenantSession(force))
|
|
61
|
+
.then((result) => {
|
|
62
|
+
const token = typeof result === "string" ? result : result.token;
|
|
63
|
+
const declared = typeof result === "string"
|
|
64
|
+
? undefined
|
|
65
|
+
: (result.expiresAt ??
|
|
66
|
+
(result.expiresIn
|
|
67
|
+
? Date.now() + result.expiresIn * 1000
|
|
68
|
+
: undefined));
|
|
69
|
+
this.session = { token, expiresAt: declared ?? expiryOf(token) };
|
|
70
|
+
return token;
|
|
71
|
+
})
|
|
72
|
+
.finally(() => {
|
|
73
|
+
this.renewing = undefined;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return this.renewing;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Whether a rejected request is worth retrying, having got a live session.
|
|
80
|
+
*
|
|
81
|
+
* Takes the token the request actually went out with, not the current one:
|
|
82
|
+
* with several requests in flight another may already have replaced it, and
|
|
83
|
+
* comparing against the current one would read that success as a failure.
|
|
84
|
+
*
|
|
85
|
+
* False means the session is dead and cannot be replaced — a cookie set at
|
|
86
|
+
* login, now expired, with nowhere to renew it from. That is worth telling
|
|
87
|
+
* the host application about, once.
|
|
88
|
+
*/
|
|
89
|
+
async recoverSession(rejected) {
|
|
90
|
+
if (this.session && this.session.token !== rejected) {
|
|
91
|
+
this.expiryReported = false;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
let fresh;
|
|
95
|
+
try {
|
|
96
|
+
// Forced: whatever is cached upstream is the token that was just
|
|
97
|
+
// refused, so asking for it again would only confirm the refusal.
|
|
98
|
+
fresh = await this.renew(true);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
fresh = undefined;
|
|
102
|
+
}
|
|
103
|
+
if (fresh && fresh !== rejected) {
|
|
104
|
+
this.expiryReported = false;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
if (!this.expiryReported) {
|
|
108
|
+
this.expiryReported = true;
|
|
109
|
+
this.config.onSessionExpired?.();
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
13
112
|
}
|
|
14
113
|
/**
|
|
15
114
|
* Make an authenticated request to the API
|
|
16
115
|
*/
|
|
17
|
-
async request(endpoint, options = {}) {
|
|
116
|
+
async request(endpoint, options = {}, isRetry = false) {
|
|
18
117
|
const url = `${this.config.baseUrl}${endpoint}`;
|
|
118
|
+
const credential = await this.authorization();
|
|
19
119
|
const headers = {
|
|
20
120
|
"Content-Type": "application/json",
|
|
21
|
-
Authorization: `Bearer ${
|
|
121
|
+
Authorization: `Bearer ${credential}`,
|
|
22
122
|
"devic-api-source": "ui",
|
|
23
123
|
...options.headers,
|
|
24
124
|
};
|
|
@@ -26,6 +126,14 @@ class DevicApiClient {
|
|
|
26
126
|
...options,
|
|
27
127
|
headers,
|
|
28
128
|
});
|
|
129
|
+
// A session can die between being checked and being used — a clock adrift,
|
|
130
|
+
// a tab asleep for an hour, the key revoked. One retry with a fresh token
|
|
131
|
+
// turns that into a pause instead of a broken conversation.
|
|
132
|
+
if (response.status === 401 && this.config.getTenantSession && !isRetry) {
|
|
133
|
+
if (await this.recoverSession(credential)) {
|
|
134
|
+
return this.request(endpoint, options, true);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
29
137
|
if (!response.ok) {
|
|
30
138
|
let errorData;
|
|
31
139
|
try {
|
|
@@ -222,18 +330,25 @@ class DevicApiClient {
|
|
|
222
330
|
/**
|
|
223
331
|
* Upload a file and get a download URL
|
|
224
332
|
*/
|
|
225
|
-
async uploadFile(file) {
|
|
333
|
+
async uploadFile(file, isRetry = false) {
|
|
226
334
|
const url = `${this.config.baseUrl}/api/v1/files/upload`;
|
|
227
335
|
const formData = new FormData();
|
|
228
336
|
formData.append("file", file);
|
|
337
|
+
const credential = await this.authorization();
|
|
229
338
|
const response = await fetch(url, {
|
|
230
339
|
method: "POST",
|
|
231
340
|
headers: {
|
|
232
|
-
Authorization: `Bearer ${
|
|
341
|
+
Authorization: `Bearer ${credential}`,
|
|
233
342
|
"devic-api-source": "ui",
|
|
234
343
|
},
|
|
235
344
|
body: formData,
|
|
236
345
|
});
|
|
346
|
+
// The FormData is rebuilt from `file` on the way back in, so unlike a
|
|
347
|
+
// consumed stream this retry is safe.
|
|
348
|
+
if (response.status === 401 && this.config.getTenantSession && !isRetry) {
|
|
349
|
+
if (await this.recoverSession(credential))
|
|
350
|
+
return this.uploadFile(file, true);
|
|
351
|
+
}
|
|
237
352
|
if (!response.ok) {
|
|
238
353
|
let errorData;
|
|
239
354
|
try {
|
|
@@ -260,7 +375,7 @@ class DevicApiClient {
|
|
|
260
375
|
* and runs speech-to-text with Devic's own OpenAI key. Returns the text and a
|
|
261
376
|
* `transcriptId` to attach to the resulting message.
|
|
262
377
|
*/
|
|
263
|
-
async transcribeAudio(audio, options) {
|
|
378
|
+
async transcribeAudio(audio, options, isRetry = false) {
|
|
264
379
|
const url = `${this.config.baseUrl}/api/v1/whisper`;
|
|
265
380
|
const formData = new FormData();
|
|
266
381
|
if (typeof audio === "string") {
|
|
@@ -277,14 +392,20 @@ class DevicApiClient {
|
|
|
277
392
|
formData.append("chatUid", options.chatUid);
|
|
278
393
|
if (options?.tenantId)
|
|
279
394
|
formData.append("tenantId", options.tenantId);
|
|
395
|
+
const credential = await this.authorization();
|
|
280
396
|
const response = await fetch(url, {
|
|
281
397
|
method: "POST",
|
|
282
398
|
headers: {
|
|
283
|
-
Authorization: `Bearer ${
|
|
399
|
+
Authorization: `Bearer ${credential}`,
|
|
284
400
|
"devic-api-source": "ui",
|
|
285
401
|
},
|
|
286
402
|
body: formData,
|
|
287
403
|
});
|
|
404
|
+
if (response.status === 401 && this.config.getTenantSession && !isRetry) {
|
|
405
|
+
if (await this.recoverSession(credential)) {
|
|
406
|
+
return this.transcribeAudio(audio, options, true);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
288
409
|
if (!response.ok) {
|
|
289
410
|
let errorData;
|
|
290
411
|
try {
|