@squord/sdk 0.0.0-stage → 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
@@ -1,3 +1,545 @@
1
- # Temporary Holding Version
1
+ # @squord/sdk — o SDK oficial do Squord
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/Squord/squord-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/Squord/squord-sdk/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@squord%2Fsdk.svg?label=%40squord%2Fsdk)](https://www.npmjs.com/package/@squord/sdk)
5
+ [![Node LTS](https://img.shields.io/badge/node-%3E%3D20-339933)](https://nodejs.org)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6)](https://www.typescriptlang.org)
7
+ [![Testes](https://img.shields.io/badge/testes-90%2F90-passing-brightgreen)](packages/sdk/test)
8
+ [![Licença: MIT](https://img.shields.io/badge/licen%C3%A7a-MIT-blue)](./LICENSE)
9
+
10
+ O SDK para criar bots do [Squord](https://docs.squord.com.br) em JavaScript e TypeScript:
11
+ REST tipado, gateway em tempo real com reconexão e resume, comandos de texto e de barra,
12
+ botões e menus, uploads, moderação, permissões e testes — sem você implementar nenhum
13
+ WebSocket, retry ou rate limit à mão.
14
+
15
+ ```bash
16
+ npm install @squord/sdk
17
+ ```
18
+
19
+ ```ts
20
+ import { SquordClient } from "@squord/sdk";
21
+
22
+ const client = new SquordClient({ token: process.env.SQUORD_BOT_TOKEN! });
23
+
24
+ client.on("message.created", async (message) => {
25
+ if (message.author.isBot) return;
26
+ if (message.content === "!ping") await message.reply("Pong! 🏓");
27
+ });
28
+
29
+ await client.login();
30
+ ```
31
+
32
+ > **Pré-requisitos**: Node.js 20+ (o gateway usa o `WebSocket` global, estável no Node 22+;
33
+ > no Node 20, passe `ws` na opção `ws` do cliente). Crie o bot e o token em
34
+ > **Configurações → Desenvolvedor** do Squord e instale-o em um Space.
35
+ > Documentação da plataforma: <https://docs.squord.com.br>.
36
+
37
+ ---
38
+
39
+ ## Índice
40
+
41
+ 1. [Começo rápido](#começo-rápido)
42
+ 2. [Instalação e requisitos](#instalação-e-requisitos)
43
+ 3. [Eventos](#eventos)
44
+ 4. [Comandos](#comandos)
45
+ 5. [Interações: botões e menus](#interações-botões-e-menus)
46
+ 6. [Mensagens, embeds e anexos](#mensagens-embeds-e-anexos)
47
+ 7. [Members, cargos e moderação](#members-cargos-e-moderação)
48
+ 8. [Permissões](#permissões)
49
+ 9. [Erros, rate limits e retentativas](#erros-rate-limits-e-retentativas)
50
+ 10. [Gateway: reconexão e resume](#gateway-reconexão-e-resume)
51
+ 11. [Modo REST-only](#modo-rest-only)
52
+ 12. [Testando seu bot](#testando-seu-bot)
53
+ 13. [Observabilidade](#observabilidade)
54
+ 14. [Limites do backend refletidos no SDK](#limites-do-backend-refletidos-no-sdk)
55
+ 15. [Estrutura do repositório](#estrutura-do-repositório)
56
+
57
+ ---
58
+
59
+ ## Começo rápido
60
+
61
+ ```ts
62
+ import { SquordClient } from "@squord/sdk";
63
+
64
+ const client = new SquordClient({
65
+ token: process.env.SQUORD_BOT_TOKEN!, // sqb_<id>.<segredo>
66
+ gracefulShutdown: true, // encerra no SIGINT/SIGTERM
67
+ });
68
+
69
+ // Ciclo de vida
70
+ client.on("ready", (event) => {
71
+ console.log(`Conectado como ${event.user.displayName}`);
72
+ client.setPresence({ status: "IDLE", customStatus: "Tocando no Lounge" });
73
+ });
74
+
75
+ // Mensagens (intent GUILD_MESSAGES; conteúdo exige a intent MESSAGE_CONTENT)
76
+ client.on("message.created", (message) => {
77
+ console.log(`${message.authorDisplayName}: ${message.content}`);
78
+ });
79
+
80
+ // Erros de handler nunca derrubam o processo: chegam aqui.
81
+ client.on("error", (error) => console.error(error));
82
+
83
+ await client.login();
84
+ ```
85
+
86
+ O `login()` valida o token (`GET /bots/@me`), conecta ao gateway e resolve no `ready`.
87
+ Tudo o que vem depois — heartbeat, reconexão com backoff, resume da sessão,
88
+ deduplicação de eventos — é responsabilidade do SDK.
89
+
90
+ ## Instalação e requisitos
91
+
92
+ ```bash
93
+ npm install @squord/sdk # ESM e CommonJS, tipos incluídos
94
+ ```
95
+
96
+ - **Node.js 20+**. O gateway usa o `WebSocket` global (estável no 22+); no Node 20,
97
+ instale `ws` e passe `new SquordClient({ ws })`.
98
+ - **Zero dependências em runtime** — só o que o Node já traz (`fetch`, `WebSocket`).
99
+ - ESM e CJS: `dist/index.js` e `dist/index.cjs`, com `.d.ts` completos.
100
+
101
+ ```jsonc
102
+ // package.json consumer, se estiver no Node 20:
103
+ { "dependencies": { "@squord/sdk": "^0.2.0", "ws": "^8" } }
104
+ ```
105
+
106
+ ```ts
107
+ import { SquordClient } from "@squord/sdk";
108
+ import WebSocket from "ws";
109
+
110
+ const client = new SquordClient({ token, ws: (url) => new WebSocket(url) });
111
+ ```
112
+
113
+ ## Eventos
114
+
115
+ Os eventos seguem os nomes reais do gateway do Squord. O payload chega hidratado
116
+ em estruturas (`Message`, `Member`, `Channel`, `Role`, `Thread`, `Guild`,
117
+ `Interaction`) quando o evento carrega uma entidade; nos demais casos, o payload
118
+ cru da documentação.
119
+
120
+ ```ts
121
+ client.on("message.created", (message) => {}); // Message
122
+ client.on("message.updated", (message) => {}); // Message (já editada)
123
+ client.on("message.deleted", (data) => {}); // { messageId, channelId, guildId }
124
+ client.on("reaction.added", (data) => {}); // { messageId, userId, reaction }
125
+ client.on("member.joined", ({ guildId, member }) => {}); // Member
126
+ client.on("member.updated", ({ guildId, member }) => {});
127
+ client.on("member.left", (data) => {}); // { guildId, userId }
128
+ client.on("member.banned", (data) => {}); // { guildId, userId, reason }
129
+ client.on("guild.updated", (guild) => {}); // Guild (Space)
130
+ client.on("channel.created", (channel) => {}); // Channel
131
+ client.on("role.created", (role) => {}); // Role
132
+ client.on("thread.created", (thread) => {}); // Thread
133
+ client.on("interaction.created", (interaction) => {}); // Interaction
134
+ client.on("application_command.updated", (data) => {});
135
+ ```
136
+
137
+ Ciclo de vida e diagnóstico: `ready`, `resumed`, `reconnecting`, `disconnect`,
138
+ `error`, `warn`, `commandError`, `raw` (frame cru), `unknownEvent`
139
+ (evento futuro ainda não mapeado — compatibilidade sem quebrar).
140
+
141
+ > **Intents** são configuradas por bot no app do Squord (ou `PATCH /bots/{id}`) e
142
+ > **não** vão no identify — o gateway lê as intents do bot ao conectar. Use
143
+ > `Intents`/`intentsValue` para calcular valores. Sem a intent privilegiada
144
+ > `MESSAGE_CONTENT`, o `content` chega vazio (exceto mensagens do próprio bot e
145
+ > menções a ele) — o SDK entrega o evento do mesmo jeito; a regra é da plataforma.
146
+
147
+ ## Comandos
148
+
149
+ O `CommandRegistry` cuida dos três tipos. Ele é opcional: você também pode
150
+ tratar `message.created`/`interaction.created` na mão.
151
+
152
+ ```ts
153
+ import { CommandRegistry, SquordClient, EmbedBuilder, userMention } from "@squord/sdk";
154
+
155
+ const client = new SquordClient({ token });
156
+ const commands = new CommandRegistry(client, { prefix: "!" });
157
+
158
+ // Comandos de texto: prefixo, aliases, args com aspas, cooldown e middleware.
159
+ commands.text({
160
+ name: "banir",
161
+ aliases: ["ban"],
162
+ cooldownMs: 5_000,
163
+ beforeExecute: async (context) => true, // middleware (permissões, logs...)
164
+ async execute(context) {
165
+ const [target, ...rest] = context.args; // args com aspas tratadas
166
+ const motivo = rest.join(" ") || "sem motivo";
167
+ await client.moderation.ban(context.message.guildId!, target, { reason: motivo });
168
+ await context.reply(`${userMention(target)} foi banido.`);
169
+ },
170
+ });
171
+
172
+ // Comandos de barra: definidos aqui e sincronizados no login (PUT em bloco).
173
+ commands.slash({
174
+ name: "eco",
175
+ description: "Repete o que você disse",
176
+ options: [
177
+ { type: "STRING", name: "texto", description: "O texto", required: true },
178
+ ],
179
+ async execute(interaction) {
180
+ await interaction.reply(interaction.getString("texto", true));
181
+ },
182
+ });
183
+
184
+ // Componentes: handler por customId (aceita prefixo com `*`).
185
+ commands.component({
186
+ customId: "confirmar:*",
187
+ async execute(interaction) {
188
+ await interaction.update({ content: "Feito!", components: [] });
189
+ },
190
+ });
191
+
192
+ await client.login(); // slash commands são publicados no ready
193
+ ```
194
+
195
+ - **Registrar é sobrescrever**: o SDK faz o `PUT` da lista completa na partida,
196
+ como recomendado pela plataforma (escopo global; para escopo por Space, use
197
+ `scope: { kind: "guild", guildIds: [...] }` — útil para dev).
198
+ - Respostas de slash: `reply`, `replyEphemeral` (só a pessoa vê), `defer` +
199
+ `followup` (padrão para trabalho demorado) e, em componentes, `update` /
200
+ `acknowledge`.
201
+ - Acesso tipado às opções: `getString`, `getInteger`, `getBoolean`, `getId`.
202
+
203
+ ## Interações: botões e menus
204
+
205
+ ```ts
206
+ import { ActionRowBuilder, Buttons, SelectMenuBuilder } from "@squord/sdk";
207
+
208
+ const row = new ActionRowBuilder()
209
+ .addButton(Buttons.primary("Confirmar").setCustomId("confirmar"))
210
+ .addButton(Buttons.secondary("Cancelar").setCustomId("cancelar"))
211
+ .addButton(Buttons.link("Documentação").setURL("https://docs.squord.com.br"));
212
+
213
+ const menuRow = new ActionRowBuilder().setSelectMenu(
214
+ new SelectMenuBuilder("tema")
215
+ .addOption({ label: "Claro", value: "claro" })
216
+ .addOption({ label: "Escuro", value: "escuro", description: "O padrão" }),
217
+ );
218
+
219
+ await channel.send({ content: "Escolha:", components: [row, menuRow] });
220
+ ```
221
+
222
+ Regras da plataforma, validadas localmente pelo builder: 5 linhas por mensagem,
223
+ 5 botões por linha ou 1 menu sozinho, `customId` único, botão `link` exige `https`.
224
+ Efeitas (`ephemeral: true`) só aceitam botões de link.
225
+
226
+ ## Mensagens, embeds e anexos
227
+
228
+ ```ts
229
+ import { EmbedBuilder } from "@squord/sdk";
230
+
231
+ // Menções seguras por padrão: sem allowedMentions, o SDK envia
232
+ // { users: true, roles: false, everyone: false }.
233
+ await channel.send({
234
+ content: "Atualização de infraestrutura",
235
+ embeds: [
236
+ new EmbedBuilder()
237
+ .setTitle("Status do servidor")
238
+ .setDescription("Todos os serviços operacionais.")
239
+ .setColor(0xf5a623)
240
+ .addFields(
241
+ { name: "API", value: "Online", inline: true },
242
+ { name: "Gateway", value: "Online", inline: true },
243
+ )
244
+ .setTimestamp(),
245
+ ],
246
+ });
247
+
248
+ // Idempotência: cada envio ganha um nonce automático; falhas de rede podem ser
249
+ // repetidas com segurança pelo cliente REST sem duplicar a mensagem.
250
+ const sent = await channel.send("Relatório importante");
251
+ await sent.edit("Relatório importante (atualizado)");
252
+ await sent.react("👍");
253
+ await sent.pin();
254
+
255
+ // Histórico com paginação por cursor (sem carregar tudo em memória):
256
+ for await (const message of paginate(
257
+ (params) => client.messages.list(channel.id, params),
258
+ )) {
259
+ // processa cada mensagem
260
+ }
261
+ ```
262
+
263
+ **Uploads** seguem o fluxo oficial de 3 passos (reserva → `PUT` dos bytes →
264
+ confirmação), abstraído em uma chamada:
265
+
266
+ ```ts
267
+ const upload = await client.uploads.upload({
268
+ guildId: spaceId, // bots sempre informam o Space
269
+ path: "./relatorios/daily.pdf",
270
+ filename: "relatorio.pdf",
271
+ contentType: "application/pdf",
272
+ });
273
+
274
+ await channel.send({ content: "Segue o relatório.", attachmentIds: [upload.id] });
275
+ ```
276
+
277
+ O `PUT` dos bytes vai para a URL assinada do storage **sem** o header do bot,
278
+ repetindo os headers do alvo exatamente; a confirmação é idempotente.
279
+
280
+ ## Members, cargos e moderação
281
+
282
+ ```ts
283
+ const guild = await client.guilds.fetch(spaceId); // Space, com self.permissions
284
+ const member = await client.members.fetch(spaceId, userId);
285
+ const page = await client.members.list(spaceId, { limit: 50, roleId }); // cursor
286
+ const roles = await client.roles.list(spaceId);
287
+
288
+ await member.addRole(roleId);
289
+ await member.setNickname("Novo apelido");
290
+ await member.timeout(new Date(Date.now() + 60_000)); // 1 min de castigo
291
+ await member.removeTimeout();
292
+ await member.kick("motivo");
293
+ await client.moderation.ban(spaceId, userId, { reason: "spam", deleteMessageSeconds: 3600 });
294
+ await client.moderation.unban(spaceId, userId);
295
+ ```
296
+
297
+ A hierarquia é respeitada pelo backend (`HIERARCHY_BLOCKED`, `ROLE_ABOVE_ACTOR`,
298
+ `OWNER_PROTECTED`, `ROLE_MANAGED` chegam como `PermissionError`/`SquordAPIError`
299
+ com o `code` estável para você decidir).
300
+
301
+ ## Permissões
302
+
303
+ Permissões são **strings decimais** de 64 bits: calcule com `BigInt`.
304
+
305
+ ```ts
306
+ import { Permissions, permissionsToString } from "@squord/sdk";
307
+
308
+ const bits = Permissions.VIEW_CHANNEL | Permissions.SEND_MESSAGES | Permissions.EMBED_LINKS;
309
+ console.log(permissionsToString(bits)); // "139713"
310
+
311
+ // O que o bot pode neste Space (bloco self do GET /guilds/{id}):
312
+ const guild = await client.guilds.fetch(spaceId);
313
+ guild.botHas(Permissions.BAN_MEMBERS);
314
+
315
+ // Resolução local com overwrites de canal (backend é a autoridade final):
316
+ const mine = await client.myPermissionsIn(channelId);
317
+ const canPin = await client.hasPermissionsIn(channelId, Permissions.PIN_MESSAGES);
318
+ ```
319
+
320
+ A utilidade `resolveChannelPermissions` implementa o algoritmo oficial
321
+ (@everyone → cargos → overwrites de cargo → overwrite de membro).
322
+
323
+ ## Erros, rate limits e retentativas
324
+
325
+ O cliente REST aplica a política recomendada pela documentação:
326
+
327
+ | Situação | Comportamento |
328
+ |---|---|
329
+ | `429` | espera `details.retryAfter` e repete (até esgotar `maxRetries`) |
330
+ | `5xx`, falha de rede | backoff exponencial **só em operações idempotentes ou com `nonce`** |
331
+ | `409 COMMANDS_CONFLICT`/`THREAD_CHANGED` | nova tentativa (a doc manda repetir) |
332
+ | demais `4xx` | falha imediata, sem loop |
333
+ | timeout | `RequestTimeoutError` tipado |
334
+
335
+ **Regra de ouro respeitada**: o SDK nunca repete um POST cujo efeito é incerto.
336
+ Envio de mensagens usa `nonce` automático (idempotência da plataforma), então pode
337
+ repetir; callbacks de interação não são repetidos — um `409 INTERACTION_ALREADY_ACKNOWLEDGED`
338
+ após repetição manual é tratado como sucesso pela utilidade `reply`.
339
+
340
+ Além do 429 reativo, o SDK observa os headers `X-RateLimit-*` (bloqueia a rota até
341
+ a janela renovar) e aplica um teto global conservador de 45 req/s (o oficial é 50).
342
+
343
+ ```ts
344
+ import { RateLimitError, PermissionError, SquordAPIError } from "@squord/sdk";
345
+
346
+ try {
347
+ await channel.send("oi");
348
+ } catch (error) {
349
+ if (error instanceof PermissionError) {
350
+ console.log("Falta a permissão:", error.missingPermission);
351
+ } else if (error instanceof SquordAPIError) {
352
+ console.log("code:", error.code, "requestId:", error.requestId);
353
+ }
354
+ }
355
+ ```
356
+
357
+ Hierarquia: `SquordError` → `SquordAPIError` (`status`, `code`, `requestId`,
358
+ `details`) → `AuthenticationError`/`InvalidTokenError`, `PermissionError`,
359
+ `NotFoundError`, `ValidationError` (`issues`), `RateLimitError` (`retryAfter`,
360
+ `bucket`, `exhaustedRetries`), `NetworkError`, `RequestTimeoutError`,
361
+ `GatewayError` (`closeCode`, `reason`).
362
+
363
+ ## Gateway: reconexão e resume
364
+
365
+ O SDK implementa o protocolo completo: `hello` → `identify` (15 s) → `ready`;
366
+ heartbeat no intervalo anunciado (com jitter no primeiro); `heartbeat.ack` com
367
+ medida de latência; conexão zumbi detectada e fechada; reconexão com backoff
368
+ exponencial e jitter (1 s → 60 s); `resume` com a última sequência (replay dos
369
+ eventos perdidos + `resumed`); `invalid_session` caindo para `identify` na mesma
370
+ conexão; deduplicação por `s`; códigos fatais (4004/4013/4014) sem retry.
371
+
372
+ ```ts
373
+ client.on("reconnecting", ({ attempt, delayMs, closeCode }) => {});
374
+ client.on("resumed", ({ replayed, sequence }) => {
375
+ console.log(`${replayed} eventos recuperados na retomada`);
376
+ });
377
+ client.on("disconnect", ({ code, reason, fatal }) => {});
378
+
379
+ client.ping; // latência do último heartbeat
380
+ client.gateway?.sequence; // última sequência processada
381
+ client.gateway?.session; // id da sessão atual
382
+ client.reconnect(); // força reconexão (tenta resume)
383
+ ```
384
+
385
+ O que o backend não garante, o SDK não inventa: se o histórico de replay não
386
+ cobrir a sequência, o servidor recusa o resume (`invalid_session`) e uma nova
387
+ sessão começa do zero — eventos do período podem ter sido perdidos; para
388
+ reconciliar, use as consultas REST (histórico de mensagens, membros).
389
+
390
+ ## Modo REST-only
391
+
392
+ Para tarefas agendadas, CI e integrações que só publicam:
393
+
394
+ ```ts
395
+ const client = new SquordClient({ token, restOnly: true });
396
+ await client.login(); // valida o token; nenhum WebSocket é aberto
397
+ await client.messages.send(channelId, "deploy concluído ✅");
398
+ await client.destroy();
399
+ ```
400
+
401
+ ## Testando seu bot
402
+
403
+ O subpath `@squord/sdk/testing` traz um gateway simulado (que fala o protocolo
404
+ real) e um REST em memória:
405
+
406
+ ```ts
407
+ import { SquordClient } from "@squord/sdk";
408
+ import { createMockGateway, createMockRest, fixtures } from "@squord/sdk/testing";
409
+
410
+ const rest = createMockRest();
411
+ rest.get("/bots/@me", () => ({ status: 200, body: fixtures.bot() }));
412
+ rest.post("/channels/:id/messages", () => ({
413
+ status: 201,
414
+ body: fixtures.message({ content: "Pong!" }),
415
+ }));
416
+
417
+ const gateway = createMockGateway({ heartbeatIntervalMs: 100 });
418
+ const client = new SquordClient({
419
+ token: "sqb_1.segredo",
420
+ apiBaseURL: "https://mock.test",
421
+ fetchImpl: rest.fetch,
422
+ ws: gateway.wsFactory,
423
+ });
424
+
425
+ client.on("message.created", (message) => {
426
+ if (message.content === "!ping") void message.reply("Pong!");
427
+ });
428
+
429
+ await client.login();
430
+ gateway.emitEvent("message.created", { message: fixtures.message({ content: "!ping" }) });
431
+
432
+ // Treine queda e retomada sem rede:
433
+ gateway.closeAll(4009, "session timeout"); // o SDK resume a sessão
434
+ gateway.requestReconnect("gateway shutting down"); // frame reconnect + 4007
435
+ ```
436
+
437
+ O `MockGateway` aceita `refuseResumeOnce` (treina `invalid_session`) e
438
+ `suppressHeartbeatAck` (treina detecção de zumbi); o `MockRest` registra todas as
439
+ chamadas para asserções (`callLog`, `lastCall`) e pode simular 429/5xx/timeout.
440
+
441
+ ## Observabilidade
442
+
443
+ ```ts
444
+ const client = new SquordClient({
445
+ token,
446
+ logLevel: "debug", // ou export SQUORD_LOG=debug / SQUORD_DEBUG=1
447
+ logger: customLogger, // Logger customizável { debug, info, warn, error, child }
448
+ });
449
+ ```
450
+
451
+ Logs são estruturados (JSON), com **redação automática de tokens** (`sqb_...`
452
+ e `Authorization` viram `[REDACTED]`) e sem conteúdo de mensagens por padrão —
453
+ só metadados operacionais (requestId, buckets, heartbeats, tentativas).
454
+
455
+ ## Limites do backend refletidos no SDK
456
+
457
+ O SDK valida localmente os limites reais antes de gastar uma chamada:
458
+ embeds (10/mensagem, 25 campos, 6000 caracteres somados, títulos 256...),
459
+ componentes (5 linhas, 5 botões/linha, `customId` 100), `content` 4000,
460
+ `attachmentIds` 10, upload 500 MB, nonce 64, nome de comando 32/regex.
461
+ Erros de validação local chegam como `SquordTypeError`; os da API, como
462
+ `ValidationError` com a lista de `issues` (`path`, `code`, `params`).
463
+
464
+ Recursos que a API **não** expõe a bots não existem no SDK: DMs, presença de
465
+ outros membros, listagem de bans, voz (o fluxo E2EE de bots de voz fica fora
466
+ desta versão). Não há mecanismo para contornar permissões.
467
+
468
+ ## Estrutura do repositório
469
+
470
+ ```
471
+ squord-sdk/
472
+ ├── packages/sdk/ # @squord/sdk (o único pacote publicado)
473
+ │ ├── src/
474
+ │ │ ├── client/ # SquordClient, opções, eventos tipados
475
+ │ │ ├── rest/ # REST, retry, rate limits, paginação
476
+ │ │ ├── gateway/ # WebSocket, heartbeat, resume, backoff
477
+ │ │ ├── managers/ # mensagens, canais, Spaces, membros, cargos,
478
+ │ │ │ # moderação, uploads, interações, comandos, bot
479
+ │ │ ├── structures/ # Message, Channel, Member, Role, User, Guild...
480
+ │ │ ├── builders/ # EmbedBuilder, botões, menus, payload
481
+ │ │ ├── commands/ # CommandRegistry (texto, slash, componentes)
482
+ │ │ ├── cache/ # cache TTL opcional
483
+ │ │ ├── errors/ # hierarquia de erros
484
+ │ │ ├── types/ # tipos dos payloads reais (API + gateway)
485
+ │ │ └── testing/ # @squord/sdk/testing: mocks e fixtures
486
+ │ └── test/ # 90 testes (unit + integração)
487
+ ├── examples/ # ping-bot, commands-bot, welcome-bot, reports
488
+ └── docs/ # guias
489
+ ```
490
+
491
+ ## Desenvolvimento
492
+
493
+ ```bash
494
+ pnpm install
495
+ pnpm build # tsup: ESM + CJS + .d.ts
496
+ pnpm test # vitest: 90 testes com gateway simulado
497
+ pnpm typecheck # tsc --noEmit (strict)
498
+ pnpm pack # gera o tarball (squord-sdk-<versão>.tgz) para instalar em outro projeto
499
+ ```
500
+
501
+ O `usage-demo/` consome o pacote como um projeto externo (via `file:`) e roda
502
+ um smoke contra os mocks — útil para conferir o pacote distribuído:
503
+
504
+ ```bash
505
+ pnpm build
506
+ cd usage-demo && npm install && npm start
507
+ ```
508
+
509
+ ## Publicar versões no npm
510
+
511
+ O pacote `@squord/sdk` é publicado **automaticamente pelo workflow
512
+ [Publicar no npm](.github/workflows/publish.yml)** quando um Release do GitHub
513
+ é criado — com typecheck, testes, build e *provenance*.
514
+
515
+ Configuração única (secret `NPM_TOKEN`): crie um token de automação no npmjs.com
516
+ (Access Tokens → **Automation**, com permissão de escrever no pacote
517
+ `@squord/sdk`) e:
518
+
519
+ ```bash
520
+ gh secret set NPM_TOKEN --repo Squord/squord-sdk
521
+ ```
522
+
523
+ Corte uma versão:
524
+
525
+ 1. Atualize `packages/sdk/package.json` e o [CHANGELOG.md](./CHANGELOG.md)
526
+ 2. `git tag v0.x.y && git push --tags`
527
+ 3. Crie o Release do GitHub — o workflow publica no npm
528
+
529
+ Enquanto o pacote não está no npm, consuma pelo tarball (gerado a partir do
530
+ repositório):
531
+
532
+ ```bash
533
+ git clone https://github.com/Squord/squord-sdk && cd squord-sdk
534
+ pnpm install && pnpm pack # gera packages/sdk/squord-sdk-0.2.0.tgz
535
+ npm install /caminho/para/squord-sdk-0.2.0.tgz
536
+ ```
537
+
538
+ ---
539
+
540
+ **Documentação da plataforma**: <https://docs.squord.com.br> ·
541
+ **Referência REST (OpenAPI)**: <https://docs.squord.com.br/api> ·
542
+ **Protocolo do gateway**: <https://docs.squord.com.br/gateway/visao-geral> ·
543
+ **Issues**: <https://github.com/Squord/squord-sdk/issues>
544
+
545
+ MIT © Squord — veja [LICENSE](./LICENSE).