@kakao-bot/sdk 0.1.0 → 0.2.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/README.md CHANGED
@@ -29,11 +29,38 @@ Node.js 20 이상, ESM을 지원합니다. 서비스 키는 브라우저 번들
29
29
 
30
30
  `queued: true`는 접수이며 실제 전달 성공이 아닙니다. 서버는 서비스별 `requestId`로 중복을 방지합니다. 같은 논리적 발송에는 같은 ID를 사용하세요. **SDK는 POST 또는 GET을 자동 재시도하지 않습니다.** 타임아웃은 이미 접수된 요청일 수 있으므로 새 ID로 재시도하면 중복 발송됩니다. `KakaoTransportError.requestId`로 요청을 식별하고 대시보드 로그를 확인하세요.
31
31
 
32
- - `KakaoApiError`: HTTP 상태·오류 코드·요청 ID. `402 plan_message_limit`은 플랜 사용량 제한, `429 rate_limited`는 단기 발송 제한입니다.
33
- - `KakaoTransportError`: 전송/타임아웃 실패. 발송 결과는 알 수 없습니다.
34
- - `KakaoProtocolError`: 예상하지 못한 서버 응답.
32
+ ## 오류 처리
33
+
34
+ 모든 오류는 `KakaoError`를 상속합니다. 한 번의 `catch`로 전부 받고, `name`이나 `instanceof`로 갈라 봅니다.
35
+
36
+ ```ts
37
+ import { isKakaoError, KakaoApiError, KakaoTimeoutError } from '@kakao-bot/sdk';
38
+
39
+ try {
40
+ await kakao.sendMessage({ roomSlug: 'deploy-alerts', text: '배포 완료', requestId: 'deploy:42' });
41
+ } catch (error) {
42
+ if (!isKakaoError(error)) throw error; // 내 코드의 버그는 그대로 올려보냅니다
43
+ logger.error(error.toJSON()); // 로그 한 줄로 바로 적을 수 있는 평평한 객체
44
+ if (error.retryable) await scheduleRetry('deploy:42', error); // 같은 requestId 로만
45
+ }
46
+ ```
47
+
48
+ | 클래스 | 뜻 | `retryable` |
49
+ |---|---|---|
50
+ | `KakaoApiError` | 허브가 응답했지만 2xx가 아님 | 429·5xx는 `true`, 그 외 4xx는 `false` |
51
+ | `KakaoTransportError` | 허브에 닿지 못함 | `true` |
52
+ | `KakaoTimeoutError` | 제한 시간 초과 (`KakaoTransportError`의 하위) | `true` |
53
+ | `KakaoProtocolError` | 2xx인데 본문이 계약과 다름 | `false` |
54
+
55
+ - **`retryable`은 "다시 보내도 안전한가"이지 "자동으로 재시도하라"가 아닙니다.** SDK는 어떤 경우에도 스스로 재시도하지 않습니다. 재시도할 때는 **반드시 같은 `requestId`** 를 쓰세요 — 새 ID로 바꾸면 중복 발송됩니다. `409 idempotency_conflict`만은 `retryable: false`인데, 이미 접수된 건이라 다시 보내면 방에 두 번 올라가기 때문입니다.
56
+ - `KakaoApiError`는 `status`·`code`·`body`·`retryAfterMs`를 싣습니다. `body`에 허브의 원본 응답이 그대로 들어 있어 `402`의 `upgrade` 링크나 `429`의 `scope`(room/global/test)를 읽을 수 있습니다.
57
+ - `code`는 알려진 코드가 자동완성되지만 임의 문자열도 허용합니다 — 허브가 코드를 추가해도 타입이 깨지지 않습니다. 목록은 `KAKAO_ERROR_CODES`.
58
+ - `retryAfterMs`는 본문 `retry_after_ms`를 우선 쓰고, 없으면 `Retry-After` 헤더(초 또는 HTTP-date)를 읽습니다.
59
+ - 전송 실패는 원인을 표준 `Error.cause`로 물고 옵니다.
60
+ - **호출자가 `signal`로 취소하면 `signal.reason`이 그대로 올라옵니다** — `fetch`와 같은 동작이라 기존 `AbortError` 처리가 그대로 먹습니다. SDK 오류로 감싸지 않습니다.
61
+ - `isKakaoError()`는 SDK가 중복 설치돼 `instanceof`가 조용히 실패하는 환경에서도 동작합니다.
35
62
  - 각 메서드의 두 번째 인자에 `{ signal }`을 넣어 취소할 수 있습니다.
