@channlize/core 0.1.0-alpha.1 → 0.2.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/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // src/client.ts
1
+ // src/http.ts
2
2
  import fetch from "cross-fetch";
3
3
 
4
4
  // src/types.ts
@@ -9,6 +9,8 @@ var ChannlizeError = class extends Error {
9
9
  this.code = code;
10
10
  this.name = "ChannlizeError";
11
11
  }
12
+ statusCode;
13
+ code;
12
14
  };
13
15
  var ValidationError = class extends ChannlizeError {
14
16
  constructor(message, field) {
@@ -16,6 +18,7 @@ var ValidationError = class extends ChannlizeError {
16
18
  this.field = field;
17
19
  this.name = "ValidationError";
18
20
  }
21
+ field;
19
22
  };
20
23
  var AuthenticationError = class extends ChannlizeError {
21
24
  constructor(message = "Authentication failed") {
@@ -30,14 +33,149 @@ var RateLimitError = class extends ChannlizeError {
30
33
  }
31
34
  };
32
35
 
36
+ // src/http.ts
37
+ async function requestJson(http, endpoint, options = {}) {
38
+ const url = `${http.baseUrl}${endpoint}`;
39
+ const controller = new AbortController();
40
+ const timeoutId = setTimeout(() => controller.abort(), http.timeout);
41
+ try {
42
+ const mergedHeaders = {
43
+ "Content-Type": "application/json",
44
+ ...options.headers
45
+ };
46
+ if (mergedHeaders["Content-Type"] === "") {
47
+ delete mergedHeaders["Content-Type"];
48
+ }
49
+ const response = await fetch(url, {
50
+ ...options,
51
+ signal: controller.signal,
52
+ headers: mergedHeaders
53
+ });
54
+ clearTimeout(timeoutId);
55
+ if (!response.ok) {
56
+ await handleErrorResponse(response);
57
+ }
58
+ const data = await response.json();
59
+ return data;
60
+ } catch (error) {
61
+ clearTimeout(timeoutId);
62
+ if (error instanceof ChannlizeError) {
63
+ throw error;
64
+ }
65
+ if (error instanceof Error && error.name === "AbortError") {
66
+ throw new ChannlizeError("Request timeout", 408, "TIMEOUT");
67
+ }
68
+ throw new ChannlizeError(
69
+ `Network error: ${error instanceof Error ? error.message : "Unknown error"}`
70
+ );
71
+ }
72
+ }
73
+ async function handleErrorResponse(response) {
74
+ const contentType = response.headers.get("content-type");
75
+ let errorData = {};
76
+ if (contentType?.includes("application/json")) {
77
+ try {
78
+ errorData = await response.json();
79
+ } catch {
80
+ }
81
+ }
82
+ const rawMessage = errorData.message;
83
+ const message = (Array.isArray(rawMessage) ? rawMessage.join(", ") : rawMessage) || `HTTP ${response.status}: ${response.statusText}`;
84
+ const code = errorData.code ?? errorData.error;
85
+ switch (response.status) {
86
+ case 401:
87
+ throw new AuthenticationError(message);
88
+ case 400:
89
+ throw new ValidationError(message, errorData.field);
90
+ case 429:
91
+ throw new RateLimitError(message);
92
+ default:
93
+ throw new ChannlizeError(message, response.status, code);
94
+ }
95
+ }
96
+
97
+ // src/notify.ts
98
+ var EVENT_KEY = /^[a-z0-9][a-z0-9._-]*$/;
99
+ var DEFAULT_BASE_URL = "https://api.channlize.com";
100
+ async function sendNotify(http, auth, event, data, options = {}) {
101
+ if (!auth.projectId) {
102
+ throw new ValidationError("projectId is required for notify", "projectId");
103
+ }
104
+ if (auth.serverApiKey.startsWith("wk_")) {
105
+ throw new ValidationError(
106
+ "notify needs a server API key (ch_live_\u2026, write scope), not the public widget key",
107
+ "serverApiKey"
108
+ );
109
+ }
110
+ if (typeof event !== "string" || event.length > 100 || !EVENT_KEY.test(event)) {
111
+ throw new ValidationError(
112
+ `Invalid event key "${event}": use lowercase letters, digits, dots, underscores or dashes (e.g. user.subscribed)`,
113
+ "event"
114
+ );
115
+ }
116
+ const headers = {
117
+ Authorization: `Bearer ${auth.serverApiKey}`
118
+ };
119
+ if (options.idempotencyKey) {
120
+ headers["Idempotency-Key"] = options.idempotencyKey;
121
+ }
122
+ const res = await requestJson(http, "/v1/notify", {
123
+ method: "POST",
124
+ headers,
125
+ body: JSON.stringify({
126
+ projectId: auth.projectId,
127
+ event,
128
+ data: data ?? {},
129
+ channel: options.channel
130
+ })
131
+ });
132
+ return res;
133
+ }
134
+ function readEnv(name) {
135
+ const env = typeof process !== "undefined" && process.env ? process.env : void 0;
136
+ const value = env?.[name];
137
+ return value && value.trim() ? value.trim() : void 0;
138
+ }
139
+ function createNotifier(config = {}) {
140
+ return (event, data, options) => {
141
+ const serverApiKey = config.serverApiKey ?? readEnv("CHANNLIZE_SERVER_API_KEY") ?? readEnv("CHANNLIZE_API_KEY");
142
+ const projectId = config.projectId ?? readEnv("CHANNLIZE_PROJECT_ID");
143
+ if (!serverApiKey) {
144
+ return Promise.reject(
145
+ new ValidationError(
146
+ "A server API key is required: pass serverApiKey or set CHANNLIZE_API_KEY (or CHANNLIZE_SERVER_API_KEY)",
147
+ "serverApiKey"
148
+ )
149
+ );
150
+ }
151
+ if (!projectId) {
152
+ return Promise.reject(
153
+ new ValidationError(
154
+ "A project ID is required: pass projectId or set CHANNLIZE_PROJECT_ID",
155
+ "projectId"
156
+ )
157
+ );
158
+ }
159
+ const baseUrl = config.baseUrl ?? readEnv("CHANNLIZE_BASE_URL") ?? DEFAULT_BASE_URL;
160
+ return sendNotify(
161
+ { baseUrl: baseUrl.replace(/\/$/, ""), timeout: config.timeout ?? 1e4 },
162
+ { serverApiKey, projectId },
163
+ event,
164
+ data,
165
+ options
166
+ );
167
+ };
168
+ }
169
+
33
170
  // src/client.ts
34
171
  var ChannlizeClient = class {
35
172
  config;
36
173
  constructor(config) {
37
174
  this.config = {
175
+ ...config,
176
+ // After the spread: an explicit `undefined` must not drop the defaults.
38
177
  baseUrl: config.baseUrl || "https://api.channlize.com",
39
- timeout: 1e4,
40
- ...config
178
+ timeout: config.timeout ?? 1e4
41
179
  };
42
180
  this.validateConfig();
43
181
  }
@@ -46,101 +184,78 @@ var ChannlizeClient = class {
46
184
  throw new ValidationError("Widget key is required");
47
185
  }
48
186
  }
49
- async makeRequest(endpoint, options = {}) {
50
- const url = `${this.config.baseUrl}${endpoint}`;
51
- const controller = new AbortController();
52
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
53
- try {
54
- const response = await fetch(url, {
55
- ...options,
56
- signal: controller.signal,
57
- headers: {
58
- "Content-Type": "application/json",
59
- ...options.headers
60
- }
61
- });
62
- clearTimeout(timeoutId);
63
- if (!response.ok) {
64
- await this.handleErrorResponse(response);
65
- }
66
- const data = await response.json();
67
- return data;
68
- } catch (error) {
69
- clearTimeout(timeoutId);
70
- if (error instanceof ChannlizeError) {
71
- throw error;
72
- }
73
- if (error instanceof Error && error.name === "AbortError") {
74
- throw new ChannlizeError("Request timeout", 408, "TIMEOUT");
75
- }
76
- throw new ChannlizeError(
77
- `Network error: ${error instanceof Error ? error.message : "Unknown error"}`
78
- );
79
- }
80
- }
81
- async handleErrorResponse(response) {
82
- const contentType = response.headers.get("content-type");
83
- let errorData = {};
84
- if (contentType?.includes("application/json")) {
85
- try {
86
- errorData = await response.json();
87
- } catch {
88
- }
89
- }
90
- const message = errorData.error || `HTTP ${response.status}: ${response.statusText}`;
91
- switch (response.status) {
92
- case 401:
93
- throw new AuthenticationError(message);
94
- case 400:
95
- throw new ValidationError(message, errorData.field);
96
- case 429:
97
- throw new RateLimitError(message);
98
- default:
99
- throw new ChannlizeError(message, response.status, errorData.code);
100
- }
187
+ makeRequest(endpoint, options = {}) {
188
+ return requestJson(this.config, endpoint, options);
101
189
  }
102
190
  /**
103
191
  * Submit a contact form
104
192
  */
105
193
  async submitContactForm(data) {
106
- const response = await this.makeRequest("/public/contact", {
107
- method: "POST",
108
- body: JSON.stringify({
109
- widgetKey: this.config.apiKey,
110
- // apiKey is actually the widgetKey
111
- subject: data.subject,
112
- message: data.message,
113
- email: data.email,
114
- name: data.name,
115
- pageUrl: data.pageUrl || (typeof window !== "undefined" ? window.location.href : void 0),
116
- userAgent: data.userAgent || (typeof navigator !== "undefined" ? navigator.userAgent : void 0)
117
- })
118
- });
194
+ const response = await this.makeRequest(
195
+ "/public/contact",
196
+ {
197
+ method: "POST",
198
+ body: JSON.stringify({
199
+ widgetKey: this.config.apiKey,
200
+ // apiKey is actually the widgetKey
201
+ subject: data.subject,
202
+ message: data.message,
203
+ email: data.email,
204
+ name: data.name,
205
+ customFields: data.customFields && Object.keys(data.customFields).length > 0 ? data.customFields : void 0,
206
+ pageUrl: data.pageUrl || (typeof window !== "undefined" ? window.location.href : void 0),
207
+ userAgent: data.userAgent || (typeof navigator !== "undefined" ? navigator.userAgent : void 0)
208
+ })
209
+ }
210
+ );
119
211
  return response.data || response;
120
212
  }
121
213
  /**
122
214
  * Create a new chat session
123
215
  */
124
216
  async createChatSession(metadata) {
125
- const response = await this.makeRequest("/v1/chat/sessions", {
126
- method: "POST",
127
- body: JSON.stringify({
128
- widgetKey: this.config.apiKey,
129
- // apiKey is actually the widgetKey
130
- metadata: {
131
- ...metadata,
132
- pageUrl: typeof window !== "undefined" ? window.location.href : void 0,
133
- userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0
134
- }
135
- })
136
- });
217
+ const response = await this.makeRequest(
218
+ "/v1/chat/sessions",
219
+ {
220
+ method: "POST",
221
+ body: JSON.stringify({
222
+ widgetKey: this.config.apiKey,
223
+ // apiKey is actually the widgetKey
224
+ metadata: {
225
+ ...metadata,
226
+ pageUrl: typeof window !== "undefined" ? window.location.href : void 0,
227
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0
228
+ }
229
+ })
230
+ }
231
+ );
137
232
  return response.data;
138
233
  }
139
234
  /**
140
- * Send a message in a chat session
235
+ * Send a message in a chat session.
236
+ * When `attachments` are provided the request is sent as multipart/form-data
237
+ * so the files are uploaded alongside the message content.
141
238
  */
142
239
  async sendMessage(sessionId, content, attachments) {
143
240
  const idempotencyKey = `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
241
+ if (attachments && attachments.length > 0) {
242
+ const formData = new FormData();
243
+ formData.append("content", content);
244
+ formData.append("idempotencyKey", idempotencyKey);
245
+ for (const file of attachments) {
246
+ formData.append("attachments", file, file.name);
247
+ }
248
+ const response2 = await this.makeRequest(
249
+ `/v1/chat/sessions/${sessionId}/messages`,
250
+ {
251
+ method: "POST",
252
+ body: formData,
253
+ // Let the browser set Content-Type with boundary for multipart
254
+ headers: { "Content-Type": "" }
255
+ }
256
+ );
257
+ return response2.data;
258
+ }
144
259
  const response = await this.makeRequest(
145
260
  `/v1/chat/sessions/${sessionId}/messages`,
146
261
  {
@@ -157,7 +272,9 @@ var ChannlizeClient = class {
157
272
  * Get chat session details
158
273
  */
159
274
  async getChatSession(sessionId) {
160
- const response = await this.makeRequest(`/v1/chat/sessions/${sessionId}`);
275
+ const response = await this.makeRequest(
276
+ `/v1/chat/sessions/${sessionId}`
277
+ );
161
278
  return response.data;
162
279
  }
163
280
  /**
@@ -179,6 +296,82 @@ var ChannlizeClient = class {
179
296
  );
180
297
  return response.data;
181
298
  }
299
+ /**
300
+ * Log tracking: record a log line from the browser (widget key →
301
+ * `POST /public/events`). Shown under Log tracking in the dashboard.
302
+ */
303
+ async trackEvent(name, options) {
304
+ const res = await this.makeRequest(
305
+ "/public/events",
306
+ {
307
+ method: "POST",
308
+ body: JSON.stringify({
309
+ widgetKey: this.config.apiKey,
310
+ name,
311
+ properties: options?.properties,
312
+ severity: options?.severity,
313
+ distinctId: options?.distinctId,
314
+ pageUrl: options?.pageUrl ?? (typeof window !== "undefined" ? window.location.href : void 0),
315
+ userAgent: options?.userAgent ?? (typeof navigator !== "undefined" ? navigator.userAgent : void 0)
316
+ })
317
+ }
318
+ );
319
+ const data = res.data ?? res;
320
+ return data;
321
+ }
322
+ /**
323
+ * Log tracking from a trusted server, using the organization API key
324
+ * (`POST /v1/events`). Shown under Log tracking in the dashboard.
325
+ */
326
+ async trackServerEvent(name, options) {
327
+ if (!this.config.serverApiKey) {
328
+ throw new ValidationError(
329
+ "serverApiKey is required for trackServerEvent"
330
+ );
331
+ }
332
+ const res = await this.makeRequest(
333
+ "/v1/events",
334
+ {
335
+ method: "POST",
336
+ body: JSON.stringify({
337
+ projectId: this.config.projectId,
338
+ name,
339
+ properties: options?.properties,
340
+ severity: options?.severity
341
+ }),
342
+ headers: {
343
+ Authorization: `Bearer ${this.config.serverApiKey}`
344
+ }
345
+ }
346
+ );
347
+ const data = res.data ?? res;
348
+ return data;
349
+ }
350
+ /**
351
+ * Events: post a Slack message from your server when something happens
352
+ * (`POST /v1/notify`). The event key picks the channel and message
353
+ * template configured under Events in the dashboard; `data` fills the
354
+ * template's `{{field}}` placeholders. Needs `serverApiKey` (write scope)
355
+ * and `projectId` — never call it from browser code.
356
+ *
357
+ * An unknown key is saved as a disabled draft and answered with
358
+ * `status: 'skipped'`, so it never throws for a missing setup.
359
+ */
360
+ async notify(event, data, options) {
361
+ if (!this.config.serverApiKey) {
362
+ throw new ValidationError("serverApiKey is required for notify");
363
+ }
364
+ return sendNotify(
365
+ this.config,
366
+ {
367
+ serverApiKey: this.config.serverApiKey,
368
+ projectId: this.config.projectId
369
+ },
370
+ event,
371
+ data,
372
+ options
373
+ );
374
+ }
182
375
  };
183
376
  export {
184
377
  AuthenticationError,
@@ -186,6 +379,7 @@ export {
186
379
  ChannlizeError,
187
380
  RateLimitError,
188
381
  ValidationError,
382
+ createNotifier,
189
383
  ChannlizeClient as default
190
384
  };
191
385
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/types.ts"],"sourcesContent":["import fetch from 'cross-fetch';\nimport type {\n ChannlizeConfig,\n ContactFormData,\n ChatMessage,\n ApiResponse,\n CreateContactResponse,\n CreateChatSessionResponse,\n SendMessageResponse,\n ChatSession,\n} from './types';\nimport {\n ChannlizeError,\n AuthenticationError,\n ValidationError,\n RateLimitError,\n} from './types';\n\n/**\n * Channlize API Client\n * Provides methods to interact with the Channlize platform\n */\nexport class ChannlizeClient {\n private readonly config: Required<ChannlizeConfig>;\n\n constructor(config: ChannlizeConfig) {\n this.config = {\n baseUrl: config.baseUrl || 'https://api.channlize.com',\n timeout: 10000,\n ...config,\n };\n\n this.validateConfig();\n }\n\n private validateConfig(): void {\n if (!this.config.apiKey) {\n throw new ValidationError('Widget key is required');\n }\n }\n\n private async makeRequest<T>(\n endpoint: string,\n options: RequestInit = {}\n ): Promise<ApiResponse<T>> {\n const url = `${this.config.baseUrl}${endpoint}`;\n \n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n ...options,\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n ...options.headers,\n },\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n await this.handleErrorResponse(response);\n }\n\n const data = await response.json();\n return data as ApiResponse<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n \n if (error instanceof ChannlizeError) {\n throw error;\n }\n \n if (error instanceof Error && error.name === 'AbortError') {\n throw new ChannlizeError('Request timeout', 408, 'TIMEOUT');\n }\n \n throw new ChannlizeError(\n `Network error: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n }\n }\n\n private async handleErrorResponse(response: Response): Promise<never> {\n const contentType = response.headers.get('content-type');\n let errorData: any = {};\n\n if (contentType?.includes('application/json')) {\n try {\n errorData = await response.json();\n } catch {\n // Ignore JSON parsing errors\n }\n }\n\n const message = errorData.error || `HTTP ${response.status}: ${response.statusText}`;\n\n switch (response.status) {\n case 401:\n throw new AuthenticationError(message);\n case 400:\n throw new ValidationError(message, errorData.field);\n case 429:\n throw new RateLimitError(message);\n default:\n throw new ChannlizeError(message, response.status, errorData.code);\n }\n }\n\n /**\n * Submit a contact form\n */\n async submitContactForm(data: ContactFormData): Promise<CreateContactResponse> {\n const response = await this.makeRequest<CreateContactResponse>('/public/contact', {\n method: 'POST',\n body: JSON.stringify({\n widgetKey: this.config.apiKey, // apiKey is actually the widgetKey\n subject: data.subject,\n message: data.message,\n email: data.email,\n name: data.name,\n pageUrl: data.pageUrl || (typeof window !== 'undefined' ? window.location.href : undefined),\n userAgent: data.userAgent || (typeof navigator !== 'undefined' ? navigator.userAgent : undefined),\n }),\n });\n\n return response.data || response;\n }\n\n /**\n * Create a new chat session\n */\n async createChatSession(metadata?: Record<string, unknown>): Promise<CreateChatSessionResponse> {\n const response = await this.makeRequest<CreateChatSessionResponse>('/v1/chat/sessions', {\n method: 'POST',\n body: JSON.stringify({\n widgetKey: this.config.apiKey, // apiKey is actually the widgetKey\n metadata: {\n ...metadata,\n pageUrl: typeof window !== 'undefined' ? window.location.href : undefined,\n userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,\n },\n }),\n });\n\n return response.data;\n }\n\n /**\n * Send a message in a chat session\n */\n async sendMessage(\n sessionId: string,\n content: string,\n attachments?: File[]\n ): Promise<SendMessageResponse> {\n // Generate idempotency key to prevent duplicate message creation\n const idempotencyKey = `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n \n // For now, we send as JSON. File attachments support can be added later.\n // TODO: Implement file upload support\n const response = await this.makeRequest<SendMessageResponse>(\n `/v1/chat/sessions/${sessionId}/messages`,\n {\n method: 'POST',\n body: JSON.stringify({\n content,\n idempotencyKey,\n }),\n }\n );\n\n return response.data;\n }\n\n /**\n * Get chat session details\n */\n async getChatSession(sessionId: string): Promise<ChatSession> {\n const response = await this.makeRequest<ChatSession>(`/v1/chat/sessions/${sessionId}`);\n return response.data;\n }\n\n /**\n * Get messages for a chat session\n */\n async getChatMessages(sessionId: string, limit = 50, offset = 0): Promise<ChatMessage[]> {\n const response = await this.makeRequest<ChatMessage[]>(\n `/v1/chat/sessions/${sessionId}/messages?limit=${limit}&offset=${offset}`\n );\n return response.data;\n }\n\n /**\n * Close a chat session\n */\n async closeChatSession(sessionId: string): Promise<ChatSession> {\n const response = await this.makeRequest<ChatSession>(\n `/v1/chat/sessions/${sessionId}/close`,\n { method: 'POST' }\n );\n return response.data;\n }\n}\n","/**\n * Core types for Channlize SDK\n */\n\nexport interface ChannlizeConfig {\n /** API key for authentication */\n apiKey: string;\n /** Base URL for the Channlize API */\n baseUrl?: string;\n /** Project ID */\n projectId: string;\n /** Request timeout in milliseconds */\n timeout?: number;\n}\n\nexport interface ContactFormData {\n /** Contact's name */\n name: string;\n /** Contact's email address */\n email: string;\n /** Subject of the inquiry */\n subject?: string;\n /** Message content */\n message: string;\n /** Custom fields */\n customFields?: Record<string, unknown>;\n /** Page URL where the form was submitted */\n pageUrl?: string;\n /** User agent string */\n userAgent?: string;\n}\n\nexport interface ChatMessage {\n /** Unique message ID */\n id: string;\n /** Message content */\n content: string;\n /** Message timestamp */\n timestamp: Date;\n /** Message author (user or agent) */\n author: 'user' | 'agent';\n /** Author metadata */\n authorData?: {\n name?: string;\n email?: string;\n avatarUrl?: string;\n };\n /** Attached files */\n attachments?: FileAttachment[];\n}\n\nexport interface FileAttachment {\n /** File ID */\n id: string;\n /** Original filename */\n name: string;\n /** File size in bytes */\n size: number;\n /** MIME type */\n type: string;\n /** Download URL */\n url: string;\n}\n\nexport interface ChatSession {\n /** Session ID */\n id: string;\n /** Session status */\n status: 'active' | 'closed' | 'waiting';\n /** Messages in the session */\n messages: ChatMessage[];\n /** Session metadata */\n metadata?: Record<string, unknown>;\n /** Created timestamp */\n createdAt: Date;\n /** Updated timestamp */\n updatedAt: Date;\n}\n\nexport interface ApiResponse<T = unknown> {\n /** Response data */\n data: T;\n /** Success status */\n success: boolean;\n /** Error message if any */\n error?: string;\n /** Additional metadata */\n meta?: Record<string, unknown>;\n}\n\nexport interface CreateContactResponse {\n /** Contact form submission ID */\n id: string;\n /** Status of the submission */\n status: 'received' | 'processing' | 'delivered';\n /** Slack thread URL if available */\n slackThreadUrl?: string;\n}\n\nexport interface CreateChatSessionResponse {\n /** Chat session data */\n session: ChatSession;\n /** SSE endpoint for real-time updates */\n sseEndpoint?: string;\n}\n\nexport interface SendMessageResponse {\n /** The sent message */\n message: ChatMessage;\n /** Updated session */\n session: ChatSession;\n}\n\n/**\n * Error types\n */\nexport class ChannlizeError extends Error {\n constructor(\n message: string,\n public statusCode?: number,\n public code?: string\n ) {\n super(message);\n this.name = 'ChannlizeError';\n }\n}\n\nexport class ValidationError extends ChannlizeError {\n constructor(message: string, public field?: string) {\n super(message, 400, 'VALIDATION_ERROR');\n this.name = 'ValidationError';\n }\n}\n\nexport class AuthenticationError extends ChannlizeError {\n constructor(message: string = 'Authentication failed') {\n super(message, 401, 'AUTHENTICATION_ERROR');\n this.name = 'AuthenticationError';\n }\n}\n\nexport class RateLimitError extends ChannlizeError {\n constructor(message: string = 'Rate limit exceeded') {\n super(message, 429, 'RATE_LIMIT_ERROR');\n this.name = 'RateLimitError';\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/http.ts","../src/types.ts","../src/notify.ts","../src/client.ts"],"sourcesContent":["import fetch from 'cross-fetch';\nimport type { ApiResponse } from './types';\nimport {\n ChannlizeError,\n AuthenticationError,\n ValidationError,\n RateLimitError,\n} from './types';\n\n/**\n * Shared HTTP layer for `ChannlizeClient` and `createNotifier`: JSON\n * requests with a timeout, and the API's error shape mapped to typed errors.\n */\nexport async function requestJson<T>(\n http: { baseUrl: string; timeout: number },\n endpoint: string,\n options: RequestInit = {}\n): Promise<ApiResponse<T>> {\n const url = `${http.baseUrl}${endpoint}`;\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), http.timeout);\n\n try {\n const mergedHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...(options.headers as Record<string, string>),\n };\n\n // When Content-Type is explicitly empty (e.g. FormData), remove it so the\n // browser / runtime can set the correct multipart boundary automatically.\n if (mergedHeaders['Content-Type'] === '') {\n delete mergedHeaders['Content-Type'];\n }\n\n const response = await fetch(url, {\n ...options,\n signal: controller.signal,\n headers: mergedHeaders,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n await handleErrorResponse(response);\n }\n\n const data = await response.json();\n return data as ApiResponse<T>;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof ChannlizeError) {\n throw error;\n }\n\n if (error instanceof Error && error.name === 'AbortError') {\n throw new ChannlizeError('Request timeout', 408, 'TIMEOUT');\n }\n\n throw new ChannlizeError(\n `Network error: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n }\n}\n\nexport async function handleErrorResponse(response: Response): Promise<never> {\n const contentType = response.headers.get('content-type');\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let errorData: any = {};\n\n if (contentType?.includes('application/json')) {\n try {\n errorData = await response.json();\n } catch {\n // Ignore JSON parsing errors\n }\n }\n\n // API error shape (api/lib/api/error.ts):\n // { statusCode, error: \"BAD_REQUEST\", message: string | string[] }\n // `error` is the machine-readable code; `message` is the human text\n // (an array of field messages for validation failures).\n const rawMessage = errorData.message;\n const message =\n (Array.isArray(rawMessage) ? rawMessage.join(', ') : rawMessage) ||\n `HTTP ${response.status}: ${response.statusText}`;\n const code: string | undefined = errorData.code ?? errorData.error;\n\n switch (response.status) {\n case 401:\n throw new AuthenticationError(message);\n case 400:\n throw new ValidationError(message, errorData.field);\n case 429:\n throw new RateLimitError(message);\n default:\n throw new ChannlizeError(message, response.status, code);\n }\n}\n","/**\n * Core types for Channlize SDK\n */\n\n/** Severity for log tracking (`trackEvent` / `trackServerEvent`); Slack alerts use project settings. */\nexport type EventSeverity = 'info' | 'warning' | 'error';\n\n/** Options for log tracking (`trackEvent` / `trackServerEvent`). */\nexport interface TrackEventOptions {\n properties?: Record<string, unknown>;\n severity?: EventSeverity;\n distinctId?: string;\n pageUrl?: string;\n userAgent?: string;\n}\n\nexport interface ChannlizeConfig {\n /** Widget key used for public SDK endpoints (contact form, chat, browser log tracking) */\n apiKey: string;\n /** Base URL for the Channlize API */\n baseUrl?: string;\n /** Project ID (required for server-side `trackServerEvent` and `notify`) */\n projectId: string;\n /** Request timeout in milliseconds */\n timeout?: number;\n /**\n * Organization (server) API key, `ch_live_…`, for `trackServerEvent`\n * (`POST /v1/events`) and `notify` (`POST /v1/notify`, needs the `write`\n * scope). Do not expose in browser bundles.\n */\n serverApiKey?: string;\n}\n\n/**\n * Events (notify Slack from code). Augment this interface to type event\n * keys and their data across your app:\n *\n * ```ts\n * declare module '@channlize/core' {\n * interface ChannlizeEvents {\n * 'user.subscribed': { username: string; plan: string };\n * }\n * }\n * ```\n *\n * Or pass a map as a type argument: `createNotifier<AppEvents>()`.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ChannlizeEvents {}\n\n/** A declared event key (autocompleted) or any other key. */\nexport type NotifyEventName<TEvents extends object = ChannlizeEvents> =\n (keyof TEvents & string) | (string & {});\n\n/** The declared data for a key, or any object for undeclared keys. */\nexport type NotifyEventData<\n TEvents extends object = ChannlizeEvents,\n K = string,\n> = K extends keyof TEvents ? TEvents[K] : Record<string, unknown>;\n\nexport interface NotifyOptions {\n /**\n * Retry-safe sends: a key already used on the project returns the first\n * delivery (`duplicate: true`) and posts nothing. Sent as `Idempotency-Key`.\n */\n idempotencyKey?: string;\n /** Slack channel ID to post to instead of the event's channel, for this call. */\n channel?: string;\n}\n\nexport interface NotifyResponse {\n success: boolean;\n /** Delivery ID (see Events → Recent deliveries in the dashboard). */\n id: string;\n event: string;\n /**\n * `queued`: will be posted to Slack. `skipped`: not posted (see `reason`):\n * the event is disabled, is new (saved as a draft to configure), or there\n * is no Slack workspace / channel. With `duplicate`, the first call's status.\n */\n status: 'queued' | 'skipped' | 'sent' | 'failed';\n reason?: string;\n duplicate?: boolean;\n}\n\n/** `createNotifier` options; each falls back to an environment variable. */\nexport interface NotifierConfig {\n /**\n * Server API key (`ch_live_…`, write scope). Default:\n * `CHANNLIZE_SERVER_API_KEY`, else `CHANNLIZE_API_KEY`.\n */\n serverApiKey?: string;\n /** Project ID. Default: `CHANNLIZE_PROJECT_ID`. */\n projectId?: string;\n /** API base URL. Default: `CHANNLIZE_BASE_URL`, else `https://api.channlize.com`. */\n baseUrl?: string;\n /** Request timeout in ms (default 10000). */\n timeout?: number;\n}\n\nexport interface ContactFormData {\n /** Contact's name */\n name: string;\n /** Contact's email address */\n email: string;\n /** Subject of the inquiry */\n subject?: string;\n /** Message content */\n message: string;\n /** Custom fields */\n customFields?: Record<string, unknown>;\n /** Page URL where the form was submitted */\n pageUrl?: string;\n /** User agent string */\n userAgent?: string;\n}\n\nexport interface ChatMessage {\n /** Unique message ID */\n id: string;\n /** Message content */\n content: string;\n /** Message timestamp */\n timestamp: Date;\n /** Message author (user or agent) */\n author: 'user' | 'agent';\n /** Author metadata */\n authorData?: {\n name?: string;\n email?: string;\n avatarUrl?: string;\n };\n /** Attached files */\n attachments?: FileAttachment[];\n}\n\nexport interface FileAttachment {\n /** File ID */\n id: string;\n /** Original filename */\n name: string;\n /** File size in bytes */\n size: number;\n /** MIME type */\n type: string;\n /** Download URL */\n url: string;\n}\n\nexport interface ChatSession {\n /** Session ID */\n id: string;\n /** Session status */\n status: 'active' | 'closed' | 'waiting';\n /** Messages in the session */\n messages: ChatMessage[];\n /** Session metadata */\n metadata?: Record<string, unknown>;\n /** Created timestamp */\n createdAt: Date;\n /** Updated timestamp */\n updatedAt: Date;\n}\n\nexport interface ApiResponse<T = unknown> {\n /** Response data */\n data: T;\n /** Success status */\n success: boolean;\n /** Error message if any */\n error?: string;\n /** Additional metadata */\n meta?: Record<string, unknown>;\n}\n\n/** Response of `POST /public/contact` (api/app/api/public/contact/route.ts). */\nexport interface CreateContactResponse {\n /** Whether the submission was accepted */\n success: boolean;\n /** Conversation the submission was filed under */\n conversationId: string;\n /** Human-readable confirmation to show the visitor */\n message: string;\n /** @deprecated Never returned by the API (always undefined). Use `conversationId`. */\n id: string;\n /** @deprecated Never returned by the API (always undefined). */\n status: 'received' | 'processing' | 'delivered';\n /** @deprecated Never returned by the API (always undefined). */\n slackThreadUrl?: string;\n}\n\nexport interface CreateChatSessionResponse {\n /** Chat session data */\n session: ChatSession;\n /** SSE endpoint for real-time updates */\n sseEndpoint?: string;\n}\n\nexport interface SendMessageResponse {\n /** The sent message */\n message: ChatMessage;\n /** Updated session */\n session: ChatSession;\n}\n\n/**\n * Error types\n */\nexport class ChannlizeError extends Error {\n constructor(\n message: string,\n public statusCode?: number,\n public code?: string\n ) {\n super(message);\n this.name = 'ChannlizeError';\n }\n}\n\nexport class ValidationError extends ChannlizeError {\n constructor(\n message: string,\n public field?: string\n ) {\n super(message, 400, 'VALIDATION_ERROR');\n this.name = 'ValidationError';\n }\n}\n\nexport class AuthenticationError extends ChannlizeError {\n constructor(message: string = 'Authentication failed') {\n super(message, 401, 'AUTHENTICATION_ERROR');\n this.name = 'AuthenticationError';\n }\n}\n\nexport class RateLimitError extends ChannlizeError {\n constructor(message: string = 'Rate limit exceeded') {\n super(message, 429, 'RATE_LIMIT_ERROR');\n this.name = 'RateLimitError';\n }\n}\n","import { requestJson } from './http';\nimport type {\n ChannlizeEvents,\n NotifierConfig,\n NotifyEventData,\n NotifyEventName,\n NotifyOptions,\n NotifyResponse,\n} from './types';\nimport { ValidationError } from './types';\n\n/** Same rule as the API: `user.subscribed`, `invoice_paid`, `signup-done`. */\nconst EVENT_KEY = /^[a-z0-9][a-z0-9._-]*$/;\n\nconst DEFAULT_BASE_URL = 'https://api.channlize.com';\n\n/** `POST /v1/notify`, shared by `ChannlizeClient.notify` and `createNotifier`. */\nexport async function sendNotify(\n http: { baseUrl: string; timeout: number },\n auth: { serverApiKey: string; projectId: string },\n event: string,\n data: Record<string, unknown> | undefined,\n options: NotifyOptions = {}\n): Promise<NotifyResponse> {\n if (!auth.projectId) {\n throw new ValidationError('projectId is required for notify', 'projectId');\n }\n if (auth.serverApiKey.startsWith('wk_')) {\n throw new ValidationError(\n 'notify needs a server API key (ch_live_…, write scope), not the public widget key',\n 'serverApiKey'\n );\n }\n if (\n typeof event !== 'string' ||\n event.length > 100 ||\n !EVENT_KEY.test(event)\n ) {\n throw new ValidationError(\n `Invalid event key \"${event}\": use lowercase letters, digits, dots, underscores or dashes (e.g. user.subscribed)`,\n 'event'\n );\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${auth.serverApiKey}`,\n };\n if (options.idempotencyKey) {\n headers['Idempotency-Key'] = options.idempotencyKey;\n }\n\n const res = await requestJson<NotifyResponse>(http, '/v1/notify', {\n method: 'POST',\n headers,\n body: JSON.stringify({\n projectId: auth.projectId,\n event,\n data: data ?? {},\n channel: options.channel,\n }),\n });\n // The API answers at the top level (no `data` envelope).\n return res as unknown as NotifyResponse;\n}\n\nfunction readEnv(name: string): string | undefined {\n // `process` is absent in browsers and some edge runtimes.\n const env =\n typeof process !== 'undefined' && process.env ? process.env : undefined;\n const value = env?.[name];\n return value && value.trim() ? value.trim() : undefined;\n}\n\nexport type Notifier<TEvents extends object = ChannlizeEvents> = <\n K extends NotifyEventName<TEvents>,\n>(\n event: K,\n data?: NotifyEventData<TEvents, K>,\n options?: NotifyOptions\n) => Promise<NotifyResponse>;\n\n/**\n * Events: a standalone server-side `notify(event, data)` function. Reads\n * the server key from `CHANNLIZE_SERVER_API_KEY` or `CHANNLIZE_API_KEY`, and\n * `CHANNLIZE_PROJECT_ID`, unless given — when it is called, so creating it\n * at module load never throws.\n *\n * ```ts\n * const notify = createNotifier();\n * await notify('user.subscribed', { username: 'ada', plan: 'Pro' });\n * ```\n */\nexport function createNotifier<TEvents extends object = ChannlizeEvents>(\n config: NotifierConfig = {}\n): Notifier<TEvents> {\n return (event, data, options) => {\n const serverApiKey =\n config.serverApiKey ??\n readEnv('CHANNLIZE_SERVER_API_KEY') ??\n readEnv('CHANNLIZE_API_KEY');\n const projectId = config.projectId ?? readEnv('CHANNLIZE_PROJECT_ID');\n if (!serverApiKey) {\n return Promise.reject(\n new ValidationError(\n 'A server API key is required: pass serverApiKey or set CHANNLIZE_API_KEY (or CHANNLIZE_SERVER_API_KEY)',\n 'serverApiKey'\n )\n );\n }\n if (!projectId) {\n return Promise.reject(\n new ValidationError(\n 'A project ID is required: pass projectId or set CHANNLIZE_PROJECT_ID',\n 'projectId'\n )\n );\n }\n const baseUrl =\n config.baseUrl ?? readEnv('CHANNLIZE_BASE_URL') ?? DEFAULT_BASE_URL;\n return sendNotify(\n { baseUrl: baseUrl.replace(/\\/$/, ''), timeout: config.timeout ?? 10000 },\n { serverApiKey, projectId },\n event,\n data as Record<string, unknown> | undefined,\n options\n );\n };\n}\n","import { requestJson } from './http';\nimport { sendNotify } from './notify';\nimport type {\n ChannlizeConfig,\n ContactFormData,\n ChatMessage,\n ApiResponse,\n CreateContactResponse,\n CreateChatSessionResponse,\n SendMessageResponse,\n ChatSession,\n TrackEventOptions,\n EventSeverity,\n ChannlizeEvents,\n NotifyEventName,\n NotifyEventData,\n NotifyOptions,\n NotifyResponse,\n} from './types';\nimport { ValidationError } from './types';\n\n/**\n * Channlize API Client\n * Provides methods to interact with the Channlize platform.\n *\n * `TEvents` types `notify()` event keys and data (defaults to the\n * augmentable `ChannlizeEvents` interface; any string key is accepted).\n */\nexport class ChannlizeClient<TEvents extends object = ChannlizeEvents> {\n private readonly config: ChannlizeConfig & {\n baseUrl: string;\n timeout: number;\n };\n\n constructor(config: ChannlizeConfig) {\n this.config = {\n ...config,\n // After the spread: an explicit `undefined` must not drop the defaults.\n baseUrl: config.baseUrl || 'https://api.channlize.com',\n timeout: config.timeout ?? 10000,\n };\n\n this.validateConfig();\n }\n\n private validateConfig(): void {\n if (!this.config.apiKey) {\n throw new ValidationError('Widget key is required');\n }\n }\n\n private makeRequest<T>(\n endpoint: string,\n options: RequestInit = {}\n ): Promise<ApiResponse<T>> {\n return requestJson<T>(this.config, endpoint, options);\n }\n\n /**\n * Submit a contact form\n */\n async submitContactForm(\n data: ContactFormData\n ): Promise<CreateContactResponse> {\n const response = await this.makeRequest<CreateContactResponse>(\n '/public/contact',\n {\n method: 'POST',\n body: JSON.stringify({\n widgetKey: this.config.apiKey, // apiKey is actually the widgetKey\n subject: data.subject,\n message: data.message,\n email: data.email,\n name: data.name,\n customFields:\n data.customFields && Object.keys(data.customFields).length > 0\n ? data.customFields\n : undefined,\n pageUrl:\n data.pageUrl ||\n (typeof window !== 'undefined' ? window.location.href : undefined),\n userAgent:\n data.userAgent ||\n (typeof navigator !== 'undefined'\n ? navigator.userAgent\n : undefined),\n }),\n }\n );\n\n return response.data || response;\n }\n\n /**\n * Create a new chat session\n */\n async createChatSession(\n metadata?: Record<string, unknown>\n ): Promise<CreateChatSessionResponse> {\n const response = await this.makeRequest<CreateChatSessionResponse>(\n '/v1/chat/sessions',\n {\n method: 'POST',\n body: JSON.stringify({\n widgetKey: this.config.apiKey, // apiKey is actually the widgetKey\n metadata: {\n ...metadata,\n pageUrl:\n typeof window !== 'undefined' ? window.location.href : undefined,\n userAgent:\n typeof navigator !== 'undefined'\n ? navigator.userAgent\n : undefined,\n },\n }),\n }\n );\n\n return response.data;\n }\n\n /**\n * Send a message in a chat session.\n * When `attachments` are provided the request is sent as multipart/form-data\n * so the files are uploaded alongside the message content.\n */\n async sendMessage(\n sessionId: string,\n content: string,\n attachments?: File[]\n ): Promise<SendMessageResponse> {\n const idempotencyKey = `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n\n if (attachments && attachments.length > 0) {\n const formData = new FormData();\n formData.append('content', content);\n formData.append('idempotencyKey', idempotencyKey);\n for (const file of attachments) {\n formData.append('attachments', file, file.name);\n }\n\n const response = await this.makeRequest<SendMessageResponse>(\n `/v1/chat/sessions/${sessionId}/messages`,\n {\n method: 'POST',\n body: formData,\n // Let the browser set Content-Type with boundary for multipart\n headers: { 'Content-Type': '' },\n }\n );\n\n return response.data;\n }\n\n const response = await this.makeRequest<SendMessageResponse>(\n `/v1/chat/sessions/${sessionId}/messages`,\n {\n method: 'POST',\n body: JSON.stringify({\n content,\n idempotencyKey,\n }),\n }\n );\n\n return response.data;\n }\n\n /**\n * Get chat session details\n */\n async getChatSession(sessionId: string): Promise<ChatSession> {\n const response = await this.makeRequest<ChatSession>(\n `/v1/chat/sessions/${sessionId}`\n );\n return response.data;\n }\n\n /**\n * Get messages for a chat session\n */\n async getChatMessages(\n sessionId: string,\n limit = 50,\n offset = 0\n ): Promise<ChatMessage[]> {\n const response = await this.makeRequest<ChatMessage[]>(\n `/v1/chat/sessions/${sessionId}/messages?limit=${limit}&offset=${offset}`\n );\n return response.data;\n }\n\n /**\n * Close a chat session\n */\n async closeChatSession(sessionId: string): Promise<ChatSession> {\n const response = await this.makeRequest<ChatSession>(\n `/v1/chat/sessions/${sessionId}/close`,\n { method: 'POST' }\n );\n return response.data;\n }\n\n /**\n * Log tracking: record a log line from the browser (widget key →\n * `POST /public/events`). Shown under Log tracking in the dashboard.\n */\n async trackEvent(\n name: string,\n options?: TrackEventOptions\n ): Promise<{ success: boolean; id: string }> {\n const res = await this.makeRequest<{ success: boolean; id: string }>(\n '/public/events',\n {\n method: 'POST',\n body: JSON.stringify({\n widgetKey: this.config.apiKey,\n name,\n properties: options?.properties,\n severity: options?.severity,\n distinctId: options?.distinctId,\n pageUrl:\n options?.pageUrl ??\n (typeof window !== 'undefined' ? window.location.href : undefined),\n userAgent:\n options?.userAgent ??\n (typeof navigator !== 'undefined'\n ? navigator.userAgent\n : undefined),\n }),\n }\n );\n const data =\n (res as ApiResponse<{ success: boolean; id: string }>).data ??\n (res as unknown as { success: boolean; id: string });\n return data;\n }\n\n /**\n * Log tracking from a trusted server, using the organization API key\n * (`POST /v1/events`). Shown under Log tracking in the dashboard.\n */\n async trackServerEvent(\n name: string,\n options?: { properties?: Record<string, unknown>; severity?: EventSeverity }\n ): Promise<{ success: boolean; id: string }> {\n if (!this.config.serverApiKey) {\n throw new ValidationError(\n 'serverApiKey is required for trackServerEvent'\n );\n }\n const res = await this.makeRequest<{ success: boolean; id: string }>(\n '/v1/events',\n {\n method: 'POST',\n body: JSON.stringify({\n projectId: this.config.projectId,\n name,\n properties: options?.properties,\n severity: options?.severity,\n }),\n headers: {\n Authorization: `Bearer ${this.config.serverApiKey}`,\n },\n }\n );\n const data =\n (res as ApiResponse<{ success: boolean; id: string }>).data ??\n (res as unknown as { success: boolean; id: string });\n return data;\n }\n\n /**\n * Events: post a Slack message from your server when something happens\n * (`POST /v1/notify`). The event key picks the channel and message\n * template configured under Events in the dashboard; `data` fills the\n * template's `{{field}}` placeholders. Needs `serverApiKey` (write scope)\n * and `projectId` — never call it from browser code.\n *\n * An unknown key is saved as a disabled draft and answered with\n * `status: 'skipped'`, so it never throws for a missing setup.\n */\n async notify<K extends NotifyEventName<TEvents>>(\n event: K,\n data?: NotifyEventData<TEvents, K>,\n options?: NotifyOptions\n ): Promise<NotifyResponse> {\n if (!this.config.serverApiKey) {\n throw new ValidationError('serverApiKey is required for notify');\n }\n return sendNotify(\n this.config,\n {\n serverApiKey: this.config.serverApiKey,\n projectId: this.config.projectId,\n },\n event,\n data as Record<string, unknown> | undefined,\n options\n );\n }\n}\n"],"mappings":"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","names":["response"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@channlize/core",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.2.1",
4
4
  "description": "Channlize Core SDK - TypeScript API wrapper for Channlize platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -18,9 +18,9 @@
18
18
  "scripts": {
19
19
  "build": "tsup",
20
20
  "dev": "tsup --watch",
21
- "lint": "eslint src --ext .ts",
22
- "lint:fix": "eslint src --ext .ts --fix",
23
- "type-check": "tsc --noEmit",
21
+ "lint": "eslint src",
22
+ "lint:fix": "eslint src --fix",
23
+ "type-check": "tsc6 --noEmit",
24
24
  "clean": "rm -rf dist"
25
25
  },
26
26
  "keywords": [
@@ -35,12 +35,10 @@
35
35
  "cross-fetch": "^4.0.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@types/node": "^20.11.5",
39
- "@typescript-eslint/eslint-plugin": "^6.21.0",
40
- "@typescript-eslint/parser": "^6.21.0",
41
- "eslint": "^8.56.0",
42
- "tsup": "^8.0.1",
43
- "typescript": "^5.3.3"
38
+ "@types/node": "^26.0.0",
39
+ "eslint": "^10.0.0",
40
+ "tsup": "^9.0.0",
41
+ "typescript": "npm:@typescript/typescript6@^6.0.2"
44
42
  },
45
43
  "publishConfig": {
46
44
  "access": "public"
@@ -50,4 +48,4 @@
50
48
  "url": "git+https://github.com/channlize/sdk.git",
51
49
  "directory": "packages/core"
52
50
  }
53
- }
51
+ }