@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/esm/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/esm/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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -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
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
onSessionExpired?: DevicProviderConfig['onSessionExpired'];
|
|
119
176
|
/**
|
|
120
177
|
* Base URL for the API
|
|
121
178
|
*/
|