@cstar.help/js 0.12.1 → 0.14.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.
@@ -23,12 +23,234 @@ __export(library_exports, {
23
23
  LibraryClient: () => LibraryClient
24
24
  });
25
25
  module.exports = __toCommonJS(library_exports);
26
+
27
+ // src/errors.ts
28
+ var ErrorType = {
29
+ AUTHENTICATION_ERROR: "authentication_error",
30
+ PERMISSION_ERROR: "permission_error",
31
+ INVALID_REQUEST_ERROR: "invalid_request_error",
32
+ NOT_FOUND_ERROR: "not_found_error",
33
+ CONFLICT_ERROR: "conflict_error",
34
+ RATE_LIMIT_ERROR: "rate_limit_error",
35
+ FEATURE_NOT_CONFIGURED: "feature_not_configured",
36
+ API_ERROR: "api_error",
37
+ SERVER_ERROR: "server_error"
38
+ };
39
+ var DEFAULT_DOC_URL = "https://cstar.help/developers/api/errors";
40
+ function normalizeArgs(messageOrStatus, maybeBody, defaults) {
41
+ if (typeof messageOrStatus === "string") {
42
+ return {
43
+ statusCode: defaults?.code === "rate_limit_error" ? 429 : 0,
44
+ body: {
45
+ type: defaults?.type ?? ErrorType.API_ERROR,
46
+ code: defaults?.code ?? "sdk_error",
47
+ message: messageOrStatus,
48
+ doc_url: defaults?.doc_url ?? DEFAULT_DOC_URL,
49
+ ...defaults?.feature ? { feature: defaults.feature } : {},
50
+ ...defaults?.enable_url ? { enable_url: defaults.enable_url } : {}
51
+ }
52
+ };
53
+ }
54
+ return {
55
+ statusCode: messageOrStatus,
56
+ body: maybeBody ?? {
57
+ type: defaults?.type ?? ErrorType.API_ERROR,
58
+ code: defaults?.code ?? "unknown_error",
59
+ message: defaults?.message ?? `Request failed with status ${messageOrStatus}`,
60
+ doc_url: defaults?.doc_url ?? DEFAULT_DOC_URL
61
+ }
62
+ };
63
+ }
64
+ var CStarError = class extends Error {
65
+ constructor(messageOrStatus, body) {
66
+ const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body);
67
+ super(resolved.message);
68
+ this.name = "CStarError";
69
+ this.type = resolved.type;
70
+ this.code = resolved.code;
71
+ this.param = resolved.param;
72
+ this.docUrl = resolved.doc_url;
73
+ this.requestId = resolved.request_id;
74
+ this.statusCode = statusCode;
75
+ }
76
+ };
77
+ var CStarAuthenticationError = class extends CStarError {
78
+ constructor(messageOrStatus, body) {
79
+ const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
80
+ type: ErrorType.AUTHENTICATION_ERROR,
81
+ code: "authentication_failed",
82
+ doc_url: `${DEFAULT_DOC_URL}#authentication-error`
83
+ });
84
+ super(statusCode, resolved);
85
+ this.name = "CStarAuthenticationError";
86
+ }
87
+ };
88
+ var CStarRateLimitError = class extends CStarError {
89
+ constructor(messageOrStatus, body, retryAfter) {
90
+ const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
91
+ type: ErrorType.RATE_LIMIT_ERROR,
92
+ code: "rate_limit_exceeded",
93
+ doc_url: `${DEFAULT_DOC_URL}#rate-limit-error`
94
+ });
95
+ super(statusCode === 0 ? 429 : statusCode, resolved);
96
+ this.name = "CStarRateLimitError";
97
+ this.retryAfter = retryAfter ?? 60;
98
+ }
99
+ };
100
+ var CStarNotFoundError = class extends CStarError {
101
+ constructor(messageOrStatus, body) {
102
+ const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
103
+ type: ErrorType.NOT_FOUND_ERROR,
104
+ code: "resource_not_found",
105
+ doc_url: `${DEFAULT_DOC_URL}#not-found-error`
106
+ });
107
+ super(statusCode, resolved);
108
+ this.name = "CStarNotFoundError";
109
+ }
110
+ };
111
+ var CStarValidationError = class extends CStarError {
112
+ constructor(messageOrStatus, body) {
113
+ const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
114
+ type: ErrorType.INVALID_REQUEST_ERROR,
115
+ code: "validation_error",
116
+ doc_url: `${DEFAULT_DOC_URL}#validation-error`
117
+ });
118
+ super(statusCode, resolved);
119
+ this.name = "CStarValidationError";
120
+ }
121
+ };
122
+ var CStarPermissionError = class extends CStarError {
123
+ constructor(messageOrStatus, body) {
124
+ const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
125
+ type: ErrorType.PERMISSION_ERROR,
126
+ code: "permission_denied",
127
+ doc_url: `${DEFAULT_DOC_URL}#permission-error`
128
+ });
129
+ super(statusCode, resolved);
130
+ this.name = "CStarPermissionError";
131
+ }
132
+ };
133
+ var CStarConflictError = class extends CStarError {
134
+ constructor(messageOrStatus, body) {
135
+ const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
136
+ type: ErrorType.CONFLICT_ERROR,
137
+ code: "conflict",
138
+ doc_url: `${DEFAULT_DOC_URL}#conflict-error`
139
+ });
140
+ super(statusCode, resolved);
141
+ this.name = "CStarConflictError";
142
+ }
143
+ };
144
+ var CStarServerError = class extends CStarError {
145
+ constructor(messageOrStatus, body) {
146
+ const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
147
+ type: ErrorType.SERVER_ERROR,
148
+ code: "server_error",
149
+ doc_url: `${DEFAULT_DOC_URL}#server-error`
150
+ });
151
+ super(statusCode, resolved);
152
+ this.name = "CStarServerError";
153
+ }
154
+ };
155
+ var CStarFeatureDisabledError = class extends CStarError {
156
+ constructor(messageOrStatus, body) {
157
+ const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
158
+ type: ErrorType.FEATURE_NOT_CONFIGURED,
159
+ code: "feature_not_configured",
160
+ doc_url: `${DEFAULT_DOC_URL}#feature-not-configured`
161
+ });
162
+ super(statusCode, resolved);
163
+ this.name = "CStarFeatureDisabledError";
164
+ this.feature = resolved.feature ?? "unknown";
165
+ this.enableUrl = resolved.enable_url ?? "";
166
+ }
167
+ };
168
+ function parsePublicError(status, body, fallbackMessage) {
169
+ const obj = body && typeof body === "object" ? body : null;
170
+ const errVal = obj?.error;
171
+ let message = fallbackMessage;
172
+ let structured = null;
173
+ if (typeof errVal === "string") {
174
+ message = errVal;
175
+ } else if (errVal && typeof errVal === "object") {
176
+ const e = errVal;
177
+ if (typeof e.message === "string") message = e.message;
178
+ structured = {
179
+ type: typeof e.type === "string" ? e.type : ErrorType.API_ERROR,
180
+ code: typeof e.code === "string" ? e.code : "api_error",
181
+ message,
182
+ param: typeof e.param === "string" ? e.param : void 0,
183
+ doc_url: typeof e.doc_url === "string" ? e.doc_url : DEFAULT_DOC_URL,
184
+ request_id: typeof e.request_id === "string" ? e.request_id : void 0,
185
+ feature: typeof e.feature === "string" ? e.feature : void 0,
186
+ enable_url: typeof e.enable_url === "string" ? e.enable_url : void 0
187
+ };
188
+ } else if (typeof obj?.message === "string") {
189
+ message = obj.message;
190
+ }
191
+ const errorBody = structured ?? {
192
+ type: ErrorType.API_ERROR,
193
+ code: status >= 500 ? "server_error" : "request_failed",
194
+ message,
195
+ doc_url: DEFAULT_DOC_URL
196
+ };
197
+ return parseError(status, { error: errorBody });
198
+ }
199
+ var STATUS_TO_CLASS = {
200
+ 400: CStarValidationError,
201
+ 401: CStarAuthenticationError,
202
+ 403: CStarPermissionError,
203
+ 404: CStarNotFoundError,
204
+ 409: CStarConflictError,
205
+ 422: CStarValidationError,
206
+ 429: CStarRateLimitError
207
+ };
208
+ var TYPE_TO_CLASS = {
209
+ [ErrorType.AUTHENTICATION_ERROR]: CStarAuthenticationError,
210
+ [ErrorType.PERMISSION_ERROR]: CStarPermissionError,
211
+ [ErrorType.RATE_LIMIT_ERROR]: CStarRateLimitError,
212
+ [ErrorType.NOT_FOUND_ERROR]: CStarNotFoundError,
213
+ [ErrorType.INVALID_REQUEST_ERROR]: CStarValidationError,
214
+ [ErrorType.CONFLICT_ERROR]: CStarConflictError,
215
+ [ErrorType.SERVER_ERROR]: CStarServerError,
216
+ [ErrorType.FEATURE_NOT_CONFIGURED]: CStarFeatureDisabledError
217
+ };
218
+ function parseError(statusCode, body, retryAfter) {
219
+ const errorBody = body?.error ?? {
220
+ type: ErrorType.API_ERROR,
221
+ code: "unknown_error",
222
+ message: `Request failed with status ${statusCode}`,
223
+ doc_url: DEFAULT_DOC_URL
224
+ };
225
+ if (statusCode === 429) {
226
+ return new CStarRateLimitError(statusCode, errorBody, retryAfter ?? 60);
227
+ }
228
+ const StatusCls = STATUS_TO_CLASS[statusCode];
229
+ if (StatusCls) {
230
+ return new StatusCls(statusCode, errorBody);
231
+ }
232
+ const TypeCls = TYPE_TO_CLASS[errorBody.type];
233
+ if (TypeCls) {
234
+ return new TypeCls(statusCode, errorBody);
235
+ }
236
+ if (statusCode >= 500) return new CStarServerError(statusCode, errorBody);
237
+ return new CStarError(statusCode, errorBody);
238
+ }
239
+
240
+ // src/library/index.ts
26
241
  var DEFAULT_BASE_URL = "https://www.cstar.help";
