@letsbot/sdk 0.0.0-stage → 0.5.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 +21 -0
- package/README.md +303 -2
- package/dist/index.cjs +1260 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +19910 -0
- package/dist/index.d.ts +19910 -0
- package/dist/index.js +1227 -0
- package/dist/index.js.map +1 -0
- package/openapi/erp-v1.yaml +3090 -0
- package/openapi/openapi.yaml +16516 -0
- package/package.json +66 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LetsBot
|
|
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
CHANGED
|
@@ -1,3 +1,304 @@
|
|
|
1
|
-
#
|
|
1
|
+
# LetsBot TypeScript SDK (`@letsbot/sdk`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official TypeScript/Node client for the LetsBot developer API (`https://letsbot.net/api/v1`).
|
|
4
|
+
|
|
5
|
+
- Zero runtime dependencies — global `fetch` (Node 18+), `node:crypto`.
|
|
6
|
+
- ESM and CommonJS builds, full types generated from the OpenAPI spec.
|
|
7
|
+
- Retries with exponential backoff and full jitter, honouring `Retry-After`.
|
|
8
|
+
- An `Idempotency-Key` on every send, so a retried send is delivered at most once.
|
|
9
|
+
- A client-side token bucket (default 1000 requests/minute, the API's per-channel limit).
|
|
10
|
+
- One error type, `LetsBotApiError`, for every refusal — including the ones the API sends with HTTP 200/202.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @letsbot/sdk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
License: MIT.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { LetsBot } from '@letsbot/sdk';
|
|
24
|
+
|
|
25
|
+
const letsbot = new LetsBot({ apiKey: process.env.LETSBOT_API_KEY! }); // the channel's lb_api_key
|
|
26
|
+
|
|
27
|
+
const sent = await letsbot.messages.send({ phone: '966501234567', body: 'Your order has shipped.' });
|
|
28
|
+
console.log(sent.success);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The API key is the channel's `lb_api_key` (panel → API → Authentication data). It is **not** the Zapier key
|
|
32
|
+
(`lbz_…`). The workspace needs an active subscription with the API Access add-on, or an active trial.
|
|
33
|
+
|
|
34
|
+
### Resources
|
|
35
|
+
|
|
36
|
+
Every endpoint in the spec is available, grouped like the API:
|
|
37
|
+
|
|
38
|
+
| Resource | Examples |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `messages` | `send`, `reply`, `react`, `forward`, `sendList`, `sendDisappearing`, `get`, `fetch`, `delete` |
|
|
41
|
+
| `media` | `sendImage`, `sendFile`, `sendVideo`, `sendAudio`, `sendVoiceNote`, `sendLocation`, `sendContact`, `shareCatalog` |
|
|
42
|
+
| `interactive` | `sendButtons`, `sendButtonsWithImage`, `sendButtonsTemplate…`, `sendCarousel` |
|
|
43
|
+
| `templates` | `send`, `sendSimple`, `sendWithImage`, `sendWithDocument`, `sendWithVideo` (official channels only) |
|
|
44
|
+
| `chats` | `list`, `conversation`, `read`, `archive`, `mute`, `pin`, `typing`, `checkPhone`, … |
|
|
45
|
+
| `contacts` | `list`, `get`, `listLabels`, `addLabel`, `listModerators`, `assignModerator` |
|
|
46
|
+
| `groups`, `invites` | `create`, `list`, `addParticipants`, `rename`, `getCode`, `sendLink`, … |
|
|
47
|
+
| `user`, `instance`, `session`, `products` | profile, presence, status, QR, catalog |
|
|
48
|
+
| `archive` | `list` (one page), `iterate` (every page) |
|
|
49
|
+
|
|
50
|
+
### CRM (`LetsBotCrm`)
|
|
51
|
+
|
|
52
|
+
The CRM API lives on the workspace's own domain (`https://{workspace}/api/v1/crm`) and takes a workspace API key
|
|
53
|
+
(`lbz_…`, Panel → Zapier → API keys) with the **crm** scope ticked. The key acts as the workspace owner.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { LetsBotCrm, verifyCrmWebhookSignature } from '@letsbot/sdk';
|
|
57
|
+
|
|
58
|
+
const crm = new LetsBotCrm({ apiKey: process.env.LETSBOT_CRM_KEY!, workspace: 'mystore.letsbot.net' });
|
|
59
|
+
|
|
60
|
+
const { data: contact } = await crm.contacts.create({ phone: '966501234567', name: 'Laila' });
|
|
61
|
+
const { data: deal } = await crm.deals.create({ title: 'Gift boxes', amount: 8750, contact_id: contact.id });
|
|
62
|
+
await crm.deals.moveStage({ id: deal.id, stage_id: 4 });
|
|
63
|
+
await crm.deals.addNote({ id: deal.id, body: 'Asked for a discount.' });
|
|
64
|
+
|
|
65
|
+
for await (const won of crm.iterate('crmListDeals', { status: 'won', sort: 'updated_at', dir: 'desc' })) {
|
|
66
|
+
console.log(won.title, won.amount);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| Resource | Methods |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `contacts` | `list`, `schema`, `search`, `get`, `create`, `update`, `addNote` |
|
|
73
|
+
| `companies` | `list`, `schema`, `search`, `get`, `create`, `update`, `delete`, `linkContact`, `unlinkContact`, `addNote` |
|
|
74
|
+
| `deals` | `list`, `pipelines`, `lostReasons`, `search`, `get`, `create`, `update`, `delete`, `moveStage`, `markWon`, `markLost`, `addNote` |
|
|
75
|
+
| `tasks` | `list`, `get`, `create`, `update`, `delete`, `complete` |
|
|
76
|
+
| `activities`, `quotes`, `products` | `list`, `get`, `create` (+ `update`, `delete`, `schema` for quotes and products) |
|
|
77
|
+
| `customObjects` | `list`, `schema` (custom objects — 404 until the workspace switches them on) |
|
|
78
|
+
| `customRecords` | `list`, `search`, `get`, `create`, `update`, `delete` — pass the object `key`, e.g. `co_projects`; values by field key |
|
|
79
|
+
| `hooks` | `list`, `events`, `sample`, `subscribe`, `unsubscribe` (`CRM_WEBHOOK_EVENTS` lists all 24 events) |
|
|
80
|
+
|
|
81
|
+
Every method takes one object: path parameters (`id`, `contactId`) plus the query (reads) or the JSON body (writes).
|
|
82
|
+
Lists take `q`, `filters` (lists-engine JSON — see `…schema()`), `view`, `sort`, `dir`, `page`, `per_page` and answer
|
|
83
|
+
`{success, data, meta}`. Errors are `LetsBotApiError` with the envelope's `code` (`missing_scope`, `not_found`,
|
|
84
|
+
`validation_failed`, `already_exists` — a duplicate phone, with the existing contact in `error.body.data`,
|
|
85
|
+
`possible_duplicate` — a likely duplicate, read it with `crmPossibleDuplicate(error)` or repeat with
|
|
86
|
+
`allow_duplicate: true`, …). Reads
|
|
87
|
+
are retried on network errors and 5xx; writes are not. The client-side limit defaults to 120 requests/minute, the
|
|
88
|
+
API's per-key limit.
|
|
89
|
+
|
|
90
|
+
Webhook deliveries (panel → CRM → Settings → API & webhooks, or `crm.hooks.subscribe`) carry
|
|
91
|
+
`X-LetsBot-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.<raw body>">`:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const valid = verifyCrmWebhookSignature(rawBody, req.headers['x-letsbot-signature'], subscriptionSecret);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`invoice.created`, `form.submitted`, `deal.assigned` and `health.at_risk` deliveries carry an extra block next to `data`:
|
|
98
|
+
`invoice`, `form` (source, form, submission, answers), `assignment` (new / previous owner) and `health` (score,
|
|
99
|
+
previous, reasons; `object` says contact or company).
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const record = await crm.customRecords.create({ key: 'co_projects', values: { name: 'Riyadh fit-out', budget: 120000 } });
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await crm.contacts.create({ phone: '966501234567', email: 'laila@example.com' });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const dup = crmPossibleDuplicate(error);
|
|
108
|
+
if (!dup) throw error;
|
|
109
|
+
await crm.contacts.update({ id: dup.existingId, name: 'Laila' });
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Any operation can also be called by its `operationId` from the spec:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
await letsbot.call('sendButtonsAlias', { phone: '966501234567', body: 'Confirm?', title: 'Order', footer: 'LetsBot', buttons: [{ id: 'yes', title: 'Yes' }] });
|
|
117
|
+
const { data, status, headers, replayed } = await letsbot.callWithResponse('getChatList');
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Business / ERP (`LetsBotErp`)
|
|
121
|
+
|
|
122
|
+
The Business API lives on the workspace domain like the CRM API and needs a workspace key with the **erp** scope
|
|
123
|
+
ticked. Each area (inventory, sales, purchasing, expenses, accounting) answers 404 until the workspace switches it on.
|
|
124
|
+
Creates return drafts (or take the one step you call); nothing messages a customer.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { LetsBotErp, verifyErpWebhookSignature } from '@letsbot/sdk';
|
|
128
|
+
|
|
129
|
+
const erp = new LetsBotErp({ apiKey: process.env.LETSBOT_KEY!, workspace: 'mystore.letsbot.net' });
|
|
130
|
+
|
|
131
|
+
const { data: order } = await erp.salesOrders.create({ customer_id: 42, lines: [{ sku: 'RY-OUD-50', qty: 2 }] });
|
|
132
|
+
await erp.salesOrders.confirm({ id: order!.id! });
|
|
133
|
+
|
|
134
|
+
const { data: invoice } = await erp.invoices.create({ order_id: order!.id! });
|
|
135
|
+
await erp.invoices.issue({ id: invoice!.id! });
|
|
136
|
+
const { data: link } = await erp.invoices.pdfLink({ id: invoice!.id! }); // link.pdf_url once shareable
|
|
137
|
+
|
|
138
|
+
await erp.payments.record({ customer_id: 42, amount: 1150, method: 'bank_transfer', reference: 'TRX-1' });
|
|
139
|
+
|
|
140
|
+
for await (const level of erp.iterate('erpListStockLevels', { warehouse_id: 1 })) console.log(level.sku, level.available);
|
|
141
|
+
|
|
142
|
+
// Webhooks: invoice.issued, invoice.paid, invoice.cleared, invoice.rejected, credit_note.issued, payment.received,
|
|
143
|
+
// order.confirmed, stock.low, po.confirmed_by_supplier, expense.posted
|
|
144
|
+
const { data: hook } = await erp.hooks.subscribe({ url: 'https://example.com/letsbot', events: ['invoice.paid'] });
|
|
145
|
+
// on receipt: verifyErpWebhookSignature(rawBody, req.headers['x-letsbot-signature'], hook.secret)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Every create (`salesOrders.create`, `invoices.create`, `invoices.createCreditNote`, `payments.record`,
|
|
149
|
+
`purchaseOrders.create`, `expenses.create`) sends an `Idempotency-Key` — pass `{ idempotencyKey: 'order-123' }` to
|
|
150
|
+
tie it to your own id — so a retry after a timeout never makes a second document.
|
|
151
|
+
|
|
152
|
+
### Templates
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
await letsbot.templates.send({
|
|
156
|
+
phone: '966501234567',
|
|
157
|
+
template_name: 'order_confirmation',
|
|
158
|
+
template_language: 'ar',
|
|
159
|
+
components: { body: ['Ahmed', 'ORD-12345', '150 SAR'] },
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Message archive
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
for await (const message of letsbot.archive.iterate({ date_from: '2026-09-01', date_to: '2026-09-07', timezone: 'Asia/Riyadh' })) {
|
|
167
|
+
console.log(message.message_date, message.from_me, message.body);
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Errors
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { LetsBotApiError, LetsBotConnectionError } from '@letsbot/sdk';
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
await letsbot.messages.send({ phone: '966501234567', body: 'Hi' });
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error instanceof LetsBotApiError) {
|
|
180
|
+
error.status; // HTTP status — can be 200 or 202, see below
|
|
181
|
+
error.body; // parsed JSON body
|
|
182
|
+
error.code; // error_code / code / error from the body, e.g. API_ADDON_REQUIRED
|
|
183
|
+
error.requestId; // the support reference on a 500 (API-XXXXXXXXXX), else undefined
|
|
184
|
+
} else if (error instanceof LetsBotConnectionError) {
|
|
185
|
+
// no HTTP response at all (DNS, reset, timeout), after retries
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**The HTTP status alone does not tell you whether a request worked.** Most endpoints answer a validation failure
|
|
191
|
+
with **HTTP 202** and `{"Status": 0, "Message": "..."}`, and pass the connection gateway's answer through with
|
|
192
|
+
HTTP 200 even when it says `{"success": false}`. The SDK treats both as failures and throws `LetsBotApiError`
|
|
193
|
+
(same rule the server uses to decide whether a send counted). Pass `throwOnLogicalFailure: false` to get those
|
|
194
|
+
bodies back instead.
|
|
195
|
+
|
|
196
|
+
The API has no request-id header; `requestId` is only filled from the `reference` of a 500.
|
|
197
|
+
|
|
198
|
+
## Retries
|
|
199
|
+
|
|
200
|
+
- Exponential backoff with full jitter: base 500 ms, at most 3 retries (`retry: { maxRetries, baseDelayMs, maxDelayMs }`).
|
|
201
|
+
- `Retry-After` is honoured. A `Retry-After` longer than `retry.maxRetryAfterMs` (default 60 s — the workspace's daily
|
|
202
|
+
send limit can say hours) is not waited out: the error is thrown with `error.retryAfter` set.
|
|
203
|
+
- **429** is retried on every endpoint — it means nothing was done.
|
|
204
|
+
- **Network errors and 5xx** are retried only when repeating is safe: on GET requests, and on **send** endpoints
|
|
205
|
+
that carry an `Idempotency-Key`. Any other POST/DELETE (archive a chat, block a user, create a group, …) is never
|
|
206
|
+
repeated after it may have reached the server.
|
|
207
|
+
- **409 `idempotency_request_in_progress`** is retried with the same key.
|
|
208
|
+
- **409 `idempotency_outcome_unknown`** is never retried (`error.isOutcomeUnknown === true`): an earlier attempt with
|
|
209
|
+
that key died before its result was known. Check the message before sending again with a new key.
|
|
210
|
+
Expect this after a 5xx on a send: the server marks the key "outcome unknown" on any 5xx, so the retry under the
|
|
211
|
+
same key comes back as this 409 rather than risking a duplicate.
|
|
212
|
+
|
|
213
|
+
## Idempotency
|
|
214
|
+
|
|
215
|
+
Every **send** endpoint (the 35 operations marked `x-letsbot-send` in the spec — `messages.send`, `templates.*`,
|
|
216
|
+
`media.send*`, `interactive.*`, `messages.forward`, `media.shareCatalog`, `invites.sendLink`, …) gets an
|
|
217
|
+
`Idempotency-Key` header: a fresh UUID per call by default, reused on every retry of that call. The server
|
|
218
|
+
keeps the result for 24 hours and replays it for the same key (`callWithResponse(...).replayed === true`).
|
|
219
|
+
|
|
220
|
+
- Pass your own key to stay idempotent across process restarts: `{ idempotencyKey: 'order-12345' }`.
|
|
221
|
+
- Pass `{ idempotencyKey: false }` to send without one — the SDK will then not retry that send after a network
|
|
222
|
+
error or 5xx.
|
|
223
|
+
- The server honours the header on send endpoints only, so the SDK does not send it anywhere else; a key given on
|
|
224
|
+
another endpoint is ignored and does not enable retries.
|
|
225
|
+
|
|
226
|
+
## Rate limit
|
|
227
|
+
|
|
228
|
+
The client waits for a token before every attempt (default 1000 per minute, the API's per-channel limit). Use
|
|
229
|
+
one client per API key so the bucket sees all of that key's traffic. `rateLimit: { requestsPerMinute: 300 }` to
|
|
230
|
+
go slower, `rateLimit: false` to turn it off.
|
|
231
|
+
|
|
232
|
+
## Webhooks (experimental — read this)
|
|
233
|
+
|
|
234
|
+
API webhooks (configured in the panel, API → Webhooks) are POSTed as `{"event": "<type>", "data": {...}}` with a
|
|
235
|
+
`Signature` header: the hex HMAC-SHA256 of the raw body. **The key is the literal string `Letsbot`, the same for
|
|
236
|
+
every LetsBot workspace**, and it is printed in this SDK. Anyone can therefore produce a valid signature:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { experimental_verifyWebhookSignature } from '@letsbot/sdk';
|
|
240
|
+
|
|
241
|
+
experimental_verifyWebhookSignature(rawBody, req.headers['signature']); // integrity only — NOT authentication
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
A `true` does **not** prove the request came from LetsBot. To authenticate webhooks, add a header with a secret of
|
|
245
|
+
your own to the webhook in the panel and compare it on receipt (see `examples/webhook-receiver.ts`). Verify the
|
|
246
|
+
raw bytes you received, before `JSON.parse`: the server's JSON escapes `/` and non-ASCII characters, so a
|
|
247
|
+
re-serialised body will not match.
|
|
248
|
+
|
|
249
|
+
## In-App Chat identity
|
|
250
|
+
|
|
251
|
+
LetsBot In-App Chat (the mobile SDKs) links a conversation to your signed-in user when the app calls
|
|
252
|
+
`identify(identityToken)`. The identity token is a short-lived JWT (HS256) that **your backend** signs with the app's
|
|
253
|
+
**Identity Secret** (panel → In-App Chat → your app). Never put the secret in the mobile app.
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { createIdentityToken } from '@letsbot/sdk';
|
|
257
|
+
|
|
258
|
+
// e.g. GET /api/letsbot/identity-token — behind your own authentication
|
|
259
|
+
app.get('/api/letsbot/identity-token', requireUser, (req, res) => {
|
|
260
|
+
const token = createIdentityToken({
|
|
261
|
+
userId: req.user.id, // sub: stable id, ≤ 191 characters
|
|
262
|
+
secret: process.env.LETSBOT_IDENTITY_SECRET!,
|
|
263
|
+
expiresIn: '24h', // default; seconds or '90s' | '15m' | '24h' | '7d', max 7 days
|
|
264
|
+
name: req.user.name, // optional; claims win over fields the app sends
|
|
265
|
+
email: req.user.email, // optional
|
|
266
|
+
phone: req.user.phone, // optional, international format
|
|
267
|
+
});
|
|
268
|
+
res.set('Cache-Control', 'no-store').json({ token });
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Claims: `sub`, `iat`, `exp` (+ optional `name`, `email`, `phone`). LetsBot allows 60 s of clock skew and rejects a
|
|
273
|
+
token whose `exp` is more than 7 days after `iat`. An expired token makes `identify` fail with `identity_expired` —
|
|
274
|
+
the app should fetch a fresh one from this endpoint and retry; a bad signature gives `identity_invalid`.
|
|
275
|
+
`createIdentityToken` throws `TypeError` / `RangeError` on an empty secret, an empty or over-long `userId`, or a
|
|
276
|
+
lifetime over 7 days. Do not log the token or the secret. Examples for PHP, Python, Go, Ruby and .NET:
|
|
277
|
+
`https://github.com/Lets-Bot/letsbot-identity-examples`.
|
|
278
|
+
|
|
279
|
+
## Development
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
npm install
|
|
283
|
+
npm run build # tsup → dist/ (ESM + CJS + .d.ts)
|
|
284
|
+
npx tsc --noEmit -p . # typecheck src, test, examples
|
|
285
|
+
npx vitest run test/retry.test.ts # one spec file at a time
|
|
286
|
+
npm run check:drift # fails when anything generated is stale
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The spec lives in the LetsBot workspace at `packages/api/openapi/openapi.yaml`; this repository keeps a copy in
|
|
290
|
+
`openapi/openapi.yaml` so it builds on its own. Inside the workspace:
|
|
291
|
+
|
|
292
|
+
- `npm run spec:sync` — copy the workspace spec here and regenerate `src/generated/` (types via
|
|
293
|
+
`openapi-typescript`, the operation table via `scripts/gen-operations.mjs`).
|
|
294
|
+
- `npm run spec:json` — regenerate `packages/api/openapi/openapi.json` from the YAML (letsbot's
|
|
295
|
+
`OpenApiRouteParityTest` reads the JSON twin).
|
|
296
|
+
- `npm run check:drift` — vendored copy == workspace spec, generated types and operation table are fresh, and the
|
|
297
|
+
JSON twin matches the YAML. Outside the workspace the two workspace checks are skipped. Point
|
|
298
|
+
`LETSBOT_API_SPEC` at another copy of the spec to check against it.
|
|
299
|
+
|
|
300
|
+
## Publishing
|
|
301
|
+
|
|
302
|
+
Published to npm as `@letsbot/sdk` (public, MIT). Bump `version` in `package.json` and `SDK_VERSION` in
|
|
303
|
+
`src/client.ts` together, move the changelog entries under the new version, then `npm publish --access public`
|
|
304
|
+
(`prepublishOnly` builds and runs the tests).
|