@glyphteck/veyl 0.1.0 → 0.1.2

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/src/client.js DELETED
@@ -1,499 +0,0 @@
1
- import { Buffer } from 'node:buffer';
2
- import process from 'node:process';
3
- import { SparkWallet } from '@buildonspark/spark-sdk';
4
- import {
5
- closeAccountSession,
6
- openRegistryAccountSession,
7
- } from '@veyl/shared/account/session';
8
- import { createAccountSessionActions } from '@veyl/shared/account/actions';
9
- import { packSeedData, unpackSeedData } from '@veyl/shared/crypto/pack';
10
- import { createSecretRegistry, decryptSeed, encryptSeedWithPassword, generateSeed } from '@veyl/shared/crypto/seed';
11
- import { resolveNetwork } from '@veyl/shared/network';
12
- import { isUsername, normalizeUsername } from '@veyl/shared/username';
13
- import { createHeadlessCloud, signInHeadless, signOutHeadless } from './cloud.js';
14
- import { deriveVaultKey } from './kdf.js';
15
- import { listenAccount } from './listen.js';
16
- import { createMachineCredential, signMachineChallenge } from './machine.js';
17
- import { runPasskeyBrowserFlow } from './passkey.js';
18
- import { cleanProfileName, ProfileStore } from './storage.js';
19
- import { serveMcp } from './mcp.js';
20
-
21
- export const API_VERSION = 1;
22
-
23
- export const COMMANDS = Object.freeze({
24
- account: Object.freeze(['create', 'login', 'me']),
25
- vault: Object.freeze(['create', 'unlock', 'lock']),
26
- chat: Object.freeze(['list', 'read', 'send']),
27
- money: Object.freeze(['balance', 'send', 'request', 'pay']),
28
- mcp: Object.freeze(['serve']),
29
- transactions: Object.freeze(['list']),
30
- listen: Object.freeze(['start']),
31
- });
32
-
33
- export class ClientNotReadyError extends Error {
34
- constructor(feature) {
35
- super(`${feature} is not available`);
36
- this.name = 'ClientNotReadyError';
37
- this.code = 'client_not_ready';
38
- }
39
- }
40
-
41
- function cleanUsername(value) {
42
- const username = normalizeUsername(String(value ?? '').trim().replace(/^@+/, ''));
43
- if (!isUsername(username)) {
44
- throw new Error('valid username required');
45
- }
46
- return username;
47
- }
48
-
49
- function cleanOptionalUsername(value) {
50
- const username = String(value ?? '').trim();
51
- return username ? cleanUsername(username) : '';
52
- }
53
-
54
- function randomSecret() {
55
- const bytes = new Uint8Array(32);
56
- globalThis.crypto.getRandomValues(bytes);
57
- return Buffer.from(bytes).toString('base64url');
58
- }
59
-
60
- export class VeylHeadlessClient {
61
- constructor(options = {}) {
62
- this.options = options;
63
- this.network = String(options.network || resolveNetwork(process.env)).toUpperCase();
64
- this.store = options.store || new ProfileStore({ dir: options.homeDir, profile: options.profile });
65
- this.profile = options.profileData || null;
66
- this.session = options.session || null;
67
- const cloud = options.cloudContext || createHeadlessCloud(options);
68
- this.auth = cloud.auth;
69
- this.cloud = cloud.cloud;
70
- this.functionBaseUrl = options.functionBaseUrl || cloud.functionBaseUrl;
71
- this.actions = createAccountSessionActions({
72
- cloud: () => this.cloud,
73
- getSession: () => this.ensureUnlocked(),
74
- });
75
-
76
- this.account = {
77
- create: (payload) => this.createAccount(payload),
78
- createPasskey: (payload) => this.createPasskeyAccount(payload),
79
- login: (payload) => this.loginAccount(payload),
80
- loginPasskey: (payload) => this.loginPasskeyAccount(payload),
81
- me: () => this.me(),
82
- };
83
- this.vault = {
84
- create: (payload) => this.createVault(payload),
85
- unlock: (payload) => this.unlockVault(payload),
86
- lock: (payload) => this.lockVault(payload),
87
- };
88
- this.chat = {
89
- list: (payload) => this.listChats(payload),
90
- read: (peer, payload) => this.readChat(peer, payload),
91
- send: (peer, message, payload) => this.sendChat(peer, message, payload),
92
- };
93
- this.money = {
94
- balance: (payload) => this.balance(payload),
95
- send: (peer, sats, payload) => this.sendMoney(peer, sats, payload),
96
- request: (peer, sats, payload) => this.requestMoney(peer, sats, payload),
97
- pay: (requestId, payload) => this.payRequest(requestId, payload),
98
- transactions: (payload) => this.transactions(payload),
99
- };
100
- this.mcp = {
101
- serve: (payload) => serveMcp(this, payload),
102
- };
103
- this.listen = (payload) => this.listenEvents(payload);
104
- }
105
-
106
- async callAccountEndpoint(path, payload, options = {}) {
107
- const headers = { 'Content-Type': 'application/json' };
108
- if (options.auth === true) {
109
- const token = await this.auth?.currentUser?.getIdToken?.();
110
- if (!token) {
111
- throw new Error('signed-in account required');
112
- }
113
- headers.Authorization = `Bearer ${token}`;
114
- }
115
- const response = await fetch(`${this.functionBaseUrl}${path}`, {
116
- method: 'POST',
117
- headers,
118
- body: JSON.stringify(payload || {}),
119
- });
120
- const data = await response.json().catch(() => null);
121
- if (!response.ok || data?.ok === false) {
122
- const error = new Error(data?.message || `headless api failed (${response.status || 0})`);
123
- error.code = data?.code || '';
124
- error.status = response.status || 0;
125
- throw error;
126
- }
127
- return data;
128
- }
129
-
130
- accountSummary(profile = this.profile) {
131
- if (!profile) {
132
- return null;
133
- }
134
- return {
135
- uid: profile.uid,
136
- username: profile.username,
137
- profile: profile.profile,
138
- credentialId: profile.credentialId,
139
- network: profile.network || this.network,
140
- hasVault: profile.hasVault === true,
141
- hasVaultSecret: !!profile.vaultSecret,
142
- walletPK: profile.walletPK || null,
143
- chatPK: profile.chatPK || null,
144
- auth: profile.auth || 'machine',
145
- signedIn: this.auth?.currentUser?.uid === profile.uid,
146
- unlocked: !!this.session,
147
- };
148
- }
149
-
150
- async saveProfile(profile) {
151
- this.profile = await this.store.save(profile);
152
- return this.profile;
153
- }
154
-
155
- async loadProfile(profileName = '') {
156
- if (this.profile && (!profileName || cleanProfileName(profileName) === this.profile.profile)) {
157
- return this.profile;
158
- }
159
- this.profile = await this.store.require(profileName);
160
- this.network = String(this.profile.network || this.network).toUpperCase();
161
- return this.profile;
162
- }
163
-
164
- async createAccount(options = {}) {
165
- const username = cleanUsername(options.username);
166
- const network = String(options.network || this.network).toUpperCase();
167
- const machine = options.credential || createMachineCredential();
168
- const start = await this.callAccountEndpoint('/account/create/start', {
169
- username,
170
- publicKeyPem: machine.publicKeyPem,
171
- });
172
- const finish = await this.callAccountEndpoint('/account/create/finish', {
173
- challengeId: start.challengeId,
174
- signature: signMachineChallenge(machine.privateKeyPem, start.challenge),
175
- });
176
- await signInHeadless(this.auth, finish.token);
177
- const profile = await this.saveProfile({
178
- profile: username,
179
- username,
180
- uid: finish.uid,
181
- network,
182
- credentialId: finish.credentialId || machine.credentialId,
183
- publicKeyPem: machine.publicKeyPem,
184
- privateKeyPem: options.saveCredential === false ? null : machine.privateKeyPem,
185
- auth: 'machine',
186
- createdAt: Date.now(),
187
- hasVault: false,
188
- });
189
- return {
190
- ...this.accountSummary(profile),
191
- publicKeyPem: machine.publicKeyPem,
192
- privateKeyPem: machine.privateKeyPem,
193
- };
194
- }
195
-
196
- async loginAccount(options = {}) {
197
- const username = cleanOptionalUsername(options.username);
198
- const profile = await this.loadProfile(username);
199
- const credentialId = options.credentialId || profile.credentialId;
200
- const privateKeyPem = options.privateKeyPem || profile.privateKeyPem;
201
- if (!credentialId || !privateKeyPem) {
202
- throw new Error('machine credential required');
203
- }
204
- const start = await this.callAccountEndpoint('/account/login/start', { credentialId });
205
- const finish = await this.callAccountEndpoint('/account/login/finish', {
206
- challengeId: start.challengeId,
207
- signature: signMachineChallenge(privateKeyPem, start.challenge),
208
- });
209
- await signInHeadless(this.auth, finish.token);
210
- const next = await this.saveProfile({
211
- ...profile,
212
- uid: finish.uid || profile.uid,
213
- username: finish.username || profile.username,
214
- credentialId,
215
- lastLoginAt: Date.now(),
216
- });
217
- return this.accountSummary(next);
218
- }
219
-
220
- async addMachineCredential(machine) {
221
- const start = await this.callAccountEndpoint('/account/credential/add/start', {
222
- publicKeyPem: machine.publicKeyPem,
223
- }, { auth: true });
224
- const finish = await this.callAccountEndpoint('/account/credential/add/finish', {
225
- challengeId: start.challengeId,
226
- signature: signMachineChallenge(machine.privateKeyPem, start.challenge),
227
- }, { auth: true });
228
- return finish;
229
- }
230
-
231
- async readUsername(uid, fallback = '') {
232
- const peer = await this.cloud.peer.read(uid).catch(() => null);
233
- return peer?.username || fallback || '';
234
- }
235
-
236
- async createPasskeyAccount(options = {}) {
237
- const username = cleanUsername(options.username);
238
- const network = String(options.network || this.network).toUpperCase();
239
- const flow = await runPasskeyBrowserFlow({
240
- mode: 'create',
241
- label: options.label || username,
242
- webUrl: options.webUrl,
243
- timeoutMs: options.timeoutMs,
244
- onUrl: options.onPasskeyUrl,
245
- });
246
- const user = await signInHeadless(this.auth, flow.token);
247
- await this.cloud.user.username.get(username);
248
-
249
- const machine = options.credential || createMachineCredential();
250
- const credential = machine ? await this.addMachineCredential(machine) : null;
251
- const profile = await this.saveProfile({
252
- profile: username,
253
- username,
254
- uid: user.uid,
255
- network,
256
- credentialId: credential?.credentialId || machine?.credentialId || null,
257
- publicKeyPem: machine?.publicKeyPem || null,
258
- privateKeyPem: options.saveCredential === false ? null : machine?.privateKeyPem || null,
259
- auth: 'passkey',
260
- createdAt: Date.now(),
261
- hasVault: false,
262
- });
263
- return {
264
- ...this.accountSummary(profile),
265
- passkey: true,
266
- publicKeyPem: machine?.publicKeyPem || null,
267
- privateKeyPem: machine?.privateKeyPem || null,
268
- };
269
- }
270
-
271
- async loginPasskeyAccount(options = {}) {
272
- const requested = cleanOptionalUsername(options.username);
273
- const stored = requested ? await this.store.load(requested) : await this.store.load();
274
- const flow = await runPasskeyBrowserFlow({
275
- mode: 'login',
276
- uid: options.uid || stored?.uid,
277
- webUrl: options.webUrl,
278
- timeoutMs: options.timeoutMs,
279
- onUrl: options.onPasskeyUrl,
280
- });
281
- const user = await signInHeadless(this.auth, flow.token);
282
- const username = await this.readUsername(user.uid, stored?.username || requested);
283
- const hasSavedCredential = !!(stored?.credentialId && stored?.privateKeyPem);
284
- const machine = hasSavedCredential ? null : options.credential || createMachineCredential();
285
- const credential = machine ? await this.addMachineCredential(machine) : null;
286
- const profile = await this.saveProfile({
287
- ...(stored || {}),
288
- profile: username || stored?.profile || user.uid,
289
- username: username || stored?.username || null,
290
- uid: user.uid,
291
- network: String(stored?.network || options.network || this.network).toUpperCase(),
292
- credentialId: credential?.credentialId || stored?.credentialId || machine?.credentialId || null,
293
- publicKeyPem: machine?.publicKeyPem || stored?.publicKeyPem || null,
294
- privateKeyPem: options.saveCredential === false ? stored?.privateKeyPem || null : machine?.privateKeyPem || stored?.privateKeyPem || null,
295
- auth: 'passkey',
296
- lastLoginAt: Date.now(),
297
- });
298
- return {
299
- ...this.accountSummary(profile),
300
- passkey: true,
301
- publicKeyPem: machine?.publicKeyPem || null,
302
- privateKeyPem: machine?.privateKeyPem || null,
303
- };
304
- }
305
-
306
- async me() {
307
- const profile = await this.store.load();
308
- if (!profile) {
309
- return null;
310
- }
311
- this.profile = profile;
312
- return this.accountSummary(profile);
313
- }
314
-
315
- async ensureAuth() {
316
- const profile = await this.loadProfile();
317
- if (this.auth?.currentUser?.uid === profile.uid) {
318
- return profile;
319
- }
320
- if (profile.auth === 'passkey' && (!profile.credentialId || !profile.privateKeyPem)) {
321
- await this.loginPasskeyAccount({ username: profile.profile, saveCredential: false });
322
- return this.profile;
323
- }
324
- await this.loginAccount({ username: profile.profile });
325
- return this.profile;
326
- }
327
-
328
- async bootSession(masterSeed, registry, profile) {
329
- const opened = await openRegistryAccountSession(masterSeed, registry, {
330
- SparkWallet,
331
- network: profile.network || this.network,
332
- });
333
- this.session = {
334
- ...opened,
335
- uid: profile.uid,
336
- username: profile.username,
337
- profile: profile.profile,
338
- network: profile.network || this.network,
339
- account: this.accountSummary(profile),
340
- };
341
- return this.session;
342
- }
343
-
344
- async createVault(options = {}) {
345
- const profile = await this.ensureAuth();
346
- if (options.overwrite !== true && await this.cloud.user.vault.exists(profile.uid)) {
347
- throw new Error('vault already exists');
348
- }
349
-
350
- const vaultSecret = String(options.secret || process.env.VEYL_VAULT_SECRET || randomSecret()).trim();
351
- if (!vaultSecret) {
352
- throw new Error('vault secret required');
353
- }
354
-
355
- const masterSeed = generateSeed();
356
- try {
357
- const registrySource = createSecretRegistry();
358
- const encrypted = await encryptSeedWithPassword(masterSeed, vaultSecret, {
359
- deriveKey: deriveVaultKey,
360
- registry: registrySource,
361
- });
362
- const session = await this.bootSession(masterSeed, encrypted.registry, profile);
363
- const vault = packSeedData(encrypted);
364
- await this.cloud.user.vault.write(profile.uid, vault);
365
- await this.cloud.user.profile.walletpk.write(session.walletPK, { network: profile.network || this.network });
366
- await this.cloud.user.profile.chatpk.write(session.chatPK);
367
- await this.cloud.user.community.accept(profile.uid).catch(() => {});
368
- await this.cloud.user.active.write(profile.uid, true).catch(() => {});
369
- const next = await this.saveProfile({
370
- ...profile,
371
- hasVault: true,
372
- vaultSecret: options.saveSecret === false ? null : vaultSecret,
373
- walletPK: session.walletPK,
374
- chatPK: session.chatPK,
375
- vaultCreatedAt: Date.now(),
376
- });
377
- this.session.account = this.accountSummary(next);
378
- return {
379
- ...this.accountSummary(next),
380
- vaultSecret,
381
- };
382
- } finally {
383
- masterSeed.fill(0);
384
- }
385
- }
386
-
387
- async unlockVault(options = {}) {
388
- const profile = await this.ensureAuth();
389
- if (this.session) {
390
- return this.accountSummary(profile);
391
- }
392
- const vaultSecret = String(options.secret || process.env.VEYL_VAULT_SECRET || profile.vaultSecret || '').trim();
393
- if (!vaultSecret) {
394
- throw new Error('vault secret required');
395
- }
396
- const vault = await this.cloud.user.vault.read(profile.uid);
397
- if (!vault) {
398
- throw new Error('vault not found');
399
- }
400
- const packed = unpackSeedData(vault);
401
- const masterSeed = await decryptSeed(packed.ct, packed.salt, packed.iv, vaultSecret, packed.kdf, {
402
- deriveKey: deriveVaultKey,
403
- });
404
- try {
405
- const session = await this.bootSession(masterSeed, packed.registry, profile);
406
- await this.cloud.user.active.write(profile.uid, true).catch(() => {});
407
- const next = await this.saveProfile({
408
- ...profile,
409
- hasVault: true,
410
- vaultSecret: options.saveSecret === false ? profile.vaultSecret || null : profile.vaultSecret || vaultSecret,
411
- walletPK: session.walletPK,
412
- chatPK: session.chatPK,
413
- lastUnlockedAt: Date.now(),
414
- });
415
- this.session.account = this.accountSummary(next);
416
- return this.accountSummary(next);
417
- } finally {
418
- masterSeed.fill(0);
419
- }
420
- }
421
-
422
- async lockVault() {
423
- const session = this.session;
424
- this.session = null;
425
- closeAccountSession(session);
426
- if (session?.uid) {
427
- await this.cloud.user.active.write(session.uid, false).catch(() => {});
428
- }
429
- return true;
430
- }
431
-
432
- async ensureUnlocked() {
433
- if (!this.session) {
434
- await this.unlockVault();
435
- }
436
- return this.session;
437
- }
438
-
439
- async resolvePeer(peer) {
440
- return this.actions.peer.resolve(peer);
441
- }
442
-
443
- async listChats(options = {}) {
444
- return this.actions.chat.list(options);
445
- }
446
-
447
- async chatForPeer(peer, options = {}) {
448
- return this.actions.chat.chatForPeer(peer, options);
449
- }
450
-
451
- async readChat(peer, options = {}) {
452
- return this.actions.chat.read(peer, options);
453
- }
454
-
455
- async sendPayload(peer, payload, options = {}) {
456
- return this.actions.chat.sendPayload(peer, payload, options);
457
- }
458
-
459
- async sendChat(peer, message, options = {}) {
460
- return this.actions.chat.send(peer, message, options);
461
- }
462
-
463
- async balance(options = {}) {
464
- return this.actions.money.balance(options);
465
- }
466
-
467
- async sendMoney(peer, sats, options = {}) {
468
- return this.actions.money.send(peer, sats, options);
469
- }
470
-
471
- async requestMoney(peer, sats, options = {}) {
472
- return this.actions.money.request(peer, sats, options);
473
- }
474
-
475
- async payRequest(requestId, options = {}) {
476
- return this.actions.money.pay(requestId, options);
477
- }
478
-
479
- async transactions(options = {}) {
480
- return this.actions.money.transactions(options);
481
- }
482
-
483
- async listenEvents(options = {}) {
484
- return listenAccount(this, options);
485
- }
486
-
487
- async close() {
488
- await this.lockVault();
489
- await signOutHeadless(this.auth).catch(() => {});
490
- }
491
- }
492
-
493
- export function createClient(options = {}) {
494
- return new VeylHeadlessClient(options);
495
- }
496
-
497
- export async function open(options = {}) {
498
- return createClient(options);
499
- }
package/src/cloud.js DELETED
@@ -1,52 +0,0 @@
1
- import { getApp, getApps, initializeApp } from 'firebase/app';
2
- import { getAuth, signInWithCustomToken, signOut } from 'firebase/auth';
3
- import { getFirestore } from 'firebase/firestore';
4
- import { getFunctions } from 'firebase/functions';
5
- import { getStorage } from 'firebase/storage';
6
- import { createFirebaseCloud } from '@veyl/shared/cloud/firebase';
7
- import { firebaseConfig as defaultFirebaseConfig } from '@veyl/shared/firebaseconfig';
8
-
9
- export const DEFAULT_FUNCTION_REGION = 'us-central1';
10
-
11
- function firebaseApp(name, config) {
12
- const existing = getApps().find((app) => app.name === name);
13
- return existing || initializeApp(config, name);
14
- }
15
-
16
- export function headlessFunctionBaseUrl(config = defaultFirebaseConfig, region = DEFAULT_FUNCTION_REGION) {
17
- const projectId = config?.projectId;
18
- if (!projectId) {
19
- throw new Error('firebase project id required');
20
- }
21
- return `https://${region}-${projectId}.cloudfunctions.net/headless`;
22
- }
23
-
24
- export function createHeadlessCloud(options = {}) {
25
- const config = options.firebaseConfig || defaultFirebaseConfig;
26
- const region = options.region || DEFAULT_FUNCTION_REGION;
27
- const app = firebaseApp(options.appName || 'veyl-headless', config);
28
- const auth = getAuth(app);
29
- const db = getFirestore(app);
30
- const functions = getFunctions(app, region);
31
- const storage = getStorage(app);
32
- return {
33
- app,
34
- auth,
35
- cloud: createFirebaseCloud({ db, auth, functions, storage }),
36
- functionBaseUrl: options.functionBaseUrl || headlessFunctionBaseUrl(config, region),
37
- };
38
- }
39
-
40
- export async function signInHeadless(auth, token) {
41
- if (!token) {
42
- throw new Error('auth token required');
43
- }
44
- const result = await signInWithCustomToken(auth, token);
45
- return result?.user || auth.currentUser || null;
46
- }
47
-
48
- export async function signOutHeadless(auth) {
49
- if (auth) {
50
- await signOut(auth);
51
- }
52
- }
package/src/index.js DELETED
@@ -1,11 +0,0 @@
1
- export {
2
- closeAccountSession,
3
- openAccountSessionFromSecrets,
4
- openLegacyAccountSession,
5
- openRegistryAccountSession,
6
- } from '@veyl/shared/account/session';
7
- export * from './client.js';
8
- export * from './cloud.js';
9
- export * from './machine.js';
10
- export * from './passkey.js';
11
- export * from './storage.js';
package/src/kdf.js DELETED
@@ -1,12 +0,0 @@
1
- import { argon2idAsync } from '@noble/hashes/argon2.js';
2
-
3
- export async function deriveVaultKey(password, salt, params = {}) {
4
- return argon2idAsync(String(password ?? ''), new Uint8Array(salt), {
5
- t: params.t,
6
- m: params.m,
7
- p: params.p,
8
- dkLen: params.dkLen,
9
- version: params.version,
10
- asyncTick: 10,
11
- });
12
- }
package/src/listen.js DELETED
@@ -1,120 +0,0 @@
1
- import { sleep } from '@veyl/shared/utils/async';
2
-
3
- const DEFAULT_INTERVAL_MS = 3000;
4
- const DEFAULT_CHAT_COUNT = 20;
5
- const DEFAULT_MESSAGE_COUNT = 10;
6
- const DEFAULT_TX_COUNT = 50;
7
-
8
- function cleanCount(value, fallback) {
9
- const count = Math.floor(Number(value));
10
- return Number.isFinite(count) && count > 0 ? Math.min(count, 100) : fallback;
11
- }
12
-
13
- function cleanInterval(value) {
14
- const interval = Math.floor(Number(value));
15
- return Number.isFinite(interval) && interval >= 1000 ? interval : DEFAULT_INTERVAL_MS;
16
- }
17
-
18
- function messageKey(message) {
19
- return [
20
- message?.chatId || '',
21
- message?.id || '',
22
- message?.cid || '',
23
- message?.type || '',
24
- message?.txId || '',
25
- message?.ts || '',
26
- ].join(':');
27
- }
28
-
29
- function transferKey(tx) {
30
- return [
31
- tx?.id || '',
32
- tx?.status || '',
33
- tx?.direction || '',
34
- tx?.amountSats || '',
35
- tx?.updatedAt || '',
36
- ].join(':');
37
- }
38
-
39
- function event(type, data = {}) {
40
- return {
41
- type,
42
- ts: Date.now(),
43
- ...data,
44
- };
45
- }
46
-
47
- function stopped(signal) {
48
- return signal?.aborted === true;
49
- }
50
-
51
- export async function listenAccount(client, options = {}) {
52
- const intervalMs = cleanInterval(options.intervalMs || options.interval);
53
- const chatCount = cleanCount(options.chatCount || options.chats, DEFAULT_CHAT_COUNT);
54
- const messageCount = cleanCount(options.messageCount || options.messages, DEFAULT_MESSAGE_COUNT);
55
- const txCount = cleanCount(options.txCount || options.transactions, DEFAULT_TX_COUNT);
56
- const includeChats = options.chats !== false;
57
- const includeTransactions = options.transactions !== false;
58
- const replay = options.replay === true;
59
- const emit = typeof options.onEvent === 'function' ? options.onEvent : () => {};
60
- const seenMessages = new Set();
61
- const seenTransfers = new Map();
62
- let initialized = false;
63
-
64
- await client.ensureUnlocked();
65
- emit(event('ready', { account: client.accountSummary() }));
66
-
67
- while (!stopped(options.signal)) {
68
- try {
69
- if (includeChats) {
70
- const chats = await client.chat.list({ count: chatCount });
71
- for (const chat of chats) {
72
- const peer = chat.peerChatPK || chat.username || chat.id;
73
- if (!peer) {
74
- continue;
75
- }
76
- const page = await client.chat.read(peer, {
77
- count: messageCount,
78
- markRead: false,
79
- });
80
- for (const message of page.messages || []) {
81
- const key = messageKey(message);
82
- if (!key || seenMessages.has(key)) {
83
- continue;
84
- }
85
- seenMessages.add(key);
86
- if (initialized || replay) {
87
- emit(event('message', {
88
- chat: page.chat || chat,
89
- message,
90
- }));
91
- }
92
- }
93
- }
94
- }
95
-
96
- if (includeTransactions) {
97
- const page = await client.money.transactions({ count: txCount });
98
- for (const tx of page.transfers || []) {
99
- const key = tx?.id || '';
100
- const value = transferKey(tx);
101
- if (!key || seenTransfers.get(key) === value) {
102
- continue;
103
- }
104
- seenTransfers.set(key, value);
105
- if (initialized || replay) {
106
- emit(event('transaction', { transaction: tx }));
107
- }
108
- }
109
- }
110
-
111
- initialized = true;
112
- } catch (error) {
113
- emit(event('error', {
114
- message: error?.message || String(error),
115
- }));
116
- }
117
-
118
- await sleep(intervalMs);
119
- }
120
- }