diff --git a/include/groups/meta_group_wrapper.hpp b/include/groups/meta_group_wrapper.hpp index 88c2ed2..af6db07 100644 --- a/include/groups/meta_group_wrapper.hpp +++ b/include/groups/meta_group_wrapper.hpp @@ -44,6 +44,7 @@ class MetaGroupWrapper : public Napi::ObjectWrap { /* Shared Actions */ Napi::Value needsPush(const Napi::CallbackInfo& info); Napi::Value push(const Napi::CallbackInfo& info); + Napi::Value pushForRecovery(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); @@ -84,6 +85,7 @@ class MetaGroupWrapper : public Napi::ObjectWrap { Napi::Value loadKeyMessage(const Napi::CallbackInfo& info); Napi::Value keyGetCurrentGen(const Napi::CallbackInfo& info); Napi::Value activeHashes(const Napi::CallbackInfo& info); + Napi::Value activeHashesByConfig(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); diff --git a/src/groups/meta_group_wrapper.cpp b/src/groups/meta_group_wrapper.cpp index 18f2523..726dfc1 100644 --- a/src/groups/meta_group_wrapper.cpp +++ b/src/groups/meta_group_wrapper.cpp @@ -88,6 +88,7 @@ void MetaGroupWrapper::Init(Napi::Env env, Napi::Object exports) { // shared exposed functions InstanceMethod("needsPush", &MetaGroupWrapper::needsPush), InstanceMethod("push", &MetaGroupWrapper::push), + InstanceMethod("pushForRecovery", &MetaGroupWrapper::pushForRecovery), InstanceMethod("needsDump", &MetaGroupWrapper::needsDump), InstanceMethod("metaDump", &MetaGroupWrapper::metaDump), InstanceMethod("metaMakeDump", &MetaGroupWrapper::metaMakeDump), @@ -142,6 +143,7 @@ void MetaGroupWrapper::Init(Napi::Env env, Napi::Object exports) { InstanceMethod( "keyGetEncryptionKeyHex", &MetaGroupWrapper::keyGetEncryptionKeyHex), InstanceMethod("activeHashes", &MetaGroupWrapper::activeHashes), + InstanceMethod("activeHashesByConfig", &MetaGroupWrapper::activeHashesByConfig), InstanceMethod("loadKeyMessage", &MetaGroupWrapper::loadKeyMessage), InstanceMethod("keyGetCurrentGen", &MetaGroupWrapper::keyGetCurrentGen), InstanceMethod("encryptMessages", &MetaGroupWrapper::encryptMessages), @@ -199,6 +201,44 @@ Napi::Value MetaGroupWrapper::push(const Napi::CallbackInfo& info) { }); } +/** + * The current serialised bytes of GroupInfo and GroupMembers, whether or not they need pushing. + * + * This exists for ONE caller: putting a config back on the swarm after it expired from there. Do + * not reach for it when syncing — use push(), which returns null for a sub-config with nothing to + * send and is what every normal code path wants. Emitting a clean config during a sync would be + * pure redundant traffic, and would drain that config's obsolete-hash list as a side effect. + * + * Two things a caller has to know: + * + * - This still goes through ConfigBase::push(), which drains the config's obsolete-hash list and + * clears it unconditionally. Those hashes come back in `hashes` and are handed over exactly + * once, so a caller that ignores them loses them permanently and leaves the superseded messages + * on the swarm forever. Issue the delete for them like a normal push does. + * Note the drain is skipped for a read-only config (a group member), so an empty list there is + * the expected result rather than a sign anything went wrong — and a member holds no Delete + * permission to act on them with anyway. + * - GroupKeys is deliberately absent. There is no way to re-emit a stored keys message: + * groups::Keys has no push(), and pending_config() is only non-null while a rekey is in + * progress. An expired keys message can only be replaced by an admin rekeying. + */ +Napi::Value MetaGroupWrapper::pushForRecovery(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + auto env = info.Env(); + auto to_push = Napi::Object::New(env); + + to_push["groupMember"s] = push_result_to_JS( + env, + this->meta_group->members->push(), + this->meta_group->members->storage_namespace()); + + to_push["groupInfo"s] = push_result_to_JS( + env, this->meta_group->info->push(), this->meta_group->info->storage_namespace()); + + 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() || @@ -786,6 +826,35 @@ Napi::Value MetaGroupWrapper::activeHashes(const Napi::CallbackInfo& info) { }); } +/** + * The same hashes as activeHashes(), but kept separate per sub-config. + * + * activeHashes() concatenates all three, which is fine for bumping TTLs — the caller just needs + * the full set. It is not enough to act on a *specific* hash, though: once the storage server + * reports one missing, "which config was that?" decides what happens next, and the three answers + * are very different. A missing GroupInfo or GroupMembers hash can be put back by re-storing the + * config; a missing GroupKeys hash cannot be put back by anyone but an admin performing a rekey. + */ +Napi::Value MetaGroupWrapper::activeHashesByConfig(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + auto env = info.Env(); + auto obj = Napi::Object::New(env); + + auto keysHashes = meta_group->keys->active_hashes(); + auto infoHashes = meta_group->info->active_hashes(); + auto memberHashes = meta_group->members->active_hashes(); + + obj["groupKeys"s] = + toJs(env, std::vector{keysHashes.begin(), keysHashes.end()}); + obj["groupInfo"s] = + toJs(env, std::vector{infoHashes.begin(), infoHashes.end()}); + obj["groupMember"s] = + toJs(env, std::vector{memberHashes.begin(), memberHashes.end()}); + + return obj; + }); +} + Napi::Value MetaGroupWrapper::encryptMessages(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { assertInfoLength(info, 1); diff --git a/types/groups/metagroup.d.ts b/types/groups/metagroup.d.ts index 52aa8c6..6fb79f9 100644 --- a/types/groups/metagroup.d.ts +++ b/types/groups/metagroup.d.ts @@ -42,6 +42,34 @@ declare module 'libsession_util_nodejs' { namespace: number; } | null; }; + /** + * The current bytes of groupInfo and groupMember whether or not they need pushing, for + * putting a config back on the swarm after it expired from there. + * + * Use `push()` for syncing — this one deliberately ignores `needsPush()`, so using it to + * sync would send configs that haven't changed. `hashes` still carries the config's + * obsolete hashes and is still handed over only once, so the caller must issue the delete + * for them (expect an empty list on a read-only config — a group member — which is normal). + * + * groupKeys is absent because a stored keys message cannot be re-emitted at all; only an + * admin rekey can replace one. + */ + pushForRecovery: () => { + groupInfo: PushConfigResult; + groupMember: PushConfigResult; + }; + /** + * The same hashes as `activeHashes()`, kept separate per sub-config. + * + * `activeHashes()` merges all three, which is enough to bump TTLs but not to act on a + * specific hash: a missing groupInfo/groupMember hash can be re-stored, a missing groupKeys + * hash can only be replaced by an admin rekey. + */ + activeHashesByConfig: () => { + groupInfo: Array; + groupMember: Array; + groupKeys: Array; + }; needsDump: () => boolean; metaDump: () => Uint8Array; metaMakeDump: () => Uint8Array; @@ -52,6 +80,15 @@ declare module 'libsession_util_nodejs' { groupInfo: ConfirmPush | null; groupMember: ConfirmPush | null; }) => void; + /** + * @returns how many of the messages handed in were merged. Compare it against what you + * passed: libSession skips a config message it cannot take and carries on without erroring, + * so a partial merge is invisible to a caller that only checks for a throw. + * + * Note the keys count is optimistic — a keys message is counted even when it turns out not + * to be encrypted to us, which is not a failure. So this detects a lossy groupInfo or + * groupMember merge, which is what matters. + */ metaMerge: ({ groupInfo, groupKeys, @@ -60,7 +97,7 @@ declare module 'libsession_util_nodejs' { groupInfo: Array | null; groupMember: Array | null; groupKeys: Array | null; - }) => void; + }) => number; }; // this just adds an argument of type GroupPubkeyType in front of the parameters of that function @@ -78,12 +115,14 @@ declare module 'libsession_util_nodejs' { // shared actions public needsPush: MetaGroupWrapper['needsPush']; public push: MetaGroupWrapper['push']; + public pushForRecovery: MetaGroupWrapper['pushForRecovery']; public needsDump: MetaGroupWrapper['needsDump']; public metaDump: MetaGroupWrapper['metaDump']; public metaMakeDump: MetaGroupWrapper['metaMakeDump']; public metaConfirmPushed: MetaGroupWrapper['metaConfirmPushed']; public metaMerge: MetaGroupWrapper['metaMerge']; public activeHashes: MetaGroupWrapper['activeHashes']; + public activeHashesByConfig: MetaGroupWrapper['activeHashesByConfig']; // info public infoGet: MetaGroupWrapper['infoGet']; @@ -128,6 +167,7 @@ declare module 'libsession_util_nodejs' { // shared actions | MakeActionCall | MakeActionCall + | MakeActionCall | MakeActionCall | MakeActionCall | MakeActionCall @@ -169,6 +209,7 @@ declare module 'libsession_util_nodejs' { | MakeActionCall | MakeActionCall | MakeActionCall + | MakeActionCall | MakeActionCall | MakeActionCall | MakeActionCall