@stone-wpp/ofc 1.0.5 → 1.0.6

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
@@ -18,7 +18,7 @@
18
18
 
19
19
  <br>
20
20
 
21
- <a href="https://github.com/dylan-Modz/stone-wpp-ofc"><img src="https://img.shields.io/badge/VERS%C3%83O-1.0.5-00BFFF?style=for-the-badge&logo=github&logoColor=white&labelColor=111820" alt="Versão 1.0.5 do código no GitHub"></a>
21
+ <a href="https://github.com/dylan-Modz/stone-wpp-ofc"><img src="https://img.shields.io/badge/VERS%C3%83O-1.0.6-00BFFF?style=for-the-badge&logo=github&logoColor=white&labelColor=111820" alt="Versão 1.0.6 do código no GitHub"></a>
22
22
  <img src="https://img.shields.io/badge/NODE.JS-20%2B-111820?style=for-the-badge&logo=node.js&logoColor=00BFFF" alt="Node.js 20 ou superior">
23
23
  <img src="https://img.shields.io/badge/LICENSE-MIT-00BFFF?style=for-the-badge&labelColor=111820" alt="Licença MIT">
24
24
 
@@ -64,6 +64,80 @@ A documentação principal da **Stone WPP** está disponível no nosso site ofic
64
64
 
65
65
  ---
66
66
 
