diff --git a/.gitignore b/.gitignore
index bf565453a5..929bda7250 100644
--- a/.gitignore
+++ b/.gitignore
@@ -81,3 +81,4 @@ website/.cache
website/test/stubs-layout-cache/_includes/*.js
apps/android/app/release
apps/multiplatform/.kotlin/sessions
+
diff --git a/bots/api/EVENTS.md b/bots/api/EVENTS.md
index 84f59e7405..d7405ef846 100644
--- a/bots/api/EVENTS.md
+++ b/bots/api/EVENTS.md
@@ -62,6 +62,11 @@ This file is generated automatically.
- [SentGroupInvitation](#sentgroupinvitation)
- [GroupLinkConnecting](#grouplinkconnecting)
+[Network connection events](#network-connection-events)
+- [HostConnected](#hostconnected)
+- [HostDisconnected](#hostdisconnected)
+- [SubscriptionStatus](#subscriptionstatus)
+
[Error events](#error-events)
- [MessageError](#messageerror)
- [ChatError](#chaterror)
@@ -685,6 +690,48 @@ Sent when bot joins group via another user link.
---
+## Network connection events
+
+
+
+
+### HostConnected
+
+Messaging or file server connected
+
+**Record type**:
+- type: "hostConnected"
+- protocol: string
+- transportHost: string
+
+---
+
+
+### HostDisconnected
+
+Messaging or file server disconnected
+
+**Record type**:
+- type: "hostDisconnected"
+- protocol: string
+- transportHost: string
+
+---
+
+
+### SubscriptionStatus
+
+Messaging subscription status changed
+
+**Record type**:
+- type: "subscriptionStatus"
+- server: string
+- subscriptionStatus: [SubscriptionStatus](./TYPES.md#subscriptionstatus)
+- connections: [string]
+
+---
+
+
## Error events
Bots may log these events for debugging. There will be many error events - this does NOT indicate a malfunction - e.g., they may happen because of bad network connectivity, or because messages may be delivered to deleted chats for a short period of time (they will be ignored).
diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md
index 1aaa291204..cded13f3a2 100644
--- a/bots/api/TYPES.md
+++ b/bots/api/TYPES.md
@@ -153,6 +153,7 @@ This file is generated automatically.
- [SndGroupEvent](#sndgroupevent)
- [SrvError](#srverror)
- [StoreError](#storeerror)
+- [SubscriptionStatus](#subscriptionstatus)
- [SwitchPhase](#switchphase)
- [TimedMessagesGroupPreference](#timedmessagesgrouppreference)
- [TimedMessagesPreference](#timedmessagespreference)
@@ -3593,6 +3594,26 @@ WorkItemError:
- errContext: string
+---
+
+## SubscriptionStatus
+
+**Discriminated union type**:
+
+Active:
+- type: "active"
+
+Pending:
+- type: "pending"
+
+Removed:
+- type: "removed"
+- subError: string
+
+NoSub:
+- type: "noSub"
+
+
---
## SwitchPhase
diff --git a/bots/src/API/Docs/Events.hs b/bots/src/API/Docs/Events.hs
index ecb8a5ba04..130ea89846 100644
--- a/bots/src/API/Docs/Events.hs
+++ b/bots/src/API/Docs/Events.hs
@@ -136,6 +136,14 @@ chatEventsDocsData =
],
[]
),
+ ( "Network connection events",
+ "",
+ [ ("CEvtHostConnected", "Messaging or file server connected"),
+ ("CEvtHostDisconnected", "Messaging or file server disconnected"),
+ ("CEvtSubscriptionStatus", "Messaging subscription status changed")
+ ],
+ []
+ ),
( "Error events",
"Bots may log these events for debugging. \
\There will be many error events - this does NOT indicate a malfunction - \
@@ -178,9 +186,6 @@ undocumentedEvents =
"CEvtCustomChatEvent",
"CEvtGroupMemberRatchetSync",
"CEvtGroupMemberSwitch",
- "CEvtHostConnected",
- "CEvtHostDisconnected",
- "CEvtSubscriptionStatus",
"CEvtNewRemoteHost",
"CEvtNoMemberContactCreating",
"CEvtNtfMessage",
diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs
index 73e427fa03..73ad90e91b 100644
--- a/bots/src/API/Docs/Types.hs
+++ b/bots/src/API/Docs/Types.hs
@@ -176,6 +176,7 @@ ciQuoteType =
updateRecord (RecordTypeInfo name fields) = RecordTypeInfo name $ map optChatDir fields
in st {recordTypes = map updateRecord records} -- need to map even though there is one constructor in this type
+-- type info, JSON encoding, constructor prefix, removed constructors, string encoding for commands, description
chatTypesDocsData :: [(SumTypeInfo, SumTypeJsonEncoding, String, [ConsName], Expr, Text)]
chatTypesDocsData =
[ ((sti @(Chat 'CTDirect)) {typeName = "AChat"}, STRecord, "", [], "", ""),
@@ -332,6 +333,7 @@ chatTypesDocsData =
(sti @SndGroupEvent, STUnion, "SGE", [], "", ""),
(sti @SrvError, STUnion, "SrvErr", [], "", ""),
(sti @StoreError, STUnion, "SE", [], "", ""),
+ (sti @SubscriptionStatus, STUnion, "SS", [], "", ""),
(sti @SwitchPhase, STEnum, "SP", [], "", ""),
(sti @TimedMessagesGroupPreference, STRecord, "", [], "", ""),
(sti @TimedMessagesPreference, STRecord, "", [], "", ""),
@@ -522,6 +524,7 @@ deriving instance Generic SndFileTransfer
deriving instance Generic SndGroupEvent
deriving instance Generic SrvError
deriving instance Generic StoreError
+deriving instance Generic SubscriptionStatus
deriving instance Generic SwitchPhase
deriving instance Generic TimedMessagesGroupPreference
deriving instance Generic TimedMessagesPreference
diff --git a/bots/src/API/TypeInfo.hs b/bots/src/API/TypeInfo.hs
index df43374ffa..a70de72d01 100644
--- a/bots/src/API/TypeInfo.hs
+++ b/bots/src/API/TypeInfo.hs
@@ -194,6 +194,7 @@ toTypeInfo tr =
primitiveToLower st@(ST t ps) = let t' = fstToLower t in if t' `elem` primitiveTypes then ST t' ps else st
stringTypes =
[ "AConnectionLink",
+ "AProtocolType",
"AgentConnId",
"AgentInvId",
"AgentRcvFileId",
@@ -212,6 +213,7 @@ toTypeInfo tr =
"ProtocolServer",
"SbKey",
"SharedMsgId",
+ "TransportHost",
"UIColor",
"UserPwd",
"XContactId"
diff --git a/packages/simplex-chat-client/types/typescript/README.md b/packages/simplex-chat-client/types/typescript/README.md
index ad13a5d76e..e30cf0d0c3 100644
--- a/packages/simplex-chat-client/types/typescript/README.md
+++ b/packages/simplex-chat-client/types/typescript/README.md
@@ -2,7 +2,7 @@
This TypeScript library provides auto-generated types for bots API: commands and responses, events and all types they use.
-It is used in [simplex-chat](https://www.npmjs.com/package/simplex-chat) library that uses WebSockets interface of SimpleX Chat CLI.
+It is used in [simplex-chat](https://www.npmjs.com/package/simplex-chat) Node.js library.
[API reference](https://github.com/simplex-chat/simplex-chat/tree/stable/bots).
diff --git a/packages/simplex-chat-client/types/typescript/package.json b/packages/simplex-chat-client/types/typescript/package.json
index c24b93fbed..a135b286c2 100644
--- a/packages/simplex-chat-client/types/typescript/package.json
+++ b/packages/simplex-chat-client/types/typescript/package.json
@@ -1,6 +1,6 @@
{
"name": "@simplex-chat/types",
- "version": "0.2.0",
+ "version": "0.3.0",
"description": "TypeScript types for SimpleX Chat bot libraries",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -35,7 +35,7 @@
"bugs": {
"url": "https://github.com/simplex-chat/simplex-chat/issues"
},
- "homepage": "https://github.com/simplex-chat/simplex-chat#readme",
+ "homepage": "https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/types/typescript#readme",
"dependencies": {
"typescript": "^5.9.2"
}
diff --git a/packages/simplex-chat-client/types/typescript/src/events.ts b/packages/simplex-chat-client/types/typescript/src/events.ts
index d7a0419bbe..cb6ba85c8b 100644
--- a/packages/simplex-chat-client/types/typescript/src/events.ts
+++ b/packages/simplex-chat-client/types/typescript/src/events.ts
@@ -46,6 +46,9 @@ export type ChatEvent =
| CEvt.JoinedGroupMemberConnecting
| CEvt.SentGroupInvitation
| CEvt.GroupLinkConnecting
+ | CEvt.HostConnected
+ | CEvt.HostDisconnected
+ | CEvt.SubscriptionStatus
| CEvt.MessageError
| CEvt.ChatError
| CEvt.ChatErrors
@@ -94,6 +97,9 @@ export namespace CEvt {
| "joinedGroupMemberConnecting"
| "sentGroupInvitation"
| "groupLinkConnecting"
+ | "hostConnected"
+ | "hostDisconnected"
+ | "subscriptionStatus"
| "messageError"
| "chatError"
| "chatErrors"
@@ -411,6 +417,25 @@ export namespace CEvt {
hostMember: T.GroupMember
}
+ export interface HostConnected extends Interface {
+ type: "hostConnected"
+ protocol: string
+ transportHost: string
+ }
+
+ export interface HostDisconnected extends Interface {
+ type: "hostDisconnected"
+ protocol: string
+ transportHost: string
+ }
+
+ export interface SubscriptionStatus extends Interface {
+ type: "subscriptionStatus"
+ server: string
+ subscriptionStatus: T.SubscriptionStatus
+ connections: string[]
+ }
+
export interface MessageError extends Interface {
type: "messageError"
user: T.User
diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts
index bca0179a91..c4371082cc 100644
--- a/packages/simplex-chat-client/types/typescript/src/types.ts
+++ b/packages/simplex-chat-client/types/typescript/src/types.ts
@@ -4299,6 +4299,37 @@ export namespace StoreError {
}
}
+export type SubscriptionStatus =
+ | SubscriptionStatus.Active
+ | SubscriptionStatus.Pending
+ | SubscriptionStatus.Removed
+ | SubscriptionStatus.NoSub
+
+export namespace SubscriptionStatus {
+ export type Tag = "active" | "pending" | "removed" | "noSub"
+
+ interface Interface {
+ type: Tag
+ }
+
+ export interface Active extends Interface {
+ type: "active"
+ }
+
+ export interface Pending extends Interface {
+ type: "pending"
+ }
+
+ export interface Removed extends Interface {
+ type: "removed"
+ subError: string
+ }
+
+ export interface NoSub extends Interface {
+ type: "noSub"
+ }
+}
+
export enum SwitchPhase {
Started = "started",
Confirmed = "confirmed",
diff --git a/packages/simplex-chat-client/typescript/README.md b/packages/simplex-chat-client/typescript/README.md
index c1756dc82c..a67e822de2 100644
--- a/packages/simplex-chat-client/typescript/README.md
+++ b/packages/simplex-chat-client/typescript/README.md
@@ -1,4 +1,8 @@
-# SimpleX Chat JavaScript client
+# SimpleX Chat JavaScript WebRTC client
+
+**THIS PACKAGE IS DEPRECATED**
+
+Use [SimpleX Chat Node.js library](https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-nodejs#readme) instead of this package.
This is a TypeScript library that defines WebSocket API client for [SimpleX Chat terminal CLI](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/CLI.md) that should be run as a WebSockets server on any port:
@@ -24,7 +28,7 @@ Please share your use cases and implementations.
## Quick start
```
-npm i simplex-chat
+npm i @simplex-chat/webrtc-client@6.5.0-beta.3
npm run build
```
diff --git a/packages/simplex-chat-client/typescript/package.json b/packages/simplex-chat-client/typescript/package.json
index b721dbcce2..c9d6165336 100644
--- a/packages/simplex-chat-client/typescript/package.json
+++ b/packages/simplex-chat-client/typescript/package.json
@@ -1,6 +1,6 @@
{
- "name": "simplex-chat",
- "version": "0.3.0",
+ "name": "@simplex-chat/webrtc-client",
+ "version": "6.5.0-beta.3",
"description": "SimpleX Chat client",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -38,12 +38,12 @@
"bugs": {
"url": "https://github.com/simplex-chat/simplex-chat/issues"
},
- "homepage": "https://github.com/simplex-chat/simplex-chat/packages/simplex-chat-client/typescript#readme",
+ "homepage": "https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript#readme",
"dependencies": {
+ "@simplex-chat/types": "^0.3.0",
"isomorphic-ws": "^4.0.1"
},
"devDependencies": {
- "@simplex-chat/types": "^0.1.0",
"@types/jest": "^27.5.1",
"@types/node": "^18.11.18",
"@typescript-eslint/eslint-plugin": "^5.23.0",
diff --git a/packages/simplex-chat-client/typescript/tests/client.test.ts b/packages/simplex-chat-client/typescript/tests/client.test.ts
index 9cd0a365f1..1c5c4d4baa 100644
--- a/packages/simplex-chat-client/typescript/tests/client.test.ts
+++ b/packages/simplex-chat-client/typescript/tests/client.test.ts
@@ -24,7 +24,7 @@ describe.skip("ChatClient (expects SimpleX Chat server with a user, without cont
const r2 = await c.msgQ.dequeue()
assert.strictEqual(r1.type, "contactConnecting")
assert.strictEqual(r2.type, "contactConnected")
- const contact1 = (r1 as CEvt.ContactConnected).contact
+ const contact1 = (r1 as CEvt.ContactConnecting).contact
// const contact2 = (r2 as C.CRContactConnected).contact
const r3 = await c.apiSendTextMessage(T.ChatType.Direct, contact1.contactId, "hello")
assert(r3[0].chatItem.content.type === "sndMsgContent" && r3[0].chatItem.content.msgContent.text === "hello")
diff --git a/packages/simplex-chat-nodejs/.gitignore b/packages/simplex-chat-nodejs/.gitignore
new file mode 100644
index 0000000000..322e38bfda
--- /dev/null
+++ b/packages/simplex-chat-nodejs/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+package-lock.json
+.vscode
+build/
+libs/
+dist/
+coverage/
+tmp/
diff --git a/packages/simplex-chat-nodejs/.npmignore b/packages/simplex-chat-nodejs/.npmignore
new file mode 100644
index 0000000000..26fdd85dff
--- /dev/null
+++ b/packages/simplex-chat-nodejs/.npmignore
@@ -0,0 +1,3 @@
+libs/
+build/
+node_modules/
diff --git a/packages/simplex-chat-nodejs/LICENSE b/packages/simplex-chat-nodejs/LICENSE
new file mode 100644
index 0000000000..0ad25db4bd
--- /dev/null
+++ b/packages/simplex-chat-nodejs/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/packages/simplex-chat-nodejs/README.md b/packages/simplex-chat-nodejs/README.md
new file mode 100644
index 0000000000..982b8d0553
--- /dev/null
+++ b/packages/simplex-chat-nodejs/README.md
@@ -0,0 +1,90 @@
+# SimpleX Chat Node.js library
+
+This library replaced now deprecated [SimpleX Chat WebRTC TypeScript client](https://www.npmjs.com/package/@simplex-chat/webrtc-client).
+
+## Use cases
+
+- chat bots: you can implement any logic of connecting with and communicating with SimpleX Chat users. Using chat groups a chat bot can connect SimpleX Chat users with each other.
+- control of the equipment: e.g. servers or home automation. SimpleX Chat provides secure and authorised connections, so this is more secure than using rest APIs.
+- any scenarios of scripted message sending.
+- chat and chat-based interfaces.
+
+Please share your use cases and implementations.
+
+## Quick start: a simple bot
+
+```
+npm i simplex-chat@6.5.0-beta.4.2
+```
+
+Simple bot that replies with squares of numbers you send to it:
+
+```javascript
+(async () => {
+ const {bot} = await import("simplex-chat")
+ // if you are running from this GitHub repo:
+ // const {bot} = await import("../dist/index.js")
+ const [chat, _user, _address] = await bot.run({
+ profile: {displayName: "Squaring bot example", fullName: ""},
+ dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""},
+ options: {
+ addressSettings: {welcomeMessage: "If you send me a number, I will calculate its square."},
+ },
+ onMessage: async (ci, content) => {
+ const n = +content.text
+ const reply = typeof n === "number" && !isNaN(n)
+ ? `${n} * ${n} = ${n * n}`
+ : `this is not a number`
+ await chat.apiSendTextReply(ci, reply)
+ }
+ })
+})()
+```
+
+If you installed this package as dependency, you can run this example with:
+
+```sh
+node ./node_modules/simplex-chat/examples/squaring-bot-readme.js`
+```
+
+If you cloned this repository, you can:
+
+```
+cd ./packages/simplex-chat-nodejs
+npm install
+npm run build
+node ./examples/squaring-bot-readme.js
+```
+
+There is an example with more options in [./examples/squaring-bot.ts](./examples/squaring-bot.ts).
+
+You can run it with: `npx ts-node ./examples/squaring-bot.ts`
+
+## Documentation
+
+The library docs are [here](./docs/README.md).
+
+Library provides these modules:
+- [bot](./docs/Namespace.bot.md): a simple declarative API to run a chat-bot with a single function call. It automates creating and updating of the bot profile, address and bot commands shown in the app UI.
+- [api](./docs/Namespace.api.md): an API to send chat commands and receive chat events to/from chat core. You need to use it in bot event handlers, and for any other use cases.
+- [core](./docs/Namespace.core.md): a low level API to the core library - the same that is used in desktop clients. You are unlikely to ever need to use this module directly.
+- [util](./docs/Namespace.util.md): useful functions for chat events and types.
+
+
+This library uses [@simplex-chat/types](https://www.npmjs.com/package/@simplex-chat/types) package with auto-generated [bot API types](../../bots/api/README.md).
+
+## Supported chat functions
+
+Library provides types and functions to:
+
+- create and change user profile (although, in most cases you can do it manually, via SimpleX Chat terminal app).
+- create and accept invitations or connect with the contacts.
+- create and manage long-term user address, accepting connection requests automatically.
+- send, receive, delete and update messages, and add message reactions.
+- create, join and manage group.
+- send and receive files.
+- etc.
+
+## License
+
+[AGPL v3](./LICENSE)
diff --git a/packages/simplex-chat-nodejs/binding.gyp b/packages/simplex-chat-nodejs/binding.gyp
new file mode 100644
index 0000000000..addb293f5b
--- /dev/null
+++ b/packages/simplex-chat-nodejs/binding.gyp
@@ -0,0 +1,35 @@
+{
+ "targets": [
+ {
+ "target_name": "simplex",
+ "sources": [ "cpp/simplex.cc" ],
+ "include_dirs": [
+ "
+#include
+#include
+#include
+#include
+#include "simplex.h"
+
+namespace simplex {
+
+using namespace Napi;
+
+void haskell_init() {
+ int argc = 6;
+ const char *argv[] = {
+ "simplex",
+ "+RTS", // requires `hs_init_with_rtsopts`
+ "-A64m", // chunk size for new allocations
+ "-H64m", // initial heap size
+ "-xn", // non-moving GC
+ "--install-signal-handlers=no",
+ nullptr};
+ char **pargv = const_cast(argv);
+ hs_init_with_rtsopts(&argc, &pargv);
+}
+
+class ResultAsyncWorker : public AsyncWorker {
+ public:
+ using ExecuteFn = std::function;
+ using ResultProcessor = std::function;
+
+ ResultAsyncWorker(Function& callback, ExecuteFn execute_fn, ResultProcessor result_processor = nullptr)
+ : AsyncWorker(callback), execute_fn_(std::move(execute_fn)), result_processor_(std::move(result_processor)) {}
+
+ void Execute() override {
+ execute_fn_(this);
+ }
+
+ void OnOK() override {
+ HandleScope scope(Env());
+ if (result_processor_) {
+ result_processor_(this, Env());
+ } else {
+ Callback().Call({Env().Null(), String::New(Env(), result_)});
+ }
+ }
+
+ void OnError(const Error& e) override {
+ HandleScope scope(Env());
+ Callback().Call({e.Value(), Env().Undefined()});
+ }
+
+ void SetResult(std::string result) {
+ result_ = std::move(result);
+ }
+
+ void SetWorkerError(const std::string& msg) {
+ SetError(msg);
+ }
+
+ const std::string& GetStringResult() const {
+ return result_;
+ }
+
+ void SetCtrl(uintptr_t ctrl) {
+ ctrl_ = ctrl;
+ }
+
+ uintptr_t GetCtrl() const {
+ return ctrl_;
+ }
+
+ protected:
+ std::string result_;
+ uintptr_t ctrl_ = 0;
+
+ private:
+ ExecuteFn execute_fn_;
+ ResultProcessor result_processor_;
+};
+
+class BinaryAsyncWorker : public AsyncWorker {
+ public:
+ using ExecuteFn = std::function;
+
+ BinaryAsyncWorker(Function& callback, ExecuteFn execute_fn)
+ : AsyncWorker(callback), execute_fn_(std::move(execute_fn)) {}
+
+ void Execute() override {
+ execute_fn_(this);
+ }
+
+ void OnOK() override {
+ HandleScope scope(Env());
+ if (original_buf == nullptr || binary_len == 0) {
+ Callback().Call({Env().Null(), Env().Undefined()});
+ return;
+ }
+ char* data_ptr = original_buf + 5;
+ auto finalizer = [](Napi::Env env, char* finalize_data, char* orig) {
+ free(orig);
+ };
+ Napi::Buffer buffer = Napi::Buffer::New(Env(), data_ptr, binary_len, finalizer, original_buf);
+ Callback().Call({Env().Null(), buffer});
+ }
+
+ void OnError(const Error& e) override {
+ HandleScope scope(Env());
+ Callback().Call({e.Value(), Env().Undefined()});
+ }
+
+ void SetWorkerError(const std::string& msg) {
+ SetError(msg);
+ }
+
+ char* original_buf = nullptr;
+ size_t binary_len = 0;
+
+ private:
+ ExecuteFn execute_fn_;
+};
+
+// Helper for converting chat_ctrl pointer to BigInt
+Napi::BigInt ToChatCtrlBigInt(Napi::Env env, uintptr_t ctrl) {
+ return Napi::BigInt::New(env, static_cast(ctrl));
+}
+
+// Helper for converting BigInt to chat_ctrl pointer
+chat_ctrl FromChatCtrlBigInt(const Napi::Value& value) {
+ Napi::Env env = value.Env();
+ if (!value.IsBigInt()) {
+ Napi::TypeError::New(env, "Expected BigInt for ctrl").ThrowAsJavaScriptException();
+ return nullptr;
+ }
+ Napi::BigInt big = value.As();
+ bool lossless;
+ uint64_t val = big.Uint64Value(&lossless);
+ if (!lossless) {
+ Napi::TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException();
+ return nullptr;
+ }
+ return reinterpret_cast(val);
+}
+
+// Helper for handling common C result patterns (no empty check)
+void HandleCResult(ResultAsyncWorker* worker, char* c_res, const std::string& func_name) {
+ if (c_res == nullptr) {
+ worker->SetWorkerError(func_name + " failed");
+ return;
+ }
+ std::string res = c_res;
+ free(c_res);
+ worker->SetResult(res);
+}
+
+Napi::Promise CreatePromiseAndCallback(Env env, Function& cb_out) {
+ Promise::Deferred deferred = Promise::Deferred::New(env);
+ cb_out = Function::New(env, [deferred](const CallbackInfo& args) {
+ if (!args[0].IsNull() && !args[0].IsUndefined()) {
+ deferred.Reject(args[0]);
+ } else {
+ deferred.Resolve(args[1]);
+ }
+ });
+ return deferred.Promise();
+}
+
+// Common result processors
+ResultAsyncWorker::ResultProcessor MigrateResultProcessor() {
+ return [](ResultAsyncWorker* worker, Napi::Env env) {
+ Napi::Array arr = Napi::Array::New(env, 2);
+ arr.Set(0u, ToChatCtrlBigInt(env, worker->GetCtrl()));
+ arr.Set(1u, Napi::String::New(env, worker->GetStringResult()));
+ worker->Callback().Call({env.Null(), arr});
+ };
+}
+
+// Refactored functions using common patterns
+
+Value ChatMigrateInit(const CallbackInfo& args) {
+ Env env = args.Env();
+ if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) {
+ TypeError::New(env, "Expected three string arguments").ThrowAsJavaScriptException();
+ return env.Undefined();
+ }
+
+ std::string path = args[0].As().Utf8Value();
+ std::string key = args[1].As().Utf8Value();
+ std::string confirm = args[2].As().Utf8Value();
+
+ Function cb;
+ Promise promise = CreatePromiseAndCallback(env, cb);
+
+ auto execute_fn = [path, key, confirm](ResultAsyncWorker* worker) {
+ chat_ctrl ctrl = nullptr;
+ char* c_res = chat_migrate_init(path.c_str(), key.c_str(), confirm.c_str(), &ctrl);
+ worker->SetCtrl(reinterpret_cast(ctrl));
+ HandleCResult(worker, c_res, "chat_migrate_init");
+ };
+
+ ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn), MigrateResultProcessor());
+ worker->Queue();
+
+ return promise;
+}
+
+Value ChatCloseStore(const CallbackInfo& args) {
+ Env env = args.Env();
+ if (args.Length() < 1 || !args[0].IsBigInt()) {
+ TypeError::New(env, "Expected bigint (ctrl)").ThrowAsJavaScriptException();
+ return env.Undefined();
+ }
+
+ chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
+
+ Function cb;
+ Promise promise = CreatePromiseAndCallback(env, cb);
+
+ auto execute_fn = [ctrl](ResultAsyncWorker* worker) {
+ char* c_res = chat_close_store(ctrl);
+ HandleCResult(worker, c_res, "chat_close_store");
+ };
+
+ ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
+ worker->Queue();
+
+ return promise;
+}
+
+Value ChatSendCmd(const CallbackInfo& args) {
+ Env env = args.Env();
+ if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsString()) {
+ TypeError::New(env, "Expected bigint (ctrl) and string (cmd)").ThrowAsJavaScriptException();
+ return env.Undefined();
+ }
+
+ chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
+ std::string cmd = args[1].As().Utf8Value();
+
+ Function cb;
+ Promise promise = CreatePromiseAndCallback(env, cb);
+
+ auto execute_fn = [ctrl, cmd](ResultAsyncWorker* worker) {
+ char* c_res = chat_send_cmd(ctrl, cmd.c_str());
+ HandleCResult(worker, c_res, "chat_send_cmd");
+ };
+
+ ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
+ worker->Queue();
+
+ return promise;
+}
+
+Value ChatRecvMsgWait(const CallbackInfo& args) {
+ Env env = args.Env();
+ if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsNumber()) {
+ TypeError::New(env, "Expected bigint (ctrl), number (wait)").ThrowAsJavaScriptException();
+ return env.Undefined();
+ }
+
+ chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
+ int wait = static_cast(args[1].As().Int32Value());
+
+ Function cb;
+ Promise promise = CreatePromiseAndCallback(env, cb);
+
+ auto execute_fn = [ctrl, wait](ResultAsyncWorker* worker) {
+ char* c_res = chat_recv_msg_wait(ctrl, wait);
+ HandleCResult(worker, c_res, "chat_recv_msg_wait");
+ };
+
+ ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
+ worker->Queue();
+
+ return promise;
+}
+
+Value ChatWriteFile(const CallbackInfo& args) {
+ Env env = args.Env();
+ if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !args[2].IsArrayBuffer()) {
+ TypeError::New(env, "Expected bigint (ctrl), string (path), ArrayBuffer").ThrowAsJavaScriptException();
+ return env.Undefined();
+ }
+
+ chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
+ std::string path = args[1].As().Utf8Value();
+ ArrayBuffer ab = args[2].As();
+ char* data = static_cast(ab.Data());
+ size_t len = ab.ByteLength();
+
+ Function cb;
+ Promise promise = CreatePromiseAndCallback(env, cb);
+
+ auto execute_fn = [ctrl, path, ab, data, len](ResultAsyncWorker* worker) {
+ (void)ab; // to keep ArrayBuffer alive
+ char* c_res = chat_write_file(ctrl, path.c_str(), data, static_cast(len));
+ HandleCResult(worker, c_res, "chat_write_file");
+ };
+
+ ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
+ worker->Queue();
+
+ return promise;
+}
+
+Value ChatReadFile(const CallbackInfo& args) {
+ Env env = args.Env();
+ if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) {
+ TypeError::New(env, "Expected three strings (path, key, nonce)").ThrowAsJavaScriptException();
+ return env.Undefined();
+ }
+
+ std::string path = args[0].As().Utf8Value();
+ std::string key = args[1].As().Utf8Value();
+ std::string nonce = args[2].As().Utf8Value();
+
+ Function cb;
+ Promise promise = CreatePromiseAndCallback(env, cb);
+
+ auto execute_fn = [path, key, nonce](BinaryAsyncWorker* worker) {
+ char* buf = chat_read_file(path.c_str(), key.c_str(), nonce.c_str());
+ if (buf == nullptr) {
+ worker->SetWorkerError("chat_read_file failed");
+ return;
+ }
+ char status = buf[0];
+ if (status == 1) {
+ std::string err = buf + 1;
+ free(buf);
+ worker->SetWorkerError(err);
+ return;
+ } else if (status == 0) {
+ uint32_t len = *(uint32_t*)(buf + 1);
+ worker->original_buf = buf;
+ worker->binary_len = len;
+ } else {
+ free(buf);
+ worker->SetWorkerError("Unexpected status from chat_read_file");
+ return;
+ }
+ };
+
+ BinaryAsyncWorker* worker = new BinaryAsyncWorker(cb, std::move(execute_fn));
+ worker->Queue();
+
+ return promise;
+}
+
+Value ChatEncryptFile(const CallbackInfo& args) {
+ Env env = args.Env();
+ if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !args[2].IsString()) {
+ TypeError::New(env, "Expected bigint (ctrl), two strings (fromPath, toPath)").ThrowAsJavaScriptException();
+ return env.Undefined();
+ }
+
+ chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
+ std::string fromPath = args[1].As().Utf8Value();
+ std::string toPath = args[2].As().Utf8Value();
+
+ Function cb;
+ Promise promise = CreatePromiseAndCallback(env, cb);
+
+ auto execute_fn = [ctrl, fromPath, toPath](ResultAsyncWorker* worker) {
+ char* c_res = chat_encrypt_file(ctrl, fromPath.c_str(), toPath.c_str());
+ HandleCResult(worker, c_res, "chat_encrypt_file");
+ };
+
+ ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
+ worker->Queue();
+
+ return promise;
+}
+
+Value ChatDecryptFile(const CallbackInfo& args) {
+ Env env = args.Env();
+ if (args.Length() < 4 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString() || !args[3].IsString()) {
+ TypeError::New(env, "Expected four strings (fromPath, key, nonce, toPath)").ThrowAsJavaScriptException();
+ return env.Undefined();
+ }
+
+ std::string fromPath = args[0].As().Utf8Value();
+ std::string key = args[1].As().Utf8Value();
+ std::string nonce = args[2].As().Utf8Value();
+ std::string toPath = args[3].As().Utf8Value();
+
+ Function cb;
+ Promise promise = CreatePromiseAndCallback(env, cb);
+
+ auto execute_fn = [fromPath, key, nonce, toPath](ResultAsyncWorker* worker) {
+ char* c_res = chat_decrypt_file(fromPath.c_str(), key.c_str(), nonce.c_str(), toPath.c_str());
+ HandleCResult(worker, c_res, "chat_decrypt_file");
+ };
+
+ ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
+ worker->Queue();
+
+ return promise;
+}
+
+Object Init(Env env, Object exports) {
+ haskell_init();
+ exports.Set("chat_migrate_init", Function::New(env, ChatMigrateInit));
+ exports.Set("chat_close_store", Function::New(env, ChatCloseStore));
+ exports.Set("chat_send_cmd", Function::New(env, ChatSendCmd));
+ exports.Set("chat_recv_msg_wait", Function::New(env, ChatRecvMsgWait));
+ exports.Set("chat_write_file", Function::New(env, ChatWriteFile));
+ exports.Set("chat_read_file", Function::New(env, ChatReadFile));
+ exports.Set("chat_encrypt_file", Function::New(env, ChatEncryptFile));
+ exports.Set("chat_decrypt_file", Function::New(env, ChatDecryptFile));
+ return exports;
+}
+
+NODE_API_MODULE(simplex, Init)
+
+}
diff --git a/packages/simplex-chat-nodejs/cpp/simplex.h b/packages/simplex-chat-nodejs/cpp/simplex.h
new file mode 100644
index 0000000000..8e579626ed
--- /dev/null
+++ b/packages/simplex-chat-nodejs/cpp/simplex.h
@@ -0,0 +1,46 @@
+//
+// simplex.h
+// SimpleX
+//
+// Created by Evgeny on 30/05/2022.
+// Copyright © 2022 SimpleX Chat. All rights reserved.
+//
+
+#ifndef SimpleX_h
+#define SimpleX_h
+
+extern "C" void hs_init(int argc, char **argv[]);
+extern "C" void hs_init_with_rtsopts(int * argc, char **argv[]);
+
+typedef long* chat_ctrl;
+
+// the last parameter is used to return the pointer to chat controller
+extern "C" char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl);
+extern "C" char *chat_close_store(chat_ctrl ctrl);
+extern "C" char *chat_reopen_store(chat_ctrl ctrl);
+extern "C" char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
+extern "C" char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
+extern "C" char *chat_parse_markdown(const char *str);
+extern "C" char *chat_parse_server(const char *str);
+extern "C" char *chat_password_hash(const char *pwd, const char *salt);
+extern "C" char *chat_valid_name(const char *name);
+extern "C" int chat_json_length(const char *str);
+extern "C" char *chat_encrypt_media(chat_ctrl ctrl, const char *key, const char *frame, const int len);
+extern "C" char *chat_decrypt_media(const char *key, const char *frame, const int len);
+
+// chat_write_file returns null-terminated string with JSON of WriteFileResult
+extern "C" char *chat_write_file(chat_ctrl ctrl, const char *path, const char *data, const int len);
+
+// chat_read_file returns a buffer with:
+// result status (1 byte), then if
+// status == 0 (success): buffer length (uint32, 4 bytes), buffer of specified length.
+// status == 1 (error): null-terminated error message string.
+extern "C" char *chat_read_file(const char *path, const char *key, const char *nonce);
+
+// chat_encrypt_file returns null-terminated string with JSON of WriteFileResult
+extern "C" char *chat_encrypt_file(chat_ctrl ctrl, const char *fromPath, const char *toPath);
+
+// chat_decrypt_file returns null-terminated string with the error message
+extern "C" char *chat_decrypt_file(const char *fromPath, const char *key, const char *nonce, const char *toPath);
+
+#endif /* simplex_h */
\ No newline at end of file
diff --git a/packages/simplex-chat-nodejs/docs/Namespace.api.md b/packages/simplex-chat-nodejs/docs/Namespace.api.md
new file mode 100644
index 0000000000..a1b3d2ca5a
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/Namespace.api.md
@@ -0,0 +1,32 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / api
+
+# api
+
+An API to send chat commands and receive chat events to/from chat core.
+You need to use it in bot event handlers, and for any other use cases.
+
+## Enumerations
+
+- [ConnReqType](api.Enumeration.ConnReqType.md)
+
+## Classes
+
+- [ChatApi](api.Class.ChatApi.md)
+- [ChatCommandError](api.Class.ChatCommandError.md)
+
+## Interfaces
+
+- [BotAddressSettings](api.Interface.BotAddressSettings.md)
+
+## Type Aliases
+
+- [EventSubscriberFunc](api.TypeAlias.EventSubscriberFunc.md)
+- [EventSubscribers](api.TypeAlias.EventSubscribers.md)
+
+## Variables
+
+- [defaultBotAddressSettings](api.Variable.defaultBotAddressSettings.md)
diff --git a/packages/simplex-chat-nodejs/docs/Namespace.bot.md b/packages/simplex-chat-nodejs/docs/Namespace.bot.md
new file mode 100644
index 0000000000..447b0b6f68
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/Namespace.bot.md
@@ -0,0 +1,20 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / bot
+
+# bot
+
+A simple declarative API to run a chat-bot with a single function call.
+It automates creating and updating of the bot profile, address and bot commands shown in the app UI.
+
+## Interfaces
+
+- [BotConfig](bot.Interface.BotConfig.md)
+- [BotDbOpts](bot.Interface.BotDbOpts.md)
+- [BotOptions](bot.Interface.BotOptions.md)
+
+## Functions
+
+- [run](bot.Function.run.md)
diff --git a/packages/simplex-chat-nodejs/docs/Namespace.core.md b/packages/simplex-chat-nodejs/docs/Namespace.core.md
new file mode 100644
index 0000000000..82b0d9ffba
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/Namespace.core.md
@@ -0,0 +1,48 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / core
+
+# core
+
+A low level API to the core library - the same that is used in desktop clients.
+You are unlikely to ever need to use this module directly.
+
+## Namespaces
+
+- [DBMigrationError](core.Namespace.DBMigrationError.md)
+- [MigrationError](core.Namespace.MigrationError.md)
+- [MTRError](core.Namespace.MTRError.md)
+
+## Enumerations
+
+- [MigrationConfirmation](core.Enumeration.MigrationConfirmation.md)
+
+## Classes
+
+- [ChatAPIError](core.Class.ChatAPIError.md)
+- [ChatInitError](core.Class.ChatInitError.md)
+
+## Interfaces
+
+- [APIResult](core.Interface.APIResult.md)
+- [CryptoArgs](core.Interface.CryptoArgs.md)
+- [UpMigration](core.Interface.UpMigration.md)
+
+## Type Aliases
+
+- [DBMigrationError](core.TypeAlias.DBMigrationError.md)
+- [MigrationError](core.TypeAlias.MigrationError.md)
+- [MTRError](core.TypeAlias.MTRError.md)
+
+## Functions
+
+- [chatCloseStore](core.Function.chatCloseStore.md)
+- [chatDecryptFile](core.Function.chatDecryptFile.md)
+- [chatEncryptFile](core.Function.chatEncryptFile.md)
+- [chatMigrateInit](core.Function.chatMigrateInit.md)
+- [chatReadFile](core.Function.chatReadFile.md)
+- [chatRecvMsgWait](core.Function.chatRecvMsgWait.md)
+- [chatSendCmd](core.Function.chatSendCmd.md)
+- [chatWriteFile](core.Function.chatWriteFile.md)
diff --git a/packages/simplex-chat-nodejs/docs/Namespace.util.md b/packages/simplex-chat-nodejs/docs/Namespace.util.md
new file mode 100644
index 0000000000..a6e7d9c7f3
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/Namespace.util.md
@@ -0,0 +1,25 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / util
+
+# util
+
+Useful functions for chat events and types.
+
+## Interfaces
+
+- [BotCommand](util.Interface.BotCommand.md)
+
+## Functions
+
+- [botAddressSettings](util.Function.botAddressSettings.md)
+- [chatInfoName](util.Function.chatInfoName.md)
+- [chatInfoRef](util.Function.chatInfoRef.md)
+- [ciBotCommand](util.Function.ciBotCommand.md)
+- [ciContentText](util.Function.ciContentText.md)
+- [contactAddressStr](util.Function.contactAddressStr.md)
+- [fromLocalProfile](util.Function.fromLocalProfile.md)
+- [reactionText](util.Function.reactionText.md)
+- [senderName](util.Function.senderName.md)
diff --git a/packages/simplex-chat-nodejs/docs/README.md b/packages/simplex-chat-nodejs/docs/README.md
new file mode 100644
index 0000000000..f954730ed6
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/README.md
@@ -0,0 +1,12 @@
+**simplex-chat**
+
+***
+
+# simplex-chat
+
+## Namespaces
+
+- [api](Namespace.api.md)
+- [bot](Namespace.bot.md)
+- [core](Namespace.core.md)
+- [util](Namespace.util.md)
diff --git a/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md b/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md
new file mode 100644
index 0000000000..6812740aa2
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md
@@ -0,0 +1,1602 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [api](Namespace.api.md) / ChatApi
+
+# Class: ChatApi
+
+Defined in: [src/api.ts:62](../src/api.ts#L62)
+
+Main API class for interacting with the chat core library.
+
+## Properties
+
+### ctrl\_
+
+> `protected` **ctrl\_**: `bigint` \| `undefined`
+
+Defined in: [src/api.ts:68](../src/api.ts#L68)
+
+## Accessors
+
+### ctrl
+
+#### Get Signature
+
+> **get** **ctrl**(): `bigint`
+
+Defined in: [src/api.ts:295](../src/api.ts#L295)
+
+Chat controller reference
+
+##### Returns
+
+`bigint`
+
+***
+
+### initialized
+
+#### Get Signature
+
+> **get** **initialized**(): `boolean`
+
+Defined in: [src/api.ts:281](../src/api.ts#L281)
+
+Chat controller is initialized
+
+##### Returns
+
+`boolean`
+
+***
+
+### started
+
+#### Get Signature
+
+> **get** **started**(): `boolean`
+
+Defined in: [src/api.ts:288](../src/api.ts#L288)
+
+Chat controller is started
+
+##### Returns
+
+`boolean`
+
+## Methods
+
+### apiAcceptContactRequest()
+
+> **apiAcceptContactRequest**(`contactReqId`): `Promise`\<`Contact`\>
+
+Defined in: [src/api.ts:697](../src/api.ts#L697)
+
+Accept contact request.
+Network usage: interactive.
+
+#### Parameters
+
+##### contactReqId
+
+`number`
+
+#### Returns
+
+`Promise`\<`Contact`\>
+
+***
+
+### apiAcceptMember()
+
+> **apiAcceptMember**(`groupId`, `groupMemberId`, `memberRole`): `Promise`\<`GroupMember`\>
+
+Defined in: [src/api.ts:517](../src/api.ts#L517)
+
+Accept group member. Requires Admin role.
+Network usage: background.
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+##### groupMemberId
+
+`number`
+
+##### memberRole
+
+`GroupMemberRole`
+
+#### Returns
+
+`Promise`\<`GroupMember`\>
+
+***
+
+### apiAddMember()
+
+> **apiAddMember**(`groupId`, `contactId`, `memberRole`): `Promise`\<`GroupMember`\>
+
+Defined in: [src/api.ts:497](../src/api.ts#L497)
+
+Add contact to group. Requires bot to have Admin role.
+Network usage: interactive.
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+##### contactId
+
+`number`
+
+##### memberRole
+
+`GroupMemberRole`
+
+#### Returns
+
+`Promise`\<`GroupMember`\>
+
+***
+
+### apiBlockMembersForAll()
+
+> **apiBlockMembersForAll**(`groupId`, `groupMemberIds`, `blocked`): `Promise`\<`void`\>
+
+Defined in: [src/api.ts:537](../src/api.ts#L537)
+
+Block members. Requires Moderator role.
+Network usage: background.
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+##### groupMemberIds
+
+`number`[]
+
+##### blocked
+
+`boolean`
+
+#### Returns
+
+`Promise`\<`void`\>
+
+***
+
+### apiCancelFile()
+
+> **apiCancelFile**(`fileId`): `Promise`\<`void`\>
+
+Defined in: [src/api.ts:487](../src/api.ts#L487)
+
+Cancel file.
+Network usage: background.
+
+#### Parameters
+
+##### fileId
+
+`number`
+
+#### Returns
+
+`Promise`\<`void`\>
+
+***
+
+### apiChatItemReaction()
+
+> **apiChatItemReaction**(`chatType`, `chatId`, `chatItemId`, `add`, `reaction`): `Promise`\<`ChatItemDeletion`[]\>
+
+Defined in: [src/api.ts:461](../src/api.ts#L461)
+
+Add/remove message reaction.
+Network usage: background.
+
+#### Parameters
+
+##### chatType
+
+`ChatType`
+
+##### chatId
+
+`number`
+
+##### chatItemId
+
+`number`
+
+##### add
+
+`boolean`
+
+##### reaction
+
+`MsgReaction`
+
+#### Returns
+
+`Promise`\<`ChatItemDeletion`[]\>
+
+***
+
+### apiConnect()
+
+> **apiConnect**(`userId`, `incognito`, `preparedLink?`): `Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\>
+
+Defined in: [src/api.ts:666](../src/api.ts#L666)
+
+Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link
+Network usage: interactive.
+
+#### Parameters
+
+##### userId
+
+`number`
+
+##### incognito
+
+`boolean`
+
+##### preparedLink?
+
+`CreatedConnLink`
+
+#### Returns
+
+`Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\>
+
+***
+
+### apiConnectActiveUser()
+
+> **apiConnectActiveUser**(`connLink`): `Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\>
+
+Defined in: [src/api.ts:675](../src/api.ts#L675)
+
+Connect via SimpleX link as string in the active user profile.
+Network usage: interactive.
+
+#### Parameters
+
+##### connLink
+
+`string`
+
+#### Returns
+
+`Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\>
+
+***
+
+### apiConnectPlan()
+
+> **apiConnectPlan**(`userId`, `connectionLink`): `Promise`\<\[`ConnectionPlan`, `CreatedConnLink`\]\>
+
+Defined in: [src/api.ts:656](../src/api.ts#L656)
+
+Determine SimpleX link type and if the bot is already connected via this link.
+Network usage: interactive.
+
+#### Parameters
+
+##### userId
+
+`number`
+
+##### connectionLink
+
+`string`
+
+#### Returns
+
+`Promise`\<\[`ConnectionPlan`, `CreatedConnLink`\]\>
+
+***
+
+### apiCreateActiveUser()
+
+> **apiCreateActiveUser**(`profile?`): `Promise`\<`User`\>
+
+Defined in: [src/api.ts:774](../src/api.ts#L774)
+
+Create new user profile
+Network usage: no.
+
+#### Parameters
+
+##### profile?
+
+`Profile`
+
+#### Returns
+
+`Promise`\<`User`\>
+
+***
+
+### apiCreateGroupLink()
+
+> **apiCreateGroupLink**(`groupId`, `memberRole`): `Promise`\<`string`\>
+
+Defined in: [src/api.ts:597](../src/api.ts#L597)
+
+Create group link.
+Network usage: interactive.
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+##### memberRole
+
+`GroupMemberRole`
+
+#### Returns
+
+`Promise`\<`string`\>
+
+***
+
+### apiCreateLink()
+
+> **apiCreateLink**(`userId`): `Promise`\<`string`\>
+
+Defined in: [src/api.ts:643](../src/api.ts#L643)
+
+Create 1-time invitation link.
+Network usage: interactive.
+
+#### Parameters
+
+##### userId
+
+`number`
+
+#### Returns
+
+`Promise`\<`string`\>
+
+***
+
+### apiCreateUserAddress()
+
+> **apiCreateUserAddress**(`userId`): `Promise`\<`CreatedConnLink`\>
+
+Defined in: [src/api.ts:312](../src/api.ts#L312)
+
+Create bot address.
+Network usage: interactive.
+
+#### Parameters
+
+##### userId
+
+`number`
+
+#### Returns
+
+`Promise`\<`CreatedConnLink`\>
+
+***
+
+### apiDeleteChat()
+
+> **apiDeleteChat**(`chatType`, `chatId`, `deleteMode`): `Promise`\<`void`\>
+
+Defined in: [src/api.ts:737](../src/api.ts#L737)
+
+Delete chat.
+Network usage: background.
+
+#### Parameters
+
+##### chatType
+
+`ChatType`
+
+##### chatId
+
+`number`
+
+##### deleteMode
+
+`ChatDeleteMode` = `...`
+
+#### Returns
+
+`Promise`\<`void`\>
+
+***
+
+### apiDeleteChatItems()
+
+> **apiDeleteChatItems**(`chatType`, `chatId`, `chatItemIds`, `deleteMode`): `Promise`\<`ChatItemDeletion`[]\>
+
+Defined in: [src/api.ts:436](../src/api.ts#L436)
+
+Delete message.
+Network usage: background.
+
+#### Parameters
+
+##### chatType
+
+`ChatType`
+
+##### chatId
+
+`number`
+
+##### chatItemIds
+
+`number`[]
+
+##### deleteMode
+
+`CIDeleteMode`
+
+#### Returns
+
+`Promise`\<`ChatItemDeletion`[]\>
+
+***
+
+### apiDeleteGroupLink()
+
+> **apiDeleteGroupLink**(`groupId`): `Promise`\<`void`\>
+
+Defined in: [src/api.ts:619](../src/api.ts#L619)
+
+Delete group link.
+Network usage: background.
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+#### Returns
+
+`Promise`\<`void`\>
+
+***
+
+### apiDeleteMemberChatItem()
+
+> **apiDeleteMemberChatItem**(`groupId`, `chatItemIds`): `Promise`\<`ChatItemDeletion`[]\>
+
+Defined in: [src/api.ts:451](../src/api.ts#L451)
+
+Moderate message. Requires Moderator role (and higher than message author's).
+Network usage: background.
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+##### chatItemIds
+
+`number`[]
+
+#### Returns
+
+`Promise`\<`ChatItemDeletion`[]\>
+
+***
+
+### apiDeleteUser()
+
+> **apiDeleteUser**(`userId`, `delSMPQueues`, `viewPwd?`): `Promise`\<`void`\>
+
+Defined in: [src/api.ts:804](../src/api.ts#L804)
+
+Delete user profile.
+Network usage: background.
+
+#### Parameters
+
+##### userId
+
+`number`
+
+##### delSMPQueues
+
+`boolean`
+
+##### viewPwd?
+
+`string`
+
+#### Returns
+
+`Promise`\<`void`\>
+
+***
+
+### apiDeleteUserAddress()
+
+> **apiDeleteUserAddress**(`userId`): `Promise`\<`void`\>
+
+Defined in: [src/api.ts:322](../src/api.ts#L322)
+
+Deletes a user address.
+Network usage: background.
+
+#### Parameters
+
+##### userId
+
+`number`
+
+#### Returns
+
+`Promise`\<`void`\>
+
+***
+
+### apiGetActiveUser()
+
+> **apiGetActiveUser**(): `Promise`\<`User` \| `undefined`\>
+
+Defined in: [src/api.ts:754](../src/api.ts#L754)
+
+Get active user profile
+Network usage: no.
+
+#### Returns
+
+`Promise`\<`User` \| `undefined`\>
+
+***
+
+### apiGetGroupLink()
+
+> **apiGetGroupLink**(`groupId`): `Promise`\<`GroupLink`\>
+
+Defined in: [src/api.ts:628](../src/api.ts#L628)
+
+Get group link.
+Network usage: no.
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+#### Returns
+
+`Promise`\<`GroupLink`\>
+
+***
+
+### apiGetGroupLinkStr()
+
+> **apiGetGroupLinkStr**(`groupId`): `Promise`\<`string`\>
+
+Defined in: [src/api.ts:634](../src/api.ts#L634)
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+#### Returns
+
+`Promise`\<`string`\>
+
+***
+
+### apiGetUserAddress()
+
+> **apiGetUserAddress**(`userId`): `Promise`\<`UserContactLink` \| `undefined`\>
+
+Defined in: [src/api.ts:332](../src/api.ts#L332)
+
+Get bot address and settings.
+Network usage: no.
+
+#### Parameters
+
+##### userId
+
+`number`
+
+#### Returns
+
+`Promise`\<`UserContactLink` \| `undefined`\>
+
+***
+
+### apiJoinGroup()
+
+> **apiJoinGroup**(`groupId`): `Promise`\<`GroupInfo`\>
+
+Defined in: [src/api.ts:507](../src/api.ts#L507)
+
+Join group.
+Network usage: interactive.
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+#### Returns
+
+`Promise`\<`GroupInfo`\>
+
+***
+
+### apiLeaveGroup()
+
+> **apiLeaveGroup**(`groupId`): `Promise`\<`GroupInfo`\>
+
+Defined in: [src/api.ts:557](../src/api.ts#L557)
+
+Leave group.
+Network usage: background.
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+#### Returns
+
+`Promise`\<`GroupInfo`\>
+
+***
+
+### apiListContacts()
+
+> **apiListContacts**(`userId`): `Promise`\<`Contact`[]\>
+
+Defined in: [src/api.ts:717](../src/api.ts#L717)
+
+Get contacts.
+Network usage: no.
+
+#### Parameters
+
+##### userId
+
+`number`
+
+#### Returns
+
+`Promise`\<`Contact`[]\>
+
+***
+
+### apiListGroups()
+
+> **apiListGroups**(`userId`, `contactId?`, `search?`): `Promise`\<`GroupInfo`[]\>
+
+Defined in: [src/api.ts:727](../src/api.ts#L727)
+
+Get groups.
+Network usage: no.
+
+#### Parameters
+
+##### userId
+
+`number`
+
+##### contactId?
+
+`number`
+
+##### search?
+
+`string`
+
+#### Returns
+
+`Promise`\<`GroupInfo`[]\>
+
+***
+
+### apiListMembers()
+
+> **apiListMembers**(`groupId`): `Promise`\<`GroupMember`[]\>
+
+Defined in: [src/api.ts:567](../src/api.ts#L567)
+
+Get group members.
+Network usage: no.
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+#### Returns
+
+`Promise`\<`GroupMember`[]\>
+
+***
+
+### apiListUsers()
+
+> **apiListUsers**(): `Promise`\<`UserInfo`[]\>
+
+Defined in: [src/api.ts:784](../src/api.ts#L784)
+
+Get all user profiles
+Network usage: no.
+
+#### Returns
+
+`Promise`\<`UserInfo`[]\>
+
+***
+
+### apiNewGroup()
+
+> **apiNewGroup**(`userId`, `groupProfile`): `Promise`\<`GroupInfo`\>
+
+Defined in: [src/api.ts:577](../src/api.ts#L577)
+
+Create group.
+Network usage: no.
+
+#### Parameters
+
+##### userId
+
+`number`
+
+##### groupProfile
+
+`GroupProfile`
+
+#### Returns
+
+`Promise`\<`GroupInfo`\>
+
+***
+
+### apiReceiveFile()
+
+> **apiReceiveFile**(`fileId`): `Promise`\<`AChatItem`\>
+
+Defined in: [src/api.ts:477](../src/api.ts#L477)
+
+Receive file.
+Network usage: no.
+
+#### Parameters
+
+##### fileId
+
+`number`
+
+#### Returns
+
+`Promise`\<`AChatItem`\>
+
+***
+
+### apiRejectContactRequest()
+
+> **apiRejectContactRequest**(`contactReqId`): `Promise`\<`void`\>
+
+Defined in: [src/api.ts:707](../src/api.ts#L707)
+
+Reject contact request. The user who sent the request is **not notified**.
+Network usage: no.
+
+#### Parameters
+
+##### contactReqId
+
+`number`
+
+#### Returns
+
+`Promise`\<`void`\>
+
+***
+
+### apiRemoveMembers()
+
+> **apiRemoveMembers**(`groupId`, `memberIds`, `withMessages`): `Promise`\<`GroupMember`[]\>
+
+Defined in: [src/api.ts:547](../src/api.ts#L547)
+
+Remove members. Requires Admin role.
+Network usage: background.
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+##### memberIds
+
+`number`[]
+
+##### withMessages
+
+`boolean` = `false`
+
+#### Returns
+
+`Promise`\<`GroupMember`[]\>
+
+***
+
+### apiSendMessages()
+
+> **apiSendMessages**(`chat`, `messages`, `liveMessage`): `Promise`\<`AChatItem`[]\>
+
+Defined in: [src/api.ts:381](../src/api.ts#L381)
+
+Send messages.
+Network usage: background.
+
+#### Parameters
+
+##### chat
+
+`ChatInfo` | `ChatRef` | \[`ChatType`, `number`\]
+
+##### messages
+
+`ComposedMessage`[]
+
+##### liveMessage
+
+`boolean` = `false`
+
+#### Returns
+
+`Promise`\<`AChatItem`[]\>
+
+***
+
+### apiSendTextMessage()
+
+> **apiSendTextMessage**(`chat`, `text`, `inReplyTo?`): `Promise`\<`AChatItem`[]\>
+
+Defined in: [src/api.ts:403](../src/api.ts#L403)
+
+Send text message.
+Network usage: background.
+
+#### Parameters
+
+##### chat
+
+`ChatInfo` | `ChatRef` | \[`ChatType`, `number`\]
+
+##### text
+
+`string`
+
+##### inReplyTo?
+
+`number`
+
+#### Returns
+
+`Promise`\<`AChatItem`[]\>
+
+***
+
+### apiSendTextReply()
+
+> **apiSendTextReply**(`chatItem`, `text`): `Promise`\<`AChatItem`[]\>
+
+Defined in: [src/api.ts:411](../src/api.ts#L411)
+
+Send text message in reply to received message.
+Network usage: background.
+
+#### Parameters
+
+##### chatItem
+
+`AChatItem`
+
+##### text
+
+`string`
+
+#### Returns
+
+`Promise`\<`AChatItem`[]\>
+
+***
+
+### apiSetActiveUser()
+
+> **apiSetActiveUser**(`userId`, `viewPwd?`): `Promise`\<`User`\>
+
+Defined in: [src/api.ts:794](../src/api.ts#L794)
+
+Set active user profile
+Network usage: no.
+
+#### Parameters
+
+##### userId
+
+`number`
+
+##### viewPwd?
+
+`string`
+
+#### Returns
+
+`Promise`\<`User`\>
+
+***
+
+### apiSetAddressSettings()
+
+> **apiSetAddressSettings**(`userId`, `__namedParameters`): `Promise`\<`void`\>
+
+Defined in: [src/api.ts:364](../src/api.ts#L364)
+
+Set bot address settings.
+Network usage: interactive.
+
+#### Parameters
+
+##### userId
+
+`number`
+
+##### \_\_namedParameters
+
+[`BotAddressSettings`](api.Interface.BotAddressSettings.md)
+
+#### Returns
+
+`Promise`\<`void`\>
+
+***
+
+### apiSetContactPrefs()
+
+> **apiSetContactPrefs**(`contactId`, `preferences`): `Promise`\<`void`\>
+
+Defined in: [src/api.ts:830](../src/api.ts#L830)
+
+Configure chat preference overrides for the contact.
+Network usage: background.
+
+#### Parameters
+
+##### contactId
+
+`number`
+
+##### preferences
+
+`Preferences`
+
+#### Returns
+
+`Promise`\<`void`\>
+
+***
+
+### apiSetGroupLinkMemberRole()
+
+> **apiSetGroupLinkMemberRole**(`groupId`, `memberRole`): `Promise`\<`void`\>
+
+Defined in: [src/api.ts:610](../src/api.ts#L610)
+
+Set member role for group link.
+Network usage: no.
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+##### memberRole
+
+`GroupMemberRole`
+
+#### Returns
+
+`Promise`\<`void`\>
+
+***
+
+### apiSetMembersRole()
+
+> **apiSetMembersRole**(`groupId`, `groupMemberIds`, `memberRole`): `Promise`\<`void`\>
+
+Defined in: [src/api.ts:527](../src/api.ts#L527)
+
+Set members role. Requires Admin role.
+Network usage: background.
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+##### groupMemberIds
+
+`number`[]
+
+##### memberRole
+
+`GroupMemberRole`
+
+#### Returns
+
+`Promise`\<`void`\>
+
+***
+
+### apiSetProfileAddress()
+
+> **apiSetProfileAddress**(`userId`, `enable`): `Promise`\<`UserProfileUpdateSummary`\>
+
+Defined in: [src/api.ts:350](../src/api.ts#L350)
+
+Add address to bot profile.
+Network usage: interactive.
+
+#### Parameters
+
+##### userId
+
+`number`
+
+##### enable
+
+`boolean`
+
+#### Returns
+
+`Promise`\<`UserProfileUpdateSummary`\>
+
+***
+
+### apiUpdateChatItem()
+
+> **apiUpdateChatItem**(`chatType`, `chatId`, `chatItemId`, `msgContent`, `liveMessage`): `Promise`\<`ChatItem`\>
+
+Defined in: [src/api.ts:419](../src/api.ts#L419)
+
+Update message.
+Network usage: background.
+
+#### Parameters
+
+##### chatType
+
+`ChatType`
+
+##### chatId
+
+`number`
+
+##### chatItemId
+
+`number`
+
+##### msgContent
+
+`MsgContent`
+
+##### liveMessage
+
+`false`
+
+#### Returns
+
+`Promise`\<`ChatItem`\>
+
+***
+
+### apiUpdateGroupProfile()
+
+> **apiUpdateGroupProfile**(`groupId`, `groupProfile`): `Promise`\<`GroupInfo`\>
+
+Defined in: [src/api.ts:587](../src/api.ts#L587)
+
+Update group profile.
+Network usage: background.
+
+#### Parameters
+
+##### groupId
+
+`number`
+
+##### groupProfile
+
+`GroupProfile`
+
+#### Returns
+
+`Promise`\<`GroupInfo`\>
+
+***
+
+### apiUpdateProfile()
+
+> **apiUpdateProfile**(`userId`, `profile`): `Promise`\<`UserProfileUpdateSummary` \| `undefined`\>
+
+Defined in: [src/api.ts:814](../src/api.ts#L814)
+
+Update user profile.
+Network usage: background.
+
+#### Parameters
+
+##### userId
+
+`number`
+
+##### profile
+
+`Profile`
+
+#### Returns
+
+`Promise`\<`UserProfileUpdateSummary` \| `undefined`\>
+
+***
+
+### close()
+
+> **close**(): `Promise`\<`void`\>
+
+Defined in: [src/api.ts:114](../src/api.ts#L114)
+
+Close chat database.
+Usually doesn't need to be called in chat bots.
+
+#### Returns
+
+`Promise`\<`void`\>
+
+***
+
+### off()
+
+> **off**\<`K`\>(`event`, `subscriber`): `void`
+
+Defined in: [src/api.ts:253](../src/api.ts#L253)
+
+Unsubscribe all or a specific handler from a specific event.
+
+#### Type Parameters
+
+##### K
+
+`K` *extends* `Tag`
+
+#### Parameters
+
+##### event
+
+`K`
+
+The event type to unsubscribe from.
+
+##### subscriber
+
+An optional subscriber function for the event.
+
+[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`K`\> | `undefined`
+
+#### Returns
+
+`void`
+
+***
+
+### offAny()
+
+> **offAny**(`receiver`): `void`
+
+Defined in: [src/api.ts:269](../src/api.ts#L269)
+
+Unsubscribe all or a specific handler from any events.
+
+#### Parameters
+
+##### receiver
+
+An optional subscriber function for the event.
+
+[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`Tag`\> | `undefined`
+
+#### Returns
+
+`void`
+
+***
+
+### on()
+
+#### Call Signature
+
+> **on**\<`K`\>(`subscribers`): `void`
+
+Defined in: [src/api.ts:163](../src/api.ts#L163)
+
+Subscribe multiple event handlers at once.
+
+##### Type Parameters
+
+###### K
+
+`K` *extends* `Tag`
+
+##### Parameters
+
+###### subscribers
+
+[`EventSubscribers`](api.TypeAlias.EventSubscribers.md)
+
+An object mapping event types (CEvt.Tag) to their subscriber functions.
+
+##### Returns
+
+`void`
+
+##### Throws
+
+If the same function is subscribed to event.
+
+#### Call Signature
+
+> **on**\<`K`\>(`event`, `subscriber`): `void`
+
+Defined in: [src/api.ts:171](../src/api.ts#L171)
+
+Subscribe a handler to a specific event.
+
+##### Type Parameters
+
+###### K
+
+`K` *extends* `Tag`
+
+##### Parameters
+
+###### event
+
+`K`
+
+The event type to subscribe to.
+
+###### subscriber
+
+[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`K`\>
+
+The subscriber function for the event.
+
+##### Returns
+
+`void`
+
+##### Throws
+
+If the same function is subscribed to event.
+
+***
+
+### onAny()
+
+> **onAny**(`receiver`): `void`
+
+Defined in: [src/api.ts:194](../src/api.ts#L194)
+
+Subscribe a handler to any event.
+
+#### Parameters
+
+##### receiver
+
+[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`Tag`\>
+
+The receiver function for any event.
+
+#### Returns
+
+`void`
+
+#### Throws
+
+If the same function is subscribed to event.
+
+***
+
+### once()
+
+> **once**\<`K`\>(`event`, `subscriber`): `void`
+
+Defined in: [src/api.ts:205](../src/api.ts#L205)
+
+Subscribe a handler to a specific event to be delivered one time.
+
+#### Type Parameters
+
+##### K
+
+`K` *extends* `Tag`
+
+#### Parameters
+
+##### event
+
+`K`
+
+The event type to subscribe to.
+
+##### subscriber
+
+[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`K`\>
+
+The subscriber function for the event.
+
+#### Returns
+
+`void`
+
+#### Throws
+
+If the same function is subscribed to event.
+
+***
+
+### recvChatEvent()
+
+> **recvChatEvent**(`wait`): `Promise`\<`ChatEvent` \| `undefined`\>
+
+Defined in: [src/api.ts:304](../src/api.ts#L304)
+
+#### Parameters
+
+##### wait
+
+`number` = `5_000_000`
+
+#### Returns
+
+`Promise`\<`ChatEvent` \| `undefined`\>
+
+***
+
+### sendChatCmd()
+
+> **sendChatCmd**(`cmd`): `Promise`\<`ChatResponse`\>
+
+Defined in: [src/api.ts:300](../src/api.ts#L300)
+
+#### Parameters
+
+##### cmd
+
+`string`
+
+#### Returns
+
+`Promise`\<`ChatResponse`\>
+
+***
+
+### startChat()
+
+> **startChat**(): `Promise`\<`void`\>
+
+Defined in: [src/api.ts:88](../src/api.ts#L88)
+
+Start chat controller. Must be called with the existing user profile.
+
+#### Returns
+
+`Promise`\<`void`\>
+
+***
+
+### stopChat()
+
+> **stopChat**(): `Promise`\<`void`\>
+
+Defined in: [src/api.ts:102](../src/api.ts#L102)
+
+Stop chat controller.
+Must be called before closing the database.
+Usually doesn't need to be called in chat bots.
+
+#### Returns
+
+`Promise`\<`void`\>
+
+***
+
+### wait()
+
+#### Call Signature
+
+> **wait**\<`K`\>(`event`): `Promise`\<`ChatEvent` & \{ `type`: `K`; \}\>
+
+Defined in: [src/api.ts:213](../src/api.ts#L213)
+
+Waits for specific event, with an optional predicate.
+Returns `undefined` on timeout if specified.
+
+##### Type Parameters
+
+###### K
+
+`K` *extends* `Tag`
+
+##### Parameters
+
+###### event
+
+`K`
+
+##### Returns
+
+`Promise`\<`ChatEvent` & \{ `type`: `K`; \}\>
+
+#### Call Signature
+
+> **wait**\<`K`\>(`event`, `predicate`): `Promise`\<`ChatEvent` & \{ `type`: `K`; \}\>
+
+Defined in: [src/api.ts:214](../src/api.ts#L214)
+
+Waits for specific event, with an optional predicate.
+Returns `undefined` on timeout if specified.
+
+##### Type Parameters
+
+###### K
+
+`K` *extends* `Tag`
+
+##### Parameters
+
+###### event
+
+`K`
+
+###### predicate
+
+(`event`) => `boolean` | `undefined`
+
+##### Returns
+
+`Promise`\<`ChatEvent` & \{ `type`: `K`; \}\>
+
+#### Call Signature
+
+> **wait**\<`K`\>(`event`, `timeout`): `Promise`\
+
+Defined in: [src/api.ts:215](../src/api.ts#L215)
+
+Waits for specific event, with an optional predicate.
+Returns `undefined` on timeout if specified.
+
+##### Type Parameters
+
+###### K
+
+`K` *extends* `Tag`
+
+##### Parameters
+
+###### event
+
+`K`
+
+###### timeout
+
+`number`
+
+##### Returns
+
+`Promise`\
+
+#### Call Signature
+
+> **wait**\<`K`\>(`event`, `predicate`, `timeout`): `Promise`\
+
+Defined in: [src/api.ts:216](../src/api.ts#L216)
+
+Waits for specific event, with an optional predicate.
+Returns `undefined` on timeout if specified.
+
+##### Type Parameters
+
+###### K
+
+`K` *extends* `Tag`
+
+##### Parameters
+
+###### event
+
+`K`
+
+###### predicate
+
+(`event`) => `boolean` | `undefined`
+
+###### timeout
+
+`number`
+
+##### Returns
+
+`Promise`\
+
+***
+
+### init()
+
+> `static` **init**(`dbFilePrefix`, `dbKey?`, `confirm?`): `Promise`\<`ChatApi`\>
+
+Defined in: [src/api.ts:76](../src/api.ts#L76)
+
+Initializes the ChatApi.
+
+#### Parameters
+
+##### dbFilePrefix
+
+`string`
+
+File prefix for the database files.
+
+##### dbKey?
+
+`string` = `""`
+
+Database encryption key.
+
+##### confirm?
+
+[`MigrationConfirmation`](core.Enumeration.MigrationConfirmation.md) = `core.MigrationConfirmation.YesUp`
+
+Migration confirmation mode.
+
+#### Returns
+
+`Promise`\<`ChatApi`\>
diff --git a/packages/simplex-chat-nodejs/docs/api.Class.ChatCommandError.md b/packages/simplex-chat-nodejs/docs/api.Class.ChatCommandError.md
new file mode 100644
index 0000000000..a4955cb3d9
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/api.Class.ChatCommandError.md
@@ -0,0 +1,205 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [api](Namespace.api.md) / ChatCommandError
+
+# Class: ChatCommandError
+
+Defined in: [src/api.ts:5](../src/api.ts#L5)
+
+## Extends
+
+- `Error`
+
+## Constructors
+
+### Constructor
+
+> **new ChatCommandError**(`message`, `response`): `ChatCommandError`
+
+Defined in: [src/api.ts:6](../src/api.ts#L6)
+
+#### Parameters
+
+##### message
+
+`string`
+
+##### response
+
+`ChatResponse`
+
+#### Returns
+
+`ChatCommandError`
+
+#### Overrides
+
+`Error.constructor`
+
+## Properties
+
+### message
+
+> **message**: `string`
+
+Defined in: [src/api.ts:6](../src/api.ts#L6)
+
+#### Inherited from
+
+`Error.message`
+
+***
+
+### name
+
+> **name**: `string`
+
+Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1076](../node_modules/typescript/lib/lib.es5.d.ts#L1076)
+
+#### Inherited from
+
+`Error.name`
+
+***
+
+### response
+
+> **response**: `ChatResponse`
+
+Defined in: [src/api.ts:6](../src/api.ts#L6)
+
+***
+
+### stack?
+
+> `optional` **stack**: `string`
+
+Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1078](../node_modules/typescript/lib/lib.es5.d.ts#L1078)
+
+#### Inherited from
+
+`Error.stack`
+
+***
+
+### stackTraceLimit
+
+> `static` **stackTraceLimit**: `number`
+
+Defined in: [node\_modules/@types/node/globals.d.ts:67](../node_modules/@types/node/globals.d.ts#L67)
+
+The `Error.stackTraceLimit` property specifies the number of stack frames
+collected by a stack trace (whether generated by `new Error().stack` or
+`Error.captureStackTrace(obj)`).
+
+The default value is `10` but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured _after_ the value has been changed.
+
+If set to a non-number value, or set to a negative number, stack traces will
+not capture any frames.
+
+#### Inherited from
+
+`Error.stackTraceLimit`
+
+## Methods
+
+### captureStackTrace()
+
+> `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void`
+
+Defined in: [node\_modules/@types/node/globals.d.ts:51](../node_modules/@types/node/globals.d.ts#L51)
+
+Creates a `.stack` property on `targetObject`, which when accessed returns
+a string representing the location in the code at which
+`Error.captureStackTrace()` was called.
+
+```js
+const myObject = {};
+Error.captureStackTrace(myObject);
+myObject.stack; // Similar to `new Error().stack`
+```
+
+The first line of the trace will be prefixed with
+`${myObject.name}: ${myObject.message}`.
+
+The optional `constructorOpt` argument accepts a function. If given, all frames
+above `constructorOpt`, including `constructorOpt`, will be omitted from the
+generated stack trace.
+
+The `constructorOpt` argument is useful for hiding implementation
+details of error generation from the user. For instance:
+
+```js
+function a() {
+ b();
+}
+
+function b() {
+ c();
+}
+
+function c() {
+ // Create an error without stack trace to avoid calculating the stack trace twice.
+ const { stackTraceLimit } = Error;
+ Error.stackTraceLimit = 0;
+ const error = new Error();
+ Error.stackTraceLimit = stackTraceLimit;
+
+ // Capture the stack trace above function b
+ Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
+ throw error;
+}
+
+a();
+```
+
+#### Parameters
+
+##### targetObject
+
+`object`
+
+##### constructorOpt?
+
+`Function`
+
+#### Returns
+
+`void`
+
+#### Inherited from
+
+`Error.captureStackTrace`
+
+***
+
+### prepareStackTrace()
+
+> `static` **prepareStackTrace**(`err`, `stackTraces`): `any`
+
+Defined in: [node\_modules/@types/node/globals.d.ts:55](../node_modules/@types/node/globals.d.ts#L55)
+
+#### Parameters
+
+##### err
+
+`Error`
+
+##### stackTraces
+
+`CallSite`[]
+
+#### Returns
+
+`any`
+
+#### See
+
+https://v8.dev/docs/stack-trace-api#customizing-stack-traces
+
+#### Inherited from
+
+`Error.prepareStackTrace`
diff --git a/packages/simplex-chat-nodejs/docs/api.Enumeration.ConnReqType.md b/packages/simplex-chat-nodejs/docs/api.Enumeration.ConnReqType.md
new file mode 100644
index 0000000000..dcabb85b67
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/api.Enumeration.ConnReqType.md
@@ -0,0 +1,27 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [api](Namespace.api.md) / ConnReqType
+
+# Enumeration: ConnReqType
+
+Defined in: [src/api.ts:15](../src/api.ts#L15)
+
+Connection request types.
+
+## Enumeration Members
+
+### Contact
+
+> **Contact**: `"contact"`
+
+Defined in: [src/api.ts:17](../src/api.ts#L17)
+
+***
+
+### Invitation
+
+> **Invitation**: `"invitation"`
+
+Defined in: [src/api.ts:16](../src/api.ts#L16)
diff --git a/packages/simplex-chat-nodejs/docs/api.Interface.BotAddressSettings.md b/packages/simplex-chat-nodejs/docs/api.Interface.BotAddressSettings.md
new file mode 100644
index 0000000000..efb4a75e81
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/api.Interface.BotAddressSettings.md
@@ -0,0 +1,60 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [api](Namespace.api.md) / BotAddressSettings
+
+# Interface: BotAddressSettings
+
+Defined in: [src/api.ts:23](../src/api.ts#L23)
+
+Bot address settings.
+
+## Properties
+
+### autoAccept?
+
+> `optional` **autoAccept**: `boolean`
+
+Defined in: [src/api.ts:28](../src/api.ts#L28)
+
+Automatically accept contact requests.
+
+#### Default
+
+```ts
+true
+```
+
+***
+
+### businessAddress?
+
+> `optional` **businessAddress**: `boolean`
+
+Defined in: [src/api.ts:41](../src/api.ts#L41)
+
+Business contact address.
+For all requests business chats will be created where other participants can be added.
+
+#### Default
+
+```ts
+false
+```
+
+***
+
+### welcomeMessage?
+
+> `optional` **welcomeMessage**: `string` \| `MsgContent`
+
+Defined in: [src/api.ts:34](../src/api.ts#L34)
+
+Optional welcome message to show before connection to the users.
+
+#### Default
+
+```ts
+undefined (no welcome message)
+```
diff --git a/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md
new file mode 100644
index 0000000000..6197befc8a
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md
@@ -0,0 +1,27 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [api](Namespace.api.md) / EventSubscriberFunc
+
+# Type Alias: EventSubscriberFunc()\
+
+> **EventSubscriberFunc**\<`K`\> = (`event`) => `void` \| `Promise`\<`void`\>
+
+Defined in: [src/api.ts:50](../src/api.ts#L50)
+
+## Type Parameters
+
+### K
+
+`K` *extends* `CEvt.Tag`
+
+## Parameters
+
+### event
+
+`ChatEvent` & \{ `type`: `K`; \}
+
+## Returns
+
+`void` \| `Promise`\<`void`\>
diff --git a/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscribers.md b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscribers.md
new file mode 100644
index 0000000000..3b63d11bd4
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscribers.md
@@ -0,0 +1,11 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [api](Namespace.api.md) / EventSubscribers
+
+# Type Alias: EventSubscribers
+
+> **EventSubscribers** = `{ [K in CEvt.Tag]?: EventSubscriberFunc }`
+
+Defined in: [src/api.ts:52](../src/api.ts#L52)
diff --git a/packages/simplex-chat-nodejs/docs/api.Variable.defaultBotAddressSettings.md b/packages/simplex-chat-nodejs/docs/api.Variable.defaultBotAddressSettings.md
new file mode 100644
index 0000000000..f076ee0fad
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/api.Variable.defaultBotAddressSettings.md
@@ -0,0 +1,11 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [api](Namespace.api.md) / defaultBotAddressSettings
+
+# Variable: defaultBotAddressSettings
+
+> `const` **defaultBotAddressSettings**: [`BotAddressSettings`](api.Interface.BotAddressSettings.md)
+
+Defined in: [src/api.ts:44](../src/api.ts#L44)
diff --git a/packages/simplex-chat-nodejs/docs/bot.Function.run.md b/packages/simplex-chat-nodejs/docs/bot.Function.run.md
new file mode 100644
index 0000000000..bc31ad01a8
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/bot.Function.run.md
@@ -0,0 +1,21 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [bot](Namespace.bot.md) / run
+
+# Function: run()
+
+> **run**(`__namedParameters`): `Promise`\<\[[`ChatApi`](api.Class.ChatApi.md), `User`, `UserContactLink` \| `undefined`\]\>
+
+Defined in: [src/bot.ts:49](../src/bot.ts#L49)
+
+## Parameters
+
+### \_\_namedParameters
+
+[`BotConfig`](bot.Interface.BotConfig.md)
+
+## Returns
+
+`Promise`\<\[[`ChatApi`](api.Class.ChatApi.md), `User`, `UserContactLink` \| `undefined`\]\>
diff --git a/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md b/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md
new file mode 100644
index 0000000000..0951c5a129
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md
@@ -0,0 +1,75 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [bot](Namespace.bot.md) / BotConfig
+
+# Interface: BotConfig
+
+Defined in: [src/bot.ts:37](../src/bot.ts#L37)
+
+## Properties
+
+### dbOpts
+
+> **dbOpts**: [`BotDbOpts`](bot.Interface.BotDbOpts.md)
+
+Defined in: [src/bot.ts:39](../src/bot.ts#L39)
+
+***
+
+### events?
+
+> `optional` **events**: [`EventSubscribers`](api.TypeAlias.EventSubscribers.md)
+
+Defined in: [src/bot.ts:46](../src/bot.ts#L46)
+
+***
+
+### onCommands?
+
+> `optional` **onCommands**: \{\[`key`: `string`\]: (`chatItem`, `command`) => `void` \| `Promise`\<`void`\> \| `undefined`; \}
+
+Defined in: [src/bot.ts:43](../src/bot.ts#L43)
+
+#### Index Signature
+
+\[`key`: `string`\]: (`chatItem`, `command`) => `void` \| `Promise`\<`void`\> \| `undefined`
+
+***
+
+### onMessage()?
+
+> `optional` **onMessage**: (`chatItem`, `content`) => `void` \| `Promise`\<`void`\>
+
+Defined in: [src/bot.ts:41](../src/bot.ts#L41)
+
+#### Parameters
+
+##### chatItem
+
+`AChatItem`
+
+##### content
+
+`MsgContent`
+
+#### Returns
+
+`void` \| `Promise`\<`void`\>
+
+***
+
+### options
+
+> **options**: [`BotOptions`](bot.Interface.BotOptions.md)
+
+Defined in: [src/bot.ts:40](../src/bot.ts#L40)
+
+***
+
+### profile
+
+> **profile**: `Profile`
+
+Defined in: [src/bot.ts:38](../src/bot.ts#L38)
diff --git a/packages/simplex-chat-nodejs/docs/bot.Interface.BotDbOpts.md b/packages/simplex-chat-nodejs/docs/bot.Interface.BotDbOpts.md
new file mode 100644
index 0000000000..7a9f113f6a
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/bot.Interface.BotDbOpts.md
@@ -0,0 +1,33 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [bot](Namespace.bot.md) / BotDbOpts
+
+# Interface: BotDbOpts
+
+Defined in: [src/bot.ts:7](../src/bot.ts#L7)
+
+## Properties
+
+### confirmMigrations?
+
+> `optional` **confirmMigrations**: [`MigrationConfirmation`](core.Enumeration.MigrationConfirmation.md)
+
+Defined in: [src/bot.ts:10](../src/bot.ts#L10)
+
+***
+
+### dbFilePrefix
+
+> **dbFilePrefix**: `string`
+
+Defined in: [src/bot.ts:8](../src/bot.ts#L8)
+
+***
+
+### dbKey?
+
+> `optional` **dbKey**: `string`
+
+Defined in: [src/bot.ts:9](../src/bot.ts#L9)
diff --git a/packages/simplex-chat-nodejs/docs/bot.Interface.BotOptions.md b/packages/simplex-chat-nodejs/docs/bot.Interface.BotOptions.md
new file mode 100644
index 0000000000..44d4380e5a
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/bot.Interface.BotOptions.md
@@ -0,0 +1,81 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [bot](Namespace.bot.md) / BotOptions
+
+# Interface: BotOptions
+
+Defined in: [src/bot.ts:13](../src/bot.ts#L13)
+
+## Properties
+
+### addressSettings?
+
+> `optional` **addressSettings**: [`BotAddressSettings`](api.Interface.BotAddressSettings.md)
+
+Defined in: [src/bot.ts:17](../src/bot.ts#L17)
+
+***
+
+### allowFiles?
+
+> `optional` **allowFiles**: `boolean`
+
+Defined in: [src/bot.ts:18](../src/bot.ts#L18)
+
+***
+
+### commands?
+
+> `optional` **commands**: `ChatBotCommand`[]
+
+Defined in: [src/bot.ts:19](../src/bot.ts#L19)
+
+***
+
+### createAddress?
+
+> `optional` **createAddress**: `boolean`
+
+Defined in: [src/bot.ts:14](../src/bot.ts#L14)
+
+***
+
+### logContacts?
+
+> `optional` **logContacts**: `boolean`
+
+Defined in: [src/bot.ts:21](../src/bot.ts#L21)
+
+***
+
+### logNetwork?
+
+> `optional` **logNetwork**: `boolean`
+
+Defined in: [src/bot.ts:22](../src/bot.ts#L22)
+
+***
+
+### updateAddress?
+
+> `optional` **updateAddress**: `boolean`
+
+Defined in: [src/bot.ts:15](../src/bot.ts#L15)
+
+***
+
+### updateProfile?
+
+> `optional` **updateProfile**: `boolean`
+
+Defined in: [src/bot.ts:16](../src/bot.ts#L16)
+
+***
+
+### useBotProfile?
+
+> `optional` **useBotProfile**: `boolean`
+
+Defined in: [src/bot.ts:20](../src/bot.ts#L20)
diff --git a/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md b/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md
new file mode 100644
index 0000000000..c6082f2985
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md
@@ -0,0 +1,205 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / ChatAPIError
+
+# Class: ChatAPIError
+
+Defined in: [src/core.ts:92](../src/core.ts#L92)
+
+## Extends
+
+- `Error`
+
+## Constructors
+
+### Constructor
+
+> **new ChatAPIError**(`message`, `chatError`): `ChatAPIError`
+
+Defined in: [src/core.ts:93](../src/core.ts#L93)
+
+#### Parameters
+
+##### message
+
+`string`
+
+##### chatError
+
+`ChatError` | `undefined`
+
+#### Returns
+
+`ChatAPIError`
+
+#### Overrides
+
+`Error.constructor`
+
+## Properties
+
+### chatError
+
+> **chatError**: `ChatError` \| `undefined` = `undefined`
+
+Defined in: [src/core.ts:93](../src/core.ts#L93)
+
+***
+
+### message
+
+> **message**: `string`
+
+Defined in: [src/core.ts:93](../src/core.ts#L93)
+
+#### Inherited from
+
+`Error.message`
+
+***
+
+### name
+
+> **name**: `string`
+
+Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1076](../node_modules/typescript/lib/lib.es5.d.ts#L1076)
+
+#### Inherited from
+
+`Error.name`
+
+***
+
+### stack?
+
+> `optional` **stack**: `string`
+
+Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1078](../node_modules/typescript/lib/lib.es5.d.ts#L1078)
+
+#### Inherited from
+
+`Error.stack`
+
+***
+
+### stackTraceLimit
+
+> `static` **stackTraceLimit**: `number`
+
+Defined in: [node\_modules/@types/node/globals.d.ts:67](../node_modules/@types/node/globals.d.ts#L67)
+
+The `Error.stackTraceLimit` property specifies the number of stack frames
+collected by a stack trace (whether generated by `new Error().stack` or
+`Error.captureStackTrace(obj)`).
+
+The default value is `10` but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured _after_ the value has been changed.
+
+If set to a non-number value, or set to a negative number, stack traces will
+not capture any frames.
+
+#### Inherited from
+
+`Error.stackTraceLimit`
+
+## Methods
+
+### captureStackTrace()
+
+> `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void`
+
+Defined in: [node\_modules/@types/node/globals.d.ts:51](../node_modules/@types/node/globals.d.ts#L51)
+
+Creates a `.stack` property on `targetObject`, which when accessed returns
+a string representing the location in the code at which
+`Error.captureStackTrace()` was called.
+
+```js
+const myObject = {};
+Error.captureStackTrace(myObject);
+myObject.stack; // Similar to `new Error().stack`
+```
+
+The first line of the trace will be prefixed with
+`${myObject.name}: ${myObject.message}`.
+
+The optional `constructorOpt` argument accepts a function. If given, all frames
+above `constructorOpt`, including `constructorOpt`, will be omitted from the
+generated stack trace.
+
+The `constructorOpt` argument is useful for hiding implementation
+details of error generation from the user. For instance:
+
+```js
+function a() {
+ b();
+}
+
+function b() {
+ c();
+}
+
+function c() {
+ // Create an error without stack trace to avoid calculating the stack trace twice.
+ const { stackTraceLimit } = Error;
+ Error.stackTraceLimit = 0;
+ const error = new Error();
+ Error.stackTraceLimit = stackTraceLimit;
+
+ // Capture the stack trace above function b
+ Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
+ throw error;
+}
+
+a();
+```
+
+#### Parameters
+
+##### targetObject
+
+`object`
+
+##### constructorOpt?
+
+`Function`
+
+#### Returns
+
+`void`
+
+#### Inherited from
+
+`Error.captureStackTrace`
+
+***
+
+### prepareStackTrace()
+
+> `static` **prepareStackTrace**(`err`, `stackTraces`): `any`
+
+Defined in: [node\_modules/@types/node/globals.d.ts:55](../node_modules/@types/node/globals.d.ts#L55)
+
+#### Parameters
+
+##### err
+
+`Error`
+
+##### stackTraces
+
+`CallSite`[]
+
+#### Returns
+
+`any`
+
+#### See
+
+https://v8.dev/docs/stack-trace-api#customizing-stack-traces
+
+#### Inherited from
+
+`Error.prepareStackTrace`
diff --git a/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md b/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md
new file mode 100644
index 0000000000..eff5123fb0
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md
@@ -0,0 +1,205 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / ChatInitError
+
+# Class: ChatInitError
+
+Defined in: [src/core.ts:116](../src/core.ts#L116)
+
+## Extends
+
+- `Error`
+
+## Constructors
+
+### Constructor
+
+> **new ChatInitError**(`message`, `dbMigrationError`): `ChatInitError`
+
+Defined in: [src/core.ts:117](../src/core.ts#L117)
+
+#### Parameters
+
+##### message
+
+`string`
+
+##### dbMigrationError
+
+[`DBMigrationError`](core.TypeAlias.DBMigrationError.md)
+
+#### Returns
+
+`ChatInitError`
+
+#### Overrides
+
+`Error.constructor`
+
+## Properties
+
+### dbMigrationError
+
+> **dbMigrationError**: [`DBMigrationError`](core.TypeAlias.DBMigrationError.md)
+
+Defined in: [src/core.ts:117](../src/core.ts#L117)
+
+***
+
+### message
+
+> **message**: `string`
+
+Defined in: [src/core.ts:117](../src/core.ts#L117)
+
+#### Inherited from
+
+`Error.message`
+
+***
+
+### name
+
+> **name**: `string`
+
+Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1076](../node_modules/typescript/lib/lib.es5.d.ts#L1076)
+
+#### Inherited from
+
+`Error.name`
+
+***
+
+### stack?
+
+> `optional` **stack**: `string`
+
+Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1078](../node_modules/typescript/lib/lib.es5.d.ts#L1078)
+
+#### Inherited from
+
+`Error.stack`
+
+***
+
+### stackTraceLimit
+
+> `static` **stackTraceLimit**: `number`
+
+Defined in: [node\_modules/@types/node/globals.d.ts:67](../node_modules/@types/node/globals.d.ts#L67)
+
+The `Error.stackTraceLimit` property specifies the number of stack frames
+collected by a stack trace (whether generated by `new Error().stack` or
+`Error.captureStackTrace(obj)`).
+
+The default value is `10` but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured _after_ the value has been changed.
+
+If set to a non-number value, or set to a negative number, stack traces will
+not capture any frames.
+
+#### Inherited from
+
+`Error.stackTraceLimit`
+
+## Methods
+
+### captureStackTrace()
+
+> `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void`
+
+Defined in: [node\_modules/@types/node/globals.d.ts:51](../node_modules/@types/node/globals.d.ts#L51)
+
+Creates a `.stack` property on `targetObject`, which when accessed returns
+a string representing the location in the code at which
+`Error.captureStackTrace()` was called.
+
+```js
+const myObject = {};
+Error.captureStackTrace(myObject);
+myObject.stack; // Similar to `new Error().stack`
+```
+
+The first line of the trace will be prefixed with
+`${myObject.name}: ${myObject.message}`.
+
+The optional `constructorOpt` argument accepts a function. If given, all frames
+above `constructorOpt`, including `constructorOpt`, will be omitted from the
+generated stack trace.
+
+The `constructorOpt` argument is useful for hiding implementation
+details of error generation from the user. For instance:
+
+```js
+function a() {
+ b();
+}
+
+function b() {
+ c();
+}
+
+function c() {
+ // Create an error without stack trace to avoid calculating the stack trace twice.
+ const { stackTraceLimit } = Error;
+ Error.stackTraceLimit = 0;
+ const error = new Error();
+ Error.stackTraceLimit = stackTraceLimit;
+
+ // Capture the stack trace above function b
+ Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
+ throw error;
+}
+
+a();
+```
+
+#### Parameters
+
+##### targetObject
+
+`object`
+
+##### constructorOpt?
+
+`Function`
+
+#### Returns
+
+`void`
+
+#### Inherited from
+
+`Error.captureStackTrace`
+
+***
+
+### prepareStackTrace()
+
+> `static` **prepareStackTrace**(`err`, `stackTraces`): `any`
+
+Defined in: [node\_modules/@types/node/globals.d.ts:55](../node_modules/@types/node/globals.d.ts#L55)
+
+#### Parameters
+
+##### err
+
+`Error`
+
+##### stackTraces
+
+`CallSite`[]
+
+#### Returns
+
+`any`
+
+#### See
+
+https://v8.dev/docs/stack-trace-api#customizing-stack-traces
+
+#### Inherited from
+
+`Error.prepareStackTrace`
diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md
new file mode 100644
index 0000000000..02cf84b763
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md
@@ -0,0 +1,41 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / ErrorMigration
+
+# Interface: ErrorMigration
+
+Defined in: [src/core.ts:144](../src/core.ts#L144)
+
+## Extends
+
+- `Interface`
+
+## Properties
+
+### dbFile
+
+> **dbFile**: `string`
+
+Defined in: [src/core.ts:146](../src/core.ts#L146)
+
+***
+
+### migrationError
+
+> **migrationError**: [`MigrationError`](core.TypeAlias.MigrationError.md)
+
+Defined in: [src/core.ts:147](../src/core.ts#L147)
+
+***
+
+### type
+
+> **type**: `"errorMigration"`
+
+Defined in: [src/core.ts:145](../src/core.ts#L145)
+
+#### Overrides
+
+`Interface.type`
diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md
new file mode 100644
index 0000000000..18e2429081
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md
@@ -0,0 +1,33 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / ErrorNotADatabase
+
+# Interface: ErrorNotADatabase
+
+Defined in: [src/core.ts:139](../src/core.ts#L139)
+
+## Extends
+
+- `Interface`
+
+## Properties
+
+### dbFile
+
+> **dbFile**: `string`
+
+Defined in: [src/core.ts:141](../src/core.ts#L141)
+
+***
+
+### type
+
+> **type**: `"errorNotADatabase"`
+
+Defined in: [src/core.ts:140](../src/core.ts#L140)
+
+#### Overrides
+
+`Interface.type`
diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md
new file mode 100644
index 0000000000..4d85b04197
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md
@@ -0,0 +1,41 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / ErrorSQL
+
+# Interface: ErrorSQL
+
+Defined in: [src/core.ts:150](../src/core.ts#L150)
+
+## Extends
+
+- `Interface`
+
+## Properties
+
+### dbFile
+
+> **dbFile**: `string`
+
+Defined in: [src/core.ts:152](../src/core.ts#L152)
+
+***
+
+### migrationSQLError
+
+> **migrationSQLError**: `string`
+
+Defined in: [src/core.ts:153](../src/core.ts#L153)
+
+***
+
+### type
+
+> **type**: `"errorSQL"`
+
+Defined in: [src/core.ts:151](../src/core.ts#L151)
+
+#### Overrides
+
+`Interface.type`
diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md
new file mode 100644
index 0000000000..34dea63aed
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md
@@ -0,0 +1,25 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / InvalidConfirmation
+
+# Interface: InvalidConfirmation
+
+Defined in: [src/core.ts:135](../src/core.ts#L135)
+
+## Extends
+
+- `Interface`
+
+## Properties
+
+### type
+
+> **type**: `"invalidConfirmation"`
+
+Defined in: [src/core.ts:136](../src/core.ts#L136)
+
+#### Overrides
+
+`Interface.type`
diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md
new file mode 100644
index 0000000000..a3ef341601
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md
@@ -0,0 +1,11 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / Tag
+
+# Type Alias: Tag
+
+> **Tag** = `"invalidConfirmation"` \| `"errorNotADatabase"` \| `"errorMigration"` \| `"errorSQL"`
+
+Defined in: [src/core.ts:129](../src/core.ts#L129)
diff --git a/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md b/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md
new file mode 100644
index 0000000000..7dfd4991bf
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md
@@ -0,0 +1,43 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / MigrationConfirmation
+
+# Enumeration: MigrationConfirmation
+
+Defined in: [src/core.ts:101](../src/core.ts#L101)
+
+Migration confirmation mode
+
+## Enumeration Members
+
+### Console
+
+> **Console**: `"console"`
+
+Defined in: [src/core.ts:104](../src/core.ts#L104)
+
+***
+
+### Error
+
+> **Error**: `"error"`
+
+Defined in: [src/core.ts:105](../src/core.ts#L105)
+
+***
+
+### YesUp
+
+> **YesUp**: `"yesUp"`
+
+Defined in: [src/core.ts:102](../src/core.ts#L102)
+
+***
+
+### YesUpDown
+
+> **YesUpDown**: `"yesUpDown"`
+
+Defined in: [src/core.ts:103](../src/core.ts#L103)
diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md b/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md
new file mode 100644
index 0000000000..deeb3213fd
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md
@@ -0,0 +1,23 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / chatCloseStore
+
+# Function: chatCloseStore()
+
+> **chatCloseStore**(`ctrl`): `Promise`\<`void`\>
+
+Defined in: [src/core.ts:17](../src/core.ts#L17)
+
+Close chat store
+
+## Parameters
+
+### ctrl
+
+`bigint`
+
+## Returns
+
+`Promise`\<`void`\>
diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md
new file mode 100644
index 0000000000..434aeeaae8
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md
@@ -0,0 +1,31 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / chatDecryptFile
+
+# Function: chatDecryptFile()
+
+> **chatDecryptFile**(`fromPath`, `__namedParameters`, `toPath`): `Promise`\<`void`\>
+
+Defined in: [src/core.ts:73](../src/core.ts#L73)
+
+Decrypt file
+
+## Parameters
+
+### fromPath
+
+`string`
+
+### \_\_namedParameters
+
+[`CryptoArgs`](core.Interface.CryptoArgs.md)
+
+### toPath
+
+`string`
+
+## Returns
+
+`Promise`\<`void`\>
diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md
new file mode 100644
index 0000000000..6aa0ad2923
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md
@@ -0,0 +1,31 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / chatEncryptFile
+
+# Function: chatEncryptFile()
+
+> **chatEncryptFile**(`ctrl`, `fromPath`, `toPath`): `Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\>
+
+Defined in: [src/core.ts:65](../src/core.ts#L65)
+
+Encrypt file
+
+## Parameters
+
+### ctrl
+
+`bigint`
+
+### fromPath
+
+`string`
+
+### toPath
+
+`string`
+
+## Returns
+
+`Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\>
diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md b/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md
new file mode 100644
index 0000000000..9116026f56
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md
@@ -0,0 +1,31 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / chatMigrateInit
+
+# Function: chatMigrateInit()
+
+> **chatMigrateInit**(`dbPath`, `dbKey`, `confirm`): `Promise`\<`bigint`\>
+
+Defined in: [src/core.ts:7](../src/core.ts#L7)
+
+Initialize chat controller
+
+## Parameters
+
+### dbPath
+
+`string`
+
+### dbKey
+
+`string`
+
+### confirm
+
+[`MigrationConfirmation`](core.Enumeration.MigrationConfirmation.md)
+
+## Returns
+
+`Promise`\<`bigint`\>
diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md
new file mode 100644
index 0000000000..27de43e63c
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md
@@ -0,0 +1,27 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / chatReadFile
+
+# Function: chatReadFile()
+
+> **chatReadFile**(`path`, `__namedParameters`): `Promise`\<`ArrayBuffer`\>
+
+Defined in: [src/core.ts:58](../src/core.ts#L58)
+
+Read buffer from encrypted file
+
+## Parameters
+
+### path
+
+`string`
+
+### \_\_namedParameters
+
+[`CryptoArgs`](core.Interface.CryptoArgs.md)
+
+## Returns
+
+`Promise`\<`ArrayBuffer`\>
diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md b/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md
new file mode 100644
index 0000000000..9bf44d6523
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md
@@ -0,0 +1,27 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / chatRecvMsgWait
+
+# Function: chatRecvMsgWait()
+
+> **chatRecvMsgWait**(`ctrl`, `wait`): `Promise`\<`ChatEvent` \| `undefined`\>
+
+Defined in: [src/core.ts:37](../src/core.ts#L37)
+
+Receive chat event
+
+## Parameters
+
+### ctrl
+
+`bigint`
+
+### wait
+
+`number`
+
+## Returns
+
+`Promise`\<`ChatEvent` \| `undefined`\>
diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md b/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md
new file mode 100644
index 0000000000..2dfcba45b4
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md
@@ -0,0 +1,27 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / chatSendCmd
+
+# Function: chatSendCmd()
+
+> **chatSendCmd**(`ctrl`, `cmd`): `Promise`\<`ChatResponse`\>
+
+Defined in: [src/core.ts:25](../src/core.ts#L25)
+
+Send chat command as string
+
+## Parameters
+
+### ctrl
+
+`bigint`
+
+### cmd
+
+`string`
+
+## Returns
+
+`Promise`\<`ChatResponse`\>
diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md
new file mode 100644
index 0000000000..3b1d770fbb
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md
@@ -0,0 +1,31 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / chatWriteFile
+
+# Function: chatWriteFile()
+
+> **chatWriteFile**(`ctrl`, `path`, `buffer`): `Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\>
+
+Defined in: [src/core.ts:50](../src/core.ts#L50)
+
+Write buffer to encrypted file
+
+## Parameters
+
+### ctrl
+
+`bigint`
+
+### path
+
+`string`
+
+### buffer
+
+`ArrayBuffer`
+
+## Returns
+
+`Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\>
diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md b/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md
new file mode 100644
index 0000000000..906ef3ec3e
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md
@@ -0,0 +1,31 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / APIResult
+
+# Interface: APIResult\
+
+Defined in: [src/core.ts:87](../src/core.ts#L87)
+
+## Type Parameters
+
+### R
+
+`R`
+
+## Properties
+
+### error?
+
+> `optional` **error**: `ChatError`
+
+Defined in: [src/core.ts:89](../src/core.ts#L89)
+
+***
+
+### result?
+
+> `optional` **result**: `R`
+
+Defined in: [src/core.ts:88](../src/core.ts#L88)
diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md b/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md
new file mode 100644
index 0000000000..eddcb0bc5a
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md
@@ -0,0 +1,27 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / CryptoArgs
+
+# Interface: CryptoArgs
+
+Defined in: [src/core.ts:111](../src/core.ts#L111)
+
+File encryption key and nonce
+
+## Properties
+
+### fileKey
+
+> **fileKey**: `string`
+
+Defined in: [src/core.ts:112](../src/core.ts#L112)
+
+***
+
+### fileNonce
+
+> **fileNonce**: `string`
+
+Defined in: [src/core.ts:113](../src/core.ts#L113)
diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md b/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md
new file mode 100644
index 0000000000..32f6c267aa
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md
@@ -0,0 +1,25 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / UpMigration
+
+# Interface: UpMigration
+
+Defined in: [src/core.ts:185](../src/core.ts#L185)
+
+## Properties
+
+### upName
+
+> **upName**: `string`
+
+Defined in: [src/core.ts:186](../src/core.ts#L186)
+
+***
+
+### withDown
+
+> **withDown**: `boolean`
+
+Defined in: [src/core.ts:187](../src/core.ts#L187)
diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md
new file mode 100644
index 0000000000..8dab81a3a3
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md
@@ -0,0 +1,33 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / [MTRError](core.Namespace.MTRError.md) / MTREDifferent
+
+# Interface: MTREDifferent
+
+Defined in: [src/core.ts:206](../src/core.ts#L206)
+
+## Extends
+
+- `Interface`
+
+## Properties
+
+### downMigrations
+
+> **downMigrations**: `string`[]
+
+Defined in: [src/core.ts:208](../src/core.ts#L208)
+
+***
+
+### type
+
+> **type**: `"different"`
+
+Defined in: [src/core.ts:207](../src/core.ts#L207)
+
+#### Overrides
+
+`Interface.type`
diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md
new file mode 100644
index 0000000000..1de634e40e
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md
@@ -0,0 +1,33 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / [MTRError](core.Namespace.MTRError.md) / MTRENoDown
+
+# Interface: MTRENoDown
+
+Defined in: [src/core.ts:201](../src/core.ts#L201)
+
+## Extends
+
+- `Interface`
+
+## Properties
+
+### type
+
+> **type**: `"noDown"`
+
+Defined in: [src/core.ts:202](../src/core.ts#L202)
+
+#### Overrides
+
+`Interface.type`
+
+***
+
+### upMigrations
+
+> **upMigrations**: [`UpMigration`](core.Interface.UpMigration.md)
+
+Defined in: [src/core.ts:203](../src/core.ts#L203)
diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md
new file mode 100644
index 0000000000..768baa0068
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md
@@ -0,0 +1,11 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / [MTRError](core.Namespace.MTRError.md) / Tag
+
+# Type Alias: Tag
+
+> **Tag** = `"noDown"` \| `"different"`
+
+Defined in: [src/core.ts:195](../src/core.ts#L195)
diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md
new file mode 100644
index 0000000000..a2742cbff2
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md
@@ -0,0 +1,33 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / [MigrationError](core.Namespace.MigrationError.md) / MEDowngrade
+
+# Interface: MEDowngrade
+
+Defined in: [src/core.ts:174](../src/core.ts#L174)
+
+## Extends
+
+- `Interface`
+
+## Properties
+
+### downMigrations
+
+> **downMigrations**: `string`[]
+
+Defined in: [src/core.ts:176](../src/core.ts#L176)
+
+***
+
+### type
+
+> **type**: `"downgrade"`
+
+Defined in: [src/core.ts:175](../src/core.ts#L175)
+
+#### Overrides
+
+`Interface.type`
diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md
new file mode 100644
index 0000000000..08fe7d56e3
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md
@@ -0,0 +1,33 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / [MigrationError](core.Namespace.MigrationError.md) / MEUpgrade
+
+# Interface: MEUpgrade
+
+Defined in: [src/core.ts:169](../src/core.ts#L169)
+
+## Extends
+
+- `Interface`
+
+## Properties
+
+### type
+
+> **type**: `"upgrade"`
+
+Defined in: [src/core.ts:170](../src/core.ts#L170)
+
+#### Overrides
+
+`Interface.type`
+
+***
+
+### upMigrations
+
+> **upMigrations**: [`UpMigration`](core.Interface.UpMigration.md)
+
+Defined in: [src/core.ts:171](../src/core.ts#L171)
diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md
new file mode 100644
index 0000000000..cd811a5747
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md
@@ -0,0 +1,33 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / [MigrationError](core.Namespace.MigrationError.md) / MigrationError
+
+# Interface: MigrationError
+
+Defined in: [src/core.ts:179](../src/core.ts#L179)
+
+## Extends
+
+- `Interface`
+
+## Properties
+
+### mtrError
+
+> **mtrError**: [`MTRError`](core.TypeAlias.MTRError.md)
+
+Defined in: [src/core.ts:181](../src/core.ts#L181)
+
+***
+
+### type
+
+> **type**: `"migrationError"`
+
+Defined in: [src/core.ts:180](../src/core.ts#L180)
+
+#### Overrides
+
+`Interface.type`
diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md
new file mode 100644
index 0000000000..5ef6e70b08
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md
@@ -0,0 +1,11 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / [MigrationError](core.Namespace.MigrationError.md) / Tag
+
+# Type Alias: Tag
+
+> **Tag** = `"upgrade"` \| `"downgrade"` \| `"migrationError"`
+
+Defined in: [src/core.ts:163](../src/core.ts#L163)
diff --git a/packages/simplex-chat-nodejs/docs/core.Namespace.DBMigrationError.md b/packages/simplex-chat-nodejs/docs/core.Namespace.DBMigrationError.md
new file mode 100644
index 0000000000..95ddfa5b24
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Namespace.DBMigrationError.md
@@ -0,0 +1,18 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / DBMigrationError
+
+# DBMigrationError
+
+## Interfaces
+
+- [ErrorMigration](core.DBMigrationError.Interface.ErrorMigration.md)
+- [ErrorNotADatabase](core.DBMigrationError.Interface.ErrorNotADatabase.md)
+- [ErrorSQL](core.DBMigrationError.Interface.ErrorSQL.md)
+- [InvalidConfirmation](core.DBMigrationError.Interface.InvalidConfirmation.md)
+
+## Type Aliases
+
+- [Tag](core.DBMigrationError.TypeAlias.Tag.md)
diff --git a/packages/simplex-chat-nodejs/docs/core.Namespace.MTRError.md b/packages/simplex-chat-nodejs/docs/core.Namespace.MTRError.md
new file mode 100644
index 0000000000..70baca917a
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Namespace.MTRError.md
@@ -0,0 +1,16 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / MTRError
+
+# MTRError
+
+## Interfaces
+
+- [MTREDifferent](core.MTRError.Interface.MTREDifferent.md)
+- [MTRENoDown](core.MTRError.Interface.MTRENoDown.md)
+
+## Type Aliases
+
+- [Tag](core.MTRError.TypeAlias.Tag.md)
diff --git a/packages/simplex-chat-nodejs/docs/core.Namespace.MigrationError.md b/packages/simplex-chat-nodejs/docs/core.Namespace.MigrationError.md
new file mode 100644
index 0000000000..cc66142dbc
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.Namespace.MigrationError.md
@@ -0,0 +1,17 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / MigrationError
+
+# MigrationError
+
+## Interfaces
+
+- [MEDowngrade](core.MigrationError.Interface.MEDowngrade.md)
+- [MEUpgrade](core.MigrationError.Interface.MEUpgrade.md)
+- [MigrationError](core.MigrationError.Interface.MigrationError.md)
+
+## Type Aliases
+
+- [Tag](core.MigrationError.TypeAlias.Tag.md)
diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md
new file mode 100644
index 0000000000..6473b3ef60
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md
@@ -0,0 +1,11 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / DBMigrationError
+
+# Type Alias: DBMigrationError
+
+> **DBMigrationError** = [`InvalidConfirmation`](core.DBMigrationError.Interface.InvalidConfirmation.md) \| [`ErrorNotADatabase`](core.DBMigrationError.Interface.ErrorNotADatabase.md) \| [`ErrorMigration`](core.DBMigrationError.Interface.ErrorMigration.md) \| [`ErrorSQL`](core.DBMigrationError.Interface.ErrorSQL.md)
+
+Defined in: [src/core.ts:122](../src/core.ts#L122)
diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md
new file mode 100644
index 0000000000..11aa5b7c24
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md
@@ -0,0 +1,11 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / MTRError
+
+# Type Alias: MTRError
+
+> **MTRError** = [`MTRENoDown`](core.MTRError.Interface.MTRENoDown.md) \| [`MTREDifferent`](core.MTRError.Interface.MTREDifferent.md)
+
+Defined in: [src/core.ts:190](../src/core.ts#L190)
diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md
new file mode 100644
index 0000000000..c15b679769
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md
@@ -0,0 +1,11 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [core](Namespace.core.md) / MigrationError
+
+# Type Alias: MigrationError
+
+> **MigrationError** = [`MEUpgrade`](core.MigrationError.Interface.MEUpgrade.md) \| [`MEDowngrade`](core.MigrationError.Interface.MEDowngrade.md) \| [`MigrationError`](core.MigrationError.Interface.MigrationError.md)
+
+Defined in: [src/core.ts:157](../src/core.ts#L157)
diff --git a/packages/simplex-chat-nodejs/docs/util.Function.botAddressSettings.md b/packages/simplex-chat-nodejs/docs/util.Function.botAddressSettings.md
new file mode 100644
index 0000000000..7fa5d81b7a
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/util.Function.botAddressSettings.md
@@ -0,0 +1,21 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [util](Namespace.util.md) / botAddressSettings
+
+# Function: botAddressSettings()
+
+> **botAddressSettings**(`__namedParameters`): [`BotAddressSettings`](api.Interface.BotAddressSettings.md)
+
+Defined in: [src/util.ts:48](../src/util.ts#L48)
+
+## Parameters
+
+### \_\_namedParameters
+
+`UserContactLink`
+
+## Returns
+
+[`BotAddressSettings`](api.Interface.BotAddressSettings.md)
diff --git a/packages/simplex-chat-nodejs/docs/util.Function.chatInfoName.md b/packages/simplex-chat-nodejs/docs/util.Function.chatInfoName.md
new file mode 100644
index 0000000000..dd68403c40
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/util.Function.chatInfoName.md
@@ -0,0 +1,21 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [util](Namespace.util.md) / chatInfoName
+
+# Function: chatInfoName()
+
+> **chatInfoName**(`cInfo`): `string`
+
+Defined in: [src/util.ts:18](../src/util.ts#L18)
+
+## Parameters
+
+### cInfo
+
+`ChatInfo`
+
+## Returns
+
+`string`
diff --git a/packages/simplex-chat-nodejs/docs/util.Function.chatInfoRef.md b/packages/simplex-chat-nodejs/docs/util.Function.chatInfoRef.md
new file mode 100644
index 0000000000..dccb2a9d29
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/util.Function.chatInfoRef.md
@@ -0,0 +1,21 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [util](Namespace.util.md) / chatInfoRef
+
+# Function: chatInfoRef()
+
+> **chatInfoRef**(`cInfo`): `ChatRef` \| `undefined`
+
+Defined in: [src/util.ts:4](../src/util.ts#L4)
+
+## Parameters
+
+### cInfo
+
+`ChatInfo`
+
+## Returns
+
+`ChatRef` \| `undefined`
diff --git a/packages/simplex-chat-nodejs/docs/util.Function.ciBotCommand.md b/packages/simplex-chat-nodejs/docs/util.Function.ciBotCommand.md
new file mode 100644
index 0000000000..bc7a66f163
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/util.Function.ciBotCommand.md
@@ -0,0 +1,21 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [util](Namespace.util.md) / ciBotCommand
+
+# Function: ciBotCommand()
+
+> **ciBotCommand**(`chatItem`): [`BotCommand`](util.Interface.BotCommand.md) \| `undefined`
+
+Defined in: [src/util.ts:78](../src/util.ts#L78)
+
+## Parameters
+
+### chatItem
+
+`ChatItem`
+
+## Returns
+
+[`BotCommand`](util.Interface.BotCommand.md) \| `undefined`
diff --git a/packages/simplex-chat-nodejs/docs/util.Function.ciContentText.md b/packages/simplex-chat-nodejs/docs/util.Function.ciContentText.md
new file mode 100644
index 0000000000..7ab0bc540c
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/util.Function.ciContentText.md
@@ -0,0 +1,21 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [util](Namespace.util.md) / ciContentText
+
+# Function: ciContentText()
+
+> **ciContentText**(`__namedParameters`): `string` \| `undefined`
+
+Defined in: [src/util.ts:64](../src/util.ts#L64)
+
+## Parameters
+
+### \_\_namedParameters
+
+`ChatItem`
+
+## Returns
+
+`string` \| `undefined`
diff --git a/packages/simplex-chat-nodejs/docs/util.Function.contactAddressStr.md b/packages/simplex-chat-nodejs/docs/util.Function.contactAddressStr.md
new file mode 100644
index 0000000000..3f6c7b9562
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/util.Function.contactAddressStr.md
@@ -0,0 +1,21 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [util](Namespace.util.md) / contactAddressStr
+
+# Function: contactAddressStr()
+
+> **contactAddressStr**(`link`): `string`
+
+Defined in: [src/util.ts:44](../src/util.ts#L44)
+
+## Parameters
+
+### link
+
+`CreatedConnLink`
+
+## Returns
+
+`string`
diff --git a/packages/simplex-chat-nodejs/docs/util.Function.fromLocalProfile.md b/packages/simplex-chat-nodejs/docs/util.Function.fromLocalProfile.md
new file mode 100644
index 0000000000..c32081b6cf
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/util.Function.fromLocalProfile.md
@@ -0,0 +1,21 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [util](Namespace.util.md) / fromLocalProfile
+
+# Function: fromLocalProfile()
+
+> **fromLocalProfile**(`__namedParameters`): `Profile`
+
+Defined in: [src/util.ts:56](../src/util.ts#L56)
+
+## Parameters
+
+### \_\_namedParameters
+
+`LocalProfile`
+
+## Returns
+
+`Profile`
diff --git a/packages/simplex-chat-nodejs/docs/util.Function.reactionText.md b/packages/simplex-chat-nodejs/docs/util.Function.reactionText.md
new file mode 100644
index 0000000000..896dc74a12
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/util.Function.reactionText.md
@@ -0,0 +1,21 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [util](Namespace.util.md) / reactionText
+
+# Function: reactionText()
+
+> **reactionText**(`reaction`): `string`
+
+Defined in: [src/util.ts:89](../src/util.ts#L89)
+
+## Parameters
+
+### reaction
+
+`ACIReaction`
+
+## Returns
+
+`string`
diff --git a/packages/simplex-chat-nodejs/docs/util.Function.senderName.md b/packages/simplex-chat-nodejs/docs/util.Function.senderName.md
new file mode 100644
index 0000000000..6bacad97f6
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/util.Function.senderName.md
@@ -0,0 +1,25 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [util](Namespace.util.md) / senderName
+
+# Function: senderName()
+
+> **senderName**(`cInfo`, `chatDir`): `string`
+
+Defined in: [src/util.ts:37](../src/util.ts#L37)
+
+## Parameters
+
+### cInfo
+
+`ChatInfo`
+
+### chatDir
+
+`CIDirection`
+
+## Returns
+
+`string`
diff --git a/packages/simplex-chat-nodejs/docs/util.Interface.BotCommand.md b/packages/simplex-chat-nodejs/docs/util.Interface.BotCommand.md
new file mode 100644
index 0000000000..21c3ad72ba
--- /dev/null
+++ b/packages/simplex-chat-nodejs/docs/util.Interface.BotCommand.md
@@ -0,0 +1,25 @@
+[**simplex-chat**](README.md)
+
+***
+
+[simplex-chat](README.md) / [util](Namespace.util.md) / BotCommand
+
+# Interface: BotCommand
+
+Defined in: [src/util.ts:72](../src/util.ts#L72)
+
+## Properties
+
+### keyword
+
+> **keyword**: `string`
+
+Defined in: [src/util.ts:73](../src/util.ts#L73)
+
+***
+
+### params
+
+> **params**: `string`
+
+Defined in: [src/util.ts:74](../src/util.ts#L74)
diff --git a/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js b/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js
new file mode 100644
index 0000000000..f880808bcf
--- /dev/null
+++ b/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js
@@ -0,0 +1,17 @@
+(async () => {
+ const {bot} = await import("../dist/index.js")
+ const [chat, _user, _address] = await bot.run({
+ profile: {displayName: "Squaring bot example", fullName: ""},
+ dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""},
+ options: {
+ addressSettings: {welcomeMessage: "If you send me a number, I will calculate its square."},
+ },
+ onMessage: async (ci, content) => {
+ const n = +content.text
+ const reply = typeof n === "number" && !isNaN(n)
+ ? `${n} * ${n} = ${n * n}`
+ : `this is not a number`
+ await chat.apiSendTextReply(ci, reply)
+ }
+ })
+})()
diff --git a/packages/simplex-chat-nodejs/examples/squaring-bot.ts b/packages/simplex-chat-nodejs/examples/squaring-bot.ts
new file mode 100644
index 0000000000..682e7b887a
--- /dev/null
+++ b/packages/simplex-chat-nodejs/examples/squaring-bot.ts
@@ -0,0 +1,42 @@
+import {T} from "@simplex-chat/types"
+import {bot, util} from "../dist"
+
+(async () => {
+ const welcomeMessage = "Hello! I am a simple squaring bot.\n\nIf you send me a number, I will calculate its square."
+ const [chat, _user, _address] = await bot.run({
+ profile: {displayName: "Squaring bot example", fullName: ""},
+ dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""},
+ options: {
+ addressSettings: {autoAccept: true, welcomeMessage, businessAddress: false},
+ commands: [ // commands to show in client UI
+ {type: "command", keyword: "help", label: "Send welcome message"},
+ {type: "command", keyword: "info", label: "More information (not implemented)"}
+ ],
+ logContacts: true,
+ logNetwork: false
+ },
+ onMessage: async (ci, content) => {
+ const n = +content.text
+ const reply = typeof n === "number" && !isNaN(n)
+ ? `${n} * ${n} = ${n * n}`
+ : `this is not a number`
+ await chat.apiSendTextReply(ci, reply)
+ },
+ onCommands: { // command handlers can be different from commands to be shown in client UI
+ "help": async (ci: T.AChatItem, _cmd: util.BotCommand) => {
+ await chat.apiSendTextMessage(ci.chatInfo, welcomeMessage)
+ },
+ // fallback handler that will be called for all other commands
+ "": async (ci: T.AChatItem, _cmd: util.BotCommand) => {
+ await chat.apiSendTextReply(ci, "This command is not supported")
+ }
+ },
+ // If you use `onMessage` and subscribe to "newChatItems" event, exclude content messages from processing
+ // If you use `onCommands` and subscribe to "newChatItems" event, exclude commands from processing
+ events: {
+ "chatItemReaction": ({added, reaction}) => {
+ console.log(`${util.senderName(reaction.chatInfo, reaction.chatReaction.chatDir)} ${added ? "added" : "removed"} reaction ${util.reactionText(reaction)}`)
+ }
+ },
+ })
+})()
diff --git a/packages/simplex-chat-nodejs/jest.config.js b/packages/simplex-chat-nodejs/jest.config.js
new file mode 100644
index 0000000000..18c5ddf0e5
--- /dev/null
+++ b/packages/simplex-chat-nodejs/jest.config.js
@@ -0,0 +1,10 @@
+module.exports = {
+ preset: "ts-jest",
+ maxWorkers: 1,
+ testEnvironment: "node",
+ transform: {
+ '^.+\\.ts$': ['ts-jest', {
+ tsconfig: 'tests/tsconfig.json'
+ }]
+ }
+}
diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json
new file mode 100644
index 0000000000..1ef1429e65
--- /dev/null
+++ b/packages/simplex-chat-nodejs/package.json
@@ -0,0 +1,58 @@
+{
+ "name": "simplex-chat",
+ "version": "6.5.0-beta.4.2",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "files": [
+ "src",
+ "cpp",
+ "dist",
+ "binding.gyp",
+ "tsconfig.json",
+ "docs",
+ "examples"
+ ],
+ "scripts": {
+ "preinstall": "node src/download-libs.js",
+ "install": "node-gyp configure; node-gyp rebuild --release",
+ "install-tools": "npm install -g node-gyp",
+ "configure": "node-gyp configure; mkdir libs 2> /dev/null | true",
+ "build": "node-gyp rebuild && tsc && cp ./src/simplex.* ./dist",
+ "run": "node src/index.js",
+ "build-run": "node-gyp build && node src/index.js",
+ "test": "jest",
+ "docs": "typedoc"
+ },
+ "dependencies": {
+ "@simplex-chat/types": "^0.3.0",
+ "extract-zip": "^2.0.1",
+ "fast-deep-equal": "^3.1.3",
+ "node-addon-api": "^8.5.0"
+ },
+ "devDependencies": {
+ "@types/jest": "^30.0.0",
+ "@types/node": "^25.0.5",
+ "jest": "^30.2.0",
+ "ts-jest": "^29.4.6",
+ "typedoc": "^0.28.15",
+ "typedoc-plugin-markdown": "^4.9.0",
+ "typescript": "^5.9.3"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/simplex-chat/simplex-chat.git"
+ },
+ "keywords": [
+ "messenger",
+ "chat",
+ "privacy",
+ "security"
+ ],
+ "author": "SimpleX Chat",
+ "license": "AGPL-3.0",
+ "bugs": {
+ "url": "https://github.com/simplex-chat/simplex-chat/issues"
+ },
+ "homepage": "https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-nodejs#readme",
+ "description": "SimpleX Chat Node.js library for chat bots"
+}
diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts
new file mode 100644
index 0000000000..dc87055f87
--- /dev/null
+++ b/packages/simplex-chat-nodejs/src/api.ts
@@ -0,0 +1,834 @@
+import {CC, CEvt, ChatEvent, ChatResponse, T} from "@simplex-chat/types"
+import * as core from "./core"
+import * as util from "./util"
+
+export class ChatCommandError extends Error {
+ constructor(public message: string, public response: ChatResponse) {
+ super(message)
+ }
+}
+
+/**
+ * Connection request types.
+ * @enum {string}
+ */
+export enum ConnReqType {
+ Invitation = "invitation",
+ Contact = "contact",
+}
+
+/**
+ * Bot address settings.
+ */
+export interface BotAddressSettings {
+ /**
+ * Automatically accept contact requests.
+ * @default true
+ */
+ autoAccept?: boolean
+
+ /**
+ * Optional welcome message to show before connection to the users.
+ * @default undefined (no welcome message)
+ */
+ welcomeMessage?: T.MsgContent | string | undefined
+
+ /**
+ * Business contact address.
+ * For all requests business chats will be created where other participants can be added.
+ * @default false
+ */
+ businessAddress?: boolean
+}
+
+export const defaultBotAddressSettings: BotAddressSettings = {
+ autoAccept: true,
+ welcomeMessage: undefined,
+ businessAddress: false
+}
+
+export type EventSubscriberFunc = (event: ChatEvent & {type: K}) => void | Promise
+
+export type EventSubscribers = {[K in CEvt.Tag]?: EventSubscriberFunc}
+
+interface EventSubscriber {
+ subscriber: EventSubscriberFunc
+ once: boolean
+}
+
+/**
+ * Main API class for interacting with the chat core library.
+ */
+export class ChatApi {
+ private receiveEvents = false
+ private eventsLoop: Promise | undefined = undefined
+ private subscribers: {[K in CEvt.Tag]?: EventSubscriber[]} = {}
+ private receivers: EventSubscriberFunc[] = []
+
+ private constructor(protected ctrl_: bigint | undefined) {}
+
+ /**
+ * Initializes the ChatApi.
+ * @param {string} dbFilePrefix - File prefix for the database files.
+ * @param {string} [dbKey=""] - Database encryption key.
+ * @param {core.MigrationConfirmation} [confirm=core.MigrationConfirmation.YesUp] - Migration confirmation mode.
+ */
+ static async init(
+ dbFilePrefix: string,
+ dbKey: string = "",
+ confirm = core.MigrationConfirmation.YesUp
+ ): Promise {
+ const ctrl = await core.chatMigrateInit(dbFilePrefix, dbKey, confirm)
+ return new ChatApi(ctrl)
+ }
+
+ /**
+ * Start chat controller. Must be called with the existing user profile.
+ */
+ async startChat(): Promise {
+ this.receiveEvents = true
+ this.eventsLoop = this.runEventsLoop()
+ const r = await this.sendChatCmd(CC.StartChat.cmdString({mainApp: true, enableSndFiles: true}))
+ if (r.type !== "chatStarted" && r.type !== "chatRunning") {
+ throw new ChatCommandError("error starting chat", r)
+ }
+ }
+
+ /**
+ * Stop chat controller.
+ * Must be called before closing the database.
+ * Usually doesn't need to be called in chat bots.
+ */
+ async stopChat(): Promise {
+ const r = await this.sendChatCmd("/_stop")
+ if (r.type !== "chatStopped") throw new ChatCommandError("error starting chat", r)
+ this.receiveEvents = false
+ if (this.eventsLoop) await this.eventsLoop
+ this.eventsLoop = undefined
+ }
+
+ /**
+ * Close chat database.
+ * Usually doesn't need to be called in chat bots.
+ */
+ async close(): Promise {
+ this.receiveEvents = false
+ if (this.eventsLoop) await this.eventsLoop
+ this.eventsLoop = undefined
+ await core.chatCloseStore(this.ctrl)
+ this.ctrl_ = undefined
+ }
+
+ private async runEventsLoop(): Promise {
+ while (this.receiveEvents) {
+ try {
+ const event = await this.recvChatEvent()
+ if (!event) continue
+ const subs = this.subscribers[event.type]
+ if (subs) {
+ for (const {subscriber, once} of [...subs]) {
+ try {
+ const p = (subscriber as EventSubscriberFunc)(event)
+ if (p instanceof Promise) await p
+ } catch(e) {
+ console.log(`${event.type} event processing error`, e)
+ }
+ if (once) this.off(event.type, subscriber as EventSubscriberFunc)
+ }
+ }
+ for (const r of [...this.receivers]) {
+ try {
+ const p = r(event)
+ if (p instanceof Promise) await p
+ } catch(e) {
+ console.log(`${event.type} event processing error`, e)
+ }
+ }
+ } catch(err) {
+ const e = err as core.ChatAPIError
+ if ("chatError" in e) {
+ console.log("Chat error", e.chatError)
+ } else {
+ console.log("Invalid event", e)
+ }
+ }
+ }
+ }
+
+ /**
+ * Subscribe multiple event handlers at once.
+ * @param subscribers - An object mapping event types (CEvt.Tag) to their subscriber functions.
+ * @throws {Error} If the same function is subscribed to event.
+ */
+ on(subscribers: EventSubscribers): void
+
+ /**
+ * Subscribe a handler to a specific event.
+ * @param {CEvt.Tag} event - The event type to subscribe to.
+ * @param subscriber - The subscriber function for the event.
+ * @throws {Error} If the same function is subscribed to event.
+ */
+ on(event: K, subscriber: EventSubscriberFunc): void
+ on(events: K | EventSubscribers, subscriber?: EventSubscriberFunc): void {
+ if (typeof events === "string" && subscriber) {
+ this.on_(events, subscriber)
+ } else {
+ const eventEntries = Object.entries(events) as [CEvt.Tag, EventSubscriberFunc | undefined][]
+ for (const [event, subscriber] of eventEntries) {
+ if (subscriber) this.on_(event, subscriber)
+ }
+ }
+ }
+
+ private on_(event: K, subscriber: EventSubscriberFunc, once: boolean = false): void {
+ const subs: EventSubscriber[] = this.subscribers[event] || (this.subscribers[event] = [])
+ if (subs.some(s => s.subscriber === subscriber)) throw Error(`this function is already subscribed to ${event}`)
+ subs.push({subscriber, once})
+ }
+
+ /**
+ * Subscribe a handler to any event.
+ * @param receiver - The receiver function for any event.
+ * @throws {Error} If the same function is subscribed to event.
+ */
+ onAny(receiver: EventSubscriberFunc): void {
+ if (this.receivers.some(s => s === receiver)) throw Error("this function is already subscribed")
+ this.receivers.push(receiver)
+ }
+
+ /**
+ * Subscribe a handler to a specific event to be delivered one time.
+ * @param {CEvt.Tag} event - The event type to subscribe to.
+ * @param subscriber - The subscriber function for the event.
+ * @throws {Error} If the same function is subscribed to event.
+ */
+ once(event: K, subscriber: EventSubscriberFunc): void {
+ this.on_(event, subscriber, true)
+ }
+
+ /**
+ * Waits for specific event, with an optional predicate.
+ * Returns `undefined` on timeout if specified.
+ */
+ wait(event: K): Promise
+ wait(event: K, predicate: ((event: ChatEvent & {type: K}) => boolean) | undefined): Promise
+ wait(event: K, timeout: number): Promise
+ wait(event: K, predicate: ((event: ChatEvent & {type: K}) => boolean) | undefined, timeout: number): Promise
+ wait(
+ event: K,
+ predicate: ((event: ChatEvent & {type: K}) => boolean) | undefined | number = undefined, // number for timeout
+ timeout: number = 0 // milliseconds, default - indefinite
+ ): Promise {
+ if (typeof predicate === "number") {
+ timeout = predicate
+ predicate = undefined
+ }
+ return new Promise((resolve, reject) => {
+ let done = false
+ const cleanup = () => {
+ done = true
+ this.off(event, subscriber)
+ }
+ const subscriber: EventSubscriberFunc = async (evt: ChatEvent & {type: K}) => {
+ if (done) return
+ if (predicate) {
+ try { if (!predicate(evt)) return }
+ catch(e) { cleanup(); reject(e); return }
+ }
+ cleanup()
+ resolve(evt)
+ }
+ this.on(event, subscriber)
+ if (timeout > 0) {
+ setTimeout(() => { if (!done) { cleanup(); resolve(undefined) } }, timeout)
+ }
+ })
+ }
+
+ /**
+ * Unsubscribe all or a specific handler from a specific event.
+ * @param {CEvt.Tag} event - The event type to unsubscribe from.
+ * @param subscriber - An optional subscriber function for the event.
+ */
+ off(event: K, subscriber: EventSubscriberFunc | undefined = undefined): void {
+ if (subscriber) {
+ const subs = this.subscribers[event]
+ if (subs) {
+ const i = subs.findIndex(s => s.subscriber === subscriber)
+ if (i >= 0) subs.splice(i, 1)
+ }
+ } else {
+ delete this.subscribers[event]
+ }
+ }
+
+ /**
+ * Unsubscribe all or a specific handler from any events.
+ * @param receiver - An optional subscriber function for the event.
+ */
+ offAny(receiver: EventSubscriberFunc | undefined = undefined): void {
+ if (receiver) {
+ const i = this.receivers.findIndex(r => r === receiver)
+ if (i >= 0) this.receivers.splice(i, 1)
+ } else {
+ this.receivers = []
+ }
+ }
+
+ /**
+ * Chat controller is initialized
+ */
+ get initialized(): boolean {
+ return typeof this.ctrl_ === "bigint"
+ }
+
+ /**
+ * Chat controller is started
+ */
+ get started(): boolean {
+ return this.receiveEvents && this.eventsLoop !== undefined
+ }
+
+ /**
+ * Chat controller reference
+ */
+ get ctrl(): bigint {
+ if (typeof this.ctrl_ === "bigint") return this.ctrl_
+ else throw Error("chat api controller not initialized")
+ }
+
+ async sendChatCmd(cmd: string): Promise {
+ return await core.chatSendCmd(this.ctrl, cmd)
+ }
+
+ async recvChatEvent(wait: number = 5_000_000): Promise {
+ return await core.chatRecvMsgWait(this.ctrl, wait)
+ }
+
+ /**
+ * Create bot address.
+ * Network usage: interactive.
+ */
+ async apiCreateUserAddress(userId: number): Promise {
+ const r = await this.sendChatCmd(CC.APICreateMyAddress.cmdString({userId}))
+ if (r.type === "userContactLinkCreated") return r.connLinkContact
+ throw new ChatCommandError("error creating user address", r)
+ }
+
+ /**
+ * Deletes a user address.
+ * Network usage: background.
+ */
+ async apiDeleteUserAddress(userId: number): Promise {
+ const r = await this.sendChatCmd(CC.APIDeleteMyAddress.cmdString({userId}))
+ if (r.type === "userContactLinkDeleted") return
+ throw new ChatCommandError("error deleting user address", r)
+ }
+
+ /**
+ * Get bot address and settings.
+ * Network usage: no.
+ */
+ async apiGetUserAddress(userId: number): Promise {
+ try {
+ const r = await this.sendChatCmd(CC.APIShowMyAddress.cmdString({userId}))
+ switch (r.type) {
+ case "userContactLink": return r.contactLink
+ default: throw new ChatCommandError("error loading user address", r)
+ }
+ } catch (err) {
+ const e = err as any
+ if (e.chatError?.type === "errorStore" && e.chatError.storeError?.type === "userContactLinkNotFound") return undefined
+ throw e
+ }
+ }
+
+ /**
+ * Add address to bot profile.
+ * Network usage: interactive.
+ */
+ async apiSetProfileAddress(userId: number, enable: boolean): Promise {
+ const r = await this.sendChatCmd(CC.APISetProfileAddress.cmdString({userId, enable}))
+ switch (r.type) {
+ case "userProfileUpdated":
+ return r.updateSummary
+ default:
+ throw new ChatCommandError("error loading user address", r)
+ }
+ }
+
+ /**
+ * Set bot address settings.
+ * Network usage: interactive.
+ */
+ async apiSetAddressSettings(userId: number, {autoAccept, welcomeMessage, businessAddress}: BotAddressSettings): Promise {
+ const autoReply = welcomeMessage || defaultBotAddressSettings.welcomeMessage
+ const settings: T.AddressSettings = {
+ autoAccept: (autoAccept === undefined ? defaultBotAddressSettings.autoAccept : autoAccept) ? {acceptIncognito: false} : undefined,
+ autoReply: typeof autoReply === "string" ? {type: "text", text: autoReply} : autoReply,
+ businessAddress: businessAddress || defaultBotAddressSettings.businessAddress || false
+ }
+ const r = await this.sendChatCmd(CC.APISetAddressSettings.cmdString({userId, settings}))
+ if (r.type !== "userContactLinkUpdated") {
+ throw new ChatCommandError("error changing user contact address settings", r)
+ }
+ }
+
+ /**
+ * Send messages.
+ * Network usage: background.
+ */
+ async apiSendMessages(chat: [T.ChatType, number] | T.ChatRef | T.ChatInfo, messages: T.ComposedMessage[], liveMessage = false): Promise {
+ const sendRef = Array.isArray(chat)
+ ? {chatType: chat[0], chatId: chat[1]}
+ : "chatType" in chat
+ ? chat
+ : util.chatInfoRef(chat)
+ if (!sendRef) throw Error("apiSendMessages: can't send messages to this chat")
+ const r = await this.sendChatCmd(
+ CC.APISendMessages.cmdString({
+ sendRef,
+ composedMessages: messages,
+ liveMessage
+ })
+ )
+ if (r.type === "newChatItems") return r.chatItems
+ throw new ChatCommandError("unexpected response", r)
+ }
+
+ /**
+ * Send text message.
+ * Network usage: background.
+ */
+ async apiSendTextMessage(chat: [T.ChatType, number] | T.ChatRef | T.ChatInfo, text: string, inReplyTo?: number): Promise {
+ return this.apiSendMessages(chat, [{msgContent: {type: "text", text}, mentions: {}, quotedItemId: inReplyTo}])
+ }
+
+ /**
+ * Send text message in reply to received message.
+ * Network usage: background.
+ */
+ async apiSendTextReply(chatItem: T.AChatItem, text: string): Promise {
+ return this.apiSendTextMessage(chatItem.chatInfo, text, chatItem.chatItem.meta.itemId)
+ }
+
+ /**
+ * Update message.
+ * Network usage: background.
+ */
+ async apiUpdateChatItem(chatType: T.ChatType, chatId: number, chatItemId: number, msgContent: T.MsgContent, liveMessage: false): Promise {
+ const r = await this.sendChatCmd(
+ CC.APIUpdateChatItem.cmdString({
+ chatRef: {chatType, chatId},
+ chatItemId,
+ liveMessage,
+ updatedMessage: {msgContent, mentions: {}},
+ })
+ )
+ if (r.type === "chatItemUpdated") return r.chatItem.chatItem
+ throw new ChatCommandError("error updating chat item", r)
+ }
+
+ /**
+ * Delete message.
+ * Network usage: background.
+ */
+ async apiDeleteChatItems(
+ chatType: T.ChatType,
+ chatId: number,
+ chatItemIds: number[],
+ deleteMode: T.CIDeleteMode
+ ): Promise {
+ const r = await this.sendChatCmd(CC.APIDeleteChatItem.cmdString({chatRef: {chatType, chatId}, chatItemIds, deleteMode}))
+ if (r.type === "chatItemsDeleted") return r.chatItemDeletions
+ throw new ChatCommandError("error deleting chat item", r)
+ }
+
+ /**
+ * Moderate message. Requires Moderator role (and higher than message author's).
+ * Network usage: background.
+ */
+ async apiDeleteMemberChatItem(groupId: number, chatItemIds: number[]): Promise {
+ const r = await this.sendChatCmd(CC.APIDeleteMemberChatItem.cmdString({groupId, chatItemIds}))
+ if (r.type === "chatItemsDeleted") return r.chatItemDeletions
+ throw new ChatCommandError("error deleting member chat item", r)
+ }
+
+ /**
+ * Add/remove message reaction.
+ * Network usage: background.
+ */
+ async apiChatItemReaction(
+ chatType: T.ChatType,
+ chatId: number,
+ chatItemId: number,
+ add: boolean,
+ reaction: T.MsgReaction
+ ) {
+ const r = await this.sendChatCmd(CC.APIChatItemReaction.cmdString({chatRef: {chatType, chatId}, chatItemId, add, reaction}))
+ if (r.type === "chatItemsDeleted") return r.chatItemDeletions
+ throw new ChatCommandError("error setting item reaction", r)
+ }
+
+ /**
+ * Receive file.
+ * Network usage: no.
+ */
+ async apiReceiveFile(fileId: number): Promise {
+ const r = await this.sendChatCmd(CC.ReceiveFile.cmdString({fileId, userApprovedRelays: true}))
+ if (r.type === "rcvFileAccepted") return r.chatItem
+ throw new ChatCommandError("error receiving file", r)
+ }
+
+ /**
+ * Cancel file.
+ * Network usage: background.
+ */
+ async apiCancelFile(fileId: number): Promise {
+ const r = await this.sendChatCmd(CC.CancelFile.cmdString({fileId}))
+ if (r.type === "sndFileCancelled" || r.type === "rcvFileCancelled") return
+ throw new ChatCommandError("error canceling file", r)
+ }
+
+ /**
+ * Add contact to group. Requires bot to have Admin role.
+ * Network usage: interactive.
+ */
+ async apiAddMember(groupId: number, contactId: number, memberRole: T.GroupMemberRole): Promise {
+ const r = await this.sendChatCmd(CC.APIAddMember.cmdString({groupId, contactId, memberRole}))
+ if (r.type === "sentGroupInvitation") return r.member
+ throw new ChatCommandError("error adding member", r)
+ }
+
+ /**
+ * Join group.
+ * Network usage: interactive.
+ */
+ async apiJoinGroup(groupId: number): Promise {
+ const r = await this.sendChatCmd(CC.APIJoinGroup.cmdString({groupId}))
+ if (r.type === "userAcceptedGroupSent") return r.groupInfo
+ throw new ChatCommandError("error joining group", r)
+ }
+
+ /**
+ * Accept group member. Requires Admin role.
+ * Network usage: background.
+ */
+ async apiAcceptMember(groupId: number, groupMemberId: number, memberRole: T.GroupMemberRole): Promise {
+ const r = await this.sendChatCmd(CC.APIAcceptMember.cmdString({groupId, groupMemberId, memberRole}))
+ if (r.type === "memberAccepted") return r.member
+ throw new ChatCommandError("error accepting member", r)
+ }
+
+ /**
+ * Set members role. Requires Admin role.
+ * Network usage: background.
+ */
+ async apiSetMembersRole(groupId: number, groupMemberIds: number[], memberRole: T.GroupMemberRole): Promise {
+ const r = await this.sendChatCmd(CC.APIMembersRole.cmdString({groupId, groupMemberIds, memberRole}))
+ if (r.type === "membersRoleUser") return
+ throw new ChatCommandError("error setting members role", r)
+ }
+
+ /**
+ * Block members. Requires Moderator role.
+ * Network usage: background.
+ */
+ async apiBlockMembersForAll(groupId: number, groupMemberIds: number[], blocked: boolean): Promise {
+ const r = await this.sendChatCmd(CC.APIBlockMembersForAll.cmdString({groupId, groupMemberIds, blocked}))
+ if (r.type === "membersBlockedForAllUser") return
+ throw new ChatCommandError("error blocking members", r)
+ }
+
+ /**
+ * Remove members. Requires Admin role.
+ * Network usage: background.
+ */
+ async apiRemoveMembers(groupId: number, memberIds: number[], withMessages = false): Promise {
+ const r = await this.sendChatCmd(CC.APIRemoveMembers.cmdString({groupId, groupMemberIds: memberIds, withMessages}))
+ if (r.type === "userDeletedMembers") return r.members
+ throw new ChatCommandError("error removing member", r)
+ }
+
+ /**
+ * Leave group.
+ * Network usage: background.
+ */
+ async apiLeaveGroup(groupId: number): Promise {
+ const r = await this.sendChatCmd(CC.APILeaveGroup.cmdString({groupId}))
+ if (r.type === "leftMemberUser") return r.groupInfo
+ throw new ChatCommandError("error leaving group", r)
+ }
+
+ /**
+ * Get group members.
+ * Network usage: no.
+ */
+ async apiListMembers(groupId: number): Promise {
+ const r = await this.sendChatCmd(CC.APIListMembers.cmdString({groupId}))
+ if (r.type === "groupMembers") return r.group.members
+ throw new ChatCommandError("error getting group members", r)
+ }
+
+ /**
+ * Create group.
+ * Network usage: no.
+ */
+ async apiNewGroup(userId: number, groupProfile: T.GroupProfile): Promise {
+ const r = await this.sendChatCmd(CC.APINewGroup.cmdString({userId, groupProfile, incognito: false}))
+ if (r.type === "groupCreated") return r.groupInfo
+ throw new ChatCommandError("error creating group", r)
+ }
+
+ /**
+ * Update group profile.
+ * Network usage: background.
+ */
+ async apiUpdateGroupProfile(groupId: number, groupProfile: T.GroupProfile): Promise {
+ const r = await this.sendChatCmd(CC.APIUpdateGroupProfile.cmdString({groupId, groupProfile}))
+ if (r.type === "groupUpdated") return r.toGroup
+ throw new ChatCommandError("error updating group", r)
+ }
+
+ /**
+ * Create group link.
+ * Network usage: interactive.
+ */
+ async apiCreateGroupLink(groupId: number, memberRole: T.GroupMemberRole): Promise {
+ const r = await this.sendChatCmd(CC.APICreateGroupLink.cmdString({groupId, memberRole}))
+ if (r.type === "groupLinkCreated") {
+ const link = r.groupLink.connLinkContact
+ return link.connShortLink || link.connFullLink
+ }
+ throw new ChatCommandError("error creating group link", r)
+ }
+
+ /**
+ * Set member role for group link.
+ * Network usage: no.
+ */
+ async apiSetGroupLinkMemberRole(groupId: number, memberRole: T.GroupMemberRole): Promise {
+ const r = await this.sendChatCmd(CC.APIGroupLinkMemberRole.cmdString({groupId, memberRole}))
+ if (r.type !== "groupLink") throw new ChatCommandError("error setting group link member role", r)
+ }
+
+ /**
+ * Delete group link.
+ * Network usage: background.
+ */
+ async apiDeleteGroupLink(groupId: number): Promise {
+ const r = await this.sendChatCmd(CC.APIDeleteGroupLink.cmdString({groupId}))
+ if (r.type !== "groupLinkDeleted") throw new ChatCommandError("error deleting group link", r)
+ }
+
+ /**
+ * Get group link.
+ * Network usage: no.
+ */
+ async apiGetGroupLink(groupId: number): Promise {
+ const r = await this.sendChatCmd(CC.APIGetGroupLink.cmdString({groupId}))
+ if (r.type === "groupLink") return r.groupLink
+ throw new ChatCommandError("error getting group link", r)
+ }
+
+ async apiGetGroupLinkStr(groupId: number): Promise {
+ const link = (await this.apiGetGroupLink(groupId)).connLinkContact
+ return link.connShortLink || link.connFullLink
+ }
+
+ /**
+ * Create 1-time invitation link.
+ * Network usage: interactive.
+ */
+ async apiCreateLink(userId: number): Promise {
+ const r = await this.sendChatCmd(CC.APIAddContact.cmdString({userId, incognito: false}))
+ if (r.type === "invitation") {
+ const link = r.connLinkInvitation
+ return link.connShortLink || link.connFullLink
+ }
+ throw new ChatCommandError("error creating link", r)
+ }
+
+ /**
+ * Determine SimpleX link type and if the bot is already connected via this link.
+ * Network usage: interactive.
+ */
+ async apiConnectPlan(userId: number, connectionLink: string): Promise<[T.ConnectionPlan, T.CreatedConnLink]> {
+ const r = await this.sendChatCmd(CC.APIConnectPlan.cmdString({userId, connectionLink}))
+ if (r.type === "connectionPlan") return [r.connectionPlan, r.connLink]
+ throw new ChatCommandError("error getting connect plan", r)
+ }
+
+ /**
+ * Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link
+ * Network usage: interactive.
+ */
+ async apiConnect(userId: number, incognito: boolean, preparedLink?: T.CreatedConnLink): Promise {
+ const r = await this.sendChatCmd(CC.APIConnect.cmdString({userId, incognito, preparedLink_: preparedLink}))
+ return this.handleConnectResult(r)
+ }
+
+ /**
+ * Connect via SimpleX link as string in the active user profile.
+ * Network usage: interactive.
+ */
+ async apiConnectActiveUser(connLink: string): Promise {
+ const r = await this.sendChatCmd(CC.Connect.cmdString({incognito: false, connLink_: connLink}))
+ return this.handleConnectResult(r)
+ }
+
+ private handleConnectResult(r: ChatResponse): ConnReqType {
+ switch (r.type) {
+ case "sentConfirmation":
+ return ConnReqType.Invitation
+ case "sentInvitation":
+ return ConnReqType.Contact
+ case "contactAlreadyExists":
+ throw new ChatCommandError("contact already exists", r)
+ default:
+ throw new ChatCommandError("connection error", r)
+ }
+ }
+
+ /**
+ * Accept contact request.
+ * Network usage: interactive.
+ */
+ async apiAcceptContactRequest(contactReqId: number): Promise {
+ const r = await this.sendChatCmd(CC.APIAcceptContact.cmdString({contactReqId}))
+ if (r.type === "acceptingContactRequest") return r.contact
+ throw new ChatCommandError("error accepting contact request", r)
+ }
+
+ /**
+ * Reject contact request. The user who sent the request is **not notified**.
+ * Network usage: no.
+ */
+ async apiRejectContactRequest(contactReqId: number): Promise {
+ const r = await this.sendChatCmd(CC.APIRejectContact.cmdString({contactReqId}))
+ if (r.type === "contactRequestRejected") return
+ throw new ChatCommandError("error rejecting contact request", r)
+ }
+
+ /**
+ * Get contacts.
+ * Network usage: no.
+ */
+ async apiListContacts(userId: number): Promise {
+ const r = await this.sendChatCmd(CC.APIListContacts.cmdString({userId}))
+ if (r.type === "contactsList") return r.contacts
+ throw new ChatCommandError("error listing contacts", r)
+ }
+
+ /**
+ * Get groups.
+ * Network usage: no.
+ */
+ async apiListGroups(userId: number, contactId?: number, search?: string): Promise {
+ const r = await this.sendChatCmd(CC.APIListGroups.cmdString({userId, contactId_: contactId, search}))
+ if (r.type === "groupsList") return r.groups
+ throw new ChatCommandError("error listing groups", r)
+ }
+
+ /**
+ * Delete chat.
+ * Network usage: background.
+ */
+ async apiDeleteChat(chatType: T.ChatType, chatId: number, deleteMode: T.ChatDeleteMode = {type: "full", notify: true}): Promise {
+ const r = await this.sendChatCmd(CC.APIDeleteChat.cmdString({chatRef: {chatType, chatId}, chatDeleteMode: deleteMode}))
+ switch (chatType) {
+ case T.ChatType.Direct:
+ if (r.type === "contactDeleted") return
+ break
+ case T.ChatType.Group:
+ if (r.type === "groupDeletedUser") return
+ break
+ }
+ throw new ChatCommandError("error deleting chat", r)
+ }
+
+ /**
+ * Get active user profile
+ * Network usage: no.
+ */
+ async apiGetActiveUser(): Promise {
+ try {
+ const r = await this.sendChatCmd(CC.ShowActiveUser.cmdString({}))
+ switch (r.type) {
+ case "activeUser":
+ return r.user
+ default:
+ throw new ChatCommandError("unexpected response", r)
+ }
+ } catch (err) {
+ const e = err as core.ChatAPIError
+ if (e.chatError?.type === "error" && e.chatError.errorType.type === "noActiveUser") return undefined
+ throw err
+ }
+ }
+
+ /**
+ * Create new user profile
+ * Network usage: no.
+ */
+ async apiCreateActiveUser(profile?: T.Profile): Promise {
+ const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false}}))
+ if (r.type === "activeUser") return r.user
+ throw new ChatCommandError("unexpected response", r)
+ }
+
+ /**
+ * Get all user profiles
+ * Network usage: no.
+ */
+ async apiListUsers(): Promise {
+ const r = await this.sendChatCmd(CC.ListUsers.cmdString({}))
+ if (r.type === "usersList") return r.users
+ throw new ChatCommandError("error listing users", r)
+ }
+
+ /**
+ * Set active user profile
+ * Network usage: no.
+ */
+ async apiSetActiveUser(userId: number, viewPwd?: string): Promise {
+ const r = await this.sendChatCmd(CC.APISetActiveUser.cmdString({userId, viewPwd}))
+ if (r.type === "activeUser") return r.user
+ throw new ChatCommandError("error setting active user", r)
+ }
+
+ /**
+ * Delete user profile.
+ * Network usage: background.
+ */
+ async apiDeleteUser(userId: number, delSMPQueues: boolean, viewPwd?: string): Promise {
+ const r = await this.sendChatCmd(CC.APIDeleteUser.cmdString({userId, delSMPQueues, viewPwd}))
+ if (r.type === "cmdOk") return
+ throw new ChatCommandError("error deleting user", r)
+ }
+
+ /**
+ * Update user profile.
+ * Network usage: background.
+ */
+ async apiUpdateProfile(userId: number, profile: T.Profile): Promise {
+ const r = await this.sendChatCmd(CC.APIUpdateProfile.cmdString({userId, profile}))
+ switch (r.type) {
+ case "userProfileNoChange":
+ return undefined
+ case "userProfileUpdated":
+ return r.updateSummary
+ default:
+ throw new ChatCommandError("error updating profile", r)
+ }
+ }
+
+ /**
+ * Configure chat preference overrides for the contact.
+ * Network usage: background.
+ */
+ async apiSetContactPrefs(contactId: number, preferences: T.Preferences): Promise {
+ const r = await this.sendChatCmd(CC.APISetContactPrefs.cmdString({contactId, preferences}))
+ if (r.type !== "contactPrefsUpdated") throw new ChatCommandError("error setting contact prefs", r)
+ }
+}
diff --git a/packages/simplex-chat-nodejs/src/bot.ts b/packages/simplex-chat-nodejs/src/bot.ts
new file mode 100644
index 0000000000..95a0c13d96
--- /dev/null
+++ b/packages/simplex-chat-nodejs/src/bot.ts
@@ -0,0 +1,216 @@
+import {T} from "@simplex-chat/types"
+import * as api from "./api"
+import * as core from "./core"
+import * as util from "./util"
+import equal = require("fast-deep-equal")
+
+export interface BotDbOpts {
+ dbFilePrefix: string // two schema files will be named _chat.db and _agent.db
+ dbKey?: string
+ confirmMigrations?: core.MigrationConfirmation
+}
+
+export interface BotOptions {
+ createAddress?: boolean
+ updateAddress?: boolean
+ updateProfile?: boolean
+ addressSettings?: api.BotAddressSettings
+ allowFiles?: boolean
+ commands?: T.ChatBotCommand[] // commands to show in client UI
+ useBotProfile?: boolean // create profile not marked as a bot, with default preferences
+ logContacts?: boolean
+ logNetwork?: boolean
+}
+
+const defaultOpts: Required = {
+ createAddress: true,
+ updateAddress: true,
+ updateProfile: true,
+ addressSettings: api.defaultBotAddressSettings,
+ allowFiles: false,
+ commands: [],
+ useBotProfile: true,
+ logContacts: true,
+ logNetwork: false
+}
+
+export interface BotConfig {
+ profile: T.Profile,
+ dbOpts: BotDbOpts,
+ options: BotOptions,
+ onMessage?: (chatItem: T.AChatItem, content: T.MsgContent) => void | Promise,
+ // command handlers can be different from commands to be shown in client UI
+ onCommands?: {[K in string]?: ((chatItem: T.AChatItem, command: util.BotCommand) => void | Promise)},
+ // If you use `onMessage` and to subscribe "newChatItems" event, exclude content messages from processing
+ // If you use `onCommands` and to subscribe "newChatItems" event, exclude commands from processing
+ events?: api.EventSubscribers
+}
+
+export async function run({profile, dbOpts, options = defaultOpts, onMessage, onCommands = {}, events = {}}: BotConfig): Promise<[api.ChatApi, T.User, T.UserContactLink | undefined]> {
+ const bot = await api.ChatApi.init(dbOpts.dbFilePrefix, dbOpts.dbKey || "", dbOpts.confirmMigrations || core.MigrationConfirmation.YesUp)
+ const opts = fullOptions(options)
+ if (onMessage) subscribeMessages(bot, onMessage)
+ if (Object.keys(onCommands).length > 0) subscribeCommands(bot, onCommands)
+ if (Object.keys(events).length > 0) bot.on(events)
+ subscribeLogEvents(bot, opts)
+ const botProfile = mkBotProfile(profile, opts)
+ const user = await createBotUser(bot, botProfile)
+ await bot.startChat()
+ const address = await createOrUpdateAddress(bot, user, opts)
+ if (address) {
+ const addressLink = util.contactAddressStr(address.connLinkContact)
+ console.log(`Bot address: ${addressLink}`)
+ if (opts.useBotProfile) botProfile.contactLink = addressLink
+ }
+ await updateBotUserProfile(bot, user, botProfile, opts)
+ return [bot, user, address]
+}
+
+function fullOptions(options: BotOptions): Required {
+ const opts = {
+ createAddress: options.createAddress ?? defaultOpts.createAddress,
+ updateAddress: options.updateAddress ?? defaultOpts.updateAddress,
+ updateProfile: options.updateProfile ?? defaultOpts.updateProfile,
+ addressSettings: options.addressSettings ?? defaultOpts.addressSettings,
+ allowFiles: options.allowFiles ?? defaultOpts.allowFiles,
+ commands: options.commands ?? defaultOpts.commands,
+ useBotProfile: options.useBotProfile ?? defaultOpts.useBotProfile,
+ logContacts: options.logContacts ?? defaultOpts.logContacts,
+ logNetwork: options.logNetwork ?? defaultOpts.logNetwork
+ }
+ const welcomeMessage = opts.addressSettings.welcomeMessage ?? defaultOpts.addressSettings.welcomeMessage
+ opts.addressSettings = {
+ autoAccept: opts.addressSettings.autoAccept ?? defaultOpts.addressSettings.autoAccept,
+ welcomeMessage: typeof welcomeMessage === "string" ? {type: "text", text: welcomeMessage} : welcomeMessage,
+ businessAddress: opts.addressSettings.businessAddress ?? defaultOpts.addressSettings.businessAddress
+ }
+ return opts
+}
+
+function mkBotProfile(profile: T.Profile, opts: Required): T.Profile {
+ if (opts.useBotProfile) {
+ const prefs = profile.preferences || {}
+ if (prefs.files || prefs.calls || prefs.voice || prefs.commands) {
+ console.log("Option useBotProfile is enabled and profile preferences used for files, calls, voice or commands, exiting")
+ process.exit()
+ }
+ prefs.files = {allow: opts.allowFiles ? T.FeatureAllowed.Yes : T.FeatureAllowed.No}
+ prefs.calls = {allow: T.FeatureAllowed.No}
+ prefs.voice = {allow: T.FeatureAllowed.No}
+ prefs.commands = opts.commands
+ profile.preferences = prefs
+ profile.peerType = T.ChatPeerType.Bot
+ } else if (opts.commands.length > 0) {
+ console.log("Option useBotProfile is disabled and commands are passed, exiting")
+ process.exit()
+ }
+ return profile
+}
+
+function subscribeMessages(bot: api.ChatApi, onMessage: (chatItem: T.AChatItem, content: T.MsgContent) => void | Promise) {
+ bot.on("newChatItems", async ({chatItems}) => {
+ for (const ci of chatItems) {
+ if (ci.chatItem.content.type === "rcvMsgContent") {
+ try {
+ const p = onMessage(ci, ci.chatItem.content.msgContent)
+ if (p instanceof Promise) await p
+ } catch (e) {
+ console.log("message processing error", e)
+ }
+ }
+ }
+ })
+}
+
+function subscribeCommands(bot: api.ChatApi, commands: {[K in string]?: ((chatItem: T.AChatItem, command: util.BotCommand) => void | Promise)}) {
+ bot.on("newChatItems", async (evt) => {
+ for (const ci of evt.chatItems) {
+ const cmd = util.ciBotCommand(ci.chatItem)
+ if (cmd) {
+ const cmdFunc = commands[cmd.keyword] || commands[""]
+ if (cmdFunc) {
+ try {
+ const p = cmdFunc(ci, cmd)
+ if (p instanceof Promise) await p
+ } catch(e) {
+ console.log(`${cmd} command processing error`, e)
+ }
+ }
+ }
+ }
+ })
+}
+
+function subscribeLogEvents(bot: api.ChatApi, opts: Required) {
+ if (opts.logContacts) {
+ bot.on({
+ "contactConnected": ({contact}) => console.log(`${contact.profile.displayName} connected`),
+ "contactDeletedByContact": ({contact}) => console.log(`${contact.profile.displayName} deleted connection with bot`)
+ })
+ }
+ if (opts.logNetwork) {
+ bot.on({
+ "hostConnected": ({transportHost}) => console.log(`connected server ${transportHost}`),
+ "hostDisconnected": ({transportHost}) => console.log(`diconnected server ${transportHost}`),
+ "subscriptionStatus": ({subscriptionStatus, connections}) => console.log(`${connections.length} subscription(s) ${subscriptionStatus.type}`)
+ })
+ }
+}
+
+async function createBotUser(bot: api.ChatApi, profile: T.Profile): Promise {
+ let user = await bot.apiGetActiveUser()
+ if (!user) {
+ console.log("No active user in database, creating...")
+ user = await bot.apiCreateActiveUser(profile)
+ }
+ console.log("Bot user: ", user.profile.displayName)
+ return user
+}
+
+async function createOrUpdateAddress(bot: api.ChatApi, user: T.User, opts: Required): Promise {
+ const {userId} = user
+ let address = await bot.apiGetUserAddress(userId)
+ if (!address) {
+ if (opts.createAddress) {
+ console.log("Bot has no address, creating...")
+ await bot.apiCreateUserAddress(userId)
+ address = await bot.apiGetUserAddress(userId)
+ if (!address) {
+ console.log("Failed reading created user address, exiting")
+ process.exit()
+ }
+ } else {
+ console.log("Warning: bot has no address")
+ return
+ }
+ }
+
+ const addressSettings = opts.addressSettings || defaultOpts.addressSettings
+ if (!equal(util.botAddressSettings(address), addressSettings)) {
+ if (opts.updateAddress) {
+ console.log("Bot address settings changed, updating...")
+ await bot.apiSetAddressSettings(userId, addressSettings)
+ } else {
+ console.log("Bot address settings changed")
+ }
+ }
+
+ return address
+}
+
+async function updateBotUserProfile(bot: api.ChatApi, user: T.User, profile: T.Profile, opts: Required): Promise {
+ const {userId} = user
+ if (!equal(util.fromLocalProfile(user.profile), profile)) {
+ if (opts.updateProfile) {
+ console.log("Bot profile changed, updating...")
+ const summary = await bot.apiUpdateProfile(userId, profile)
+ console.log(
+ summary
+ ? `Bot profile updated: ${summary.updateSuccesses} updated contact(s), ${summary.updateFailures} failed contact update(s).`
+ : "Unexpected: profile did not change!"
+ )
+ } else {
+ console.log("Bot profile changed")
+ }
+ }
+}
diff --git a/packages/simplex-chat-nodejs/src/core.ts b/packages/simplex-chat-nodejs/src/core.ts
new file mode 100644
index 0000000000..949c2356af
--- /dev/null
+++ b/packages/simplex-chat-nodejs/src/core.ts
@@ -0,0 +1,210 @@
+import {ChatEvent, ChatResponse, T} from "@simplex-chat/types"
+import * as simplex from "./simplex"
+
+/**
+ * Initialize chat controller
+ */
+export async function chatMigrateInit(dbPath: string, dbKey: string, confirm: MigrationConfirmation): Promise {
+ const [ctrl, res] = await simplex.chat_migrate_init(dbPath, dbKey, confirm)
+ const json = JSON.parse(res)
+ if (json.type === 'ok') return ctrl
+ throw new ChatInitError("Database or migration error (see dbMigrationError property)", json as DBMigrationError)
+}
+
+/**
+ * Close chat store
+ */
+export async function chatCloseStore(ctrl: bigint): Promise {
+ const res = await simplex.chat_close_store(ctrl)
+ if (res !== "") throw new Error(res)
+}
+
+/**
+ * Send chat command as string
+ */
+export async function chatSendCmd(ctrl: bigint, cmd: string): Promise {
+ const res = await simplex.chat_send_cmd(ctrl, cmd)
+ const json = JSON.parse(res) as APIResult
+ // console.log(cmd.slice(0, 16), json.result?.type || json.error)
+ if (typeof json.result === 'object') return json.result
+ if (typeof json.error === 'object') throw new ChatAPIError("Chat command error (see chatError property)", json.error as T.ChatError)
+ throw new ChatAPIError("Invalid chat command result")
+}
+
+/**
+ * Receive chat event
+ */
+export async function chatRecvMsgWait(ctrl: bigint, wait: number): Promise {
+ const res = await simplex.chat_recv_msg_wait(ctrl, wait)
+ if (res === "") return undefined
+ const json = JSON.parse(res) as APIResult
+ // if (json.result) console.log("event", json.result.type)
+ if (typeof json.result === 'object') return json.result
+ if (typeof json.error === 'object') throw new ChatAPIError("Chat event error (see chatError property)", json.error as T.ChatError)
+ throw new ChatAPIError("Invalid chat event")
+}
+
+/**
+ * Write buffer to encrypted file
+ */
+export async function chatWriteFile(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise {
+ const res = await simplex.chat_write_file(ctrl, path, buffer)
+ return cryptoArgsResult(res)
+}
+
+/**
+ * Read buffer from encrypted file
+ */
+export async function chatReadFile(path: string, {fileKey, fileNonce}: CryptoArgs): Promise {
+ return await simplex.chat_read_file(path, fileKey, fileNonce)
+}
+
+/**
+ * Encrypt file
+ */
+export async function chatEncryptFile(ctrl: bigint, fromPath: string, toPath: string): Promise {
+ const res = await simplex.chat_encrypt_file(ctrl, fromPath, toPath)
+ return cryptoArgsResult(res)
+}
+
+/**
+ * Decrypt file
+ */
+export async function chatDecryptFile(fromPath: string, {fileKey, fileNonce}: CryptoArgs, toPath: string): Promise {
+ const res = await simplex.chat_decrypt_file(fromPath, fileKey, fileNonce, toPath)
+ if (res !== "") throw new Error(res)
+}
+
+function cryptoArgsResult(res: string): CryptoArgs {
+ const json = JSON.parse(res)
+ switch (json.type) {
+ case "result": return json.cryptoArgs as CryptoArgs
+ case "error": throw Error(json.writeError)
+ default: throw Error("unexpected chat_write_file result: " + res)
+ }
+}
+
+export interface APIResult {
+ result?: R
+ error?: T.ChatError
+}
+
+export class ChatAPIError extends Error {
+ constructor(public message: string, public chatError: T.ChatError | undefined = undefined) {
+ super(message)
+ }
+}
+
+/**
+ * Migration confirmation mode
+ */
+export enum MigrationConfirmation {
+ YesUp = "yesUp",
+ YesUpDown = "yesUpDown",
+ Console = "console",
+ Error = "error"
+}
+
+/**
+ * File encryption key and nonce
+ */
+export interface CryptoArgs {
+ fileKey: string
+ fileNonce: string
+}
+
+export class ChatInitError extends Error {
+ constructor(public message: string, public dbMigrationError: DBMigrationError) {
+ super(message)
+ }
+}
+
+export type DBMigrationError =
+ | DBMigrationError.InvalidConfirmation
+ | DBMigrationError.ErrorNotADatabase // invalid/corrupt database file or incorrect encryption key
+ | DBMigrationError.ErrorMigration
+ | DBMigrationError.ErrorSQL
+
+export namespace DBMigrationError {
+ export type Tag = "invalidConfirmation" | "errorNotADatabase" | "errorMigration" | "errorSQL"
+
+ interface Interface {
+ type: Tag
+ }
+
+ export interface InvalidConfirmation extends Interface {
+ type: "invalidConfirmation"
+ }
+
+ export interface ErrorNotADatabase extends Interface {
+ type: "errorNotADatabase"
+ dbFile: string
+ }
+
+ export interface ErrorMigration extends Interface {
+ type: "errorMigration"
+ dbFile: string
+ migrationError: MigrationError
+ }
+
+ export interface ErrorSQL extends Interface {
+ type: "errorSQL"
+ dbFile: string
+ migrationSQLError: string
+ }
+}
+
+export type MigrationError =
+ | MigrationError.MEUpgrade
+ | MigrationError.MEDowngrade
+ | MigrationError.MigrationError
+
+export namespace MigrationError {
+ export type Tag = "upgrade" | "downgrade" | "migrationError"
+
+ interface Interface {
+ type: Tag
+ }
+
+ export interface MEUpgrade extends Interface {
+ type: "upgrade"
+ upMigrations: UpMigration
+ }
+
+ export interface MEDowngrade extends Interface {
+ type: "downgrade"
+ downMigrations: string[]
+ }
+
+ export interface MigrationError extends Interface {
+ type: "migrationError"
+ mtrError: MTRError
+ }
+}
+
+export interface UpMigration {
+ upName: string
+ withDown: boolean
+}
+
+export type MTRError =
+ | MTRError.MTRENoDown
+ | MTRError.MTREDifferent
+
+export namespace MTRError {
+ export type Tag = "noDown" | "different"
+
+ interface Interface {
+ type: Tag
+ }
+
+ export interface MTRENoDown extends Interface {
+ type: "noDown"
+ upMigrations: UpMigration
+ }
+
+ export interface MTREDifferent extends Interface {
+ type: "different"
+ downMigrations: string[]
+ }
+}
diff --git a/packages/simplex-chat-nodejs/src/download-libs.js b/packages/simplex-chat-nodejs/src/download-libs.js
new file mode 100644
index 0000000000..6b8f583155
--- /dev/null
+++ b/packages/simplex-chat-nodejs/src/download-libs.js
@@ -0,0 +1,224 @@
+const https = require('https');
+const fs = require('fs');
+const path = require('path');
+const extract = require('extract-zip');
+
+const GITHUB_REPO = 'simplex-chat/simplex-chat-libs';
+const RELEASE_TAG = 'v6.5.0-beta.4';
+const ROOT_DIR = process.cwd(); // Root of the package being installed
+const LIBS_DIR = path.join(ROOT_DIR, 'libs')
+const INSTALLED_FILE = path.join(LIBS_DIR, 'installed.txt');
+
+// Detect platform and architecture
+function getPlatformInfo() {
+ const platform = process.platform;
+ const arch = process.arch;
+
+ let platformName;
+ let archName;
+
+ if (platform === 'linux') {
+ platformName = 'linux';
+ } else if (platform === 'darwin') {
+ platformName = 'macos';
+ } else if (platform === 'win32') {
+ platformName = 'windows';
+ } else {
+ throw new Error(`Unsupported platform: ${platform}`);
+ }
+
+ if (arch === 'x64') {
+ archName = 'x86_64';
+ } else if (arch === 'arm64') {
+ archName = 'aarch64';
+ } else {
+ throw new Error(`Unsupported architecture: ${arch}`);
+ }
+
+ return { platformName, archName };
+}
+
+// Cleanup on libs version mismatch
+function cleanLibsDirectory() {
+ if (fs.existsSync(LIBS_DIR)) {
+ console.log('Cleaning old libraries...');
+ fs.rmSync(LIBS_DIR, { recursive: true, force: true });
+ fs.mkdirSync(LIBS_DIR, { recursive: true });
+ console.log('✓ Old libraries removed');
+ }
+}
+
+// Check if libraries are already installed with the correct version
+function isAlreadyInstalled() {
+ if (!fs.existsSync(INSTALLED_FILE)) {
+ return false;
+ }
+
+ try {
+ const installedVersion = fs.readFileSync(INSTALLED_FILE, 'utf-8').trim();
+ if (installedVersion === RELEASE_TAG) {
+ console.log(`✓ Libraries version ${RELEASE_TAG} already installed`);
+ return true;
+ } else {
+ console.log(`Version mismatch: installed ${installedVersion}, need ${RELEASE_TAG}`);
+ cleanLibsDirectory();
+ return false;
+ }
+ } catch (err) {
+ console.warn(`Could not read installed.txt: ${err.message}`);
+ return false;
+ }
+}
+
+async function install() {
+ try {
+ // Check if already installed
+ if (isAlreadyInstalled()) {
+ return;
+ }
+
+ const { platformName, archName } = getPlatformInfo();
+ const repoName = GITHUB_REPO.split('/')[1];
+ const zipFilename = `${repoName}-${platformName}-${archName}.zip`;
+ const ZIP_URL = `https://github.com/${GITHUB_REPO}/releases/download/${RELEASE_TAG}/${zipFilename}`;
+ const ZIP_PATH = path.join(ROOT_DIR, zipFilename);
+ const TEMP_EXTRACT_DIR = path.join(ROOT_DIR, '.temp-extract');
+
+ console.log(`Detected: ${platformName} ${archName}`);
+ console.log(`Downloading: ${zipFilename}`);
+
+ // Create libs directory
+ if (!fs.existsSync(LIBS_DIR)) {
+ fs.mkdirSync(LIBS_DIR, { recursive: true });
+ }
+
+ // Download zip with error handling
+ await downloadFile(ZIP_URL, ZIP_PATH);
+
+ // Extract to temporary directory
+ console.log('Extracting to temporary directory...');
+ if (!fs.existsSync(TEMP_EXTRACT_DIR)) {
+ fs.mkdirSync(TEMP_EXTRACT_DIR, { recursive: true });
+ }
+ await extract(ZIP_PATH, { dir: TEMP_EXTRACT_DIR });
+
+ // Move libs folder contents to final location
+ console.log('Moving libraries to libs/...');
+ const libsSourcePath = path.join(TEMP_EXTRACT_DIR, 'libs');
+
+ if (fs.existsSync(libsSourcePath)) {
+ // Copy all files from libs folder to LIBS_DIR
+ const files = fs.readdirSync(libsSourcePath);
+ files.forEach(file => {
+ const src = path.join(libsSourcePath, file);
+ const dest = path.join(LIBS_DIR, file);
+
+ if (fs.statSync(src).isDirectory()) {
+ copyDirSync(src, dest);
+ } else {
+ fs.copyFileSync(src, dest);
+ }
+ });
+ } else {
+ throw new Error('libs folder not found in zip archive');
+ }
+
+ // Write installed.txt with version
+ fs.writeFileSync(INSTALLED_FILE, RELEASE_TAG, 'utf-8');
+ console.log(`✓ Wrote version ${RELEASE_TAG} to installed.txt`);
+
+ // Cleanup
+ fs.rmSync(TEMP_EXTRACT_DIR, { recursive: true, force: true });
+ fs.unlinkSync(ZIP_PATH);
+ console.log('✓ Installation complete');
+ } catch (err) {
+ console.error('✗ Failed:', err.message);
+ process.exit(1);
+ }
+}
+
+// Helper function to recursively copy directories
+function copyDirSync(src, dest) {
+ if (!fs.existsSync(dest)) {
+ fs.mkdirSync(dest, { recursive: true });
+ }
+ const files = fs.readdirSync(src);
+ files.forEach(file => {
+ const srcFile = path.join(src, file);
+ const destFile = path.join(dest, file);
+ if (fs.statSync(srcFile).isDirectory()) {
+ copyDirSync(srcFile, destFile);
+ } else {
+ fs.copyFileSync(srcFile, destFile);
+ }
+ });
+}
+
+function downloadFile(url, dest) {
+ return new Promise((resolve, reject) => {
+ const file = fs.createWriteStream(dest);
+
+ https.get(url, { headers: { 'User-Agent': 'Node.js' } }, (response) => {
+ // Handle redirects
+ if (response.statusCode === 302 || response.statusCode === 301) {
+ file.destroy();
+ fs.unlink(dest, () => {});
+ return downloadFile(response.headers.location, dest)
+ .then(resolve)
+ .catch(reject);
+ }
+
+ // Handle 404
+ if (response.statusCode === 404) {
+ file.destroy();
+ fs.unlink(dest, () => {});
+ reject(new Error(
+ `Release artifact not found (404). Check:\n` +
+ ` - Repository exists: ${url.split('/releases')[0]}\n` +
+ ` - Release tag exists: ${RELEASE_TAG}\n` +
+ ` - Artifact filename is correct`
+ ));
+ return;
+ }
+
+ // Handle 403
+ if (response.statusCode === 403) {
+ file.destroy();
+ fs.unlink(dest, () => {});
+ reject(new Error(
+ `Access denied (403). The repository may be private.\n` +
+ `Set GITHUB_TOKEN environment variable for private repos.`
+ ));
+ return;
+ }
+
+ // Handle other HTTP errors
+ if (response.statusCode < 200 || response.statusCode >= 300) {
+ file.destroy();
+ fs.unlink(dest, () => {});
+ reject(new Error(
+ `HTTP ${response.statusCode}: Failed to download from ${url}`
+ ));
+ return;
+ }
+
+ response.pipe(file);
+
+ file.on('finish', () => {
+ file.close();
+ resolve();
+ });
+
+ file.on('error', (err) => {
+ fs.unlink(dest, () => {});
+ reject(new Error(`File write error: ${err.message}`));
+ });
+ }).on('error', (err) => {
+ file.destroy();
+ fs.unlink(dest, () => {});
+ reject(new Error(`Download error: ${err.message}`));
+ });
+ });
+}
+
+install();
diff --git a/packages/simplex-chat-nodejs/src/index.ts b/packages/simplex-chat-nodejs/src/index.ts
new file mode 100644
index 0000000000..80180ae282
--- /dev/null
+++ b/packages/simplex-chat-nodejs/src/index.ts
@@ -0,0 +1,22 @@
+/**
+ * A simple declarative API to run a chat-bot with a single function call.
+ * It automates creating and updating of the bot profile, address and bot commands shown in the app UI.
+ */
+export * as bot from "./bot"
+
+/**
+ * An API to send chat commands and receive chat events to/from chat core.
+ * You need to use it in bot event handlers, and for any other use cases.
+ */
+export * as api from "./api"
+
+/**
+ * A low level API to the core library - the same that is used in desktop clients.
+ * You are unlikely to ever need to use this module directly.
+ */
+export * as core from "./core"
+
+/**
+ * Useful functions for chat events and types.
+ */
+export * as util from "./util"
diff --git a/packages/simplex-chat-nodejs/src/simplex.d.ts b/packages/simplex-chat-nodejs/src/simplex.d.ts
new file mode 100644
index 0000000000..10c2f6608a
--- /dev/null
+++ b/packages/simplex-chat-nodejs/src/simplex.d.ts
@@ -0,0 +1,10 @@
+// These functions are defined in CPP add-on ../cpp/simplex.cc
+
+export function chat_migrate_init(dbPath: string, dbKey: string, confirm: string): Promise<[bigint, string]>
+export function chat_close_store(ctrl: bigint): Promise
+export function chat_send_cmd(ctrl: bigint, cmd: string): Promise
+export function chat_recv_msg_wait(ctrl: bigint, wait: number): Promise
+export function chat_write_file(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise
+export function chat_read_file(path: string, key: string, nonce: string): Promise
+export function chat_encrypt_file(ctrl: bigint, fromPath: string, toPath: string): Promise
+export function chat_decrypt_file(fromPath: string, key: string, nonce: string, toPath: string): Promise
diff --git a/packages/simplex-chat-nodejs/src/simplex.js b/packages/simplex-chat-nodejs/src/simplex.js
new file mode 100644
index 0000000000..fd26c67438
--- /dev/null
+++ b/packages/simplex-chat-nodejs/src/simplex.js
@@ -0,0 +1 @@
+module.exports = require("../build/Release/simplex")
diff --git a/packages/simplex-chat-nodejs/src/util.ts b/packages/simplex-chat-nodejs/src/util.ts
new file mode 100644
index 0000000000..52e7843a95
--- /dev/null
+++ b/packages/simplex-chat-nodejs/src/util.ts
@@ -0,0 +1,92 @@
+import {T} from "@simplex-chat/types"
+import {BotAddressSettings} from "./api"
+
+export function chatInfoRef(cInfo: T.ChatInfo): T.ChatRef | undefined {
+ switch (cInfo.type) {
+ case T.ChatType.Direct: return {chatType: T.ChatType.Direct, chatId: cInfo.contact.contactId}
+ case T.ChatType.Group: {
+ const chatScope: T.GroupChatScope | undefined =
+ cInfo.groupChatScope?.type == "memberSupport"
+ ? {type: "memberSupport", groupMemberId_: cInfo.groupChatScope.groupMember_?.groupMemberId}
+ : undefined
+ return {chatType: T.ChatType.Group, chatId: cInfo.groupInfo.groupId, chatScope}
+ }
+ default: return undefined
+ }
+}
+
+export function chatInfoName(cInfo: T.ChatInfo): string {
+ switch (cInfo.type) {
+ case "direct": return `@${cInfo.contact.profile.displayName}`
+ case "group": {
+ const scope = cInfo.groupChatScope
+ const scopeName = scope?.type === "memberSupport"
+ ? `(support${scope.groupMember_ ? ` ${scope.groupMember_.memberProfile.displayName}` : ""})`
+ : ""
+ return `#${cInfo.groupInfo.groupProfile.displayName}${scopeName}`
+ }
+ case "local": return "private notes"
+ case "contactRequest": return `request from @${cInfo.contactRequest.profile.displayName}`
+ case "contactConnection": {
+ const alias = cInfo.contactConnection.localAlias
+ return `pending connection${alias ? ` (@${alias})` : ""}`
+ }
+ }
+}
+
+export function senderName(cInfo: T.ChatInfo, chatDir: T.CIDirection) {
+ const sender = chatDir.type === "groupRcv"
+ ? ` @${chatDir.groupMember.memberProfile.displayName}`
+ : ""
+ return chatInfoName(cInfo) + sender
+}
+
+export function contactAddressStr(link: T.CreatedConnLink): string {
+ return link.connShortLink || link.connFullLink
+}
+
+export function botAddressSettings({addressSettings}: T.UserContactLink): BotAddressSettings {
+ return {
+ autoAccept: addressSettings.autoAccept ? true : false,
+ welcomeMessage: addressSettings.autoReply,
+ businessAddress: addressSettings.businessAddress
+ }
+}
+
+export function fromLocalProfile({displayName, fullName, shortDescr, image, contactLink, preferences, peerType}: T.LocalProfile): T.Profile {
+ const profile = {displayName, fullName, shortDescr, image, contactLink, preferences, peerType}
+ for (const key in profile) {
+ if (typeof (profile as any)[key] === "undefined") delete (profile as any)[key]
+ }
+ return profile
+}
+
+export function ciContentText({content}: T.ChatItem): string | undefined {
+ switch (content.type) {
+ case "sndMsgContent": return content.msgContent.text;
+ case "rcvMsgContent": return content.msgContent.text;
+ default: return undefined;
+ }
+}
+
+export interface BotCommand {
+ keyword: string
+ params: string
+}
+
+// returns command (without /) and trimmed parameters
+export function ciBotCommand(chatItem: T.ChatItem): BotCommand | undefined {
+ const msg = ciContentText(chatItem)?.trim()
+ if (msg) {
+ const r = msg.match(/\/([^\s]+)(.*)/)
+ if (r && r.length >= 3) {
+ return {keyword: r[1], params: r[2].trim()}
+ }
+ }
+ return undefined
+}
+
+export function reactionText(reaction: T.ACIReaction): string {
+ const r = reaction.chatReaction
+ return r.reaction.type === "emoji" ? r.reaction.emoji : r.reaction.tag
+}
diff --git a/packages/simplex-chat-nodejs/tests/api.test.ts b/packages/simplex-chat-nodejs/tests/api.test.ts
new file mode 100644
index 0000000000..52153ecfed
--- /dev/null
+++ b/packages/simplex-chat-nodejs/tests/api.test.ts
@@ -0,0 +1,67 @@
+import * as path from "path"
+import * as fs from "fs"
+import {CEvt, T} from "@simplex-chat/types"
+import {api} from ".."
+
+const CT = T.ChatType
+
+describe("API tests (use preset servers)", () => {
+ const tmpDir = "./tests/tmp"
+ const alicePath = path.join(tmpDir, "alice")
+ const bobPath = path.join(tmpDir, "bob")
+
+ beforeEach(() => fs.mkdirSync(tmpDir, {recursive: true}))
+ afterEach(() => fs.rmSync(tmpDir, {recursive: true, force: true}))
+
+ it("should send/receive message", async () => {
+ // create users and start chat controllers
+ const alice = await api.ChatApi.init(alicePath)
+ const bob = await api.ChatApi.init(bobPath)
+ const servers: string[] = []
+ let eventCount = 0
+ alice.on("hostConnected" as CEvt.Tag, async ({transportHost}: any) => { servers.push(transportHost) })
+ alice.onAny(async () => { eventCount++ })
+ await expect(alice.apiGetActiveUser()).resolves.toBeUndefined()
+ const aliceUser = await alice.apiCreateActiveUser({displayName: "alice", fullName: ""})
+ await expect(alice.apiGetActiveUser()).resolves.toMatchObject(aliceUser)
+ await bob.apiCreateActiveUser({displayName: "bob", fullName: ""})
+ await alice.startChat()
+ await bob.startChat()
+ // connect via link
+ const link = await alice.apiCreateLink(aliceUser.userId)
+ await expect(bob.apiConnectActiveUser(link)).resolves.toBe(api.ConnReqType.Invitation)
+ const [bobContact, aliceContact] = await Promise.all([
+ (await alice.wait("contactConnected")).contact,
+ (await bob.wait("contactConnected")).contact
+ ])
+ expect(bobContact).toMatchObject({profile: {displayName: "bob"}})
+ expect(aliceContact).toMatchObject({profile: {displayName: "alice"}})
+ // exchange messages
+ const isMessage = ({contactId}: T.Contact, msg: string) => (evt: CEvt.NewChatItems) =>
+ evt.chatItems.some(ci => ci.chatInfo.type === CT.Direct && ci.chatInfo.contact.contactId === contactId && ci.chatItem.meta.itemText === msg)
+ await alice.apiSendTextMessage([CT.Direct, bobContact.contactId], "hello")
+ await bob.wait("newChatItems", isMessage(aliceContact, "hello"))
+ await bob.apiSendTextMessage([CT.Direct, aliceContact.contactId], "hello too")
+ await alice.wait("newChatItems", isMessage(bobContact, "hello too"), 10000)
+ await alice.apiSendTextMessage([CT.Direct, bobContact.contactId], "how are you?")
+ await bob.wait("newChatItems", isMessage(aliceContact, "how are you?"))
+ await bob.apiSendTextMessage([CT.Direct, aliceContact.contactId], "ok, and you?")
+ await alice.wait("newChatItems", isMessage(bobContact, "ok, and you?"), 10000)
+ // no more messages
+ await expect(alice.wait("newChatItems", 500)).resolves.toBeUndefined()
+ await expect(bob.wait("newChatItems", 500)).resolves.toBeUndefined()
+ // delete contacts, stop chat controllers and close databases
+ await alice.apiDeleteChat(CT.Direct, bobContact.contactId)
+ await bob.wait("contactDeletedByContact")
+ await bob.apiDeleteChat(CT.Direct, aliceContact.contactId)
+ await alice.stopChat()
+ await bob.stopChat()
+ await alice.close()
+ await bob.close()
+ await expect(alice.startChat).rejects.toThrow()
+ await expect(bob.startChat).rejects.toThrow()
+ expect(servers.length).toBe(2)
+ expect(servers[0] !== servers[1]).toBe(true)
+ expect(eventCount > 0).toBe(true)
+ }, 30000)
+})
diff --git a/packages/simplex-chat-nodejs/tests/bot.test.ts b/packages/simplex-chat-nodejs/tests/bot.test.ts
new file mode 100644
index 0000000000..b1fd9d0186
--- /dev/null
+++ b/packages/simplex-chat-nodejs/tests/bot.test.ts
@@ -0,0 +1,60 @@
+import * as path from "path"
+import * as fs from "fs"
+import * as assert from "assert"
+import {CEvt, T} from "@simplex-chat/types"
+import {api, bot, util} from ".."
+
+const CT = T.ChatType
+
+describe("Bot tests (use preset servers)", () => {
+ const tmpDir = "./tests/tmp"
+ const botPath = path.join(tmpDir, "bot")
+ const alicePath = path.join(tmpDir, "alice")
+
+ beforeEach(() => fs.mkdirSync(tmpDir, {recursive: true}))
+ afterEach(() => fs.rmSync(tmpDir, {recursive: true, force: true}))
+
+ it("should reply to messages", async () => {
+ // run bot
+ const [chat, botUser, botAddress] = await bot.run({
+ profile: {displayName: "Squaring bot", fullName: ""},
+ dbOpts: {dbFilePrefix: botPath, dbKey: ""},
+ options: {
+ addressSettings: {welcomeMessage: "If you send me a number, I will calculate its square."},
+ },
+ onMessage: async (ci, content) => {
+ const n = +content.text
+ const reply = typeof n === "number" && !isNaN(n) ? `${n} * ${n} = ${n * n}` : `this is not a number`
+ await chat.apiSendTextReply(ci, reply)
+ }
+ })
+ assert(typeof botAddress === "object")
+ // create user
+ const alice = await api.ChatApi.init(alicePath)
+ const aliceUser = await alice.apiCreateActiveUser({displayName: "alice", fullName: ""})
+ await alice.startChat()
+ // connect to bot
+ const [plan, link] = await alice.apiConnectPlan(aliceUser.userId, util.contactAddressStr(botAddress.connLinkContact))
+ assert(plan.type === "contactAddress")
+ await expect(alice.apiConnect(aliceUser.userId, false, link)).resolves.toBe(api.ConnReqType.Contact)
+ const [botContact, aliceContact] = await Promise.all([
+ (await alice.wait("contactConnected")).contact,
+ (await chat.wait("contactConnected")).contact
+ ])
+ expect(botContact.profile.displayName).toBe("Squaring bot")
+ // send message to bot
+ const isMessage = ({contactId}: T.Contact, msg: string) => (evt: CEvt.NewChatItems) =>
+ evt.chatItems.some(ci => ci.chatInfo.type === CT.Direct && ci.chatInfo.contact.contactId === contactId && ci.chatItem.meta.itemText === msg)
+ await alice.apiSendTextMessage([CT.Direct, botContact.contactId], "2")
+ console.log("after sending message")
+ await alice.wait("newChatItems", isMessage(botContact, "2 * 2 = 4"), 5000)
+ // cleanup
+ await alice.apiDeleteChat(CT.Direct, botContact.contactId)
+ await chat.wait("contactDeletedByContact", ({contact}) => contact.contactId === aliceContact.contactId)
+ await chat.apiDeleteUserAddress(botUser.userId)
+ await chat.stopChat()
+ await chat.close()
+ await alice.stopChat()
+ await alice.close()
+ }, 30000)
+})
diff --git a/packages/simplex-chat-nodejs/tests/core.test.ts b/packages/simplex-chat-nodejs/tests/core.test.ts
new file mode 100644
index 0000000000..141f35746d
--- /dev/null
+++ b/packages/simplex-chat-nodejs/tests/core.test.ts
@@ -0,0 +1,85 @@
+import * as fs from "fs";
+import * as path from "path";
+import {core} from "../src/index";
+
+describe("Core tests", () => {
+ const tmpDir = "./tests/tmp";
+ const dbPath = path.join(tmpDir, "simplex_v1");
+
+ beforeEach(() => fs.mkdirSync(tmpDir, {recursive: true}));
+ afterEach(() => fs.rmSync(tmpDir, {recursive: true, force: true}));
+
+ it("should initialize chat controller", async () => {
+ const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);
+ expect(typeof ctrl).toBe("bigint");
+ await expect(core.chatCloseStore(ctrl)).resolves.toBe(undefined);
+
+ await expect(core.chatMigrateInit(dbPath, "wrong_key", core.MigrationConfirmation.YesUp)).rejects.toMatchObject({
+ message: "Database or migration error (see dbMigrationError property)",
+ dbMigrationError: expect.objectContaining({type: "errorNotADatabase"})
+ });
+ });
+
+ it("should send command and receive event", async () => {
+ const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);
+
+ await expect(core.chatSendCmd(ctrl, "/v")).resolves.toMatchObject({
+ type: "versionInfo"
+ });
+ await expect(core.chatSendCmd(ctrl, '/debug event {"type": "chatSuspended"}')).resolves.toMatchObject({
+ type: "cmdOk"
+ });
+
+ const wait = 500_000;
+ await expect(core.chatRecvMsgWait(ctrl, wait)).resolves.toMatchObject({
+ type: "chatSuspended"
+ });
+ await expect(core.chatRecvMsgWait(ctrl, wait)).resolves.toBe(undefined);
+
+ await expect(core.chatSendCmd(ctrl, "/unknown")).rejects.toMatchObject({
+ message: "Chat command error (see chatError property)",
+ chatError: expect.objectContaining({type: "error"})
+ });
+
+ await core.chatCloseStore(ctrl);
+ });
+
+ it("should write/read encrypted file from/to buffer", async () => {
+ const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);
+
+ const filePath = path.join(tmpDir, "write_file.txt");
+ const buffer = new Uint8Array([0, 1, 2]).buffer;
+ const cryptoArgs = await core.chatWriteFile(ctrl, filePath, buffer);
+ expect(typeof cryptoArgs.fileKey).toBe("string");
+ expect(typeof cryptoArgs.fileNonce).toBe("string");
+
+ const buffer2 = await core.chatReadFile(filePath, cryptoArgs);
+ expect(Buffer.from(buffer2).equals(Buffer.from(buffer))).toBe(true);
+
+ await expect(core.chatWriteFile(ctrl, path.join(tmpDir, "unknown", "unknown.txt"), buffer)).rejects.toThrow();
+ await expect(core.chatReadFile(path.join(tmpDir, "unknown.txt"), cryptoArgs)).rejects.toThrow();
+
+ await core.chatCloseStore(ctrl);
+ });
+
+ it("should encrypt/decrypt file", async () => {
+ const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp);
+
+ const unencryptedPath = path.join(tmpDir, "file_unencrypted.txt");
+ fs.writeFileSync(unencryptedPath, "unencrypted\n");
+ const encryptedPath = path.join(tmpDir, "file_encrypted.txt");
+ const cryptoArgs = await core.chatEncryptFile(ctrl, unencryptedPath, encryptedPath);
+ expect(typeof cryptoArgs.fileKey).toBe("string");
+ expect(typeof cryptoArgs.fileNonce).toBe("string");
+
+ const decryptedPath: string = path.join(tmpDir, "file_decrypted.txt");
+ await expect(core.chatDecryptFile(encryptedPath, cryptoArgs, decryptedPath)).resolves.toBe(undefined);
+
+ expect(fs.readFileSync(decryptedPath, "utf8")).toBe("unencrypted\n");
+
+ await expect(core.chatEncryptFile(ctrl, path.join(tmpDir, "unknown.txt"), encryptedPath)).rejects.toThrow();
+ await expect(core.chatDecryptFile(path.join(tmpDir, "unknown.txt"), cryptoArgs, decryptedPath)).rejects.toThrow();
+
+ await core.chatCloseStore(ctrl);
+ });
+});
diff --git a/packages/simplex-chat-nodejs/tests/tsconfig.json b/packages/simplex-chat-nodejs/tests/tsconfig.json
new file mode 100644
index 0000000000..47e973f3af
--- /dev/null
+++ b/packages/simplex-chat-nodejs/tests/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "types": ["node", "jest"]
+ },
+ "include": ["**/*", "../src/**/*"]
+}
diff --git a/packages/simplex-chat-nodejs/tsconfig.json b/packages/simplex-chat-nodejs/tsconfig.json
new file mode 100644
index 0000000000..f5dc986431
--- /dev/null
+++ b/packages/simplex-chat-nodejs/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "include": ["src"],
+ "compilerOptions": {
+ "declaration": true,
+ "forceConsistentCasingInFileNames": true,
+ "lib": ["ES2018"],
+ "module": "CommonJS",
+ "moduleResolution": "Node",
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitAny": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noEmitOnError": true,
+ "outDir": "dist",
+ "sourceMap": true,
+ "strict": true,
+ "strictNullChecks": true,
+ "target": "ES2018",
+ "types": ["node"]
+ }
+}
diff --git a/packages/simplex-chat-nodejs/typedoc.json b/packages/simplex-chat-nodejs/typedoc.json
new file mode 100644
index 0000000000..3523115f49
--- /dev/null
+++ b/packages/simplex-chat-nodejs/typedoc.json
@@ -0,0 +1,14 @@
+{
+ "name": "simplex-chat",
+ "plugin": ["typedoc-plugin-markdown"],
+ "entryPoints": [
+ "./src/index.ts",
+ "../simplex-chat-client/types/typescript/src/index.ts"
+ ],
+ "entryPointStrategy": "expand",
+ "tsconfig": "./tsconfig.json",
+ "sourceLinkTemplate": "../{path}#L{line}",
+ "disableGit": true,
+ "flattenOutputFiles": true,
+ "out": "./docs"
+}
\ No newline at end of file