@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.
Files changed (79) hide show
  1. package/README.md +74 -1
  2. package/dist/cjs/api/client.js +127 -6
  3. package/dist/cjs/api/client.js.map +1 -1
  4. package/dist/cjs/components/AICommandBar/AICommandBar.js +4 -2
  5. package/dist/cjs/components/AICommandBar/AICommandBar.js.map +1 -1
  6. package/dist/cjs/components/AICommandBar/useAICommandBar.js +4 -2
  7. package/dist/cjs/components/AICommandBar/useAICommandBar.js.map +1 -1
  8. package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js +4 -2
  9. package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
  10. package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js +4 -2
  11. package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
  12. package/dist/cjs/components/ChatDrawer/ChatDrawer.js +18 -9
  13. package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
  14. package/dist/cjs/components/ChatDrawer/ChatInput.js +9 -3
  15. package/dist/cjs/components/ChatDrawer/ChatInput.js.map +1 -1
  16. package/dist/cjs/components/ChatDrawer/ChatMessages.js +9 -3
  17. package/dist/cjs/components/ChatDrawer/ChatMessages.js.map +1 -1
  18. package/dist/cjs/components/ChatDrawer/ConversationSelector.js +9 -7
  19. package/dist/cjs/components/ChatDrawer/ConversationSelector.js.map +1 -1
  20. package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js +10 -3
  21. package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
  22. package/dist/cjs/components/ChatDrawer/UsageBar.js +8 -2
  23. package/dist/cjs/components/ChatDrawer/UsageBar.js.map +1 -1
  24. package/dist/cjs/components/CoreMemoryModal/CoreMemoryModal.js +7 -3
  25. package/dist/cjs/components/CoreMemoryModal/CoreMemoryModal.js.map +1 -1
  26. package/dist/cjs/components/IntegrationsModal/useIntegrations.js +12 -5
  27. package/dist/cjs/components/IntegrationsModal/useIntegrations.js.map +1 -1
  28. package/dist/cjs/components/ThreadStateTag/ThreadStateTag.js +10 -3
  29. package/dist/cjs/components/ThreadStateTag/ThreadStateTag.js.map +1 -1
  30. package/dist/cjs/hooks/useDevicChat.js +4 -2
  31. package/dist/cjs/hooks/useDevicChat.js.map +1 -1
  32. package/dist/cjs/index.js +2 -0
  33. package/dist/cjs/index.js.map +1 -1
  34. package/dist/cjs/provider/DevicProvider.js +31 -3
  35. package/dist/cjs/provider/DevicProvider.js.map +1 -1
  36. package/dist/cjs/provider/sharedSession.js +71 -0
  37. package/dist/cjs/provider/sharedSession.js.map +1 -0
  38. package/dist/esm/api/client.d.ts +68 -3
  39. package/dist/esm/api/client.js +127 -6
  40. package/dist/esm/api/client.js.map +1 -1
  41. package/dist/esm/components/AICommandBar/AICommandBar.js +4 -2
  42. package/dist/esm/components/AICommandBar/AICommandBar.js.map +1 -1
  43. package/dist/esm/components/AICommandBar/useAICommandBar.js +4 -2
  44. package/dist/esm/components/AICommandBar/useAICommandBar.js.map +1 -1
  45. package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js +4 -2
  46. package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
  47. package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js +4 -2
  48. package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
  49. package/dist/esm/components/ChatDrawer/ChatDrawer.js +18 -9
  50. package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
  51. package/dist/esm/components/ChatDrawer/ChatInput.js +9 -3
  52. package/dist/esm/components/ChatDrawer/ChatInput.js.map +1 -1
  53. package/dist/esm/components/ChatDrawer/ChatMessages.js +9 -3
  54. package/dist/esm/components/ChatDrawer/ChatMessages.js.map +1 -1
  55. package/dist/esm/components/ChatDrawer/ConversationSelector.js +9 -7
  56. package/dist/esm/components/ChatDrawer/ConversationSelector.js.map +1 -1
  57. package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js +10 -3
  58. package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
  59. package/dist/esm/components/ChatDrawer/UsageBar.js +8 -2
  60. package/dist/esm/components/ChatDrawer/UsageBar.js.map +1 -1
  61. package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.js +7 -3
  62. package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.js.map +1 -1
  63. package/dist/esm/components/IntegrationsModal/useIntegrations.js +12 -5
  64. package/dist/esm/components/IntegrationsModal/useIntegrations.js.map +1 -1
  65. package/dist/esm/components/ThreadStateTag/ThreadStateTag.js +10 -3
  66. package/dist/esm/components/ThreadStateTag/ThreadStateTag.js.map +1 -1
  67. package/dist/esm/hooks/useDevicChat.js +4 -2
  68. package/dist/esm/hooks/useDevicChat.js.map +1 -1
  69. package/dist/esm/index.d.ts +2 -1
  70. package/dist/esm/index.js +1 -0
  71. package/dist/esm/index.js.map +1 -1
  72. package/dist/esm/provider/DevicProvider.d.ts +1 -1
  73. package/dist/esm/provider/DevicProvider.js +31 -3
  74. package/dist/esm/provider/DevicProvider.js.map +1 -1
  75. package/dist/esm/provider/sharedSession.d.ts +18 -0
  76. package/dist/esm/provider/sharedSession.js +68 -0
  77. package/dist/esm/provider/sharedSession.js.map +1 -0
  78. package/dist/esm/provider/types.d.ts +62 -5
  79. package/package.json +1 -1