67
+ ## 🛡️ Proteção nativa dos grupos — v1.0.6
68
+
69
+ A `groupProtection` integra proteção contra flood, mensagens de pagamento em rajada, citações suspeitas e indícios de mensagens invisíveis. Ela reutiliza o recebimento, o retry e o PDO nativos da Stone. É **opcional** e **não depende de patches externos**.
70
+
71
+ A função pertence à categoria **Grupos → Configurações de grupos**, no site de documentação.
72
+
73
+ ```js
74
+ // 1. Detectar e avisar (padrão: não pune automaticamente)
75
+ await stone.groupProtection(grupoJid, { enabled: true })
76
+
77
+ // 2. Ouvir ocorrências e resultados
78
+ stone.ev.on('group-protection.alert', alerta => {
79
+ console.log(alerta.reason, alerta.severity, alerta.participant)
80
+ })
81
+ stone.ev.on('group-protection.action', resultado => {
82
+ console.log(resultado)
83
+ })
84
+
85
+ // 3. Moderação automática opcional (use apenas em grupos administrados por você)
86
+ await stone.groupProtection(grupoJid, {
87
+ enabled: true,
88
+ mode: 'protect',
89
+ paymentBurst: 3,
90
+ messageBurst: 14,
91
+ windowMs: 8000,
92
+ lockMs: 12000,
93
+ editNotice: true // edita apenas o aviso criado pelo próprio bot
94
+ })
95
+
96
+ // 4. Consultar ou desativar
97
+ console.log(await stone.groupProtection(grupoJid))
98
+ await stone.groupProtection(grupoJid, { enabled: false })
99
+ ```
100
+
101
+ Os modos são `alert` (apenas alertas), `delete` (tenta apagar), `remove` (tenta apagar e remover participante) e `protect` (também fecha temporariamente o grupo). Ações administrativas exigem que o bot seja administrador e **não são disparadas por simples falha de descriptografia**.
102
+
103
+ Agora a proteção também oferece **lista de imunidade** (`immunity`), **telemetria de recibos de retry e PDO**, **histórico de incidentes** e **persistência opcional** com Stone Store. O `PDO` e a descriptografia continuam sendo os mecanismos originais da Stone, sem alteração das chaves.
104
+
105
+ Para adicionar até três monitores complementares, use `monitors: [monitor1, monitor2, monitor3]` ou `monitorFactories` para criar/reconectar sessões extras após queda. O pareamento exige autorização: o código pode ser entregue ao titular usando `onPairingCode`, mas nunca é confirmado automaticamente em nome dele.
106
+
107
+ ```js
108
+ import { createStoneStore } from '@stone-wpp/ofc'
109
+ const store = await createStoneStore({ type: 'json', path: './dados/protecao.json' })
110
+ await stone.groupProtection(grupoJid, {
111
+ enabled: true,
112
+ mode: 'alert',
113
+ immunity: [administradorJid],
114
+ observeReceipts: true,
115
+ receiptBurst: 6,
116
+ store
117
+ })
118
+ // Após reiniciar com a mesma base de dados:
119
+ await stone.groupProtection(grupoJid, { restore: true, store })
120
+ const { history } = await stone.groupProtection(grupoJid, { getHistory: true })
121
+ console.log(history)
122
+ ```
123
+
124
+ Os registros de histórico contêm somente metadados sanitizados, sem copiar texto ou ciphertext das mensagens. Recibos e PDO são apenas sinais técnicos: não autorizam expulsão automática.
125
+
126
+ ### ✏️ Editar mensagens da própria conta
127
+
128
+ A Stone já suporta edição nativa com `sendMessage(jid, { text, edit: key })`. A v1.0.6 adiciona o atalho `editMessage`, documentado em **Mensagens → Apagar e editar**:
129
+
130
+ ```js
131
+ const enviada = await stone.sendMessage(grupoJid, { text: 'Aviso inicial' })
132
+ await stone.editMessage(grupoJid, enviada.key, 'Aviso atualizado')
133
+ ```
134
+
135
+ O WhatsApp não garante que seja possível editar texto escrito por terceiros; um administrador pode excluir uma mensagem alheia, mas não reescrevê-la em nome de outra pessoa.
136
+
137
+ **Documentação completa:** [groupProtection e seus limites](docs/group-protection.md) · [Configurações de grupos no site](https://stone-wpp.squareweb.app/#/groups/configuracoes) · [Apagar e editar no site](https://stone-wpp.squareweb.app/#/messages/apagar).
138
+
139
+ ---
140
+
67
141
  ## 📦 INSTALAÇÃO PELO NPM
68
142
 
69
143
  ```bash
@@ -0,0 +1,203 @@
1
+ # groupProtection — proteção de grupos na Stone WPP
2
+
3
+ A API nativa `sock.groupProtection(jid, options)` usa a recepção da própria Stone e
4
+ não modifica o protocolo de criptografia. Detecta rajadas de mensagens de pagamento
5
+ e flood, observa falhas de descriptografia, recibos de retry e solicitações PDO de
6
+ recuperação. **Nenhuma dessas observações é garantia de ataque ou de mensagem recuperada.**
7
+
8
+ ## 1. Ativar sem risco
9
+
10
+ ```js
11
+ await sock.groupProtection(grupoJid, {
12
+ enabled: true,
13
+ mode: 'alert',
14
+ observeInvisible: true,
15
+ observeReceipts: true
16
+ })
17
+
18
+ sock.ev.on('group-protection.alert', alerta => {
19
+ console.log(alerta.jid, alerta.reason, alerta.severity, alerta.verified)
20
+ })
21
+ ```
22
+
23
+ O modo `alert` não apaga conteúdo e não remove pessoas. Use-o em um grupo próprio
24
+ para calibrar os limites antes de optar por moderação automática.
25
+
26
+ ## 2. Moderação automática e imunidade
27
+
28
+ ```js
29
+ await sock.groupProtection(grupoJid, {
30
+ enabled: true,
31
+ mode: 'protect',
32
+ paymentBurst: 3,
33
+ messageBurst: 14,
34
+ windowMs: 8000,
35
+ lockMs: 12000,
36
+ editNotice: true,
37
+ immunity: [donoJid, moderadorJid]
38
+ })
39
+ ```
40
+
41
+ | Opção | Significado |
42
+ | --- | --- |
43
+ | `mode: 'alert'` | Só registra alertas (padrão) |
44
+ | `mode: 'delete'` | Tenta excluir a mensagem de rajada verificada |
45
+ | `mode: 'remove'` | Tenta excluir e remover participante |
46
+ | `mode: 'protect'` | Tenta fechar temporariamente, excluir e remover |
47
+ | `paymentBurst` | Número de mensagens de pagamento por remetente na janela (2–30) |
48
+ | `messageBurst` | Número de mensagens por remetente na janela (3–100) |
49
+ | `windowMs` | Janela de observação em milissegundos (1.000–60.000) |
50
+ | `lockMs` | Intervalo de fechamento do grupo (3.000–120.000 ms) |
51
+ | `immunity` | Até 200 JIDs que jamais serão removidos automaticamente |
52
+ | `editNotice` | Edita **somente avisos enviados pelo bot**, nunca mensagens de terceiros |
53
+ | `observeReceipts` | Observa recibos de retry (padrão: true) |
54
+ | `receiptBurst` | Volume que gera alerta leve de recibos (3–50) |
55
+ | `historyLimit` | Registros recentes mantidos por grupo (20–500) |
56
+
57
+ O bot precisa ser administrador. Pessoas sem identidade confirmada, administradores
58
+ e JIDs imunes não são expulsos. **PN e LID são tipos diferentes de identificador**:
59
+ inclua as formas conhecidas na imunidade se aplicável. Rajadas de mensagens legítimas
60
+ podem parecer ataques; ajuste os limites conforme o grupo.
61
+
62
+ ## 3. Recibos ocultos e PDO já existente
63
+
64
+ A Stone já possui `requestPlaceholderResend`, retry e PDO. A proteção acrescenta
65
+ **telemetria passiva** ao caminho nativo, sem duplicar solicitações:
66
+
67
+ ```js
68
+ sock.ev.on('group-protection.pdo-observed', ({ jid, messageId, status }) => {
69
+ console.log('PDO do grupo:', jid, messageId, status)
70
+ })
71
+
72
+ sock.ev.on('group-protection.alert', alerta => {
73
+ if (alerta.reason === 'receipt-suspected') {
74
+ console.log('Rajada de retry observada — sem expulsão automática')
75
+ }
76
+ })
77
+ ```
78
+
79
+ `group-protection.receipt` é o evento bruto com `remoteJid`, `participant`,
80
+ `messageId` e `type: 'retry'`. `group-protection.pdo` informa eventos nativos
81
+ `requested`, `resolved` ou `timeout`; `group-protection.pdo-observed`
82
+ exibe apenas os PDO dos grupos ativados. Recebimentos de retry e falhas de
83
+ descriptografia são **sinais técnicos**, não prova de intenção maliciosa.
84
+ Um pedido PDO pode falhar, expirar ou retornar sem recuperar o conteúdo.
85
+
86
+ ## 4. Salvar configurações e consultar histórico
87
+
88
+ Use `createStoneStore` com JSON ou SQLite. O armazenamento é **opcional** e
89
+ precisa ser informado explicitamente. Exemplo:
90
+
91
+ ```js
92
+ import { createStoneStore } from '@stone-wpp/ofc'
93
+
94
+ const store = await createStoneStore({ type: 'json', path: './dados/protecao.json' })
95
+ await sock.groupProtection(grupoJid, {
96
+ enabled: true,
97
+ mode: 'alert',
98
+ store,
99
+ immunity: [donoJid],
100
+ historyLimit: 100
101
+ })
102
+
103
+ // Após reiniciar o bot, com o mesmo store:
104
+ await sock.groupProtection(grupoJid, { restore: true, store })
105
+
106
+ // Consultar últimos 30 registros, sem dados de mensagens:
107
+ const status = await sock.groupProtection(grupoJid, { getHistory: true, limit: 30 })
108
+ console.log(status.history)
109
+
110
+ // Desativar e salvar o estado desligado:
111
+ await sock.groupProtection(grupoJid, { enabled: false, store })
112
+ ```
113
+
114
+ O histórico salva somente grupo, participante, tipo de alerta/ação, situação de
115
+ moderação e horário. Não armazena automaticamente texto de mensagens, ciphertext
116
+ ou chaves da sessão. A proteção só restaura grupos que o seu aplicativo indicar;
117
+ o store **não inicia sessões nem descobre todos os grupos sozinho**.
118
+
119
+ ## 5. Até três monitores complementares com reconexão
120
+
121
+ `monitors` aceita sockets Stone já criados. Para **recriação automática**
122
+ de um monitor após queda temporária use `monitorFactories`: a função factory
123
+ deve criar o socket autenticado e carregar/persistir sua própria sessão.
124
+
125
+ ```js
126
+ await sock.groupProtection(grupoJid, {
127
+ enabled: true,
128
+ mode: 'alert',
129
+ monitorFactories: [
130
+ async ({ attempt }) => criarMonitorComSessaoSalva('auxiliar1', attempt),
131
+ async ({ attempt }) => criarMonitorComSessaoSalva('auxiliar2', attempt)
132
+ ],
133
+ monitorReconnectMs: 3000
134
+ })
135
+
136
+ sock.ev.on('group-protection.monitor', ({ index, status, error }) => {
137
+ console.log('Monitor:', index, status, error || '')
138
+ })
139
+ ```
140
+
141
+ ### Pareamento autorizado de um monitor novo
142
+
143
+ Se a sessão auxiliar ainda não foi vinculada, é possível solicitar um código
144
+ **somente quando o titular configurar explicitamente** o número e callback.
145
+ A Stone informa o código; o proprietário confirma no aplicativo WhatsApp.
146
+
147
+ ```js
148
+ import makeStone, { useMultiFileAuthState } from '@stone-wpp/ofc'
149
+
150
+ async function criarMonitor() {
151
+ const { state, saveCreds } = await useMultiFileAuthState('./sessao-monitor-1')
152
+ const monitor = makeStone({ auth: state })
153
+ monitor.ev.on('creds.update', saveCreds)
154
+ return monitor
155
+ }
156
+ await sock.groupProtection(grupoJid, {
157
+ enabled: true,
158
+ mode: 'alert',
159
+ monitorFactories: [{
160
+ create: criarMonitor,
161
+ phoneNumber: '5511999999999', // use seu número com DDI
162
+ onPairingCode({ code }) {
163
+ console.log('Vincule no WhatsApp do titular:', code)
164
+ }
165
+ }]
166
+ })
167
+ ```
168
+
169
+ Não há pareamento sem o usuário confirmar o código no telefone.
170
+ Cada monitor precisa de pasta de credenciais distinta. Se houver logout,
171
+ a reconexão para, evitando tentativas silenciosas.
172
+
173
+ A função `criarMonitorComSessaoSalva` é criada **pela sua aplicação**;
174
+ ela não é uma exportação da Stone. Ela deve utilizar suas próprias credenciais
175
+ de WhatsApp, do mesmo modo que você cria a sessão principal. Cada número
176
+ auxiliar precisa ser autenticado **com autorização do proprietário** e
177
+ participar do grupo. Não há pareamento silencioso: em caso de logout (401),
178
+ a proteção não tenta conectar novamente sem novo vínculo.
179
+
180
+ `monitors` e `monitorFactories` juntos aceitam **até três** sessões adicionais.
181
+ A Stone deduplica IDs recebidos de mais de uma sessão e observa apenas os
182
+ grupos em que o monitor foi configurado. Uma mensagem omitida pelo WhatsApp
183
+ para todas as sessões continua invisível.
184
+
185
+ ## 6. Status, erros e testes
186
+
187
+ ```js
188
+ const state = await sock.groupProtection(grupoJid)
189
+ console.log(state.enabled, state.mode, state.monitorCount)
190
+ console.log(state.alerts, state.actions)
191
+ ```
192
+
193
+ Eventos de acompanhamento:
194
+ - `group-protection.alert` — motivo, gravidade, evidência limitada e origem.
195
+ - `group-protection.action` — tentativa e resultado da moderação.
196
+ - `group-protection.monitor` — monitor conectado, erro ou logout.
197
+ - `group-protection.pdo-observed` — telemetria de recuperação de placeholder.
198
+
199
+ Testes locais: `npm test`. Ative inicialmente `mode: 'alert'` em grupos controlados,
200
+ verifique casos normais (pagamento legítimo, perda de conexão, um participante
201
+ muito ativo) e só depois considere `delete`, `remove` ou `protect`.
202
+ A proteção não falsifica mensagens atribuídas a terceiros, não garante
203
+ detectar todos os bypasses e não substitui as regras oficiais do WhatsApp.
@@ -0,0 +1,146 @@
1
+ export type GroupProtectionMode = 'alert' | 'delete' | 'remove' | 'protect'
2
+
3
+ /** Persistência opcional: utilize createStoneStore({ type: 'json' | 'sqlite', path }). */
4
+ export interface GroupProtectionStore {
5
+ get<T = unknown>(key: string): Promise<T | undefined>
6
+ set(key: string, value: unknown): Promise<boolean>
7
+ }
8
+
9
+ export interface GroupProtectionHistoryEntry {
10
+ type: 'alert' | 'action'
11
+ jid: string
12
+ participant: string | null
13
+ reason: string | null
14
+ severity: string | null
15
+ action: string | null
16
+ mode: string | null
17
+ deleted: boolean
18
+ removed: boolean
19
+ locked: boolean
20
+ receivedAt: number
21
+ }
22
+
23
+ export interface GroupProtectionMonitor {
24
+ ev: {
25
+ on(name: string, fn: (...args: any[]) => void): unknown
26
+ off(name: string, fn: (...args: any[]) => void): unknown
27
+ }
28
+ }
29
+
30
+ /** Cria/recria sessão auxiliar do próprio usuário. O aplicativo ainda persiste a autenticação. */
31
+ export interface GroupProtectionMonitorPairing {
32
+ create(args: { jid: string; attempt: number }): Promise<GroupProtectionMonitor>
33
+ /** Número com DDI para solicitar o código somente se a sessão não estiver registrada. */
34
+ phoneNumber?: string
35
+ /** Callback privado: mostre o código ao titular para confirmação no WhatsApp. */
36
+ onPairingCode?: (info: { jid: string; index: number; code: string }) => void | Promise<void>
37
+ }
38
+
39
+ export interface GroupProtectionOptions {
40
+ /** Desativada por padrão; chamar com {enabled: true} para ativar. */
41
+ enabled?: boolean
42
+ /** alert: apenas eventos. delete: apaga; remove: apaga + remove membro; protect: fecha temporariamente + apaga + remove. */
43
+ mode?: GroupProtectionMode
44
+ /** Mínimo de mensagens de pagamento do mesmo remetente por janela (padrão: 3). */
45
+ paymentBurst?: number
46
+ /** Mínimo de mensagens totais por janela (padrão: 14). */
47
+ messageBurst?: number
48
+ /** Janela de detecção em milissegundos (padrão: 8000). */
49
+ windowMs?: number
50
+ /** Tempo de fechamento temporário no modo protect (padrão: 12000). */
51
+ lockMs?: number
52
+ observeInvisible?: boolean
53
+ observePayments?: boolean
54
+ observeQuotes?: boolean
55
+ observeFlood?: boolean
56
+ /** Se true, publica um aviso do bot e edita o aviso ao concluir as ações. */
57
+ editNotice?: boolean
58
+ /** Até três sockets adicionais já autenticados e presentes nos grupos. */
59
+ monitors?: GroupProtectionMonitor[]
60
+ /** Factories de até 3 sessões extras já autenticadas; reconecta em queda temporária.
61
+ * O próprio aplicativo deve criar os sockets e persistir as credenciais.
62
+ * Logout não causa novo pareamento automático. */
63
+ monitorFactories?: Array<
64
+ ((args: { jid: string; attempt: number }) => Promise<GroupProtectionMonitor>)
65
+ | GroupProtectionMonitorPairing
66
+ >
67
+ monitorReconnectMs?: number
68
+ /** Recibos de retry são tratados apenas como telemetria, nunca prova de ataque. */
69
+ observeReceipts?: boolean
70
+ receiptBurst?: number
71
+ /** JIDs imunes a remoção (PN e LID são comparados separadamente). */
72
+ immunity?: string[]
73
+ /** Limite de registros mantidos por grupo, entre 20 e 500. */
74
+ historyLimit?: number
75
+ /** StoneStore opcional para persistir regras e histórico. */
76
+ store?: GroupProtectionStore
77
+ /** Restaura regras previamente salvas no store; requer store. */
78
+ restore?: boolean
79
+ /** Consulta o histórico sem modificar as configurações; requer store ou ativação prévia. */
80
+ getHistory?: boolean
81
+ /** Limita linhas retornadas na consulta de histórico. */
82
+ limit?: number
83
+ }
84
+
85
+ export interface GroupProtectionStatus {
86
+ jid: string
87
+ enabled: boolean
88
+ mode?: GroupProtectionMode
89
+ paymentBurst?: number
90
+ messageBurst?: number
91
+ windowMs?: number
92
+ lockMs?: number
93
+ observeInvisible?: boolean
94
+ observePayments?: boolean
95
+ observeQuotes?: boolean
96
+ observeFlood?: boolean
97
+ observeReceipts?: boolean
98
+ receiptBurst?: number
99
+ immunity?: string[]
100
+ historyLimit?: number
101
+ history?: GroupProtectionHistoryEntry[]
102
+ restored?: boolean
103
+ monitorCount: number
104
+ alerts: number
105
+ actions: number
106
+ }
107
+
108
+ export interface GroupProtectionAlert {
109
+ jid: string
110
+ participant: string
111
+ messageId: string
112
+ reason: 'payment' | 'flood' | 'invisible-suspected' | 'quoted-payment' | 'receipt-suspected'
113
+ severity: 'low' | 'medium' | 'high'
114
+ verified: boolean
115
+ source: string
116
+ evidence: Record<string, unknown>
117
+ mode: GroupProtectionMode
118
+ receivedAt: number
119
+ }
120
+
121
+ export interface GroupProtectionAction {
122
+ jid: string
123
+ participant?: string
124
+ messageId?: string
125
+ action?: 'reopen'
126
+ attemptedMode?: GroupProtectionMode
127
+ deleted?: boolean
128
+ removed?: boolean
129
+ locked?: boolean
130
+ ok?: boolean
131
+ error?: string
132
+ deleteError?: string
133
+ removeError?: string
134
+ lockError?: string
135
+ noticeEdited?: boolean
136
+ noticeError?: string
137
+ noticeEditError?: string
138
+ }
139
+
140
+ export interface GroupProtectionMethods {
141
+ /** Configura, consulta, restaura, desativa ou consulta histórico por grupo.
142
+ * Persistência exige store explícito; login de monitores exige autenticação prévia. */
143
+ groupProtection(jid: string, options?: GroupProtectionOptions): Promise<GroupProtectionStatus>
144
+ }
145
+
146
+ export declare function createGroupProtectionMethods(socket: unknown): GroupProtectionMethods
@@ -0,0 +1,571 @@
1
+ /**
2
+ * Stone WPP - proteção de grupos baseada em sinais observados.
3
+ * Não modifica mensagens, não falsifica provas e não substitui o retry criptográfico.
4
+ */
5
+ const PAYMENT_FIELDS = new Set([
6
+ 'requestPaymentMessage', 'sendPaymentMessage', 'paymentInviteMessage',
7
+ 'cancelPaymentRequestMessage', 'declinePaymentRequestMessage',
8
+ 'orderMessage', 'invoiceMessage'
9
+ ])
10
+ const PAYMENT_BUTTONS = new Set(['payment_info', 'review_and_pay', 'payment_instruction'])
11
+ const DEFAULTS = Object.freeze({
12
+ enabled: true,
13
+ mode: 'alert', // alert | delete | remove | protect
14
+ paymentBurst: 3,
15
+ messageBurst: 14,
16
+ windowMs: 8000,
17
+ lockMs: 12000,
18
+ observeInvisible: true,
19
+ observePayments: true,
20
+ observeQuotes: true,
21
+ observeFlood: true,
22
+ editNotice: false,
23
+ observeReceipts: true,
24
+ receiptBurst: 6,
25
+ immunity: [],
26
+ historyLimit: 100
27
+ })
28
+
29
+ const GROUP_SUFFIX = '@g.us'
30
+ const MAX_MONITORS = 3
31
+ const MAX_SEEN = 6000
32
+ const SEEN_TTL = 120000
33
+
34
+ // Compara apenas o mesmo domínio de JID: PN e LID não são intercambiáveis.
35
+ const canonicalJid = jid => {
36
+ if (typeof jid !== 'string' || !jid.includes('@')) return ''
37
+ const [user, domain] = jid.split('@')
38
+ return user.split(':')[0] + '@' + domain.toLowerCase()
39
+ }
40
+ const sameUser = (a, b) => !!canonicalJid(a) && canonicalJid(a) === canonicalJid(b)
41
+ const isImmune = (identity, list) => Array.isArray(list) && list.some(jid => sameUser(jid, identity))
42
+ const STORE_PREFIX = 'stone:groupProtection:'
43
+ const storedSettings = settings => Object.fromEntries(Object.keys(DEFAULTS).map(key => [key, settings[key]]))
44
+ const recordSafe = event => ({
45
+ jid: event.jid, participant: event.participant || null,
46
+ reason: event.reason || null, severity: event.severity || null,
47
+ action: event.action || null, mode: event.mode || event.attemptedMode || null,
48
+ deleted: !!event.deleted, removed: !!event.removed, locked: !!event.locked,
49
+ receivedAt: event.receivedAt || Date.now()
50
+ })
51
+ const isAdmin = p => !!p?.admin
52
+ const boundedInt = (v, min, max, key) => {
53
+ if (!Number.isInteger(v) || v < min || v > max) {
54
+ throw new RangeError(`${key} deve ser um inteiro entre ${min} e ${max}`)
55
+ }
56
+ return v
57
+ }
58
+
59
+ function scanPayment(root) {
60
+ if (!root || typeof root !== 'object') return null
61
+ const seen = new WeakSet()
62
+ const todo = [{ node: root, depth: 0 }]
63
+ let checked = 0
64
+ while (todo.length && checked++ < 180) {
65
+ const { node, depth } = todo.pop()
66
+ if (!node || typeof node !== 'object' || seen.has(node) || depth > 7) continue
67
+ seen.add(node)
68
+ for (const [key, value] of Object.entries(node)) {
69
+ if (!value || key === 'quotedMessage') continue
70
+ if (PAYMENT_FIELDS.has(key)) return key
71
+ if (key === 'nativeFlowMessage' && Array.isArray(value.buttons)) {
72
+ for (const button of value.buttons) {
73
+ if (PAYMENT_BUTTONS.has(String(button?.name || '').toLowerCase())) return 'nativeFlowPayment'
74
+ }
75
+ }
76
+ if (typeof value === 'object') todo.push({ node: value, depth: depth + 1 })
77
+ }
78
+ }
79
+ return null
80
+ }
81
+
82
+ function findQuoted(root) {
83
+ let message = root
84
+ for (let i = 0; i < 5 && message && typeof message === 'object'; i++) {
85
+ for (const key of ['extendedTextMessage', 'imageMessage', 'videoMessage', 'documentMessage', 'buttonsResponseMessage', 'templateButtonReplyMessage']) {
86
+ const context = message[key]?.contextInfo
87
+ if (context?.quotedMessage) return { message: context.quotedMessage, participant: context.participant, stanzaId: context.stanzaId }
88
+ }
89
+ message = message.ephemeralMessage?.message || message.viewOnceMessage?.message || message.viewOnceMessageV2?.message || message.documentWithCaptionMessage?.message
90
+ }
91
+ return null
92
+ }
93
+
94
+ /** Instala proteção opcional em um socket sem alterar a recepção nativa. */
95
+ export function createGroupProtectionMethods(socket) {
96
+ const groups = new Map()
97
+ const observers = new Map()
98
+ const seen = new Map()
99
+ const buckets = new Map()
100
+ const busy = new Set()
101
+ const recentActions = new Map()
102
+ const locks = new Map()
103
+ const persistence = new Map()
104
+ const managed = new Map()
105
+
106
+ function markOnce(key) {
107
+ const now = Date.now()
108
+ if (seen.has(key) && now - seen.get(key) < SEEN_TTL) return false
109
+ seen.set(key, now)
110
+ if (seen.size > MAX_SEEN) {
111
+ for (const [k, t] of seen) if (now - t >= SEEN_TTL) seen.delete(k)
112
+ while (seen.size > MAX_SEEN) seen.delete(seen.keys().next().value)
113
+ }
114
+ return true
115
+ }
116
+
117
+ function count(group, participant, kind, windowMs, mark) {
118
+ const key = `${group}\u0000${participant}\u0000${kind}`
119
+ const now = Date.now()
120
+ const old = (buckets.get(key) || []).filter(item => now - item.at < windowMs)
121
+ if (mark && !old.some(item => item.id === mark)) old.push({ id: mark, at: now })
122
+ buckets.set(key, old.slice(-80))
123
+ if (buckets.size > 3000) {
124
+ for (const [k, entries] of buckets) {
125
+ if (!entries.length || now - entries[entries.length - 1].at > 2 * windowMs) buckets.delete(k)
126
+ }
127
+ }
128
+ return old.length
129
+ }
130
+
131
+ function appendHistory(state, type, incident) {
132
+ const entry = { type, ...recordSafe(incident) }
133
+ state.history.push(entry)
134
+ if (state.history.length > state.settings.historyLimit) state.history.splice(0, state.history.length - state.settings.historyLimit)
135
+ if (state.store) {
136
+ const key = STORE_PREFIX + 'history:' + entry.jid
137
+ const previous = persistence.get(key) || Promise.resolve()
138
+ const task = previous.catch(() => {}).then(async () => {
139
+ await state.store.set(key, state.history.slice())
140
+ })
141
+ persistence.set(key, task)
142
+ task.catch(error => socket.logger?.warn?.({ error }, 'groupProtection history save failed'))
143
+ }
144
+ }
145
+ function emit(name, data) {
146
+ const state = groups.get(data?.jid)
147
+ if (state && (name === 'group-protection.alert' || name === 'group-protection.action')) {
148
+ appendHistory(state, name.endsWith('.alert') ? 'alert' : 'action', data)
149
+ }
150
+ try { socket.ev.emit(name, data) } catch (error) {
151
+ socket.logger?.warn?.({ error }, 'groupProtection event handler failed')
152
+ }
153
+ }
154
+
155
+ async function act(incident, message) {
156
+ const state = groups.get(incident.jid)
157
+ if (!state || state.settings.mode === 'alert' || incident.severity !== 'high' || !incident.verified) return
158
+ const lockKey = `${incident.jid}\u0000${incident.participant}`
159
+ if (busy.has(lockKey) || Date.now() - (recentActions.get(lockKey) || 0) < 30000) return
160
+ busy.add(lockKey)
161
+ recentActions.set(lockKey, Date.now())
162
+ if (recentActions.size > 3000) {
163
+ for (const [key, at] of recentActions) if (Date.now() - at > 60000) recentActions.delete(key)
164
+ }
165
+ const result = { ...incident, attemptedMode: state.settings.mode, deleted: false, removed: false, locked: false }
166
+ let notice = null
167
+ try {
168
+ const metadata = await socket.groupMetadata(incident.jid)
169
+ const members = Array.isArray(metadata?.participants) ? metadata.participants : []
170
+ const self = members.find(p => sameUser(p.id, socket.user?.id) || sameUser(p.id, socket.authState?.creds?.me?.lid))
171
+ const actor = members.find(p => sameUser(p.id, incident.participant))
172
+ if (!isAdmin(self)) {
173
+ result.error = 'bot_not_admin'
174
+ return
175
+ }
176
+ if (!actor || isAdmin(actor) || sameUser(actor.id, self.id)) {
177
+ result.error = 'target_unverified_or_admin'
178
+ return
179
+ }
180
+ if (isImmune(actor.id, state.settings.immunity)
181
+ || isImmune(actor.phoneNumber, state.settings.immunity)
182
+ || isImmune(actor.lid, state.settings.immunity)
183
+ || isImmune(incident.participant, state.settings.immunity)) {
184
+ result.error = 'immune_participant'
185
+ return
186
+ }
187
+ if (state.settings.editNotice) {
188
+ try {
189
+ notice = await socket.sendMessage(incident.jid, { text: 'Stone Protection: ameaça identificada. Verificando e aplicando as ações de moderação...' })
190
+ } catch (error) { result.noticeError = String(error?.message || error) }
191
+ }
192
+ if (state.settings.mode === 'protect' && !metadata.announce && !locks.has(incident.jid)) {
193
+ try {
194
+ await socket.groupSettingUpdate(incident.jid, 'announcement')
195
+ result.locked = true
196
+ const timer = setTimeout(async () => {
197
+ locks.delete(incident.jid)
198
+ try {
199
+ const latest = await socket.groupMetadata(incident.jid)
200
+ if (latest?.announce === true) await socket.groupSettingUpdate(incident.jid, 'not_announcement')
201
+ }
202
+ catch (error) { emit('group-protection.action', { jid: incident.jid, action: 'reopen', ok: false, error: String(error?.message || error) }) }
203
+ }, state.settings.lockMs)
204
+ timer.unref?.()
205
+ locks.set(incident.jid, timer)
206
+ } catch (error) { result.lockError = String(error?.message || error) }
207
+ }
208
+ if (message?.key?.id && message.key.remoteJid === incident.jid) {
209
+ try {
210
+ await socket.sendMessage(incident.jid, { delete: message.key })
211
+ result.deleted = true
212
+ } catch (error) { result.deleteError = String(error?.message || error) }
213
+ }
214
+ if (state.settings.mode === 'remove' || state.settings.mode === 'protect') {
215
+ try {
216
+ const response = await socket.groupParticipantsUpdate(incident.jid, [actor.id], 'remove')
217
+ result.removed = Array.isArray(response) && response.some(r => String(r.status) === '200')
218
+ } catch (error) { result.removeError = String(error?.message || error) }
219
+ }
220
+ } catch (error) { result.error = String(error?.message || error) }
221
+ finally {
222
+ if (notice?.key?.id && notice.key.fromMe === true) {
223
+ const status = result.removed ? 'participante removido' : result.deleted ? 'mensagem removida' : 'ações parciais ou indisponíveis'
224
+ try {
225
+ await socket.sendMessage(incident.jid, {
226
+ text: `Stone Protection: verificação concluída — ${status}.`,
227
+ edit: notice.key
228
+ })
229
+ result.noticeEdited = true
230
+ } catch (error) {
231
+ result.noticeEditError = String(error?.message || error)
232
+ }
233
+ }
234
+ busy.delete(lockKey)
235
+ state.actions++
236
+ emit('group-protection.action', result)
237
+ }
238
+ }
239
+
240
+ function report(incident, message) {
241
+ const state = groups.get(incident.jid)
242
+ if (!state) return
243
+ const evidenceKey = `${incident.jid}\u0000${incident.messageId}\u0000${incident.reason}\u0000${incident.participant}`
244
+ if (!markOnce(evidenceKey)) return
245
+ state.alerts++
246
+ emit('group-protection.alert', { ...incident, mode: state.settings.mode, receivedAt: Date.now() })
247
+ void act(incident, message).catch(error => socket.logger?.warn?.({ error }, 'groupProtection action failed'))
248
+ }
249
+
250
+ function handleSignal(data, source) {
251
+ if (!data?.remoteJid?.endsWith(GROUP_SUFFIX) || !data.messageId) return
252
+ const state = groups.get(data.remoteJid)
253
+ if (!state?.settings.observeInvisible) return
254
+ const actor = data.participant || data.participantAlt
255
+ if (!actor) return
256
+ const signature = String(data.decryptFail || '').toLowerCase()
257
+ const highSignal = signature === 'hide'
258
+ if (!markOnce(`signal\u0000${data.remoteJid}\u0000${data.messageId}\u0000${actor}`)) return
259
+ const hits = count(data.remoteJid, actor, 'ciphertext', state.settings.windowMs, data.messageId)
260
+ // Ciphertext não é prova de ataque; nunca punir só por falha de descriptografia.
261
+ if (highSignal || hits >= state.settings.paymentBurst) {
262
+ report({ jid: data.remoteJid, participant: actor, messageId: data.messageId, reason: 'invisible-suspected', severity: highSignal ? 'medium' : 'low', verified: false, source, evidence: { decryptFail: signature || null, ciphertextCount: hits } })
263
+ }
264
+ }
265
+
266
+ // Recibos são somente telemetria: pedidos de retransmissão são normais
267
+ // em redes instáveis e NÃO identificam culpabilidade de um participante.
268
+ function handleReceipt(data, source) {
269
+ if (!data?.remoteJid?.endsWith(GROUP_SUFFIX) || !data.messageId || !data.participant) return
270
+ const state = groups.get(data.remoteJid)
271
+ if (!state?.settings.observeReceipts || data.type !== 'retry') return
272
+ const mark = `receipt\u0000${data.remoteJid}\u0000${data.messageId}\u0000${data.participant}`
273
+ if (!markOnce(mark)) return
274
+ const n = count(data.remoteJid, data.participant, 'retry-receipt', state.settings.windowMs, data.messageId)
275
+ if (n === state.settings.receiptBurst || (n > state.settings.receiptBurst && n % state.settings.receiptBurst === 0)) {
276
+ report({ jid: data.remoteJid, participant: data.participant,
277
+ messageId: data.messageId, reason: 'receipt-suspected', severity: 'low',
278
+ verified: false, source, evidence: { type: data.type, receiptCount: n } })
279
+ }
280
+ }
281
+
282
+ function handlePdo(data, source) {
283
+ if (!data?.remoteJid?.endsWith(GROUP_SUFFIX)) return
284
+ const state = groups.get(data.remoteJid)
285
+ if (!state?.settings.observeInvisible || !data.messageId) return
286
+ emit('group-protection.pdo-observed', {
287
+ jid: data.remoteJid, messageId: data.messageId,
288
+ status: data.status, source, receivedAt: Date.now()
289
+ })
290
+ }
291
+
292
+ function handleMessage(message, source, kind = 'notify') {
293
+ const jid = message?.key?.remoteJid
294
+ if (!jid?.endsWith(GROUP_SUFFIX) || message.key.fromMe) return
295
+ const state = groups.get(jid)
296
+ if (!state || (kind !== 'notify' && kind !== 'append')) return
297
+ const id = message.key.id
298
+ const participant = message.key.participant || message.participant || message.key.participantAlt
299
+ if (!id || !participant || !message.message) return
300
+ const stamp = Number(message.messageTimestamp || 0)
301
+ if (stamp && Math.abs(Date.now() - stamp * 1000) > 120000) return
302
+ if (!markOnce(`message\u0000${jid}\u0000${id}`)) return
303
+ const settings = state.settings
304
+ const paymentType = settings.observePayments ? scanPayment(message.message) : null
305
+ const volume = settings.observeFlood ? count(jid, participant, 'messages', settings.windowMs, id) : 0
306
+ if (paymentType) {
307
+ const payments = count(jid, participant, 'payment', settings.windowMs, id)
308
+ report({ jid, participant, messageId: id, reason: 'payment', severity: payments >= settings.paymentBurst ? 'high' : 'low', verified: payments >= settings.paymentBurst, source, evidence: { paymentType, paymentCount: payments } }, message)
309
+ }
310
+ if (settings.observeFlood && volume >= settings.messageBurst) {
311
+ report({ jid, participant, messageId: id, reason: 'flood', severity: 'high', verified: true, source, evidence: { messageCount: volume } }, message)
312
+ }
313
+ // Citações são evidência auxiliar, nunca prova para punir outro participante.
314
+ if (settings.observeQuotes) {
315
+ const quoted = findQuoted(message.message)
316
+ const quotePayment = quoted && scanPayment(quoted.message)
317
+ if (quotePayment) {
318
+ report({ jid, participant: quoted.participant || participant, messageId: quoted.stanzaId || id, reason: 'quoted-payment', severity: 'medium', verified: false, source, evidence: { quotedBy: participant, paymentType: quotePayment } })
319
+ }
320
+ }
321
+ }
322
+
323
+ function connectObserver(monitor, index) {
324
+ if (!monitor?.ev || typeof monitor.ev.on !== 'function') throw new TypeError('monitor deve ser um socket Stone WPP conectado')
325
+ if (observers.has(monitor)) return
326
+ const source = index === -1 ? 'primary' : `monitor-${index + 1}`
327
+ const allowed = groupJid => monitor === socket || groups.get(groupJid)?.monitors.includes(monitor)
328
+ const onUpsert = payload => {
329
+ for (const message of payload?.messages || []) {
330
+ if (allowed(message?.key?.remoteJid)) handleMessage(message, source, payload?.type)
331
+ }
332
+ }
333
+ const onSignal = payload => {
334
+ if (allowed(payload?.remoteJid)) handleSignal(payload, source)
335
+ }
336
+ monitor.ev.on('messages.upsert', onUpsert)
337
+ const onReceipt = payload => {
338
+ if (allowed(payload?.remoteJid)) handleReceipt(payload, source)
339
+ }
340
+ const onPdo = payload => {
341
+ if (allowed(payload?.remoteJid)) handlePdo(payload, source)
342
+ }
343
+ monitor.ev.on('group-protection.ciphertext', onSignal)
344
+ monitor.ev.on('group-protection.receipt', onReceipt)
345
+ monitor.ev.on('group-protection.pdo', onPdo)
346
+ observers.set(monitor, () => {
347
+ monitor.ev.off('messages.upsert', onUpsert)
348
+ monitor.ev.off('group-protection.ciphertext', onSignal)
349
+ monitor.ev.off('group-protection.receipt', onReceipt)
350
+ monitor.ev.off('group-protection.pdo', onPdo)
351
+ })
352
+ }
353
+
354
+ function pruneObservers() {
355
+ const active = new Set([...groups.values()].flatMap(g => g.monitors))
356
+ for (const [monitor, detach] of observers) {
357
+ if (monitor !== socket && !active.has(monitor)) { detach(); observers.delete(monitor) }
358
+ }
359
+ }
360
+ function statusFor(jid, state, extra = {}) {
361
+ if (!state) return { jid, enabled: false, monitorCount: 0, alerts: 0, actions: 0, ...extra }
362
+ return { jid, ...state.settings, monitorCount: state.monitors.length,
363
+ alerts: state.alerts, actions: state.actions, ...extra }
364
+ }
365
+ function stopManaged(jid) {
366
+ for (const [key, active] of managed) {
367
+ if (!key.startsWith(jid + '\u0000')) continue
368
+ active.cancelled = true
369
+ if (active.timer) clearTimeout(active.timer)
370
+ if (active.socket && active.onUpdate) active.socket.ev.off('connection.update', active.onUpdate)
371
+ managed.delete(key)
372
+ }
373
+ }
374
+ async function startManaged(jid, index, factory, state, reconnectMs) {
375
+ const key = jid + '\u0000' + index
376
+ if (managed.get(key)?.factory === factory) return
377
+ const current = managed.get(key)
378
+ if (current) {
379
+ current.cancelled = true
380
+ if (current.timer) clearTimeout(current.timer)
381
+ if (current.socket && current.onUpdate) current.socket.ev.off('connection.update', current.onUpdate)
382
+ }
383
+ const item = { factory, timer: null, socket: null, onUpdate: null, attempt: 0, cancelled: false }
384
+ managed.set(key, item)
385
+ const attach = async () => {
386
+ if (item.cancelled || groups.get(jid) !== state) return
387
+ item.attempt++
388
+ try {
389
+ const create = typeof factory === 'function' ? factory : factory.create
390
+ const monitor = await create({ jid, attempt: item.attempt })
391
+ if (!monitor?.ev || typeof monitor.ev.on !== 'function' || monitor === socket) {
392
+ throw new TypeError('monitorFactory deve retornar outro socket autenticado')
393
+ }
394
+ if (item.cancelled || groups.get(jid) !== state) return
395
+ // Pareamento autorizado: o proprietário recebe o código via callback.
396
+ // Nenhuma sessão é vinculada sem a confirmação no próprio WhatsApp.
397
+ if (typeof factory === 'object' && factory.phoneNumber
398
+ && monitor.authState?.creds?.registered === false) {
399
+ if (typeof monitor.requestPairingCode !== 'function') {
400
+ throw new TypeError('socket auxiliar não suporta requestPairingCode')
401
+ }
402
+ const code = await monitor.requestPairingCode(factory.phoneNumber)
403
+ await factory.onPairingCode({ jid, index, code })
404
+ }
405
+ if (item.socket && item.onUpdate) item.socket.ev.off('connection.update', item.onUpdate)
406
+ const previous = item.socket
407
+ item.socket = monitor
408
+ if (previous) {
409
+ state.monitors = state.monitors.filter(m => m !== previous)
410
+ }
411
+ if (!state.monitors.includes(monitor)) state.monitors.push(monitor)
412
+ connectObserver(monitor, index)
413
+ pruneObservers()
414
+ item.onUpdate = update => {
415
+ if (update?.connection === 'open') {
416
+ item.attempt = 0
417
+ emit('group-protection.monitor', { jid, index, status: 'connected' })
418
+ }
419
+ if (update?.connection !== 'close' || item.cancelled) return
420
+ const code = update?.lastDisconnect?.error?.output?.statusCode
421
+ // Logged out: é necessário parear novamente com consentimento.
422
+ if (code === 401) {
423
+ monitor.ev.off('connection.update', item.onUpdate)
424
+ state.monitors = state.monitors.filter(m => m !== monitor)
425
+ item.socket = null
426
+ pruneObservers()
427
+ emit('group-protection.monitor', { jid, index, status: 'logged-out' })
428
+ return
429
+ }
430
+ monitor.ev.off('connection.update', item.onUpdate)
431
+ state.monitors = state.monitors.filter(m => m !== monitor)
432
+ pruneObservers()
433
+ schedule()
434
+ }
435
+ monitor.ev.on('connection.update', item.onUpdate)
436
+ emit('group-protection.monitor', { jid, index, status: 'attached' })
437
+ } catch (error) {
438
+ emit('group-protection.monitor', { jid, index, status: 'error',
439
+ error: String(error?.message || error) })
440
+ schedule()
441
+ }
442
+ }
443
+ const schedule = () => {
444
+ if (item.cancelled || groups.get(jid) !== state) return
445
+ const ms = Math.min(60000, reconnectMs * Math.pow(2, Math.min(item.attempt - 1, 5)))
446
+ item.timer = setTimeout(() => { item.timer = null; void attach() }, ms)
447
+ item.timer.unref?.()
448
+ }
449
+ await attach()
450
+ }
451
+
452
+ connectObserver(socket, -1)
453
+
454
+ return {
455
+ /**
456
+ * Consulta / configura a proteção. Monitors: sockets Stone extras autenticados
457
+ * separadamente e presentes nos mesmos grupos (até três). A Stone não cria sessões
458
+ * extras automaticamente nem promete acessar mensagens omitidas pelo WhatsApp.
459
+ */
460
+ async groupProtection(jid, options) {
461
+ if (typeof jid !== 'string' || !jid.endsWith(GROUP_SUFFIX)) {
462
+ throw new TypeError('groupProtection requer JID de grupo (@g.us)')
463
+ }
464
+ const old = groups.get(jid)
465
+ if (options === undefined) return statusFor(jid, old)
466
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
467
+ throw new TypeError('options deve ser um objeto')
468
+ }
469
+ const store = options.store || old?.store
470
+ if (store && (typeof store.get !== 'function' || typeof store.set !== 'function')) {
471
+ throw new TypeError('store requer métodos get e set')
472
+ }
473
+ if (options.getHistory === true) {
474
+ const limit = options.limit === undefined ? 100 : boundedInt(options.limit, 1, 500, 'limit')
475
+ const saved = store ? await store.get(STORE_PREFIX + 'history:' + jid) : null
476
+ const history = old?.history?.length ? old.history : (Array.isArray(saved) ? saved : [])
477
+ return statusFor(jid, old, { history: history.slice(-limit) })
478
+ }
479
+ if (options.enabled === false) {
480
+ const savedStore = store
481
+ groups.delete(jid)
482
+ stopManaged(jid)
483
+ pruneObservers()
484
+ if (savedStore) {
485
+ await savedStore.set(STORE_PREFIX + 'settings:' + jid, { enabled: false })
486
+ }
487
+ return statusFor(jid, null)
488
+ }
489
+ let restored = null
490
+ if (options.restore === true) {
491
+ if (!store) throw new TypeError('restore requer store')
492
+ restored = await store.get(STORE_PREFIX + 'settings:' + jid)
493
+ if (!restored?.enabled) return statusFor(jid, old, { restored: false })
494
+ }
495
+ const input = { ...(old?.settings || DEFAULTS), ...(restored || {}), ...options, enabled: true }
496
+ if (!['alert', 'delete', 'remove', 'protect'].includes(input.mode)) {
497
+ throw new TypeError('mode: alert, delete, remove ou protect')
498
+ }
499
+ for (const key of ['observeInvisible', 'observePayments', 'observeQuotes', 'observeFlood', 'editNotice', 'observeReceipts']) {
500
+ if (typeof input[key] !== 'boolean') throw new TypeError(`${key} deve ser booleano`)
501
+ }
502
+ input.paymentBurst = boundedInt(input.paymentBurst, 2, 30, 'paymentBurst')
503
+ input.messageBurst = boundedInt(input.messageBurst, 3, 100, 'messageBurst')
504
+ input.receiptBurst = boundedInt(input.receiptBurst, 3, 50, 'receiptBurst')
505
+ input.windowMs = boundedInt(input.windowMs, 1000, 60000, 'windowMs')
506
+ input.lockMs = boundedInt(input.lockMs, 3000, 120000, 'lockMs')
507
+ input.historyLimit = boundedInt(input.historyLimit, 20, 500, 'historyLimit')
508
+ if (!Array.isArray(input.immunity) || input.immunity.length > 200
509
+ || !input.immunity.every(jid => typeof jid === 'string' && canonicalJid(jid))) {
510
+ throw new TypeError('immunity deve conter até 200 JIDs válidos')
511
+ }
512
+ input.immunity = [...new Set(input.immunity.map(canonicalJid))]
513
+ const staticMonitors = options.monitors === undefined ? (old?.staticMonitors || []) : options.monitors
514
+ const factories = options.monitorFactories === undefined ? (old?.factories || []) : options.monitorFactories
515
+ if (!Array.isArray(staticMonitors) || !Array.isArray(factories)
516
+ || staticMonitors.length + factories.length > MAX_MONITORS
517
+ || new Set(staticMonitors).size !== staticMonitors.length || staticMonitors.includes(socket)
518
+ || !factories.every(f => typeof f === 'function' ||
519
+ (f && typeof f === 'object' && typeof f.create === 'function'
520
+ && (!f.phoneNumber || (typeof f.phoneNumber === 'string'
521
+ && /^\d{8,15}$/.test(f.phoneNumber)
522
+ && typeof f.onPairingCode === 'function'))))) {
523
+ throw new TypeError('Até 3 monitores: sockets distintos e/ou monitorFactories')
524
+ }
525
+ for (const monitor of staticMonitors) {
526
+ if (!monitor?.ev || typeof monitor.ev.on !== 'function') {
527
+ throw new TypeError('monitor deve ser socket Stone válido')
528
+ }
529
+ }
530
+ const reconnectMs = options.monitorReconnectMs === undefined
531
+ ? (old?.monitorReconnectMs || 3000)
532
+ : boundedInt(options.monitorReconnectMs, 1000, 60000, 'monitorReconnectMs')
533
+ const settings = storedSettings(input)
534
+ const state = old || { alerts: 0, actions: 0, history: [], monitors: [] }
535
+ state.settings = settings
536
+ state.staticMonitors = staticMonitors
537
+ state.factories = factories
538
+ state.monitorReconnectMs = reconnectMs
539
+ state.store = store
540
+ state.monitors = [...staticMonitors, ...[...managed.entries()]
541
+ .filter(([key, item]) => key.startsWith(jid + '\u0000')
542
+ && !item.cancelled && item.socket && factories.includes(item.factory))
543
+ .map(([, item]) => item.socket)]
544
+ if (store && !old?.history?.length) {
545
+ const history = await store.get(STORE_PREFIX + 'history:' + jid)
546
+ if (Array.isArray(history)) state.history = history.slice(-settings.historyLimit)
547
+ }
548
+ groups.set(jid, state)
549
+ staticMonitors.forEach((monitor, index) => connectObserver(monitor, index))
550
+ for (const [key, item] of managed) {
551
+ if (!key.startsWith(jid + '\u0000')) continue
552
+ const index = Number(key.slice((jid + '\u0000').length))
553
+ if (factories[index] !== item.factory) {
554
+ item.cancelled = true
555
+ if (item.timer) clearTimeout(item.timer)
556
+ if (item.socket && item.onUpdate) item.socket.ev.off('connection.update', item.onUpdate)
557
+ state.monitors = state.monitors.filter(monitor => monitor !== item.socket)
558
+ managed.delete(key)
559
+ }
560
+ }
561
+ for (let i = 0; i < factories.length; i++) {
562
+ await startManaged(jid, i, factories[i], state, reconnectMs)
563
+ }
564
+ pruneObservers()
565
+ if (store) {
566
+ await store.set(STORE_PREFIX + 'settings:' + jid, settings)
567
+ }
568
+ return statusFor(jid, state, { ...(options.restore ? { restored: true } : {}) })
569
+ }
570
+ }
571
+ }
@@ -256,5 +256,5 @@ declare const makeWASocket: (config: UserFacingSocketConfig) => {
256
256
  }[] | undefined>;
257
257
  fetchAccountReachoutTimelock: () => Promise<import("../index.js").ReachoutTimelockState>;
258
258
  fetchNewChatMessageCap: () => Promise<import("../index.js").NewChatMessageCapInfo>;
259
- } & import('./stone-features.js').StoneAdditionalMethods;
259
+ } & import('./stone-features.js').StoneAdditionalMethods & import('./group-protection.js').GroupProtectionMethods;
260
260
  export default makeWASocket;
@@ -1,6 +1,7 @@
1
1
  import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
2
2
  import { makeCommunitiesSocket } from './communities.js';
3
3
  import { createStoneAdditionalMethods } from './stone-features.js';
4
+ import { createGroupProtectionMethods } from './group-protection.js';
4
5
  // export the last socket layer
5
6
  const makeWASocket = (config) => {
6
7
  const newConfig = {
@@ -8,6 +9,6 @@ const makeWASocket = (config) => {
8
9
  ...config
9
10
  };
10
11
  const socket = makeCommunitiesSocket(newConfig);
11
- return Object.assign(socket, createStoneAdditionalMethods(socket));
12
+ return Object.assign(socket, createStoneAdditionalMethods(socket), createGroupProtectionMethods(socket));
12
13
  };
13
14
  export default makeWASocket;
@@ -37,6 +37,10 @@ export const makeMessagesRecvSocket = (config) => {
37
37
  // Debounce identity-change session refreshes per JID to avoid bursts
38
38
  const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false });
39
39
  let sendActiveReceipts = false;
40
+ const observePdo = data => {
41
+ try { ev.emit('group-protection.pdo', data); }
42
+ catch (error) { logger.debug({ error }, 'PDO telemetry listener failed'); }
43
+ };
40
44
  const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
41
45
  if (!authState.creds.me?.id) {
42
46
  throw new Boom('Not authenticated');
@@ -69,6 +73,9 @@ export const makeMessagesRecvSocket = (config) => {
69
73
  await delay(2000);
70
74
  if (!(await placeholderResendCache.get(messageKey?.id))) {
71
75
  logger.debug({ messageKey }, 'message received while resend requested');
76
+ if (isJidGroup(messageKey?.remoteJid)) {
77
+ observePdo({ remoteJid: messageKey.remoteJid, messageId: messageKey.id, status: 'resolved' });
78
+ }
72
79
  return 'RESOLVED';
73
80
  }
74
81
  const pdoMessage = {
@@ -83,8 +90,14 @@ export const makeMessagesRecvSocket = (config) => {
83
90
  if (await placeholderResendCache.get(messageKey?.id)) {
84
91
  logger.debug({ messageKey }, 'PDO message without response after 8 seconds. Phone possibly offline');
85
92
  await placeholderResendCache.del(messageKey?.id);
93
+ if (isJidGroup(messageKey?.remoteJid)) {
94
+ observePdo({ remoteJid: messageKey.remoteJid, messageId: messageKey.id, status: 'timeout' });
95
+ }
86
96
  }
87
97
  }, 8000);
98
+ if (isJidGroup(messageKey?.remoteJid)) {
99
+ observePdo({ remoteJid: messageKey.remoteJid, messageId: messageKey.id, status: 'requested' });
100
+ }
88
101
  return sendPeerDataOperationMessage(pdoMessage);
89
102
  };
90
103
  const handleMexNotification = async (node) => {
@@ -1175,6 +1188,19 @@ export const makeMessagesRecvSocket = (config) => {
1175
1188
  const items = getBinaryNodeChildren(content[0], 'item');
1176
1189
  ids.push(...items.map(i => i.attrs.id));
1177
1190
  }
1191
+ // Metadata somente: recibos de retry não comprovam ação maliciosa.
1192
+ if (attrs.type === 'retry' && isJidGroup(remoteJid) && attrs.participant) {
1193
+ for (const id of ids.filter(Boolean)) {
1194
+ try {
1195
+ ev.emit('group-protection.receipt', {
1196
+ remoteJid, messageId: id, participant: attrs.participant,
1197
+ type: 'retry', receivedAt: Date.now()
1198
+ });
1199
+ } catch (error) {
1200
+ logger.debug({ error }, 'receipt security telemetry failed');
1201
+ }
1202
+ }
1203
+ }
1178
1204
  try {
1179
1205
  await Promise.all([
1180
1206
  receiptMutex.mutex(async () => {
@@ -1300,7 +1326,7 @@ export const makeMessagesRecvSocket = (config) => {
1300
1326
  && !msg.key.fromMe
1301
1327
  && String(node?.attrs?.type || '').toLowerCase() !== 'reaction') {
1302
1328
  try {
1303
- ev.emit('tokito.security.ciphertext', {
1329
+ const securityObservation = {
1304
1330
  remoteJid: msg.key.remoteJid,
1305
1331
  messageId: msg.key.id,
1306
1332
  participant: msg.key.participant || msg.participant || node?.attrs?.participant || '',
@@ -1309,7 +1335,10 @@ export const makeMessagesRecvSocket = (config) => {
1309
1335
  encType: encNode?.attrs?.type || null,
1310
1336
  messageTimestamp: msg.messageTimestamp,
1311
1337
  receivedAt: Date.now()
1312
- });
1338
+ };
1339
+ // Evento genérico da Stone, preservando o evento legado para compatibilidade.
1340
+ ev.emit('group-protection.ciphertext', securityObservation);
1341
+ ev.emit('tokito.security.ciphertext', securityObservation);
1313
1342
  } catch (err) {
1314
1343
  logger.debug({ err }, 'ciphertext observation failed');
1315
1344
  }
@@ -2,6 +2,8 @@ import type { MiscMessageGenerationOptions, WAMessage, WAMessageKey } from '../T
2
2
  import type { Label } from '../Types/Label.js'
3
3
 
4
4
  export interface StoneAdditionalMethods {
5
+ /** Edita o texto de mensagem enviada pelo próprio bot; não edita mensagens de terceiros. */
6
+ editMessage(jid: string, messageKey: WAMessageKey, text: string): Promise<WAMessage | undefined>;
5
7
  /** Pesquisa username pelo USync. Não retorna números de telefone. */
6
8
  queryUsernameExists(username: string, pin?: string): Promise<{ username: string; jid: string; wid: string; exists: true } | null>;
7
9
  /** Cria ou altera uma nota da conta WhatsApp Business via app-state. */
@@ -57,6 +57,30 @@ export function createStoneAdditionalMethods(socket) {
57
57
  })
58
58
 
59
59
  return {
60
+ /**
61
+ * Edita uma mensagem enviada pela própria conta.
62
+ * A Stone já implementa MESSAGE_EDIT no sendMessage: aqui há um atalho
63
+ * com validação para não tentar falsificar mensagens de terceiros.
64
+ */
65
+ async editMessage(jid, messageKey, text) {
66
+ requireJid(jid)
67
+ if (!messageKey || typeof messageKey !== 'object' || !messageKey.id) {
68
+ throw new TypeError('Informe a chave (key) da mensagem enviada pelo bot')
69
+ }
70
+ if (messageKey.fromMe !== true) {
71
+ throw new TypeError('Só é possível editar mensagens enviadas pela própria conta')
72
+ }
73
+ if (messageKey.remoteJid && messageKey.remoteJid !== jid) {
74
+ throw new TypeError('A mensagem original não pertence a esta conversa')
75
+ }
76
+ if (typeof text !== 'string' || !text.trim()) {
77
+ throw new TypeError('Informe o novo texto da mensagem')
78
+ }
79
+ return socket.sendMessage(jid, {
80
+ text,
81
+ edit: { ...messageKey, remoteJid: jid, fromMe: true }
82
+ })
83
+ },
60
84
  /**
61
85
  * Consulta um @username pelo USync do WhatsApp, com PIN opcional.
62
86
  * Não revela nem infere o número de telefone.
@@ -10,6 +10,61 @@ import type { LabelAssociation } from './LabelAssociation.js';
10
10
  import type { MessageUpsertType, MessageUserReceiptUpdate, WAMessage, WAMessageKey, WAMessageUpdate } from './Message.js';
11
11
  import type { ConnectionState, NewChatMessageCapInfo } from './State.js';
12
12
  export type StoneWPPEventMap = {
13
+ /** Sinal da recepção nativa de ciphertext (não confirma ataque). */
14
+ 'group-protection.ciphertext': {
15
+ remoteJid: string;
16
+ messageId: string;
17
+ participant: string;
18
+ participantAlt?: string;
19
+ decryptFail?: string | null;
20
+ encType?: string | null;
21
+ messageTimestamp?: number | bigint | object | null;
22
+ receivedAt: number;
23
+ };
24
+ /** Sinal da recepção nativa de ciphertext (não confirma ataque). */
25
+ 'tokito.security.ciphertext': {
26
+ remoteJid: string;
27
+ messageId: string;
28
+ participant: string;
29
+ participantAlt?: string;
30
+ decryptFail?: string | null;
31
+ encType?: string | null;
32
+ messageTimestamp?: number | bigint | object | null;
33
+ receivedAt: number;
34
+ };
35
+ /** Proteção de grupos: alerta de anomalia ou flood. */
36
+ 'group-protection.alert': import('../Socket/group-protection.js').GroupProtectionAlert;
37
+ /** Ação da proteção: resultado real da tentativa. */
38
+ 'group-protection.action': import('../Socket/group-protection.js').GroupProtectionAction;
39
+ /** Recibo de retry de grupo, sem conteúdo e sem provar ataque. */
40
+ 'group-protection.receipt': {
41
+ remoteJid: string;
42
+ messageId: string;
43
+ participant: string;
44
+ type: 'retry';
45
+ receivedAt: number;
46
+ };
47
+ /** Telemetria nativa do PDO de placeholder resend. */
48
+ 'group-protection.pdo': {
49
+ remoteJid: string;
50
+ messageId: string;
51
+ status: 'requested' | 'resolved' | 'timeout';
52
+ };
53
+ /** Notificação filtrada de PDO de grupos monitorados. */
54
+ 'group-protection.pdo-observed': {
55
+ jid: string;
56
+ messageId: string;
57
+ status: string;
58
+ source: string;
59
+ receivedAt: number;
60
+ };
61
+ /** Estado de sockets complementares gerenciados pela aplicação. */
62
+ 'group-protection.monitor': {
63
+ jid: string;
64
+ index: number;
65
+ status: 'attached' | 'connected' | 'logged-out' | 'error';
66
+ error?: string;
67
+ };
13
68
  /** connection state has been updated -- WS closed, opened, connecting etc. */
14
69
  'connection.update': Partial<ConnectionState>;
15
70
  /** credentials updated -- some metadata, keys or something */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stone-wpp/ofc",
3
3
  "type": "module",
4
- "version": "1.0.5",
4
+ "version": "1.0.6",
5
5
  "description": "Stone WPP — biblioteca modular para WhatsApp em Node.js: conexão, mensagens, grupos, canais, armazenamento e extensões",
6
6
  "keywords": [
7
7
  "stone-wpp",
@@ -26,10 +26,13 @@
26
26
  "Protocol/**/*",
27
27
  "engine-requirements.js",
28
28
  "README.md",
29
- "LICENSE"
29
+ "LICENSE",
30
+ "docs/**/*"
30
31
  ],
31
32
  "scripts": {
32
- "preinstall": "node ./engine-requirements.js"
33
+ "preinstall": "node ./engine-requirements.js",
34
+ "test": "node --test test/*.test.mjs",
35
+ "prepublishOnly": "npm test"
33
36
  },
34
37
  "dependencies": {
35
38
  "@cacheable/node-cache": "^1.4.0",
@@ -95,6 +98,9 @@
95
98
  "node": ">=20.0.0"
96
99
  },
97
100
  "private": false,
101
+ "publishConfig": {
102
+ "access": "public"
103
+ },
98
104
  "repository": {
99
105
  "type": "git",
100
106
  "url": "git+https://github.com/dylan-Modz/stone-wpp-ofc.git"