tokolaku-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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tokolaku
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # tokolaku-sdk
2
+
3
+ Official TypeScript/JavaScript SDK for the Tokolaku Engine API — AI bot replies, omnichannel messaging (WhatsApp/Instagram/Messenger), and webhook verification.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install tokolaku-sdk
9
+ # or
10
+ yarn add tokolaku-sdk
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ *Bahasa Indonesia ringkas: buat instance `Tokolaku` dengan API key, lalu panggil `botReply` untuk balasan AI atau `messages.send` untuk kirim pesan lewat channel resmi (WhatsApp/Instagram/Messenger) yang sudah terhubung.*
16
+
17
+ ```ts
18
+ import { Tokolaku } from "tokolaku-sdk";
19
+ // CommonJS: const { Tokolaku } = require("tokolaku-sdk");
20
+
21
+ const tokolaku = new Tokolaku(process.env.TOKOLAKU_API_KEY!);
22
+ // or with options: new Tokolaku({ apiKey, baseUrl, timeoutMs, maxRetries })
23
+
24
+ // 1. AI bot reply for a single customer message
25
+ const { reply, parts } = await tokolaku.botReply({
26
+ message: "Halo, apakah produk ini ready stock?",
27
+ session_id: "wa:628123456789", // keeps multi-turn context
28
+ });
29
+ console.log(reply);
30
+
31
+ // 2. Send a text message through a connected channel
32
+ const sent = await tokolaku.messages.send({
33
+ to: "628123456789",
34
+ text: "Terima kasih sudah menghubungi kami!",
35
+ channel_id: "ch_abc123",
36
+ });
37
+ console.log(sent.id, sent.status);
38
+ ```
39
+
40
+ `messages.send` also accepts a business-initiated template message — pass `template` instead of `text` (exactly one of the two, never both):
41
+
42
+ ```ts
43
+ await tokolaku.messages.send({
44
+ to: "628123456789",
45
+ template: { name: "order_update", language: "id", category: "utility" },
46
+ channel_id: "ch_abc123",
47
+ });
48
+ ```
49
+
50
+ ## Error handling
51
+
52
+ Every failed request rejects with an instance of `TokolakuAPIError` (or one of its subclasses). `status` is `null` when the request never got an HTTP response (network error, timeout); `code` is the backend's machine-readable error code when available.
53
+
54
+ | Class | HTTP status | When it's thrown |
55
+ |---|---|---|
56
+ | `TokolakuValidationError` | 400, 422 | Invalid request params — also thrown client-side before any network call (e.g. `messages.send` with both `text` and `template`, or neither) |
57
+ | `TokolakuAuthenticationError` | 401 | Missing or invalid API key |
58
+ | `TokolakuInsufficientBalanceError` | 402 | Tenant balance too low to cover the charge |
59
+ | `TokolakuPermissionError` | 403 | API key lacks permission for this action |
60
+ | `TokolakuRateLimitError` | 429 | Rate limit exceeded |
61
+ | `TokolakuAPIError` | any other status, or `null` | Base class — also covers network errors, timeouts, and malformed responses not mapped above |
62
+ | `TokolakuWebhookSignatureError` | — | Webhook signature missing or invalid (does **not** extend `TokolakuAPIError`) |
63
+
64
+ ```ts
65
+ import { Tokolaku, TokolakuAPIError, TokolakuInsufficientBalanceError, TokolakuRateLimitError } from "tokolaku-sdk";
66
+
67
+ try {
68
+ await tokolaku.botReply({ message: "Halo" });
69
+ } catch (err) {
70
+ if (err instanceof TokolakuInsufficientBalanceError) {
71
+ // top up balance, notify the tenant
72
+ } else if (err instanceof TokolakuRateLimitError) {
73
+ // back off and retry later
74
+ } else if (err instanceof TokolakuAPIError) {
75
+ console.error(err.status, err.code, err.message);
76
+ } else {
77
+ throw err; // not an SDK error
78
+ }
79
+ }
80
+ ```
81
+
82
+ ## Retry policy
83
+
84
+ The SDK retries automatically (`maxRetries`, default `2`) using exponential backoff with full jitter (base 250ms, capped at 1s; a `Retry-After` response header wins when present). The policy is **money-aware**: it only retries when a retry cannot cause a duplicate side effect.
85
+
86
+ | Condition | `botReply` | `messages.send` |
87
+ |---|---|---|
88
+ | `429 Too Many Requests` | Retried | Retried |
89
+ | Network error (`fetch` throws) | Retried | Retried |
90
+ | `5xx` server error | Retried | **Not** retried |
91
+ | Timeout (`code: "timeout"`) | **Not** retried | **Not** retried |
92
+ | `2xx` with malformed JSON body (`code: "invalid_response"`) | **Not** retried | **Not** retried |
93
+
94
+ - `botReply` has no side effect if it fails, so it retries on `429`, any `5xx`, and network errors.
95
+ - **`messages.send` TIDAK di-retry pada timeout/5xx karena pesan mungkin sudah terkirim** — the message may already have been sent and charged even though the client never saw a successful response, and the API does not yet expose an idempotency key. It only retries on `429` and network errors (no HTTP response was ever received, so nothing could have been sent).
96
+ - A timeout (`code: "timeout"`) is never retried on either endpoint, since it's ambiguous whether the server received/processed the request.
97
+ - A `2xx` response with a body that fails to parse as JSON (`code: "invalid_response"`, `status: 200`) is never retried on either endpoint — the request already reached the server and had its side effect (reply generated / message sent and charged); retrying would risk a double-send or burning AI quota for nothing.
98
+
99
+ ## Webhooks
100
+
101
+ Verify the `x-tokolaku-signature` header (`sha256=<hex>`, HMAC-SHA256 of the **raw** request body) before trusting a webhook payload. Always use the raw, unmodified request body — a re-serialized JSON string will not match the signature.
102
+
103
+ ```ts
104
+ import { verifyWebhookSignature, constructEvent, TokolakuWebhookSignatureError } from "tokolaku-sdk/webhooks";
105
+ ```
106
+
107
+ ### Express
108
+
109
+ ```ts
110
+ import express from "express";
111
+ import { constructEvent, TokolakuWebhookSignatureError } from "tokolaku-sdk/webhooks";
112
+
113
+ const app = express();
114
+
115
+ app.post(
116
+ "/webhooks/tokolaku",
117
+ express.raw({ type: "application/json" }), // keep the raw Buffer — do NOT use express.json() on this route
118
+ (req, res) => {
119
+ const rawBody = req.body.toString("utf8");
120
+ try {
121
+ const { event } = constructEvent(rawBody, req.header("x-tokolaku-signature"), process.env.TOKOLAKU_WEBHOOK_SECRET!);
122
+ // ... handle event
123
+ res.json({ received: true });
124
+ } catch (err) {
125
+ if (err instanceof TokolakuWebhookSignatureError) {
126
+ return res.status(401).json({ error: "invalid signature" });
127
+ }
128
+ throw err;
129
+ }
130
+ },
131
+ );
132
+ ```
133
+
134
+ ### Fastify
135
+
136
+ ```ts
137
+ import Fastify from "fastify";
138
+ import { constructEvent, TokolakuWebhookSignatureError } from "tokolaku-sdk/webhooks";
139
+
140
+ const app = Fastify();
141
+
142
+ // Capture the raw body before Fastify's default JSON parser touches it.
143
+ app.addContentTypeParser("application/json", { parseAs: "string" }, (req, body, done) => {
144
+ (req as any).rawBody = body;
145
+ try {
146
+ done(null, JSON.parse(body as string));
147
+ } catch (err) {
148
+ done(err as Error);
149
+ }
150
+ });
151
+
152
+ app.post("/webhooks/tokolaku", async (req, reply) => {
153
+ try {
154
+ const { event } = constructEvent((req as any).rawBody, req.headers["x-tokolaku-signature"] as string, process.env.TOKOLAKU_WEBHOOK_SECRET!);
155
+ // ... handle event
156
+ return reply.send({ received: true });
157
+ } catch (err) {
158
+ if (err instanceof TokolakuWebhookSignatureError) {
159
+ return reply.code(401).send({ error: "invalid signature" });
160
+ }
161
+ throw err;
162
+ }
163
+ });
164
+ ```
165
+
166
+ ## Requirements
167
+
168
+ - Node.js ≥ 18
169
+
170
+ ## License
171
+
172
+ MIT
173
+
174
+ ## Docs
175
+
176
+ Full API reference: [https://tokolaku.id/api-docs](https://tokolaku.id/api-docs)
@@ -0,0 +1,61 @@
1
+ // src/errors.ts
2
+ var TokolakuAPIError = class extends Error {
3
+ status;
4
+ code;
5
+ constructor(message, opts) {
6
+ super(message);
7
+ this.name = new.target.name;
8
+ this.status = opts.status;
9
+ this.code = opts.code;
10
+ }
11
+ };
12
+ var TokolakuAuthenticationError = class extends TokolakuAPIError {
13
+ };
14
+ var TokolakuInsufficientBalanceError = class extends TokolakuAPIError {
15
+ };
16
+ var TokolakuPermissionError = class extends TokolakuAPIError {
17
+ };
18
+ var TokolakuRateLimitError = class extends TokolakuAPIError {
19
+ };
20
+ var TokolakuValidationError = class extends TokolakuAPIError {
21
+ };
22
+ var TokolakuWebhookSignatureError = class extends Error {
23
+ constructor(message = "Signature webhook tidak valid") {
24
+ super(message);
25
+ this.name = "TokolakuWebhookSignatureError";
26
+ }
27
+ };
28
+ var STATUS_CLASS = {
29
+ 400: TokolakuValidationError,
30
+ 401: TokolakuAuthenticationError,
31
+ 402: TokolakuInsufficientBalanceError,
32
+ 403: TokolakuPermissionError,
33
+ 422: TokolakuValidationError,
34
+ 429: TokolakuRateLimitError
35
+ };
36
+ function mapResponseError(status, bodyText) {
37
+ let code = null;
38
+ let message = bodyText ? bodyText.slice(0, 500) : `HTTP ${status}`;
39
+ try {
40
+ const parsed = JSON.parse(bodyText);
41
+ if (parsed?.error) {
42
+ code = parsed.error.code ?? null;
43
+ message = parsed.error.message ?? message;
44
+ }
45
+ } catch {
46
+ }
47
+ const Cls = STATUS_CLASS[status] ?? TokolakuAPIError;
48
+ return new Cls(message, { status, code });
49
+ }
50
+
51
+ export {
52
+ TokolakuAPIError,
53
+ TokolakuAuthenticationError,
54
+ TokolakuInsufficientBalanceError,
55
+ TokolakuPermissionError,
56
+ TokolakuRateLimitError,
57
+ TokolakuValidationError,
58
+ TokolakuWebhookSignatureError,
59
+ mapResponseError
60
+ };
61
+ //# sourceMappingURL=chunk-6YQXWMOC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts"],"sourcesContent":["/** Base error semua kegagalan API. `status` null = kegagalan sebelum ada\n * respons HTTP (network/timeout). `code` = kode envelope BE, mis.\n * \"insufficient_balance\"; null bila body bukan JSON envelope. */\nexport class TokolakuAPIError extends Error {\n readonly status: number | null;\n readonly code: string | null;\n constructor(message: string, opts: { status: number | null; code: string | null }) {\n super(message);\n this.name = new.target.name;\n this.status = opts.status;\n this.code = opts.code;\n }\n}\n\nexport class TokolakuAuthenticationError extends TokolakuAPIError {} // 401\nexport class TokolakuInsufficientBalanceError extends TokolakuAPIError {} // 402\nexport class TokolakuPermissionError extends TokolakuAPIError {} // 403\nexport class TokolakuRateLimitError extends TokolakuAPIError {} // 429\nexport class TokolakuValidationError extends TokolakuAPIError {} // 400/422 + validasi klien\n\nexport class TokolakuWebhookSignatureError extends Error {\n constructor(message = \"Signature webhook tidak valid\") {\n super(message);\n this.name = \"TokolakuWebhookSignatureError\";\n }\n}\n\nconst STATUS_CLASS: Record<number, new (m: string, o: { status: number | null; code: string | null }) => TokolakuAPIError> = {\n 400: TokolakuValidationError,\n 401: TokolakuAuthenticationError,\n 402: TokolakuInsufficientBalanceError,\n 403: TokolakuPermissionError,\n 422: TokolakuValidationError,\n 429: TokolakuRateLimitError,\n};\n\n/** Terjemahkan respons non-2xx jadi error class. Envelope BE:\n * `{ error: { code, message } }`. Body non-JSON dipotong 500 char. */\nexport function mapResponseError(status: number, bodyText: string): TokolakuAPIError {\n let code: string | null = null;\n let message = bodyText ? bodyText.slice(0, 500) : `HTTP ${status}`;\n try {\n const parsed = JSON.parse(bodyText) as { error?: { code?: string; message?: string } };\n if (parsed?.error) {\n code = parsed.error.code ?? null;\n message = parsed.error.message ?? message;\n }\n } catch {\n /* non-JSON — pakai default */\n }\n const Cls = STATUS_CLASS[status] ?? TokolakuAPIError;\n return new Cls(message, { status, code });\n}\n"],"mappings":";AAGO,IAAM,mBAAN,cAA+B,MAAM;AAAA,EACjC;AAAA,EACA;AAAA,EACT,YAAY,SAAiB,MAAsD;AACjF,UAAM,OAAO;AACb,SAAK,OAAO,WAAW;AACvB,SAAK,SAAS,KAAK;AACnB,SAAK,OAAO,KAAK;AAAA,EACnB;AACF;AAEO,IAAM,8BAAN,cAA0C,iBAAiB;AAAC;AAC5D,IAAM,mCAAN,cAA+C,iBAAiB;AAAC;AACjE,IAAM,0BAAN,cAAsC,iBAAiB;AAAC;AACxD,IAAM,yBAAN,cAAqC,iBAAiB;AAAC;AACvD,IAAM,0BAAN,cAAsC,iBAAiB;AAAC;AAExD,IAAM,gCAAN,cAA4C,MAAM;AAAA,EACvD,YAAY,UAAU,iCAAiC;AACrD,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAM,eAAuH;AAAA,EAC3H,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAIO,SAAS,iBAAiB,QAAgB,UAAoC;AACnF,MAAI,OAAsB;AAC1B,MAAI,UAAU,WAAW,SAAS,MAAM,GAAG,GAAG,IAAI,QAAQ,MAAM;AAChE,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,QAAQ;AAClC,QAAI,QAAQ,OAAO;AACjB,aAAO,OAAO,MAAM,QAAQ;AAC5B,gBAAU,OAAO,MAAM,WAAW;AAAA,IACpC;AAAA,EACF,QAAQ;AAAA,EAER;AACA,QAAM,MAAM,aAAa,MAAM,KAAK;AACpC,SAAO,IAAI,IAAI,SAAS,EAAE,QAAQ,KAAK,CAAC;AAC1C;","names":[]}
@@ -0,0 +1,26 @@
1
+ /** Base error semua kegagalan API. `status` null = kegagalan sebelum ada
2
+ * respons HTTP (network/timeout). `code` = kode envelope BE, mis.
3
+ * "insufficient_balance"; null bila body bukan JSON envelope. */
4
+ declare class TokolakuAPIError extends Error {
5
+ readonly status: number | null;
6
+ readonly code: string | null;
7
+ constructor(message: string, opts: {
8
+ status: number | null;
9
+ code: string | null;
10
+ });
11
+ }
12
+ declare class TokolakuAuthenticationError extends TokolakuAPIError {
13
+ }
14
+ declare class TokolakuInsufficientBalanceError extends TokolakuAPIError {
15
+ }
16
+ declare class TokolakuPermissionError extends TokolakuAPIError {
17
+ }
18
+ declare class TokolakuRateLimitError extends TokolakuAPIError {
19
+ }
20
+ declare class TokolakuValidationError extends TokolakuAPIError {
21
+ }
22
+ declare class TokolakuWebhookSignatureError extends Error {
23
+ constructor(message?: string);
24
+ }
25
+
26
+ export { TokolakuAPIError as T, TokolakuAuthenticationError as a, TokolakuInsufficientBalanceError as b, TokolakuPermissionError as c, TokolakuRateLimitError as d, TokolakuValidationError as e, TokolakuWebhookSignatureError as f };
@@ -0,0 +1,26 @@
1
+ /** Base error semua kegagalan API. `status` null = kegagalan sebelum ada
2
+ * respons HTTP (network/timeout). `code` = kode envelope BE, mis.
3
+ * "insufficient_balance"; null bila body bukan JSON envelope. */
4
+ declare class TokolakuAPIError extends Error {
5
+ readonly status: number | null;
6
+ readonly code: string | null;
7
+ constructor(message: string, opts: {
8
+ status: number | null;
9
+ code: string | null;
10
+ });
11
+ }
12
+ declare class TokolakuAuthenticationError extends TokolakuAPIError {
13
+ }
14
+ declare class TokolakuInsufficientBalanceError extends TokolakuAPIError {
15
+ }
16
+ declare class TokolakuPermissionError extends TokolakuAPIError {
17
+ }
18
+ declare class TokolakuRateLimitError extends TokolakuAPIError {
19
+ }
20
+ declare class TokolakuValidationError extends TokolakuAPIError {
21
+ }
22
+ declare class TokolakuWebhookSignatureError extends Error {
23
+ constructor(message?: string);
24
+ }
25
+
26
+ export { TokolakuAPIError as T, TokolakuAuthenticationError as a, TokolakuInsufficientBalanceError as b, TokolakuPermissionError as c, TokolakuRateLimitError as d, TokolakuValidationError as e, TokolakuWebhookSignatureError as f };
package/dist/index.cjs ADDED
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ Tokolaku: () => Tokolaku,
24
+ TokolakuAPIError: () => TokolakuAPIError,
25
+ TokolakuAuthenticationError: () => TokolakuAuthenticationError,
26
+ TokolakuInsufficientBalanceError: () => TokolakuInsufficientBalanceError,
27
+ TokolakuPermissionError: () => TokolakuPermissionError,
28
+ TokolakuRateLimitError: () => TokolakuRateLimitError,
29
+ TokolakuValidationError: () => TokolakuValidationError,
30
+ TokolakuWebhookSignatureError: () => TokolakuWebhookSignatureError,
31
+ default: () => Tokolaku
32
+ });
33
+ module.exports = __toCommonJS(src_exports);
34
+
35
+ // src/errors.ts
36
+ var TokolakuAPIError = class extends Error {
37
+ status;
38
+ code;
39
+ constructor(message, opts) {
40
+ super(message);
41
+ this.name = new.target.name;
42
+ this.status = opts.status;
43
+ this.code = opts.code;
44
+ }
45
+ };
46
+ var TokolakuAuthenticationError = class extends TokolakuAPIError {
47
+ };
48
+ var TokolakuInsufficientBalanceError = class extends TokolakuAPIError {
49
+ };
50
+ var TokolakuPermissionError = class extends TokolakuAPIError {
51
+ };
52
+ var TokolakuRateLimitError = class extends TokolakuAPIError {
53
+ };
54
+ var TokolakuValidationError = class extends TokolakuAPIError {
55
+ };
56
+ var TokolakuWebhookSignatureError = class extends Error {
57
+ constructor(message = "Signature webhook tidak valid") {
58
+ super(message);
59
+ this.name = "TokolakuWebhookSignatureError";
60
+ }
61
+ };
62
+ var STATUS_CLASS = {
63
+ 400: TokolakuValidationError,
64
+ 401: TokolakuAuthenticationError,
65
+ 402: TokolakuInsufficientBalanceError,
66
+ 403: TokolakuPermissionError,
67
+ 422: TokolakuValidationError,
68
+ 429: TokolakuRateLimitError
69
+ };
70
+ function mapResponseError(status, bodyText) {
71
+ let code = null;
72
+ let message = bodyText ? bodyText.slice(0, 500) : `HTTP ${status}`;
73
+ try {
74
+ const parsed = JSON.parse(bodyText);
75
+ if (parsed?.error) {
76
+ code = parsed.error.code ?? null;
77
+ message = parsed.error.message ?? message;
78
+ }
79
+ } catch {
80
+ }
81
+ const Cls = STATUS_CLASS[status] ?? TokolakuAPIError;
82
+ return new Cls(message, { status, code });
83
+ }
84
+
85
+ // src/retry.ts
86
+ function shouldRetry(policy, error) {
87
+ if (error.code === "timeout") return false;
88
+ if (error.status === 429) return true;
89
+ if (error.code === "network_error") return true;
90
+ if (policy === "botReply" && error.status !== null && error.status >= 500) return true;
91
+ return false;
92
+ }
93
+ function retryDelayMs(attempt, retryAfterSec) {
94
+ if (retryAfterSec != null && Number.isFinite(retryAfterSec)) return Math.max(0, retryAfterSec * 1e3);
95
+ const cap = Math.min(1e3, 250 * 2 ** attempt);
96
+ return Math.round(cap * (0.5 + Math.random() * 0.5));
97
+ }
98
+
99
+ // src/client.ts
100
+ var DEFAULT_BASE_URL = "https://api.tokolaku.id";
101
+ var DEFAULT_TIMEOUT_MS = 3e4;
102
+ var DEFAULT_MAX_RETRIES = 2;
103
+ var Tokolaku = class {
104
+ #apiKey;
105
+ #baseUrl;
106
+ #timeoutMs;
107
+ #maxRetries;
108
+ #fetch;
109
+ constructor(apiKeyOrOptions) {
110
+ const o = typeof apiKeyOrOptions === "string" ? { apiKey: apiKeyOrOptions } : apiKeyOrOptions;
111
+ if (!o.apiKey) throw new TokolakuValidationError("apiKey wajib diisi", { status: null, code: "missing_api_key" });
112
+ this.#apiKey = o.apiKey;
113
+ this.#baseUrl = (o.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
114
+ this.#timeoutMs = o.timeoutMs ?? DEFAULT_TIMEOUT_MS;
115
+ this.#maxRetries = o.maxRetries ?? DEFAULT_MAX_RETRIES;
116
+ this.#fetch = o.fetchImpl ?? globalThis.fetch;
117
+ }
118
+ /** Balasan AI bot tenant untuk satu pesan pelanggan (POST /api/v1/bot/reply). */
119
+ async botReply(params) {
120
+ return this.#request("/api/v1/bot/reply", params, "botReply");
121
+ }
122
+ messages = {
123
+ /** Kirim pesan text/template via channel resmi (POST /api/v1/messages).
124
+ * `type` diinferensi: field `text` → "text", field `template` → "template". */
125
+ send: async (params) => {
126
+ const hasText = "text" in params && params.text != null;
127
+ const hasTemplate = "template" in params && params.template != null;
128
+ if (hasText === hasTemplate) {
129
+ throw new TokolakuValidationError(
130
+ "Isi tepat satu: `text` (pesan sesi) ATAU `template` (business-initiated)",
131
+ { status: null, code: "invalid_params" }
132
+ );
133
+ }
134
+ const body = { ...params, type: hasText ? "text" : "template" };
135
+ return this.#request("/api/v1/messages", body, "messages");
136
+ }
137
+ };
138
+ async #request(path, body, policy) {
139
+ let lastError = null;
140
+ for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {
141
+ let retryAfterSec = null;
142
+ try {
143
+ return await this.#once(path, body, (v) => {
144
+ retryAfterSec = v;
145
+ });
146
+ } catch (e) {
147
+ const err = e instanceof TokolakuAPIError ? e : new TokolakuAPIError(String(e), { status: null, code: "network_error" });
148
+ lastError = err;
149
+ if (attempt >= this.#maxRetries || !shouldRetry(policy, err)) throw err;
150
+ await new Promise((r) => setTimeout(r, retryDelayMs(attempt, retryAfterSec)));
151
+ }
152
+ }
153
+ throw lastError;
154
+ }
155
+ async #once(path, body, onRetryAfter) {
156
+ const controller = new AbortController();
157
+ const timer = setTimeout(() => controller.abort(), this.#timeoutMs);
158
+ try {
159
+ const res = await this.#fetch(`${this.#baseUrl}${path}`, {
160
+ method: "POST",
161
+ headers: { "authorization": `Bearer ${this.#apiKey}`, "content-type": "application/json" },
162
+ body: JSON.stringify(body),
163
+ signal: controller.signal
164
+ });
165
+ const text = await res.text();
166
+ if (!res.ok) {
167
+ const ra = res.headers.get("retry-after");
168
+ onRetryAfter(ra != null && /^\d+$/.test(ra) ? Number(ra) : null);
169
+ throw mapResponseError(res.status, text);
170
+ }
171
+ try {
172
+ return JSON.parse(text);
173
+ } catch {
174
+ throw new TokolakuAPIError("Respons server bukan JSON valid", { status: res.status, code: "invalid_response" });
175
+ }
176
+ } catch (e) {
177
+ if (e instanceof TokolakuAPIError) throw e;
178
+ if (e instanceof Error && e.name === "AbortError") {
179
+ throw new TokolakuAPIError(`Timeout setelah ${this.#timeoutMs}ms`, { status: null, code: "timeout" });
180
+ }
181
+ const msg = e instanceof Error ? e.message : String(e);
182
+ throw new TokolakuAPIError(`Network error: ${msg}`, { status: null, code: "network_error" });
183
+ } finally {
184
+ clearTimeout(timer);
185
+ }
186
+ }
187
+ };
188
+ // Annotate the CommonJS export names for ESM import in node:
189
+ 0 && (module.exports = {
190
+ Tokolaku,
191
+ TokolakuAPIError,
192
+ TokolakuAuthenticationError,
193
+ TokolakuInsufficientBalanceError,
194
+ TokolakuPermissionError,
195
+ TokolakuRateLimitError,
196
+ TokolakuValidationError,
197
+ TokolakuWebhookSignatureError
198
+ });
199
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/retry.ts","../src/client.ts"],"sourcesContent":["// src/index.ts\nexport { Tokolaku } from \"./client.js\";\nexport { Tokolaku as default } from \"./client.js\";\nexport {\n TokolakuAPIError,\n TokolakuAuthenticationError,\n TokolakuInsufficientBalanceError,\n TokolakuPermissionError,\n TokolakuRateLimitError,\n TokolakuValidationError,\n TokolakuWebhookSignatureError,\n} from \"./errors.js\";\nexport type {\n TokolakuOptions,\n BotReplyParams, BotReplyResponse,\n SendTextParams, SendTemplateParams, SendMessageParams, SendMessageResponse,\n} from \"./types.js\";\n","/** Base error semua kegagalan API. `status` null = kegagalan sebelum ada\n * respons HTTP (network/timeout). `code` = kode envelope BE, mis.\n * \"insufficient_balance\"; null bila body bukan JSON envelope. */\nexport class TokolakuAPIError extends Error {\n readonly status: number | null;\n readonly code: string | null;\n constructor(message: string, opts: { status: number | null; code: string | null }) {\n super(message);\n this.name = new.target.name;\n this.status = opts.status;\n this.code = opts.code;\n }\n}\n\nexport class TokolakuAuthenticationError extends TokolakuAPIError {} // 401\nexport class TokolakuInsufficientBalanceError extends TokolakuAPIError {} // 402\nexport class TokolakuPermissionError extends TokolakuAPIError {} // 403\nexport class TokolakuRateLimitError extends TokolakuAPIError {} // 429\nexport class TokolakuValidationError extends TokolakuAPIError {} // 400/422 + validasi klien\n\nexport class TokolakuWebhookSignatureError extends Error {\n constructor(message = \"Signature webhook tidak valid\") {\n super(message);\n this.name = \"TokolakuWebhookSignatureError\";\n }\n}\n\nconst STATUS_CLASS: Record<number, new (m: string, o: { status: number | null; code: string | null }) => TokolakuAPIError> = {\n 400: TokolakuValidationError,\n 401: TokolakuAuthenticationError,\n 402: TokolakuInsufficientBalanceError,\n 403: TokolakuPermissionError,\n 422: TokolakuValidationError,\n 429: TokolakuRateLimitError,\n};\n\n/** Terjemahkan respons non-2xx jadi error class. Envelope BE:\n * `{ error: { code, message } }`. Body non-JSON dipotong 500 char. */\nexport function mapResponseError(status: number, bodyText: string): TokolakuAPIError {\n let code: string | null = null;\n let message = bodyText ? bodyText.slice(0, 500) : `HTTP ${status}`;\n try {\n const parsed = JSON.parse(bodyText) as { error?: { code?: string; message?: string } };\n if (parsed?.error) {\n code = parsed.error.code ?? null;\n message = parsed.error.message ?? message;\n }\n } catch {\n /* non-JSON — pakai default */\n }\n const Cls = STATUS_CLASS[status] ?? TokolakuAPIError;\n return new Cls(message, { status, code });\n}\n","import { TokolakuAPIError } from \"./errors.js\";\n\nexport type RetryPolicy = \"botReply\" | \"messages\";\n\n/** Retry uang-sadar:\n * - botReply: 429, 5xx, network error (tanpa efek samping bila gagal).\n * - messages: HANYA 429 + network TypeError (pesan mungkin sudah terkirim\n * & tercharge pada timeout/5xx — API belum punya idempotency key).\n * - timeout (code \"timeout\") TIDAK pernah di-retry. */\nexport function shouldRetry(policy: RetryPolicy, error: TokolakuAPIError): boolean {\n if (error.code === \"timeout\") return false;\n if (error.status === 429) return true;\n // \"invalid_response\" (200 OK tapi body JSON rusak) SENGAJA tidak match rule apa pun di\n // bawah ini — efek samping server sudah terjadi, jadi non-retryable untuk kedua policy.\n if (error.code === \"network_error\") return true;\n if (policy === \"botReply\" && error.status !== null && error.status >= 500) return true;\n return false;\n}\n\n/** Exponential backoff + full jitter, base 250ms cap 1s; Retry-After menang. */\nexport function retryDelayMs(attempt: number, retryAfterSec: number | null): number {\n if (retryAfterSec != null && Number.isFinite(retryAfterSec)) return Math.max(0, retryAfterSec * 1000);\n const cap = Math.min(1000, 250 * 2 ** attempt);\n return Math.round(cap * (0.5 + Math.random() * 0.5));\n}\n","// src/client.ts\nimport { mapResponseError, TokolakuAPIError, TokolakuValidationError } from \"./errors.js\";\nimport { shouldRetry, retryDelayMs, type RetryPolicy } from \"./retry.js\";\nimport type {\n BotReplyParams, BotReplyResponse, SendMessageParams, SendMessageResponse, TokolakuOptions,\n} from \"./types.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.tokolaku.id\";\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst DEFAULT_MAX_RETRIES = 2;\n\nexport class Tokolaku {\n readonly #apiKey: string;\n readonly #baseUrl: string;\n readonly #timeoutMs: number;\n readonly #maxRetries: number;\n readonly #fetch: typeof fetch;\n\n constructor(apiKeyOrOptions: string | TokolakuOptions) {\n const o: TokolakuOptions =\n typeof apiKeyOrOptions === \"string\" ? { apiKey: apiKeyOrOptions } : apiKeyOrOptions;\n if (!o.apiKey) throw new TokolakuValidationError(\"apiKey wajib diisi\", { status: null, code: \"missing_api_key\" });\n this.#apiKey = o.apiKey;\n this.#baseUrl = (o.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.#timeoutMs = o.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.#maxRetries = o.maxRetries ?? DEFAULT_MAX_RETRIES;\n this.#fetch = o.fetchImpl ?? globalThis.fetch;\n }\n\n /** Balasan AI bot tenant untuk satu pesan pelanggan (POST /api/v1/bot/reply). */\n async botReply(params: BotReplyParams): Promise<BotReplyResponse> {\n return this.#request<BotReplyResponse>(\"/api/v1/bot/reply\", params, \"botReply\");\n }\n\n readonly messages = {\n /** Kirim pesan text/template via channel resmi (POST /api/v1/messages).\n * `type` diinferensi: field `text` → \"text\", field `template` → \"template\". */\n send: async (params: SendMessageParams): Promise<SendMessageResponse> => {\n const hasText = \"text\" in params && params.text != null;\n const hasTemplate = \"template\" in params && (params as { template?: unknown }).template != null;\n if (hasText === hasTemplate) {\n throw new TokolakuValidationError(\n \"Isi tepat satu: `text` (pesan sesi) ATAU `template` (business-initiated)\",\n { status: null, code: \"invalid_params\" },\n );\n }\n const body = { ...params, type: hasText ? \"text\" : \"template\" };\n return this.#request<SendMessageResponse>(\"/api/v1/messages\", body, \"messages\");\n },\n };\n\n async #request<T>(path: string, body: unknown, policy: RetryPolicy): Promise<T> {\n let lastError: TokolakuAPIError | null = null;\n for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {\n let retryAfterSec: number | null = null;\n try {\n return await this.#once<T>(path, body, (v) => { retryAfterSec = v; });\n } catch (e) {\n // defensif: #once selalu melempar TokolakuAPIError; cabang else = jaring pengaman\n const err = e instanceof TokolakuAPIError\n ? e\n : new TokolakuAPIError(String(e), { status: null, code: \"network_error\" });\n lastError = err;\n if (attempt >= this.#maxRetries || !shouldRetry(policy, err)) throw err;\n await new Promise((r) => setTimeout(r, retryDelayMs(attempt, retryAfterSec)));\n }\n }\n throw lastError!;\n }\n\n async #once<T>(path: string, body: unknown, onRetryAfter: (sec: number | null) => void): Promise<T> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.#timeoutMs);\n try {\n const res = await this.#fetch(`${this.#baseUrl}${path}`, {\n method: \"POST\",\n headers: { \"authorization\": `Bearer ${this.#apiKey}`, \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n const text = await res.text();\n if (!res.ok) {\n const ra = res.headers.get(\"retry-after\");\n onRetryAfter(ra != null && /^\\d+$/.test(ra) ? Number(ra) : null);\n throw mapResponseError(res.status, text);\n }\n try {\n return JSON.parse(text) as T;\n } catch {\n // Respons HTTP sudah diterima (efek samping server, mis. pesan terkirim &\n // tercharge, SUDAH terjadi) — parse gagal BUKAN network error & TIDAK boleh di-retry.\n throw new TokolakuAPIError(\"Respons server bukan JSON valid\", { status: res.status, code: \"invalid_response\" });\n }\n } catch (e) {\n if (e instanceof TokolakuAPIError) throw e;\n if (e instanceof Error && e.name === \"AbortError\") {\n throw new TokolakuAPIError(`Timeout setelah ${this.#timeoutMs}ms`, { status: null, code: \"timeout\" });\n }\n const msg = e instanceof Error ? e.message : String(e);\n throw new TokolakuAPIError(`Network error: ${msg}`, { status: null, code: \"network_error\" });\n } finally {\n clearTimeout(timer);\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,68 @@
1
+ export { T as TokolakuAPIError, a as TokolakuAuthenticationError, b as TokolakuInsufficientBalanceError, c as TokolakuPermissionError, d as TokolakuRateLimitError, e as TokolakuValidationError, f as TokolakuWebhookSignatureError } from './errors-PooMi-0n.cjs';
2
+
3
+ type TokolakuOptions = {
4
+ apiKey: string;
5
+ /** Default "https://api.tokolaku.id" */
6
+ baseUrl?: string;
7
+ /** Default 30_000 ms */
8
+ timeoutMs?: number;
9
+ /** Default 2 — lihat kebijakan retry per endpoint di README */
10
+ maxRetries?: number;
11
+ /** Default globalThis.fetch — untuk testing/runtime custom */
12
+ fetchImpl?: typeof fetch;
13
+ };
14
+ type BotReplyParams = {
15
+ /** Pesan pelanggan, 1..4000 char */
16
+ message: string;
17
+ /** ID sesi percakapan (≤120 char) — jaga konteks multi-turn */
18
+ session_id?: string;
19
+ history?: {
20
+ role: "user" | "assistant";
21
+ content: string;
22
+ }[];
23
+ };
24
+ type BotReplyResponse = {
25
+ reply: string;
26
+ parts: string[];
27
+ };
28
+ type SendTextParams = {
29
+ to: string;
30
+ text: string;
31
+ channel_id?: string;
32
+ };
33
+ type SendTemplateParams = {
34
+ to: string;
35
+ template: {
36
+ name: string;
37
+ language: string;
38
+ category: "marketing" | "utility" | "authentication";
39
+ components?: unknown[];
40
+ };
41
+ channel_id?: string;
42
+ /** ISO-2, default "ID" di server */
43
+ country_code?: string;
44
+ };
45
+ type SendMessageParams = SendTextParams | SendTemplateParams;
46
+ type SendMessageResponse = {
47
+ id: string;
48
+ channel_id: string;
49
+ to: string;
50
+ type: "text" | "template";
51
+ status: "sent";
52
+ provider_message_id: string | null;
53
+ charged_idr: number;
54
+ };
55
+
56
+ declare class Tokolaku {
57
+ #private;
58
+ constructor(apiKeyOrOptions: string | TokolakuOptions);
59
+ /** Balasan AI bot tenant untuk satu pesan pelanggan (POST /api/v1/bot/reply). */
60
+ botReply(params: BotReplyParams): Promise<BotReplyResponse>;
61
+ readonly messages: {
62
+ /** Kirim pesan text/template via channel resmi (POST /api/v1/messages).
63
+ * `type` diinferensi: field `text` → "text", field `template` → "template". */
64
+ send: (params: SendMessageParams) => Promise<SendMessageResponse>;
65
+ };
66
+ }
67
+
68
+ export { type BotReplyParams, type BotReplyResponse, type SendMessageParams, type SendMessageResponse, type SendTemplateParams, type SendTextParams, Tokolaku, type TokolakuOptions, Tokolaku as default };
@@ -0,0 +1,68 @@
1
+ export { T as TokolakuAPIError, a as TokolakuAuthenticationError, b as TokolakuInsufficientBalanceError, c as TokolakuPermissionError, d as TokolakuRateLimitError, e as TokolakuValidationError, f as TokolakuWebhookSignatureError } from './errors-PooMi-0n.js';
2
+
3
+ type TokolakuOptions = {
4
+ apiKey: string;
5
+ /** Default "https://api.tokolaku.id" */
6
+ baseUrl?: string;
7
+ /** Default 30_000 ms */
8
+ timeoutMs?: number;
9
+ /** Default 2 — lihat kebijakan retry per endpoint di README */
10
+ maxRetries?: number;
11
+ /** Default globalThis.fetch — untuk testing/runtime custom */
12
+ fetchImpl?: typeof fetch;
13
+ };
14
+ type BotReplyParams = {
15
+ /** Pesan pelanggan, 1..4000 char */
16
+ message: string;
17
+ /** ID sesi percakapan (≤120 char) — jaga konteks multi-turn */
18
+ session_id?: string;
19
+ history?: {
20
+ role: "user" | "assistant";
21
+ content: string;
22
+ }[];
23
+ };
24
+ type BotReplyResponse = {
25
+ reply: string;
26
+ parts: string[];
27
+ };
28
+ type SendTextParams = {
29
+ to: string;
30
+ text: string;
31
+ channel_id?: string;
32
+ };
33
+ type SendTemplateParams = {
34
+ to: string;
35
+ template: {
36
+ name: string;
37
+ language: string;
38
+ category: "marketing" | "utility" | "authentication";
39
+ components?: unknown[];
40
+ };
41
+ channel_id?: string;
42
+ /** ISO-2, default "ID" di server */
43
+ country_code?: string;
44
+ };
45
+ type SendMessageParams = SendTextParams | SendTemplateParams;
46
+ type SendMessageResponse = {
47
+ id: string;
48
+ channel_id: string;
49
+ to: string;
50
+ type: "text" | "template";
51
+ status: "sent";
52
+ provider_message_id: string | null;
53
+ charged_idr: number;
54
+ };
55
+
56
+ declare class Tokolaku {
57
+ #private;
58
+ constructor(apiKeyOrOptions: string | TokolakuOptions);
59
+ /** Balasan AI bot tenant untuk satu pesan pelanggan (POST /api/v1/bot/reply). */
60
+ botReply(params: BotReplyParams): Promise<BotReplyResponse>;
61
+ readonly messages: {
62
+ /** Kirim pesan text/template via channel resmi (POST /api/v1/messages).
63
+ * `type` diinferensi: field `text` → "text", field `template` → "template". */
64
+ send: (params: SendMessageParams) => Promise<SendMessageResponse>;
65
+ };
66
+ }
67
+
68
+ export { type BotReplyParams, type BotReplyResponse, type SendMessageParams, type SendMessageResponse, type SendTemplateParams, type SendTextParams, Tokolaku, type TokolakuOptions, Tokolaku as default };
package/dist/index.js ADDED
@@ -0,0 +1,126 @@
1
+ import {
2
+ TokolakuAPIError,
3
+ TokolakuAuthenticationError,
4
+ TokolakuInsufficientBalanceError,
5
+ TokolakuPermissionError,
6
+ TokolakuRateLimitError,
7
+ TokolakuValidationError,
8
+ TokolakuWebhookSignatureError,
9
+ mapResponseError
10
+ } from "./chunk-6YQXWMOC.js";
11
+
12
+ // src/retry.ts
13
+ function shouldRetry(policy, error) {
14
+ if (error.code === "timeout") return false;
15
+ if (error.status === 429) return true;
16
+ if (error.code === "network_error") return true;
17
+ if (policy === "botReply" && error.status !== null && error.status >= 500) return true;
18
+ return false;
19
+ }
20
+ function retryDelayMs(attempt, retryAfterSec) {
21
+ if (retryAfterSec != null && Number.isFinite(retryAfterSec)) return Math.max(0, retryAfterSec * 1e3);
22
+ const cap = Math.min(1e3, 250 * 2 ** attempt);
23
+ return Math.round(cap * (0.5 + Math.random() * 0.5));
24
+ }
25
+
26
+ // src/client.ts
27
+ var DEFAULT_BASE_URL = "https://api.tokolaku.id";
28
+ var DEFAULT_TIMEOUT_MS = 3e4;
29
+ var DEFAULT_MAX_RETRIES = 2;
30
+ var Tokolaku = class {
31
+ #apiKey;
32
+ #baseUrl;
33
+ #timeoutMs;
34
+ #maxRetries;
35
+ #fetch;
36
+ constructor(apiKeyOrOptions) {
37
+ const o = typeof apiKeyOrOptions === "string" ? { apiKey: apiKeyOrOptions } : apiKeyOrOptions;
38
+ if (!o.apiKey) throw new TokolakuValidationError("apiKey wajib diisi", { status: null, code: "missing_api_key" });
39
+ this.#apiKey = o.apiKey;
40
+ this.#baseUrl = (o.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
41
+ this.#timeoutMs = o.timeoutMs ?? DEFAULT_TIMEOUT_MS;
42
+ this.#maxRetries = o.maxRetries ?? DEFAULT_MAX_RETRIES;
43
+ this.#fetch = o.fetchImpl ?? globalThis.fetch;
44
+ }
45
+ /** Balasan AI bot tenant untuk satu pesan pelanggan (POST /api/v1/bot/reply). */
46
+ async botReply(params) {
47
+ return this.#request("/api/v1/bot/reply", params, "botReply");
48
+ }
49
+ messages = {
50
+ /** Kirim pesan text/template via channel resmi (POST /api/v1/messages).
51
+ * `type` diinferensi: field `text` → "text", field `template` → "template". */
52
+ send: async (params) => {
53
+ const hasText = "text" in params && params.text != null;
54
+ const hasTemplate = "template" in params && params.template != null;
55
+ if (hasText === hasTemplate) {
56
+ throw new TokolakuValidationError(
57
+ "Isi tepat satu: `text` (pesan sesi) ATAU `template` (business-initiated)",
58
+ { status: null, code: "invalid_params" }
59
+ );
60
+ }
61
+ const body = { ...params, type: hasText ? "text" : "template" };
62
+ return this.#request("/api/v1/messages", body, "messages");
63
+ }
64
+ };
65
+ async #request(path, body, policy) {
66
+ let lastError = null;
67
+ for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {
68
+ let retryAfterSec = null;
69
+ try {
70
+ return await this.#once(path, body, (v) => {
71
+ retryAfterSec = v;
72
+ });
73
+ } catch (e) {
74
+ const err = e instanceof TokolakuAPIError ? e : new TokolakuAPIError(String(e), { status: null, code: "network_error" });
75
+ lastError = err;
76
+ if (attempt >= this.#maxRetries || !shouldRetry(policy, err)) throw err;
77
+ await new Promise((r) => setTimeout(r, retryDelayMs(attempt, retryAfterSec)));
78
+ }
79
+ }
80
+ throw lastError;
81
+ }
82
+ async #once(path, body, onRetryAfter) {
83
+ const controller = new AbortController();
84
+ const timer = setTimeout(() => controller.abort(), this.#timeoutMs);
85
+ try {
86
+ const res = await this.#fetch(`${this.#baseUrl}${path}`, {
87
+ method: "POST",
88
+ headers: { "authorization": `Bearer ${this.#apiKey}`, "content-type": "application/json" },
89
+ body: JSON.stringify(body),
90
+ signal: controller.signal
91
+ });
92
+ const text = await res.text();
93
+ if (!res.ok) {
94
+ const ra = res.headers.get("retry-after");
95
+ onRetryAfter(ra != null && /^\d+$/.test(ra) ? Number(ra) : null);
96
+ throw mapResponseError(res.status, text);
97
+ }
98
+ try {
99
+ return JSON.parse(text);
100
+ } catch {
101
+ throw new TokolakuAPIError("Respons server bukan JSON valid", { status: res.status, code: "invalid_response" });
102
+ }
103
+ } catch (e) {
104
+ if (e instanceof TokolakuAPIError) throw e;
105
+ if (e instanceof Error && e.name === "AbortError") {
106
+ throw new TokolakuAPIError(`Timeout setelah ${this.#timeoutMs}ms`, { status: null, code: "timeout" });
107
+ }
108
+ const msg = e instanceof Error ? e.message : String(e);
109
+ throw new TokolakuAPIError(`Network error: ${msg}`, { status: null, code: "network_error" });
110
+ } finally {
111
+ clearTimeout(timer);
112
+ }
113
+ }
114
+ };
115
+ export {
116
+ Tokolaku,
117
+ TokolakuAPIError,
118
+ TokolakuAuthenticationError,
119
+ TokolakuInsufficientBalanceError,
120
+ TokolakuPermissionError,
121
+ TokolakuRateLimitError,
122
+ TokolakuValidationError,
123
+ TokolakuWebhookSignatureError,
124
+ Tokolaku as default
125
+ };
126
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/retry.ts","../src/client.ts"],"sourcesContent":["import { TokolakuAPIError } from \"./errors.js\";\n\nexport type RetryPolicy = \"botReply\" | \"messages\";\n\n/** Retry uang-sadar:\n * - botReply: 429, 5xx, network error (tanpa efek samping bila gagal).\n * - messages: HANYA 429 + network TypeError (pesan mungkin sudah terkirim\n * & tercharge pada timeout/5xx — API belum punya idempotency key).\n * - timeout (code \"timeout\") TIDAK pernah di-retry. */\nexport function shouldRetry(policy: RetryPolicy, error: TokolakuAPIError): boolean {\n if (error.code === \"timeout\") return false;\n if (error.status === 429) return true;\n // \"invalid_response\" (200 OK tapi body JSON rusak) SENGAJA tidak match rule apa pun di\n // bawah ini — efek samping server sudah terjadi, jadi non-retryable untuk kedua policy.\n if (error.code === \"network_error\") return true;\n if (policy === \"botReply\" && error.status !== null && error.status >= 500) return true;\n return false;\n}\n\n/** Exponential backoff + full jitter, base 250ms cap 1s; Retry-After menang. */\nexport function retryDelayMs(attempt: number, retryAfterSec: number | null): number {\n if (retryAfterSec != null && Number.isFinite(retryAfterSec)) return Math.max(0, retryAfterSec * 1000);\n const cap = Math.min(1000, 250 * 2 ** attempt);\n return Math.round(cap * (0.5 + Math.random() * 0.5));\n}\n","// src/client.ts\nimport { mapResponseError, TokolakuAPIError, TokolakuValidationError } from \"./errors.js\";\nimport { shouldRetry, retryDelayMs, type RetryPolicy } from \"./retry.js\";\nimport type {\n BotReplyParams, BotReplyResponse, SendMessageParams, SendMessageResponse, TokolakuOptions,\n} from \"./types.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.tokolaku.id\";\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst DEFAULT_MAX_RETRIES = 2;\n\nexport class Tokolaku {\n readonly #apiKey: string;\n readonly #baseUrl: string;\n readonly #timeoutMs: number;\n readonly #maxRetries: number;\n readonly #fetch: typeof fetch;\n\n constructor(apiKeyOrOptions: string | TokolakuOptions) {\n const o: TokolakuOptions =\n typeof apiKeyOrOptions === \"string\" ? { apiKey: apiKeyOrOptions } : apiKeyOrOptions;\n if (!o.apiKey) throw new TokolakuValidationError(\"apiKey wajib diisi\", { status: null, code: \"missing_api_key\" });\n this.#apiKey = o.apiKey;\n this.#baseUrl = (o.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.#timeoutMs = o.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.#maxRetries = o.maxRetries ?? DEFAULT_MAX_RETRIES;\n this.#fetch = o.fetchImpl ?? globalThis.fetch;\n }\n\n /** Balasan AI bot tenant untuk satu pesan pelanggan (POST /api/v1/bot/reply). */\n async botReply(params: BotReplyParams): Promise<BotReplyResponse> {\n return this.#request<BotReplyResponse>(\"/api/v1/bot/reply\", params, \"botReply\");\n }\n\n readonly messages = {\n /** Kirim pesan text/template via channel resmi (POST /api/v1/messages).\n * `type` diinferensi: field `text` → \"text\", field `template` → \"template\". */\n send: async (params: SendMessageParams): Promise<SendMessageResponse> => {\n const hasText = \"text\" in params && params.text != null;\n const hasTemplate = \"template\" in params && (params as { template?: unknown }).template != null;\n if (hasText === hasTemplate) {\n throw new TokolakuValidationError(\n \"Isi tepat satu: `text` (pesan sesi) ATAU `template` (business-initiated)\",\n { status: null, code: \"invalid_params\" },\n );\n }\n const body = { ...params, type: hasText ? \"text\" : \"template\" };\n return this.#request<SendMessageResponse>(\"/api/v1/messages\", body, \"messages\");\n },\n };\n\n async #request<T>(path: string, body: unknown, policy: RetryPolicy): Promise<T> {\n let lastError: TokolakuAPIError | null = null;\n for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {\n let retryAfterSec: number | null = null;\n try {\n return await this.#once<T>(path, body, (v) => { retryAfterSec = v; });\n } catch (e) {\n // defensif: #once selalu melempar TokolakuAPIError; cabang else = jaring pengaman\n const err = e instanceof TokolakuAPIError\n ? e\n : new TokolakuAPIError(String(e), { status: null, code: \"network_error\" });\n lastError = err;\n if (attempt >= this.#maxRetries || !shouldRetry(policy, err)) throw err;\n await new Promise((r) => setTimeout(r, retryDelayMs(attempt, retryAfterSec)));\n }\n }\n throw lastError!;\n }\n\n async #once<T>(path: string, body: unknown, onRetryAfter: (sec: number | null) => void): Promise<T> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.#timeoutMs);\n try {\n const res = await this.#fetch(`${this.#baseUrl}${path}`, {\n method: \"POST\",\n headers: { \"authorization\": `Bearer ${this.#apiKey}`, \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n const text = await res.text();\n if (!res.ok) {\n const ra = res.headers.get(\"retry-after\");\n onRetryAfter(ra != null && /^\\d+$/.test(ra) ? Number(ra) : null);\n throw mapResponseError(res.status, text);\n }\n try {\n return JSON.parse(text) as T;\n } catch {\n // Respons HTTP sudah diterima (efek samping server, mis. pesan terkirim &\n // tercharge, SUDAH terjadi) — parse gagal BUKAN network error & TIDAK boleh di-retry.\n throw new TokolakuAPIError(\"Respons server bukan JSON valid\", { status: res.status, code: \"invalid_response\" });\n }\n } catch (e) {\n if (e instanceof TokolakuAPIError) throw e;\n if (e instanceof Error && e.name === \"AbortError\") {\n throw new TokolakuAPIError(`Timeout setelah ${this.#timeoutMs}ms`, { status: null, code: \"timeout\" });\n }\n const msg = e instanceof Error ? e.message : String(e);\n throw new TokolakuAPIError(`Network error: ${msg}`, { status: null, code: \"network_error\" });\n } finally {\n clearTimeout(timer);\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/webhooks.ts
21
+ var webhooks_exports = {};
22
+ __export(webhooks_exports, {
23
+ TokolakuWebhookSignatureError: () => TokolakuWebhookSignatureError,
24
+ constructEvent: () => constructEvent,
25
+ verifyWebhookSignature: () => verifyWebhookSignature
26
+ });
27
+ module.exports = __toCommonJS(webhooks_exports);
28
+ var import_node_crypto = require("crypto");
29
+
30
+ // src/errors.ts
31
+ var TokolakuWebhookSignatureError = class extends Error {
32
+ constructor(message = "Signature webhook tidak valid") {
33
+ super(message);
34
+ this.name = "TokolakuWebhookSignatureError";
35
+ }
36
+ };
37
+
38
+ // src/webhooks.ts
39
+ function verifyWebhookSignature(rawBody, signatureHeader, secret) {
40
+ if (!signatureHeader?.startsWith("sha256=")) return false;
41
+ const hex = signatureHeader.slice("sha256=".length);
42
+ if (!/^[0-9a-f]{64}$/i.test(hex)) return false;
43
+ const expected = (0, import_node_crypto.createHmac)("sha256", secret).update(rawBody).digest();
44
+ const given = Buffer.from(hex, "hex");
45
+ return (0, import_node_crypto.timingSafeEqual)(given, expected);
46
+ }
47
+ function constructEvent(rawBody, signatureHeader, secret) {
48
+ if (!verifyWebhookSignature(rawBody, signatureHeader, secret)) {
49
+ throw new TokolakuWebhookSignatureError();
50
+ }
51
+ return { event: JSON.parse(rawBody) };
52
+ }
53
+ // Annotate the CommonJS export names for ESM import in node:
54
+ 0 && (module.exports = {
55
+ TokolakuWebhookSignatureError,
56
+ constructEvent,
57
+ verifyWebhookSignature
58
+ });
59
+ //# sourceMappingURL=webhooks.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/webhooks.ts","../src/errors.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { TokolakuWebhookSignatureError } from \"./errors.js\";\n\n/** Verifikasi header `x-tokolaku-signature` (format `sha256=<hex>`,\n * HMAC-SHA256(secret, rawBody)) — compare timing-safe. rawBody HARUS\n * string mentah persis seperti diterima (bukan hasil re-serialize).\n * Hex format strict: exactly 64 hex chars, case-insensitive. */\nexport function verifyWebhookSignature(\n rawBody: string,\n signatureHeader: string | undefined,\n secret: string,\n): boolean {\n if (!signatureHeader?.startsWith(\"sha256=\")) return false;\n const hex = signatureHeader.slice(\"sha256=\".length);\n if (!/^[0-9a-f]{64}$/i.test(hex)) return false; // strict: exactly 64 hex chars, reject trailing garbage\n const expected = createHmac(\"sha256\", secret).update(rawBody).digest();\n const given = Buffer.from(hex, \"hex\");\n return timingSafeEqual(given, expected);\n}\n\n/** Verify + parse. Signature invalid → throw TokolakuWebhookSignatureError.\n * Signature VALID tapi rawBody bukan JSON valid → SyntaxError dari JSON.parse\n * (sengaja tidak dibungkus — itu bug payload, bukan soal keamanan). */\nexport function constructEvent<T = unknown>(\n rawBody: string,\n signatureHeader: string | undefined,\n secret: string,\n): { event: T } {\n if (!verifyWebhookSignature(rawBody, signatureHeader, secret)) {\n throw new TokolakuWebhookSignatureError();\n }\n return { event: JSON.parse(rawBody) as T };\n}\n\n// Re-export supaya konsumen bisa import class error dari subpath yang sama\n// dengan fungsi yang melemparnya (sesuai contoh README).\nexport { TokolakuWebhookSignatureError } from \"./errors.js\";\n","/** Base error semua kegagalan API. `status` null = kegagalan sebelum ada\n * respons HTTP (network/timeout). `code` = kode envelope BE, mis.\n * \"insufficient_balance\"; null bila body bukan JSON envelope. */\nexport class TokolakuAPIError extends Error {\n readonly status: number | null;\n readonly code: string | null;\n constructor(message: string, opts: { status: number | null; code: string | null }) {\n super(message);\n this.name = new.target.name;\n this.status = opts.status;\n this.code = opts.code;\n }\n}\n\nexport class TokolakuAuthenticationError extends TokolakuAPIError {} // 401\nexport class TokolakuInsufficientBalanceError extends TokolakuAPIError {} // 402\nexport class TokolakuPermissionError extends TokolakuAPIError {} // 403\nexport class TokolakuRateLimitError extends TokolakuAPIError {} // 429\nexport class TokolakuValidationError extends TokolakuAPIError {} // 400/422 + validasi klien\n\nexport class TokolakuWebhookSignatureError extends Error {\n constructor(message = \"Signature webhook tidak valid\") {\n super(message);\n this.name = \"TokolakuWebhookSignatureError\";\n }\n}\n\nconst STATUS_CLASS: Record<number, new (m: string, o: { status: number | null; code: string | null }) => TokolakuAPIError> = {\n 400: TokolakuValidationError,\n 401: TokolakuAuthenticationError,\n 402: TokolakuInsufficientBalanceError,\n 403: TokolakuPermissionError,\n 422: TokolakuValidationError,\n 429: TokolakuRateLimitError,\n};\n\n/** Terjemahkan respons non-2xx jadi error class. Envelope BE:\n * `{ error: { code, message } }`. Body non-JSON dipotong 500 char. */\nexport function mapResponseError(status: number, bodyText: string): TokolakuAPIError {\n let code: string | null = null;\n let message = bodyText ? bodyText.slice(0, 500) : `HTTP ${status}`;\n try {\n const parsed = JSON.parse(bodyText) as { error?: { code?: string; message?: string } };\n if (parsed?.error) {\n code = parsed.error.code ?? null;\n message = parsed.error.message ?? message;\n }\n } catch {\n /* non-JSON — pakai default */\n }\n const Cls = STATUS_CLASS[status] ?? TokolakuAPIError;\n return new Cls(message, { status, code });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAA4C;;;ACoBrC,IAAM,gCAAN,cAA4C,MAAM;AAAA,EACvD,YAAY,UAAU,iCAAiC;AACrD,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;;;ADlBO,SAAS,uBACd,SACA,iBACA,QACS;AACT,MAAI,CAAC,iBAAiB,WAAW,SAAS,EAAG,QAAO;AACpD,QAAM,MAAM,gBAAgB,MAAM,UAAU,MAAM;AAClD,MAAI,CAAC,kBAAkB,KAAK,GAAG,EAAG,QAAO;AACzC,QAAM,eAAW,+BAAW,UAAU,MAAM,EAAE,OAAO,OAAO,EAAE,OAAO;AACrE,QAAM,QAAQ,OAAO,KAAK,KAAK,KAAK;AACpC,aAAO,oCAAgB,OAAO,QAAQ;AACxC;AAKO,SAAS,eACd,SACA,iBACA,QACc;AACd,MAAI,CAAC,uBAAuB,SAAS,iBAAiB,MAAM,GAAG;AAC7D,UAAM,IAAI,8BAA8B;AAAA,EAC1C;AACA,SAAO,EAAE,OAAO,KAAK,MAAM,OAAO,EAAO;AAC3C;","names":[]}
@@ -0,0 +1,15 @@
1
+ export { f as TokolakuWebhookSignatureError } from './errors-PooMi-0n.cjs';
2
+
3
+ /** Verifikasi header `x-tokolaku-signature` (format `sha256=<hex>`,
4
+ * HMAC-SHA256(secret, rawBody)) — compare timing-safe. rawBody HARUS
5
+ * string mentah persis seperti diterima (bukan hasil re-serialize).
6
+ * Hex format strict: exactly 64 hex chars, case-insensitive. */
7
+ declare function verifyWebhookSignature(rawBody: string, signatureHeader: string | undefined, secret: string): boolean;
8
+ /** Verify + parse. Signature invalid → throw TokolakuWebhookSignatureError.
9
+ * Signature VALID tapi rawBody bukan JSON valid → SyntaxError dari JSON.parse
10
+ * (sengaja tidak dibungkus — itu bug payload, bukan soal keamanan). */
11
+ declare function constructEvent<T = unknown>(rawBody: string, signatureHeader: string | undefined, secret: string): {
12
+ event: T;
13
+ };
14
+
15
+ export { constructEvent, verifyWebhookSignature };
@@ -0,0 +1,15 @@
1
+ export { f as TokolakuWebhookSignatureError } from './errors-PooMi-0n.js';
2
+
3
+ /** Verifikasi header `x-tokolaku-signature` (format `sha256=<hex>`,
4
+ * HMAC-SHA256(secret, rawBody)) — compare timing-safe. rawBody HARUS
5
+ * string mentah persis seperti diterima (bukan hasil re-serialize).
6
+ * Hex format strict: exactly 64 hex chars, case-insensitive. */
7
+ declare function verifyWebhookSignature(rawBody: string, signatureHeader: string | undefined, secret: string): boolean;
8
+ /** Verify + parse. Signature invalid → throw TokolakuWebhookSignatureError.
9
+ * Signature VALID tapi rawBody bukan JSON valid → SyntaxError dari JSON.parse
10
+ * (sengaja tidak dibungkus — itu bug payload, bukan soal keamanan). */
11
+ declare function constructEvent<T = unknown>(rawBody: string, signatureHeader: string | undefined, secret: string): {
12
+ event: T;
13
+ };
14
+
15
+ export { constructEvent, verifyWebhookSignature };
@@ -0,0 +1,26 @@
1
+ import {
2
+ TokolakuWebhookSignatureError
3
+ } from "./chunk-6YQXWMOC.js";
4
+
5
+ // src/webhooks.ts
6
+ import { createHmac, timingSafeEqual } from "crypto";
7
+ function verifyWebhookSignature(rawBody, signatureHeader, secret) {
8
+ if (!signatureHeader?.startsWith("sha256=")) return false;
9
+ const hex = signatureHeader.slice("sha256=".length);
10
+ if (!/^[0-9a-f]{64}$/i.test(hex)) return false;
11
+ const expected = createHmac("sha256", secret).update(rawBody).digest();
12
+ const given = Buffer.from(hex, "hex");
13
+ return timingSafeEqual(given, expected);
14
+ }
15
+ function constructEvent(rawBody, signatureHeader, secret) {
16
+ if (!verifyWebhookSignature(rawBody, signatureHeader, secret)) {
17
+ throw new TokolakuWebhookSignatureError();
18
+ }
19
+ return { event: JSON.parse(rawBody) };
20
+ }
21
+ export {
22
+ TokolakuWebhookSignatureError,
23
+ constructEvent,
24
+ verifyWebhookSignature
25
+ };
26
+ //# sourceMappingURL=webhooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/webhooks.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { TokolakuWebhookSignatureError } from \"./errors.js\";\n\n/** Verifikasi header `x-tokolaku-signature` (format `sha256=<hex>`,\n * HMAC-SHA256(secret, rawBody)) — compare timing-safe. rawBody HARUS\n * string mentah persis seperti diterima (bukan hasil re-serialize).\n * Hex format strict: exactly 64 hex chars, case-insensitive. */\nexport function verifyWebhookSignature(\n rawBody: string,\n signatureHeader: string | undefined,\n secret: string,\n): boolean {\n if (!signatureHeader?.startsWith(\"sha256=\")) return false;\n const hex = signatureHeader.slice(\"sha256=\".length);\n if (!/^[0-9a-f]{64}$/i.test(hex)) return false; // strict: exactly 64 hex chars, reject trailing garbage\n const expected = createHmac(\"sha256\", secret).update(rawBody).digest();\n const given = Buffer.from(hex, \"hex\");\n return timingSafeEqual(given, expected);\n}\n\n/** Verify + parse. Signature invalid → throw TokolakuWebhookSignatureError.\n * Signature VALID tapi rawBody bukan JSON valid → SyntaxError dari JSON.parse\n * (sengaja tidak dibungkus — itu bug payload, bukan soal keamanan). */\nexport function constructEvent<T = unknown>(\n rawBody: string,\n signatureHeader: string | undefined,\n secret: string,\n): { event: T } {\n if (!verifyWebhookSignature(rawBody, signatureHeader, secret)) {\n throw new TokolakuWebhookSignatureError();\n }\n return { event: JSON.parse(rawBody) as T };\n}\n\n// Re-export supaya konsumen bisa import class error dari subpath yang sama\n// dengan fungsi yang melemparnya (sesuai contoh README).\nexport { TokolakuWebhookSignatureError } from \"./errors.js\";\n"],"mappings":";;;;;AAAA,SAAS,YAAY,uBAAuB;AAOrC,SAAS,uBACd,SACA,iBACA,QACS;AACT,MAAI,CAAC,iBAAiB,WAAW,SAAS,EAAG,QAAO;AACpD,QAAM,MAAM,gBAAgB,MAAM,UAAU,MAAM;AAClD,MAAI,CAAC,kBAAkB,KAAK,GAAG,EAAG,QAAO;AACzC,QAAM,WAAW,WAAW,UAAU,MAAM,EAAE,OAAO,OAAO,EAAE,OAAO;AACrE,QAAM,QAAQ,OAAO,KAAK,KAAK,KAAK;AACpC,SAAO,gBAAgB,OAAO,QAAQ;AACxC;AAKO,SAAS,eACd,SACA,iBACA,QACc;AACd,MAAI,CAAC,uBAAuB,SAAS,iBAAiB,MAAM,GAAG;AAC7D,UAAM,IAAI,8BAA8B;AAAA,EAC1C;AACA,SAAO,EAAE,OAAO,KAAK,MAAM,OAAO,EAAO;AAC3C;","names":[]}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "tokolaku-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Official TypeScript SDK for the Tokolaku Engine API — AI bot replies, omnichannel messaging (WhatsApp/Instagram/Messenger), and webhook verification.",
5
+ "keywords": ["tokolaku", "whatsapp", "chatbot", "sdk", "api", "messaging"],
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" },
13
+ "./webhooks": { "types": "./dist/webhooks.d.ts", "import": "./dist/webhooks.js", "require": "./dist/webhooks.cjs" }
14
+ },
15
+ "files": ["dist"],
16
+ "sideEffects": false,
17
+ "engines": { "node": ">=18" },
18
+ "repository": { "type": "git", "url": "git+https://github.com/Rustam335/tokolaku-sdk-js.git" },
19
+ "homepage": "https://tokolaku.id/developers",
20
+ "scripts": {
21
+ "build": "tsup",
22
+ "test": "node --test --import tsx \"test/**/*.test.ts\"",
23
+ "typecheck": "tsc --noEmit",
24
+ "prepublishOnly": "yarn typecheck && yarn test && yarn build"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.0.0",
28
+ "tsup": "^8.0.0",
29
+ "tsx": "^4.19.0",
30
+ "typescript": "^5.6.0"
31
+ }
32
+ }