@cstar.help/js 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.
@@ -0,0 +1,172 @@
1
+ /** Configuration for the ChatClient. */
2
+ interface ChatConfig {
3
+ /** Team slug (e.g., 'acme'). Used to identify the team. */
4
+ teamSlug: string;
5
+ /** Override the base URL (defaults to https://app.cstar.help). */
6
+ baseUrl?: string;
7
+ /** Supabase project URL for realtime subscriptions (optional). */
8
+ supabaseUrl?: string;
9
+ /** Supabase anon key for realtime subscriptions (optional). */
10
+ supabaseAnonKey?: string;
11
+ /** Polling interval in ms when Supabase Realtime is not available (defaults to 3000). */
12
+ pollingInterval?: number;
13
+ }
14
+ /** Parameters for identifying a customer via HMAC signature verification. */
15
+ interface IdentifyParams {
16
+ /** Customer ID from your system. */
17
+ externalId: string;
18
+ /** Customer email address. */
19
+ email: string;
20
+ /** Customer display name. */
21
+ name?: string;
22
+ /** Avatar URL (not included in signature). */
23
+ avatarUrl?: string;
24
+ /** Custom metadata (not included in signature). */
25
+ metadata?: Record<string, {
26
+ value: string;
27
+ agentVisible: boolean;
28
+ }>;
29
+ /** Unix timestamp (seconds). Used for replay protection. */
30
+ timestamp: number;
31
+ }
32
+ /** Identity verification response. */
33
+ interface IdentifyResult {
34
+ verified: true;
35
+ customer: {
36
+ id: string;
37
+ externalId: string;
38
+ email: string;
39
+ name: string;
40
+ };
41
+ sessionToken: string;
42
+ expiresAt: string;
43
+ }
44
+ /** A customer conversation (maps to a ticket). */
45
+ interface Conversation {
46
+ id: string;
47
+ ticketNumber: string;
48
+ subject: string;
49
+ status: string;
50
+ messageCount: number;
51
+ createdAt: string;
52
+ updatedAt: string;
53
+ closedAt: string | null;
54
+ }
55
+ /** Paginated conversation list response. */
56
+ interface ConversationListResponse {
57
+ conversations: Conversation[];
58
+ cursor: string | null;
59
+ hasMore: boolean;
60
+ }
61
+ /** A message in a conversation. */
62
+ interface WidgetMessage {
63
+ id: string;
64
+ content: string;
65
+ sender: 'customer' | 'agent';
66
+ sender_name: string;
67
+ created_at: string;
68
+ read_at: string | null;
69
+ attachments: unknown[];
70
+ }
71
+ /** Parameters for listing conversations. */
72
+ interface ConversationListParams {
73
+ limit?: number;
74
+ cursor?: string;
75
+ }
76
+ /** Parameters for creating a new conversation. */
77
+ interface ConversationCreateParams {
78
+ subject: string;
79
+ message?: string;
80
+ }
81
+ /** Typing indicator payload from realtime subscription. */
82
+ interface TypingPayload {
83
+ agent_id: string;
84
+ agent_name: string;
85
+ is_typing: boolean;
86
+ }
87
+ /** Callback for message events. */
88
+ type MessageCallback = (message: WidgetMessage) => void;
89
+ /** Callback for typing indicator events. */
90
+ type TypingCallback = (payload: TypingPayload) => void;
91
+
92
+ /**
93
+ * Customer-facing chat client for the cStar widget API.
94
+ *
95
+ * ```typescript
96
+ * const chat = new ChatClient({ teamSlug: 'acme' });
97
+ * await chat.identify(
98
+ * { externalId: 'usr_123', email: 'jane@co.com', name: 'Jane', timestamp: Date.now() / 1000 },
99
+ * 'hmac_signature_hex'
100
+ * );
101
+ * const { conversations } = await chat.conversations.list();
102
+ * ```
103
+ */
104
+ declare class ChatClient {
105
+ private readonly baseUrl;
106
+ private readonly teamSlug;
107
+ private readonly pollingInterval;
108
+ private sessionToken;
109
+ private teamId;
110
+ private pollingTimers;
111
+ /** Sub-resource for managing conversations. */
112
+ readonly conversations: ConversationsApi;
113
+ /** Sub-resource for managing messages. */
114
+ readonly messages: MessagesApi;
115
+ constructor(config: ChatConfig);
116
+ /**
117
+ * Identify a customer using HMAC signature verification.
118
+ * Must be called before accessing conversations or messages.
119
+ *
120
+ * @param customer - Customer identity fields
121
+ * @param signature - HMAC-SHA256 hex signature of the signed fields
122
+ * @param testMode - Whether to use test mode (relaxed timestamp validation)
123
+ */
124
+ identify(customer: IdentifyParams, signature: string, testMode?: boolean): Promise<IdentifyResult>;
125
+ /**
126
+ * Subscribe to new messages on a conversation.
127
+ * Falls back to polling when Supabase Realtime is not available.
128
+ *
129
+ * @returns Unsubscribe function
130
+ */
131
+ onMessage(ticketId: string, callback: MessageCallback): () => void;
132
+ /**
133
+ * Subscribe to typing indicator events.
134
+ * Requires Supabase Realtime — returns a no-op if unavailable.
135
+ *
136
+ * @returns Unsubscribe function
137
+ */
138
+ onTyping(_ticketId: string, _callback: TypingCallback): () => void;
139
+ /** Disconnect all subscriptions and clean up. */
140
+ disconnect(): void;
141
+ /** @internal Make an authenticated request to the widget API. */
142
+ fetch<T>(path: string, options: {
143
+ method: string;
144
+ body?: unknown;
145
+ query?: Record<string, string | undefined>;
146
+ }): Promise<T>;
147
+ /** @internal Get the stored team ID for requests that need it. */
148
+ getTeamId(): string | null;
149
+ /** @internal Set team ID from identify response or other source. */
150
+ setTeamId(teamId: string): void;
151
+ private buildUrl;
152
+ }
153
+ /** Conversations sub-resource on ChatClient. */
154
+ declare class ConversationsApi {
155
+ private readonly chat;
156
+ constructor(chat: ChatClient);
157
+ /** List conversations for the identified customer. */
158
+ list(params?: ConversationListParams): Promise<ConversationListResponse>;
159
+ /** Create a new conversation (ticket). */
160
+ create(params: ConversationCreateParams): Promise<Conversation>;
161
+ }
162
+ /** Messages sub-resource on ChatClient. */
163
+ declare class MessagesApi {
164
+ private readonly chat;
165
+ constructor(chat: ChatClient);
166
+ /** List messages for a conversation. */
167
+ list(ticketId: string): Promise<WidgetMessage[]>;
168
+ /** Send a message to a conversation. */
169
+ send(ticketId: string, content: string): Promise<WidgetMessage>;
170
+ }
171
+
172
+ export { ChatClient, type ChatConfig, type Conversation, type ConversationCreateParams, type ConversationListParams, type ConversationListResponse, type IdentifyParams, type IdentifyResult, type MessageCallback, type TypingCallback, type WidgetMessage };
@@ -0,0 +1,213 @@
1
+ // src/chat/index.ts
2
+ var DEFAULT_BASE_URL = "https://app.cstar.help";
3
+ var DEFAULT_POLLING_INTERVAL = 3e3;
4
+ var ChatClient = class {
5
+ constructor(config) {
6
+ this.sessionToken = null;
7
+ this.teamId = null;
8
+ this.pollingTimers = /* @__PURE__ */ new Map();
9
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
10
+ this.teamSlug = config.teamSlug;
11
+ this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
12
+ this.conversations = new ConversationsApi(this);
13
+ this.messages = new MessagesApi(this);
14
+ }
15
+ /**
16
+ * Identify a customer using HMAC signature verification.
17
+ * Must be called before accessing conversations or messages.
18
+ *
19
+ * @param customer - Customer identity fields
20
+ * @param signature - HMAC-SHA256 hex signature of the signed fields
21
+ * @param testMode - Whether to use test mode (relaxed timestamp validation)
22
+ */
23
+ async identify(customer, signature, testMode = false) {
24
+ const response = await this.fetch(
25
+ "/api/widget/verify-identity",
26
+ {
27
+ method: "POST",
28
+ body: {
29
+ teamSlug: this.teamSlug,
30
+ testMode,
31
+ customer,
32
+ signature
33
+ }
34
+ }
35
+ );
36
+ this.sessionToken = response.data.sessionToken;
37
+ return response.data;
38
+ }
39
+ /**
40
+ * Subscribe to new messages on a conversation.
41
+ * Falls back to polling when Supabase Realtime is not available.
42
+ *
43
+ * @returns Unsubscribe function
44
+ */
45
+ onMessage(ticketId, callback) {
46
+ let lastMessageId = null;
47
+ const poll = async () => {
48
+ try {
49
+ const messages = await this.messages.list(ticketId);
50
+ if (messages.length > 0) {
51
+ const latest = messages[messages.length - 1];
52
+ if (lastMessageId && latest.id !== lastMessageId) {
53
+ const lastIdx = messages.findIndex((m) => m.id === lastMessageId);
54
+ const newMessages = lastIdx >= 0 ? messages.slice(lastIdx + 1) : [latest];
55
+ for (const msg of newMessages) {
56
+ callback(msg);
57
+ }
58
+ }
59
+ lastMessageId = latest.id;
60
+ }
61
+ } catch {
62
+ }
63
+ };
64
+ poll();
65
+ const timer = setInterval(poll, this.pollingInterval);
66
+ this.pollingTimers.set(ticketId, timer);
67
+ return () => {
68
+ clearInterval(timer);
69
+ this.pollingTimers.delete(ticketId);
70
+ };
71
+ }
72
+ /**
73
+ * Subscribe to typing indicator events.
74
+ * Requires Supabase Realtime — returns a no-op if unavailable.
75
+ *
76
+ * @returns Unsubscribe function
77
+ */
78
+ onTyping(_ticketId, _callback) {
79
+ return () => {
80
+ };
81
+ }
82
+ /** Disconnect all subscriptions and clean up. */
83
+ disconnect() {
84
+ for (const timer of this.pollingTimers.values()) {
85
+ clearInterval(timer);
86
+ }
87
+ this.pollingTimers.clear();
88
+ this.sessionToken = null;
89
+ }
90
+ /** @internal Make an authenticated request to the widget API. */
91
+ async fetch(path, options) {
92
+ const url = this.buildUrl(path, options.query);
93
+ const headers = {
94
+ "Content-Type": "application/json"
95
+ };
96
+ if (this.sessionToken) {
97
+ headers["Authorization"] = `Bearer ${this.sessionToken}`;
98
+ }
99
+ const response = await globalThis.fetch(url, {
100
+ method: options.method,
101
+ headers,
102
+ body: options.body ? JSON.stringify(options.body) : void 0
103
+ });
104
+ if (!response.ok) {
105
+ const errorBody = await response.json().catch(() => ({ error: { message: "Request failed" } }));
106
+ throw new Error(errorBody.error?.message ?? `Widget API error: ${response.status}`);
107
+ }
108
+ return response.json();
109
+ }
110
+ /** @internal Get the stored team ID for requests that need it. */
111
+ getTeamId() {
112
+ return this.teamId;
113
+ }
114
+ /** @internal Set team ID from identify response or other source. */
115
+ setTeamId(teamId) {
116
+ this.teamId = teamId;
117
+ }
118
+ buildUrl(path, query) {
119
+ const base = `${this.baseUrl}${path}`;
120
+ if (!query) return base;
121
+ const params = new URLSearchParams();
122
+ for (const [key, value] of Object.entries(query)) {
123
+ if (value !== void 0) params.set(key, value);
124
+ }
125
+ const qs = params.toString();
126
+ return qs ? `${base}?${qs}` : base;
127
+ }
128
+ };
129
+ var ConversationsApi = class {
130
+ constructor(chat) {
131
+ this.chat = chat;
132
+ }
133
+ /** List conversations for the identified customer. */
134
+ async list(params) {
135
+ const teamId = this.chat.getTeamId();
136
+ const response = await this.chat.fetch(
137
+ "/api/widget/conversations",
138
+ {
139
+ method: "GET",
140
+ query: {
141
+ teamId: teamId ?? void 0,
142
+ limit: params?.limit?.toString(),
143
+ cursor: params?.cursor
144
+ }
145
+ }
146
+ );
147
+ return response.data;
148
+ }
149
+ /** Create a new conversation (ticket). */
150
+ async create(params) {
151
+ const teamId = this.chat.getTeamId();
152
+ const response = await this.chat.fetch(
153
+ "/api/widget/tickets",
154
+ {
155
+ method: "POST",
156
+ body: {
157
+ teamId,
158
+ subject: params.subject,
159
+ message: params.message
160
+ }
161
+ }
162
+ );
163
+ return {
164
+ id: response.data.id,
165
+ ticketNumber: "",
166
+ subject: response.data.subject,
167
+ status: response.data.status,
168
+ messageCount: params.message ? 1 : 0,
169
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
170
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
171
+ closedAt: null
172
+ };
173
+ }
174
+ };
175
+ var MessagesApi = class {
176
+ constructor(chat) {
177
+ this.chat = chat;
178
+ }
179
+ /** List messages for a conversation. */
180
+ async list(ticketId) {
181
+ const teamId = this.chat.getTeamId();
182
+ const response = await this.chat.fetch(
183
+ "/api/widget/messages",
184
+ {
185
+ method: "GET",
186
+ query: {
187
+ teamId: teamId ?? void 0,
188
+ ticketId
189
+ }
190
+ }
191
+ );
192
+ return response.data.messages;
193
+ }
194
+ /** Send a message to a conversation. */
195
+ async send(ticketId, content) {
196
+ const teamId = this.chat.getTeamId();
197
+ const response = await this.chat.fetch(
198
+ "/api/widget/messages",
199
+ {
200
+ method: "POST",
201
+ body: {
202
+ teamId,
203
+ ticketId,
204
+ content
205
+ }
206
+ }
207
+ );
208
+ return response.data.message;
209
+ }
210
+ };
211
+ export {
212
+ ChatClient
213
+ };