diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fc6eee7..52d63c0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,7 @@ concurrency: jobs: build: runs-on: ${{ matrix.os }} + strategy: fail-fast: false matrix: @@ -49,6 +50,13 @@ jobs: run: | yarn global add node-gyp@latest + # - name: sed it + # if: runner.os == 'Windows' + # shell: bash + # run: | + # sed -i "s/target_compile_options(oxen-logging-warnings INTERFACE/#target_compile_options(oxen-logging-warnings INTERFACE/" libsession-util/external/oxen-libquic/external/oxen-logging/CMakeLists.txt + # cat libsession-util/external/oxen-libquic/external/oxen-logging/CMakeLists.txt + - name: build libsession-util-nodejs shell: bash - run: yarn install --frozen-lockfile --network-timeout 600000 + run: yarn install --frozen-lockfile diff --git a/.gitignore b/.gitignore index 0061e0c..31dd01f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ /package-lock.json /compile_commands.json /.cache +.yarn/ +*.cjs +*.mjs + diff --git a/.gitmodules b/.gitmodules index 8720b3c..d6ca3b1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "libsession-util"] path = libsession-util - url = https://github.com/oxen-io/libsession-util.git + url = https://github.com/session-foundation/libsession-util.git diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 index b162bd8..0000000 --- a/.yarnrc +++ /dev/null @@ -1 +0,0 @@ ---install.frozen-lockfile true diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..5fdaec4 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,3 @@ +nodeLinker: node-modules + +patchFolder: patches diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f885bc..0f540ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,19 @@ set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) set(VERBOSE ON) +# Detect the number of processors +include(ProcessorCount) +ProcessorCount(N) + +# Set a default value in case the detection fails +if(NOT N EQUAL 0) + set(CMAKE_BUILD_PARALLEL_LEVEL ${N}) +else() + set(CMAKE_BUILD_PARALLEL_LEVEL 4) # Fallback to 16 if detection fails +endif() +message(STATUS "Number of processors detected: ${N}") + + add_definitions(-DNAPI_VERSION=8) set(CMAKE_CONFIGURATION_TYPES Release) @@ -15,11 +28,15 @@ SET(CMAKE_EXPORT_COMPILE_COMMANDS ON) SET(CMAKE_BUILD_TYPE Release) SET(WITH_TESTS OFF) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -file(GLOB SOURCE_FILES src/*.cpp) +# when building from a release of libsession on desktop, it complains that ios-cmake is not up to date +# as it is not part of the archive. We actually don't care about it on session-desktop +set(SUBMODULE_CHECK OFF) + +file(GLOB SOURCE_FILES src/*.cpp src/groups/*.cpp src/multi_encrypt/*.cpp) add_subdirectory(libsession-util) @@ -33,7 +50,7 @@ add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC}) target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_JS_INC} "node_modules/node-addon-api" "../../node_modules/node-addon-api" "node_modules/node-api-headers/include" "../../node_modules/node-api-headers/include") set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") -target_link_libraries(${PROJECT_NAME} PRIVATE ${CMAKE_JS_LIB} libsession::config libsession::crypto) +target_link_libraries(${PROJECT_NAME} PRIVATE ${CMAKE_JS_LIB} ${LIBSESSION_STATIC_BUNDLE_LIBS}) if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) # Generate node.lib diff --git a/README.md b/README.md index d57b09f..e45c109 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Clone this project to somewhere **not** part of `session-desktop` node_modules: ``` cd [FOLDER_NOT_IN_SESSION_DESKTOP] -git clone --recursive git@github.com:oxen-io/libsession-util-nodejs.git +git clone --recursive git@github.com:session-foundation/libsession-util-nodejs.git ``` Always do your changes in `[FOLDER_NOT_IN_SESSION_DESKTOP]/libsession-util-nodejs`, never in the one under session-desktop's `node_modules` as you might override your local changes. @@ -31,7 +31,7 @@ Replace `[SESSION_DESKTOP_PATH]` with the full path to your `session-desktop` fo Every part of this command is needed and might need to be updated using your paths. Also, the `worker:libsession` needs to be recompiled too to include the just created .node file in itself. This is done by the `yarn build:workers` command. -Note: The `electron` property in the `config` object will need to be updated in the `package.json` every time we update `electron` package in [session-desktop](https://github.com/oxen-io/session-desktop/) so that the versions match. It is a node version, but not part of the official node docs. If you compiled the node module for an incorrect electron/node version you will get an error on `session-desktop` start. +Note: The `electron` property in the `config` object will need to be updated in the `package.json` every time we update `electron` package in [session-desktop](https://github.com/session-foundation/session-desktop/) so that the versions match. It is a node version, but not part of the official node docs. If you compiled the node module for an incorrect electron/node version you will get an error on `session-desktop` start. ### Making a Release and updating Session-desktop @@ -71,7 +71,7 @@ Once this is done, update the dependency on `session-desktop`. Make sure to remove the existing one first (with the include `yarn remove` below) as you might have messed up your `node_modules` doing the dev instructions. ``` -yarn remove libsession_util_nodejs && yarn add https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.15/libsession_util_nodejs-v0.1.15.tar.gz +yarn remove libsession_util_nodejs && yarn add https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.1.15/libsession_util_nodejs-v0.1.15.tar.gz ``` Keep in mind that you need to update the two version numbers (e.g. `0.1.15`) to the just created release version of this project. diff --git a/index.d.ts b/index.d.ts index 3b670f4..60dddc8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,4 @@ -/// -/// + /// declare module 'libsession_util_nodejs' { @@ -10,7 +9,7 @@ declare module 'libsession_util_nodejs' { * - one side are calls made by the webworker directly to the wrapper * - the other side are calls made by the renderer to the webworker (which should forward them to the wrapper) * - * We cannot pass unserializable data between those two, so we need to have a serializable way of calling one + * We cannot pass non serializable data between those two, so we need to have a serializable way of calling one * method of a wrapper with the required arguments. * Those serializable data, are `UserConfigActionsType` or just any of the `*ActionsType`. They are defined with a tuple of what each methods accepts on which wrapper with which argument. * diff --git a/libsession-util b/libsession-util index 0193c36..c29c934 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 0193c36e0dad461385d6407a00f33b7314e6d740 +Subproject commit c29c93457eb6abfdb9e13af378cc67a2dc68115d diff --git a/package.json b/package.json index 200ab01..9c3b46f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "main": "index.js", "name": "libsession_util_nodejs", "description": "Wrappers for the Session Util Library", - "version": "0.3.23", + "version": "0.4.4", "license": "GPL-3.0", "author": { "name": "Oxen Project", @@ -10,15 +10,16 @@ }, "scripts": { "clean": "rimraf .cache build", - "install": "cmake-js compile --runtime=electron --runtime-version=25.8.4 -p16 --CDSUBMODULE_CHECK=OFF --CDLOCAL_MIRROR=https://oxen.rocks/deps --CDENABLE_ONIONREQ=OFF" + "install": "cmake-js compile --runtime=electron --runtime-version=25.8.4 --CDSUBMODULE_CHECK=OFF --CDLOCAL_MIRROR=https://oxen.rocks/deps --CDENABLE_ONIONREQ=OFF --CDWITH_TESTS=OFF" }, "devDependencies": { "clang-format": "^1.8.0", "rimraf": "2.6.2" }, "dependencies": { - "cmake-js": "^7.2.1", + "cmake-js": "7.2.1", "node-addon-api": "^6.1.0" }, - "typings": "index.d.ts" -} \ No newline at end of file + "typings": "index.d.ts", + "packageManager": "yarn@1.22.19" +} diff --git a/shared.d.ts b/shared.d.ts deleted file mode 100644 index ef47215..0000000 --- a/shared.d.ts +++ /dev/null @@ -1,83 +0,0 @@ -declare module 'libsession_util_nodejs' { - /** - * - * Utilities - * - */ - - type AsyncWrapper any> = ( - ...args: Parameters - ) => Promise>; - - export type RecordOfFunctions = Record any>; - - type MakeWrapperActionCalls = { - [Property in keyof Type]: AsyncWrapper; - }; - - export type ProfilePicture = { - url: string | null; - key: Uint8Array | null; - }; - - export type PushConfigResult = { data: Uint8Array; seqno: number; hashes: Array }; - export type MergeSingle = { hash: string; data: Uint8Array }; - - type MakeActionCall = [B, ...Parameters]; - - /** - * - * Base Config wrapper logic - * - */ - - type BaseConfigWrapper = { - needsDump: () => boolean; - needsPush: () => boolean; - push: () => PushConfigResult; - dump: () => Uint8Array; - confirmPushed: (seqno: number, hash: string) => void; - merge: (toMerge: Array) => Array; // merge returns the array of hashes that merged correctly - storageNamespace: () => number; - currentHashes: () => Array; - }; - - export type BaseConfigActions = - | MakeActionCall - | MakeActionCall - | MakeActionCall - | MakeActionCall - | MakeActionCall - | MakeActionCall - | MakeActionCall - | MakeActionCall; - - export abstract class BaseConfigWrapperNode { - public needsDump: BaseConfigWrapper['needsDump']; - public needsPush: BaseConfigWrapper['needsPush']; - public push: BaseConfigWrapper['push']; - public dump: BaseConfigWrapper['dump']; - public confirmPushed: BaseConfigWrapper['confirmPushed']; - public merge: BaseConfigWrapper['merge']; - public storageNamespace: BaseConfigWrapper['storageNamespace']; - public currentHashes: BaseConfigWrapper['currentHashes']; - } - - export type BaseWrapperActionsCalls = MakeWrapperActionCalls; - - export type ConstantsType = { - /** 100 bytes */ - CONTACT_MAX_NAME_LENGTH: number; - /** 100 bytes - for legacy groups and communities */ - BASE_GROUP_MAX_NAME_LENGTH: number; - /** 100 bytes */ - GROUP_INFO_MAX_NAME_LENGTH: number; - /** 411 bytes - * - * BASE_URL_MAX_LENGTH + '/r/' + ROOM_MAX_LENGTH + qs_pubkey.size() + hex pubkey + null terminator - */ - COMMUNITY_FULL_URL_MAX_LENGTH: number; - }; - - export const CONSTANTS: ConstantsType; -} diff --git a/src/addon.cpp b/src/addon.cpp index 776a304..e7fe92c 100644 --- a/src/addon.cpp +++ b/src/addon.cpp @@ -4,19 +4,26 @@ #include "constants.hpp" #include "contacts_config.hpp" #include "convo_info_volatile_config.hpp" +#include "groups/meta_group_wrapper.hpp" +#include "multi_encrypt/multi_encrypt.hpp" #include "user_config.hpp" #include "user_groups_config.hpp" Napi::Object InitAll(Napi::Env env, Napi::Object exports) { using namespace session::nodeapi; - ConstantsWrapper::Init(env, exports); + + // Group wrappers init + MetaGroupWrapper::Init(env, exports); + + // User wrappers init UserConfigWrapper::Init(env, exports); ContactsConfigWrapper::Init(env, exports); UserGroupsWrapper::Init(env, exports); ConvoInfoVolatileWrapper::Init(env, exports); // Fully static wrappers init + MultiEncryptWrapper::Init(env, exports); BlindingWrapper::Init(env, exports); return exports; diff --git a/src/base_config.cpp b/src/base_config.cpp index ce4976e..57163f8 100644 --- a/src/base_config.cpp +++ b/src/base_config.cpp @@ -15,12 +15,6 @@ Napi::Value ConfigBaseImpl::needsPush(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { return get_config().needs_push(); }); } -Napi::Value ConfigBaseImpl::storageNamespace(const Napi::CallbackInfo& info) { - return wrapResult(info, [&] { - return static_cast(get_config().storage_namespace()); - }); -} - Napi::Value ConfigBaseImpl::currentHashes(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { return (get_config().current_hashes()); }); } @@ -28,15 +22,10 @@ Napi::Value ConfigBaseImpl::currentHashes(const Napi::CallbackInfo& info) { Napi::Value ConfigBaseImpl::push(const Napi::CallbackInfo& info) { return wrapResult(info, [&]() { assertInfoLength(info, 0); - auto [seqno, to_push, hashes] = get_config().push(); + auto& conf = get_config(); + auto to_push = conf.push(); - auto env = info.Env(); - Napi::Object result = Napi::Object::New(env); - result["data"] = toJs(env, to_push); - result["seqno"] = toJs(env, seqno); - result["hashes"] = toJs(env, hashes); - - return result; + return push_result_to_JS(info.Env(), to_push, conf.storage_namespace()); }); } @@ -47,10 +36,17 @@ Napi::Value ConfigBaseImpl::dump(const Napi::CallbackInfo& info) { }); } +Napi::Value ConfigBaseImpl::makeDump(const Napi::CallbackInfo& info) { + return wrapResult(info, [&]() { + assertInfoLength(info, 0); + return get_config().make_dump(); + }); +} + void ConfigBaseImpl::confirmPushed(const Napi::CallbackInfo& info) { return wrapResult(info, [&]() { assertInfoLength(info, 2); - assertIsNumber(info[0]); + assertIsNumber(info[0], "confirmPushed"); assertIsString(info[1]); get_config().confirm_pushed( diff --git a/src/base_config.hpp b/src/base_config.hpp index 4363520..d99250e 100644 --- a/src/base_config.hpp +++ b/src/base_config.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -11,6 +12,8 @@ #include "session/types.hpp" #include "utilities.hpp" +using ustring_view = std::basic_string_view; + namespace session::nodeapi { class ConfigBaseImpl; @@ -30,11 +33,11 @@ class ConfigBaseImpl { // These are exposed as read-only accessors rather than methods: Napi::Value needsDump(const Napi::CallbackInfo& info); Napi::Value needsPush(const Napi::CallbackInfo& info); - Napi::Value storageNamespace(const Napi::CallbackInfo& info); Napi::Value currentHashes(const Napi::CallbackInfo& info); Napi::Value push(const Napi::CallbackInfo& info); Napi::Value dump(const Napi::CallbackInfo& info); + Napi::Value makeDump(const Napi::CallbackInfo& info); void confirmPushed(const Napi::CallbackInfo& info); Napi::Value merge(const Napi::CallbackInfo& info); @@ -50,11 +53,10 @@ class ConfigBaseImpl { properties.push_back(T::InstanceMethod("needsDump", &T::needsDump)); properties.push_back(T::InstanceMethod("needsPush", &T::needsPush)); - properties.push_back(T::InstanceMethod("storageNamespace", &T::storageNamespace)); properties.push_back(T::InstanceMethod("currentHashes", &T::currentHashes)); - properties.push_back(T::InstanceMethod("push", &T::push)); properties.push_back(T::InstanceMethod("dump", &T::dump)); + properties.push_back(T::InstanceMethod("makeDump", &T::makeDump)); properties.push_back(T::InstanceMethod("confirmPushed", &T::confirmPushed)); properties.push_back(T::InstanceMethod("merge", &T::merge)); @@ -90,7 +92,7 @@ class ConfigBaseImpl { assertInfoLength(info, 2); // we should get secret key as first arg and optional dumped as second argument - assertIsUInt8Array(info[0]); + assertIsUInt8Array(info[0], "base construct"); assertIsUInt8ArrayOrNull(info[1]); ustring_view secretKey = toCppBufferView(info[0], class_name + ".new"); @@ -104,18 +106,6 @@ class ConfigBaseImpl { Napi::Env env = info.Env(); - config->logger = [env, class_name](session::config::LogLevel, std::string_view x) { - std::string toLog = - "libsession-util:" + std::string(class_name) + ": " + std::string(x) + "\n"; - - Napi::Function consoleLog = env.Global() - .Get("console") - .As() - .Get("log") - .As(); - consoleLog.Call({Napi::String::New(env, toLog)}); - }; - return config; }); } @@ -135,18 +125,6 @@ class ConfigBaseImpl { "Error retrieving config: config instance is not of the requested type"}; } - // Same as above, but return a shared ptr. - template < - typename T = config::ConfigBase, - std::enable_if_t, int> = 0> - std::shared_ptr config_ptr() { - assert(conf_); // should not be possible to construct without this set - if (auto t = std::dynamic_pointer_cast(conf_)) - return t; - throw std::invalid_argument{ - "Error retrieving config: config instance is not of the requested type"}; - } - // Helper function for doing the subtype napi Init call. This sets up the class registration, // sets it in the exports, and appends the base methods and properties (needsDump, etc.) to the // given methods/properties list. diff --git a/src/blinding/blinding.hpp b/src/blinding/blinding.hpp index 88d4f62..d244696 100644 --- a/src/blinding/blinding.hpp +++ b/src/blinding/blinding.hpp @@ -19,7 +19,7 @@ class BlindingWrapper : public Napi::ObjectWrap { public: BlindingWrapper(const Napi::CallbackInfo& info) : Napi::ObjectWrap{info} { throw std::invalid_argument( - "BlindingWrapper is all static and don't need to be constructed"); + "BlindingWrapper is static and doesn't need to be constructed"); } static void Init(Napi::Env env, Napi::Object exports) { @@ -50,7 +50,7 @@ class BlindingWrapper : public Napi::ObjectWrap { if (obj.IsEmpty()) throw std::invalid_argument("blindVersionPubkey received empty"); - assertIsUInt8Array(obj.Get("ed25519SecretKey")); + assertIsUInt8Array(obj.Get("ed25519SecretKey"), "BlindingWrapper::blindVersionPubkey"); auto ed25519_secret_key = toCppBuffer(obj.Get("ed25519SecretKey"), "blindVersionPubkey.ed25519SecretKey"); @@ -76,11 +76,11 @@ class BlindingWrapper : public Napi::ObjectWrap { if (obj.IsEmpty()) throw std::invalid_argument("blindVersionSign received empty"); - assertIsUInt8Array(obj.Get("ed25519SecretKey")); + assertIsUInt8Array(obj.Get("ed25519SecretKey"), "BlindingWrapper::blindVersionSign"); auto ed25519_secret_key = toCppBuffer(obj.Get("ed25519SecretKey"), "blindVersionSign.ed25519SecretKey"); - assertIsNumber(obj.Get("sigTimestampSeconds")); + assertIsNumber(obj.Get("sigTimestampSeconds"), "BlindingWrapper::blindVersionSign"); auto sig_timestamp = toCppInteger( obj.Get("sigTimestampSeconds"), "blindVersionSign.sigTimestampSeconds", false); diff --git a/src/contacts_config.cpp b/src/contacts_config.cpp index a1a0739..19c9593 100644 --- a/src/contacts_config.cpp +++ b/src/contacts_config.cpp @@ -49,7 +49,7 @@ struct toJs_impl { obj["createdAtSeconds"] = toJs(env, contact.created); obj["expirationMode"] = toJs(env, expiration_mode_string(contact.exp_mode)); obj["expirationTimerSeconds"] = toJs(env, contact.exp_timer.count()); - obj["profilePicture"] = object_from_profile_pic(env, contact.profile_picture); + obj["profilePicture"] = toJs(env, contact.profile_picture); return obj; } diff --git a/src/convo_info_volatile_config.cpp b/src/convo_info_volatile_config.cpp index ddf3991..9cf82dd 100644 --- a/src/convo_info_volatile_config.cpp +++ b/src/convo_info_volatile_config.cpp @@ -50,6 +50,19 @@ struct toJs_impl : toJs_impl { } }; +template <> +struct toJs_impl { + Napi::Object operator()(const Napi::Env& env, const convo::group group_info) { + auto obj = Napi::Object::New(env); + + obj["pubkeyHex"] = toJs(env, group_info.id); + obj["unread"] = toJs(env, group_info.unread); + obj["lastRead"] = toJs(env, group_info.last_read); + + return obj; + } +}; + void ConvoInfoVolatileWrapper::Init(Napi::Env env, Napi::Object exports) { InitHelper( env, @@ -69,6 +82,12 @@ void ConvoInfoVolatileWrapper::Init(Napi::Env env, Napi::Object exports) { InstanceMethod("setLegacyGroup", &ConvoInfoVolatileWrapper::setLegacyGroup), InstanceMethod("eraseLegacyGroup", &ConvoInfoVolatileWrapper::eraseLegacyGroup), + // group related methods + InstanceMethod("getGroup", &ConvoInfoVolatileWrapper::getGroup), + InstanceMethod("getAllGroups", &ConvoInfoVolatileWrapper::getAllGroups), + InstanceMethod("setGroup", &ConvoInfoVolatileWrapper::setGroup), + InstanceMethod("eraseGroup", &ConvoInfoVolatileWrapper::eraseGroup), + // communities related methods InstanceMethod("getCommunity", &ConvoInfoVolatileWrapper::getCommunity), InstanceMethod( @@ -107,7 +126,7 @@ void ConvoInfoVolatileWrapper::set1o1(const Napi::CallbackInfo& info) { assertIsString(first); auto second = info[1]; - assertIsNumber(second); + assertIsNumber(second, "set1o1"); auto third = info[2]; assertIsBoolean(third); @@ -123,6 +142,10 @@ void ConvoInfoVolatileWrapper::set1o1(const Napi::CallbackInfo& info) { }); } +Napi::Value ConvoInfoVolatileWrapper::erase1o1(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { return config.erase_1to1(getStringArgs<1>(info)); }); +} + /** * ================================================= * ================= Legacy groups ================= @@ -144,7 +167,7 @@ void ConvoInfoVolatileWrapper::setLegacyGroup(const Napi::CallbackInfo& info) { auto first = info[0]; assertIsString(first); auto second = info[1]; - assertIsNumber(second); + assertIsNumber(second, "setLegacyGroup"); auto third = info[2]; assertIsBoolean(third); @@ -166,8 +189,45 @@ Napi::Value ConvoInfoVolatileWrapper::eraseLegacyGroup(const Napi::CallbackInfo& return wrapResult(info, [&] { return config.erase_legacy_group(getStringArgs<1>(info)); }); } -Napi::Value ConvoInfoVolatileWrapper::erase1o1(const Napi::CallbackInfo& info) { - return wrapResult(info, [&] { return config.erase_1to1(getStringArgs<1>(info)); }); +/** + * ================================================= + * ===================== Groups ==================== + * ================================================= + */ + +Napi::Value ConvoInfoVolatileWrapper::getGroup(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { return config.get_group(getStringArgs<1>(info)); }); +} + +Napi::Value ConvoInfoVolatileWrapper::getAllGroups(const Napi::CallbackInfo& info) { + return get_all_impl(info, config.size_groups(), config.begin_groups(), config.end()); +} + +void ConvoInfoVolatileWrapper::setGroup(const Napi::CallbackInfo& info) { + wrapExceptions(info, [&] { + assertInfoLength(info, 3); + auto first = info[0]; + assertIsString(first); + auto second = info[1]; + assertIsNumber(second, "setGroup"); + + auto third = info[2]; + assertIsBoolean(third); + + auto convo = config.get_or_construct_group(toCppString(first, "convoInfo.setGroup1")); + + if (auto last_read = toCppInteger(second, "convoInfo.setGroup2"); + last_read > convo.last_read) + convo.last_read = last_read; + + convo.unread = toCppBoolean(third, "convoInfo.setGroup3"); + + config.set(convo); + }); +} + +Napi::Value ConvoInfoVolatileWrapper::eraseGroup(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { return config.erase_group(getStringArgs<1>(info)); }); } /** @@ -194,7 +254,7 @@ void ConvoInfoVolatileWrapper::setCommunityByFullUrl(const Napi::CallbackInfo& i assertIsString(first); auto second = info[1]; - assertIsNumber(second); + assertIsNumber(second, "setCommunityByFullUrl"); auto third = info[2]; assertIsBoolean(third); diff --git a/src/convo_info_volatile_config.hpp b/src/convo_info_volatile_config.hpp index 2c491fc..57191ff 100644 --- a/src/convo_info_volatile_config.hpp +++ b/src/convo_info_volatile_config.hpp @@ -29,6 +29,12 @@ class ConvoInfoVolatileWrapper : public ConfigBaseImpl, void setLegacyGroup(const Napi::CallbackInfo& info); Napi::Value eraseLegacyGroup(const Napi::CallbackInfo& info); + // group related methods + Napi::Value getGroup(const Napi::CallbackInfo& info); + Napi::Value getAllGroups(const Napi::CallbackInfo& info); + void setGroup(const Napi::CallbackInfo& info); + Napi::Value eraseGroup(const Napi::CallbackInfo& info); + // communities related methods Napi::Value getCommunity(const Napi::CallbackInfo& info); Napi::Value getAllCommunities(const Napi::CallbackInfo& info); diff --git a/src/groups/meta_group.hpp b/src/groups/meta_group.hpp new file mode 100644 index 0000000..a11d7be --- /dev/null +++ b/src/groups/meta_group.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include "session/config/groups/info.hpp" +#include "session/config/groups/keys.hpp" +#include "session/config/groups/members.hpp" + +namespace session::nodeapi { + +using config::groups::Info; +using config::groups::Keys; +using config::groups::Members; +using session::config::profile_pic; +using std::pair; +using std::string; +using std::tuple; +using std::vector; + +using std::make_shared; +using std::shared_ptr; + +class MetaGroup { + public: + shared_ptr info; + shared_ptr members; + shared_ptr keys; + string edGroupPubKey; + std::optional edGroupSecKey; + + MetaGroup( + shared_ptr info, + shared_ptr members, + shared_ptr keys, + session::ustring edGroupPubKey, + std::optional edGroupSecKey) : + info{info}, members{members}, keys{keys}, edGroupPubKey{oxenc::to_hex(edGroupPubKey)} { + + if (edGroupSecKey.has_value()) { + this->edGroupSecKey = oxenc::to_hex(*edGroupSecKey); + } else { + this->edGroupSecKey = std::nullopt; + } + } +}; +} // namespace session::nodeapi \ No newline at end of file diff --git a/src/groups/meta_group_wrapper.cpp b/src/groups/meta_group_wrapper.cpp new file mode 100644 index 0000000..c6e2752 --- /dev/null +++ b/src/groups/meta_group_wrapper.cpp @@ -0,0 +1,775 @@ +#include "meta_group_wrapper.hpp" + +#include + +#include + +#include "oxenc/bt_producer.h" +#include "session/types.hpp" + +namespace session::nodeapi { + +MetaGroupWrapper::MetaGroupWrapper(const Napi::CallbackInfo& info) : + meta_group{std::move(MetaBaseWrapper::constructGroupWrapper(info, "MetaGroupWrapper"))}, + Napi::ObjectWrap{info} {} + +void MetaGroupWrapper::Init(Napi::Env env, Napi::Object exports) { + MetaBaseWrapper::NoBaseClassInitHelper( + env, + exports, + "MetaGroupWrapperNode", + { + // shared exposed functions + InstanceMethod("needsPush", &MetaGroupWrapper::needsPush), + InstanceMethod("push", &MetaGroupWrapper::push), + InstanceMethod("needsDump", &MetaGroupWrapper::needsDump), + InstanceMethod("metaDump", &MetaGroupWrapper::metaDump), + InstanceMethod("metaMakeDump", &MetaGroupWrapper::metaMakeDump), + InstanceMethod("metaConfirmPushed", &MetaGroupWrapper::metaConfirmPushed), + InstanceMethod("metaMerge", &MetaGroupWrapper::metaMerge), + + // infos exposed functions + InstanceMethod("infoGet", &MetaGroupWrapper::infoGet), + InstanceMethod("infoSet", &MetaGroupWrapper::infoSet), + InstanceMethod("infoDestroy", &MetaGroupWrapper::infoDestroy), + + // members exposed functions + InstanceMethod("memberGet", &MetaGroupWrapper::memberGet), + InstanceMethod("memberGetOrConstruct", &MetaGroupWrapper::memberGetOrConstruct), + InstanceMethod( + "memberConstructAndSet", &MetaGroupWrapper::memberConstructAndSet), + InstanceMethod("memberGetAll", &MetaGroupWrapper::memberGetAll), + InstanceMethod( + "memberGetAllPendingRemovals", + &MetaGroupWrapper::memberGetAllPendingRemovals), + InstanceMethod( + "membersMarkPendingRemoval", + &MetaGroupWrapper::membersMarkPendingRemoval), + InstanceMethod( + "memberSetNameTruncated", &MetaGroupWrapper::memberSetNameTruncated), + InstanceMethod("memberSetInvited", &MetaGroupWrapper::memberSetInvited), + InstanceMethod("memberSetAccepted", &MetaGroupWrapper::memberSetAccepted), + InstanceMethod("memberSetPromoted", &MetaGroupWrapper::memberSetPromoted), + InstanceMethod( + "memberSetPromotionSent", &MetaGroupWrapper::memberSetPromotionSent), + InstanceMethod( + "memberSetPromotionFailed", + &MetaGroupWrapper::memberSetPromotionFailed), + InstanceMethod( + "memberSetPromotionAccepted", + &MetaGroupWrapper::memberSetPromotionAccepted), + InstanceMethod( + "memberSetProfilePicture", &MetaGroupWrapper::memberSetProfilePicture), + InstanceMethod("memberEraseAndRekey", &MetaGroupWrapper::memberEraseAndRekey), + + // keys exposed functions + InstanceMethod("keysNeedsRekey", &MetaGroupWrapper::keysNeedsRekey), + InstanceMethod("keyRekey", &MetaGroupWrapper::keyRekey), + InstanceMethod("keyGetAll", &MetaGroupWrapper::keyGetAll), + InstanceMethod("currentHashes", &MetaGroupWrapper::currentHashes), + InstanceMethod("loadKeyMessage", &MetaGroupWrapper::loadKeyMessage), + InstanceMethod("keyGetCurrentGen", &MetaGroupWrapper::keyGetCurrentGen), + InstanceMethod("encryptMessages", &MetaGroupWrapper::encryptMessages), + InstanceMethod("decryptMessage", &MetaGroupWrapper::decryptMessage), + InstanceMethod("makeSwarmSubAccount", &MetaGroupWrapper::makeSwarmSubAccount), + InstanceMethod("swarmSubAccountToken", &MetaGroupWrapper::swarmSubAccountToken), + InstanceMethod( + "swarmVerifySubAccount", &MetaGroupWrapper::swarmVerifySubAccount), + InstanceMethod("loadAdminKeys", &MetaGroupWrapper::loadAdminKeys), + InstanceMethod("keysAdmin", &MetaGroupWrapper::keysAdmin), + InstanceMethod("swarmSubaccountSign", &MetaGroupWrapper::swarmSubaccountSign), + InstanceMethod( + "generateSupplementKeys", &MetaGroupWrapper::generateSupplementKeys), + }); +} + +/* #region SHARED ACTIONS */ + +Napi::Value MetaGroupWrapper::needsPush(const Napi::CallbackInfo& info) { + + return wrapResult(info, [&] { + return this->meta_group->members->needs_push() || this->meta_group->info->needs_push() || + this->meta_group->keys->pending_config(); + }); +} + +Napi::Value MetaGroupWrapper::push(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + auto env = info.Env(); + auto to_push = Napi::Object::New(env); + + if (this->meta_group->members->needs_push()) + to_push["groupMember"s] = push_result_to_JS( + env, + this->meta_group->members->push(), + this->meta_group->members->storage_namespace()); + else + to_push["groupMember"s] = env.Null(); + + if (this->meta_group->info->needs_push()) + to_push["groupInfo"s] = push_result_to_JS( + env, + this->meta_group->info->push(), + this->meta_group->info->storage_namespace()); + else + to_push["groupInfo"s] = env.Null(); + + if (auto pending_config = this->meta_group->keys->pending_config()) + to_push["groupKeys"s] = push_key_entry_to_JS( + env, *(pending_config), this->meta_group->keys->storage_namespace()); + else + to_push["groupKeys"s] = env.Null(); + + return to_push; + }); +} + +Napi::Value MetaGroupWrapper::needsDump(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + return this->meta_group->members->needs_dump() || this->meta_group->info->needs_dump() || + this->meta_group->keys->needs_dump(); + }); +} + +Napi::Value MetaGroupWrapper::metaDump(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + oxenc::bt_dict_producer combined; + + // NOTE: the keys have to be in ascii-sorted order: + combined.append("info", session::from_unsigned_sv(this->meta_group->info->dump())); + combined.append("keys", session::from_unsigned_sv(this->meta_group->keys->dump())); + combined.append("members", session::from_unsigned_sv(this->meta_group->members->dump())); + auto to_dump = std::move(combined).str(); + + return session::ustring{to_unsigned_sv(to_dump)}; + }); +} + +Napi::Value MetaGroupWrapper::metaMakeDump(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + oxenc::bt_dict_producer combined; + + // NOTE: the keys have to be in ascii-sorted order: + combined.append("info", session::from_unsigned_sv(this->meta_group->info->make_dump())); + combined.append("keys", session::from_unsigned_sv(this->meta_group->keys->make_dump())); + combined.append( + "members", session::from_unsigned_sv(this->meta_group->members->make_dump())); + auto to_dump = std::move(combined).str(); + + return ustring{to_unsigned_sv(to_dump)}; + }); +} + +void MetaGroupWrapper::metaConfirmPushed(const Napi::CallbackInfo& info) { + wrapExceptions(info, [&]() { + assertInfoLength(info, 1); + auto arg = info[0]; + assertIsObject(arg); + auto obj = arg.As(); + + auto groupInfo = obj.Get("groupInfo"); + auto groupMember = obj.Get("groupMember"); + + if (!groupInfo.IsNull() && !groupInfo.IsUndefined()) { + assertIsArray(groupInfo); + auto groupInfoArr = groupInfo.As(); + if (groupInfoArr.Length() != 2) { + throw std::invalid_argument("groupInfoArr length was not 2"); + } + + auto seqno = maybeNonemptyInt( + groupInfoArr.Get("0"), "MetaGroupWrapper::metaConfirmPushed groupInfo seqno"); + auto hash = maybeNonemptyString( + groupInfoArr.Get("1"), "MetaGroupWrapper::metaConfirmPushed groupInfo hash"); + if (seqno && hash) + this->meta_group->info->confirm_pushed(*seqno, *hash); + } + + if (!groupMember.IsNull() && !groupMember.IsUndefined()) { + assertIsArray(groupMember); + auto groupMemberArr = groupMember.As(); + if (groupMemberArr.Length() != 2) { + throw std::invalid_argument("groupMemberArr length was not 2"); + } + + auto seqno = maybeNonemptyInt( + groupMemberArr.Get("0"), + "MetaGroupWrapper::metaConfirmPushed groupMemberArr seqno"); + auto hash = maybeNonemptyString( + groupMemberArr.Get("1"), + "MetaGroupWrapper::metaConfirmPushed groupMemberArr hash"); + if (seqno && hash) + this->meta_group->members->confirm_pushed(*seqno, *hash); + } + }); +}; + +Napi::Value MetaGroupWrapper::metaMerge(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + auto arg = info[0]; + assertIsObject(arg); + auto obj = arg.As(); + + auto groupInfo = obj.Get("groupInfo"); + auto groupMember = obj.Get("groupMember"); + auto groupKeys = obj.Get("groupKeys"); + + auto count_merged = 0; + + // Note: we need to process keys first as they might allow us the incoming info+members + // details + if (!groupKeys.IsNull() && !groupKeys.IsUndefined()) { + assertIsArray(groupKeys); + auto asArr = groupKeys.As(); + + for (uint32_t i = 0; i < asArr.Length(); i++) { + Napi::Value item = asArr[i]; + assertIsObject(item); + if (item.IsEmpty()) + throw std::invalid_argument("MetaMerge.item groupKeys received empty"); + + Napi::Object itemObject = item.As(); + assertIsString(itemObject.Get("hash")); + assertIsUInt8Array(itemObject.Get("data"), "groupKeys merge"); + assertIsNumber(itemObject.Get("timestampMs"), "timestampMs groupKeys"); + + auto hash = toCppString(itemObject.Get("hash"), "meta.merge keys hash"); + auto data = toCppBuffer(itemObject.Get("data"), "meta.merge keys data"); + auto timestamp_ms = toCppInteger( + itemObject.Get("timestampMs"), "meta.merge keys timestampMs", false); + + this->meta_group->keys->load_key_message( + hash, + data, + timestamp_ms, + *(this->meta_group->info), + *(this->meta_group->members)); + count_merged++; // load_key_message doesn't necessarely merge something as not + // all keys are for us. + } + } + + if (!groupInfo.IsNull() && !groupInfo.IsUndefined()) { + assertIsArray(groupInfo); + auto asArr = groupInfo.As(); + + std::vector> conf_strs; + conf_strs.reserve(asArr.Length()); + + for (uint32_t i = 0; i < asArr.Length(); i++) { + Napi::Value item = asArr[i]; + assertIsObject(item); + if (item.IsEmpty()) + throw std::invalid_argument("MetaMerge.item groupInfo received empty"); + + Napi::Object itemObject = item.As(); + assertIsString(itemObject.Get("hash")); + assertIsUInt8Array(itemObject.Get("data"), "groupInfo merge"); + conf_strs.emplace_back( + toCppString(itemObject.Get("hash"), "meta.merge"), + toCppBufferView(itemObject.Get("data"), "meta.merge")); + } + + auto info_merged = this->meta_group->info->merge(conf_strs); + count_merged += info_merged.size(); + } + + if (!groupMember.IsNull() && !groupMember.IsUndefined()) { + assertIsArray(groupMember); + auto asArr = groupMember.As(); + + std::vector> conf_strs; + conf_strs.reserve(asArr.Length()); + + for (uint32_t i = 0; i < asArr.Length(); i++) { + Napi::Value item = asArr[i]; + assertIsObject(item); + if (item.IsEmpty()) + throw std::invalid_argument("MetaMerge.item groupMember received empty"); + + Napi::Object itemObject = item.As(); + assertIsString(itemObject.Get("hash")); + assertIsUInt8Array(itemObject.Get("data"), "groupMember merge"); + conf_strs.emplace_back( + toCppString(itemObject.Get("hash"), "meta.merge"), + toCppBufferView(itemObject.Get("data"), "meta.merge")); + } + + auto member_merged = this->meta_group->members->merge(conf_strs); + + count_merged += member_merged.size(); + } + + if (this->meta_group->keys->needs_rekey()) { + this->meta_group->keys->rekey(*(this->meta_group->info), *(this->meta_group->members)); + } + return count_merged; + }); +} + +/* #endregion */ + +/* #region INFO ACTIONS */ + +Napi::Value MetaGroupWrapper::infoGet(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + auto env = info.Env(); + auto obj = Napi::Object::New(env); + + obj["name"] = toJs(env, this->meta_group->info->get_name()); + obj["createdAtSeconds"] = toJs(env, this->meta_group->info->get_created()); + obj["deleteAttachBeforeSeconds"] = + toJs(env, this->meta_group->info->get_delete_attach_before()); + obj["deleteBeforeSeconds"] = toJs(env, this->meta_group->info->get_delete_before()); + + if (auto expiry = this->meta_group->info->get_expiry_timer(); expiry) + obj["expirySeconds"] = toJs(env, expiry->count()); + else + obj["expirySeconds"] = env.Null(); + + obj["isDestroyed"] = toJs(env, this->meta_group->info->is_destroyed()); + obj["profilePicture"] = toJs(env, this->meta_group->info->get_profile_pic()); + + return obj; + }); +} + +Napi::Value MetaGroupWrapper::infoSet(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + auto arg = info[0]; + assertIsObject(arg); + auto obj = arg.As(); + + if (auto name = maybeNonemptyString(obj.Get("name"), "MetaGroupWrapper::setInfo name")) + this->meta_group->info->set_name(*name); + + if (auto created = maybeNonemptyInt( + obj.Get("createdAtSeconds"), "MetaGroupWrapper::setInfo set_created")) + this->meta_group->info->set_created(std::move(*created)); + + if (auto expiry = maybeNonemptyInt( + obj.Get("expirySeconds"), "MetaGroupWrapper::setInfo set_expiry_timer")) + this->meta_group->info->set_expiry_timer(std::chrono::seconds{*expiry}); + + if (auto deleteBefore = maybeNonemptyInt( + obj.Get("deleteBeforeSeconds"), "MetaGroupWrapper::setInfo set_delete_before")) + this->meta_group->info->set_delete_before(std::move(*deleteBefore)); + + if (auto deleteAttachBefore = maybeNonemptyInt( + obj.Get("deleteAttachBeforeSeconds"), + "MetaGroupWrapper::setInfo set_delete_attach_before")) + this->meta_group->info->set_delete_attach_before(std::move(*deleteAttachBefore)); + + if (auto profilePicture = obj.Get("profilePicture")) { + auto profilePic = profile_pic_from_object(profilePicture); + this->meta_group->info->set_profile_pic(profilePic); + } + + return this->infoGet(info); + }); +} + +Napi::Value MetaGroupWrapper::infoDestroy(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + meta_group->info->destroy_group(); + return this->infoGet(info); + }); +} + +/* #endregion */ + +/* #region MEMBERS ACTIONS */ + +Napi::Value MetaGroupWrapper::memberGetAll(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + std::vector allMembers; + for (auto& member : *this->meta_group->members) { + allMembers.push_back(member); + } + return allMembers; + }); +} + +Napi::Value MetaGroupWrapper::memberGetAllPendingRemovals(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + std::vector allMembersRemoved; + for (auto& member : *this->meta_group->members) { + if (member.is_removed()) { + allMembersRemoved.push_back(member); + } + } + return allMembersRemoved; + }); +} + +Napi::Value MetaGroupWrapper::memberGet(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsString(info[0]); + + auto pubkeyHex = toCppString(info[0], "memberGet"); + return meta_group->members->get(pubkeyHex); + }); +} + +Napi::Value MetaGroupWrapper::memberGetOrConstruct(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsString(info[0]); + + auto pubkeyHex = toCppString(info[0], "memberGetOrConstruct"); + return meta_group->members->get_or_construct(pubkeyHex); + }); +} + +Napi::Value MetaGroupWrapper::memberConstructAndSet(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsString(info[0]); + + auto pubkeyHex = toCppString(info[0], "memberConstructAndSet"); + auto created = meta_group->members->get_or_construct(pubkeyHex); + meta_group->members->set(created); + return created; + }); +} + +void MetaGroupWrapper::memberSetNameTruncated(const Napi::CallbackInfo& info) { + wrapExceptions(info, [&] { + assertIsString(info[0]); + assertIsString(info[1]); + + auto pubkeyHex = toCppString(info[0], "memberSetNameTruncated pubkeyHex"); + auto newName = toCppString(info[1], "memberSetNameTruncated newName"); + auto m = this->meta_group->members->get(pubkeyHex); + if (m) { + m->set_name(newName); + this->meta_group->members->set(*m); + } + }); +} + +void MetaGroupWrapper::memberSetInvited(const Napi::CallbackInfo& info) { + wrapExceptions(info, [&] { + assertIsString(info[0]); + assertIsBoolean(info[1]); + auto pubkeyHex = toCppString(info[0], "memberSetInvited"); + auto failed = toCppBoolean(info[1], "memberSetInvited"); + + auto m = this->meta_group->members->get(pubkeyHex); + if (m) { + m->set_invited(failed); + this->meta_group->members->set(*m); + } + }); +} + +void MetaGroupWrapper::memberSetAccepted(const Napi::CallbackInfo& info) { + wrapExceptions(info, [&] { + assertInfoLength(info, 1); + assertIsString(info[0]); + + auto pubkeyHex = toCppString(info[0], "memberSetAccepted"); + auto m = this->meta_group->members->get(pubkeyHex); + if (m) { + m->set_accepted(); + this->meta_group->members->set(*m); + } + }); +} + +void MetaGroupWrapper::memberSetPromoted(const Napi::CallbackInfo& info) { + wrapExceptions(info, [&] { + assertInfoLength(info, 1); + assertIsString(info[0]); + auto pubkeyHex = toCppString(info[0], "memberSetPromoted"); + auto m = this->meta_group->members->get(pubkeyHex); + if (m) { + m->set_promoted(); + this->meta_group->members->set(*m); + } + }); +} + +void MetaGroupWrapper::memberSetPromotionSent(const Napi::CallbackInfo& info) { + wrapExceptions(info, [&] { + assertInfoLength(info, 1); + assertIsString(info[0]); + auto pubkeyHex = toCppString(info[0], "memberSetPromotionSent"); + auto m = this->meta_group->members->get(pubkeyHex); + if (m) { + m->set_promotion_sent(); + this->meta_group->members->set(*m); + } + }); +} + +void MetaGroupWrapper::memberSetPromotionFailed(const Napi::CallbackInfo& info) { + wrapExceptions(info, [&] { + assertInfoLength(info, 1); + assertIsString(info[0]); + auto pubkeyHex = toCppString(info[0], "memberSetPromotionFailed"); + auto m = this->meta_group->members->get(pubkeyHex); + if (m) { + m->set_promotion_failed(); + this->meta_group->members->set(*m); + } + }); +} + +void MetaGroupWrapper::memberSetPromotionAccepted(const Napi::CallbackInfo& info) { + wrapExceptions(info, [&] { + assertInfoLength(info, 1); + assertIsString(info[0]); + auto pubkeyHex = toCppString(info[0], "memberSetPromotionAccepted"); + auto m = this->meta_group->members->get(pubkeyHex); + if (m) { + m->set_promotion_accepted(); + this->meta_group->members->set(*m); + } + }); +} + +void MetaGroupWrapper::memberSetProfilePicture(const Napi::CallbackInfo& info) { + wrapExceptions(info, [&] { + assertInfoLength(info, 2); + assertIsString(info[0]); + assertIsObject(info[1]); + + auto pubkeyHex = toCppString(info[0], "memberSetProfilePicture"); + auto profilePicture = profile_pic_from_object(info[1]); + + auto m = this->meta_group->members->get(pubkeyHex); + if (m) { + m->profile_picture = profilePicture; + this->meta_group->members->set(*m); + } + }); +} + +void MetaGroupWrapper::membersMarkPendingRemoval(const Napi::CallbackInfo& info) { + wrapExceptions(info, [&] { + assertInfoLength(info, 2); + auto toUpdateJSValue = info[0]; + auto withMessageJSValue = info[1]; + + assertIsArray(toUpdateJSValue); + assertIsBoolean(withMessageJSValue); + bool withMessages = toCppBoolean(withMessageJSValue, "membersMarkPendingRemoval"); + + auto toUpdateJS = toUpdateJSValue.As(); + for (uint32_t i = 0; i < toUpdateJS.Length(); i++) { + auto pubkeyHex = toCppString(toUpdateJS[i], "membersMarkPendingRemoval"); + auto existing = this->meta_group->members->get(pubkeyHex); + if (existing) { + existing->set_removed(withMessages); + this->meta_group->members->set(*existing); + } + } + }); +} + +Napi::Value MetaGroupWrapper::memberEraseAndRekey(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + auto toRemoveJSValue = info[0]; + + assertIsArray(toRemoveJSValue); + + auto toRemoveJS = toRemoveJSValue.As(); + auto rekeyed = false; + for (uint32_t i = 0; i < toRemoveJS.Length(); i++) { + auto pubkeyHex = toCppString(toRemoveJS[i], "memberEraseAndRekey"); + rekeyed |= this->meta_group->members->erase(pubkeyHex); + } + + if (rekeyed) { + meta_group->keys->rekey(*(this->meta_group->info), *(this->meta_group->members)); + } + + return rekeyed; + }); +} + +/* #endregion */ + +/* #region KEYS ACTIONS */ +Napi::Value MetaGroupWrapper::keysNeedsRekey(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { return meta_group->keys->needs_rekey(); }); +} + +Napi::Value MetaGroupWrapper::keyRekey(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + return meta_group->keys->rekey(*(meta_group->info), *(meta_group->members)); + }); +} + +Napi::Value MetaGroupWrapper::keyGetAll(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { return meta_group->keys->group_keys(); }); +} + +Napi::Value MetaGroupWrapper::loadKeyMessage(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 3); + assertIsString(info[0]); + assertIsUInt8Array(info[1], "loadKeyMessage"); + assertIsNumber(info[2], "loadKeyMessage"); + + auto hash = toCppString(info[0], "loadKeyMessage"); + auto data = toCppBuffer(info[1], "loadKeyMessage"); + auto timestamp_ms = toCppInteger(info[2], "loadKeyMessage"); + + return meta_group->keys->load_key_message( + hash, data, timestamp_ms, *(this->meta_group->info), *(this->meta_group->members)); + }); +} + +Napi::Value MetaGroupWrapper::keyGetCurrentGen(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 0); + return meta_group->keys->current_generation(); + }); +} + +Napi::Value MetaGroupWrapper::currentHashes(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + auto keysHashes = meta_group->keys->current_hashes(); + auto infoHashes = meta_group->info->current_hashes(); + auto memberHashes = meta_group->members->current_hashes(); + std::vector merged; + std::copy(std::begin(keysHashes), std::end(keysHashes), std::back_inserter(merged)); + std::copy(std::begin(infoHashes), std::end(infoHashes), std::back_inserter(merged)); + std::copy(std::begin(memberHashes), std::end(memberHashes), std::back_inserter(merged)); + return merged; + }); +} + +Napi::Value MetaGroupWrapper::encryptMessages(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsArray(info[0]); + + auto plaintextsJS = info[0].As(); + uint32_t arrayLength = plaintextsJS.Length(); + std::vector encryptedMessages; + encryptedMessages.reserve(arrayLength); + + for (uint32_t i = 0; i < plaintextsJS.Length(); i++) { + auto plaintext = toCppBuffer(plaintextsJS[i], "encryptMessages"); + + encryptedMessages.push_back(this->meta_group->keys->encrypt_message(plaintext)); + } + return encryptedMessages; + }); +} + +Napi::Value MetaGroupWrapper::decryptMessage(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsUInt8Array(info[0], "decryptMessage"); + + auto ciphertext = toCppBuffer(info[0], "decryptMessage"); + auto decrypted = this->meta_group->keys->decrypt_message(ciphertext); + + return decrypt_result_to_JS(info.Env(), decrypted); + }); +} + +Napi::Value MetaGroupWrapper::makeSwarmSubAccount(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsString(info[0]); + + auto memberPk = toCppString(info[0], "makeSwarmSubAccount"); + ustring subaccount = this->meta_group->keys->swarm_make_subaccount(memberPk); + + session::nodeapi::checkOrThrow( + subaccount.length() == 100, "expected subaccount to be 100 bytes long"); + + return subaccount; + }); +} + +Napi::Value MetaGroupWrapper::swarmSubAccountToken(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsString(info[0]); + + auto memberPk = toCppString(info[0], "swarmSubAccountToken"); + ustring subaccount = this->meta_group->keys->swarm_subaccount_token(memberPk); + + session::nodeapi::checkOrThrow( + subaccount.length() == 36, "expected subaccount token to be 36 bytes long"); + + return oxenc::to_hex(subaccount); + }); +} + +Napi::Value MetaGroupWrapper::swarmVerifySubAccount(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsUInt8Array(info[0], "swarmVerifySubAccount"); + + auto signingValue = toCppBuffer(info[0], "swarmVerifySubAccount"); + return this->meta_group->keys->swarm_verify_subaccount(signingValue); + }); +} + +Napi::Value MetaGroupWrapper::loadAdminKeys(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsUInt8Array(info[0], "loadAdminKeys"); + + auto secret = toCppBuffer(info[0], "loadAdminKeys"); + this->meta_group->keys->load_admin_key( + secret, *(this->meta_group->info), *(this->meta_group->members)); + return info.Env().Null(); + }); +} + +Napi::Value MetaGroupWrapper::keysAdmin(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 0); + return this->meta_group->keys->admin(); + }); +} + +Napi::Value MetaGroupWrapper::generateSupplementKeys(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + auto membersJSValue = info[0]; + assertIsArray(membersJSValue); + + auto membersJS = membersJSValue.As(); + uint32_t arrayLength = membersJS.Length(); + std::vector membersToAdd; + membersToAdd.reserve(arrayLength); + std::vector membersCpp; + membersCpp.reserve(arrayLength); + + for (uint32_t i = 0; i < membersJS.Length(); i++) { + auto memberPk = toCppString(membersJS[i], "generateSupplementKeys"); + membersCpp.push_back(memberPk); + } + return this->meta_group->keys->key_supplement(membersCpp); + }); +} + +Napi::Value MetaGroupWrapper::swarmSubaccountSign(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 2); + assertIsUInt8Array(info[0], "swarmSubaccountSign 0"); + assertIsUInt8Array(info[1], "swarmSubaccountSign 1"); + + auto message = toCppBuffer(info[0], "swarmSubaccountSign message"); + auto authdata = toCppBuffer(info[1], "swarmSubaccountSign authdata"); + auto subaccountSign = this->meta_group->keys->swarm_subaccount_sign(message, authdata); + + return subaccountSign; + }); +} +/* #endregion */ + +} // namespace session::nodeapi diff --git a/src/groups/meta_group_wrapper.hpp b/src/groups/meta_group_wrapper.hpp new file mode 100644 index 0000000..2b2562c --- /dev/null +++ b/src/groups/meta_group_wrapper.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include + +#include "../meta/meta_base_wrapper.hpp" +#include "../profile_pic.hpp" +#include "../utilities.hpp" +#include "./meta_group.hpp" +#include "oxenc/bt_producer.h" +#include "session/config/user_groups.hpp" + +namespace session::nodeapi { +using config::groups::Members; +using session::config::GROUP_DESTROYED; +using session::config::KICKED_FROM_GROUP; +using session::config::NOT_REMOVED; +using session::config::groups::member; +using session::nodeapi::MetaGroup; + +template <> +struct toJs_impl { + Napi::Object operator()(const Napi::Env& env, const member& info) { + auto obj = Napi::Object::New(env); + + obj["pubkeyHex"] = toJs(env, info.session_id); + obj["name"] = toJs(env, info.name); + obj["profilePicture"] = toJs(env, info.profile_picture); + obj["removedStatus"] = toJs(env, info.removed_status); + + // promoted() is true as soon as the member is scheduled to be promoted + // Note: this should be part of `libsession-util`, not `libsession-util-nodejs` + if (info.promoted() && !info.promotion_pending()) { + obj["memberStatus"] = toJs(env, "PROMOTION_ACCEPTED"); + } else if (info.promotion_failed()) { + obj["memberStatus"] = toJs(env, "PROMOTION_FAILED"); + } else if (info.promotion_pending()) { + obj["memberStatus"] = toJs(env, "PROMOTION_SENT"); + } else if (info.admin) { + obj["memberStatus"] = toJs(env, "PROMOTION_NOT_SENT"); + } else if (info.invite_status == 0) { + obj["memberStatus"] = toJs(env, "INVITE_ACCEPTED"); + } else if (info.invite_not_sent()) { + obj["memberStatus"] = toJs(env, "INVITE_NOT_SENT"); + } else if (info.invite_failed()) { + obj["memberStatus"] = toJs(env, "INVITE_FAILED"); + } else if (info.invite_pending()) { + // Note: INVITE_NOT_SENT is 3, which makes invite_pending() return true, so be sure to + // check for invite_not_sent() above. + obj["memberStatus"] = toJs(env, "INVITE_SENT"); + } else { + obj["memberStatus"] = toJs(env, "UNKNOWN"); + } + + obj["nominatedAdmin"] = toJs(env, info.admin); + + if (!info.is_removed()) { + obj["removedStatus"] = toJs(env, "NOT_REMOVED"); + } else { + if (info.should_remove_messages()) { + obj["removedStatus"] = toJs(env, "REMOVED_MEMBER_AND_MESSAGES"); + } else { + obj["removedStatus"] = toJs(env, "REMOVED_MEMBER"); + } + } + return obj; + } +}; + +template <> +struct toJs_impl { + Napi::Object operator()(const Napi::Env& env, const Keys::swarm_auth& auth) { + auto obj = Napi::Object::New(env); + + obj["subaccount"] = toJs(env, auth.subaccount); + obj["subaccount_sig"] = toJs(env, auth.subaccount_sig); + obj["signature"] = toJs(env, auth.signature); + return obj; + } +}; + +class MetaGroupWrapper : public Napi::ObjectWrap { + public: + static void Init(Napi::Env env, Napi::Object exports); + + explicit MetaGroupWrapper(const Napi::CallbackInfo& info); + + private: + std::unique_ptr meta_group; + + /* Shared Actions */ + Napi::Value needsPush(const Napi::CallbackInfo& info); + Napi::Value push(const Napi::CallbackInfo& info); + Napi::Value needsDump(const Napi::CallbackInfo& info); + Napi::Value metaDump(const Napi::CallbackInfo& info); + Napi::Value metaMakeDump(const Napi::CallbackInfo& info); + void metaConfirmPushed(const Napi::CallbackInfo& info); + Napi::Value metaMerge(const Napi::CallbackInfo& info); + + /** Info Actions */ + Napi::Value infoGet(const Napi::CallbackInfo& info); + Napi::Value infoSet(const Napi::CallbackInfo& info); + Napi::Value infoDestroy(const Napi::CallbackInfo& info); + + /** Members Actions */ + Napi::Value memberGetAll(const Napi::CallbackInfo& info); + Napi::Value memberGetAllPendingRemovals(const Napi::CallbackInfo& info); + Napi::Value memberGet(const Napi::CallbackInfo& info); + Napi::Value memberGetOrConstruct(const Napi::CallbackInfo& info); + Napi::Value memberConstructAndSet(const Napi::CallbackInfo& info); + + void memberSetNameTruncated(const Napi::CallbackInfo& info); + void memberSetInvited(const Napi::CallbackInfo& info); + void memberSetAccepted(const Napi::CallbackInfo& info); + void memberSetPromoted(const Napi::CallbackInfo& info); + void memberSetPromotionSent(const Napi::CallbackInfo& info); + void memberSetPromotionFailed(const Napi::CallbackInfo& info); + void memberSetPromotionAccepted(const Napi::CallbackInfo& info); + void memberSetProfilePicture(const Napi::CallbackInfo& info); + Napi::Value memberEraseAndRekey(const Napi::CallbackInfo& info); + void membersMarkPendingRemoval(const Napi::CallbackInfo& info); + + /** Keys Actions */ + Napi::Value keysNeedsRekey(const Napi::CallbackInfo& info); + Napi::Value keyRekey(const Napi::CallbackInfo& info); + Napi::Value keyGetAll(const Napi::CallbackInfo& info); + Napi::Value loadKeyMessage(const Napi::CallbackInfo& info); + Napi::Value keyGetCurrentGen(const Napi::CallbackInfo& info); + Napi::Value currentHashes(const Napi::CallbackInfo& info); + Napi::Value encryptMessages(const Napi::CallbackInfo& info); + Napi::Value decryptMessage(const Napi::CallbackInfo& info); + Napi::Value makeSwarmSubAccount(const Napi::CallbackInfo& info); + Napi::Value swarmSubAccountToken(const Napi::CallbackInfo& info); + Napi::Value generateSupplementKeys(const Napi::CallbackInfo& info); + Napi::Value swarmSubaccountSign(const Napi::CallbackInfo& info); + Napi::Value swarmVerifySubAccount(const Napi::CallbackInfo& info); + Napi::Value loadAdminKeys(const Napi::CallbackInfo& info); + Napi::Value keysAdmin(const Napi::CallbackInfo& info); +}; + +} // namespace session::nodeapi diff --git a/src/meta/meta_base_wrapper.hpp b/src/meta/meta_base_wrapper.hpp index 95a94a6..75e71e9 100644 --- a/src/meta/meta_base_wrapper.hpp +++ b/src/meta/meta_base_wrapper.hpp @@ -3,6 +3,7 @@ #include #include "../base_config.hpp" +#include "../groups/meta_group.hpp" namespace session::nodeapi { @@ -29,6 +30,81 @@ class MetaBaseWrapper { exports.Set(class_name, cls); } + + static std::unique_ptr constructGroupWrapper( + const Napi::CallbackInfo& info, const std::string& class_name) { + return wrapExceptions(info, [&] { + if (!info.IsConstructCall()) + throw std::invalid_argument{ + "You need to call the constructor with the `new` syntax"}; + + assertInfoLength(info, 1); + auto arg = info[0]; + assertIsObject(arg); + auto obj = arg.As(); + + if (obj.IsEmpty()) + throw std::invalid_argument("constructGroupWrapper received empty"); + + assertIsUInt8Array(obj.Get("userEd25519Secretkey"), "constructGroupWrapper userEd"); + auto user_ed25519_secretkey = toCppBuffer( + obj.Get("userEd25519Secretkey"), + class_name + ":constructGroupWrapper.userEd25519Secretkey"); + + assertIsUInt8Array(obj.Get("groupEd25519Pubkey"), "constructGroupWrapper groupEd"); + auto group_ed25519_pubkey = toCppBuffer( + obj.Get("groupEd25519Pubkey"), + class_name + ":constructGroupWrapper.groupEd25519Pubkey"); + + std::optional group_ed25519_secretkey = maybeNonemptyBuffer( + obj.Get("groupEd25519Secretkey"), + class_name + ":constructGroupWrapper.groupEd25519Secretkey"); + + std::optional dumped_meta = maybeNonemptyBuffer( + obj.Get("metaDumped"), class_name + ":constructGroupWrapper.metaDumped"); + + std::optional dumped_info; + std::optional dumped_members; + std::optional dumped_keys; + + if (dumped_meta) { + auto dumped_meta_str = from_unsigned_sv(*dumped_meta); + + oxenc::bt_dict_consumer combined{dumped_meta_str}; + // NB: must read in ascii-sorted order: + if (!combined.skip_until("info")) + throw std::runtime_error{"info dump not found in combined dump!"}; + dumped_info = session::to_unsigned_sv(combined.consume_string_view()); + + if (!combined.skip_until("keys")) + throw std::runtime_error{"keys dump not found in combined dump!"}; + dumped_keys = session::to_unsigned_sv(combined.consume_string_view()); + + if (!combined.skip_until("members")) + throw std::runtime_error{"members dump not found in combined dump!"}; + dumped_members = session::to_unsigned_sv(combined.consume_string_view()); + } + + // Note, we keep shared_ptr for those as the Keys one need a reference to Members and + // Info on its own currently. + auto info = std::make_shared( + group_ed25519_pubkey, group_ed25519_secretkey, dumped_info); + + auto members = std::make_shared( + group_ed25519_pubkey, group_ed25519_secretkey, dumped_members); + + auto keys = std::make_shared( + user_ed25519_secretkey, + group_ed25519_pubkey, + group_ed25519_secretkey, + dumped_keys, + *info, + *members); + + return std::make_unique( + info, members, keys, group_ed25519_pubkey, group_ed25519_secretkey); + }); + } }; } // namespace session::nodeapi diff --git a/src/multi_encrypt/multi_encrypt.hpp b/src/multi_encrypt/multi_encrypt.hpp new file mode 100644 index 0000000..6816fda --- /dev/null +++ b/src/multi_encrypt/multi_encrypt.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include + +#include + +#include "../utilities.hpp" +#include "session/config/user_profile.hpp" +#include "session/multi_encrypt.hpp" +#include "session/random.hpp" + +namespace session::nodeapi { + +class MultiEncryptWrapper : public Napi::ObjectWrap { + public: + MultiEncryptWrapper(const Napi::CallbackInfo& info) : + Napi::ObjectWrap{info} { + throw std::invalid_argument( + "MultiEncryptWrapper is static and doesn't need to be constructed"); + } + + static void Init(Napi::Env env, Napi::Object exports) { + MetaBaseWrapper::NoBaseClassInitHelper( + env, + exports, + "MultiEncryptWrapperNode", + { + StaticMethod<&MultiEncryptWrapper::multiEncrypt>( + "multiEncrypt", + static_cast( + napi_writable | napi_configurable)), + StaticMethod<&MultiEncryptWrapper::multiDecryptEd25519>( + "multiDecryptEd25519", + static_cast( + napi_writable | napi_configurable)), + + }); + } + + private: + static Napi::Value multiEncrypt(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsObject(info[0]); + auto obj = info[0].As(); + + if (obj.IsEmpty()) + throw std::invalid_argument("multiEncrypt received empty"); + + assertIsUInt8Array(obj.Get("ed25519SecretKey"), "multiEncrypt.ed25519SecretKey"); + auto ed25519SecretKey = + toCppBuffer(obj.Get("ed25519SecretKey"), "multiEncrypt.ed25519SecretKey"); + + assertIsString(obj.Get("domain")); + auto domain = toCppString(obj.Get("domain"), "multiEncrypt.domain"); + + // handle the messages conversion + auto messagesJSValue = obj.Get("messages"); + assertIsArray(messagesJSValue); + auto messagesJS = messagesJSValue.As(); + std::vector messages; + messages.reserve(messagesJS.Length()); + for (uint32_t i = 0; i < messagesJS.Length(); i++) { + auto itemValue = messagesJS.Get(i); + assertIsUInt8Array(itemValue, "multiEncrypt.itemValue.message"); + auto item = toCppBuffer(itemValue, "multiEncrypt.itemValue.message"); + messages.push_back(item); + } + + // handle the recipients conversion + auto recipientsJSValue = obj.Get("recipients"); + assertIsArray(recipientsJSValue); + auto recipientsJS = recipientsJSValue.As(); + std::vector recipients; + recipients.reserve(recipientsJS.Length()); + for (uint32_t i = 0; i < recipientsJS.Length(); i++) { + auto itemValue = recipientsJS.Get(i); + assertIsUInt8Array(itemValue, "multiEncrypt.itemValue.recipient"); + auto item = toCppBuffer(itemValue, "multiEncrypt.itemValue.recipient"); + recipients.push_back(item); + } + ustring random_nonce = session::random::random(24); + + std::vector messages_sv(messages.begin(), messages.end()); + std::vector recipients_sv(recipients.begin(), recipients.end()); + + // Note: this function needs the first 2 args to be vector of sv explicitly + return session::encrypt_for_multiple_simple( + messages_sv, recipients_sv, ed25519SecretKey, domain, random_nonce); + }); + }; + + static Napi::Value multiDecryptEd25519(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsObject(info[0]); + auto obj = info[0].As(); + + if (obj.IsEmpty()) + throw std::invalid_argument("multiDecryptEd25519 received empty"); + + assertIsUInt8Array(obj.Get("encoded"), "multiDecryptEd25519.encoded"); + auto encoded = toCppBuffer(obj.Get("encoded"), "multiDecryptEd25519.encoded"); + + assertIsUInt8Array( + obj.Get("userEd25519SecretKey"), "multiDecryptEd25519.userEd25519SecretKey"); + auto ed25519_secret_key = toCppBuffer( + obj.Get("userEd25519SecretKey"), "multiDecryptEd25519.userEd25519SecretKey"); + + assertIsUInt8Array( + obj.Get("senderEd25519Pubkey"), "multiDecryptEd25519.senderEd25519Pubkey"); + auto sender_ed25519_pubkey = toCppBuffer( + obj.Get("senderEd25519Pubkey"), "multiDecryptEd25519.senderEd25519Pubkey"); + + assertIsString(obj.Get("domain")); + auto domain = toCppString(obj.Get("domain"), "multiDecryptEd25519.domain"); + + return session::decrypt_for_multiple_simple_ed25519( + encoded, ed25519_secret_key, sender_ed25519_pubkey, domain); + }); + }; +}; + +} // namespace session::nodeapi diff --git a/src/profile_pic.cpp b/src/profile_pic.cpp index e513112..14d2040 100644 --- a/src/profile_pic.cpp +++ b/src/profile_pic.cpp @@ -5,18 +5,6 @@ namespace session::nodeapi { -Napi::Object object_from_profile_pic(const Napi::Env& env, const config::profile_pic& pic) { - auto obj = Napi::Object::New(env); - if (pic) { - obj["url"] = toJs(env, pic.url); - obj["key"] = toJs(env, pic.key); - } else { - obj["url"] = env.Null(); - obj["key"] = env.Null(); - } - return obj; -} - config::profile_pic profile_pic_from_object(Napi::Value val) { if (val.IsNull() || val.IsUndefined()) return {}; @@ -34,7 +22,7 @@ config::profile_pic profile_pic_from_object(Napi::Value val) { return {}; assertIsString(url); - assertIsUInt8Array(key); + assertIsUInt8Array(key, "profile_pic_from_object"); auto url_str = toCppString(url, "profile_pic_from_object"); if (url_str.size() > config::profile_pic::MAX_URL_LENGTH) diff --git a/src/profile_pic.hpp b/src/profile_pic.hpp index d6c3aed..c959571 100644 --- a/src/profile_pic.hpp +++ b/src/profile_pic.hpp @@ -1,16 +1,31 @@ #include #include "session/config/profile_pic.hpp" +#include "utilities.hpp" namespace session::nodeapi { // Returns {"url": "...", "key": buffer} object; both values will be Null if the pic is not set. -Napi::Object object_from_profile_pic(const Napi::Env& env, const config::profile_pic& pic); -// Constructs a profile_pic from a Napi::Value which must be either Null or an Object; if an Object -// then it *must* contain "url" (string or null) and "key" (uint8array of size 32 or null) keys; if -// either is empty or Null then you get back an empty (i.e. clearing) profile_pic. Throws on -// invalid input. +template <> +struct toJs_impl { + Napi::Object operator()(const Napi::Env& env, const config::profile_pic& pic) { + auto obj = Napi::Object::New(env); + if (pic) { + obj["url"] = toJs(env, pic.url); + obj["key"] = toJs(env, pic.key); + } else { + obj["url"] = env.Null(); + obj["key"] = env.Null(); + } + return obj; + } +}; + +// Constructs a profile_pic from a Napi::Value which must be either Null or an Object; if an +// Object then it *must* contain "url" (string or null) and "key" (uint8array of size 32 or +// null) keys; if either is empty or Null then you get back an empty (i.e. clearing) +// profile_pic. Throws on invalid input. config::profile_pic profile_pic_from_object(Napi::Value val); } // namespace session::nodeapi diff --git a/src/user_config.cpp b/src/user_config.cpp index 60b8b4b..4f1937c 100644 --- a/src/user_config.cpp +++ b/src/user_config.cpp @@ -1,7 +1,10 @@ #include "user_config.hpp" +#include + #include "base_config.hpp" #include "profile_pic.hpp" +#include "session/config/base.hpp" #include "session/config/user_profile.hpp" namespace session::nodeapi { @@ -36,7 +39,6 @@ UserConfigWrapper::UserConfigWrapper(const Napi::CallbackInfo& info) : ConfigBaseImpl{construct(info, "UserConfig")}, Napi::ObjectWrap{info} {} - Napi::Value UserConfigWrapper::getPriority(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { auto env = info.Env(); @@ -54,7 +56,16 @@ Napi::Value UserConfigWrapper::getName(const Napi::CallbackInfo& info) { Napi::Value UserConfigWrapper::getProfilePic(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { auto env = info.Env(); - return object_from_profile_pic(env, config.get_profile_pic()); + auto pic = config.get_profile_pic(); + auto obj = Napi::Object::New(env); + if (pic) { + obj["url"] = toJs(env, pic.url); + obj["key"] = toJs(env, pic.key); + } else { + obj["url"] = env.Null(); + obj["key"] = env.Null(); + } + return obj; }); } @@ -63,7 +74,7 @@ void UserConfigWrapper::setPriority(const Napi::CallbackInfo& info) { auto env = info.Env(); assertInfoLength(info, 1); auto priority = info[0]; - assertIsNumber(priority); + assertIsNumber(priority, "UserConfigWrapper::setPriority"); auto new_priority = toPriority(priority, config.get_nts_priority()); config.set_nts_priority(new_priority); @@ -145,7 +156,7 @@ void UserConfigWrapper::setNoteToSelfExpiry(const Napi::CallbackInfo& info) { assertInfoLength(info, 1); auto expirySeconds = info[0]; - assertIsNumber(expirySeconds); + assertIsNumber(expirySeconds, "setNoteToSelfExpiry"); auto expiryCppSeconds = toCppInteger(expirySeconds, "set_nts_expiry", false); config.set_nts_expiry(std::chrono::seconds{expiryCppSeconds}); diff --git a/src/user_groups_config.cpp b/src/user_groups_config.cpp index ed46edb..1b29ffc 100644 --- a/src/user_groups_config.cpp +++ b/src/user_groups_config.cpp @@ -13,6 +13,7 @@ namespace session::nodeapi { using config::community_info; +using config::group_info; using config::legacy_group_info; using config::UserGroups; @@ -56,6 +57,25 @@ struct toJs_impl { } }; +template <> +struct toJs_impl { + Napi::Object operator()(const Napi::Env& env, const group_info& info) { + auto obj = Napi::Object::New(env); + + obj["pubkeyHex"] = toJs(env, info.id); + obj["secretKey"] = toJs(env, info.secretkey); + obj["priority"] = toJs(env, info.priority); + obj["joinedAtSeconds"] = toJs(env, info.joined_at); + obj["name"] = toJs(env, info.name); + obj["authData"] = toJs(env, info.auth_data); + obj["invitePending"] = toJs(env, info.invited); + obj["kicked"] = toJs(env, info.kicked()); + obj["destroyed"] = toJs(env, info.isDestroyed()); + + return obj; + } +}; + void UserGroupsWrapper::Init(Napi::Env env, Napi::Object exports) { InitHelper( env, @@ -78,6 +98,17 @@ void UserGroupsWrapper::Init(Napi::Env env, Napi::Object exports) { InstanceMethod("getAllLegacyGroups", &UserGroupsWrapper::getAllLegacyGroups), InstanceMethod("setLegacyGroup", &UserGroupsWrapper::setLegacyGroup), InstanceMethod("eraseLegacyGroup", &UserGroupsWrapper::eraseLegacyGroup), + + // Groups related methods + InstanceMethod("createGroup", &UserGroupsWrapper::createGroup), + InstanceMethod("getGroup", &UserGroupsWrapper::getGroup), + InstanceMethod("getAllGroups", &UserGroupsWrapper::getAllGroups), + InstanceMethod("setGroup", &UserGroupsWrapper::setGroup), + InstanceMethod("markGroupKicked", &UserGroupsWrapper::markGroupKicked), + InstanceMethod("markGroupInvited", &UserGroupsWrapper::markGroupInvited), + InstanceMethod("markGroupDestroyed", &UserGroupsWrapper::markGroupDestroyed), + InstanceMethod("eraseGroup", &UserGroupsWrapper::eraseGroup), + }); } @@ -104,7 +135,7 @@ void UserGroupsWrapper::setCommunityByFullUrl(const Napi::CallbackInfo& info) { toCppString(first, "group.SetCommunityByFullUrl")); auto second = info[1]; - assertIsNumber(second); + assertIsNumber(second, "setCommunityByFullUrl"); createdOrFound.priority = toPriority(second, createdOrFound.priority); config.set(createdOrFound); @@ -177,7 +208,8 @@ void UserGroupsWrapper::setLegacyGroup(const Napi::CallbackInfo& info) { group.disappearing_timer = std::chrono::seconds{toCppInteger( obj.Get("disappearingTimerSeconds"), - "legacyGroup.set disappearingTimerSeconds", true)}; + "legacyGroup.set disappearingTimerSeconds", + true)}; auto membersJSValue = obj.Get("members"); assertIsArray(membersJSValue); @@ -236,4 +268,119 @@ Napi::Value UserGroupsWrapper::eraseLegacyGroup(const Napi::CallbackInfo& info) return wrapResult(info, [&] { return config.erase_legacy_group(getStringArgs<1>(info)); }); } +/** + * ================================================= + * ===================== GROUPS ==================== + * ================================================= + */ + +Napi::Value UserGroupsWrapper::createGroup(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { return config.create_group(); }); +} + +Napi::Value UserGroupsWrapper::getGroup(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { return config.get_group(getStringArgs<1>(info)); }); +} + +Napi::Value UserGroupsWrapper::getAllGroups(const Napi::CallbackInfo& info) { + + return get_all_impl(info, config.size_groups(), config.begin_groups(), config.end()); +} + +Napi::Value UserGroupsWrapper::setGroup(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsObject(info[0]); + auto obj = info[0].As(); + + if (obj.IsEmpty()) + throw std::invalid_argument("setGroup received empty"); + + assertIsString(obj.Get("pubkeyHex")); + auto groupPk = toCppString(obj.Get("pubkeyHex"), "legacyGroup.set"); + + // we should get a `UserGroupsSet` object. If any fields are null, skip updating them. + // Otherwise, use the corresponding value to update what we got from the + // `get_or_construct_group` below + + auto group_info = config.get_or_construct_group(groupPk); + + if (auto priority = + maybeNonemptyInt(obj.Get("priority"), "UserGroupsWrapper::setGroup priority")) { + group_info.priority = toPriority(obj.Get("priority"), group_info.priority); + } + + if (auto joinedAtSeconds = maybeNonemptyInt( + obj.Get("joinedAtSeconds"), "UserGroupsWrapper::setGroup joinedAtSeconds")) { + group_info.joined_at = *joinedAtSeconds; + } + + if (auto invited = maybeNonemptyBoolean( + obj.Get("invitePending"), "UserGroupsWrapper::setGroup invitePending")) { + group_info.invited = *invited; + } + + if (auto secretKey = maybeNonemptyBuffer( + obj.Get("secretKey"), "UserGroupsWrapper::setGroup secretKey")) { + group_info.secretkey = *secretKey; + } + + if (auto authData = maybeNonemptyBuffer( + obj.Get("authData"), "UserGroupsWrapper::setGroup authData")) { + group_info.auth_data = *authData; + } + + if (auto name = maybeNonemptyString(obj.Get("name"), "UserGroupsWrapper::setGroup name")) { + group_info.name = *name; + } + + config.set(group_info); + + return config.get_or_construct_group(groupPk); + }); +} + +Napi::Value UserGroupsWrapper::markGroupKicked(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + auto groupPk = getStringArgs<1>(info); + + auto group = config.get_group(groupPk); + if (group) { + group->markKicked(); + config.set(*group); + } + return config.get_or_construct_group(groupPk); + }); +} + +Napi::Value UserGroupsWrapper::markGroupInvited(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + auto groupPk = getStringArgs<1>(info); + + auto group = config.get_group(groupPk); + if (group) { + group->markInvited(); + config.set(*group); + } + return config.get_or_construct_group(groupPk); + }); +} + +Napi::Value UserGroupsWrapper::markGroupDestroyed(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + auto groupPk = getStringArgs<1>(info); + + auto group = config.get_group(groupPk); + if (group) { + group->markDestroyed(); + config.set(*group); + } + return config.get_or_construct_group(groupPk); + }); +} + +Napi::Value UserGroupsWrapper::eraseGroup(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { return config.erase_group(getStringArgs<1>(info)); }); +} + } // namespace session::nodeapi diff --git a/src/user_groups_config.hpp b/src/user_groups_config.hpp index 7af3372..be8bcf1 100644 --- a/src/user_groups_config.hpp +++ b/src/user_groups_config.hpp @@ -28,6 +28,16 @@ class UserGroupsWrapper : public ConfigBaseImpl, public Napi::ObjectWrap -namespace session::nodeapi { +#include "session/config/namespaces.hpp" -static void checkOrThrow(bool condition, const char* msg) { - if (!condition) - throw std::invalid_argument{msg}; -} +namespace session::nodeapi { void assertInfoLength(const Napi::CallbackInfo& info, const int expected) { checkOrThrow(info.Length() == expected, "Invalid number of arguments"); @@ -21,10 +18,10 @@ void assertIsStringOrNull(const Napi::Value& val) { checkOrThrow(val.IsString() || val.IsNull(), "Wrong arguments: expected string or null"); } -void assertIsNumber(const Napi::Value& val) { +void assertIsNumber(const Napi::Value& val, const std::string& identifier) { checkOrThrow( val.IsNumber() && !val.IsEmpty() && !val.IsNull() && !val.IsUndefined(), - "Wrong arguments: expected number"); + std::string("Wrong arguments: expected number" + identifier).c_str()); } void assertIsArray(const Napi::Value& val) { @@ -45,8 +42,10 @@ void assertIsUInt8ArrayOrNull(const Napi::Value& val) { checkOrThrow(val.IsNull() || IsUint8Array(val), "Wrong arguments: expected uint8Array or null"); } -void assertIsUInt8Array(const Napi::Value& val) { - checkOrThrow(IsUint8Array(val), "Wrong arguments: expected Buffer"); +void assertIsUInt8Array(const Napi::Value& val, const std::string& identifier) { + checkOrThrow( + IsUint8Array(val), + std::string("Wrong arguments: expected uint8Array" + identifier).c_str()); } void assertIsString(const Napi::Value& val) { @@ -123,6 +122,29 @@ int64_t toCppInteger(Napi::Value x, const std::string& identifier, bool allowUnd throw std::invalid_argument{"Unsupported type for "s + identifier + ": expected a number"}; } +std::optional maybeNonemptyInt(Napi::Value x, const std::string& identifier) { + if (x.IsNull() || x.IsUndefined()) + return std::nullopt; + if (x.IsNumber()) { + auto num = x.As().Int64Value(); + return num; + } + + throw std::invalid_argument{"maybeNonemptyInt with invalid type, called from " + identifier}; +} + +std::optional maybeNonemptyBoolean(Napi::Value x, const std::string& identifier) { + if (x.IsNull() || x.IsUndefined()) + return std::nullopt; + if (x.IsBoolean()) { + + return x.As().Value(); + } + + throw std::invalid_argument{ + "maybeNonemptyBoolean with invalid type, called from " + identifier}; +} + bool toCppBoolean(Napi::Value x, const std::string& identifier) { if (x.IsNull() || x.IsUndefined()) return false; @@ -168,4 +190,40 @@ int64_t unix_timestamp_now() { return duration_cast(system_clock::now().time_since_epoch()).count(); } +Napi::Object push_result_to_JS( + const Napi::Env& env, + const push_entry_t& push_entry, + const session::config::Namespace& push_namespace) { + auto obj = Napi::Object::New(env); + + obj["seqno"] = toJs(env, std::get<0>(push_entry)); + obj["data"] = toJs(env, std::get<1>(push_entry)); + obj["hashes"] = toJs(env, std::get<2>(push_entry)); + obj["namespace"] = toJs(env, push_namespace); + + return obj; +}; + +Napi::Object push_key_entry_to_JS( + const Napi::Env& env, + const session::ustring_view& key_data, + const session::config::Namespace& push_namespace) { + auto obj = Napi::Object::New(env); + + obj["data"] = toJs(env, key_data); + obj["namespace"] = toJs(env, push_namespace); + + return obj; +}; + +Napi::Object decrypt_result_to_JS( + const Napi::Env& env, const std::pair decrypted) { + auto obj = Napi::Object::New(env); + + obj["pubkeyHex"] = toJs(env, decrypted.first); + obj["plaintext"] = toJs(env, decrypted.second); + + return obj; +} + } // namespace session::nodeapi diff --git a/src/utilities.hpp b/src/utilities.hpp index 7cdc428..1a0d8bd 100644 --- a/src/utilities.hpp +++ b/src/utilities.hpp @@ -3,31 +3,35 @@ #include #include +#include #include #include +#include #include +#include "session/config/namespaces.hpp" #include "session/types.hpp" +#include "utilities.hpp" namespace session::nodeapi { using namespace std::literals; -// template -// T object_wrap_impl(const Napi::ObjectWrap&); -// template -// using object_wrap_t = decltype(object_wrap_impl(std::declval())); +static void checkOrThrow(bool condition, const char* msg) { + if (!condition) + throw std::invalid_argument{msg}; +} void assertInfoLength(const Napi::CallbackInfo& info, const int expected); void assertInfoMinLength(const Napi::CallbackInfo& info, const int minLength); void assertIsStringOrNull(const Napi::Value& value); -void assertIsNumber(const Napi::Value& value); +void assertIsNumber(const Napi::Value& value, const std::string& identifier); void assertIsArray(const Napi::Value& value); void assertIsObject(const Napi::Value& value); void assertIsUInt8ArrayOrNull(const Napi::Value& value); -void assertIsUInt8Array(const Napi::Value& value); +void assertIsUInt8Array(const Napi::Value& value, const std::string& identifier); void assertIsString(const Napi::Value& value); void assertIsBoolean(const Napi::Value& value); @@ -48,11 +52,13 @@ auto getStringArgs(const Napi::CallbackInfo& info) { return args; } - std::string toCppString(Napi::Value x, const std::string& identifier); ustring toCppBuffer(Napi::Value x, const std::string& identifier); ustring_view toCppBufferView(Napi::Value x, const std::string& identifier); int64_t toCppInteger(Napi::Value x, const std::string& identifier, bool allowUndefined = false); +std::optional maybeNonemptyInt(Napi::Value x, const std::string& identifier); +std::optional maybeNonemptyBoolean(Napi::Value x, const std::string& identifier); + bool toCppBoolean(Napi::Value x, const std::string& identifier); // If the object is null/undef/empty returns nullopt, otherwise if a String returns a std::string of @@ -93,26 +99,38 @@ template <> struct toJs_impl { auto operator()(const Napi::Env& env, bool b) const { return Napi::Boolean::New(env, b); } }; + +template <> +struct toJs_impl { + auto operator()(const Napi::Env& env, session::config::Namespace b) const { + return Napi::Number::New(env, static_cast(b)); + } +}; + template struct toJs_impl>> { auto operator()(const Napi::Env& env, T n) const { return Napi::Number::New(env, n); } }; + template struct toJs_impl>> { auto operator()(const Napi::Env& env, std::string_view s) const { return Napi::String::New(env, s.data(), s.size()); } }; + template struct toJs_impl>> { auto operator()(const Napi::Env& env, ustring_view b) const { return Napi::Buffer::Copy(env, b.data(), b.size()); } }; + template struct toJs_impl>> { auto operator()(const Napi::Env& env, const T& val) { return val; } }; + template struct toJs_impl> { auto operator()(const Napi::Env& env, const std::vector& val) { @@ -122,6 +140,19 @@ struct toJs_impl> { return arr; } }; + +template +struct toJs_impl> { + auto operator()(const Napi::Env& env, const std::unordered_set& set) { + std::vector as_array(set.begin(), set.end()); + + auto arr = Napi::Array::New(env, as_array.size()); + for (size_t i = 0; i < as_array.size(); i++) + arr[i] = toJs(env, as_array[i]); + return arr; + } +}; + template struct toJs_impl> { Napi::Value operator()(const Napi::Env& env, const std::optional& val) { @@ -161,7 +192,8 @@ inline std::optional maybe_string(std::string_view val) { // - The return value will be returned as-is if it is already a Napi::Value (or subtype) // - The return will be void if void // - Otherwise the return value will be passed through toJs() to convert it to a Napi::Value. -// See toJs below, but generally this supports numeric types, bools, strings, ustrings, and vectors of any of those primitives. +// See toJs below, but generally this supports numeric types, bools, strings, ustrings, and vectors +// of any of those primitives. // // General use is: // @@ -219,4 +251,22 @@ int64_t toPriority(Napi::Value x, int64_t currentPriority); int64_t unix_timestamp_now(); +using push_entry_t = std::tuple< + session::config::seqno_t, + session::ustring, + std::vector>>; + +Napi::Object push_result_to_JS( + const Napi::Env& env, + const push_entry_t& push_entry, + const session::config::Namespace& push_namespace); + +Napi::Object push_key_entry_to_JS( + const Napi::Env& env, + const session::ustring_view& key_data, + const session::config::Namespace& push_namespace); + +Napi::Object decrypt_result_to_JS( + const Napi::Env& env, const std::pair decrypted); + } // namespace session::nodeapi diff --git a/types/blinding/blinding.d.ts b/types/blinding/blinding.d.ts index 5c0ad23..d15774e 100644 --- a/types/blinding/blinding.d.ts +++ b/types/blinding/blinding.d.ts @@ -20,7 +20,7 @@ declare module 'libsession_util_nodejs' { export type BlindingActionsCalls = MakeWrapperActionCalls; /** - * To be used inside the web worker only (calls are synchronous and won't work asynchrously) + * To be used inside the web worker only (calls are synchronous and won't work asynchronously) */ export class BlindingWrapperNode { public static blindVersionPubkey: BlindingWrapper['blindVersionPubkey']; diff --git a/types/groups/groupinfo.d.ts b/types/groups/groupinfo.d.ts new file mode 100644 index 0000000..bf9cb95 --- /dev/null +++ b/types/groups/groupinfo.d.ts @@ -0,0 +1,10 @@ +/// + +declare module 'libsession_util_nodejs' { + export type GroupInfoWrapper = { + // GroupInfo related methods + infoGet: () => GroupInfoGet; + infoSet: (info: GroupInfoSet) => GroupInfoGet; + infoDestroy: () => void; + }; +} diff --git a/types/groups/groupkeys.d.ts b/types/groups/groupkeys.d.ts new file mode 100644 index 0000000..41d92e1 --- /dev/null +++ b/types/groups/groupkeys.d.ts @@ -0,0 +1,27 @@ +/// + +declare module 'libsession_util_nodejs' { + export type GroupKeysWrapper = { + // GroupKeys related methods + keysNeedsRekey: () => boolean; + keyRekey: () => Uint8Array; + keyGetAll: () => Array; + loadKeyMessage: (hash: string, data: Uint8Array, timestampMs: number) => boolean; + keysAdmin: () => boolean; + keyGetCurrentGen: () => number; + + currentHashes: () => Array; + encryptMessages: (plaintexts: Array) => Array; + decryptMessage: (ciphertext: Uint8Array) => { pubkeyHex: string; plaintext: Uint8Array }; + makeSwarmSubAccount: (memberPubkeyHex: PubkeyType) => Uint8ArrayLen100; + generateSupplementKeys: (membersPubkeyHex: Array) => Uint8Array; + swarmSubaccountSign: ( + message: Uint8Array, + authData: Uint8ArrayLen100 + ) => SwarmSubAccountSignResult; + + swarmSubAccountToken: (memberPk: PubkeyType) => string; // hex encoded + swarmVerifySubAccount: (signingValue: Uint8ArrayLen100) => boolean; + loadAdminKeys: (secret: Uint8ArrayLen64) => void; + }; +} diff --git a/types/groups/groupmembers.d.ts b/types/groups/groupmembers.d.ts new file mode 100644 index 0000000..b414105 --- /dev/null +++ b/types/groups/groupmembers.d.ts @@ -0,0 +1,78 @@ +/// + +declare module 'libsession_util_nodejs' { + /** + * + * GroupMembers wrapper logic + * + */ + type GroupMemberShared = { + pubkeyHex: PubkeyType; + name: string | null; + profilePicture: ProfilePicture | null; + }; + + type MemberStateGroupV2 = + | 'INVITE_NOT_SENT' // as soon as we've scheduled that guy to be invited, but before we've tried sending the invite message + | 'INVITE_FAILED' + | 'INVITE_SENT' + | 'INVITE_ACCEPTED' // regular member + | 'PROMOTION_NOT_SENT' // as soon as we've scheduled that guy to be an admin, but before we've tried sending the promotion message + | 'PROMOTION_FAILED' + | 'PROMOTION_SENT' + | 'PROMOTION_ACCEPTED' // regular admin + | 'UNKNOWN'; + + export type GroupMemberGet = GroupMemberShared & { + memberStatus: MemberStateGroupV2; + + /** + * NOT_REMOVED = 0: + * REMOVED_MEMBER = 1, + * REMOVED_MEMBER_AND_MESSAGES = 2; + */ + removedStatus: 'NOT_REMOVED' | 'REMOVED_MEMBER' | 'REMOVED_MEMBER_AND_MESSAGES' | 'UNKNOWN'; + /** + * True if the member is scheduled to get the keys (.admin field of libsession). + * This is equivalent of memberStatus being one of: + * - PROMOTION_NOT_SENT + * - PROMOTION_FAILED + * - PROMOTION_SENT + * - PROMOTION_ACCEPTED + */ + nominatedAdmin: boolean; + }; + + type GroupMemberWrapper = { + // GroupMember related methods + memberGet: (pubkeyHex: PubkeyType) => GroupMemberGet | null; + memberGetOrConstruct: (pubkeyHex: PubkeyType) => GroupMemberGet; + memberConstructAndSet: (pubkeyHex: PubkeyType) => void; + + memberGetAll: () => Array; + memberGetAllPendingRemovals: () => Array; + + // setters + memberSetNameTruncated: (pubkeyHex: PubkeyType, newName: string) => void; + + /** A member's invite state defaults to invite-not-sent. Use this function to mark that you've sent one, or at least tried (failed: boolean)*/ + memberSetInvited: (pubkeyHex: PubkeyType, failed: boolean) => void; + /** User has accepted an invitation and is now a regular member of the group */ + memberSetAccepted: (pubkeyHex: PubkeyType) => void; + + /** Mark the member as waiting a promotion to be sent to them */ + memberSetPromoted: (pubkeyHex: PubkeyType) => void; + /** Called when we did send the promotion to the member */ + memberSetPromotionSent: (pubkeyHex: PubkeyType) => void; + /** Called when we did send the promotion to the member, but failed */ + memberSetPromotionFailed: (pubkeyHex: PubkeyType) => void; + /** Called when the member accepted the promotion */ + memberSetPromotionAccepted: (pubkeyHex: PubkeyType) => void; + + memberSetProfilePicture: (pubkeyHex: PubkeyType, profilePicture: ProfilePicture) => void; + membersMarkPendingRemoval: (members: Array, withMessages: boolean) => void; + + // eraser + memberEraseAndRekey: (members: Array) => boolean; + }; +} diff --git a/types/groups/index.d.ts b/types/groups/index.d.ts new file mode 100644 index 0000000..311fdbb --- /dev/null +++ b/types/groups/index.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/types/groups/metagroup.d.ts b/types/groups/metagroup.d.ts new file mode 100644 index 0000000..f812070 --- /dev/null +++ b/types/groups/metagroup.d.ts @@ -0,0 +1,169 @@ +/// +/// +/// +/// + +declare module 'libsession_util_nodejs' { + export type ConfirmKeysPush = [data: Uint8Array, hash: string, timestampMs: number]; + + export type GroupWrapperConstructor = { + /** + * The user's ed25519 secret key, length 64. + */ + userEd25519Secretkey: Uint8Array; + /** + * The group ed25519 pubkey without the 03 prefix, length 32. + */ + groupEd25519Pubkey: Uint8Array; + /** + * The group ed25519 priv key if we have it (len 64). Having this means we have admin rights in the group. + * This usually comes from the user group wrapper if we have it. + */ + groupEd25519Secretkey: Uint8Array | null; + /** + * The unified dumps (as saved in the db) for this group. i.e. Keys, Members and Info concatenated, see MetaGroupWrapper::metaDump for details. + */ + metaDumped: Uint8Array | null; + }; + + type MetaGroupWrapper = GroupInfoWrapper & + GroupMemberWrapper & + GroupKeysWrapper & { + // shared actions + init: (options: GroupWrapperConstructor) => void; + free: () => void; + needsPush: () => boolean; + push: () => { + groupInfo: PushConfigResult | null; + groupMember: PushConfigResult | null; + groupKeys: PushKeyConfigResult | null; + }; + needsDump: () => boolean; + metaDump: () => Uint8Array; + metaMakeDump: () => Uint8Array; + metaConfirmPushed: ({ + groupInfo, + groupMember, + }: { + groupInfo: ConfirmPush | null; + groupMember: ConfirmPush | null; + }) => void; + metaMerge: ({ + groupInfo, + groupKeys, + groupMember, + }: { + groupInfo: Array | null; + groupMember: Array | null; + groupKeys: Array | null; + }) => void; + }; + + // this just adds an argument of type GroupPubkeyType in front of the parameters of that function + type AddGroupPkToFunction any> = ( + ...args: [GroupPubkeyType, ...Parameters] + ) => ReturnType; + + export type MetaGroupWrapperActionsCalls = MakeWrapperActionCalls<{ + [key in keyof MetaGroupWrapper]: AddGroupPkToFunction; + }>; + + export class MetaGroupWrapperNode { + constructor(options: GroupWrapperConstructor); + + // shared actions + public needsPush: MetaGroupWrapper['needsPush']; + public push: MetaGroupWrapper['push']; + public needsDump: MetaGroupWrapper['needsDump']; + public metaDump: MetaGroupWrapper['metaDump']; + public metaMakeDump: MetaGroupWrapper['metaMakeDump']; + public metaConfirmPushed: MetaGroupWrapper['metaConfirmPushed']; + public metaMerge: MetaGroupWrapper['metaMerge']; + public currentHashes: MetaGroupWrapper['currentHashes']; + + // info + public infoGet: MetaGroupWrapper['infoGet']; + public infoSet: MetaGroupWrapper['infoSet']; + public infoDestroy: MetaGroupWrapper['infoDestroy']; + + // members + public memberGet: MetaGroupWrapper['memberGet']; + public memberGetOrConstruct: MetaGroupWrapper['memberGetOrConstruct']; + public memberConstructAndSet: MetaGroupWrapper['memberConstructAndSet']; + public memberGetAll: MetaGroupWrapper['memberGetAll']; + public memberGetAllPendingRemovals: MetaGroupWrapper['memberGetAllPendingRemovals']; + public memberSetAccepted: MetaGroupWrapper['memberSetAccepted']; + public memberSetNameTruncated: MetaGroupWrapper['memberSetNameTruncated']; + public memberSetPromoted: MetaGroupWrapper['memberSetPromoted']; + public memberSetPromotionAccepted: MetaGroupWrapper['memberSetPromotionAccepted']; + public memberSetPromotionSent: MetaGroupWrapper['memberSetPromotionSent']; + public memberSetPromotionFailed: MetaGroupWrapper['memberSetPromotionFailed']; + public memberSetInvited: MetaGroupWrapper['memberSetInvited']; + public memberEraseAndRekey: MetaGroupWrapper['memberEraseAndRekey']; + public membersMarkPendingRemoval: MetaGroupWrapper['membersMarkPendingRemoval']; + public memberSetProfilePicture: MetaGroupWrapper['memberSetProfilePicture']; + + // keys + public keysNeedsRekey: MetaGroupWrapper['keysNeedsRekey']; + public keyRekey: MetaGroupWrapper['keyRekey']; + public loadKeyMessage: MetaGroupWrapper['loadKeyMessage']; + public keysAdmin: MetaGroupWrapper['keysAdmin']; + public keyGetCurrentGen: MetaGroupWrapper['keyGetCurrentGen']; + public encryptMessages: MetaGroupWrapper['encryptMessages']; + public decryptMessage: MetaGroupWrapper['decryptMessage']; + public makeSwarmSubAccount: MetaGroupWrapper['makeSwarmSubAccount']; + public swarmSubaccountSign: MetaGroupWrapper['swarmSubaccountSign']; + } + + export type MetaGroupActionsType = + | ['init', GroupWrapperConstructor] + + // shared actions + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + + // info actions + | MakeActionCall + | MakeActionCall + | MakeActionCall + + // member actions + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + + // keys actions + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall; +} diff --git a/types/index.d.ts b/types/index.d.ts index d766c8c..0693f7e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1 +1,5 @@ /// +/// +/// +/// +/// diff --git a/types/multi_encrypt/index.d.ts b/types/multi_encrypt/index.d.ts new file mode 100644 index 0000000..8183bb7 --- /dev/null +++ b/types/multi_encrypt/index.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/types/multi_encrypt/multi_encrypt.d.ts b/types/multi_encrypt/multi_encrypt.d.ts new file mode 100644 index 0000000..499075d --- /dev/null +++ b/types/multi_encrypt/multi_encrypt.d.ts @@ -0,0 +1,43 @@ +/// + +declare module 'libsession_util_nodejs' { + type MultiEncryptWrapper = { + multiEncrypt: (opts: { + /** + * len 64: ed25519 secretKey with pubkey + */ + ed25519SecretKey: Uint8ArrayLen64; + domain: EncryptionDomain; + messages: Array; + recipients: Array; + }) => Uint8Array; + multiDecryptEd25519: (opts: { + encoded: Uint8Array; + /** + * len 64: ed25519 secretKey with pubkey + */ + userEd25519SecretKey: Uint8ArrayLen64; + senderEd25519Pubkey: Uint8Array; + domain: EncryptionDomain; + }) => Uint8Array | null; + }; + + export type MultiEncryptActionsCalls = MakeWrapperActionCalls; + + /** + * To be used inside the web worker only (calls are synchronous and won't work asynchronously) + */ + export class MultiEncryptWrapperNode { + public static multiEncrypt: MultiEncryptWrapper['multiEncrypt']; + public static multiDecryptEd25519: MultiEncryptWrapper['multiDecryptEd25519']; + } + + /** + * Those actions are used internally for the web worker communication. + * You should never need to import them in Session directly + * You will need to add an entry here if you add a new function + */ + export type MultiEncryptActionsType = + | MakeActionCall + | MakeActionCall; +} diff --git a/types/shared.d.ts b/types/shared.d.ts new file mode 100644 index 0000000..69fbd47 --- /dev/null +++ b/types/shared.d.ts @@ -0,0 +1,162 @@ +declare module 'libsession_util_nodejs' { + type Uint8ArrayFixedLength = { + buffer: Uint8Array; + length: T; + }; + /** + * Allow a single type to be Nullable. i.e. string => string | null + */ + export type Nullable = T | null; + + /** + * Allow all the fields of a type to be -themselves- nullable. + * i.e. {field1: string, field2: number} => {field1: string | null, field2: number | null} + */ + type AllFieldsNullable = { + [P in keyof T]: Nullable; + }; + + type AsyncWrapper any> = ( + ...args: Parameters + ) => Promise>; + + export type RecordOfFunctions = Record any>; + + type MakeWrapperActionCalls = { + [Property in keyof Type]: AsyncWrapper; + }; + + export type ProfilePicture = { + url: string | null; + key: Uint8Array | null; + }; + + export type PushConfigResult = { + data: Uint8Array; + seqno: number; + hashes: Array; + namespace: number; + }; + + export type PushKeyConfigResult = Pick; + + export type ConfirmPush = [seqno: number, hash: string]; + export type MergeSingle = { hash: string; data: Uint8Array }; + + type MakeActionCall = [B, ...Parameters]; + + /** + * + * Base Config wrapper logic + * + */ + + export type BaseConfigWrapper = { + needsDump: () => boolean; + needsPush: () => boolean; + push: () => PushConfigResult; + dump: () => Uint8Array; + makeDump: () => Uint8Array; + confirmPushed: (seqno: number, hash: string) => void; + merge: (toMerge: Array) => Array; // merge returns the array of hashes that merged correctly + storageNamespace: () => number; + currentHashes: () => Array; + }; + + export type GenericWrapperActionsCall = ( + wrapperId: A, + ...args: Parameters + ) => Promise>; + + export type BaseConfigActions = + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall; + + export abstract class BaseConfigWrapperNode { + public needsDump: BaseConfigWrapper['needsDump']; + public needsPush: BaseConfigWrapper['needsPush']; + public push: BaseConfigWrapper['push']; + public dump: BaseConfigWrapper['dump']; + public makeDump: BaseConfigWrapper['makeDump']; + public confirmPushed: BaseConfigWrapper['confirmPushed']; + public merge: BaseConfigWrapper['merge']; + public storageNamespace: BaseConfigWrapper['storageNamespace']; + public currentHashes: BaseConfigWrapper['currentHashes']; + } + + export type BaseWrapperActionsCalls = MakeWrapperActionCalls; + + export type GroupPubkeyType = `03${string}`; // type of a string which starts by the 03 prefixed used for closed group + export type PubkeyType = `05${string}`; // type of a string which starts by the 05 prefixed used for **legacy** closed group and session ids + export type BlindedPubkeyType = `15${string}`; + + type MakeGroupActionCall = [ + B, + ...Parameters + ]; // all of the groupActionCalls need the pubkey of the group we are targeting + + type AsyncGroupWrapper any> = ( + groupPk: GroupPubkeyType, + ...args: Parameters + ) => Promise>; + + type MakeGroupWrapperActionCalls = { + [Property in keyof Omit]: AsyncGroupWrapper; + }; + + export type WithPriority = { priority: number }; // -1 means hidden, 0 means normal, > 1 means pinned + export type WithGroupPubkey = { groupPk: GroupPubkeyType }; + export type WithPubkey = { pubkey: PubkeyType }; + + type GroupInfoShared = { + name: string | null; + createdAtSeconds: number | null; + deleteAttachBeforeSeconds: number | null; + deleteBeforeSeconds: number | null; + expirySeconds: number | null; + profilePicture: ProfilePicture | null; + }; + + export type GroupInfoGet = GroupInfoShared & { + isDestroyed: boolean; + }; + + export type GroupInfoSet = GroupInfoShared & {}; + + export type SwarmSubAccountSignResult = { + subaccount: string; + subaccount_sig: string; // keeping the case like this so we can send it as is (this is what the snode API expects) + signature: string; + }; + + // those types are not enforced currently, they all are just Uint8Arrays, but having them separate right away will make the enforcing of them later, easier + export type Uint8ArrayLen64 = Uint8Array; + export type Uint8ArrayLen32 = Uint8Array; + export type Uint8ArrayLen36 = Uint8Array; // subaccount tokens are 36 bytes long + export type Uint8ArrayLen100 = Uint8Array; // subaccount auth data are 100 bytes long + + export type EncryptionDomain = 'SessionGroupKickedMessage'; + + export type ConstantsType = { + /** 100 bytes */ + CONTACT_MAX_NAME_LENGTH: number; + /** 100 bytes - for legacy groups and communities */ + BASE_GROUP_MAX_NAME_LENGTH: number; + /** 100 bytes */ + GROUP_INFO_MAX_NAME_LENGTH: number; + /** 411 bytes + * + * BASE_URL_MAX_LENGTH + '/r/' + ROOM_MAX_LENGTH + qs_pubkey.size() + hex pubkey + null terminator + */ + COMMUNITY_FULL_URL_MAX_LENGTH: number; + }; + + export const CONSTANTS: ConstantsType; +} diff --git a/user/contacts.d.ts b/types/user/contacts.d.ts similarity index 95% rename from user/contacts.d.ts rename to types/user/contacts.d.ts index a6872cf..9fcecfe 100644 --- a/user/contacts.d.ts +++ b/types/user/contacts.d.ts @@ -1,3 +1,5 @@ +/// + /** * * Contacts wrapper logic @@ -24,12 +26,11 @@ declare module 'libsession_util_nodejs' { // TODO legacy messages support will be removed in a future release | 'legacy'; - type ContactInfoShared = { + type ContactInfoShared = WithPriority & { id: string; name?: string; nickname?: string; profilePicture?: ProfilePicture; - priority: number; // -1 means hidden, 0 means normal, > 1 means pinned createdAtSeconds: number; // can only be set the first time a contact is created, a new change won't overide the value in the wrapper. expirationMode?: DisappearingMessageConversationModeType; expirationTimerSeconds?: number; diff --git a/user/convovolatile.d.ts b/types/user/convovolatile.d.ts similarity index 84% rename from user/convovolatile.d.ts rename to types/user/convovolatile.d.ts index eed63fb..fd2cc1c 100644 --- a/user/convovolatile.d.ts +++ b/types/user/convovolatile.d.ts @@ -1,3 +1,6 @@ +/// +/// + declare module 'libsession_util_nodejs' { export type ConvoVolatileType = '1o1' | UserGroupsType; @@ -8,6 +11,7 @@ declare module 'libsession_util_nodejs' { type ConvoInfoVolatile1o1 = BaseConvoInfoVolatile & { pubkeyHex: string }; type ConvoInfoVolatileLegacyGroup = BaseConvoInfoVolatile & { pubkeyHex: string }; + type ConvoInfoVolatileGroup = BaseConvoInfoVolatile & { pubkeyHex: GroupPubkeyType }; type ConvoInfoVolatileCommunity = BaseConvoInfoVolatile & CommunityDetails; // type ConvoInfoVolatileCommunity = BaseConvoInfoVolatile & { pubkeyHex: string }; // we need a `set` with the full url but maybe not for the `get` @@ -29,6 +33,12 @@ declare module 'libsession_util_nodejs' { setLegacyGroup: (pubkeyHex: string, lastRead: number, unread: boolean) => void; eraseLegacyGroup: (pubkeyHex: string) => boolean; + // group related methods + getGroup: (pubkeyHex: GroupPubkeyType) => ConvoInfoVolatileGroup | null; + getAllGroups: () => Array; + setGroup: (pubkeyHex: GroupPubkeyType, lastRead: number, unread: boolean) => void; + eraseGroup: (pubkeyHex: GroupPubkeyType) => boolean; + // communities related methods getCommunity: (communityFullUrl: string) => ConvoInfoVolatileCommunity | null; // pubkey not required getAllCommunities: () => Array; @@ -71,6 +81,10 @@ declare module 'libsession_util_nodejs' { | MakeActionCall | MakeActionCall | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall | MakeActionCall | MakeActionCall | MakeActionCall diff --git a/user/index.d.ts b/types/user/index.d.ts similarity index 100% rename from user/index.d.ts rename to types/user/index.d.ts diff --git a/user/userconfig.d.ts b/types/user/userconfig.d.ts similarity index 98% rename from user/userconfig.d.ts rename to types/user/userconfig.d.ts index 378c753..95ac407 100644 --- a/user/userconfig.d.ts +++ b/types/user/userconfig.d.ts @@ -18,7 +18,6 @@ declare module 'libsession_util_nodejs' { setName: (name: string) => void; setNameTruncated: (name: string) => void; setProfilePic: (pic: ProfilePicture) => void; - setEnableBlindedMsgRequest: (msgRequest: boolean) => void; getEnableBlindedMsgRequest: () => boolean | undefined; setNoteToSelfExpiry: (expirySeconds: number) => void; @@ -31,7 +30,7 @@ declare module 'libsession_util_nodejs' { export type UserConfigWrapperActionsCalls = MakeWrapperActionCalls; /** - * To be used inside the web worker only (calls are synchronous and won't work asynchrously) + * To be used inside the web worker only (calls are synchronous and won't work asynchronously) */ export class UserConfigWrapperNode extends BaseConfigWrapperNode { constructor(secretKey: Uint8Array, dump: Uint8Array | null); diff --git a/user/usergroups.d.ts b/types/user/usergroups.d.ts similarity index 53% rename from user/usergroups.d.ts rename to types/user/usergroups.d.ts index a0221b0..53cfa3a 100644 --- a/user/usergroups.d.ts +++ b/types/user/usergroups.d.ts @@ -1,3 +1,5 @@ +/// + declare module 'libsession_util_nodejs' { /** * @@ -5,7 +7,7 @@ declare module 'libsession_util_nodejs' { * */ - export type UserGroupsType = 'Community' | 'LegacyGroup'; + export type UserGroupsType = 'Community' | 'LegacyGroup' | 'Group'; export type CommunityDetails = { fullUrlWithPubkey: string; @@ -13,28 +15,69 @@ declare module 'libsession_util_nodejs' { roomCasePreserved: string; }; - export type CommunityInfo = CommunityDetails & { - pubkeyHex: string; - priority: number; // -1 means hidden, 0 means normal, > 0 means pinned. We currently don't support hidden communities on the client though - }; + export type CommunityInfo = CommunityDetails & + WithPriority & { + pubkeyHex: string; + }; export type LegacyGroupMemberInfo = { pubkeyHex: string; isAdmin: boolean; }; - export type LegacyGroupInfo = { + type BaseUserGroup = WithPriority & { + joinedAtSeconds: number; // equivalent to the lastJoinedTimestamp in Session desktop but in seconds rather than MS + // NOTE Properties need to be optional going forward to backward compatibility + disappearingTimerSeconds?: number; // in seconds, 0 or undefined == disabled. + }; + + export type LegacyGroupInfo = BaseUserGroup & { pubkeyHex: string; // The legacy group "session id" (33 bytes). name: string; // human-readable; this should normally always be set, but in theory could be set to an empty string. - encPubkey: Uint8Array; // bytes (32 or empty) + encPubkey: Uint8ArrayLen32; // bytes (32 or empty) encSeckey: Uint8Array; // bytes (32 or empty) - priority: number; // -1 means hidden, 0 means normal, > 1 means pinned. We currently don't support hidden groups on the client though members: Array; - joinedAtSeconds: number; // equivalent to the lastJoinedTimestamp in Session desktop but in seconds rather than MS - // NOTE Properties need to be optional going forward to backward compatibility - disappearingTimerSeconds?: number; // in seconds, 0 or undefined == disabled. }; + type UserGroupsGet = BaseUserGroup & { + /** + * The group "session id" (33 bytes), starting with 03. + */ + pubkeyHex: GroupPubkeyType; + /** + * The group admin secret key if we have it, length 64. + */ + secretKey: Uint8ArrayLen64 | null; + /** + * The group auth data we were given if we have it, length 100. + */ + authData: Uint8ArrayLen100 | null; + /** + * The group name. + */ + name: string | null; + /** + * True if the invite is pending, i.e. we've received it but haven't approved or auto-approved it yet. + */ + invitePending: boolean; + /** + * If we were kicked from that group, this will be true. + * Note: if the group was `destroyed` this will be false, but `destroyed` will be true + */ + kicked: boolean; + /** + * If the group was destroyed, this will be true + */ + destroyed: boolean; + }; + + /** + * We can set anything on a UserGroup and can omit fields by explicitly setting them to null. + * The only one which cannot be omitted is the pubkeyHex + */ + type UserGroupsSet = Pick & + AllFieldsNullable>; + type UserGroupsWrapper = BaseConfigWrapper & { init: (secretKey: Uint8Array, dump: Uint8Array | null) => void; /** This function is used to free wrappers from memory only */ @@ -62,6 +105,17 @@ declare module 'libsession_util_nodejs' { getAllLegacyGroups: () => Array; setLegacyGroup: (info: LegacyGroupInfo) => boolean; eraseLegacyGroup: (pubkeyHex: string) => boolean; + + // Groups related methods + // the create group always returns the secretKey as we've just created it + createGroup: () => UserGroupsGet & NonNullable>; + getGroup: (pubkeyHex: GroupPubkeyType) => UserGroupsGet | null; + getAllGroups: () => Array; + setGroup: (info: UserGroupsSet) => UserGroupsGet; + markGroupKicked: (pubkeyHex: GroupPubkeyType) => UserGroupsGet; + markGroupInvited: (pubkeyHex: GroupPubkeyType) => UserGroupsGet; + markGroupDestroyed: (pubkeyHex: GroupPubkeyType) => UserGroupsGet; + eraseGroup: (pubkeyHex: GroupPubkeyType) => boolean; }; export type UserGroupsWrapperActionsCalls = MakeWrapperActionCalls; @@ -80,6 +134,16 @@ declare module 'libsession_util_nodejs' { public getAllLegacyGroups: UserGroupsWrapper['getAllLegacyGroups']; public setLegacyGroup: UserGroupsWrapper['setLegacyGroup']; public eraseLegacyGroup: UserGroupsWrapper['eraseLegacyGroup']; + + // groups related methods + public createGroup: UserGroupsWrapper['createGroup']; + public getGroup: UserGroupsWrapper['getGroup']; + public getAllGroups: UserGroupsWrapper['getAllGroups']; + public setGroup: UserGroupsWrapper['setGroup']; + public markGroupKicked: UserGroupsWrapper['markGroupKicked']; + public markGroupInvited: UserGroupsWrapper['markGroupInvited']; + public markGroupDestroyed: UserGroupsWrapper['markGroupDestroyed']; + public eraseGroup: UserGroupsWrapper['eraseGroup']; } export type UserGroupsConfigActionsType = @@ -93,5 +157,13 @@ declare module 'libsession_util_nodejs' { | MakeActionCall | MakeActionCall | MakeActionCall - | MakeActionCall; + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall; } diff --git a/yarn.lock b/yarn.lock index 236d8c7..3212d31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,19 +28,19 @@ are-we-there-yet@^3.0.0: readable-stream "^3.6.0" async@^3.2.3: - version "3.2.5" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" - integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -axios@^1.6.5: - version "1.7.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" - integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== +axios@^1.3.2: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -82,24 +82,24 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" -cmake-js@^7.2.1: - version "7.3.0" - resolved "https://registry.yarnpkg.com/cmake-js/-/cmake-js-7.3.0.tgz#6fd6234b7aeec4545c1c806f9e3f7ffacd9798b2" - integrity sha512-dXs2zq9WxrV87bpJ+WbnGKv8WUBXDw8blNiwNHoRe/it+ptscxhQHKB1SJXa1w+kocLMeP28Tk4/eTCezg4o+w== +cmake-js@7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/cmake-js/-/cmake-js-7.2.1.tgz#757c0d39994121b084bab96290baf115ee7712cd" + integrity sha512-AdPSz9cSIJWdKvm0aJgVu3X8i0U3mNTswJkSHzZISqmYVjZk7Td4oDFg0mCBA383wO+9pG5Ix7pEP1CZH9x2BA== dependencies: - axios "^1.6.5" + axios "^1.3.2" debug "^4" - fs-extra "^11.2.0" + fs-extra "^10.1.0" lodash.isplainobject "^4.0.6" memory-stream "^1.0.0" - node-api-headers "^1.1.0" + node-api-headers "^0.0.2" npmlog "^6.0.2" rc "^1.2.7" - semver "^7.5.4" - tar "^6.2.0" + semver "^7.3.8" + tar "^6.1.11" url-join "^4.0.1" which "^2.0.2" - yargs "^17.7.2" + yargs "^17.6.0" color-convert@^2.0.1: version "2.0.1" @@ -136,11 +136,11 @@ console-control-strings@^1.1.0: integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== debug@^4: - version "4.3.5" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" - integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== dependencies: - ms "2.1.2" + ms "^2.1.3" deep-extend@^0.6.0: version "0.6.0" @@ -163,14 +163,14 @@ emoji-regex@^8.0.0: integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== escalade@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" - integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== follow-redirects@^1.15.6: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== form-data@^4.0.0: version "4.0.0" @@ -181,10 +181,10 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -fs-extra@^11.2.0: - version "11.2.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" - integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== +fs-extra@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" @@ -274,9 +274,9 @@ ini@~1.3.0: integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== is-core-module@^2.13.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" - integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== dependencies: hasown "^2.0.2" @@ -360,20 +360,20 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== node-addon-api@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== -node-api-headers@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/node-api-headers/-/node-api-headers-1.2.0.tgz#b717cd420aec79031f8dc83a50eb0a8bdf24c70d" - integrity sha512-L9AiEkBfgupC0D/LsudLPOhzy/EdObsp+FHyL1zSK0kKv5FDA9rJMoRz8xd+ojxzlqfg0tTZm2h8ot2nS7bgRA== +node-api-headers@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/node-api-headers/-/node-api-headers-0.0.2.tgz#31f4c6c2750b63e598128e76a60aefca6d76ac5d" + integrity sha512-YsjmaKGPDkmhoNKIpkChtCsPVaRE0a274IdERKnuc/E8K1UJdBZ4/mvI006OijlQZHCfpRNOH3dfHQs92se8gg== npmlog@^6.0.2: version "6.0.2" @@ -452,7 +452,7 @@ safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -semver@^7.5.4: +semver@^7.3.8: version "7.6.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== @@ -500,7 +500,7 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -tar@^6.2.0: +tar@^6.1.11: version "6.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== @@ -570,7 +570,7 @@ yargs-parser@^21.1.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^17.7.2: +yargs@^17.6.0: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==