cime-sdk 1.0.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/dist/index.mjs ADDED
@@ -0,0 +1,768 @@
1
+ import axios from 'axios';
2
+ import axiosRetry from 'axios-retry';
3
+ import WebSocket from 'ws';
4
+ import { EventEmitter } from 'events';
5
+
6
+ // src/utils/version.ts
7
+
8
+ // package.json
9
+ var package_default = {
10
+ name: "cime-sdk",
11
+ version: "1.0.0",
12
+ description: "A structural and object-oriented TypeScript SDK for ci.me API",
13
+ main: "dist/index.js",
14
+ module: "dist/index.mjs",
15
+ types: "dist/index.d.ts",
16
+ files: [
17
+ "dist"
18
+ ],
19
+ exports: {
20
+ ".": {
21
+ require: "./dist/index.js",
22
+ import: "./dist/index.mjs",
23
+ types: "./dist/index.d.ts"
24
+ }
25
+ },
26
+ scripts: {
27
+ build: "tsup",
28
+ dev: "tsup --watch",
29
+ test: "vitest",
30
+ typecheck: "tsc --noEmit"
31
+ },
32
+ keywords: ["cime", "sdk", "typescript"],
33
+ author: "whitespaca",
34
+ license: "MIT",
35
+ repository: { type: "git", url: "https://github.com/whitespaca/cime-sdk" },
36
+ type: "commonjs",
37
+ dependencies: {
38
+ axios: "^1.15.0",
39
+ "axios-retry": "^4.5.0",
40
+ ws: "^8.20.0"
41
+ },
42
+ devDependencies: {
43
+ "@types/node": "^25.6.0",
44
+ "@types/ws": "^8.18.1",
45
+ tsup: "^8.5.1",
46
+ typescript: "^6.0.2",
47
+ vitest: "^4.1.4"
48
+ }
49
+ };
50
+
51
+ // src/utils/version.ts
52
+ function getVersion() {
53
+ return package_default.version;
54
+ }
55
+ async function checkVersion() {
56
+ try {
57
+ const localVersion = package_default.version;
58
+ const packageName = package_default.name;
59
+ const { data, status } = await axios.get(`https://registry.npmjs.org/${packageName}/latest`, {
60
+ timeout: 3e3
61
+ });
62
+ if (status !== 200 || !data || !data.version) {
63
+ return;
64
+ }
65
+ const remoteVersion = data.version;
66
+ if (isNewerVersion(localVersion, remoteVersion)) {
67
+ console.log(`
68
+ ======================================================`);
69
+ console.log(`\u{1F680} [CIME SDK] \uC0C8\uB85C\uC6B4 \uBC84\uC804\uC744 \uCC3E\uC558\uC2B5\uB2C8\uB2E4!`);
70
+ console.log(`\uD604\uC7AC \uBC84\uC804: ${localVersion} -> \uCD5C\uC2E0 \uBC84\uC804: ${remoteVersion}`);
71
+ console.log(`UPDATE: npm install ${packageName}@${remoteVersion}`);
72
+ console.log(`======================================================
73
+ `);
74
+ }
75
+ } catch (error) {
76
+ return;
77
+ }
78
+ }
79
+ function isNewerVersion(local, remote) {
80
+ const lParts = local.split(".").map(Number);
81
+ const rParts = remote.split(".").map(Number);
82
+ const maxLength = Math.max(lParts.length, rParts.length);
83
+ for (let i = 0; i < maxLength; i++) {
84
+ const l = lParts[i] || 0;
85
+ const r = rParts[i] || 0;
86
+ if (r > l) return true;
87
+ if (r < l) return false;
88
+ }
89
+ return false;
90
+ }
91
+
92
+ // src/errors/CimeAPIError.ts
93
+ var CimeAPIError = class _CimeAPIError extends Error {
94
+ statusCode;
95
+ /**
96
+ * @param errorResponse - API 서버에서 반환한 에러 객체
97
+ */
98
+ constructor(errorResponse) {
99
+ super(errorResponse.message);
100
+ this.name = "CimeAPIError";
101
+ this.statusCode = errorResponse.statusCode;
102
+ Object.setPrototypeOf(this, _CimeAPIError.prototype);
103
+ }
104
+ };
105
+
106
+ // src/utils/httpClient.ts
107
+ function createHttpClient(options) {
108
+ const client = axios.create({
109
+ baseURL: "https://ci.me/api/openapi",
110
+ timeout: options.timeout || 1e4,
111
+ headers: {
112
+ "Content-Type": "application/json",
113
+ "User-Agent": `CimeSDK/${getVersion()} (Node)`
114
+ }
115
+ });
116
+ client.interceptors.request.use((config) => {
117
+ if (options.accessToken) {
118
+ config.headers["Authorization"] = `Bearer ${options.accessToken}`;
119
+ }
120
+ if (options.clientId && options.clientSecret) {
121
+ config.headers["Client-Id"] = options.clientId;
122
+ config.headers["Client-Secret"] = options.clientSecret;
123
+ }
124
+ return config;
125
+ });
126
+ axiosRetry(client, {
127
+ retries: options.retries ?? 3,
128
+ retryDelay: axiosRetry.exponentialDelay,
129
+ retryCondition: (error) => {
130
+ return axiosRetry.isNetworkOrIdempotentRequestError(error) || error.response?.status === 500;
131
+ }
132
+ });
133
+ client.interceptors.response.use(
134
+ (response) => {
135
+ return response.data.content;
136
+ },
137
+ (error) => {
138
+ if (error.response && error.response.data) {
139
+ throw new CimeAPIError(error.response.data);
140
+ }
141
+ throw error;
142
+ }
143
+ );
144
+ return client;
145
+ }
146
+
147
+ // src/api/auth.ts
148
+ var AuthAPI = class {
149
+ constructor(http, config) {
150
+ this.http = http;
151
+ this.config = config;
152
+ }
153
+ http;
154
+ config;
155
+ /**
156
+ * Authorization Code를 사용하여 Access Token과 Refresh Token을 발급받습니다.
157
+ * @param code Redirect URI로 전달받은 인가 코드
158
+ */
159
+ async get(code) {
160
+ this.validateCredentials();
161
+ const { data } = await this.http.post("/api/openapi/auth/v1/token", {
162
+ grantType: "authorization_code",
163
+ clientId: this.config.clientId,
164
+ clientSecret: this.config.clientSecret,
165
+ code
166
+ });
167
+ return data;
168
+ }
169
+ /**
170
+ * Refresh Token을 사용하여 Access Token을 갱신합니다.
171
+ * @param refreshToken 이전에 발급받은 리프레시 토큰
172
+ */
173
+ async refresh(refreshToken) {
174
+ this.validateCredentials();
175
+ const { data } = await this.http.post("/api/openapi/auth/v1/token", {
176
+ grantType: "refresh_token",
177
+ clientId: this.config.clientId,
178
+ clientSecret: this.config.clientSecret,
179
+ refreshToken
180
+ });
181
+ return data;
182
+ }
183
+ /**
184
+ * 내부 헬퍼: 토큰 발급 시 필요한 Client ID 및 Secret 유효성 검사 (Fail-fast)
185
+ */
186
+ validateCredentials() {
187
+ if (!this.config.clientId || !this.config.clientSecret) {
188
+ throw new Error(
189
+ "[Cime SDK] OAuth \uD1A0\uD070\uC744 \uBC1C\uAE09/\uAC31\uC2E0\uD558\uB824\uBA74 \uCD08\uAE30\uD654 \uC2DC clientId\uC640 clientSecret\uC774 \uD544\uC694\uD569\uB2C8\uB2E4."
190
+ );
191
+ }
192
+ }
193
+ };
194
+
195
+ // src/api/users.ts
196
+ var UsersAPI = class {
197
+ constructor(httpClient, options) {
198
+ this.httpClient = httpClient;
199
+ this.options = options;
200
+ }
201
+ httpClient;
202
+ options;
203
+ /**
204
+ * 현재 Access Token에 연결된 사용자의 채널 정보를 조회합니다.
205
+ *
206
+ * @requires Scope: `READ:USER`
207
+ * @throws {CimeAPIError} Access Token이 없거나 만료되었을 때 (401), 또는 권한이 부족할 때 발생합니다.
208
+ * @returns 사용자의 채널 상세 정보
209
+ * * @example
210
+ * ```typescript
211
+ * const me = await client.users.get();
212
+ * console.log(`안녕하세요, ${me.channelName}님!`);
213
+ * ```
214
+ */
215
+ async get() {
216
+ if (!this.options.scopes?.includes("READ:USER")) {
217
+ throw new Error('[UsersAPI] "READ:USER" \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. OAuth \uC778\uC99D \uC2DC \uD574\uB2F9 \uAD8C\uD55C\uC744 \uD3EC\uD568\uC2DC\uCF1C \uC8FC\uC138\uC694.');
218
+ }
219
+ return this.httpClient.get("/open/v1/users/me");
220
+ }
221
+ };
222
+
223
+ // src/api/channels.ts
224
+ var ChannelsAPI = class {
225
+ constructor(httpClient, options) {
226
+ this.httpClient = httpClient;
227
+ this.options = options;
228
+ }
229
+ httpClient;
230
+ options;
231
+ /**
232
+ * 채널 ID 목록으로 채널의 기본 정보를 조회합니다. (최대 20개)
233
+ *
234
+ * @requires Auth: `Client ID / Secret` (공개 API)
235
+ * @param channelIds - 조회할 채널 ID 문자열 또는 ID 문자열의 배열
236
+ * @returns 채널 정보 목록
237
+ * * @example
238
+ * ```typescript
239
+ * const { data } = await client.channels.getChannels(['12345', '67890']);
240
+ * ```
241
+ */
242
+ async getChannels(channelIds) {
243
+ const parsedIds = Array.isArray(channelIds) ? channelIds.join(",") : channelIds;
244
+ return this.httpClient.get("/open/v1/channels", {
245
+ params: { channelIds: parsedIds }
246
+ });
247
+ }
248
+ /**
249
+ * 인증된 사용자 채널의 팔로워 목록을 조회합니다.
250
+ *
251
+ * @requires Auth: `Access Token`
252
+ * @requires Scope: `READ:CHANNEL`
253
+ * @param params - 페이징 파라미터 (page, size)
254
+ * @returns 팔로워 목록
255
+ */
256
+ async getFollowers(params) {
257
+ if (!this.options.scopes?.includes("READ:CHANNEL")) {
258
+ throw new Error('[ChannelsAPI] "READ:CHANNEL" \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. OAuth \uC778\uC99D \uC2DC \uD574\uB2F9 \uAD8C\uD55C\uC744 \uD3EC\uD568\uC2DC\uCF1C \uC8FC\uC138\uC694.');
259
+ }
260
+ return this.httpClient.get("/open/v1/channels/followers", {
261
+ params
262
+ });
263
+ }
264
+ /**
265
+ * 인증된 사용자 채널의 구독자 목록을 조회합니다.
266
+ *
267
+ * @requires Auth: `Access Token`
268
+ * @requires Scope: `READ:SUBSCRIPTION`
269
+ * @param params - 페이징 및 정렬 파라미터 (page, size, sort)
270
+ * @returns 구독자 목록
271
+ */
272
+ async getSubscribers(params) {
273
+ if (!this.options.scopes?.includes("READ:SUBSCRIPTION")) {
274
+ throw new Error('[ChannelsAPI] "READ:SUBSCRIPTION" \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. OAuth \uC778\uC99D \uC2DC \uD574\uB2F9 \uAD8C\uD55C\uC744 \uD3EC\uD568\uC2DC\uCF1C \uC8FC\uC138\uC694.');
275
+ }
276
+ return this.httpClient.get("/open/v1/channels/subscribers", {
277
+ params
278
+ });
279
+ }
280
+ /**
281
+ * 인증된 사용자 채널의 관리자 목록을 조회합니다.
282
+ *
283
+ * @requires Auth: `Access Token`
284
+ * @requires Scope: `READ:CHANNEL`
285
+ * @returns 관리자 목록
286
+ */
287
+ async getManagers() {
288
+ if (!this.options.scopes?.includes("READ:CHANNEL")) {
289
+ throw new Error('[ChannelsAPI] "READ:CHANNEL" \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. OAuth \uC778\uC99D \uC2DC \uD574\uB2F9 \uAD8C\uD55C\uC744 \uD3EC\uD568\uC2DC\uCF1C \uC8FC\uC138\uC694.');
290
+ }
291
+ return this.httpClient.get("/open/v1/channels/streaming-roles");
292
+ }
293
+ };
294
+
295
+ // src/api/live.ts
296
+ var LiveAPI = class {
297
+ constructor(httpClient, options) {
298
+ this.httpClient = httpClient;
299
+ this.options = options;
300
+ }
301
+ httpClient;
302
+ options;
303
+ /**
304
+ * 현재 진행 중인 라이브 방송 목록을 조회합니다.
305
+ *
306
+ * @requires Auth: `Client ID / Secret` (공개 API)
307
+ * @param params - 커서 기반 페이징 파라미터 (size, next)
308
+ * @returns 라이브 목록과 다음 페이지 커서 정보
309
+ */
310
+ async getLives(params) {
311
+ return this.httpClient.get("/open/v1/lives", {
312
+ params
313
+ });
314
+ }
315
+ /**
316
+ * 인증된 사용자 채널의 라이브 설정을 조회합니다.
317
+ *
318
+ * @requires Auth: `Access Token`
319
+ * @requires Scope: `READ:LIVE_STREAM_SETTINGS`
320
+ * @returns 라이브 설정 정보
321
+ */
322
+ async getSettings() {
323
+ if (!this.options.scopes?.includes("READ:LIVE_STREAM_SETTINGS")) {
324
+ throw new Error('[LiveAPI] "READ:LIVE_STREAM_SETTINGS" \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. OAuth \uC778\uC99D \uC2DC \uD574\uB2F9 \uAD8C\uD55C\uC744 \uD3EC\uD568\uC2DC\uCF1C \uC8FC\uC138\uC694.');
325
+ }
326
+ return this.httpClient.get("/open/v1/lives/setting");
327
+ }
328
+ /**
329
+ * 인증된 사용자 채널의 라이브 설정을 변경합니다. (전달된 필드만 업데이트)
330
+ *
331
+ * @requires Auth: `Access Token`
332
+ * @requires Scope: `WRITE:LIVE_STREAM_SETTINGS`
333
+ * @param params - 변경할 라이브 설정 데이터
334
+ */
335
+ async updateSettings(params) {
336
+ if (!this.options.scopes?.includes("WRITE:LIVE_STREAM_SETTINGS")) {
337
+ throw new Error('[LiveAPI] "WRITE:LIVE_STREAM_SETTINGS" \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. OAuth \uC778\uC99D \uC2DC \uD574\uB2F9 \uAD8C\uD55C\uC744 \uD3EC\uD568\uC2DC\uCF1C \uC8FC\uC138\uC694.');
338
+ }
339
+ await this.httpClient.patch("/open/v1/lives/setting", params);
340
+ }
341
+ /**
342
+ * 인증된 사용자 채널의 스트림 키(Stream Key)를 조회합니다.
343
+ * OBS 등 외부 방송 도구 연동 시 사용됩니다.
344
+ *
345
+ * @requires Auth: `Access Token`
346
+ * @requires Scope: `READ:LIVE_STREAM_KEY`
347
+ * @returns 스트림 키
348
+ */
349
+ async getStreamKey() {
350
+ if (!this.options.scopes?.includes("READ:LIVE_STREAM_KEY")) {
351
+ throw new Error('[LiveAPI] "READ:LIVE_STREAM_KEY" \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. OAuth \uC778\uC99D \uC2DC \uD574\uB2F9 \uAD8C\uD55C\uC744 \uD3EC\uD568\uC2DC\uCF1C \uC8FC\uC138\uC694.');
352
+ }
353
+ return this.httpClient.get("/open/v1/streams/key");
354
+ }
355
+ /**
356
+ * 특정 채널의 라이브 방송 여부와 상태를 확인합니다.
357
+ * 이 API는 인증 없이 호출할 수 있는 완전 공개 API입니다.
358
+ *
359
+ * @requires Auth: 불필요
360
+ * @param channelId - 상태를 조회할 채널의 ID
361
+ * @returns 라이브 상태 정보
362
+ */
363
+ async getLiveStatus(channelId) {
364
+ return this.httpClient.get(`/v1/${channelId}/live-status`);
365
+ }
366
+ };
367
+
368
+ // src/api/chat.ts
369
+ var ChatAPI = class {
370
+ constructor(httpClient, options) {
371
+ this.httpClient = httpClient;
372
+ this.options = options;
373
+ }
374
+ httpClient;
375
+ options;
376
+ /**
377
+ * 인증된 사용자 채널의 채팅 설정을 조회합니다.
378
+ *
379
+ * @requires Auth: `Access Token`
380
+ * @requires Scope: `READ:LIVE_CHAT_SETTINGS`
381
+ * @returns 채팅 설정 정보
382
+ */
383
+ async getSettings() {
384
+ if (!this.options.scopes?.includes("READ:LIVE_CHAT_SETTINGS")) {
385
+ throw new Error('[ChatAPI] "READ:LIVE_CHAT_SETTINGS" \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. OAuth \uC778\uC99D \uC2DC \uD574\uB2F9 \uAD8C\uD55C\uC744 \uD3EC\uD568\uC2DC\uCF1C \uC8FC\uC138\uC694.');
386
+ }
387
+ return this.httpClient.get("/open/v1/chats/settings");
388
+ }
389
+ /**
390
+ * 인증된 사용자 채널의 채팅 설정을 변경합니다. (전달된 필드만 업데이트)
391
+ *
392
+ * @requires Auth: `Access Token`
393
+ * @requires Scope: `WRITE:LIVE_CHAT_SETTINGS`
394
+ * @param params - 변경할 채팅 설정 데이터
395
+ */
396
+ async updateSettings(params) {
397
+ if (!this.options.scopes?.includes("WRITE:LIVE_CHAT_SETTINGS")) {
398
+ throw new Error('[ChatAPI] "WRITE:LIVE_CHAT_SETTINGS" \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. OAuth \uC778\uC99D \uC2DC \uD574\uB2F9 \uAD8C\uD55C\uC744 \uD3EC\uD568\uC2DC\uCF1C \uC8FC\uC138\uC694.');
399
+ }
400
+ await this.httpClient.put("/open/v1/chats/settings", params);
401
+ }
402
+ /**
403
+ * 인증된 사용자의 채널 라이브 채팅에 메시지를 전송합니다.
404
+ *
405
+ * @requires Auth: `Access Token`
406
+ * @requires Scope: `WRITE:LIVE_CHAT`
407
+ * @param params - 전송할 메시지 정보 및 발신자 타입
408
+ * @returns 전송된 메시지 ID
409
+ */
410
+ async sendMessage(params) {
411
+ if (!this.options.scopes?.includes("WRITE:LIVE_CHAT")) {
412
+ throw new Error('[ChatAPI] "WRITE:LIVE_CHAT" \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. OAuth \uC778\uC99D \uC2DC \uD574\uB2F9 \uAD8C\uD55C\uC744 \uD3EC\uD568\uC2DC\uCF1C \uC8FC\uC138\uC694.');
413
+ }
414
+ return this.httpClient.post("/open/v1/chats/send", params);
415
+ }
416
+ /**
417
+ * 인증된 사용자의 채널 라이브 채팅에 공지사항을 등록합니다.
418
+ *
419
+ * @requires Auth: `Access Token`
420
+ * @requires Scope: `WRITE:LIVE_CHAT_NOTICE`
421
+ * @param params - 등록할 공지 메시지 내용 또는 기존 메시지 ID
422
+ * @throws {Error} 파라미터가 모두 누락된 경우 서버 요청 전 사전 차단합니다.
423
+ */
424
+ async registerNotice(params) {
425
+ if (!this.options.scopes?.includes("WRITE:LIVE_CHAT_NOTICE")) {
426
+ throw new Error('[ChatAPI] "WRITE:LIVE_CHAT_NOTICE" \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. OAuth \uC778\uC99D \uC2DC \uD574\uB2F9 \uAD8C\uD55C\uC744 \uD3EC\uD568\uC2DC\uCF1C \uC8FC\uC138\uC694.');
427
+ }
428
+ if (!params.message && !params.messageId) {
429
+ throw new Error('[CimeClient.ChatAPI] registerNotice requires either "message" or "messageId".');
430
+ }
431
+ await this.httpClient.post("/open/v1/chats/notice", params);
432
+ }
433
+ };
434
+
435
+ // src/api/categories.ts
436
+ var CategoriesAPI = class {
437
+ constructor(httpClient) {
438
+ this.httpClient = httpClient;
439
+ }
440
+ httpClient;
441
+ /**
442
+ * 라이브 설정 등에 사용할 카테고리를 키워드로 검색합니다.
443
+ *
444
+ * @requires Auth: `Client ID / Secret` (공개 API)
445
+ * @param params - 검색 키워드 및 페이지 크기 파라미터
446
+ * @returns 검색된 카테고리 목록
447
+ */
448
+ async searchCategories(params) {
449
+ return this.httpClient.get("/open/v1/categories/search", {
450
+ params
451
+ });
452
+ }
453
+ };
454
+
455
+ // src/api/restrict.ts
456
+ var RestrictAPI = class {
457
+ constructor(httpClient, options) {
458
+ this.httpClient = httpClient;
459
+ this.options = options;
460
+ }
461
+ httpClient;
462
+ options;
463
+ /**
464
+ * 인증된 사용자의 채널에서 특정 사용자를 추방합니다.
465
+ *
466
+ * @requires Auth: `Access Token`
467
+ * @requires Scope: `WRITE:USER_BLOCK`
468
+ * @param params - 추방할 대상 채널 ID
469
+ */
470
+ async restrictUser(params) {
471
+ if (!this.options.scopes?.includes("WRITE:USER_BLOCK")) {
472
+ throw new Error('[RestrictAPI] "WRITE:USER_BLOCK" \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. OAuth \uC778\uC99D \uC2DC \uD574\uB2F9 \uAD8C\uD55C\uC744 \uD3EC\uD568\uC2DC\uCF1C \uC8FC\uC138\uC694.');
473
+ }
474
+ await this.httpClient.post("/open/v1/restrict-channels", params);
475
+ }
476
+ /**
477
+ * 인증된 사용자의 채널에서 추방된 사용자 목록을 조회합니다.
478
+ *
479
+ * @requires Auth: `Access Token`
480
+ * @requires Scope: `READ:USER_BLOCK`
481
+ * @param params - 커서 기반 페이징 파라미터 (size, next)
482
+ * @returns 추방된 사용자 목록
483
+ */
484
+ async getRestrictedUsers(params) {
485
+ if (!this.options.scopes?.includes("READ:USER_BLOCK")) {
486
+ throw new Error('[RestrictAPI] "READ:USER_BLOCK" \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. OAuth \uC778\uC99D \uC2DC \uD574\uB2F9 \uAD8C\uD55C\uC744 \uD3EC\uD568\uC2DC\uCF1C \uC8FC\uC138\uC694.');
487
+ }
488
+ return this.httpClient.get("/open/v1/restrict-channels", {
489
+ params
490
+ });
491
+ }
492
+ /**
493
+ * 인증된 사용자의 채널에서 특정 사용자의 추방을 해제합니다.
494
+ *
495
+ * @requires Auth: `Access Token`
496
+ * @requires Scope: `WRITE:USER_BLOCK`
497
+ * @param params - 추방 해제할 대상 채널 ID
498
+ */
499
+ async unrestrictUser(params) {
500
+ if (!this.options.scopes?.includes("WRITE:USER_BLOCK")) {
501
+ throw new Error('[RestrictAPI] "WRITE:USER_BLOCK" \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. OAuth \uC778\uC99D \uC2DC \uD574\uB2F9 \uAD8C\uD55C\uC744 \uD3EC\uD568\uC2DC\uCF1C \uC8FC\uC138\uC694.');
502
+ }
503
+ await this.httpClient.delete("/open/v1/restrict-channels", {
504
+ data: params
505
+ });
506
+ }
507
+ };
508
+
509
+ // src/api/sessions.ts
510
+ var SessionsAPI = class {
511
+ constructor(httpClient) {
512
+ this.httpClient = httpClient;
513
+ }
514
+ httpClient;
515
+ /**
516
+ * WebSocket 연결을 위한 새로운 세션을 생성합니다.
517
+ *
518
+ * @param type - 세션 유형 ('USER' 또는 'CLIENT')
519
+ * @returns WebSocket URL을 포함한 세션 정보
520
+ */
521
+ async createSession(type) {
522
+ const endpoint = type === "USER" ? "/open/v1/sessions/auth" : "/open/v1/sessions/auth/client";
523
+ return this.httpClient.get(endpoint);
524
+ }
525
+ /**
526
+ * 특정 세션에 이벤트를 구독합니다.
527
+ *
528
+ * @requires Auth: `Access Token`
529
+ * @param event - 구독할 이벤트 이름 (chat, donation, subscription)
530
+ * @param sessionKey - 세션 키
531
+ */
532
+ async subscribeEvent(event, sessionKey) {
533
+ await this.httpClient.post(`/open/v1/sessions/events/subscribe/${event}`, null, {
534
+ params: { sessionKey }
535
+ });
536
+ }
537
+ /**
538
+ * 특정 세션에서 이벤트 구독을 해제합니다.
539
+ *
540
+ * @requires Auth: `Access Token`
541
+ * @param event - 구독 해제할 이벤트 이름
542
+ * @param sessionKey - 세션 키
543
+ */
544
+ async unsubscribeEvent(event, sessionKey) {
545
+ await this.httpClient.post(`/open/v1/sessions/events/unsubscribe/${event}`, null, {
546
+ params: { sessionKey }
547
+ });
548
+ }
549
+ };
550
+ var CimeEventClient = class extends EventEmitter {
551
+ constructor(sessionsApi, options) {
552
+ super();
553
+ this.sessionsApi = sessionsApi;
554
+ this.options = options;
555
+ }
556
+ sessionsApi;
557
+ options;
558
+ ws = null;
559
+ sessionKey = null;
560
+ pingTimer = null;
561
+ reconnectTimer = null;
562
+ isConnecting = false;
563
+ isIntentionalClose = false;
564
+ activeSubscriptions = /* @__PURE__ */ new Set();
565
+ async connect() {
566
+ if (this.isConnecting || this.ws?.readyState === WebSocket.OPEN) return;
567
+ this.isConnecting = true;
568
+ this.isIntentionalClose = false;
569
+ try {
570
+ const { url } = await this.sessionsApi.createSession(this.options.type);
571
+ const parsedUrl = new URL(url);
572
+ this.sessionKey = parsedUrl.searchParams.get("sessionKey");
573
+ if (!this.sessionKey) {
574
+ throw new Error("[CimeEventClient] Failed to extract sessionKey from URL.");
575
+ }
576
+ this.ws = new WebSocket(url);
577
+ this.ws.on("open", async () => {
578
+ this.isConnecting = false;
579
+ this.startPing();
580
+ this.emit("connected");
581
+ for (const event of this.activeSubscriptions) {
582
+ const apiEventName = event.toLowerCase();
583
+ await this.sessionsApi.subscribeEvent(apiEventName, this.sessionKey).catch((err) => {
584
+ this.emit("error", new Error(`Failed to resubscribe ${event}: ${err.message}`));
585
+ });
586
+ }
587
+ });
588
+ this.ws.on("message", (data) => {
589
+ this.handleMessage(data.toString());
590
+ });
591
+ this.ws.on("close", () => {
592
+ this.cleanup();
593
+ this.emit("disconnected");
594
+ this.handleReconnect();
595
+ });
596
+ this.ws.on("error", (error) => {
597
+ this.emit("error", error);
598
+ });
599
+ } catch (error) {
600
+ this.isConnecting = false;
601
+ this.emit("error", error);
602
+ this.handleReconnect();
603
+ }
604
+ }
605
+ /**
606
+ * 실시간 이벤트를 구독합니다.
607
+ * @param event - 'CHAT' | 'DONATION' | 'SUBSCRIPTION'
608
+ */
609
+ async subscribe(event) {
610
+ if (this.activeSubscriptions.size >= 30) {
611
+ throw new Error("[CimeEventClient] Exceeded maximum 30 subscriptions per session.");
612
+ }
613
+ this.activeSubscriptions.add(event);
614
+ const apiEventName = event.toLowerCase();
615
+ if (this.sessionKey && this.ws?.readyState === WebSocket.OPEN) {
616
+ await this.sessionsApi.subscribeEvent(apiEventName, this.sessionKey);
617
+ }
618
+ }
619
+ /**
620
+ * 실시간 이벤트 구독을 해제합니다.
621
+ * @param event - 'CHAT' | 'DONATION' | 'SUBSCRIPTION'
622
+ */
623
+ async unsubscribe(event) {
624
+ this.activeSubscriptions.delete(event);
625
+ const apiEventName = event.toLowerCase();
626
+ if (this.sessionKey && this.ws?.readyState === WebSocket.OPEN) {
627
+ await this.sessionsApi.unsubscribeEvent(apiEventName, this.sessionKey);
628
+ }
629
+ }
630
+ disconnect() {
631
+ this.isIntentionalClose = true;
632
+ this.activeSubscriptions.clear();
633
+ this.cleanup();
634
+ if (this.ws) {
635
+ this.ws.close();
636
+ this.ws = null;
637
+ }
638
+ }
639
+ handleMessage(message) {
640
+ try {
641
+ const payload = JSON.parse(message);
642
+ if (payload.action === "PONG") return;
643
+ if (payload.event && payload.data) {
644
+ const typedPayload = payload;
645
+ this.emit(typedPayload.event, typedPayload.data);
646
+ }
647
+ } catch (error) {
648
+ this.emit("error", new Error(`[CimeEventClient] Failed to parse message: ${message}`));
649
+ }
650
+ }
651
+ startPing() {
652
+ const interval = this.options.pingInterval || 6e4;
653
+ this.pingTimer = setInterval(() => {
654
+ if (this.ws?.readyState === WebSocket.OPEN) {
655
+ this.ws.send(JSON.stringify({ type: "PING" }));
656
+ }
657
+ }, interval);
658
+ }
659
+ cleanup() {
660
+ if (this.pingTimer) {
661
+ clearInterval(this.pingTimer);
662
+ this.pingTimer = null;
663
+ }
664
+ if (this.reconnectTimer) {
665
+ clearTimeout(this.reconnectTimer);
666
+ this.reconnectTimer = null;
667
+ }
668
+ this.isConnecting = false;
669
+ }
670
+ async handleReconnect() {
671
+ if (this.isIntentionalClose) return;
672
+ if (this.options.onTokenRefresh) {
673
+ try {
674
+ await this.options.onTokenRefresh();
675
+ } catch (err) {
676
+ this.emit("error", new Error("[CimeEventClient] Token refresh failed during reconnect."));
677
+ }
678
+ }
679
+ this.reconnectTimer = setTimeout(() => {
680
+ this.emit("reconnecting");
681
+ this.connect();
682
+ }, 5e3);
683
+ }
684
+ };
685
+
686
+ // src/client.ts
687
+ var CimeClient = class {
688
+ httpClient;
689
+ options;
690
+ auth;
691
+ users;
692
+ channels;
693
+ live;
694
+ chat;
695
+ categories;
696
+ restrict;
697
+ sessions;
698
+ constructor(options) {
699
+ if (!options.accessToken && (!options.clientId || !options.clientSecret)) {
700
+ throw new Error("[CimeClient] Authentication credentials (accessToken OR clientId/Secret) are required.");
701
+ }
702
+ this.options = { ...options };
703
+ this.httpClient = createHttpClient(this.options);
704
+ this.auth = new AuthAPI(this.httpClient, { clientId: this.options.clientId, clientSecret: this.options.clientSecret });
705
+ this.users = new UsersAPI(this.httpClient, this.options);
706
+ this.channels = new ChannelsAPI(this.httpClient, this.options);
707
+ this.live = new LiveAPI(this.httpClient, this.options);
708
+ this.chat = new ChatAPI(this.httpClient, this.options);
709
+ this.categories = new CategoriesAPI(this.httpClient);
710
+ this.restrict = new RestrictAPI(this.httpClient, this.options);
711
+ this.sessions = new SessionsAPI(this.httpClient);
712
+ }
713
+ setAccessToken(token) {
714
+ this.options.accessToken = token;
715
+ }
716
+ /**
717
+ * 실시간 이벤트를 수신하기 위한 WebSocket 클라이언트를 생성합니다.
718
+ */
719
+ createEventClient(options) {
720
+ if (options.refreshToken && !options.onTokenRefresh) {
721
+ options.onTokenRefresh = async () => {
722
+ try {
723
+ const tokenInfo = await this.auth.refresh(options.refreshToken);
724
+ this.setAccessToken(tokenInfo.accessToken);
725
+ options.refreshToken = tokenInfo.refreshToken;
726
+ } catch (error) {
727
+ throw new Error("\uC790\uB3D9 \uD1A0\uD070 \uAC31\uC2E0\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
728
+ }
729
+ };
730
+ }
731
+ return new CimeEventClient(this.sessions, options);
732
+ }
733
+ /** * OAuth 인증 플로우를 통해 Access Token을 발급받고 클라이언트에 설정합니다.
734
+ * @param code Redirect URI로 전달받은 인가 코드
735
+ * @throws 인증 정보(clientId/Secret)가 없으면 에러를 반환합니다.
736
+ */
737
+ async authorize(code) {
738
+ if (!this.options.clientId || !this.options.clientSecret) {
739
+ throw new Error("[CimeClient] OAuth authentication requires clientId and clientSecret to be set in options.");
740
+ }
741
+ const tokenResponse = await this.auth.get(code);
742
+ this.setAccessToken(tokenResponse.accessToken);
743
+ return tokenResponse;
744
+ }
745
+ };
746
+
747
+ // src/utils/chatUtils.ts
748
+ function renderChatEmojisToHTML(data, className = "cime-emoji") {
749
+ if (!data.emojis || Object.keys(data.emojis).length === 0) {
750
+ return data.content;
751
+ }
752
+ let htmlMessage = data.content;
753
+ for (const [token, imageUrl] of Object.entries(data.emojis)) {
754
+ const escapedToken = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
755
+ const regex = new RegExp(escapedToken, "g");
756
+ htmlMessage = htmlMessage.replace(regex, () => {
757
+ return `<img src="${imageUrl}" alt="${token}" class="${className}" />`;
758
+ });
759
+ }
760
+ return htmlMessage;
761
+ }
762
+
763
+ // src/index.ts
764
+ checkVersion();
765
+
766
+ export { CimeAPIError, CimeClient, CimeEventClient, getVersion, renderChatEmojisToHTML };
767
+ //# sourceMappingURL=index.mjs.map
768
+ //# sourceMappingURL=index.mjs.map