@diegoaltoworks/chatter 0.40.0 → 0.42.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
|
@@ -76,6 +76,7 @@ Complete guides for setup, deployment, and integration:
|
|
|
76
76
|
- **[Deployment](./docs/deployment.md)** - Google Cloud Run, Fly.io, Railway, VPS
|
|
77
77
|
- **[Testing](./docs/testing.md)** - Comprehensive testing guide
|
|
78
78
|
- **[Code Quality](./docs/CODE_QUALITY.md)** - Security assessment and architecture review
|
|
79
|
+
- **[Sprint Review](./docs/sprint-review.md)** - Cross-module & security-invariant audit
|
|
79
80
|
- **[FAQs](./docs/faqs.md)** - Troubleshooting and common questions
|
|
80
81
|
|
|
81
82
|
## Demo
|
package/dist/bin/wa-pair.js
CHANGED
|
@@ -156,6 +156,8 @@ var MAX_PAIRING_RECONNECTS = 12;
|
|
|
156
156
|
var PAIRING_RECONNECT_DELAY_MS = 2e3;
|
|
157
157
|
var PAIRING_RECONNECT_MAX_DELAY_MS = 8e3;
|
|
158
158
|
var RESTART_REQUIRED_DELAY_MS = 500;
|
|
159
|
+
var REGISTERED_CONFIRM_TIMEOUT_MS = 15e3;
|
|
160
|
+
var REGISTERED_CONFIRM_POLL_MS = 500;
|
|
159
161
|
function pairingReconnectDelayMs(statusCode, attempt) {
|
|
160
162
|
if (statusCode === RESTART_REQUIRED_STATUS) return RESTART_REQUIRED_DELAY_MS;
|
|
161
163
|
return Math.min(
|
|
@@ -192,6 +194,41 @@ function runPairing(deps) {
|
|
|
192
194
|
resolve(result);
|
|
193
195
|
};
|
|
194
196
|
const status = (message) => deps.onStatus?.(message);
|
|
197
|
+
const wait = (ms) => new Promise((resolve2) => {
|
|
198
|
+
schedule(() => resolve2(), ms);
|
|
199
|
+
});
|
|
200
|
+
let saves = Promise.resolve();
|
|
201
|
+
const queueSave = (saveCreds) => {
|
|
202
|
+
saves = saves.then(saveCreds).catch((error) => {
|
|
203
|
+
status(`Failed to save the session credentials: ${describeError(error)}`);
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
const flushSaves = async () => {
|
|
207
|
+
let awaited;
|
|
208
|
+
while (awaited !== saves) {
|
|
209
|
+
awaited = saves;
|
|
210
|
+
await awaited;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const confirmRegistered = async () => {
|
|
214
|
+
await flushSaves();
|
|
215
|
+
const verify = deps.verifyRegistered;
|
|
216
|
+
if (!verify) return true;
|
|
217
|
+
const pollMs = deps.registeredPollMs ?? REGISTERED_CONFIRM_POLL_MS;
|
|
218
|
+
const polls = Math.max(
|
|
219
|
+
1,
|
|
220
|
+
Math.ceil((deps.registeredTimeoutMs ?? REGISTERED_CONFIRM_TIMEOUT_MS) / pollMs)
|
|
221
|
+
);
|
|
222
|
+
for (let poll = 0; poll < polls; poll++) {
|
|
223
|
+
if (await verify()) return true;
|
|
224
|
+
if (poll === polls - 1) break;
|
|
225
|
+
if (poll === 0)
|
|
226
|
+
status("Link is open \u2014 waiting for the session to be saved as registered...");
|
|
227
|
+
await wait(pollMs);
|
|
228
|
+
await flushSaves();
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
};
|
|
195
232
|
const requestCode = async (sock) => {
|
|
196
233
|
if (settled || codeRequested || !deps.phoneNumber) return;
|
|
197
234
|
try {
|
|
@@ -216,7 +253,15 @@ function runPairing(deps) {
|
|
|
216
253
|
}
|
|
217
254
|
}
|
|
218
255
|
if (connection === "open") {
|
|
219
|
-
|
|
256
|
+
if (await confirmRegistered()) {
|
|
257
|
+
settle({ ok: true, userId: sock.user?.id });
|
|
258
|
+
} else {
|
|
259
|
+
settle({
|
|
260
|
+
ok: false,
|
|
261
|
+
reason: "unregistered",
|
|
262
|
+
message: "The link opened but the stored session never read back as registered, so the server would not find it. Run wa-pair again (add --reset to pair from scratch)."
|
|
263
|
+
});
|
|
264
|
+
}
|
|
220
265
|
return;
|
|
221
266
|
}
|
|
222
267
|
if (connection === "close") {
|
|
@@ -252,13 +297,29 @@ function runPairing(deps) {
|
|
|
252
297
|
if (settled) return;
|
|
253
298
|
const mine = generation;
|
|
254
299
|
const attempt = attempts;
|
|
300
|
+
await flushSaves();
|
|
301
|
+
if (settled) return;
|
|
255
302
|
let sock;
|
|
303
|
+
let saveCreds;
|
|
256
304
|
try {
|
|
257
|
-
|
|
305
|
+
const opened = await deps.connect(attempt);
|
|
306
|
+
if ("sock" in opened) {
|
|
307
|
+
sock = opened.sock;
|
|
308
|
+
saveCreds = opened.saveCreds;
|
|
309
|
+
} else {
|
|
310
|
+
sock = opened;
|
|
311
|
+
}
|
|
258
312
|
} catch (error) {
|
|
259
313
|
settle({ ok: false, reason: "error", message: describeError(error) });
|
|
260
314
|
return;
|
|
261
315
|
}
|
|
316
|
+
if (saveCreds) {
|
|
317
|
+
const save = saveCreds;
|
|
318
|
+
sock.ev.on("creds.update", () => {
|
|
319
|
+
if (settled || mine !== generation) return;
|
|
320
|
+
queueSave(save);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
262
323
|
sock.ev.on("connection.update", (update) => {
|
|
263
324
|
if (mine !== generation || settled) return;
|
|
264
325
|
handleUpdate(update, sock).catch((error) => {
|
|
@@ -418,11 +479,15 @@ async function start() {
|
|
|
418
479
|
logger,
|
|
419
480
|
markOnlineOnConnect: false
|
|
420
481
|
});
|
|
421
|
-
sock
|
|
422
|
-
|
|
482
|
+
return { sock, saveCreds };
|
|
483
|
+
};
|
|
484
|
+
const storedIsRegistered = async () => {
|
|
485
|
+
const { state } = await useTursoAuthState(db, sessionSecret, sessionId, runtime);
|
|
486
|
+
return state.creds.registered === true;
|
|
423
487
|
};
|
|
424
488
|
const result = await runPairing({
|
|
425
489
|
connect,
|
|
490
|
+
verifyRegistered: storedIsRegistered,
|
|
426
491
|
loggedOutCode: baileys.DisconnectReason.loggedOut,
|
|
427
492
|
phoneNumber,
|
|
428
493
|
onQr: renderQr,
|
|
@@ -440,8 +505,10 @@ async function start() {
|
|
|
440
505
|
console.error(`\u274C ${result.message}`);
|
|
441
506
|
process.exit(1);
|
|
442
507
|
}
|
|
443
|
-
console.log(
|
|
444
|
-
|
|
508
|
+
console.log(
|
|
509
|
+
`
|
|
510
|
+
\u2705 Paired session "${sessionId}" as ${result.userId}. Session saved (encrypted) and verified registered.`
|
|
511
|
+
);
|
|
445
512
|
console.log("The running server will pick this session up on its next connect.");
|
|
446
513
|
process.exit(0);
|
|
447
514
|
}
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
* So a close during pairing is normal, not fatal: reconnect with stored creds,
|
|
15
15
|
* bounded, until `open` confirms the link. Only an explicit logout or running
|
|
16
16
|
* out of attempts ends the run.
|
|
17
|
+
*
|
|
18
|
+
* Credentials are persisted by this loop rather than by the caller's socket
|
|
19
|
+
* wiring, because the saves have to be ordered against the reconnects: the
|
|
20
|
+
* registration that a 515 interrupts is only useful if it reaches storage
|
|
21
|
+
* BEFORE the next attempt re-reads the session, and the flag that marks the
|
|
22
|
+
* session usable (`registered`) is often written by a `creds.update` that
|
|
23
|
+
* lands around or after `open`. A run therefore ends successfully only once
|
|
24
|
+
* the caller confirms — by reading storage back — that the stored session is
|
|
25
|
+
* registered.
|
|
17
26
|
*/
|
|
18
27
|
/** Baileys' "restart required" — emitted immediately after a QR scan registers the device. */
|
|
19
28
|
export declare const RESTART_REQUIRED_STATUS = 515;
|
|
@@ -52,7 +61,7 @@ export declare function decidePairingClose(input: PairingCloseInput): PairingClo
|
|
|
52
61
|
/** The slice of a Baileys socket the pairing loop uses. */
|
|
53
62
|
export interface PairingSocket {
|
|
54
63
|
ev: {
|
|
55
|
-
on: (event: "connection.update", listener: (update: PairingConnectionUpdate) => void) => void;
|
|
64
|
+
on: (event: "connection.update" | "creds.update", listener: (update: PairingConnectionUpdate) => void) => void;
|
|
56
65
|
};
|
|
57
66
|
user?: {
|
|
58
67
|
id?: string;
|
|
@@ -61,6 +70,16 @@ export interface PairingSocket {
|
|
|
61
70
|
/** Closes a superseded socket so it stops writing to the session. */
|
|
62
71
|
end?: (error?: Error) => void;
|
|
63
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* A socket plus the way to persist the credentials it mutates. Returning the
|
|
75
|
+
* pair (instead of wiring `creds.update` at the call site) lets the loop order
|
|
76
|
+
* saves against reconnects — see the module comment.
|
|
77
|
+
*/
|
|
78
|
+
export interface PairingAttempt {
|
|
79
|
+
sock: PairingSocket;
|
|
80
|
+
/** Writes this attempt's credentials to storage; called on every `creds.update`. */
|
|
81
|
+
saveCreds?: () => Promise<void>;
|
|
82
|
+
}
|
|
64
83
|
export interface PairingConnectionUpdate {
|
|
65
84
|
connection?: string;
|
|
66
85
|
qr?: string;
|
|
@@ -73,17 +92,29 @@ export type PairingResult = {
|
|
|
73
92
|
userId?: string;
|
|
74
93
|
} | {
|
|
75
94
|
ok: false;
|
|
76
|
-
reason: "loggedOut" | "exhausted" | "error";
|
|
95
|
+
reason: "loggedOut" | "exhausted" | "error" | "unregistered";
|
|
77
96
|
message: string;
|
|
78
97
|
};
|
|
79
98
|
export interface PairingRunDeps {
|
|
80
99
|
/**
|
|
81
|
-
* Opens a socket for `attempt` (0 = first). MUST
|
|
100
|
+
* Opens a socket for `attempt` (0 = first). MUST re-read the stored
|
|
82
101
|
* credentials — a reconnect that starts from blank creds shows a fresh QR
|
|
83
102
|
* instead of finishing the link the phone is waiting on.
|
|
84
103
|
*/
|
|
85
|
-
connect: (attempt: number) => PairingSocket | Promise<PairingSocket>;
|
|
104
|
+
connect: (attempt: number) => PairingSocket | PairingAttempt | Promise<PairingSocket | PairingAttempt>;
|
|
86
105
|
loggedOutCode: number;
|
|
106
|
+
/**
|
|
107
|
+
* Reads the STORED session back and answers whether it is registered — i.e.
|
|
108
|
+
* whether the server would find a usable session. Polled after `open` while
|
|
109
|
+
* late `creds.update` events are still being persisted; a run that never
|
|
110
|
+
* sees `true` fails with reason `unregistered` rather than claiming success
|
|
111
|
+
* over a session nothing can use. Omit to skip the check.
|
|
112
|
+
*/
|
|
113
|
+
verifyRegistered?: () => boolean | Promise<boolean>;
|
|
114
|
+
/** How long to wait for the stored session to read back registered. */
|
|
115
|
+
registeredTimeoutMs?: number;
|
|
116
|
+
/** Gap between those reads. */
|
|
117
|
+
registeredPollMs?: number;
|
|
87
118
|
/** Digits-only number for pairing-code mode; omit for QR mode. */
|
|
88
119
|
phoneNumber?: string;
|
|
89
120
|
maxAttempts?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/pairing.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/pairing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED