zapo-js 1.2.1 → 1.3.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.
Files changed (38) hide show
  1. package/dist/client/WaClient.d.ts +84 -49
  2. package/dist/client/WaClient.js +70 -50
  3. package/dist/client/WaClientFactory.d.ts +7 -0
  4. package/dist/client/WaClientFactory.js +1 -0
  5. package/dist/client/plugins/define.d.ts +23 -0
  6. package/dist/client/plugins/define.js +22 -0
  7. package/dist/client/plugins/index.d.ts +4 -0
  8. package/dist/client/plugins/index.js +9 -0
  9. package/dist/client/plugins/install.d.ts +18 -0
  10. package/dist/client/plugins/install.js +83 -0
  11. package/dist/client/plugins/types.d.ts +76 -0
  12. package/dist/client/plugins/types.js +6 -0
  13. package/dist/client/types.d.ts +6 -0
  14. package/dist/esm/client/WaClient.js +70 -49
  15. package/dist/esm/client/WaClientFactory.js +1 -0
  16. package/dist/esm/client/plugins/define.js +19 -0
  17. package/dist/esm/client/plugins/index.js +3 -0
  18. package/dist/esm/client/plugins/install.js +80 -0
  19. package/dist/esm/client/plugins/types.js +3 -0
  20. package/dist/esm/index.js +2 -0
  21. package/dist/esm/infra/log/ConsoleLogger.js +3 -9
  22. package/dist/esm/infra/log/PinoLogger.js +24 -12
  23. package/dist/esm/infra/log/types.js +8 -1
  24. package/dist/esm/transport/index.js +2 -0
  25. package/dist/esm/util/index.js +1 -1
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.js +7 -2
  28. package/dist/infra/log/ConsoleLogger.d.ts +4 -2
  29. package/dist/infra/log/ConsoleLogger.js +5 -11
  30. package/dist/infra/log/PinoLogger.d.ts +4 -2
  31. package/dist/infra/log/PinoLogger.js +24 -12
  32. package/dist/infra/log/types.d.ts +8 -1
  33. package/dist/infra/log/types.js +9 -1
  34. package/dist/transport/index.d.ts +3 -0
  35. package/dist/transport/index.js +11 -1
  36. package/dist/util/index.d.ts +1 -1
  37. package/dist/util/index.js +4 -1
  38. package/package.json +1 -1
@@ -13,58 +13,12 @@ import type { WaPresenceCoordinator } from './coordinators/WaPresenceCoordinator
13
13
  import type { WaPrivacyCoordinator } from './coordinators/WaPrivacyCoordinator';
14
14
  import type { WaProfileCoordinator } from './coordinators/WaProfileCoordinator';
15
15
  import type { WaStatusCoordinator } from './coordinators/WaStatusCoordinator';
16
+ import type { WaClientExposedFromPlugins, WaClientPluginDefinition, WaClientPluginEventsFromPlugins } from './plugins/types';
16
17
  import type { WaClientEventMap, WaClientOptions, WaIgnoreKey, WaIgnoreKeyPredicate } from './types';
17
18
  import type { Logger } from '../infra/log/types';
18
19
  import { type WaLogoutReason } from '../protocol/stream';
19
- /**
20
- * Top-level WhatsApp client. Owns the transport, auth, signal, and per-feature
21
- * coordinators (accessible via getters such as {@link message}, {@link group},
22
- * {@link newsletter}, etc.) and re-emits every {@link WaClientEventMap} event.
23
- *
24
- * Lifecycle: construct with {@link WaClientOptions}, call {@link connect} to
25
- * open the socket, react to `connection`/`auth_qr`/`auth_pairing_code` events,
26
- * then use the coordinator getters to drive the session. Call {@link disconnect}
27
- * to shut down cleanly or {@link logout} to remove the companion device.
28
- *
29
- * @example
30
- * ```ts
31
- * import { createPinoLogger, createStore, WaClient } from '..'
32
- * import { createSqliteStore } from '@zapo-js/store-sqlite'
33
- *
34
- * const store = createStore({
35
- * backends: { sqlite: createSqliteStore({ path: '.auth/state.sqlite' }) },
36
- * providers: {
37
- * auth: 'sqlite',
38
- * signal: 'sqlite',
39
- * preKey: 'sqlite',
40
- * session: 'sqlite',
41
- * identity: 'sqlite',
42
- * senderKey: 'sqlite',
43
- * appState: 'sqlite',
44
- * privacyToken: 'sqlite',
45
- * messages: 'sqlite',
46
- * threads: 'sqlite',
47
- * contacts: 'sqlite'
48
- * }
49
- * })
50
- *
51
- * const client = new WaClient(
52
- * { store, sessionId: 'default' },
53
- * await createPinoLogger({ level: 'info', pretty: true })
54
- * )
55
- *
56
- * client.on('auth_qr', ({ qr, ttlMs }) => console.log('scan:', qr, ttlMs))
57
- * client.on('connection', (event) => console.log('connection', event))
58
- * client.on('message', async (event) => {
59
- * if (event.message?.conversation === 'ping') {
60
- * await client.message.send(event.chatJid!, 'pong')
61
- * }
62
- * })
63
- *
64
- * await client.connect()
65
- * ```
66
- */
67
- export declare class WaClient extends EventEmitter {
20
+ /** @internal Implementation backing the exported {@link WaClient}. */
21
+ declare class WaClientImpl extends EventEmitter {
68
22
  private readonly options;
69
23
  private readonly logger;
70
24
  private readonly stores;
@@ -76,6 +30,7 @@ export declare class WaClient extends EventEmitter {
76
30
  private acceptingIncomingEvents;
77
31
  private activeIncomingHandlers;
78
32
  private readonly incomingHandlersDrainedWaiters;
33
+ private disposePlugins;
79
34
  /**
80
35
  * @param options Client configuration (store, transport, addons, history...).
81
36
  * @param logger Optional structured logger. Defaults to a `ConsoleLogger('info')`.
@@ -230,3 +185,83 @@ export declare class WaClient extends EventEmitter {
230
185
  private notifyIncomingHandlersDrained;
231
186
  private handleError;
232
187
  }
188
+ /**
189
+ * A WhatsApp client instance: per-feature coordinator getters ({@link message},
190
+ * {@link group}, {@link newsletter}, ...) plus typed `on`/`once`/`off`/`emit`.
191
+ *
192
+ * `TPluginEvents` carries the events of the plugins installed on this client.
193
+ * Prefer deriving the precise type from the construction site (e.g.
194
+ * `type AppClient = ReturnType<typeof createClient>`) over the bare `WaClient`,
195
+ * which only knows the core events and no plugin getters.
196
+ */
197
+ export interface WaClient<TPluginEvents = {}> extends WaClientImpl {
198
+ on<K extends keyof (WaClientEventMap & TPluginEvents)>(event: K, listener: (WaClientEventMap & TPluginEvents)[K]): this;
199
+ once<K extends keyof (WaClientEventMap & TPluginEvents)>(event: K, listener: (WaClientEventMap & TPluginEvents)[K]): this;
200
+ off<K extends keyof (WaClientEventMap & TPluginEvents)>(event: K, listener: (WaClientEventMap & TPluginEvents)[K]): this;
201
+ emit<K extends keyof (WaClientEventMap & TPluginEvents)>(event: K, payload: Parameters<Extract<(WaClientEventMap & TPluginEvents)[K], (...args: never[]) => unknown>>[0]): boolean;
202
+ }
203
+ /**
204
+ * Constructor surface for {@link WaClient}. `new WaClient({ plugins })` infers the
205
+ * exposed plugin getters (e.g. `client.voip`) from the plugin values passed:
206
+ * no global type augmentation, so a getter exists only when its plugin is installed.
207
+ */
208
+ export interface WaClientConstructor {
209
+ new <const P extends readonly WaClientPluginDefinition[] = []>(options: Omit<WaClientOptions, 'plugins'> & {
210
+ readonly plugins?: P;
211
+ }, logger?: Logger): WaClient<WaClientPluginEventsFromPlugins<P>> & WaClientExposedFromPlugins<P>;
212
+ }
213
+ /**
214
+ * Top-level WhatsApp client. Owns the transport, auth, signal, and per-feature
215
+ * coordinators (accessible via getters such as {@link message}, {@link group},
216
+ * {@link newsletter}, etc.) and re-emits every {@link WaClientEventMap} event.
217
+ *
218
+ * Lifecycle: construct with {@link WaClientOptions}, call {@link connect} to
219
+ * open the socket, react to `connection`/`auth_qr`/`auth_pairing_code` events,
220
+ * then use the coordinator getters to drive the session. Call {@link disconnect}
221
+ * to shut down cleanly or {@link logout} to remove the companion device.
222
+ *
223
+ * Pass `plugins` to expose plugin getters and events on the returned client,
224
+ * typed from the values you pass: e.g. `plugins: [voipPlugin()]` adds
225
+ * `client.voip` and the `voip_*` events, and only then. See
226
+ * {@link WaClientConstructor}.
227
+ *
228
+ * @example
229
+ * ```ts
230
+ * import { createPinoLogger, createStore, WaClient } from '..'
231
+ * import { createSqliteStore } from '@zapo-js/store-sqlite'
232
+ *
233
+ * const store = createStore({
234
+ * backends: { sqlite: createSqliteStore({ path: '.auth/state.sqlite' }) },
235
+ * providers: {
236
+ * auth: 'sqlite',
237
+ * signal: 'sqlite',
238
+ * preKey: 'sqlite',
239
+ * session: 'sqlite',
240
+ * identity: 'sqlite',
241
+ * senderKey: 'sqlite',
242
+ * appState: 'sqlite',
243
+ * privacyToken: 'sqlite',
244
+ * messages: 'sqlite',
245
+ * threads: 'sqlite',
246
+ * contacts: 'sqlite'
247
+ * }
248
+ * })
249
+ *
250
+ * const client = new WaClient(
251
+ * { store, sessionId: 'default' },
252
+ * await createPinoLogger({ level: 'info', pretty: true })
253
+ * )
254
+ *
255
+ * client.on('auth_qr', ({ qr, ttlMs }) => console.log('scan:', qr, ttlMs))
256
+ * client.on('connection', (event) => console.log('connection', event))
257
+ * client.on('message', async (event) => {
258
+ * if (event.message?.conversation === 'ping') {
259
+ * await client.message.send(event.chatJid!, 'pong')
260
+ * }
261
+ * })
262
+ *
263
+ * await client.connect()
264
+ * ```
265
+ */
266
+ export declare const WaClient: WaClientConstructor;
267
+ export {};
@@ -6,6 +6,7 @@ const ignore_key_1 = require("./messaging/ignore-key");
6
6
  const history_sync_1 = require("./persistence/history-sync");
7
7
  const mailbox_1 = require("./persistence/mailbox");
8
8
  const WriteBehindPersistence_1 = require("./persistence/WriteBehindPersistence");
9
+ const install_1 = require("./plugins/install");
9
10
  const WaClientFactory_1 = require("./WaClientFactory");
10
11
  const ConsoleLogger_1 = require("../infra/log/ConsoleLogger");
11
12
  const _proto_1 = require("../proto");
@@ -22,55 +23,8 @@ const SYNC_RELATED_PROTOCOL_TYPES = new Set([
22
23
  _proto_1.proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_MESSAGE,
23
24
  _proto_1.proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE
24
25
  ]);
25
- /**
26
- * Top-level WhatsApp client. Owns the transport, auth, signal, and per-feature
27
- * coordinators (accessible via getters such as {@link message}, {@link group},
28
- * {@link newsletter}, etc.) and re-emits every {@link WaClientEventMap} event.
29
- *
30
- * Lifecycle: construct with {@link WaClientOptions}, call {@link connect} to
31
- * open the socket, react to `connection`/`auth_qr`/`auth_pairing_code` events,
32
- * then use the coordinator getters to drive the session. Call {@link disconnect}
33
- * to shut down cleanly or {@link logout} to remove the companion device.
34
- *
35
- * @example
36
- * ```ts
37
- * import { createPinoLogger, createStore, WaClient } from '..'
38
- * import { createSqliteStore } from '@zapo-js/store-sqlite'
39
- *
40
- * const store = createStore({
41
- * backends: { sqlite: createSqliteStore({ path: '.auth/state.sqlite' }) },
42
- * providers: {
43
- * auth: 'sqlite',
44
- * signal: 'sqlite',
45
- * preKey: 'sqlite',
46
- * session: 'sqlite',
47
- * identity: 'sqlite',
48
- * senderKey: 'sqlite',
49
- * appState: 'sqlite',
50
- * privacyToken: 'sqlite',
51
- * messages: 'sqlite',
52
- * threads: 'sqlite',
53
- * contacts: 'sqlite'
54
- * }
55
- * })
56
- *
57
- * const client = new WaClient(
58
- * { store, sessionId: 'default' },
59
- * await createPinoLogger({ level: 'info', pretty: true })
60
- * )
61
- *
62
- * client.on('auth_qr', ({ qr, ttlMs }) => console.log('scan:', qr, ttlMs))
63
- * client.on('connection', (event) => console.log('connection', event))
64
- * client.on('message', async (event) => {
65
- * if (event.message?.conversation === 'ping') {
66
- * await client.message.send(event.chatJid!, 'pong')
67
- * }
68
- * })
69
- *
70
- * await client.connect()
71
- * ```
72
- */
73
- class WaClient extends node_events_1.EventEmitter {
26
+ /** @internal Implementation backing the exported {@link WaClient}. */
27
+ class WaClientImpl extends node_events_1.EventEmitter {
74
28
  /**
75
29
  * @param options Client configuration (store, transport, addons, history...).
76
30
  * @param logger Optional structured logger. Defaults to a `ConsoleLogger('info')`.
@@ -81,6 +35,7 @@ class WaClient extends node_events_1.EventEmitter {
81
35
  this.acceptingIncomingEvents = true;
82
36
  this.activeIncomingHandlers = 0;
83
37
  this.incomingHandlersDrainedWaiters = [];
38
+ this.disposePlugins = null;
84
39
  const base = (0, WaClientFactory_1.resolveWaClientBase)(options, logger);
85
40
  this.options = base.options;
86
41
  this.logger = base.logger;
@@ -118,6 +73,13 @@ class WaClient extends node_events_1.EventEmitter {
118
73
  this.deps = dependencies;
119
74
  this.appStateSync = dependencies.appStateSync;
120
75
  this.mediaTransfer = dependencies.mediaTransfer;
76
+ this.disposePlugins = (0, install_1.installWaClientPlugins)(this, {
77
+ options: this.options,
78
+ logger: this.logger,
79
+ stores: this.stores,
80
+ deps: this.deps,
81
+ queryWithContext: this.queryWithContext.bind(this)
82
+ }, this.options.plugins ?? []);
121
83
  this.bindNodeTransportEvents();
122
84
  this.on('connection', (event) => {
123
85
  if (event.status !== 'close')
@@ -384,6 +346,11 @@ class WaClient extends node_events_1.EventEmitter {
384
346
  remaining: writeBehindFlush.remaining
385
347
  });
386
348
  }
349
+ if (this.disposePlugins) {
350
+ const dispose = this.disposePlugins;
351
+ this.disposePlugins = null;
352
+ await dispose();
353
+ }
387
354
  await this.deps.connectionManager.disconnect();
388
355
  this.emit('connection', {
389
356
  status: 'close',
@@ -606,4 +573,57 @@ class WaClient extends node_events_1.EventEmitter {
606
573
  this.emit('debug_client_error', { error });
607
574
  }
608
575
  }
609
- exports.WaClient = WaClient;
576
+ /**
577
+ * Top-level WhatsApp client. Owns the transport, auth, signal, and per-feature
578
+ * coordinators (accessible via getters such as {@link message}, {@link group},
579
+ * {@link newsletter}, etc.) and re-emits every {@link WaClientEventMap} event.
580
+ *
581
+ * Lifecycle: construct with {@link WaClientOptions}, call {@link connect} to
582
+ * open the socket, react to `connection`/`auth_qr`/`auth_pairing_code` events,
583
+ * then use the coordinator getters to drive the session. Call {@link disconnect}
584
+ * to shut down cleanly or {@link logout} to remove the companion device.
585
+ *
586
+ * Pass `plugins` to expose plugin getters and events on the returned client,
587
+ * typed from the values you pass: e.g. `plugins: [voipPlugin()]` adds
588
+ * `client.voip` and the `voip_*` events, and only then. See
589
+ * {@link WaClientConstructor}.
590
+ *
591
+ * @example
592
+ * ```ts
593
+ * import { createPinoLogger, createStore, WaClient } from '..'
594
+ * import { createSqliteStore } from '@zapo-js/store-sqlite'
595
+ *
596
+ * const store = createStore({
597
+ * backends: { sqlite: createSqliteStore({ path: '.auth/state.sqlite' }) },
598
+ * providers: {
599
+ * auth: 'sqlite',
600
+ * signal: 'sqlite',
601
+ * preKey: 'sqlite',
602
+ * session: 'sqlite',
603
+ * identity: 'sqlite',
604
+ * senderKey: 'sqlite',
605
+ * appState: 'sqlite',
606
+ * privacyToken: 'sqlite',
607
+ * messages: 'sqlite',
608
+ * threads: 'sqlite',
609
+ * contacts: 'sqlite'
610
+ * }
611
+ * })
612
+ *
613
+ * const client = new WaClient(
614
+ * { store, sessionId: 'default' },
615
+ * await createPinoLogger({ level: 'info', pretty: true })
616
+ * )
617
+ *
618
+ * client.on('auth_qr', ({ qr, ttlMs }) => console.log('scan:', qr, ttlMs))
619
+ * client.on('connection', (event) => console.log('connection', event))
620
+ * client.on('message', async (event) => {
621
+ * if (event.message?.conversation === 'ping') {
622
+ * await client.message.send(event.chatJid!, 'pong')
623
+ * }
624
+ * })
625
+ *
626
+ * await client.connect()
627
+ * ```
628
+ */
629
+ exports.WaClient = WaClientImpl;
@@ -36,6 +36,7 @@ import { SignalMissingPreKeysSyncApi } from '../signal/api/SignalMissingPreKeysS
36
36
  import { SignalRotateKeyApi } from '../signal/api/SignalRotateKeyApi';
37
37
  import { SignalSessionSyncApi } from '../signal/api/SignalSessionSyncApi';
38
38
  import { SenderKeyManager } from '../signal/group/SenderKeyManager';
39
+ import { type SignalSessionResolver } from '../signal/session/resolver';
39
40
  import { SignalProtocol } from '../signal/session/SignalProtocol';
40
41
  import type { WaStoredContactRecord } from '../store/contracts/contact.store';
41
42
  import { WaKeepAlive } from '../transport/keepalive/WaKeepAlive';
@@ -72,6 +73,11 @@ interface WaClientBuildRuntime {
72
73
  */
73
74
  readonly persistContact: (record: WaStoredContactRecord) => void;
74
75
  }
76
+ /**
77
+ * Internal coordinator graph wired by {@link buildWaClientDependencies}. Exposed
78
+ * to {@link WaClientPluginContext.deps} for plugin authors – advanced API; new
79
+ * coordinators may appear in minor releases.
80
+ */
75
81
  export interface WaClientDependencies {
76
82
  readonly nodeTransport: WaNodeTransport;
77
83
  readonly nodeOrchestrator: WaNodeOrchestrator;
@@ -87,6 +93,7 @@ export interface WaClientDependencies {
87
93
  readonly signalMissingPreKeysSync: SignalMissingPreKeysSyncApi;
88
94
  readonly signalRotateKey: SignalRotateKeyApi;
89
95
  readonly signalSessionSync: SignalSessionSyncApi;
96
+ readonly sessionResolver: SignalSessionResolver;
90
97
  readonly authClient: WaAuthClient;
91
98
  readonly messageDispatch: WaMessageDispatchCoordinator;
92
99
  readonly messageCoordinator: WaMessageCoordinator;
@@ -1030,6 +1030,7 @@ function buildWaClientDependencies(input) {
1030
1030
  signalMissingPreKeysSync,
1031
1031
  signalRotateKey,
1032
1032
  signalSessionSync,
1033
+ sessionResolver,
1033
1034
  authClient,
1034
1035
  messageDispatch,
1035
1036
  messageCoordinator,
@@ -0,0 +1,23 @@
1
+ import type { WaClientPluginContext, WaClientPluginDefinition } from '../plugins/types';
2
+ import type { WaClient } from '../WaClient';
3
+ interface WaClientBehaviorPluginInput {
4
+ readonly id: string;
5
+ readonly setup: (ctx: WaClientPluginContext) => void;
6
+ readonly dispose?: (ctx: WaClientPluginContext) => void | Promise<void>;
7
+ }
8
+ interface WaClientExposePluginInput<K extends string, T> {
9
+ readonly id: string;
10
+ readonly exposeAs: K;
11
+ readonly setup: (ctx: WaClientPluginContext) => T;
12
+ readonly dispose?: (instance: T, ctx: WaClientPluginContext) => void | Promise<void>;
13
+ }
14
+ /** Type-safe helper for authoring {@link WaClientPluginDefinition} values. */
15
+ export declare function defineWaClientPlugin(input: WaClientBehaviorPluginInput): WaClientPluginDefinition;
16
+ export declare function defineWaClientPlugin<K extends string, T, E = {}>(input: WaClientExposePluginInput<K, T> & {
17
+ readonly exposeAs: K extends keyof WaClient ? never : K;
18
+ }): WaClientPluginDefinition & {
19
+ readonly exposeAs: K;
20
+ readonly setup: (ctx: WaClientPluginContext) => T;
21
+ readonly __pluginEvents?: E;
22
+ };
23
+ export {};
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defineWaClientPlugin = defineWaClientPlugin;
4
+ function defineWaClientPlugin(input) {
5
+ if ('exposeAs' in input && input.exposeAs !== undefined) {
6
+ const exposeInput = input;
7
+ return {
8
+ id: exposeInput.id,
9
+ exposeAs: exposeInput.exposeAs,
10
+ setup: exposeInput.setup,
11
+ dispose: exposeInput.dispose
12
+ ? (instance, ctx) => exposeInput.dispose(instance, ctx)
13
+ : undefined
14
+ };
15
+ }
16
+ const behaviorInput = input;
17
+ return {
18
+ id: behaviorInput.id,
19
+ setup: behaviorInput.setup,
20
+ dispose: behaviorInput.dispose ? (_instance, ctx) => behaviorInput.dispose(ctx) : undefined
21
+ };
22
+ }
@@ -0,0 +1,4 @@
1
+ export { defineWaClientPlugin } from '../plugins/define';
2
+ export { installWaClientPlugins, type WaClientPluginInstallInput } from '../plugins/install';
3
+ export type { WaClientPluginContext, WaClientPluginDefinition } from '../plugins/types';
4
+ export { isWaClientExposePluginDefinition } from '../plugins/types';
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isWaClientExposePluginDefinition = exports.installWaClientPlugins = exports.defineWaClientPlugin = void 0;
4
+ var define_1 = require("../plugins/define");
5
+ Object.defineProperty(exports, "defineWaClientPlugin", { enumerable: true, get: function () { return define_1.defineWaClientPlugin; } });
6
+ var install_1 = require("../plugins/install");
7
+ Object.defineProperty(exports, "installWaClientPlugins", { enumerable: true, get: function () { return install_1.installWaClientPlugins; } });
8
+ var types_1 = require("../plugins/types");
9
+ Object.defineProperty(exports, "isWaClientExposePluginDefinition", { enumerable: true, get: function () { return types_1.isWaClientExposePluginDefinition; } });
@@ -0,0 +1,18 @@
1
+ import { type WaClientPluginContext, type WaClientPluginDefinition } from '../plugins/types';
2
+ import type { WaClientOptions } from '../types';
3
+ import type { WaClient } from '../WaClient';
4
+ import type { WaClientDependencies } from '../WaClientFactory';
5
+ import type { Logger } from '../../infra/log/types';
6
+ import type { WaStore } from '../../store/types';
7
+ export interface WaClientPluginInstallInput {
8
+ readonly options: Readonly<WaClientOptions>;
9
+ readonly logger: Logger;
10
+ readonly stores: ReturnType<WaStore['session']>;
11
+ readonly deps: WaClientDependencies;
12
+ readonly queryWithContext: WaClientPluginContext['queryWithContext'];
13
+ }
14
+ /**
15
+ * Installs {@link WaClientOptions.plugins} on `client`. Returns a dispose
16
+ * function invoked by {@link WaClient.disconnect}.
17
+ */
18
+ export declare function installWaClientPlugins(client: WaClient, input: WaClientPluginInstallInput, plugins: readonly WaClientPluginDefinition[]): () => Promise<void>;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installWaClientPlugins = installWaClientPlugins;
4
+ const types_1 = require("../plugins/types");
5
+ const primitives_1 = require("../../util/primitives");
6
+ /**
7
+ * Installs {@link WaClientOptions.plugins} on `client`. Returns a dispose
8
+ * function invoked by {@link WaClient.disconnect}.
9
+ */
10
+ function installWaClientPlugins(client, input, plugins) {
11
+ const seenIds = new Set();
12
+ const seenExposeAs = new Set();
13
+ const disposeCallbacks = [];
14
+ const registerDispose = (fn) => {
15
+ disposeCallbacks[disposeCallbacks.length] = fn;
16
+ };
17
+ const baseCtx = {
18
+ client,
19
+ options: input.options,
20
+ logger: input.logger,
21
+ stores: input.stores,
22
+ deps: input.deps,
23
+ emit: client.emit.bind(client),
24
+ on: client.on.bind(client),
25
+ off: client.off.bind(client),
26
+ once: client.once.bind(client),
27
+ queryWithContext: input.queryWithContext,
28
+ registerIncomingHandler: (registration) => input.deps.lowLevelCoordinator.registerIncomingHandler(registration),
29
+ registerIncomingStanzaFilter: (filter) => input.deps.lowLevelCoordinator.registerIncomingStanzaFilter(filter),
30
+ registerDispose
31
+ };
32
+ for (let index = 0; index < plugins.length; index += 1) {
33
+ const plugin = plugins[index];
34
+ if (seenIds.has(plugin.id)) {
35
+ throw new Error(`duplicate wa client plugin id: ${plugin.id}`);
36
+ }
37
+ seenIds.add(plugin.id);
38
+ const pluginCtx = {
39
+ ...baseCtx,
40
+ logger: input.logger.child({ plugin: plugin.id })
41
+ };
42
+ if ((0, types_1.isWaClientExposePluginDefinition)(plugin)) {
43
+ if (seenExposeAs.has(plugin.exposeAs)) {
44
+ throw new Error(`duplicate wa client plugin exposeAs: ${plugin.exposeAs}`);
45
+ }
46
+ seenExposeAs.add(plugin.exposeAs);
47
+ if (plugin.exposeAs in client) {
48
+ throw new Error(`wa client plugin exposeAs "${plugin.exposeAs}" collides with a reserved client member`);
49
+ }
50
+ const instance = plugin.setup(pluginCtx);
51
+ Object.defineProperty(client, plugin.exposeAs, {
52
+ get: () => instance,
53
+ enumerable: true,
54
+ configurable: false
55
+ });
56
+ if (plugin.dispose) {
57
+ const dispose = plugin.dispose;
58
+ registerDispose(() => dispose(instance, pluginCtx));
59
+ }
60
+ pluginCtx.logger.debug('wa client plugin installed', { exposeAs: plugin.exposeAs });
61
+ }
62
+ else {
63
+ plugin.setup(pluginCtx);
64
+ if (plugin.dispose) {
65
+ const dispose = plugin.dispose;
66
+ registerDispose(() => dispose(undefined, pluginCtx));
67
+ }
68
+ pluginCtx.logger.debug('wa client plugin installed');
69
+ }
70
+ }
71
+ return async () => {
72
+ for (let index = disposeCallbacks.length - 1; index >= 0; index -= 1) {
73
+ try {
74
+ await disposeCallbacks[index]();
75
+ }
76
+ catch (error) {
77
+ input.logger.warn('wa client plugin dispose failed', {
78
+ message: (0, primitives_1.toError)(error).message
79
+ });
80
+ }
81
+ }
82
+ };
83
+ }
@@ -0,0 +1,76 @@
1
+ import type { WaLowLevelCoordinator } from '../coordinators/WaLowLevelCoordinator';
2
+ import type { WaClientOptions } from '../types';
3
+ import type { WaClient } from '../WaClient';
4
+ import type { WaClientDependencies } from '../WaClientFactory';
5
+ import type { Logger } from '../../infra/log/types';
6
+ import type { WaStore } from '../../store/types';
7
+ import type { BinaryNode } from '../../transport/types';
8
+ /**
9
+ * Host context passed to every {@link WaClientPluginDefinition.setup}. Carries
10
+ * the full {@link WaClientDependencies} graph plus event/handler helpers.
11
+ *
12
+ * @sensitive deps may reach key material through nested coordinators – do not
13
+ * log or persist deps wholesale.
14
+ */
15
+ export interface WaClientPluginContext {
16
+ readonly client: WaClient;
17
+ readonly options: Readonly<WaClientOptions>;
18
+ readonly logger: Logger;
19
+ readonly stores: ReturnType<WaStore['session']>;
20
+ /**
21
+ * Full coordinator dependency graph. Advanced API for plugin authors –
22
+ * new coordinators may appear in minor releases.
23
+ */
24
+ readonly deps: WaClientDependencies;
25
+ /** Loose so a plugin can emit its own events; consumers see them typed on the client. */
26
+ readonly emit: (event: string | symbol, ...args: unknown[]) => boolean;
27
+ readonly on: WaClient['on'];
28
+ readonly off: WaClient['off'];
29
+ readonly once: WaClient['once'];
30
+ readonly queryWithContext: (context: string, node: BinaryNode, timeoutMs?: number, contextData?: Readonly<Record<string, unknown>>, options?: {
31
+ readonly useSystemId?: boolean;
32
+ }) => Promise<BinaryNode>;
33
+ readonly registerIncomingHandler: WaLowLevelCoordinator['registerIncomingHandler'];
34
+ readonly registerIncomingStanzaFilter: WaLowLevelCoordinator['registerIncomingStanzaFilter'];
35
+ /** Runs on {@link WaClient.disconnect} after incoming handlers drain. */
36
+ readonly registerDispose: (fn: () => void | Promise<void>) => void;
37
+ }
38
+ /**
39
+ * Runtime plugin registration. Use {@link defineWaClientPlugin} for inference.
40
+ * When `exposeAs` is set, `setup` should return the value exposed at
41
+ * `client[exposeAs]`; otherwise only side effects (handlers, listeners) run.
42
+ */
43
+ export interface WaClientPluginDefinition {
44
+ readonly id: string;
45
+ readonly exposeAs?: string;
46
+ readonly setup: (ctx: WaClientPluginContext) => unknown;
47
+ readonly dispose?: (instance: unknown, ctx: WaClientPluginContext) => void | Promise<void>;
48
+ }
49
+ export declare function isWaClientExposePluginDefinition(plugin: WaClientPluginDefinition): plugin is WaClientPluginDefinition & {
50
+ readonly exposeAs: string;
51
+ };
52
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
53
+ /** `{ [exposeAs]: setup-return }` for one plugin definition; `{}` (no-op) for behavior plugins. */
54
+ type ExposedOf<P> = P extends {
55
+ readonly exposeAs: infer K extends string;
56
+ readonly setup: (...args: never[]) => infer T;
57
+ } ? {
58
+ readonly [Q in K]: T;
59
+ } : {};
60
+ /**
61
+ * Getters contributed by a tuple of plugin definitions, derived from the values
62
+ * passed to the client (no global augmentation). `[voipPlugin()]` yields
63
+ * `{ readonly voip: WaVoipCoordinator }`.
64
+ */
65
+ export type WaClientExposedFromPlugins<P extends readonly unknown[]> = UnionToIntersection<ExposedOf<P[number]>>;
66
+ /** Event map a plugin contributes, carried as a phantom marker on its definition. */
67
+ type EventsOf<P> = P extends {
68
+ readonly __pluginEvents?: infer E;
69
+ } ? unknown extends E ? {} : E : {};
70
+ /**
71
+ * Client events contributed by a tuple of plugin definitions, derived from the
72
+ * plugin values (no global augmentation). Threaded into the client's
73
+ * `on`/`once`/`off`/`emit` so a `voip_*` event exists only when voip is installed.
74
+ */
75
+ export type WaClientPluginEventsFromPlugins<P extends readonly unknown[]> = UnionToIntersection<EventsOf<P[number]>>;
76
+ export {};
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isWaClientExposePluginDefinition = isWaClientExposePluginDefinition;
4
+ function isWaClientExposePluginDefinition(plugin) {
5
+ return plugin.exposeAs !== undefined && plugin.exposeAs.length > 0;
6
+ }
@@ -3,6 +3,7 @@ import type { DataForKey, WaAppstateActionKey, WaAppstateIndexArgs } from '../ap
3
3
  import type { WaAuthClientOptions, WaAuthCredentials, WaAuthDangerousOptions, WaAuthSocketOptions } from '../auth/types';
4
4
  import type { WaCallGroupParticipant, WaCallType } from './events/call';
5
5
  import type { IncomingPresenceType, PresenceLastSeen } from './events/presence';
6
+ import type { WaClientPluginDefinition } from './plugins/types';
6
7
  import type { WaMediaProcessor } from '../media/processor';
7
8
  import type { WaLinkPreviewOptions } from '../message/addons/link-preview/types';
8
9
  import type { WaQuoteRef, WaSendContextInfo } from '../message/context-info';
@@ -173,6 +174,11 @@ export interface WaClientOptions extends WaAuthClientOptions, WaAuthSocketOption
173
174
  * default auto-fetch globally.
174
175
  */
175
176
  readonly linkPreview?: WaLinkPreviewOptions;
177
+ /**
178
+ * Optional client plugins – behavior hooks and/or coordinators exposed at
179
+ * `client[exposeAs]`. See {@link defineWaClientPlugin}.
180
+ */
181
+ readonly plugins?: readonly WaClientPluginDefinition[];
176
182
  /**
177
183
  * Test-only overrides intended for running against a fake server.
178
184
  *