@@ -6,6 +6,7 @@ export { CoreMemoryModal } from './components/CoreMemoryModal';
6
6
  export type { CoreMemoryModalProps } from './components/CoreMemoryModal';
7
7
  export { IntegrationsModal, IntegrationsLauncher, IntegrationsHint, IntegrationLogo, useIntegrations, } from './components/IntegrationsModal';
8
8
  export type { IntegrationsModalProps, IntegrationsLauncherProps, IntegrationsHintProps, IntegrationLogoProps, IntegrationsState, IntegrationsScope, UseIntegrationsOptions, } from './components/IntegrationsModal';
9
+ export { createSharedSession } from './provider/sharedSession';
9
10
  export { themeVars, isDarkTheme } from './components/theme';
10
11
  export type { DevicTheme } from './components/theme';
11
12
  export { ThreadStateTag } from './components/ThreadStateTag';
@@ -19,7 +20,7 @@ export type { AIElementWrapperProps, AIElementWrapperOptions, AIElementWrapperHa
19
20
  export { useDevicChat, usePolling, useModelInterface, useSpeechRecording } from './hooks';
20
21
  export type { UseDevicChatOptions, UseDevicChatResult, UsePollingOptions, UsePollingResult, UseModelInterfaceOptions, UseModelInterfaceResult, PendingWidgetCall, HandleToolCallsResult, UseSpeechRecordingOptions, UseSpeechRecordingResult, SpeechRecordingStatus, } from './hooks';
21
22
  export { DevicApiClient, DevicApiError } from './api/client';
22
- export type { DevicApiClientConfig } from './api/client';
23
+ export type { DevicApiClientConfig, TenantSessionToken } from './api/client';
23
24
  export { AgentThreadState, } from './api/types';
24
25
  export type { ChatMessage, ChatFile, MessageContent, ToolCall, ToolCallResponse, ProcessMessageDto, AssistantResponse, AsyncResponse, RealtimeChatHistory, RealtimeStatus, ChatHistory, AssistantSpecialization, ModelInterfaceTool, ModelInterfaceToolSchema, ResponseWidgetProps, ResponseWidgetConfig, PreviousMessage, ApiError, ConversationSummary, FeedbackSubmission, FeedbackEntry, AgentThreadDto, AgentTaskDto, AgentDto, HandOffToolResponse, ToolGroupCall, ToolGroupConfig, WhisperTranscriptionResponse, TenantLimitExceeded, TenantUsage, TenantUsageRule, TenantUsageHistoryRow, TenantUsageHistoryQuery, RecalledMemoryRecord, RecalledMemoryFact, RecalledMemoryEntity, RecalledMemoryTurn, CoreMemorySnapshot, CoreMemoryEntry, CoreMemoryLimits, CoreMemoryList, Integration, IntegrationAccount, } from './api/types';
25
26
  export { MessageActions, FeedbackModal } from './components/Feedback';
package/dist/esm/index.js CHANGED
@@ -16,6 +16,7 @@ export { IntegrationsLauncher } from './components/IntegrationsModal/Integration
16
16
  export { IntegrationsHint } from './components/IntegrationsModal/IntegrationsHint.js';
17
17
  export { IntegrationLogo } from './components/IntegrationsModal/IntegrationLogo.js';
18
18
  export { useIntegrations } from './components/IntegrationsModal/useIntegrations.js';
19
+ export { createSharedSession } from './provider/sharedSession.js';
19
20
  export { isDarkTheme, themeVars } from './components/theme.js';
20
21
  export { ThreadStateTag } from './components/ThreadStateTag/ThreadStateTag.js';
21
22
  export { AICommandBar } from './components/AICommandBar/AICommandBar.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -14,4 +14,4 @@ import type { DevicProviderProps } from './types';
14
14
  * </DevicProvider>
15
15
  * ```
16
16
  */
17
- export declare function DevicProvider({ apiKey, baseUrl, tenantId, tenantMetadata, subtenantId, subtenantMetadata, tags, debug, children, }: DevicProviderProps): JSX.Element;
17
+ export declare function DevicProvider({ apiKey, getTenantSession, onSessionExpired, baseUrl, tenantId, tenantMetadata, subtenantId, subtenantMetadata, tags, debug, children, }: DevicProviderProps): JSX.Element;
@@ -2,6 +2,7 @@ import { jsx } from 'react/jsx-runtime';
2
2
  import { useState, useRef, useCallback, useMemo } from 'react';
3
3
  import { DevicContext } from './DevicContext.js';
4
4
  import { DevicApiClient } from '../api/client.js';
5
+ import { createSharedSession } from './sharedSession.js';
5
6
  import { generateId } from '../utils/index.js';
6
7
 
7
8
  const DEFAULT_BASE_URL = 'https://api.devic.ai';
@@ -20,7 +21,7 @@ const DEFAULT_BASE_URL = 'https://api.devic.ai';
20
21
  * </DevicProvider>
21
22
  * ```
22
23
  */
23
- function DevicProvider({ apiKey, baseUrl = DEFAULT_BASE_URL, tenantId, tenantMetadata, subtenantId, subtenantMetadata, tags, debug, children, }) {
24
+ function DevicProvider({ apiKey, getTenantSession, onSessionExpired, baseUrl = DEFAULT_BASE_URL, tenantId, tenantMetadata, subtenantId, subtenantMetadata, tags, debug, children, }) {
24
25
  const [references, setReferences] = useState([]);
25
26
  const drawerRef = useRef(null);
26
27
  const addReference = useCallback((ref) => {
@@ -45,17 +46,41 @@ function DevicProvider({ apiKey, baseUrl = DEFAULT_BASE_URL, tenantId, tenantMet
45
46
  const openDrawer = useCallback(() => {
46
47
  drawerRef.current?.open();
47
48
  }, []);
48
- const client = useMemo(() => new DevicApiClient({ apiKey, baseUrl }), [apiKey, baseUrl]);
49
+ // Both callbacks are read through a ref and handed on as stable wrappers.
50
+ // An inline `getTenantSession={() => …}` is a new function on every render,
51
+ // and passing it straight through would rebuild the client — throwing away
52
+ // the session it holds, so the page fetches a token per render — and rebuild
53
+ // the context, so every component below it would do the same.
54
+ const sessionSourceRef = useRef(getTenantSession);
55
+ sessionSourceRef.current = getTenantSession;
56
+ const expiredRef = useRef(onSessionExpired);
57
+ expiredRef.current = onSessionExpired;
58
+ const usesSessions = !!getTenantSession;
59
+ // One session for the whole tree: every component below builds its own API
60
+ // client, and each dedupes only its own renewals — so without this, N clients
61
+ // ask for N tokens. See `createSharedSession`.
62
+ const tenantSession = useMemo(() => usesSessions
63
+ ? createSharedSession(() => sessionSourceRef.current())
64
+ : undefined, [usesSessions]);
65
+ const sessionExpired = useMemo(() => () => expiredRef.current?.(), []);
66
+ const client = useMemo(() => new DevicApiClient({
67
+ apiKey,
68
+ baseUrl,
69
+ getTenantSession: tenantSession,
70
+ onSessionExpired: sessionExpired,
71
+ }), [apiKey, baseUrl, tenantSession, sessionExpired]);
49
72
  const contextValue = useMemo(() => ({
50
73
  client,
51
74
  apiKey,
75
+ getTenantSession: tenantSession,
76
+ onSessionExpired: sessionExpired,
52
77
  baseUrl,
53
78
  tenantId,
54
79
  tenantMetadata,
55
80
  subtenantId,
56
81
  subtenantMetadata,
57
82
  tags,
58
- isConfigured: !!apiKey,
83
+ isConfigured: !!apiKey || usesSessions,
59
84
  debug,
60
85
  references,
61
86
  addReference,
@@ -66,6 +91,9 @@ function DevicProvider({ apiKey, baseUrl = DEFAULT_BASE_URL, tenantId, tenantMet
66
91
  }), [
67
92
  client,
68
93
  apiKey,
94
+ tenantSession,
95
+ sessionExpired,
96
+ usesSessions,
69
97
  baseUrl,
70
98
  tenantId,
71
99
  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({ apiKey, baseUrl }),\n [apiKey, baseUrl]\n );\n\n const contextValue = useMemo<DevicContextValue>(\n () => ({\n client,\n apiKey,\n baseUrl,\n tenantId,\n tenantMetadata,\n subtenantId,\n subtenantMetadata,\n tags,\n isConfigured: !!apiKey,\n debug,\n references,\n addReference,\n removeReference,\n clearReferences,\n registerDrawer,\n openDrawer,\n }),\n [\n client,\n apiKey,\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":["_jsx"],"mappings":";;;;;;AAWA,MAAM,gBAAgB,GAAG,sBAAsB;AAE/C;;;;;;;;;;;;;;AAcG;AACG,SAAU,aAAa,CAAC,EAC5B,MAAM,EACN,OAAO,GAAG,gBAAgB,EAC1B,QAAQ,EACR,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,IAAI,EACJ,KAAK,EACL,QAAQ,GACW,EAAA;IACnB,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAgB,EAAE,CAAC;AAC/D,IAAA,MAAM,SAAS,GAAG,MAAM,CAA4B,IAAI,CAAC;AAEzD,IAAA,MAAM,YAAY,GAAG,WAAW,CAAC,CAAC,GAA4B,KAAY;AACxE,QAAA,MAAM,EAAE,GAAG,UAAU,EAAE;AACvB,QAAA,aAAa,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,IAAI,EAAE,EAAE,GAAG,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;AAClD,QAAA,OAAO,EAAE;IACX,CAAC,EAAE,EAAE,CAAC;AAEN,IAAA,MAAM,eAAe,GAAG,WAAW,CAAC,CAAC,EAAU,KAAI;QACjD,aAAa,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1D,CAAC,EAAE,EAAE,CAAC;AAEN,IAAA,MAAM,eAAe,GAAG,WAAW,CAAC,MAAK;QACvC,aAAa,CAAC,EAAE,CAAC;IACnB,CAAC,EAAE,EAAE,CAAC;AAEN,IAAA,MAAM,cAAc,GAAG,WAAW,CAAC,CAAC,MAA0B,KAAI;AAChE,QAAA,SAAS,CAAC,OAAO,GAAG,MAAM;AAC1B,QAAA,OAAO,MAAK;AACV,YAAA,IAAI,SAAS,CAAC,OAAO,KAAK,MAAM,EAAE;AAChC,gBAAA,SAAS,CAAC,OAAO,GAAG,IAAI;YAC1B;AACF,QAAA,CAAC;IACH,CAAC,EAAE,EAAE,CAAC;AAEN,IAAA,MAAM,UAAU,GAAG,WAAW,CAAC,MAAK;AAClC,QAAA,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE;IAC3B,CAAC,EAAE,EAAE,CAAC;IAEN,MAAM,MAAM,GAAG,OAAO,CACpB,MAAM,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAC7C,CAAC,MAAM,EAAE,OAAO,CAAC,CAClB;AAED,IAAA,MAAM,YAAY,GAAG,OAAO,CAC1B,OAAO;QACL,MAAM;QACN,MAAM;QACN,OAAO;QACP,QAAQ;QACR,cAAc;QACd,WAAW;QACX,iBAAiB;QACjB,IAAI;QACJ,YAAY,EAAE,CAAC,CAAC,MAAM;QACtB,KAAK;QACL,UAAU;QACV,YAAY;QACZ,eAAe;QACf,eAAe;QACf,cAAc;QACd,UAAU;AACX,KAAA,CAAC,EACF;QACE,MAAM;QACN,MAAM;QACN,OAAO;QACP,QAAQ;QACR,cAAc;QACd,WAAW;QACX,iBAAiB;QACjB,IAAI;QACJ,KAAK;QACL,UAAU;QACV,YAAY;QACZ,eAAe;QACf,eAAe;QACf,cAAc;QACd,UAAU;AACX,KAAA,CACF;AAED,IAAA,QACEA,GAAA,CAAC,YAAY,CAAC,QAAQ,EAAA,EAAC,KAAK,EAAE,YAAY,EAAA,QAAA,EACvC,QAAQ,EAAA,CACa;AAE5B;;;;"}
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":["_jsx"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { TenantSessionToken } from '../api/client';
2
+ /** Whatever the integrator returned, as a token with an expiry we can compare. */
3
+ export declare function normaliseSession(result: string | TenantSessionToken): TenantSessionToken;
4
+ /**
5
+ * One session for a whole tree of components.
6
+ *
7
+ * Every component in this library builds its own API client, and each of those
8
+ * dedupes only its own renewals — so N clients ask for N tokens. Opening the
9
+ * drawer fired three requests at the integrator's backend before anyone had
10
+ * said a word. The sharing belongs where there is exactly one of something,
11
+ * which is the provider, and not in each client, where there is not.
12
+ *
13
+ * `force` is what a client passes after the API refused the token it just used.
14
+ * The cached answer is then known to be dead, so it is skipped: without that, a
15
+ * shared cache would hand the refused token straight back and turn a
16
+ * recoverable 401 into a widget that never speaks again.
17
+ */
18
+ export declare function createSharedSession(source: () => Promise<string | TenantSessionToken>): (force?: boolean) => Promise<TenantSessionToken>;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Renew this long before expiry. A little wider than the client's own margin,
3
+ * so the sharing decides when to renew and the clients find a live token rather
4
+ * than each reaching for one.
5
+ */
6
+ const RENEWAL_MARGIN_MS = 90000;
7
+ /** The `exp` inside a JWT, in epoch ms. Unknown when it cannot be read. */
8
+ function expiryOf(token) {
9
+ try {
10
+ const payload = token.split('.')[1];
11
+ if (!payload)
12
+ return undefined;
13
+ const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
14
+ const exp = JSON.parse(json)?.exp;
15
+ return typeof exp === 'number' ? exp * 1000 : undefined;
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ /** Whatever the integrator returned, as a token with an expiry we can compare. */
22
+ function normaliseSession(result) {
23
+ if (typeof result === 'string') {
24
+ return { token: result, expiresAt: expiryOf(result) };
25
+ }
26
+ const declared = result.expiresAt ??
27
+ (result.expiresIn ? Date.now() + result.expiresIn * 1000 : undefined);
28
+ return { ...result, expiresAt: declared ?? expiryOf(result.token) };
29
+ }
30
+ /**
31
+ * One session for a whole tree of components.
32
+ *
33
+ * Every component in this library builds its own API client, and each of those
34
+ * dedupes only its own renewals — so N clients ask for N tokens. Opening the
35
+ * drawer fired three requests at the integrator's backend before anyone had
36
+ * said a word. The sharing belongs where there is exactly one of something,
37
+ * which is the provider, and not in each client, where there is not.
38
+ *
39
+ * `force` is what a client passes after the API refused the token it just used.
40
+ * The cached answer is then known to be dead, so it is skipped: without that, a
41
+ * shared cache would hand the refused token straight back and turn a
42
+ * recoverable 401 into a widget that never speaks again.
43
+ */
44
+ function createSharedSession(source) {
45
+ let value;
46
+ let inFlight;
47
+ return (force = false) => {
48
+ const stillGood = value &&
49
+ (value.expiresAt === undefined ||
50
+ value.expiresAt - Date.now() > RENEWAL_MARGIN_MS);
51
+ if (stillGood && !force)
52
+ return Promise.resolve(value);
53
+ if (!inFlight) {
54
+ inFlight = Promise.resolve(source())
55
+ .then((result) => {
56
+ value = normaliseSession(result);
57
+ return value;
58
+ })
59
+ .finally(() => {
60
+ inFlight = undefined;
61
+ });
62
+ }
63
+ return inFlight;
64
+ };
65
+ }
66
+
67
+ export { createSharedSession, normaliseSession };
68
+ //# 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;;;;"}
@@ -42,9 +42,56 @@ export interface SubtenantMetadata {
42
42
  */
43
43
  export interface DevicProviderConfig {
44
44
  /**
45
- * API key for authentication
46
- */
47
- apiKey: string;
45
+ * API key for authentication.
46
+ *
47
+ * Optional when `getTenantSession` is supplied — a page that authenticates with
48
+ * tenant sessions has no reason to carry a key at all.
49
+ */
50
+ apiKey?: string;
51
+ /**
52
+ * Fetches a tenant session from YOUR backend, which is the only place that
53
+ * knows which of your users is logged in.
54
+ *
55
+ * This is what makes the tenant a fact rather than a claim. With an API key
56
+ * alone the tenant is whatever the page declares, and the key sits in the
57
+ * bundle for anyone to read — so anyone can declare any tenant. A session is
58
+ * signed by your server, expires, and cannot reach beyond what an end user is
59
+ * allowed to do.
60
+ *
61
+ * It does not have to reach your backend on every call: if you mint the
62
+ * session inside your own login and put it in a cookie, this is just
63
+ * `async () => readCookie(…)`. Give it a lifetime matching your own session
64
+ * (`ttlSeconds`, up to 12 h) and pair it with `onSessionExpired`.
65
+ *
66
+ * Return the token, or `{ token, expiresAt }` if you know when it dies. It is
67
+ * called again on its own before expiry and after a rejected request, so it
68
+ * must be safe to call repeatedly.
69
+ *
70
+ * @example
71
+ * ```tsx
72
+ * <DevicProvider
73
+ * getTenantSession={async () => {
74
+ * const r = await fetch('/api/devic-session', { credentials: 'include' });
75
+ * return r.json(); // { token, expiresAt }
76
+ * }}
77
+ * >
78
+ * ```
79
+ */
80
+ getTenantSession?: () => Promise<string | {
81
+ token: string;
82
+ expiresAt?: number;
83
+ expiresIn?: number;
84
+ }>;
85
+ /**
86
+ * Called when the session is dead and cannot be replaced — the API rejected
87
+ * it and `getTenantSession` handed back the same expired token.
88
+ *
89
+ * Matters most when the session comes from a cookie with no way to renew it:
90
+ * without this the widget simply stops answering, at the exact moment the
91
+ * user's own login has also expired. Refresh, send them to log in, or say
92
+ * something — but say it.
93
+ */
94
+ onSessionExpired?: () => void;
48
95
  /**
49
96
  * Base URL for the Devic API
50
97
  * @default 'https://api.devic.ai'
@@ -113,9 +160,19 @@ export interface DevicContextValue {
113
160
  */
114
161
  client: DevicApiClient;
115
162
  /**
116
- * Current API key
163
+ * Current API key. Absent when the page authenticates with tenant sessions.
164
+ */
165
+ apiKey?: string;
166
+ /**
167
+ * The session source, when one was configured. Passed down so a component
168
+ * used outside this provider can still authenticate the same way.
169
+ */
170
+ getTenantSession?: DevicProviderConfig['getTenantSession'];
171
+ /**
172
+ * Reported when the session cannot be replaced. Passed down for the same
173
+ * reason as the session source itself.
117
174
  */
118
- apiKey: string;
175
+ onSessionExpired?: DevicProviderConfig['onSessionExpired'];
119
176
  /**
120
177
  * Base URL for the API
121
178
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devicai/ui",
3
- "version": "0.40.3",
3
+ "version": "0.41.1",
4
4
  "type": "module",
5
5
  "description": "React component library for Devic AI assistants",
6
6
  "engines": {