@jokkoo/sdk-web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 Jokkoo
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @jokkoo/sdk-web
2
+
3
+ Framework-agnostic Jokkoo client embed SDK core: API client, realtime transport, token management, and shared utilities (formatting, i18n, message mapping, uploads).
4
+
5
+ UI bindings for specific frameworks (`@jokkoo/sdk-react`, `@jokkoo/sdk-react-native`, etc.) build on this package.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @jokkoo/sdk-web
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```typescript
16
+ import { Jokkoo, JokkooApiClient } from "@jokkoo/sdk-web"
17
+
18
+ Jokkoo.init({
19
+ clientToken: "ct_...",
20
+ userTokenProvider: async () => {
21
+ const res = await fetch("/api/jokkoo-token")
22
+ const data = await res.json()
23
+ return data.token as string
24
+ },
25
+ locale: "fr",
26
+ debug: true,
27
+ })
28
+
29
+ await Jokkoo.getRealtimeClient().connect()
30
+
31
+ const api = new JokkooApiClient(Jokkoo.getConfig(), Jokkoo.getTokenManager())
32
+ const conversations = await api.getConversations(20, 0)
33
+ ```
34
+
35
+ For React apps, prefer [`@jokkoo/sdk-react`](../sdk-react) which wraps this core with a floating widget UI.
36
+
37
+ ## What's included
38
+
39
+ - **Core** — `Jokkoo` singleton, `TokenManager`, config validation, debug logging
40
+ - **API** — REST client for conversations, messages, uploads, satisfaction
41
+ - **Realtime** — Socket.IO client on namespace `/sdk` and event constants
42
+ - **Lib** — pure helpers (pagination, formatting, attachments, voice metering, satisfaction)
43
+ - **i18n** — English and French translation catalogs
44
+
45
+ ## Web channels
46
+
47
+ When using a web channel (`channelType === "web"`), configure **allowed origins** in the Jokkoo dashboard so the API accepts browser requests from your host app.
48
+
49
+ ## License
50
+
51
+ Apache-2.0
@@ -0,0 +1,419 @@
1
+ import { Socket } from 'socket.io-client';
2
+
3
+ type ConversationStatus = "open" | "pending" | "pending_closure" | "snoozed" | "closed";
4
+ type ChannelType = "mobile" | "web" | "whatsapp" | "email";
5
+ type MessageSenderType = "end_user" | "agent" | "system";
6
+ type MessageType = "comment" | "note" | "activity";
7
+ interface MessageMetadata {
8
+ activityType?: string;
9
+ isVoiceMessage?: boolean;
10
+ audioDurationMs?: number;
11
+ [key: string]: unknown;
12
+ }
13
+ interface MessageAttachment {
14
+ fileId: string;
15
+ fileName: string;
16
+ mimeType: string;
17
+ size?: number | null;
18
+ }
19
+ type ConversationId = number;
20
+ type ClosedConversationReplyBehavior = "create_new" | "reopen";
21
+ interface ClosedConversationPolicy {
22
+ endUserReplyBehavior: ClosedConversationReplyBehavior;
23
+ reopenGracePeriodDays: number;
24
+ }
25
+ interface Conversation {
26
+ id: ConversationId;
27
+ channelType: ChannelType;
28
+ status: ConversationStatus;
29
+ lastMessageText: string | null;
30
+ lastMessageSenderType?: MessageSenderType | null;
31
+ assignedAgentName?: string | null;
32
+ assignedAgentAvatarUrl?: string | null;
33
+ createdAt: string;
34
+ updatedAt: string;
35
+ closedAt?: string | null;
36
+ }
37
+ interface UnreadCountResponse {
38
+ count: number;
39
+ conversationIds: number[];
40
+ }
41
+ interface Message {
42
+ id: number;
43
+ conversationId: number;
44
+ endUserId: number;
45
+ senderId: string | null;
46
+ senderType: MessageSenderType;
47
+ messageType?: MessageType;
48
+ metadata?: MessageMetadata | null;
49
+ attachments?: MessageAttachment[];
50
+ content: string;
51
+ createdAt: string;
52
+ }
53
+ interface PaginatedResponse<T> {
54
+ data: T[];
55
+ total: number;
56
+ limit: number;
57
+ offset: number;
58
+ }
59
+ interface SendMessagePayload {
60
+ content: string;
61
+ conversationId?: number;
62
+ forceNew?: boolean;
63
+ fileIds?: string[];
64
+ metadata?: MessageMetadata | null;
65
+ }
66
+ declare const NEW_CONVERSATION_ID = 0;
67
+
68
+ type JokkooLocale = "fr" | "en" | (string & {});
69
+ type UserTokenProvider = () => Promise<string>;
70
+ type TokenState = "idle" | "loading" | "ready" | "error";
71
+ type TokenStateListener = (state: TokenState, error?: Error) => void;
72
+ interface JokkooConfig {
73
+ /** Public channel identifier from Jokkoo dashboard */
74
+ clientToken: string;
75
+ /** Callback that returns a fresh user_token JWT from the tenant backend */
76
+ userTokenProvider: UserTokenProvider;
77
+ /** Jokkoo API base URL (defaults to production) */
78
+ baseUrl?: string;
79
+ /** End-user locale (defaults to "fr") */
80
+ locale?: JokkooLocale;
81
+ /** Enable debug logging */
82
+ debug?: boolean;
83
+ /** Seconds before JWT expiry to proactively refresh (default: 60) */
84
+ tokenRefreshBufferSeconds?: number;
85
+ }
86
+ interface ResolvedJokkooConfig {
87
+ clientToken: string;
88
+ userTokenProvider: UserTokenProvider;
89
+ baseUrl: string;
90
+ locale: JokkooLocale;
91
+ debug: boolean;
92
+ tokenRefreshBufferSeconds: number;
93
+ }
94
+ interface JokkooState {
95
+ initialized: boolean;
96
+ clientToken?: string;
97
+ baseUrl?: string;
98
+ locale?: JokkooLocale;
99
+ }
100
+
101
+ declare class TokenManager {
102
+ private readonly config;
103
+ private readonly debug;
104
+ private cachedToken;
105
+ private refreshTimer;
106
+ private inflightRefresh;
107
+ private state;
108
+ private listeners;
109
+ constructor(config: ResolvedJokkooConfig);
110
+ subscribe(listener: TokenStateListener): () => void;
111
+ getState(): TokenState;
112
+ getToken(): Promise<string>;
113
+ invalidateCachedToken(): void;
114
+ refreshToken(): Promise<string>;
115
+ destroy(): void;
116
+ private scheduleRefresh;
117
+ private setState;
118
+ }
119
+
120
+ declare const CLIENT_TOKEN_HEADER = "x-client-token";
121
+ declare const USER_TOKEN_HEADER = "x-user-token";
122
+ declare class JokkooApiError extends Error {
123
+ readonly status: number;
124
+ readonly body?: unknown | undefined;
125
+ constructor(message: string, status: number, body?: unknown | undefined);
126
+ }
127
+ declare class JokkooApiClient {
128
+ private readonly config;
129
+ private readonly tokenManager;
130
+ private readonly debug;
131
+ constructor(config: ResolvedJokkooConfig, tokenManager: TokenManager);
132
+ resolveUrl(path: string): string;
133
+ getConversations(limit: number, offset: number): Promise<PaginatedResponse<Conversation>>;
134
+ getUnreadCount(): Promise<UnreadCountResponse>;
135
+ markAsRead(conversationId: number): Promise<void>;
136
+ getMessages(conversationId: number, limit: number, offset: number): Promise<PaginatedResponse<Message>>;
137
+ sendMessage(payload: SendMessagePayload): Promise<Message>;
138
+ requestUploadUrl(fileName: string, mimeType: string, provider?: "s3" | "r2"): Promise<{
139
+ fileId: string;
140
+ uploadUrl: string;
141
+ expiresAt: string;
142
+ }>;
143
+ confirmUpload(fileId: string): Promise<{
144
+ id: string;
145
+ fileName: string;
146
+ mimeType: string;
147
+ size?: number | null;
148
+ }>;
149
+ getDownloadUrl(fileId: string): Promise<{
150
+ downloadUrl: string;
151
+ expiresAt: string;
152
+ }>;
153
+ getConversationPolicies(): Promise<ClosedConversationPolicy>;
154
+ recordSatisfaction(conversationId: number, score: number): Promise<void>;
155
+ private request;
156
+ }
157
+
158
+ interface DebugLogger {
159
+ log: (message: string, ...details: unknown[]) => void;
160
+ warn: (message: string, ...details: unknown[]) => void;
161
+ error: (message: string, ...details: unknown[]) => void;
162
+ request: (method: string, url: string, details?: Record<string, unknown>) => void;
163
+ response: (method: string, url: string, status: number, durationMs: number, details?: Record<string, unknown>) => void;
164
+ requestError: (method: string, url: string, durationMs: number, error: unknown) => void;
165
+ }
166
+ declare function createDebugLogger(enabled: boolean): DebugLogger;
167
+ declare function redactToken(token: string): string;
168
+
169
+ type RealtimeStatus = "disconnected" | "connecting" | "connected" | "error";
170
+ type RealtimeStatusListener = (status: RealtimeStatus) => void;
171
+
172
+ declare class JokkooRealtimeClient {
173
+ private readonly config;
174
+ private readonly tokenManager;
175
+ private socket;
176
+ private status;
177
+ private readonly listeners;
178
+ constructor(config: ResolvedJokkooConfig, tokenManager: TokenManager);
179
+ connect(): Promise<void>;
180
+ disconnect(): void;
181
+ getStatus(): RealtimeStatus;
182
+ onStatusChange(listener: RealtimeStatusListener): () => void;
183
+ /** @internal For future event subscription by feature modules */
184
+ getSocket(): Socket | null;
185
+ private refreshAuth;
186
+ private setStatus;
187
+ }
188
+
189
+ declare class JokkooConfigError extends Error {
190
+ constructor(message: string);
191
+ }
192
+ declare function validateConfig(config: JokkooConfig): ResolvedJokkooConfig;
193
+
194
+ declare class JokkooSDK {
195
+ private config;
196
+ private tokenManager;
197
+ private realtimeClient;
198
+ private initialized;
199
+ init(config: JokkooConfig): void;
200
+ destroy(): void;
201
+ isInitialized(): boolean;
202
+ getState(): JokkooState;
203
+ /** @internal Used by UI and future messaging modules */
204
+ getTokenManager(): TokenManager;
205
+ /** @internal Used by UI and future messaging modules */
206
+ getRealtimeClient(): JokkooRealtimeClient;
207
+ /** @internal Used by UI and future messaging modules */
208
+ getConfig(): ResolvedJokkooConfig;
209
+ private assertInitialized;
210
+ }
211
+ declare const Jokkoo: JokkooSDK;
212
+
213
+ type ChatMessageSender = "user" | "agent" | "system";
214
+ interface ChatAttachment {
215
+ fileId: string;
216
+ fileName: string;
217
+ mimeType: string;
218
+ size?: number | null;
219
+ }
220
+ interface ChatMessage {
221
+ id: string;
222
+ sender: ChatMessageSender;
223
+ text: string;
224
+ timestamp: string;
225
+ agentName?: string;
226
+ agentInitials?: string;
227
+ agentAvatarUrl?: string | null;
228
+ attachments?: ChatAttachment[];
229
+ metadata?: Record<string, unknown> | null;
230
+ }
231
+ declare function mapApiAttachmentToChatAttachment(attachment: MessageAttachment): ChatAttachment;
232
+
233
+ declare const REALTIME_EVENTS: {
234
+ readonly ping: "ping";
235
+ readonly pong: "pong";
236
+ readonly messageCreated: "message.created";
237
+ readonly conversationCreated: "conversation.created";
238
+ readonly conversationUpdated: "conversation.updated";
239
+ };
240
+ declare const REALTIME_NAMESPACE = "/sdk";
241
+
242
+ interface Translations {
243
+ supportCenter: string;
244
+ activeConversation: (count: number) => string;
245
+ myRequests: string;
246
+ newSupportRequest: string;
247
+ noConversationsYet: string;
248
+ unableToLoadConversations: string;
249
+ retry: string;
250
+ newRequest: string;
251
+ support: string;
252
+ noMessagesYet: string;
253
+ newSupportRequestHeader: string;
254
+ request: string;
255
+ typeYourMessage: string;
256
+ sendMessage: string;
257
+ recordVoiceMessage: string;
258
+ unableToLoadMessages: string;
259
+ discussionEnded: string;
260
+ discussionClosedCanReply: string;
261
+ rateConversation: string;
262
+ rateConversationDescription: string;
263
+ thankYouForRating: string;
264
+ ratingVeryUnsatisfied: string;
265
+ ratingUnsatisfied: string;
266
+ ratingNeutral: string;
267
+ ratingSatisfied: string;
268
+ ratingVerySatisfied: string;
269
+ sendMessageError: string;
270
+ statusNew: string;
271
+ statusInProgress: string;
272
+ statusResolved: string;
273
+ statusEnded: string;
274
+ ticketResolved: string;
275
+ supportAgent: string;
276
+ welcomeMessage: string;
277
+ now: string;
278
+ newMessageIndicator: (count: number) => string;
279
+ attachFile: string;
280
+ pickPhotoLibrary: string;
281
+ pickCamera: string;
282
+ pickDocument: string;
283
+ cancel: string;
284
+ uploadFailed: string;
285
+ maxAttachmentsReached: string;
286
+ sharedFiles: string;
287
+ uploadingAttachment: string;
288
+ attachmentReady: string;
289
+ retryUpload: string;
290
+ openExternally: string;
291
+ downloadAttachment: string;
292
+ previewUnavailableInApp: string;
293
+ previewLoadFailed: string;
294
+ previewImageFile: string;
295
+ previewVideoFile: string;
296
+ previewDocumentFile: string;
297
+ voiceMessage: string;
298
+ recording: string;
299
+ tapToRecord: string;
300
+ cancelRecording: string;
301
+ sendRecording: string;
302
+ microphonePermissionDenied: string;
303
+ voiceMessageTooShort: string;
304
+ }
305
+
306
+ declare const en: Translations;
307
+
308
+ declare const fr: Translations;
309
+
310
+ declare function getTranslations(locale?: string): Translations;
311
+
312
+ declare function isTicketActivityMessage(message: Message): boolean;
313
+ /**
314
+ * Activity messages that should render as passive in-thread events
315
+ * (no bubble, no sender). Satisfaction surveys stay special-cased elsewhere.
316
+ */
317
+ declare function isPassiveActivityMessage(message: Message): boolean;
318
+ /** Agent and system messages (including ticket activities) count toward unread. */
319
+ declare function shouldCountMessageAsUnread(message: Message): boolean;
320
+
321
+ declare function isImageMimeType(mimeType: string): boolean;
322
+ declare function isVideoMimeType(mimeType: string): boolean;
323
+ declare function isAudioMimeType(mimeType: string): boolean;
324
+ declare function isDocumentMimeType(mimeType: string): boolean;
325
+ declare function canPreviewInApp(mimeType: string): boolean;
326
+ declare function formatAttachmentSize(size?: number | null): string;
327
+ declare function formatAudioDuration(durationMs: number): string;
328
+ declare function isVoiceMessageMetadata(metadata?: Record<string, unknown> | null): metadata is {
329
+ isVoiceMessage: true;
330
+ audioDurationMs?: number;
331
+ };
332
+
333
+ declare function isWithinReopenGracePeriod(conversation: Pick<Conversation, "closedAt">, policy: Pick<ClosedConversationPolicy, "reopenGracePeriodDays">): boolean;
334
+ declare function canReplyToClosedConversation(conversation: Pick<Conversation, "status" | "closedAt">, policy: ClosedConversationPolicy | undefined): boolean;
335
+
336
+ declare const MESSAGE_CONTENT_MIN_LENGTH = 2;
337
+ declare const MESSAGE_CONTENT_MAX_LENGTH = 2000;
338
+ declare const MAX_IMAGE_SIZE_BYTES: number;
339
+ declare const MAX_VIDEO_SIZE_BYTES: number;
340
+ declare const MAX_DOCUMENT_SIZE_BYTES: number;
341
+ declare const MAX_AUDIO_SIZE_BYTES: number;
342
+ declare const MAX_ATTACHMENTS_PER_MESSAGE = 5;
343
+ declare const ALLOWED_ATTACHMENT_MIME_TYPES: readonly ["image/jpeg", "image/png", "image/gif", "image/webp", "video/mp4", "video/quicktime", "video/webm", "application/pdf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "audio/aac", "audio/mp4", "audio/mpeg", "audio/webm"];
344
+ declare const VOICE_MESSAGE_MIME_TYPE: "audio/mp4";
345
+ declare const VOICE_MESSAGE_STORAGE_PROVIDER: "s3";
346
+
347
+ type ConversationTitleSource = Pick<Conversation, "assignedAgentName" | "status">;
348
+ declare function getInitialsFromName(name: string): string;
349
+ declare function getConversationDisplayName(conversation: ConversationTitleSource, t: Translations): string;
350
+ declare function getAgentInitials(assignedAgentName: string | null | undefined, fallback: string): string;
351
+ declare function getAgentDisplayName(assignedAgentName: string | null | undefined, t: Translations): string;
352
+
353
+ type PendingAttachmentStatus = "pending" | "uploading" | "confirming" | "confirmed" | "error";
354
+ interface PendingAttachment {
355
+ id: string;
356
+ fileName: string;
357
+ mimeType: string;
358
+ uri: string;
359
+ size: number;
360
+ status: PendingAttachmentStatus;
361
+ progress: number;
362
+ error?: string;
363
+ confirmedFileId?: string;
364
+ }
365
+ declare function uploadAttachmentFile(apiClient: JokkooApiClient, attachment: PendingAttachment, onProgress: (progress: number, status: PendingAttachmentStatus) => void): Promise<PendingAttachment>;
366
+ declare function createPendingAttachment(input: {
367
+ fileName: string;
368
+ mimeType: string;
369
+ uri: string;
370
+ size: number;
371
+ }): PendingAttachment;
372
+ declare function canAddMoreAttachments(currentCount: number): boolean;
373
+
374
+ type SupportedLocale = "fr" | "en";
375
+ declare function formatMessageTimestamp(value: string | Date, locale?: string): string;
376
+ declare function formatRelativeTimestamp(value: string | Date, options?: {
377
+ locale?: string;
378
+ nowLabel?: string;
379
+ }): string;
380
+
381
+ declare function mapMessageToChatMessage(message: Message, translations: Translations, locale?: string, assignedAgentName?: string | null, assignedAgentAvatarUrl?: string | null): ChatMessage;
382
+ declare function createWelcomeMessage(translations: Translations): ChatMessage;
383
+
384
+ type MessagesPageParam = "latest" | number;
385
+ declare function fetchMessagesPage(apiClient: JokkooApiClient, conversationId: number, pageSize: number, pageParam: MessagesPageParam): Promise<PaginatedResponse<Message>>;
386
+ declare function getOlderMessagesPageParam(lastPage: PaginatedResponse<Message>): number | undefined;
387
+
388
+ type RealtimeConversationPayload = {
389
+ id: number;
390
+ endUserId: number;
391
+ channelType: ChannelType | string;
392
+ status: string;
393
+ lastMessageText: string | null;
394
+ lastMessageAt?: string | Date | null;
395
+ assignedAgentName?: string | null;
396
+ createdAt: string | Date;
397
+ updatedAt: string | Date;
398
+ };
399
+ declare function mapRealtimeConversationPayload(conversation: RealtimeConversationPayload): Conversation;
400
+ declare function normalizeRealtimeMessage(message: Message): Message;
401
+
402
+ declare function isSatisfactionRequestMessage(message: Message): boolean;
403
+ declare function getSatisfactionSurveyMessage(messages: Message[]): string | undefined;
404
+ declare function conversationNeedsRating(status: ConversationStatus | undefined, messages: Message[]): boolean;
405
+
406
+ declare function normalizeMeteringLevel(metering: number): number;
407
+ declare function smoothMeteringSample(previous: number, next: number, alpha?: number): number;
408
+ declare function createIdleMeteringLevels(barCount?: number): number[];
409
+ declare function shiftMeteringLevel(levels: number[], newLevel: number): number[];
410
+ declare function applyLiveBarVariation(level: number, index: number): number;
411
+ declare function createPlaybackBarHeights(count: number, seed: number): number[];
412
+ declare function blendLiveWaveformLevels(levels: number[]): number[];
413
+
414
+ declare function uploadVoiceRecording(apiClient: JokkooApiClient, uri: string, fileName: string, onProgress?: (progress: number) => void): Promise<string>;
415
+ /** Upload a voice recording Blob directly (preferred for web MediaRecorder). */
416
+ declare function uploadVoiceBlob(apiClient: JokkooApiClient, blob: Blob, fileName: string, mimeType?: string, onProgress?: (progress: number) => void): Promise<string>;
417
+ declare function createVoiceMessageFileName(extension?: string): string;
418
+
419
+ export { ALLOWED_ATTACHMENT_MIME_TYPES, CLIENT_TOKEN_HEADER, type ChannelType, type ChatAttachment, type ChatMessage, type ChatMessageSender, type ClosedConversationPolicy, type Conversation, type ConversationId, type ConversationStatus, type DebugLogger, Jokkoo, JokkooApiClient, JokkooApiError, type JokkooConfig, JokkooConfigError, type JokkooLocale, JokkooRealtimeClient, type JokkooState, MAX_ATTACHMENTS_PER_MESSAGE, MAX_AUDIO_SIZE_BYTES, MAX_DOCUMENT_SIZE_BYTES, MAX_IMAGE_SIZE_BYTES, MAX_VIDEO_SIZE_BYTES, MESSAGE_CONTENT_MAX_LENGTH, MESSAGE_CONTENT_MIN_LENGTH, type Message, type MessageAttachment, type MessageMetadata, type MessageSenderType, type MessageType, NEW_CONVERSATION_ID, type PaginatedResponse, type PendingAttachment, type PendingAttachmentStatus, REALTIME_EVENTS, REALTIME_NAMESPACE, type RealtimeConversationPayload, type RealtimeStatus, type RealtimeStatusListener, type ResolvedJokkooConfig, type SendMessagePayload, type SupportedLocale, TokenManager, type TokenState, type TokenStateListener, type Translations, USER_TOKEN_HEADER, type UnreadCountResponse, type UserTokenProvider, VOICE_MESSAGE_MIME_TYPE, VOICE_MESSAGE_STORAGE_PROVIDER, applyLiveBarVariation, blendLiveWaveformLevels, canAddMoreAttachments, canPreviewInApp, canReplyToClosedConversation, conversationNeedsRating, createDebugLogger, createIdleMeteringLevels, createPendingAttachment, createPlaybackBarHeights, createVoiceMessageFileName, createWelcomeMessage, en, fetchMessagesPage, formatAttachmentSize, formatAudioDuration, formatMessageTimestamp, formatRelativeTimestamp, fr, getAgentDisplayName, getAgentInitials, getConversationDisplayName, getInitialsFromName, getOlderMessagesPageParam, getSatisfactionSurveyMessage, getTranslations, isAudioMimeType, isDocumentMimeType, isImageMimeType, isPassiveActivityMessage, isSatisfactionRequestMessage, isTicketActivityMessage, isVideoMimeType, isVoiceMessageMetadata, isWithinReopenGracePeriod, mapApiAttachmentToChatAttachment, mapMessageToChatMessage, mapRealtimeConversationPayload, normalizeMeteringLevel, normalizeRealtimeMessage, redactToken, shiftMeteringLevel, shouldCountMessageAsUnread, smoothMeteringSample, uploadAttachmentFile, uploadVoiceBlob, uploadVoiceRecording, validateConfig };