27
242
  var LibraryClient = class {
28
243
  constructor(config) {
29
244
  this.resolvedTeamId = null;
30
245
  this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
31
246
  this.teamSlug = config.teamSlug;
247
+ this.publishableKey = config.publishableKey ?? null;
248
+ this.mode = this.publishableKey?.startsWith("pk_test_") ? "test" : "live";
249
+ if (this.publishableKey && !/^pk_(test|live)_/.test(this.publishableKey)) {
250
+ throw new Error(
251
+ "[cStar] LibraryClient: publishableKey must start with `pk_test_` or `pk_live_`. Secret keys (`sk_*`) cannot be used in browser/anonymous contexts."
252
+ );
253
+ }
32
254
  }
33
255
  /** List all public categories with article counts. */
34
256
  async categories() {
@@ -78,13 +300,19 @@ var LibraryClient = class {
78
300
  */
79
301
  async recordView(slug) {
80
302
  const url = `${this.baseUrl}/api/library/${this.teamSlug}/articles/${slug}/view`;
303
+ const headers = { "Content-Type": "application/json" };
304
+ if (this.publishableKey) headers["Authorization"] = `Bearer ${this.publishableKey}`;
81
305
  const response = await globalThis.fetch(url, {
82
306
  method: "POST",
83
- headers: { "Content-Type": "application/json" }
307
+ headers
84
308
  });
85
309
  if (!response.ok) {
86
- const errorBody = await response.json().catch(() => ({ error: "Request failed" }));
87
- throw new Error(errorBody.error ?? `View tracking failed: ${response.status}`);
310
+ const errorBody = await response.json().catch(() => null);
311
+ throw parsePublicError(
312
+ response.status,
313
+ errorBody,
314
+ `View tracking failed: ${response.status}`
315
+ );
88
316
  }
89
317
  const body = await response.json();
90
318
  return { viewCount: body.viewCount };
@@ -106,14 +334,27 @@ var LibraryClient = class {
106
334
  */
107
335
  async submitFeedback(slug, params) {
108
336
  const url = `${this.baseUrl}/api/library/${this.teamSlug}/articles/${slug}/feedback`;
337
+ const headers = { "Content-Type": "application/json" };
338
+ if (this.publishableKey) headers["Authorization"] = `Bearer ${this.publishableKey}`;
109
339
  const response = await globalThis.fetch(url, {
110
340
  method: "POST",
111
- headers: { "Content-Type": "application/json" },
341
+ headers,
112
342
  body: JSON.stringify(params)
113
343
  });
114
- const body = await response.json();
344
+ const body = await response.json().catch(() => null);
115
345
  if (!response.ok) {
116
- throw new Error(body.error ?? `Feedback submission failed: ${response.status}`);
346
+ throw parsePublicError(
347
+ response.status,
348
+ body,
349
+ body?.error ?? `Feedback submission failed: ${response.status}`
350
+ );
351
+ }
352
+ if (body === null) {
353
+ throw parsePublicError(
354
+ response.status,
355
+ null,
356
+ `Feedback submission returned an invalid JSON response (status ${response.status})`
357
+ );
117
358
  }
118
359
  return body;
119
360
  }
@@ -144,13 +385,17 @@ var LibraryClient = class {
144
385
  const qs = params.toString();
145
386
  if (qs) url += `?${qs}`;
146
387
  }
388
+ const headers = { "Content-Type": "application/json" };
389
+ if (this.publishableKey) {
390
+ headers["Authorization"] = `Bearer ${this.publishableKey}`;
391
+ }
147
392
  const response = await globalThis.fetch(url, {
148
393
  method: "GET",
149
- headers: { "Content-Type": "application/json" }
394
+ headers
150
395
  });
151
396
  if (!response.ok) {
152
- const errorBody = await response.json().catch(() => ({ error: { message: "Request failed" } }));
153
- throw new Error(errorBody.error?.message ?? `Library API error: ${response.status}`);
397
+ const errorBody = await response.json().catch(() => null);
398
+ throw parsePublicError(response.status, errorBody, `Library API error: ${response.status}`);
154
399
  }
155
400
  return response.json();
156
401
  }
@@ -2,6 +2,22 @@
2
2
  interface LibraryConfig {
3
3
  /** Team slug (e.g., 'acme'). Identifies the knowledge base to query. */
4
4
  teamSlug: string;
5
+ /**
6
+ * Optional publishable key (`pk_test_*` or `pk_live_*`).
7
+ *
8
+ * - With no key, the client behaves anonymously and returns live data
9
+ * (backwards-compatible with existing widget integrations).
10
+ * - With `pk_test_*`, the client switches to test mode and returns only
11
+ * `is_test_data = true` rows — useful for previewing seeded test data
12
+ * against your workspace before going live.
13
+ * - With `pk_live_*`, the client returns live data with the optional
14
+ * benefit of higher per-key rate limits.
15
+ *
16
+ * The key is sent as `Authorization: Bearer <key>` and is validated
17
+ * server-side against the team. Wrong-team keys 403; secret keys (`sk_*`)
18
+ * are rejected at this surface.
19
+ */
20
+ publishableKey?: string;
5
21
  /** Override the base URL (defaults to https://www.cstar.help). */
6
22
  baseUrl?: string;
7
23
  }
@@ -73,17 +89,24 @@ interface ArticleFeedbackResult {
73
89
  }
74
90
 
75
91
  /**
76
- * Public knowledge base client. No authentication required.
92
+ * Public knowledge base client. Authentication is optional — supply a
93
+ * publishable key (`pk_test_*` or `pk_live_*`) to switch the data scope
94
+ * (test vs live) and bypass the anonymous rate limit.
77
95
  *
78
96
  * ```typescript
97
+ * // Anonymous live (default — backwards-compatible widget setup)
79
98
  * const library = new LibraryClient({ teamSlug: 'acme' });
80
- * const categories = await library.categories();
81
- * const results = await library.search('reset password');
99
+ *
100
+ * // Preview seeded test data against your workspace
101
+ * const test = new LibraryClient({ teamSlug: 'acme', publishableKey: 'pk_test_...' });
82
102
  * ```
83
103
  */
84
104
  declare class LibraryClient {
85
105
  private readonly baseUrl;
86
106
  private readonly teamSlug;
107
+ private readonly publishableKey;
108
+ /** Resolved mode based on the publishable-key prefix. */
109
+ readonly mode: 'live' | 'test';
87
110
  private resolvedTeamId;
88
111
  constructor(config: LibraryConfig);
89
112
  /** List all public categories with article counts. */
@@ -2,6 +2,22 @@
2
2
  interface LibraryConfig {
3
3
  /** Team slug (e.g., 'acme'). Identifies the knowledge base to query. */
4
4
  teamSlug: string;
5
+ /**
6
+ * Optional publishable key (`pk_test_*` or `pk_live_*`).
7
+ *
8
+ * - With no key, the client behaves anonymously and returns live data
9
+ * (backwards-compatible with existing widget integrations).
10
+ * - With `pk_test_*`, the client switches to test mode and returns only
11
+ * `is_test_data = true` rows — useful for previewing seeded test data
12
+ * against your workspace before going live.
13
+ * - With `pk_live_*`, the client returns live data with the optional
14
+ * benefit of higher per-key rate limits.
15
+ *
16
+ * The key is sent as `Authorization: Bearer <key>` and is validated
17
+ * server-side against the team. Wrong-team keys 403; secret keys (`sk_*`)
18
+ * are rejected at this surface.
19
+ */
20
+ publishableKey?: string;
5
21
  /** Override the base URL (defaults to https://www.cstar.help). */
6
22
  baseUrl?: string;
7
23
  }
@@ -73,17 +89,24 @@ interface ArticleFeedbackResult {
73
89
  }
74
90
 
75
91
  /**
76
- * Public knowledge base client. No authentication required.
92
+ * Public knowledge base client. Authentication is optional — supply a
93
+ * publishable key (`pk_test_*` or `pk_live_*`) to switch the data scope
94
+ * (test vs live) and bypass the anonymous rate limit.
77
95
  *
78
96
  * ```typescript
97
+ * // Anonymous live (default — backwards-compatible widget setup)
79
98
  * const library = new LibraryClient({ teamSlug: 'acme' });
80
- * const categories = await library.categories();
81
- * const results = await library.search('reset password');
99
+ *
100
+ * // Preview seeded test data against your workspace
101
+ * const test = new LibraryClient({ teamSlug: 'acme', publishableKey: 'pk_test_...' });
82
102
  * ```
83
103
  */
84
104
  declare class LibraryClient {
85
105
  private readonly baseUrl;
86
106
  private readonly teamSlug;
107
+ private readonly publishableKey;
108
+ /** Resolved mode based on the publishable-key prefix. */
109
+ readonly mode: 'live' | 'test';
87
110
  private resolvedTeamId;
88
111
  constructor(config: LibraryConfig);
89
112
  /** List all public categories with article counts. */