36
- - `fetch`와 `timeoutMs`를 생성자에서 주입할 수 있습니다.
63
+ - `fetch`와 `timeoutMs`(기본 10초)를 생성자에서 주입할 수 있습니다.
37
64
 
38
65
  ## 릴리스
39
66
 
@@ -0,0 +1,118 @@
1
+ /**
2
+ * 허브 호출이 실패했을 때 던지는 것들.
3
+ *
4
+ * 설계는 널리 쓰이는 SDK 들의 합의를 그대로 따른다 — 공통 베이스 클래스 하나(Stripe 의
5
+ * `StripeError`, AWS SDK v3 의 `ServiceException`, Octokit 의 `RequestError` 와 같은 자리),
6
+ * 원본 응답 보존(Stripe `.raw`), 재시도 가능 여부 표시(AWS `$retryable`),
7
+ * 그리고 ES2022 표준 `Error.cause` 로 원인 체인.
8
+ *
9
+ * 핵심은 **소비자가 같은 표를 각자 베껴 쓰지 않게 하는 것**이다. "409 는 다시 보내면 안 되고
10
+ * 429 는 보내도 된다" 는 허브 쪽 지식이라, 서비스마다 손으로 옮겨 적으면 언젠가 한 곳이 틀리고
11
+ * 그 대가는 단톡방에 두 번 올라간 공지다.
12
+ */
13
+ /**
14
+ * 중복 설치된 SDK 사이에서도 에러를 식별하기 위한 브랜드.
15
+ *
16
+ * `instanceof` 는 같은 클래스의 사본이 둘 이상 로드되면(pnpm 중첩 설치, 번들러가 ESM/CJS 를
17
+ * 각각 끌어오는 경우) 조용히 false 가 된다. `Symbol.for` 는 realm 전역 레지스트리라 사본이
18
+ * 몇 개든 같은 심볼을 가리킨다. 내부용이지만 d.ts 에 이름이 필요해 export 한다.
19
+ */
20
+ export declare const KAKAO_ERROR_BRAND: unique symbol;
21
+ /** 허브가 돌려주는 오류 코드. 서버 라우트(`src/outbound/routes.ts`)가 정본이다. */
22
+ export declare const KAKAO_ERROR_CODES: readonly ["validation_error", "invalid_room_slug", "unauthorized", "plan_message_limit", "insufficient_scope", "room_not_bound", "room_inactive", "idempotency_conflict", "rate_limited", "kill_switch", "queue_full", "request_failed"];
23
+ export type KakaoErrorCode = (typeof KAKAO_ERROR_CODES)[number];
24
+ /**
25
+ * 알려진 코드는 자동완성으로 뜨되, 허브가 새 코드를 추가해도 타입이 깨지지 않는다.
26
+ * `(string & {})` 는 유니온이 `string` 으로 뭉개지는 것을 막는 관용구다.
27
+ */
28
+ export type KakaoErrorCodeLike = KakaoErrorCode | (string & {});
29
+ /** 로그에 그대로 적기 좋은 평평한 모양. `toJSON()` 의 결과. */
30
+ export interface KakaoErrorJSON {
31
+ name: string;
32
+ message: string;
33
+ retryable: boolean;
34
+ requestId?: string;
35
+ status?: number;
36
+ code?: string;
37
+ retryAfterMs?: number;
38
+ timeoutMs?: number;
39
+ }
40
+ /** 모든 허브 오류의 베이스. `catch` 에서 `isKakaoError(e)` 한 번으로 전부 잡는다. */
41
+ export declare abstract class KakaoError extends Error {
42
+ readonly [KAKAO_ERROR_BRAND]: true;
43
+ /** 서브클래스를 구분하는 판별자. `instanceof` 가 막히는 환경에서도 읽을 수 있다. */
44
+ abstract readonly name: string;
45
+ /**
46
+ * 같은 요청을 다시 보내도 **안전한가**. 자동 재시도 지시가 아니다 —
47
+ * SDK 는 어떤 경우에도 스스로 재시도하지 않는다.
48
+ */
49
+ abstract readonly retryable: boolean;
50
+ readonly requestId?: string;
51
+ constructor(message: string, requestId?: string, options?: {
52
+ cause?: unknown;
53
+ });
54
+ toJSON(): KakaoErrorJSON;
55
+ }
56
+ /**
57
+ * 허브가 응답은 했지만 2xx 가 아니었다.
58
+ *
59
+ * 본문을 통째로 `body` 에 보관한다 — 402 는 `upgrade` 링크를, 429 는 `scope`(test/global/room)를
60
+ * 함께 싣는데, `error` 필드만 빼고 버리면 그 안내가 사라진다.
61
+ */
62
+ export declare class KakaoApiError extends KakaoError {
63
+ readonly name = "KakaoApiError";
64
+ readonly status: number;
65
+ readonly code: KakaoErrorCodeLike;
66
+ readonly retryable: boolean;
67
+ /** 파싱된 응답 본문. JSON 이 아니었으면 `null`. */
68
+ readonly body: unknown;
69
+ /** `Retry-After` 또는 본문 `retry_after_ms` 에서 읽은 대기 시간. 없으면 `undefined`. */
70
+ readonly retryAfterMs?: number;
71
+ constructor(status: number, code: KakaoErrorCodeLike, requestId?: string, options?: {
72
+ body?: unknown;
73
+ retryAfterMs?: number;
74
+ cause?: unknown;
75
+ });
76
+ toJSON(): KakaoErrorJSON;
77
+ }
78
+ /**
79
+ * 응답을 받지 못했다. 허브에 닿기 전에 끊겼다는 뜻이고, 발송 여부는 알 수 없다.
80
+ *
81
+ * 같은 `requestId` 로 다시 보내면 허브가 중복을 막아 주므로 재시도가 안전하다 —
82
+ * **새 id 로 바꿔 보내면 중복 발송된다.**
83
+ */
84
+ export declare class KakaoTransportError extends KakaoError {
85
+ readonly name: "KakaoTransportError" | "KakaoTimeoutError";
86
+ readonly retryable = true;
87
+ constructor(requestId?: string, options?: {
88
+ cause?: unknown;
89
+ message?: string;
90
+ });
91
+ }
92
+ /**
93
+ * 제한 시간 안에 응답이 오지 않았다. `KakaoTransportError` 의 하위라
94
+ * 기존 `catch (e) { if (e instanceof KakaoTransportError) … }` 가 그대로 잡는다.
95
+ */
96
+ export declare class KakaoTimeoutError extends KakaoTransportError {
97
+ readonly name = "KakaoTimeoutError";
98
+ readonly timeoutMs: number;
99
+ constructor(timeoutMs: number, requestId?: string, options?: {
100
+ cause?: unknown;
101
+ });
102
+ toJSON(): KakaoErrorJSON;
103
+ }
104
+ /**
105
+ * 2xx 를 받았지만 본문이 계약과 달랐다. 허브는 받았을 가능성이 크므로 재시도하지 않는다 —
106
+ * 사람이 대시보드 로그를 보고 판단해야 한다.
107
+ */
108
+ export declare class KakaoProtocolError extends KakaoError {
109
+ readonly name = "KakaoProtocolError";
110
+ readonly retryable = false;
111
+ constructor(requestId?: string, options?: {
112
+ cause?: unknown;
113
+ });
114
+ }
115
+ /**
116
+ * 허브 오류인지 판별한다. 중복 설치로 `instanceof` 가 막힌 환경에서도 동작한다.
117
+ */
118
+ export declare function isKakaoError(value: unknown): value is KakaoError;
package/dist/errors.js ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * 허브 호출이 실패했을 때 던지는 것들.
3
+ *
4
+ * 설계는 널리 쓰이는 SDK 들의 합의를 그대로 따른다 — 공통 베이스 클래스 하나(Stripe 의
5
+ * `StripeError`, AWS SDK v3 의 `ServiceException`, Octokit 의 `RequestError` 와 같은 자리),
6
+ * 원본 응답 보존(Stripe `.raw`), 재시도 가능 여부 표시(AWS `$retryable`),
7
+ * 그리고 ES2022 표준 `Error.cause` 로 원인 체인.
8
+ *
9
+ * 핵심은 **소비자가 같은 표를 각자 베껴 쓰지 않게 하는 것**이다. "409 는 다시 보내면 안 되고
10
+ * 429 는 보내도 된다" 는 허브 쪽 지식이라, 서비스마다 손으로 옮겨 적으면 언젠가 한 곳이 틀리고
11
+ * 그 대가는 단톡방에 두 번 올라간 공지다.
12
+ */
13
+ /**
14
+ * 중복 설치된 SDK 사이에서도 에러를 식별하기 위한 브랜드.
15
+ *
16
+ * `instanceof` 는 같은 클래스의 사본이 둘 이상 로드되면(pnpm 중첩 설치, 번들러가 ESM/CJS 를
17
+ * 각각 끌어오는 경우) 조용히 false 가 된다. `Symbol.for` 는 realm 전역 레지스트리라 사본이
18
+ * 몇 개든 같은 심볼을 가리킨다. 내부용이지만 d.ts 에 이름이 필요해 export 한다.
19
+ */
20
+ export const KAKAO_ERROR_BRAND = Symbol.for("@kakao-bot/sdk.error");
21
+ /** 허브가 돌려주는 오류 코드. 서버 라우트(`src/outbound/routes.ts`)가 정본이다. */
22
+ export const KAKAO_ERROR_CODES = [
23
+ "validation_error",
24
+ "invalid_room_slug",
25
+ "unauthorized",
26
+ "plan_message_limit",
27
+ "insufficient_scope",
28
+ "room_not_bound",
29
+ "room_inactive",
30
+ "idempotency_conflict",
31
+ "rate_limited",
32
+ "kill_switch",
33
+ "queue_full",
34
+ "request_failed",
35
+ ];
36
+ /** 모든 허브 오류의 베이스. `catch` 에서 `isKakaoError(e)` 한 번으로 전부 잡는다. */
37
+ export class KakaoError extends Error {
38
+ [KAKAO_ERROR_BRAND] = true;
39
+ requestId;
40
+ constructor(message, requestId, options) {
41
+ super(message, options);
42
+ this.requestId = requestId;
43
+ }
44
+ toJSON() {
45
+ return {
46
+ name: this.name,
47
+ message: this.message,
48
+ retryable: this.retryable,
49
+ ...(this.requestId === undefined ? {} : { requestId: this.requestId }),
50
+ };
51
+ }
52
+ }
53
+ /**
54
+ * 허브가 응답은 했지만 2xx 가 아니었다.
55
+ *
56
+ * 본문을 통째로 `body` 에 보관한다 — 402 는 `upgrade` 링크를, 429 는 `scope`(test/global/room)를
57
+ * 함께 싣는데, `error` 필드만 빼고 버리면 그 안내가 사라진다.
58
+ */
59
+ export class KakaoApiError extends KakaoError {
60
+ name = "KakaoApiError";
61
+ status;
62
+ code;
63
+ retryable;
64
+ /** 파싱된 응답 본문. JSON 이 아니었으면 `null`. */
65
+ body;
66
+ /** `Retry-After` 또는 본문 `retry_after_ms` 에서 읽은 대기 시간. 없으면 `undefined`. */
67
+ retryAfterMs;
68
+ constructor(status, code, requestId, options) {
69
+ super(`Kakao Bot API returned ${status} (${code})`, requestId, options);
70
+ this.status = status;
71
+ this.code = code;
72
+ this.body = options?.body ?? null;
73
+ this.retryAfterMs = options?.retryAfterMs;
74
+ this.retryable = isRetryableStatus(status, code);
75
+ }
76
+ toJSON() {
77
+ return {
78
+ ...super.toJSON(),
79
+ status: this.status,
80
+ code: this.code,
81
+ ...(this.retryAfterMs === undefined ? {} : { retryAfterMs: this.retryAfterMs }),
82
+ };
83
+ }
84
+ }
85
+ /**
86
+ * 응답을 받지 못했다. 허브에 닿기 전에 끊겼다는 뜻이고, 발송 여부는 알 수 없다.
87
+ *
88
+ * 같은 `requestId` 로 다시 보내면 허브가 중복을 막아 주므로 재시도가 안전하다 —
89
+ * **새 id 로 바꿔 보내면 중복 발송된다.**
90
+ */
91
+ export class KakaoTransportError extends KakaoError {
92
+ name = "KakaoTransportError";
93
+ retryable = true;
94
+ constructor(requestId, options) {
95
+ super(options?.message ??
96
+ "Kakao Bot request did not complete. Delivery state is unknown; retry with the same request ID.", requestId, options);
97
+ }
98
+ }
99
+ /**
100
+ * 제한 시간 안에 응답이 오지 않았다. `KakaoTransportError` 의 하위라
101
+ * 기존 `catch (e) { if (e instanceof KakaoTransportError) … }` 가 그대로 잡는다.
102
+ */
103
+ export class KakaoTimeoutError extends KakaoTransportError {
104
+ name = "KakaoTimeoutError";
105
+ timeoutMs;
106
+ constructor(timeoutMs, requestId, options) {
107
+ super(requestId, {
108
+ ...options,
109
+ message: `Kakao Bot request timed out after ${timeoutMs}ms. Delivery state is unknown; retry with the same request ID.`,
110
+ });
111
+ this.timeoutMs = timeoutMs;
112
+ }
113
+ toJSON() {
114
+ return { ...super.toJSON(), timeoutMs: this.timeoutMs };
115
+ }
116
+ }
117
+ /**
118
+ * 2xx 를 받았지만 본문이 계약과 달랐다. 허브는 받았을 가능성이 크므로 재시도하지 않는다 —
119
+ * 사람이 대시보드 로그를 보고 판단해야 한다.
120
+ */
121
+ export class KakaoProtocolError extends KakaoError {
122
+ name = "KakaoProtocolError";
123
+ retryable = false;
124
+ constructor(requestId, options) {
125
+ super("Kakao Bot returned an unexpected response.", requestId, options);
126
+ }
127
+ }
128
+ /**
129
+ * 응답을 받은 실패가 재시도해도 안전한지.
130
+ *
131
+ * - `idempotency_conflict` — 같은 id 가 다른 내용으로 이미 접수됐다. 다시 보내면 중복이다.
132
+ * - 429 / 5xx — 일시적이다(한도, `kill_switch`, `queue_full` 모두 풀린다).
133
+ * - 그 밖의 4xx — 권한·플랜·본문 문제라 같은 요청을 다시 보내도 같은 답이 온다.
134
+ */
135
+ function isRetryableStatus(status, code) {
136
+ if (code === "idempotency_conflict")
137
+ return false;
138
+ return status === 429 || status >= 500;
139
+ }
140
+ /**
141
+ * 허브 오류인지 판별한다. 중복 설치로 `instanceof` 가 막힌 환경에서도 동작한다.
142
+ */
143
+ export function isKakaoError(value) {
144
+ return (!!value &&
145
+ typeof value === "object" &&
146
+ value[KAKAO_ERROR_BRAND] === true);
147
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export type { KakaoErrorCode, KakaoErrorCodeLike, KakaoErrorJSON } from "./errors.js";
2
+ export { isKakaoError, KAKAO_ERROR_BRAND, KAKAO_ERROR_CODES, KakaoApiError, KakaoError, KakaoProtocolError, KakaoTimeoutError, KakaoTransportError, } from "./errors.js";
1
3
  export interface KakaoBotOptions {
2
4
  /** Hub origin, e.g. https://hub.example.com. Never use the phone's private endpoint. */
3
5
  baseUrl: string;
@@ -42,24 +44,6 @@ export interface Room {
42
44
  export interface RequestOptions {
43
45
  signal?: AbortSignal;
44
46
  }
45
- export declare class KakaoApiError extends Error {
46
- readonly status: number;
47
- readonly code: string;
48
- readonly requestId?: string | undefined;
49
- readonly name = "KakaoApiError";
50
- constructor(status: number, code: string, requestId?: string | undefined);
51
- }
52
- export declare class KakaoTransportError extends Error {
53
- readonly requestId?: string | undefined;
54
- readonly name = "KakaoTransportError";
55
- /** A timed-out send may already have been queued. Never retry with a new request ID. */
56
- constructor(requestId?: string | undefined);
57
- }
58
- export declare class KakaoProtocolError extends Error {
59
- readonly requestId?: string | undefined;
60
- readonly name = "KakaoProtocolError";
61
- constructor(requestId?: string | undefined);
62
- }
63
47
  export declare class KakaoBot {
64
48
  private readonly baseUrl;
65
49
  private readonly apiKey;
package/dist/index.js CHANGED
@@ -1,32 +1,5 @@
1
- export class KakaoApiError extends Error {
2
- status;
3
- code;
4
- requestId;
5
- name = "KakaoApiError";
6
- constructor(status, code, requestId) {
7
- super(`Kakao Bot API returned ${status} (${code})`);
8
- this.status = status;
9
- this.code = code;
10
- this.requestId = requestId;
11
- }
12
- }
13
- export class KakaoTransportError extends Error {
14
- requestId;
15
- name = "KakaoTransportError";
16
- /** A timed-out send may already have been queued. Never retry with a new request ID. */
17
- constructor(requestId) {
18
- super("Kakao Bot request did not complete. Delivery state is unknown; inspect the dashboard before retrying.");
19
- this.requestId = requestId;
20
- }
21
- }
22
- export class KakaoProtocolError extends Error {
23
- requestId;
24
- name = "KakaoProtocolError";
25
- constructor(requestId) {
26
- super("Kakao Bot returned an unexpected response.");
27
- this.requestId = requestId;
28
- }
29
- }
1
+ export { isKakaoError, KAKAO_ERROR_BRAND, KAKAO_ERROR_CODES, KakaoApiError, KakaoError, KakaoProtocolError, KakaoTimeoutError, KakaoTransportError, } from "./errors.js";
2
+ import { KakaoApiError, KakaoProtocolError, KakaoTimeoutError, KakaoTransportError, } from "./errors.js";
30
3
  function object(value) {
31
4
  return !!value && typeof value === "object" && !Array.isArray(value);
32
5
  }
@@ -114,15 +87,43 @@ export class KakaoBot {
114
87
  });
115
88
  data = await response.json().catch(() => null);
116
89
  }
117
- catch {
118
- throw new KakaoTransportError(requestId);
90
+ catch (cause) {
91
+ // The caller's own cancellation is not our failure. Rethrow their reason unchanged,
92
+ // the way fetch does, so `AbortError` handling upstream keeps working.
93
+ if (signal?.aborted)
94
+ throw signal.reason;
95
+ if (timeout.aborted)
96
+ throw new KakaoTimeoutError(this.timeoutMs, requestId, { cause });
97
+ throw new KakaoTransportError(requestId, { cause });
119
98
  }
120
99
  if (!response.ok) {
121
100
  const code = object(data) && typeof data.error === "string" && /^[a-z0-9_]{1,80}$/.test(data.error)
122
101
  ? data.error
123
102
  : "request_failed";
124
- throw new KakaoApiError(response.status, code, requestId);
103
+ throw new KakaoApiError(response.status, code, requestId, {
104
+ body: data,
105
+ retryAfterMs: retryAfterMs(response, data),
106
+ });
125
107
  }
126
108
  return data;
127
109
  }
128
110
  }
111
+ /**
112
+ * How long to wait before retrying, in milliseconds.
113
+ *
114
+ * The hub sends an exact `retry_after_ms` in the body; `Retry-After` (RFC 9110) is the
115
+ * interoperable fallback and may also arrive from a proxy in front of the hub, in either
116
+ * the delay-seconds or the HTTP-date form.
117
+ */
118
+ function retryAfterMs(response, body) {
119
+ if (object(body) && typeof body.retry_after_ms === "number" && body.retry_after_ms >= 0)
120
+ return Math.round(body.retry_after_ms);
121
+ const header = response.headers.get("retry-after");
122
+ if (!header)
123
+ return undefined;
124
+ const seconds = Number(header);
125
+ if (Number.isFinite(seconds) && seconds >= 0)
126
+ return Math.round(seconds * 1000);
127
+ const at = Date.parse(header);
128
+ return Number.isNaN(at) ? undefined : Math.max(0, at - Date.now());
129
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kakao-bot/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Typed server-side client for the Kakao Bot Hub API",
5
5
  "license": "MIT",
6
6
  "author": "Jeongmin Lee",