From 540b51af4375d027f81b35fc20967d4a36bde73f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 23:17:08 -0300 Subject: [PATCH 01/19] WIP: re-pin vendored libsession to 73e4d1e6 (Pro wire-format refactor) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump the libsession-util submodule from 99f1b52 (v1.6.0-10) to 73e4d1e6 (v1.6.3-24, jagerman/libsession-util `pro-backend-updates-final`) — the FINAL PIN for the Session Pro wire-format redesign. This brings the redesigned session::pro_backend free-function API (request builders returning {endpoint, body}, paired response parsers, provider_urls, opaque payment_id, ms->seconds, revocation_tag) plus the renamed protobuf/config fields. The N-API glue (src/pro/*, src/constants.cpp, types/pro/pro.d.ts) still targets the OLD API and will NOT build until reworked to match — done in follow-up commits. Needs `git submodule update --init --recursive` inside libsession-util before building. Co-Authored-By: Claude Opus 4.8 (1M context) --- libsession-util | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsession-util b/libsession-util index 99f1b52..73e4d1e 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 99f1b5201c68a48f700ecb58c284d1a584f0699a +Subproject commit 73e4d1e65db1bfeb5949034bf3ca52724e002ab3 From 25374412e02e43923bcbee3d759542c55150fc4f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 23:18:07 -0300 Subject: [PATCH 02/19] =?UTF-8?q?WIP:=20nodejs=20glue=20=E2=80=94=20drop?= =?UTF-8?q?=20LIBSESSION=5FPRO=5FPROVIDERS=20metadata=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit libsession stopped shipping provider display metadata (store/platform/account NAMES are translation data owned by each client; the per-provider URLs move to an on-demand ProWrapper.providerUrls(code) lookup). Remove the make_provider block + LIBSESSION_PRO_PROVIDERS constant; keep the general LIBSESSION_PRO_URLS. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/constants.cpp | 43 ++++--------------------------------------- 1 file changed, 4 insertions(+), 39 deletions(-) diff --git a/src/constants.cpp b/src/constants.cpp index 6e403ae..96d0f85 100644 --- a/src/constants.cpp +++ b/src/constants.cpp @@ -4,7 +4,6 @@ #include "session/config/contacts.hpp" #include "session/config/groups/info.hpp" #include "session/config/user_groups.hpp" -#include "session/pro_backend.h" #include "session/session_protocol.h" #include "session/version.h" #include "utilities.hpp" @@ -24,43 +23,10 @@ Napi::Object ConstantsWrapper::Init(Napi::Env env, Napi::Object exports) { pro_urls["pro_access_not_found"] = toJs(env, SESSION_PROTOCOL_STRINGS.url_pro_access_not_found); pro_urls["support_url"] = toJs(env, SESSION_PROTOCOL_STRINGS.url_pro_support); - auto make_provider = [&](int provider, int other_provider) { - const auto& meta = SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[provider]; - auto obj = Napi::Object::New(env); - obj["device"] = toJs(env, meta.device); - obj["store"] = toJs(env, meta.store); - obj["platform"] = toJs(env, meta.platform); - obj["platform_account"] = toJs(env, meta.platform_account); - obj["refund_support_url"] = toJs(env, meta.refund_support_url); - obj["refund_status_url"] = toJs(env, meta.refund_status_url); - obj["refund_platform_url"] = toJs(env, meta.refund_platform_url); - obj["update_subscription_url"] = toJs(env, meta.update_subscription_url); - obj["cancel_subscription_url"] = toJs(env, meta.cancel_subscription_url); - obj["store_other"] = - toJs(env, SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[other_provider].store); - return obj; - }; - - auto pro_provider_nil = make_provider( - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL, SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL); - auto pro_provider_google = make_provider( - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE); - auto pro_provider_ios = make_provider( - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE); - - auto pro_provider_rangeproof = make_provider( - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF, - // Use NIL as the second provider for Rangeproof so that it does not define an alternate - // store label (i.e., no explicit "other" store for Rangeproof in these constants). - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL); - - auto pro_providers = Napi::Object::New(env); - pro_providers["Nil"] = toJs(env, pro_provider_nil); - pro_providers["Google"] = toJs(env, pro_provider_google); - pro_providers["iOS"] = toJs(env, pro_provider_ios); - pro_providers["Rangeproof"] = toJs(env, pro_provider_rangeproof); + // Provider display metadata (store/platform/account NAMES) is no longer shipped by libsession — + // those are translation data owned by each client (keyed on the provider slug). The per-provider + // support/management URLs are still libsession-owned but are now fetched on demand via + // ProWrapper.providerUrls(code) rather than baked into a constants table here. // construct javascript constants object Napi::Function cls = DefineClass( @@ -87,7 +53,6 @@ Napi::Object ConstantsWrapper::Init(Napi::Env env, Napi::Object exports) { Napi::Number::New(env, session::config::community::FULL_URL_MAX_LENGTH), napi_enumerable), ObjectWrap::StaticValue("LIBSESSION_PRO_URLS", pro_urls, napi_enumerable), - ObjectWrap::StaticValue("LIBSESSION_PRO_PROVIDERS", pro_providers, napi_enumerable), ObjectWrap::StaticValue( "LIBSESSION_UTIL_VERSION", Napi::String::New(env, LIBSESSION_UTIL_VERSION_FULL), From e5162784eed3777348a7674ac3387cfe6541d8c2 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 23:37:37 -0300 Subject: [PATCH 03/19] =?UTF-8?q?WIP:=20nodejs=20glue=20=E2=80=94=20rework?= =?UTF-8?q?=20Pro=20N-API=20surface=20to=20the=20redesigned=20C++=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebind the ProWrapper N-API surface to session::pro_backend's redesigned free-function API (libsession is now the single source of truth for both request bodies and response parsing). Requests (include/pro/pro.hpp): proProofRequest / proRevocationsRequest / proStatusRequest now drop the wire `version`, take whole-second timestamps (ms->seconds at the boundary), widen ticket to 64-bit, and return {endpoint, body} (ProRequest) instead of a bare body string. Responses (NEW): parseProProofResponse / parseRevocationsResponse / parsePaymentDetailsResponse marshal the parsed C++ structs to JS — so desktop deletes its zod wire schemas and consumes these instead. status/user-status enums are surfaced as their raw ints (they line up 1:1 with the desktop numeric enums); provider/plan are opaque wire slugs; payment collapses to one opaque payment_id; timestamps emit ms (JS domain). Provider URLs: NEW providerUrls(code) -> the 5 support/management URLs or null. Display NAMES are gone from libsession (client i18n owns them). pro.cpp: drop the now-dead provider/plan/status enum->string helpers (only the pro-features status helper remains). Config glue caught by the same re-pin (proof/convo/user-profile fields): - pro/types.hpp ProProof: gen_index_hash->revocation_tag, expiry ms->seconds at the C++ boundary (JS keeps genIndexHashB64/expiryMs). - convo_info_volatile_config: pro_gen_index_hash->pro_revocation_tag, pro_expiry_unix_ts now whole seconds. - user_config: proof.revocation_tag, proof.expiry_unix_ts seconds, and get/set_pro_access_expiry now whole seconds (ms<->seconds at the boundary). types/pro/pro.d.ts + shared.d.ts: drop ProOriginatingPlatform / ProBackendProviderConstant* / LIBSESSION_PRO_PROVIDERS; add ProRequest, the response-parser return types, ProviderUrls, and the new method signatures. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/pro/pro.hpp | 303 +++++++++++++++++++++-------- include/pro/types.hpp | 6 +- src/convo_info_volatile_config.cpp | 19 +- src/pro/pro.cpp | 100 +--------- src/user_config.cpp | 26 ++- types/pro/pro.d.ts | 194 +++++++++++++----- types/shared.d.ts | 2 - 7 files changed, 406 insertions(+), 244 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index 5ea7f40..7065c87 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -4,24 +4,18 @@ #include #include +#include #include #include #include "meta/meta_base_wrapper.hpp" #include "pro/types.hpp" -#include "session/pro_backend.h" #include "session/pro_backend.hpp" -#include "session/session_protocol.h" #include "session/session_protocol.hpp" #include "utilities.hpp" namespace session::nodeapi { -std::string_view proBackendEnumToString(SESSION_PRO_BACKEND_PAYMENT_PROVIDER v); -std::string_view proBackendEnumToString(SESSION_PRO_BACKEND_PAYMENT_STATUS v); -std::string_view proBackendEnumToString(SESSION_PRO_BACKEND_PLAN v); -std::string_view proBackendEnumToString(SESSION_PRO_BACKEND_USER_PRO_STATUS v); -std::string_view proBackendEnumToString(SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT v); std::string_view proBackendEnumToString(session::ProFeaturesForMsgStatus v); class ProWrapper : public Napi::ObjectWrap { @@ -51,23 +45,63 @@ class ProWrapper : public Napi::ObjectWrap { static_cast( napi_writable | napi_configurable)), - // Pro requests - StaticMethod<&ProWrapper::proProofRequestBody>( - "proProofRequestBody", + // Pro requests -> {endpoint, body} + StaticMethod<&ProWrapper::proProofRequest>( + "proProofRequest", static_cast( napi_writable | napi_configurable)), - StaticMethod<&ProWrapper::proRevocationsRequestBody>( - "proRevocationsRequestBody", + StaticMethod<&ProWrapper::proRevocationsRequest>( + "proRevocationsRequest", static_cast( napi_writable | napi_configurable)), - StaticMethod<&ProWrapper::proStatusRequestBody>( - "proStatusRequestBody", + StaticMethod<&ProWrapper::proStatusRequest>( + "proStatusRequest", + static_cast( + napi_writable | napi_configurable)), + + // Pro response parsers -> typed structs (libsession is the source of truth) + StaticMethod<&ProWrapper::parseProProofResponse>( + "parseProProofResponse", + static_cast( + napi_writable | napi_configurable)), + StaticMethod<&ProWrapper::parseRevocationsResponse>( + "parseRevocationsResponse", + static_cast( + napi_writable | napi_configurable)), + StaticMethod<&ProWrapper::parsePaymentDetailsResponse>( + "parsePaymentDetailsResponse", + static_cast( + napi_writable | napi_configurable)), + + // Per-provider support/management URLs (or null) + StaticMethod<&ProWrapper::providerUrls>( + "providerUrls", static_cast( napi_writable | napi_configurable)), }); } private: + // The wire carries whole-second timestamps; the JS domain is milliseconds, so we convert at this + // boundary (the signed request timestamp round-trips losslessly as the value is always seconds). + static std::chrono::sys_seconds unixTsMsToSeconds(Napi::Value v, const std::string& id) { + return std::chrono::floor(toCppSysMs(v, id)); + } + + static Napi::Object proRequestToJs(const Napi::Env& env, const pro_backend::ProRequest& req) { + auto obj = Napi::Object::New(env); + obj["endpoint"] = toJs(env, req.endpoint); + obj["body"] = toJs(env, req.body); + return obj; + } + + static Napi::Array errorsToJs(const Napi::Env& env, const std::vector& errors) { + auto arr = Napi::Array::New(env, errors.size()); + for (size_t i = 0; i < errors.size(); i++) + arr.Set(i, toJs(env, errors[i])); + return arr; + } + static Napi::Value proFeaturesForMessage(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { // we expect one argument that matches: @@ -84,8 +118,6 @@ class ProWrapper : public Napi::ObjectWrap { if (first.IsEmpty()) throw std::invalid_argument("proFeaturesForMessage first received empty"); - auto lossless = true; - assertIsString(first.Get("utf16"), "proFeaturesForMessage.utf16"); std::u16string utf16 = first.Get("utf16").As().Utf16Value(); ProFeaturesForMsg pro_features_msg = @@ -120,8 +152,6 @@ class ProWrapper : public Napi::ObjectWrap { if (first.IsEmpty()) throw std::invalid_argument("utf16Count first received empty"); - auto lossless = true; - assertIsString(first.Get("utf16"), "utf16Count.utf16"); std::u16string utf16 = first.Get("utf16").As().Utf16Value(); size_t codepoint_count = session::utf16_count(utf16); @@ -166,11 +196,9 @@ class ProWrapper : public Napi::ObjectWrap { }); }; - static Napi::Value proProofRequestBody(const Napi::CallbackInfo& info) { + static Napi::Value proProofRequest(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { - // we expect arguments that match: // first: { - // "requestVersion": number, // "masterPrivKeyHex": string, // "rotatingPrivKeyHex": string, // "unixTsMs": number, @@ -182,43 +210,29 @@ class ProWrapper : public Napi::ObjectWrap { auto first = info[0].As(); if (first.IsEmpty()) - throw std::invalid_argument("proProofRequestBody first received empty"); - - assertIsNumber(first.Get("requestVersion"), "proProofRequestBody.requestVersion"); - Napi::Number requestVersion = first.Get("requestVersion").As(); - assertIsNumber(first.Get("unixTsMs"), "proProofRequestBody.unixTsMs"); - auto unix_ts_ms = toCppSysMs(first.Get("unixTsMs"), "proProofRequestBody.unixTsMs"); - - assertIsString(first.Get("masterPrivKeyHex"), "proProofRequestBody.masterPrivKeyHex"); - assertIsString( - first.Get("rotatingPrivKeyHex"), "proProofRequestBody.rotatingPrivKeyHex"); - - auto master_privkey_js = first.Get("masterPrivKeyHex"); - auto rotating_privkey_js = first.Get("rotatingPrivKeyHex"); - std::string master_privkey = - toCppString(master_privkey_js, "proProofRequestBody.masterPrivKeyHex"); - std::string rotating_privkey = - toCppString(rotating_privkey_js, "proProofRequestBody.rotatingPrivKeyHex"); - - auto master_privkey_decoded = from_hex(master_privkey); - auto rotating_privkey_decoded = from_hex(rotating_privkey); - - std::string json = pro_backend::GenerateProProofRequest::build_to_json( - static_cast(requestVersion.Int32Value()), - to_span(master_privkey_decoded), - to_span(rotating_privkey_decoded), - unix_ts_ms); - - return json; + throw std::invalid_argument("proProofRequest first received empty"); + + assertIsNumber(first.Get("unixTsMs"), "proProofRequest.unixTsMs"); + auto unix_ts = unixTsMsToSeconds(first.Get("unixTsMs"), "proProofRequest.unixTsMs"); + + assertIsString(first.Get("masterPrivKeyHex"), "proProofRequest.masterPrivKeyHex"); + assertIsString(first.Get("rotatingPrivKeyHex"), "proProofRequest.rotatingPrivKeyHex"); + auto master_privkey = from_hex( + toCppString(first.Get("masterPrivKeyHex"), "proProofRequest.masterPrivKeyHex")); + auto rotating_privkey = from_hex(toCppString( + first.Get("rotatingPrivKeyHex"), "proProofRequest.rotatingPrivKeyHex")); + + auto req = session::pro_backend::pro_proof_request( + to_span(master_privkey), to_span(rotating_privkey), unix_ts); + + return proRequestToJs(env, req); }); }; - static Napi::Value proRevocationsRequestBody(const Napi::CallbackInfo& info) { + static Napi::Value proRevocationsRequest(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { - // we expect arguments that match: // first: { - // "requestVersion": number, - // "ticket": number, + // "ticket": number, // 64-bit; 0 if unknown // } assertInfoLength(info, 1); @@ -226,29 +240,21 @@ class ProWrapper : public Napi::ObjectWrap { auto env = info.Env(); auto first = info[0].As(); - if (first.IsEmpty()) - throw std::invalid_argument("proRevocationsRequestBody first received empty"); + throw std::invalid_argument("proRevocationsRequest first received empty"); - assertIsNumber(first.Get("requestVersion"), "proRevocationsRequestBody.requestVersion"); - assertIsNumber(first.Get("ticket"), "proRevocationsRequestBody.ticket"); - auto requestVersion = first.Get("requestVersion").As(); - auto ticket = first.Get("ticket").As(); + assertIsNumber(first.Get("ticket"), "proRevocationsRequest.ticket"); + auto ticket = first.Get("ticket").As().Int64Value(); - auto revocationsRequest = pro_backend::GetProRevocationsRequest{ - .version = static_cast(requestVersion.Int32Value()), - .ticket = ticket.Uint32Value(), - }; + auto req = session::pro_backend::revocations_request(ticket); - return revocationsRequest.to_json(); + return proRequestToJs(env, req); }); }; - static Napi::Value proStatusRequestBody(const Napi::CallbackInfo& info) { + static Napi::Value proStatusRequest(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { - // we expect arguments that match: // first: { - // "requestVersion": number, // "masterPrivKeyHex": string, // "unixTsMs": number, // "count": number, @@ -259,31 +265,156 @@ class ProWrapper : public Napi::ObjectWrap { auto env = info.Env(); auto first = info[0].As(); - if (first.IsEmpty()) - throw std::invalid_argument("proStatusRequestBody first received empty"); + throw std::invalid_argument("proStatusRequest first received empty"); - assertIsNumber(first.Get("requestVersion"), "proStatusRequestBody.requestVersion"); - assertIsNumber(first.Get("unixTsMs"), "proStatusRequestBody.unixTsMs"); - assertIsNumber(first.Get("count"), "proStatusRequestBody.count"); - auto requestVersion = first.Get("requestVersion").As(); - auto unix_ts_ms = toCppSysMs(first.Get("unixTsMs"), "proStatusRequestBody.unixTsMs"); - auto count = toCppInteger(first.Get("count"), "proStatusRequestBody.count"); - assertIsString(first.Get("masterPrivKeyHex"), "proStatusRequestBody.masterPrivKeyHex"); + assertIsNumber(first.Get("unixTsMs"), "proStatusRequest.unixTsMs"); + assertIsNumber(first.Get("count"), "proStatusRequest.count"); + auto unix_ts = unixTsMsToSeconds(first.Get("unixTsMs"), "proStatusRequest.unixTsMs"); + auto count = toCppInteger(first.Get("count"), "proStatusRequest.count"); - auto master_privkey_js = first.Get("masterPrivKeyHex"); - auto master_privkey = - toCppString(master_privkey_js, "proStatusRequestBody.masterPrivKeyHex"); + assertIsString(first.Get("masterPrivKeyHex"), "proStatusRequest.masterPrivKeyHex"); + auto master_privkey = from_hex( + toCppString(first.Get("masterPrivKeyHex"), "proStatusRequest.masterPrivKeyHex")); - auto master_privkey_decoded = from_hex(master_privkey); + auto req = session::pro_backend::payment_details_request( + to_span(master_privkey), unix_ts, static_cast(count)); - auto json = pro_backend::GetProDetailsRequest::build_to_json( - static_cast(requestVersion.Int32Value()), - to_span(master_privkey_decoded), - unix_ts_ms, - count); + return proRequestToJs(env, req); + }); + }; - return json; + static std::string requestJsonArg(const Napi::CallbackInfo& info, const std::string& id) { + assertInfoLength(info, 1); + assertIsObject(info[0]); + auto first = info[0].As(); + if (first.IsEmpty()) + throw std::invalid_argument(id + " first received empty"); + assertIsString(first.Get("json"), id + ".json"); + return toCppString(first.Get("json"), id + ".json"); + } + + static Napi::Value parseProProofResponse(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + auto env = info.Env(); + auto json = requestJsonArg(info, "parseProProofResponse"); + + auto resp = session::pro_backend::parse_pro_proof(json); + + auto obj = Napi::Object::New(env); + obj["status"] = toJs(env, resp.status); + obj["errors"] = errorsToJs(env, resp.errors); + obj["proof"] = toJs(env, resp.proof); + return obj; + }); + }; + + static Napi::Value parseRevocationsResponse(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + auto env = info.Env(); + auto json = requestJsonArg(info, "parseRevocationsResponse"); + + auto resp = session::pro_backend::parse_revocations(json); + + auto obj = Napi::Object::New(env); + obj["status"] = toJs(env, resp.status); + obj["errors"] = errorsToJs(env, resp.errors); + obj["ticket"] = toJs(env, resp.ticket); + obj["retryInS"] = toJs(env, static_cast(resp.retry_in.count())); + obj["retainForS"] = toJs(env, static_cast(resp.retain_for.count())); + + auto items = Napi::Array::New(env, resp.items.size()); + for (size_t i = 0; i < resp.items.size(); i++) { + auto item = Napi::Object::New(env); + item["genIndexHashB64"] = toJs(env, oxenc::to_base64(resp.items[i].revocation_tag)); + // effective instant (whole seconds) -> ms JS domain + item["effectiveMs"] = toJs( + env, resp.items[i].effective_unix_ts.time_since_epoch().count() * 1000); + items.Set(i, item); + } + obj["items"] = items; + return obj; + }); + }; + + static Napi::Value parsePaymentDetailsResponse(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + auto env = info.Env(); + auto json = requestJsonArg(info, "parsePaymentDetailsResponse"); + + auto resp = session::pro_backend::parse_payment_details(json); + + auto obj = Napi::Object::New(env); + obj["status"] = toJs(env, resp.status); + obj["errors"] = errorsToJs(env, resp.errors); + obj["userStatus"] = toJs(env, static_cast(resp.user_status)); + obj["errorReport"] = toJs(env, static_cast(resp.error_report)); + obj["autoRenewing"] = toJs(env, resp.auto_renewing); + obj["expiryMs"] = toJs(env, resp.expiry_unix_ts.time_since_epoch().count() * 1000); + obj["gracePeriodDurationMs"] = + toJs(env, static_cast(resp.grace_period_duration.count()) * 1000); + obj["refundRequestedTsMs"] = + toJs(env, resp.refund_requested_unix_ts.time_since_epoch().count() * 1000); + obj["paymentsTotal"] = toJs(env, resp.payments_total); + + auto items = Napi::Array::New(env, resp.items.size()); + for (size_t i = 0; i < resp.items.size(); i++) { + const auto& src = resp.items[i]; + auto item = Napi::Object::New(env); + item["status"] = toJs(env, static_cast(src.status)); + item["plan"] = toJs(env, src.plan); + item["paymentProvider"] = toJs(env, src.payment_provider); + item["autoRenewing"] = toJs(env, src.auto_renewing); + // purchased/revoked are millisecond-resolution sys_ms (count() is already ms) + item["purchasedTsMs"] = toJs(env, src.purchased_unix_ts.time_since_epoch().count()); + item["revokedTsMs"] = toJs(env, src.revoked_unix_ts.time_since_epoch().count()); + // the rest are whole seconds -> ms + item["redeemedTsMs"] = + toJs(env, src.redeemed_unix_ts.time_since_epoch().count() * 1000); + item["expiryTsMs"] = + toJs(env, src.expiry_unix_ts.time_since_epoch().count() * 1000); + item["gracePeriodDurationMs"] = + toJs(env, static_cast(src.grace_period_duration.count()) * 1000); + item["platformRefundExpiryTsMs"] = toJs( + env, src.platform_refund_expiry_unix_ts.time_since_epoch().count() * 1000); + item["refundRequestedTsMs"] = + toJs(env, src.refund_requested_unix_ts.time_since_epoch().count() * 1000); + item["paymentId"] = toJs(env, src.payment_id); + items.Set(i, item); + } + obj["items"] = items; + return obj; + }); + }; + + static Napi::Value providerUrls(const Napi::CallbackInfo& info) { + return wrapResult(info, [&]() -> Napi::Value { + // first: { + // "code": string, // provider slug, e.g. "app_store" + // } + + assertInfoLength(info, 1); + assertIsObject(info[0]); + auto env = info.Env(); + + auto first = info[0].As(); + if (first.IsEmpty()) + throw std::invalid_argument("providerUrls first received empty"); + + assertIsString(first.Get("code"), "providerUrls.code"); + auto code = toCppString(first.Get("code"), "providerUrls.code"); + + auto urls = session::pro_backend::provider_urls(code); + if (!urls) + return env.Null(); + + auto obj = Napi::Object::New(env); + obj["refundPlatformUrl"] = toJs(env, urls->refund_platform_url); + obj["refundSupportUrl"] = toJs(env, urls->refund_support_url); + obj["refundStatusUrl"] = toJs(env, urls->refund_status_url); + obj["updateSubscriptionUrl"] = toJs(env, urls->update_subscription_url); + obj["cancelSubscriptionUrl"] = toJs(env, urls->cancel_subscription_url); + return obj; }); }; }; diff --git a/include/pro/types.hpp b/include/pro/types.hpp index ed717b8..057ad2c 100644 --- a/include/pro/types.hpp +++ b/include/pro/types.hpp @@ -16,9 +16,11 @@ struct toJs_impl { auto obj = Napi::Object::New(env); obj["version"] = toJs(env, pro_proof.version); - obj["genIndexHashB64"] = toJs(env, oxenc::to_base64(pro_proof.gen_index_hash)); + // `revocation_tag` (renamed from `gen_index_hash`); kept the JS key `genIndexHashB64` to avoid + // churning every desktop consumer. `expiry_unix_ts` is now whole seconds -> emit ms (the JS domain). + obj["genIndexHashB64"] = toJs(env, oxenc::to_base64(pro_proof.revocation_tag)); obj["rotatingPubkeyHex"] = toJs(env, oxenc::to_hex(pro_proof.rotating_pubkey)); - obj["expiryMs"] = toJs(env, pro_proof.expiry_unix_ts.time_since_epoch().count()); + obj["expiryMs"] = toJs(env, pro_proof.expiry_unix_ts.time_since_epoch().count() * 1000); obj["signatureHex"] = toJs(env, oxenc::to_hex(pro_proof.sig)); return obj; diff --git a/src/convo_info_volatile_config.cpp b/src/convo_info_volatile_config.cpp index 74dc6d1..95da70a 100644 --- a/src/convo_info_volatile_config.cpp +++ b/src/convo_info_volatile_config.cpp @@ -58,13 +58,15 @@ struct toJs_impl { obj["pubkeyHex"] = toJs(env, info_1o1.session_id); addBaseValues(env, obj, info_1o1); - if (info_1o1.pro_gen_index_hash->empty() || + if (info_1o1.pro_revocation_tag->empty() || !info_1o1.pro_expiry_unix_ts.time_since_epoch().count()) { obj["proGenIndexHashB64"] = env.Null(); obj["proExpiryTsMs"] = env.Null(); } else { - obj["proGenIndexHashB64"] = toJs(env, to_base64(*info_1o1.pro_gen_index_hash)); - obj["proExpiryTsMs"] = toJs(env, info_1o1.pro_expiry_unix_ts); + obj["proGenIndexHashB64"] = toJs(env, to_base64(*info_1o1.pro_revocation_tag)); + // config field is now whole seconds; the JS `proExpiryTsMs` key stays milliseconds + obj["proExpiryTsMs"] = + toJs(env, info_1o1.pro_expiry_unix_ts.time_since_epoch().count() * 1000); } return obj; @@ -191,16 +193,17 @@ void ConvoInfoVolatileWrapper::set1o1(const Napi::CallbackInfo& info) { if (proGenIndexHashB64Cpp.has_value()) { if (proGenIndexHashB64Cpp->empty()) { // if the first is set, but empty, we want to reset the field - convo.pro_gen_index_hash = std::nullopt; + convo.pro_revocation_tag = std::nullopt; } else { // this throws if the size is wrong - convo.pro_gen_index_hash = from_base64_to_array<32>(*proGenIndexHashB64Cpp); + convo.pro_revocation_tag = from_base64_to_array<32>(*proGenIndexHashB64Cpp); } } if (proExpiryUnixTsMsCpp.has_value()) { - // if the field is set (not null), we want to write the change as is - convo.pro_expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(*proExpiryUnixTsMsCpp)); + // if the field is set (not null), we want to write the change as is (ms -> whole seconds) + convo.pro_expiry_unix_ts = std::chrono::floor( + std::chrono::sys_time( + std::chrono::milliseconds(*proExpiryUnixTsMsCpp))); } config.set(convo); diff --git a/src/pro/pro.cpp b/src/pro/pro.cpp index 01e1e0b..8c8674d 100644 --- a/src/pro/pro.cpp +++ b/src/pro/pro.cpp @@ -2,102 +2,10 @@ namespace session::nodeapi { -std::string_view ProBackendEnumToString(SESSION_PRO_BACKEND_PAYMENT_PROVIDER v) { - switch (v) { - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL: return "NIL"; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE: return "GOOGLE_PLAY_STORE"; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE: return "IOS_APP_STORE"; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: return "RANGEPROOF"; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT: - throw std::invalid_argument("SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT"); - } - UNREACHABLE(); -} - -std::string_view ProBackendEnumToString(SESSION_PRO_BACKEND_PAYMENT_STATUS v) { - switch (v) { - case SESSION_PRO_BACKEND_PAYMENT_STATUS_NIL: return "NIL"; - case SESSION_PRO_BACKEND_PAYMENT_STATUS_UNREDEEMED: return "UNREDEEMED"; - case SESSION_PRO_BACKEND_PAYMENT_STATUS_REDEEMED: return "REDEEMED"; - case SESSION_PRO_BACKEND_PAYMENT_STATUS_EXPIRED: return "EXPIRED"; - case SESSION_PRO_BACKEND_PAYMENT_STATUS_REFUNDED: return "REFUNDED"; - case SESSION_PRO_BACKEND_PAYMENT_STATUS_COUNT: - throw std::invalid_argument("SESSION_PRO_BACKEND_PAYMENT_STATUS_COUNT"); - } - UNREACHABLE(); -} - -std::string_view proBackendEnumToString(SESSION_PRO_BACKEND_PLAN v) { - switch (v) { - case SESSION_PRO_BACKEND_PLAN_NIL: return "NIL"; - case SESSION_PRO_BACKEND_PLAN_ONE_MONTH: return "ONE_MONTH"; - case SESSION_PRO_BACKEND_PLAN_THREE_MONTHS: return "THREE_MONTHS"; - case SESSION_PRO_BACKEND_PLAN_TWELVE_MONTHS: return "TWELVE_MONTHS"; - case SESSION_PRO_BACKEND_PLAN_COUNT: - throw std::invalid_argument("SESSION_PRO_BACKEND_PLAN_COUNT"); - } - UNREACHABLE(); -} - -std::string_view proBackendEnumToString(SESSION_PRO_BACKEND_USER_PRO_STATUS v) { - switch (v) { - case SESSION_PRO_BACKEND_USER_PRO_STATUS_NEVER_BEEN_PRO: return "NEVER_BEEN_PRO"; - case SESSION_PRO_BACKEND_USER_PRO_STATUS_ACTIVE: return "ACTIVE"; - case SESSION_PRO_BACKEND_USER_PRO_STATUS_EXPIRED: return "EXPIRED"; - case SESSION_PRO_BACKEND_USER_PRO_STATUS_COUNT: - throw std::invalid_argument("SESSION_PRO_BACKEND_USER_PRO_STATUS_COUNT"); - } - UNREACHABLE(); -} - -std::string_view ProBackendEnumToString(SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT v) { - switch (v) { - case SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_SUCCESS: return "SUCCESS"; - case SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR: return "GENERIC_ERROR"; - case SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_COUNT: - throw std::invalid_argument("SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT"); - } - UNREACHABLE(); -} - -std::string_view proBackendEnumPlanToString(SESSION_PRO_BACKEND_PLAN v) { - switch (v) { - case SESSION_PRO_BACKEND_PLAN_NIL: return "NIL"; - case SESSION_PRO_BACKEND_PLAN_ONE_MONTH: return "ONE_MONTH"; - case SESSION_PRO_BACKEND_PLAN_THREE_MONTHS: return "THREE_MONTHS"; - case SESSION_PRO_BACKEND_PLAN_TWELVE_MONTHS: return "TWELVE_MONTHS"; - case SESSION_PRO_BACKEND_PLAN_COUNT: - throw std::invalid_argument("SESSION_PRO_BACKEND_PLAN_COUNT"); - } - UNREACHABLE(); -} - -std::string_view proBackendEnumPaymentProviderToString(SESSION_PRO_BACKEND_PAYMENT_PROVIDER v) { - switch (v) { - // Note: we want those to map ProOriginatingPlatform keys - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL: return "Nil"; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE: return "GooglePlayStore"; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE: return "iOSAppStore"; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: return "Rangeproof"; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT: - throw std::invalid_argument("SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT"); - } - UNREACHABLE(); -} - -std::string_view proBackendEnumPaymentStatusToString(SESSION_PRO_BACKEND_PAYMENT_STATUS v) { - switch (v) { - case SESSION_PRO_BACKEND_PAYMENT_STATUS_NIL: return "NIL"; - case SESSION_PRO_BACKEND_PAYMENT_STATUS_UNREDEEMED: return "UNREDEEMED"; - case SESSION_PRO_BACKEND_PAYMENT_STATUS_REDEEMED: return "REDEEMED"; - case SESSION_PRO_BACKEND_PAYMENT_STATUS_EXPIRED: return "EXPIRED"; - case SESSION_PRO_BACKEND_PAYMENT_STATUS_REFUNDED: return "REFUNDED"; - case SESSION_PRO_BACKEND_PAYMENT_STATUS_COUNT: - throw std::invalid_argument("SESSION_PRO_BACKEND_PAYMENT_STATUS_COUNT"); - } - UNREACHABLE(); -} - +// Provider, plan and payment-status are no longer fixed enums on the wire: provider/plan are opaque +// strings (emitted verbatim) and the response structs expose payment/user status as small enums that +// we surface to JS as their raw integer values (they line up 1:1 with the desktop numeric enums), so +// the only enum->string mapping still needed is for the pro-features result status. std::string_view proBackendEnumToString(session::ProFeaturesForMsgStatus v) { switch (v) { case session::ProFeaturesForMsgStatus::Success: return "SUCCESS"; diff --git a/src/user_config.cpp b/src/user_config.cpp index d2bd0ba..02bda6f 100644 --- a/src/user_config.cpp +++ b/src/user_config.cpp @@ -44,7 +44,7 @@ session::config::ProConfig pro_config_from_object(Napi::Object input) { std::copy( gen_index_hash_cpp.begin(), gen_index_hash_cpp.end(), - pro_config.proof.gen_index_hash.begin()); + pro_config.proof.revocation_tag.begin()); // extract backend signature auto signature_hex_js = proof_js.Get("signatureHex"); @@ -55,10 +55,10 @@ session::config::ProConfig pro_config_from_object(Napi::Object input) { std::copy(signature_cpp.begin(), signature_cpp.end(), pro_config.proof.sig.begin()); assert_length(signature_cpp, 64, "pro_config_from_object.signature_cpp"); - // extract expiryMs + // extract expiryMs (JS domain is ms; the proof's expiry_unix_ts is now whole seconds) assertIsNumber(proof_js.Get("expiryMs"), "pro_config_from_object.expiryMs"); - pro_config.proof.expiry_unix_ts = - toCppSysMs(proof_js.Get("expiryMs"), "pro_config_from_object.expiryMs"); + pro_config.proof.expiry_unix_ts = std::chrono::floor( + toCppSysMs(proof_js.Get("expiryMs"), "pro_config_from_object.expiryMs")); return pro_config; }; @@ -291,7 +291,16 @@ Napi::Value UserConfigWrapper::getProProfileBitset(const Napi::CallbackInfo& inf } Napi::Value UserConfigWrapper::getProAccessExpiry(const Napi::CallbackInfo& info) { - return wrapResult(info, [&] { return toJs(info.Env(), config.get_pro_access_expiry()); }); + return wrapResult(info, [&] { + // libsession now stores this in whole seconds; the JS domain is milliseconds + auto expiry_s = config.get_pro_access_expiry(); + std::optional> expiry_ms; + if (expiry_s) + expiry_ms = std::chrono::sys_time( + std::chrono::duration_cast( + expiry_s->time_since_epoch())); + return toJs(info.Env(), expiry_ms); + }); } void UserConfigWrapper::setProBadge(const Napi::CallbackInfo& info) { @@ -324,7 +333,12 @@ void UserConfigWrapper::setProAccessExpiry(const Napi::CallbackInfo& info) { auto proAccessExpiryMs = maybeNonemptyTimeMs(info[0], "UserConfigWrapper::setProAccessExpiry"); - config.set_pro_access_expiry(proAccessExpiryMs); + // libsession now stores this in whole seconds; the JS domain is milliseconds + std::optional proAccessExpiry; + if (proAccessExpiryMs) + proAccessExpiry = std::chrono::floor(*proAccessExpiryMs); + + config.set_pro_access_expiry(proAccessExpiry); }); } diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts index 5ba49ee..6843af5 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -13,9 +13,11 @@ declare module 'libsession_util_nodejs' { type ProStatus = 'ValidOrExpired' | 'Invalid'; type WithProProfileBitset = { proProfileBitset: bigint }; type WithProMessageBitset = { proMessageBitset: bigint }; + /** + * base64 of the proof's revocation tag (historically the "gen index hash") + */ type WithGenIndexHash = { genIndexHashB64: string }; - type WithRequestVersion = { requestVersion: number }; type WithTicket = { ticket: number }; type WithUnixTsMs = { @@ -58,26 +60,10 @@ declare module 'libsession_util_nodejs' { proProof: Omit; }; - export type ProOriginatingPlatform = 'Nil' | 'Google' | 'iOS' | 'Rangeproof'; - - export type ProBackendProviderConstantType = { - device: string; - store: string; - store_other: string; - platform: string; - platform_account: string; - refund_support_url: string; - refund_status_url: string; - refund_platform_url: string; - update_subscription_url: string; - cancel_subscription_url: string; - }; - - export type ProBackendProviderConstantsType = Record< - ProOriginatingPlatform, - ProBackendProviderConstantType - >; - + /** + * General (non per-provider) Pro URLs. These are libsession-owned constants (not translation data) + * still surfaced via LIBSESSION_PRO_URLS. Per-provider URLs are fetched via ProWrapper.providerUrls(). + */ export type ProBackendUrlsType = { roadmap: string; privacy_policy: string; @@ -86,12 +72,117 @@ declare module 'libsession_util_nodejs' { support_url: string; }; - type ProRevocationItem = WithGenIndexHash & { - expiryUnixTsMs: number; + /** + * Per-provider support/management URLs, looked up by provider slug via ProWrapper.providerUrls(). + * `null` for a provider with no applicable URLs (unknown slug, or e.g. rangeproof). + */ + type ProviderUrls = { + refundPlatformUrl: string; + refundSupportUrl: string; + refundStatusUrl: string; + updateSubscriptionUrl: string; + cancelSubscriptionUrl: string; }; type WithMasterPrivKeyHex = { masterPrivKeyHex: string }; + /** + * A request to POST to the Session Pro backend. libsession owns the endpoint<->body pairing. + */ + type ProRequest = { + /** + * Endpoint path relative to the backend base URL, e.g. "generate_pro_proof". + */ + endpoint: string; + /** + * The JSON request body to POST. + */ + body: string; + }; + + /** + * A parsed backend response. Always check `errors`/`status` before using the typed fields. + */ + type WithProResponseHeader = { + /** + * 0 on success; for add-payment this maps to the add-payment status enum, otherwise a generic + * success/error code. + */ + status: number; + /** + * Parse/processing errors; empty on success (the parse may be partial if non-empty). + */ + errors: Array; + }; + + type GenerateProProofResponse = WithProResponseHeader & { + proof: ProProof; + }; + + type ProRevocationItem = WithGenIndexHash & { + /** + * A matching proof is revoked once the client clock reaches this unix instant (milliseconds). + */ + effectiveMs: number; + }; + + type GetProRevocationsResponse = WithProResponseHeader & { + ticket: number; + /** + * Recommended seconds to wait before polling the revocation list again. + */ + retryInS: number; + /** + * Seconds to retain each item after first seeing it (memory-only aging). + */ + retainForS: number; + items: Array; + }; + + /** + * A single Pro payment item. `status` is the numeric payment-status enum + * (0=Nil,1=Unredeemed,2=Redeemed,3=Expired,4=Revoked). provider/plan are opaque wire slugs. + */ + type ProPaymentItem = { + status: number; + /** + * Billing-period slug, e.g. "1m"/"3m"/"1y" (opaque). + */ + plan: string; + /** + * Provider slug, e.g. "google_play"/"app_store" (opaque). + */ + paymentProvider: string; + autoRenewing: boolean; + purchasedTsMs: number; + revokedTsMs: number; + redeemedTsMs: number; + expiryTsMs: number; + gracePeriodDurationMs: number; + platformRefundExpiryTsMs: number; + refundRequestedTsMs: number; + /** + * Opaque payment identifier (confidential). + */ + paymentId: string; + }; + + type GetProDetailsResponse = WithProResponseHeader & { + /** + * numeric user-status enum (0=NeverBeenPro,1=Active,2=Expired) + */ + userStatus: number; + /** + * numeric error-report enum (0=Success,1=GenericError) + */ + errorReport: number; + autoRenewing: boolean; + expiryMs: number; + gracePeriodDurationMs: number; + refundRequestedTsMs: number; + paymentsTotal: number; + items: Array; + }; type ProWrapper = { proFeaturesForMessage: (args: { utf16: string }) => WithProMessageBitset & { @@ -102,25 +193,32 @@ declare module 'libsession_util_nodejs' { truncateAt: number; }; - proProofRequestBody: ( - args: WithMasterPrivKeyHex & WithRequestVersion & WithUnixTsMs & WithRotatingPrivKeyHex - ) => string; + proProofRequest: ( + args: WithMasterPrivKeyHex & WithRotatingPrivKeyHex & WithUnixTsMs + ) => ProRequest; /** - * @param version: Request version. The latest accepted version is 0 - * @param ticket: 4-byte monotonic integer for the caller's revocation list iteration. Set to 0 if unknown; otherwise, use the latest known `ticket` from a prior `GetProRevocationsResponse` to allow - the Session Pro Backend to omit the revocation list if it has not changed. - * @returns the stringified body to include in the request + * @param ticket: 64-bit monotonic revocation-list iteration. Set to 0 if unknown; otherwise use + * the latest known `ticket` from a prior GetProRevocationsResponse so the backend may omit an + * unchanged list. */ - proRevocationsRequestBody: (args: WithRequestVersion & WithTicket) => string; + proRevocationsRequest: (args: WithTicket) => ProRequest; + + proStatusRequest: ( + args: WithMasterPrivKeyHex & WithUnixTsMs & { count: number } + ) => ProRequest; - proStatusRequestBody: ( - args: WithMasterPrivKeyHex & - WithRequestVersion & - WithUnixTsMs & { - count: number; - } - ) => string; + /** + * Parse an add-payment / generate-proof reply (both carry a freshly-issued proof). + */ + parseProProofResponse: (args: { json: string }) => GenerateProProofResponse; + parseRevocationsResponse: (args: { json: string }) => GetProRevocationsResponse; + parsePaymentDetailsResponse: (args: { json: string }) => GetProDetailsResponse; + + /** + * Support/management URLs for a provider slug, or null if none apply. + */ + providerUrls: (args: { code: string }) => ProviderUrls | null; }; export type ProActionsCalls = MakeWrapperActionCalls; @@ -132,9 +230,13 @@ declare module 'libsession_util_nodejs' { public static proFeaturesForMessage: ProWrapper['proFeaturesForMessage']; public static utf16Count: ProWrapper['utf16Count']; public static utf16CountTruncatedToCodepoints: ProWrapper['utf16CountTruncatedToCodepoints']; - public static proProofRequestBody: ProWrapper['proProofRequestBody']; - public static proRevocationsRequestBody: ProWrapper['proRevocationsRequestBody']; - public static proStatusRequestBody: ProWrapper['proStatusRequestBody']; + public static proProofRequest: ProWrapper['proProofRequest']; + public static proRevocationsRequest: ProWrapper['proRevocationsRequest']; + public static proStatusRequest: ProWrapper['proStatusRequest']; + public static parseProProofResponse: ProWrapper['parseProProofResponse']; + public static parseRevocationsResponse: ProWrapper['parseRevocationsResponse']; + public static parsePaymentDetailsResponse: ProWrapper['parsePaymentDetailsResponse']; + public static providerUrls: ProWrapper['providerUrls']; } /** @@ -146,7 +248,11 @@ declare module 'libsession_util_nodejs' { | MakeActionCall | MakeActionCall | MakeActionCall - | MakeActionCall - | MakeActionCall - | MakeActionCall; + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall; } diff --git a/types/shared.d.ts b/types/shared.d.ts index 9578af3..7d2c8cf 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -178,8 +178,6 @@ declare module 'libsession_util_nodejs' { LIBSESSION_NODEJS_COMMIT: string; /** Object containing pro urls **/ LIBSESSION_PRO_URLS: ProBackendUrlsType; - /** Object containing mapped provider constants */ - LIBSESSION_PRO_PROVIDERS: ProBackendProviderConstantsType; }; export const CONSTANTS: ConstantsType; From ecdb80762d0077370ac3cb2396401252423ad486 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sun, 19 Jul 2026 00:00:42 -0300 Subject: [PATCH 04/19] =?UTF-8?q?WIP:=20nodejs=20glue=20=E2=80=94=20re-pin?= =?UTF-8?q?=20to=20FINAL=20PIN=20857783e6=20(status=20strings=20+=20URL/PU?= =?UTF-8?q?BKEY)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump the vendored libsession submodule 73e4d1e6 -> 857783e6 (the FINAL PIN) and adapt to the two changes since 73e4d1e6: - get-details `status` / `user_status` are now opaque string codes (the SESSION_PRO_BACKEND_PAYMENT_STATUS / _USER_PRO_STATUS enums were deleted): parsePaymentDetailsResponse now emits them as pass-through strings (item status: unredeemed/redeemed/expired/revoked; userStatus: never/active/expired). error_report stays a numeric enum. pro.d.ts retyped. - Expose the new libsession-owned backend identity so desktop stops hand-carrying its own: LIBSESSION_PRO_BACKEND_URL (session::pro_backend::URL, the overridable prod default) + LIBSESSION_PRO_BACKEND_PUBKEY_HEX (hex of session::pro_backend::PUBKEY) on the CONSTANTS wrapper; declared in shared.d.ts. Desktop will read these and derive X25519 on the fly. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/pro/pro.hpp | 6 ++++-- libsession-util | 2 +- src/constants.cpp | 13 +++++++++++++ types/pro/pro.d.ts | 9 ++++++--- types/shared.d.ts | 4 ++++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index 7065c87..529fa78 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -347,7 +347,8 @@ class ProWrapper : public Napi::ObjectWrap { auto obj = Napi::Object::New(env); obj["status"] = toJs(env, resp.status); obj["errors"] = errorsToJs(env, resp.errors); - obj["userStatus"] = toJs(env, static_cast(resp.user_status)); + // user_status is now an opaque string code (never/active/expired; unknowns pass through) + obj["userStatus"] = toJs(env, resp.user_status); obj["errorReport"] = toJs(env, static_cast(resp.error_report)); obj["autoRenewing"] = toJs(env, resp.auto_renewing); obj["expiryMs"] = toJs(env, resp.expiry_unix_ts.time_since_epoch().count() * 1000); @@ -361,7 +362,8 @@ class ProWrapper : public Napi::ObjectWrap { for (size_t i = 0; i < resp.items.size(); i++) { const auto& src = resp.items[i]; auto item = Napi::Object::New(env); - item["status"] = toJs(env, static_cast(src.status)); + // status is now an opaque string code (unredeemed/redeemed/expired/revoked; pass through) + item["status"] = toJs(env, src.status); item["plan"] = toJs(env, src.plan); item["paymentProvider"] = toJs(env, src.payment_provider); item["autoRenewing"] = toJs(env, src.auto_renewing); diff --git a/libsession-util b/libsession-util index 73e4d1e..857783e 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 73e4d1e65db1bfeb5949034bf3ca52724e002ab3 +Subproject commit 857783e6dbe4b17ba8d27e1c0396905467cb34fb diff --git a/src/constants.cpp b/src/constants.cpp index 96d0f85..0c23267 100644 --- a/src/constants.cpp +++ b/src/constants.cpp @@ -1,9 +1,12 @@ #include "constants.hpp" +#include + #include "js_native_api_types.h" #include "session/config/contacts.hpp" #include "session/config/groups/info.hpp" #include "session/config/user_groups.hpp" +#include "session/pro_backend.hpp" #include "session/session_protocol.h" #include "session/version.h" #include "utilities.hpp" @@ -53,6 +56,16 @@ Napi::Object ConstantsWrapper::Init(Napi::Env env, Napi::Object exports) { Napi::Number::New(env, session::config::community::FULL_URL_MAX_LENGTH), napi_enumerable), ObjectWrap::StaticValue("LIBSESSION_PRO_URLS", pro_urls, napi_enumerable), + // Session Pro backend identity — the single source of truth clients read instead of + // hand-carrying their own copies (URL is the overridable prod/default; pubkey is hex). + ObjectWrap::StaticValue( + "LIBSESSION_PRO_BACKEND_URL", + Napi::String::New(env, std::string(session::pro_backend::URL)), + napi_enumerable), + ObjectWrap::StaticValue( + "LIBSESSION_PRO_BACKEND_PUBKEY_HEX", + Napi::String::New(env, oxenc::to_hex(session::pro_backend::PUBKEY)), + napi_enumerable), ObjectWrap::StaticValue( "LIBSESSION_UTIL_VERSION", Napi::String::New(env, LIBSESSION_UTIL_VERSION_FULL), diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts index 6843af5..fe0ecec 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -144,7 +144,10 @@ declare module 'libsession_util_nodejs' { * (0=Nil,1=Unredeemed,2=Redeemed,3=Expired,4=Revoked). provider/plan are opaque wire slugs. */ type ProPaymentItem = { - status: number; + /** + * Opaque payment-status code slug: "unredeemed"/"redeemed"/"expired"/"revoked" (unknowns pass through). + */ + status: string; /** * Billing-period slug, e.g. "1m"/"3m"/"1y" (opaque). */ @@ -169,9 +172,9 @@ declare module 'libsession_util_nodejs' { type GetProDetailsResponse = WithProResponseHeader & { /** - * numeric user-status enum (0=NeverBeenPro,1=Active,2=Expired) + * Opaque account-status code slug: "never"/"active"/"expired" (unknowns pass through). */ - userStatus: number; + userStatus: string; /** * numeric error-report enum (0=Success,1=GenericError) */ diff --git a/types/shared.d.ts b/types/shared.d.ts index 7d2c8cf..da45973 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -178,6 +178,10 @@ declare module 'libsession_util_nodejs' { LIBSESSION_NODEJS_COMMIT: string; /** Object containing pro urls **/ LIBSESSION_PRO_URLS: ProBackendUrlsType; + /** Session Pro backend base URL (overridable prod/default) */ + LIBSESSION_PRO_BACKEND_URL: string; + /** Session Pro backend Ed25519 signing pubkey (hex, 64 chars) */ + LIBSESSION_PRO_BACKEND_PUBKEY_HEX: string; }; export const CONSTANTS: ConstantsType; From 43ebbede7e691a08266efe73eaebbcfdaca17bd9 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sun, 19 Jul 2026 15:59:43 -0300 Subject: [PATCH 05/19] =?UTF-8?q?WIP:=20nodejs=20glue=20=E2=80=94=20Pro=20?= =?UTF-8?q?response=20parsers=20take=20RAW=20bytes,=20not=20a=20JSON=20str?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The response wire format is a contract between libsession and the backend only; the client must not parse or even assume it's JSON. So parseProProofResponse / parseRevocationsResponse / parsePaymentDetailsResponse now take { body: Uint8Array } (the raw response bytes relayed from the network) instead of { json: string }, and hand the bytes straight to libsession's parser as a string_view over the buffer. Desktop can relay its onion bodyBinary through with zero decoding — a future wire-format change (e.g. bt-encoding) touches backend + libsession + this glue only. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/pro/pro.hpp | 26 +++++++++++++++++--------- types/pro/pro.d.ts | 10 ++++++---- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index 529fa78..fa686a3 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -284,22 +284,30 @@ class ProWrapper : public Napi::ObjectWrap { }); }; - static std::string requestJsonArg(const Napi::CallbackInfo& info, const std::string& id) { + // The response body is relayed RAW from the network (the client never parses it — the wire format + // is a contract between libsession and the backend only). Callers pass the raw bytes as a + // Uint8Array; we hand them straight to libsession's parser, no client-side decoding/assumption. + static std::vector requestBodyBytes( + const Napi::CallbackInfo& info, const std::string& id) { assertInfoLength(info, 1); assertIsObject(info[0]); auto first = info[0].As(); if (first.IsEmpty()) throw std::invalid_argument(id + " first received empty"); - assertIsString(first.Get("json"), id + ".json"); - return toCppString(first.Get("json"), id + ".json"); + assertIsUInt8Array(first.Get("body"), id + ".body"); + return toCppBuffer(first.Get("body"), id + ".body"); + } + + static std::string_view asJsonView(const std::vector& body) { + return std::string_view(reinterpret_cast(body.data()), body.size()); } static Napi::Value parseProProofResponse(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { auto env = info.Env(); - auto json = requestJsonArg(info, "parseProProofResponse"); + auto body = requestBodyBytes(info, "parseProProofResponse"); - auto resp = session::pro_backend::parse_pro_proof(json); + auto resp = session::pro_backend::parse_pro_proof(asJsonView(body)); auto obj = Napi::Object::New(env); obj["status"] = toJs(env, resp.status); @@ -312,9 +320,9 @@ class ProWrapper : public Napi::ObjectWrap { static Napi::Value parseRevocationsResponse(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { auto env = info.Env(); - auto json = requestJsonArg(info, "parseRevocationsResponse"); + auto body = requestBodyBytes(info, "parseRevocationsResponse"); - auto resp = session::pro_backend::parse_revocations(json); + auto resp = session::pro_backend::parse_revocations(asJsonView(body)); auto obj = Napi::Object::New(env); obj["status"] = toJs(env, resp.status); @@ -340,9 +348,9 @@ class ProWrapper : public Napi::ObjectWrap { static Napi::Value parsePaymentDetailsResponse(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { auto env = info.Env(); - auto json = requestJsonArg(info, "parsePaymentDetailsResponse"); + auto body = requestBodyBytes(info, "parsePaymentDetailsResponse"); - auto resp = session::pro_backend::parse_payment_details(json); + auto resp = session::pro_backend::parse_payment_details(asJsonView(body)); auto obj = Napi::Object::New(env); obj["status"] = toJs(env, resp.status); diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts index fe0ecec..396c8b9 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -212,11 +212,13 @@ declare module 'libsession_util_nodejs' { ) => ProRequest; /** - * Parse an add-payment / generate-proof reply (both carry a freshly-issued proof). + * Parse a backend reply. The `body` is the RAW response bytes relayed from the network — the wire + * format is a libsession<->backend contract, so the client never parses it; libsession does, and + * returns these typed structs. (parseProProofResponse covers add-payment + generate-proof.) */ - parseProProofResponse: (args: { json: string }) => GenerateProProofResponse; - parseRevocationsResponse: (args: { json: string }) => GetProRevocationsResponse; - parsePaymentDetailsResponse: (args: { json: string }) => GetProDetailsResponse; + parseProProofResponse: (args: { body: Uint8Array }) => GenerateProProofResponse; + parseRevocationsResponse: (args: { body: Uint8Array }) => GetProRevocationsResponse; + parsePaymentDetailsResponse: (args: { body: Uint8Array }) => GetProDetailsResponse; /** * Support/management URLs for a provider slug, or null if none apply. From b75c132775138a4a9fc6f3fbab0cc5df8d2d3efe Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 20 Jul 2026 09:38:59 -0300 Subject: [PATCH 06/19] =?UTF-8?q?nodejs=20glue:=20revocations=20=E2=80=94?= =?UTF-8?q?=20emit=20retryAtMs=20(absolute=20ms)=20+=20retainForMs,=20not?= =?UTF-8?q?=20seconds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit libsession-util-nodejs should hand nodejs values in nodejs's units (milliseconds), converting from C++'s natural seconds — not push C++ units onto the JS side. parseRevocationsResponse: - retryInS (seconds duration) -> retryAtMs: the absolute retry-at instant in ms (`system_clock::now() + max(retry_in, 0)`, same wall clock as JS Date.now() since same process). Subsumes desktop's `Math.max` + `Date.now() + retry_in*1000` — the caller now feeds retryAtMs straight to updateNextRunAtMs. - retainForS (seconds) -> retainForMs (ms). Stays a duration (applied per item as seenAt + retainForMs), just in ms. pro.d.ts GetProRevocationsResponse updated to retryAtMs/retainForMs. (All other pro glue timestamps/durations already emit ms; these two were the stragglers.) Co-Authored-By: Claude Opus 4.8 (1M context) --- include/pro/pro.hpp | 14 ++++++++++++-- types/pro/pro.d.ts | 10 ++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index fa686a3..c89806c 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -16,6 +16,8 @@ namespace session::nodeapi { +using namespace std::literals; + std::string_view proBackendEnumToString(session::ProFeaturesForMsgStatus v); class ProWrapper : public Napi::ObjectWrap { @@ -328,8 +330,16 @@ class ProWrapper : public Napi::ObjectWrap { obj["status"] = toJs(env, resp.status); obj["errors"] = errorsToJs(env, resp.errors); obj["ticket"] = toJs(env, resp.ticket); - obj["retryInS"] = toJs(env, static_cast(resp.retry_in.count())); - obj["retainForS"] = toJs(env, static_cast(resp.retain_for.count())); + // The backend returns a retry *delay*; resolve it to the absolute unix instant (ms) at which + // the revocation list may next be polled, clamped so it is never in the past. Handing back an + // absolute instant lets callers schedule the next poll without needing a clock of their own. + auto retryIn = std::max(resp.retry_in, 0s); + auto retryAt = std::chrono::system_clock::now() + retryIn; + obj["retryAtMs"] = toJs(env, static_cast(std::chrono::duration_cast< + std::chrono::milliseconds>(retryAt.time_since_epoch()).count())); + // retain_for stays a duration (applied per item as seen + retain_for); milliseconds for nodejs. + obj["retainForMs"] = toJs(env, static_cast( + std::chrono::duration_cast(resp.retain_for).count())); auto items = Napi::Array::New(env, resp.items.size()); for (size_t i = 0; i < resp.items.size(); i++) { diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts index 396c8b9..b248e30 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -129,13 +129,15 @@ declare module 'libsession_util_nodejs' { type GetProRevocationsResponse = WithProResponseHeader & { ticket: number; /** - * Recommended seconds to wait before polling the revocation list again. + * Absolute unix instant (ms) at which to next poll the revocation list — already `now + retry_in` + * (clamped ≥ now), so callers can feed it straight to a next-run scheduler without any arithmetic. */ - retryInS: number; + retryAtMs: number; /** - * Seconds to retain each item after first seeing it (memory-only aging). + * Duration (ms) to retain each item after first seeing it — applied per item as `seenAt + retainForMs` + * (memory-only aging); stays a duration since each item is seen at a different time. */ - retainForS: number; + retainForMs: number; items: Array; }; From 4584feebb309af6ced0db842dd4c277661798822 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 20 Jul 2026 09:50:53 -0300 Subject: [PATCH 07/19] nodejs glue: re-pin to 418cdaba (*_at renames, ProRequest content_type, PUBKEY_X25519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump vendored libsession 857783e6 -> 418cdaba and adapt to that pin: - C++ response/proof/config members renamed `*_unix_ts` -> `*_at` (no wire change): update every read — GetProDetailsResponse.expiry_at/refund_requested_at, ProPaymentItem.{purchased,redeemed,expiry,platform_refund_expiry,revoked, refund_requested}_at, ProRevocationItem.effective_at, ProProof.expiry_at, convo pro_expiry_at, ProConfig proof.expiry_at. JS field names/units unchanged (still ms). (get/set_pro_access_expiry were not renamed.) - ProRequest now carries content_type: expose it as `contentType` on the request builders' output so clients set Content-Type from libsession, not a hardcoded format. (C++ payload member is `data`; still surfaced under the JS key `body`.) - Add LIBSESSION_PRO_BACKEND_PUBKEY_X25519_HEX (to_hex(pro_backend::PUBKEY_X25519)) so desktop reads the X25519 pubkey from libsession instead of deriving it. - .d.ts updated: ProRequest.contentType, ConstantsType.LIBSESSION_PRO_BACKEND_PUBKEY_X25519_HEX. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/pro/pro.hpp | 22 ++++++++++++---------- include/pro/types.hpp | 4 ++-- libsession-util | 2 +- src/constants.cpp | 5 +++++ src/convo_info_volatile_config.cpp | 6 +++--- src/user_config.cpp | 2 +- types/pro/pro.d.ts | 6 +++++- types/shared.d.ts | 2 ++ 8 files changed, 31 insertions(+), 18 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index c89806c..f0cbdff 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -93,7 +93,9 @@ class ProWrapper : public Napi::ObjectWrap { static Napi::Object proRequestToJs(const Napi::Env& env, const pro_backend::ProRequest& req) { auto obj = Napi::Object::New(env); obj["endpoint"] = toJs(env, req.endpoint); - obj["body"] = toJs(env, req.body); + obj["contentType"] = toJs(env, req.content_type); + // C++ member is the opaque `data` payload; expose it under the JS key `body` (what desktop reads) + obj["body"] = toJs(env, req.data); return obj; } @@ -347,7 +349,7 @@ class ProWrapper : public Napi::ObjectWrap { item["genIndexHashB64"] = toJs(env, oxenc::to_base64(resp.items[i].revocation_tag)); // effective instant (whole seconds) -> ms JS domain item["effectiveMs"] = toJs( - env, resp.items[i].effective_unix_ts.time_since_epoch().count() * 1000); + env, resp.items[i].effective_at.time_since_epoch().count() * 1000); items.Set(i, item); } obj["items"] = items; @@ -369,11 +371,11 @@ class ProWrapper : public Napi::ObjectWrap { obj["userStatus"] = toJs(env, resp.user_status); obj["errorReport"] = toJs(env, static_cast(resp.error_report)); obj["autoRenewing"] = toJs(env, resp.auto_renewing); - obj["expiryMs"] = toJs(env, resp.expiry_unix_ts.time_since_epoch().count() * 1000); + obj["expiryMs"] = toJs(env, resp.expiry_at.time_since_epoch().count() * 1000); obj["gracePeriodDurationMs"] = toJs(env, static_cast(resp.grace_period_duration.count()) * 1000); obj["refundRequestedTsMs"] = - toJs(env, resp.refund_requested_unix_ts.time_since_epoch().count() * 1000); + toJs(env, resp.refund_requested_at.time_since_epoch().count() * 1000); obj["paymentsTotal"] = toJs(env, resp.payments_total); auto items = Napi::Array::New(env, resp.items.size()); @@ -386,19 +388,19 @@ class ProWrapper : public Napi::ObjectWrap { item["paymentProvider"] = toJs(env, src.payment_provider); item["autoRenewing"] = toJs(env, src.auto_renewing); // purchased/revoked are millisecond-resolution sys_ms (count() is already ms) - item["purchasedTsMs"] = toJs(env, src.purchased_unix_ts.time_since_epoch().count()); - item["revokedTsMs"] = toJs(env, src.revoked_unix_ts.time_since_epoch().count()); + item["purchasedTsMs"] = toJs(env, src.purchased_at.time_since_epoch().count()); + item["revokedTsMs"] = toJs(env, src.revoked_at.time_since_epoch().count()); // the rest are whole seconds -> ms item["redeemedTsMs"] = - toJs(env, src.redeemed_unix_ts.time_since_epoch().count() * 1000); + toJs(env, src.redeemed_at.time_since_epoch().count() * 1000); item["expiryTsMs"] = - toJs(env, src.expiry_unix_ts.time_since_epoch().count() * 1000); + toJs(env, src.expiry_at.time_since_epoch().count() * 1000); item["gracePeriodDurationMs"] = toJs(env, static_cast(src.grace_period_duration.count()) * 1000); item["platformRefundExpiryTsMs"] = toJs( - env, src.platform_refund_expiry_unix_ts.time_since_epoch().count() * 1000); + env, src.platform_refund_expiry_at.time_since_epoch().count() * 1000); item["refundRequestedTsMs"] = - toJs(env, src.refund_requested_unix_ts.time_since_epoch().count() * 1000); + toJs(env, src.refund_requested_at.time_since_epoch().count() * 1000); item["paymentId"] = toJs(env, src.payment_id); items.Set(i, item); } diff --git a/include/pro/types.hpp b/include/pro/types.hpp index 057ad2c..b12c160 100644 --- a/include/pro/types.hpp +++ b/include/pro/types.hpp @@ -17,10 +17,10 @@ struct toJs_impl { obj["version"] = toJs(env, pro_proof.version); // `revocation_tag` (renamed from `gen_index_hash`); kept the JS key `genIndexHashB64` to avoid - // churning every desktop consumer. `expiry_unix_ts` is now whole seconds -> emit ms (the JS domain). + // churning every desktop consumer. `expiry_at` is whole seconds on the C++ side -> emit ms (JS domain). obj["genIndexHashB64"] = toJs(env, oxenc::to_base64(pro_proof.revocation_tag)); obj["rotatingPubkeyHex"] = toJs(env, oxenc::to_hex(pro_proof.rotating_pubkey)); - obj["expiryMs"] = toJs(env, pro_proof.expiry_unix_ts.time_since_epoch().count() * 1000); + obj["expiryMs"] = toJs(env, pro_proof.expiry_at.time_since_epoch().count() * 1000); obj["signatureHex"] = toJs(env, oxenc::to_hex(pro_proof.sig)); return obj; diff --git a/libsession-util b/libsession-util index 857783e..418cdab 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 857783e6dbe4b17ba8d27e1c0396905467cb34fb +Subproject commit 418cdabad62c1a11ab22298f5682b7dd2956952e diff --git a/src/constants.cpp b/src/constants.cpp index 0c23267..2ed73a6 100644 --- a/src/constants.cpp +++ b/src/constants.cpp @@ -66,6 +66,11 @@ Napi::Object ConstantsWrapper::Init(Napi::Env env, Napi::Object exports) { "LIBSESSION_PRO_BACKEND_PUBKEY_HEX", Napi::String::New(env, oxenc::to_hex(session::pro_backend::PUBKEY)), napi_enumerable), + // X25519 form of the backend pubkey (for onion routing), so clients needn't derive it + ObjectWrap::StaticValue( + "LIBSESSION_PRO_BACKEND_PUBKEY_X25519_HEX", + Napi::String::New(env, oxenc::to_hex(session::pro_backend::PUBKEY_X25519)), + napi_enumerable), ObjectWrap::StaticValue( "LIBSESSION_UTIL_VERSION", Napi::String::New(env, LIBSESSION_UTIL_VERSION_FULL), diff --git a/src/convo_info_volatile_config.cpp b/src/convo_info_volatile_config.cpp index 95da70a..e8ef09b 100644 --- a/src/convo_info_volatile_config.cpp +++ b/src/convo_info_volatile_config.cpp @@ -59,14 +59,14 @@ struct toJs_impl { addBaseValues(env, obj, info_1o1); if (info_1o1.pro_revocation_tag->empty() || - !info_1o1.pro_expiry_unix_ts.time_since_epoch().count()) { + !info_1o1.pro_expiry_at.time_since_epoch().count()) { obj["proGenIndexHashB64"] = env.Null(); obj["proExpiryTsMs"] = env.Null(); } else { obj["proGenIndexHashB64"] = toJs(env, to_base64(*info_1o1.pro_revocation_tag)); // config field is now whole seconds; the JS `proExpiryTsMs` key stays milliseconds obj["proExpiryTsMs"] = - toJs(env, info_1o1.pro_expiry_unix_ts.time_since_epoch().count() * 1000); + toJs(env, info_1o1.pro_expiry_at.time_since_epoch().count() * 1000); } return obj; @@ -201,7 +201,7 @@ void ConvoInfoVolatileWrapper::set1o1(const Napi::CallbackInfo& info) { } if (proExpiryUnixTsMsCpp.has_value()) { // if the field is set (not null), we want to write the change as is (ms -> whole seconds) - convo.pro_expiry_unix_ts = std::chrono::floor( + convo.pro_expiry_at = std::chrono::floor( std::chrono::sys_time( std::chrono::milliseconds(*proExpiryUnixTsMsCpp))); } diff --git a/src/user_config.cpp b/src/user_config.cpp index 02bda6f..39d6c77 100644 --- a/src/user_config.cpp +++ b/src/user_config.cpp @@ -57,7 +57,7 @@ session::config::ProConfig pro_config_from_object(Napi::Object input) { // extract expiryMs (JS domain is ms; the proof's expiry_unix_ts is now whole seconds) assertIsNumber(proof_js.Get("expiryMs"), "pro_config_from_object.expiryMs"); - pro_config.proof.expiry_unix_ts = std::chrono::floor( + pro_config.proof.expiry_at = std::chrono::floor( toCppSysMs(proof_js.Get("expiryMs"), "pro_config_from_object.expiryMs")); return pro_config; diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts index b248e30..2b4388a 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -95,7 +95,11 @@ declare module 'libsession_util_nodejs' { */ endpoint: string; /** - * The JSON request body to POST. + * The value to send as the request's `Content-Type` header — relay verbatim; do not assume a format. + */ + contentType: string; + /** + * The opaque request payload to POST. Relay it untouched — do not parse, inspect, or modify it. */ body: string; }; diff --git a/types/shared.d.ts b/types/shared.d.ts index da45973..780a017 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -182,6 +182,8 @@ declare module 'libsession_util_nodejs' { LIBSESSION_PRO_BACKEND_URL: string; /** Session Pro backend Ed25519 signing pubkey (hex, 64 chars) */ LIBSESSION_PRO_BACKEND_PUBKEY_HEX: string; + /** Session Pro backend X25519 pubkey for onion routing (hex, 64 chars) */ + LIBSESSION_PRO_BACKEND_PUBKEY_X25519_HEX: string; }; export const CONSTANTS: ConstantsType; From 1a63d9b89166ec8a8b66fdfaca62064810c969f5 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 20 Jul 2026 16:21:12 -0300 Subject: [PATCH 08/19] pro glue: re-pin a9e790a8 + Delta #12 response envelope Re-pin libsession submodule 418cdaba -> a9e790a8 (Delta #12). Response envelope is now a closed status enum + optional error_code slug + optional single diagnostic error, replacing the numeric status + errors[]: - emitResponseHeader() renders the C++ ResponseBase: status as its wire string ("ok"/"fail"/"error"), error_code/error (std::optional -> string|null); drops errorsToJs. - pro.d.ts WithProResponseHeader: { status: 'ok'|'fail'|'error'; errorCode: string|null; error: string|null }. Desktop consumers re-key success to status === 'ok' (app repo). Co-Authored-By: Claude Opus 4.8 (1M context) --- include/pro/pro.hpp | 34 +++++++++++++++++++++++----------- libsession-util | 2 +- types/pro/pro.d.ts | 18 ++++++++++++------ 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index f0cbdff..4e40d87 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -99,11 +99,26 @@ class ProWrapper : public Napi::ObjectWrap { return obj; } - static Napi::Array errorsToJs(const Napi::Env& env, const std::vector& errors) { - auto arr = Napi::Array::New(env, errors.size()); - for (size_t i = 0; i < errors.size(); i++) - arr.Set(i, toJs(env, errors[i])); - return arr; + // Delta #12: the response envelope is a CLOSED status enum + an optional machine slug (error_code) + // + an optional English diagnostic (error) — no more errors[] array. Render status as its wire + // string ("ok"/"fail"/"error") for JS consumers; error_code/error are null on success. + static std::string_view responseStatusToJs(session::pro_backend::ResponseStatus s) { + using RS = session::pro_backend::ResponseStatus; + switch (s) { + case RS::Ok: return "ok"; + case RS::Fail: return "fail"; + case RS::Error: return "error"; + } + return "error"; // fail-closed on an unexpected value + } + + static void emitResponseHeader( + const Napi::Env& env, + Napi::Object& obj, + const session::pro_backend::ResponseBase& resp) { + obj["status"] = toJs(env, responseStatusToJs(resp.status)); + obj["errorCode"] = resp.error_code ? toJs(env, *resp.error_code) : env.Null(); + obj["error"] = resp.error ? toJs(env, *resp.error) : env.Null(); } static Napi::Value proFeaturesForMessage(const Napi::CallbackInfo& info) { @@ -314,8 +329,7 @@ class ProWrapper : public Napi::ObjectWrap { auto resp = session::pro_backend::parse_pro_proof(asJsonView(body)); auto obj = Napi::Object::New(env); - obj["status"] = toJs(env, resp.status); - obj["errors"] = errorsToJs(env, resp.errors); + emitResponseHeader(env, obj, resp); obj["proof"] = toJs(env, resp.proof); return obj; }); @@ -329,8 +343,7 @@ class ProWrapper : public Napi::ObjectWrap { auto resp = session::pro_backend::parse_revocations(asJsonView(body)); auto obj = Napi::Object::New(env); - obj["status"] = toJs(env, resp.status); - obj["errors"] = errorsToJs(env, resp.errors); + emitResponseHeader(env, obj, resp); obj["ticket"] = toJs(env, resp.ticket); // The backend returns a retry *delay*; resolve it to the absolute unix instant (ms) at which // the revocation list may next be polled, clamped so it is never in the past. Handing back an @@ -365,8 +378,7 @@ class ProWrapper : public Napi::ObjectWrap { auto resp = session::pro_backend::parse_payment_details(asJsonView(body)); auto obj = Napi::Object::New(env); - obj["status"] = toJs(env, resp.status); - obj["errors"] = errorsToJs(env, resp.errors); + emitResponseHeader(env, obj, resp); // user_status is now an opaque string code (never/active/expired; unknowns pass through) obj["userStatus"] = toJs(env, resp.user_status); obj["errorReport"] = toJs(env, static_cast(resp.error_report)); diff --git a/libsession-util b/libsession-util index 418cdab..a9e790a 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 418cdabad62c1a11ab22298f5682b7dd2956952e +Subproject commit a9e790a8049484b844e8bd3104e53980e9679120 diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts index 2b4388a..469b66d 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -105,18 +105,24 @@ declare module 'libsession_util_nodejs' { }; /** - * A parsed backend response. Always check `errors`/`status` before using the typed fields. + * A parsed backend response (Delta #12). Check `status === 'ok'` before using the typed payload. */ type WithProResponseHeader = { /** - * 0 on success; for add-payment this maps to the add-payment status enum, otherwise a generic - * success/error code. + * Outcome category (closed set): 'ok' = success; 'fail' = client input / precondition rejected; + * 'error' = backend fault (retryable). */ - status: number; + status: 'ok' | 'fail' | 'error'; /** - * Parse/processing errors; empty on success (the parse may be partial if non-empty). + * On non-'ok', a stable machine slug (spec §5.1) — map known ones to a localized string, fall back + * to `error` for an unrecognized slug. null on success. */ - errors: Array; + errorCode: string | null; + /** + * On non-'ok', an English diagnostic — NOT user-facing (show only when the slug has no i18n entry); + * always safe to log. null on success. + */ + error: string | null; }; type GenerateProProofResponse = WithProResponseHeader & { From 1543c315cb1e8be4098a7750712eca2f725a5cee Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 20 Jul 2026 23:47:26 -0300 Subject: [PATCH 09/19] Pro: expose visible_platforms() (N-API); re-pin libsession c6e5a210; fix decode_for_community ms->s Add ProWrapper.visiblePlatforms() returning the purchasable provider slugs. Bump the libsession-util submodule a9e790a8 -> c6e5a210. Delta #13 changed session::decode_for_community's timestamp param to sys_seconds, so convert nowMs accordingly in encrypt_decrypt. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/pro/pro.hpp | 20 ++++++++++++++++++++ libsession-util | 2 +- src/encrypt_decrypt/encrypt_decrypt.cpp | 4 +++- types/pro/pro.d.ts | 7 +++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index 4e40d87..6f3049d 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -80,6 +80,12 @@ class ProWrapper : public Napi::ObjectWrap { "providerUrls", static_cast( napi_writable | napi_configurable)), + + // Purchasable payment-provider slugs to surface to users + StaticMethod<&ProWrapper::visiblePlatforms>( + "visiblePlatforms", + static_cast( + napi_writable | napi_configurable)), }); } @@ -451,6 +457,20 @@ class ProWrapper : public Napi::ObjectWrap { return obj; }); }; + + // The purchasable payment-provider slugs to surface to users (single source of truth in + // libsession; excludes non-purchasable providers like rangeproof). Order is not significant. + static Napi::Value visiblePlatforms(const Napi::CallbackInfo& info) { + return wrapResult(info, [&]() -> Napi::Value { + auto env = info.Env(); + auto platforms = session::pro_backend::visible_platforms(); // span + auto arr = Napi::Array::New(env, platforms.size()); + uint32_t i = 0; + for (auto slug : platforms) + arr[i++] = Napi::String::New(env, std::string(slug)); + return arr; + }); + }; }; }; // namespace session::nodeapi diff --git a/libsession-util b/libsession-util index a9e790a..c6e5a21 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit a9e790a8049484b844e8bd3104e53980e9679120 +Subproject commit c6e5a2107b6c8adc6fba9afc6bb92f2e77101249 diff --git a/src/encrypt_decrypt/encrypt_decrypt.cpp b/src/encrypt_decrypt/encrypt_decrypt.cpp index 8c04352..ba158b1 100644 --- a/src/encrypt_decrypt/encrypt_decrypt.cpp +++ b/src/encrypt_decrypt/encrypt_decrypt.cpp @@ -637,7 +637,9 @@ Napi::Value MultiEncryptWrapper::decryptForCommunity(const Napi::CallbackInfo& i auto contentOrEnvelope = extractContentOrEnvelope(obj, "decryptForCommunity.obj.contentOrEnvelope"); decrypted.push_back(session::decode_for_community( - contentOrEnvelope, nowMs, proBackendPubkeyHex)); + contentOrEnvelope, + std::chrono::floor(nowMs), + proBackendPubkeyHex)); decryptedServerIds.push_back(serverId); } catch (const std::exception& e) { diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts index 469b66d..d40e700 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -236,6 +236,13 @@ declare module 'libsession_util_nodejs' { * Support/management URLs for a provider slug, or null if none apply. */ providerUrls: (args: { code: string }) => ProviderUrls | null; + + /** + * The purchasable payment-provider slugs to surface to users (single source of truth in + * libsession; excludes non-purchasable providers like rangeproof). Order is not significant — the + * caller applies its own ordering and skips slugs it has no display translation for. + */ + visiblePlatforms: () => Array; }; export type ProActionsCalls = MakeWrapperActionCalls; From f9cf828502095330269c5512eed2a9e6fa113ccd Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 21 Jul 2026 22:07:25 -0300 Subject: [PATCH 10/19] Pro glue: rename JS key genIndexHashB64 -> revocationTagB64 (match libsession revocation_tag) Co-Authored-By: Claude Opus 4.8 (1M context) --- include/pro/pro.hpp | 2 +- include/pro/types.hpp | 4 ++-- src/convo_info_volatile_config.cpp | 18 +++++++++--------- src/user_config.cpp | 16 ++++++++-------- types/pro/pro.d.ts | 6 +++--- types/user/convovolatile.d.ts | 6 +++--- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index 6f3049d..4695247 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -365,7 +365,7 @@ class ProWrapper : public Napi::ObjectWrap { auto items = Napi::Array::New(env, resp.items.size()); for (size_t i = 0; i < resp.items.size(); i++) { auto item = Napi::Object::New(env); - item["genIndexHashB64"] = toJs(env, oxenc::to_base64(resp.items[i].revocation_tag)); + item["revocationTagB64"] = toJs(env, oxenc::to_base64(resp.items[i].revocation_tag)); // effective instant (whole seconds) -> ms JS domain item["effectiveMs"] = toJs( env, resp.items[i].effective_at.time_since_epoch().count() * 1000); diff --git a/include/pro/types.hpp b/include/pro/types.hpp index b12c160..397eb0e 100644 --- a/include/pro/types.hpp +++ b/include/pro/types.hpp @@ -16,9 +16,9 @@ struct toJs_impl { auto obj = Napi::Object::New(env); obj["version"] = toJs(env, pro_proof.version); - // `revocation_tag` (renamed from `gen_index_hash`); kept the JS key `genIndexHashB64` to avoid + // `revocation_tag` (renamed from `revocation_tag`); kept the JS key `revocationTagB64` to avoid // churning every desktop consumer. `expiry_at` is whole seconds on the C++ side -> emit ms (JS domain). - obj["genIndexHashB64"] = toJs(env, oxenc::to_base64(pro_proof.revocation_tag)); + obj["revocationTagB64"] = toJs(env, oxenc::to_base64(pro_proof.revocation_tag)); obj["rotatingPubkeyHex"] = toJs(env, oxenc::to_hex(pro_proof.rotating_pubkey)); obj["expiryMs"] = toJs(env, pro_proof.expiry_at.time_since_epoch().count() * 1000); obj["signatureHex"] = toJs(env, oxenc::to_hex(pro_proof.sig)); diff --git a/src/convo_info_volatile_config.cpp b/src/convo_info_volatile_config.cpp index e8ef09b..fcfb0c3 100644 --- a/src/convo_info_volatile_config.cpp +++ b/src/convo_info_volatile_config.cpp @@ -60,10 +60,10 @@ struct toJs_impl { if (info_1o1.pro_revocation_tag->empty() || !info_1o1.pro_expiry_at.time_since_epoch().count()) { - obj["proGenIndexHashB64"] = env.Null(); + obj["proRevocationTagB64"] = env.Null(); obj["proExpiryTsMs"] = env.Null(); } else { - obj["proGenIndexHashB64"] = toJs(env, to_base64(*info_1o1.pro_revocation_tag)); + obj["proRevocationTagB64"] = toJs(env, to_base64(*info_1o1.pro_revocation_tag)); // config field is now whole seconds; the JS `proExpiryTsMs` key stays milliseconds obj["proExpiryTsMs"] = toJs(env, info_1o1.pro_expiry_at.time_since_epoch().count() * 1000); @@ -178,10 +178,10 @@ void ConvoInfoVolatileWrapper::set1o1(const Napi::CallbackInfo& info) { convo.unread = parsed.forcedUnread; // 1o1 also have a pro gen index hash & pro expiry - auto proGenIndexHashB64Js = parsed.obj.Get("proGenIndexHashB64"); - assertIsStringOrNull(proGenIndexHashB64Js, fnName + "proGenIndexHashB64Js"); - auto proGenIndexHashB64Cpp = - maybeNonemptyString(proGenIndexHashB64Js, fnName + "proGenIndexHashB64Cpp"); + auto proRevocationTagB64Js = parsed.obj.Get("proRevocationTagB64"); + assertIsStringOrNull(proRevocationTagB64Js, fnName + "proRevocationTagB64Js"); + auto proRevocationTagB64Cpp = + maybeNonemptyString(proRevocationTagB64Js, fnName + "proRevocationTagB64Cpp"); auto proExpiryUnixTsMsJs = parsed.obj.Get("proExpiryTsMs"); assertIsNumberOrNull(proExpiryUnixTsMsJs, fnName + "proExpiryUnixTsMsJs"); @@ -190,13 +190,13 @@ void ConvoInfoVolatileWrapper::set1o1(const Napi::CallbackInfo& info) { // Note: null is used to ignore an update. i.e. if the field is unset, we do not want to // overwrite the current value. // To reset it, set it to empty string - if (proGenIndexHashB64Cpp.has_value()) { - if (proGenIndexHashB64Cpp->empty()) { + if (proRevocationTagB64Cpp.has_value()) { + if (proRevocationTagB64Cpp->empty()) { // if the first is set, but empty, we want to reset the field convo.pro_revocation_tag = std::nullopt; } else { // this throws if the size is wrong - convo.pro_revocation_tag = from_base64_to_array<32>(*proGenIndexHashB64Cpp); + convo.pro_revocation_tag = from_base64_to_array<32>(*proRevocationTagB64Cpp); } } if (proExpiryUnixTsMsCpp.has_value()) { diff --git a/src/user_config.cpp b/src/user_config.cpp index 39d6c77..6d1b488 100644 --- a/src/user_config.cpp +++ b/src/user_config.cpp @@ -35,15 +35,15 @@ session::config::ProConfig pro_config_from_object(Napi::Object input) { assertIsNumber(proof_js.Get("version"), "pro_config_from_object.version"); pro_config.proof.version = toCppInteger(proof_js.Get("version"), "pro_config_from_object.version"); - // extract genIndexHashB64 - auto gen_index_hash_b64 = proof_js.Get("genIndexHashB64"); - assertIsString(gen_index_hash_b64, "pro_config_from_object.genIndexHashB64"); - auto gen_index_hash_b64_cpp = - toCppString(gen_index_hash_b64, "pro_config_from_object.genIndexHashB64"); - auto gen_index_hash_cpp = from_base64_to_vector(gen_index_hash_b64_cpp); + // extract revocationTagB64 + auto revocation_tag_b64 = proof_js.Get("revocationTagB64"); + assertIsString(revocation_tag_b64, "pro_config_from_object.revocationTagB64"); + auto revocation_tag_b64_cpp = + toCppString(revocation_tag_b64, "pro_config_from_object.revocationTagB64"); + auto revocation_tag_cpp = from_base64_to_vector(revocation_tag_b64_cpp); std::copy( - gen_index_hash_cpp.begin(), - gen_index_hash_cpp.end(), + revocation_tag_cpp.begin(), + revocation_tag_cpp.end(), pro_config.proof.revocation_tag.begin()); // extract backend signature diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts index d40e700..e3b0066 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -16,7 +16,7 @@ declare module 'libsession_util_nodejs' { /** * base64 of the proof's revocation tag (historically the "gen index hash") */ - type WithGenIndexHash = { genIndexHashB64: string }; + type WithRevocationTag = { revocationTagB64: string }; type WithTicket = { ticket: number }; @@ -24,7 +24,7 @@ declare module 'libsession_util_nodejs' { unixTsMs: number; }; - type ProProof = WithGenIndexHash & { + type ProProof = WithRevocationTag & { version: number; /** * HexString, 64 chars @@ -129,7 +129,7 @@ declare module 'libsession_util_nodejs' { proof: ProProof; }; - type ProRevocationItem = WithGenIndexHash & { + type ProRevocationItem = WithRevocationTag & { /** * A matching proof is revoked once the client clock reaches this unix instant (milliseconds). */ diff --git a/types/user/convovolatile.d.ts b/types/user/convovolatile.d.ts index 681de51..9d1b355 100644 --- a/types/user/convovolatile.d.ts +++ b/types/user/convovolatile.d.ts @@ -11,7 +11,7 @@ declare module 'libsession_util_nodejs' { type ConvoVolatile1o1GetExtra = { pubkeyHex: string; - } & { proExpiryTsMs: number | null; genIndexHashB64: string | null }; + } & { proExpiryTsMs: number | null; revocationTagB64: string | null }; type ConvoVolatile1o1SetExtra = { /** @@ -21,11 +21,11 @@ declare module 'libsession_util_nodejs' { */ proExpiryTsMs: number | null; /** - * The base64 encoded `genIndexHash` of the proof (32 bytes) + * The base64 encoded `revocationTag` of the proof (32 bytes) * If null, no changes will be made. * To force the field to be reset, you need to provide an empty string here */ - proGenIndexHashB64: string | null; + proRevocationTagB64: string | null; }; type ConvoInfoVolatileGet1o1 = BaseConvoInfoVolatile & ConvoVolatile1o1GetExtra; From 2adde993d51e03090393c108ae2ca279659b04e0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 21 Jul 2026 23:03:46 -0300 Subject: [PATCH 11/19] pro: normalise timestamp emission through a single toJsMs() helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The glue converted every libsession Pro timestamp to the JS millisecond domain by hand-writing `.time_since_epoch().count() * 1000` (for the sys_seconds fields) while deliberately omitting the *1000 for the two sys_ms fields (purchased_at/revoked_at). That left correctness resting on the author remembering each field's C++ resolution — a *1000 accidentally applied to a sys_ms field is an instant 1000x bug, and passing a sys_seconds straight to toJs() silently emits seconds into a `...Ms` key. Add toJsMs() overloads (a sys_ms time_point and a milliseconds duration) that rely on chrono's implicit, lossless widening: sys_seconds -> sys_ms and std::chrono::seconds -> milliseconds both convert at the call site, so every field routes through one helper with no hand-multiplication. purchased_at/ revoked_at stop being special cases. A finer-than-ms source (e.g. sys_time) fails to compile rather than silently truncating. No behavioural change: every emitted value is the same millisecond quantity as before. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/pro/pro.hpp | 41 +++++++++++++++-------------------------- include/pro/types.hpp | 4 ++-- include/utilities.hpp | 14 ++++++++++++++ 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index 4695247..fd38931 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -356,19 +356,15 @@ class ProWrapper : public Napi::ObjectWrap { // absolute instant lets callers schedule the next poll without needing a clock of their own. auto retryIn = std::max(resp.retry_in, 0s); auto retryAt = std::chrono::system_clock::now() + retryIn; - obj["retryAtMs"] = toJs(env, static_cast(std::chrono::duration_cast< - std::chrono::milliseconds>(retryAt.time_since_epoch()).count())); + obj["retryAtMs"] = toJsMs(env, std::chrono::floor(retryAt)); // retain_for stays a duration (applied per item as seen + retain_for); milliseconds for nodejs. - obj["retainForMs"] = toJs(env, static_cast( - std::chrono::duration_cast(resp.retain_for).count())); + obj["retainForMs"] = toJsMs(env, resp.retain_for); auto items = Napi::Array::New(env, resp.items.size()); for (size_t i = 0; i < resp.items.size(); i++) { auto item = Napi::Object::New(env); item["revocationTagB64"] = toJs(env, oxenc::to_base64(resp.items[i].revocation_tag)); - // effective instant (whole seconds) -> ms JS domain - item["effectiveMs"] = toJs( - env, resp.items[i].effective_at.time_since_epoch().count() * 1000); + item["effectiveMs"] = toJsMs(env, resp.items[i].effective_at); items.Set(i, item); } obj["items"] = items; @@ -389,11 +385,9 @@ class ProWrapper : public Napi::ObjectWrap { obj["userStatus"] = toJs(env, resp.user_status); obj["errorReport"] = toJs(env, static_cast(resp.error_report)); obj["autoRenewing"] = toJs(env, resp.auto_renewing); - obj["expiryMs"] = toJs(env, resp.expiry_at.time_since_epoch().count() * 1000); - obj["gracePeriodDurationMs"] = - toJs(env, static_cast(resp.grace_period_duration.count()) * 1000); - obj["refundRequestedTsMs"] = - toJs(env, resp.refund_requested_at.time_since_epoch().count() * 1000); + obj["expiryMs"] = toJsMs(env, resp.expiry_at); + obj["gracePeriodDurationMs"] = toJsMs(env, resp.grace_period_duration); + obj["refundRequestedTsMs"] = toJsMs(env, resp.refund_requested_at); obj["paymentsTotal"] = toJs(env, resp.payments_total); auto items = Napi::Array::New(env, resp.items.size()); @@ -405,20 +399,15 @@ class ProWrapper : public Napi::ObjectWrap { item["plan"] = toJs(env, src.plan); item["paymentProvider"] = toJs(env, src.payment_provider); item["autoRenewing"] = toJs(env, src.auto_renewing); - // purchased/revoked are millisecond-resolution sys_ms (count() is already ms) - item["purchasedTsMs"] = toJs(env, src.purchased_at.time_since_epoch().count()); - item["revokedTsMs"] = toJs(env, src.revoked_at.time_since_epoch().count()); - // the rest are whole seconds -> ms - item["redeemedTsMs"] = - toJs(env, src.redeemed_at.time_since_epoch().count() * 1000); - item["expiryTsMs"] = - toJs(env, src.expiry_at.time_since_epoch().count() * 1000); - item["gracePeriodDurationMs"] = - toJs(env, static_cast(src.grace_period_duration.count()) * 1000); - item["platformRefundExpiryTsMs"] = toJs( - env, src.platform_refund_expiry_at.time_since_epoch().count() * 1000); - item["refundRequestedTsMs"] = - toJs(env, src.refund_requested_at.time_since_epoch().count() * 1000); + // purchased/revoked carry sub-second (ms) precision; the rest are whole seconds. + // toJsMs normalises every one of them to the ms JS domain (see utilities.hpp). + item["purchasedTsMs"] = toJsMs(env, src.purchased_at); + item["revokedTsMs"] = toJsMs(env, src.revoked_at); + item["redeemedTsMs"] = toJsMs(env, src.redeemed_at); + item["expiryTsMs"] = toJsMs(env, src.expiry_at); + item["gracePeriodDurationMs"] = toJsMs(env, src.grace_period_duration); + item["platformRefundExpiryTsMs"] = toJsMs(env, src.platform_refund_expiry_at); + item["refundRequestedTsMs"] = toJsMs(env, src.refund_requested_at); item["paymentId"] = toJs(env, src.payment_id); items.Set(i, item); } diff --git a/include/pro/types.hpp b/include/pro/types.hpp index 397eb0e..912462a 100644 --- a/include/pro/types.hpp +++ b/include/pro/types.hpp @@ -17,10 +17,10 @@ struct toJs_impl { obj["version"] = toJs(env, pro_proof.version); // `revocation_tag` (renamed from `revocation_tag`); kept the JS key `revocationTagB64` to avoid - // churning every desktop consumer. `expiry_at` is whole seconds on the C++ side -> emit ms (JS domain). + // churning every desktop consumer. obj["revocationTagB64"] = toJs(env, oxenc::to_base64(pro_proof.revocation_tag)); obj["rotatingPubkeyHex"] = toJs(env, oxenc::to_hex(pro_proof.rotating_pubkey)); - obj["expiryMs"] = toJs(env, pro_proof.expiry_at.time_since_epoch().count() * 1000); + obj["expiryMs"] = toJsMs(env, pro_proof.expiry_at); obj["signatureHex"] = toJs(env, oxenc::to_hex(pro_proof.sig)); return obj; diff --git a/include/utilities.hpp b/include/utilities.hpp index 23ebb92..d340bab 100644 --- a/include/utilities.hpp +++ b/include/utilities.hpp @@ -20,6 +20,7 @@ #include "session/session_protocol.hpp" #include "session/types.h" #include "session/types.hpp" +#include "session/util.hpp" namespace session::nodeapi { @@ -275,6 +276,19 @@ struct toJs_impl> { } }; +// Normalise any system-clock time_point or duration to the JS domain as epoch-milliseconds. The +// wire mixes whole-second (`sys_seconds` / `std::chrono::seconds`) and millisecond-resolution +// (`sys_ms`) values; both widen implicitly and losslessly to the ms parameter types below, so +// callers never hand-multiply by 1000 (and a finer-than-ms source fails to compile rather than +// silently truncating). Prefer these over passing a `sys_seconds` straight to `toJs`, whose +// specialisation emits *seconds* and is an easy way to land the wrong unit in a `...Ms` field. +inline auto toJsMs(const Napi::Env& env, session::sys_ms tp) { + return toJs(env, tp); +} +inline auto toJsMs(const Napi::Env& env, std::chrono::milliseconds d) { + return toJs(env, d); +} + // Returns {"url": "...", "key": buffer} object; both values will be Null if the pic is not set. template <> From 78f8f4158c564f44e396be402293375f835c77af Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 22 Jul 2026 10:29:51 +0200 Subject: [PATCH 12/19] chore: lint --- include/pro/pro.hpp | 41 ++++++++++++++++++------------ include/pro/types.hpp | 4 +-- src/constants.cpp | 6 ++--- src/convo_info_volatile_config.cpp | 3 ++- src/pro/pro.cpp | 6 ++--- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index fd38931..ef6184b 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -90,8 +90,9 @@ class ProWrapper : public Napi::ObjectWrap { } private: - // The wire carries whole-second timestamps; the JS domain is milliseconds, so we convert at this - // boundary (the signed request timestamp round-trips losslessly as the value is always seconds). + // The wire carries whole-second timestamps; the JS domain is milliseconds, so we convert at + // this boundary (the signed request timestamp round-trips losslessly as the value is always + // seconds). static std::chrono::sys_seconds unixTsMsToSeconds(Napi::Value v, const std::string& id) { return std::chrono::floor(toCppSysMs(v, id)); } @@ -100,12 +101,14 @@ class ProWrapper : public Napi::ObjectWrap { auto obj = Napi::Object::New(env); obj["endpoint"] = toJs(env, req.endpoint); obj["contentType"] = toJs(env, req.content_type); - // C++ member is the opaque `data` payload; expose it under the JS key `body` (what desktop reads) + // C++ member is the opaque `data` payload; expose it under the JS key `body` (what desktop + // reads) obj["body"] = toJs(env, req.data); return obj; } - // Delta #12: the response envelope is a CLOSED status enum + an optional machine slug (error_code) + // Delta #12: the response envelope is a CLOSED status enum + an optional machine slug + // (error_code) // + an optional English diagnostic (error) — no more errors[] array. Render status as its wire // string ("ok"/"fail"/"error") for JS consumers; error_code/error are null on success. static std::string_view responseStatusToJs(session::pro_backend::ResponseStatus s) { @@ -299,8 +302,8 @@ class ProWrapper : public Napi::ObjectWrap { auto count = toCppInteger(first.Get("count"), "proStatusRequest.count"); assertIsString(first.Get("masterPrivKeyHex"), "proStatusRequest.masterPrivKeyHex"); - auto master_privkey = from_hex( - toCppString(first.Get("masterPrivKeyHex"), "proStatusRequest.masterPrivKeyHex")); + auto master_privkey = from_hex(toCppString( + first.Get("masterPrivKeyHex"), "proStatusRequest.masterPrivKeyHex")); auto req = session::pro_backend::payment_details_request( to_span(master_privkey), unix_ts, static_cast(count)); @@ -309,9 +312,10 @@ class ProWrapper : public Napi::ObjectWrap { }); }; - // The response body is relayed RAW from the network (the client never parses it — the wire format - // is a contract between libsession and the backend only). Callers pass the raw bytes as a - // Uint8Array; we hand them straight to libsession's parser, no client-side decoding/assumption. + // The response body is relayed RAW from the network (the client never parses it — the wire + // format is a contract between libsession and the backend only). Callers pass the raw bytes as + // a Uint8Array; we hand them straight to libsession's parser, no client-side + // decoding/assumption. static std::vector requestBodyBytes( const Napi::CallbackInfo& info, const std::string& id) { assertInfoLength(info, 1); @@ -351,19 +355,22 @@ class ProWrapper : public Napi::ObjectWrap { auto obj = Napi::Object::New(env); emitResponseHeader(env, obj, resp); obj["ticket"] = toJs(env, resp.ticket); - // The backend returns a retry *delay*; resolve it to the absolute unix instant (ms) at which - // the revocation list may next be polled, clamped so it is never in the past. Handing back an - // absolute instant lets callers schedule the next poll without needing a clock of their own. + // The backend returns a retry *delay*; resolve it to the absolute unix instant (ms) at + // which the revocation list may next be polled, clamped so it is never in the past. + // Handing back an absolute instant lets callers schedule the next poll without needing + // a clock of their own. auto retryIn = std::max(resp.retry_in, 0s); auto retryAt = std::chrono::system_clock::now() + retryIn; obj["retryAtMs"] = toJsMs(env, std::chrono::floor(retryAt)); - // retain_for stays a duration (applied per item as seen + retain_for); milliseconds for nodejs. + // retain_for stays a duration (applied per item as seen + retain_for); milliseconds for + // nodejs. obj["retainForMs"] = toJsMs(env, resp.retain_for); auto items = Napi::Array::New(env, resp.items.size()); for (size_t i = 0; i < resp.items.size(); i++) { auto item = Napi::Object::New(env); - item["revocationTagB64"] = toJs(env, oxenc::to_base64(resp.items[i].revocation_tag)); + item["revocationTagB64"] = + toJs(env, oxenc::to_base64(resp.items[i].revocation_tag)); item["effectiveMs"] = toJsMs(env, resp.items[i].effective_at); items.Set(i, item); } @@ -381,7 +388,8 @@ class ProWrapper : public Napi::ObjectWrap { auto obj = Napi::Object::New(env); emitResponseHeader(env, obj, resp); - // user_status is now an opaque string code (never/active/expired; unknowns pass through) + // user_status is now an opaque string code (never/active/expired; unknowns pass + // through) obj["userStatus"] = toJs(env, resp.user_status); obj["errorReport"] = toJs(env, static_cast(resp.error_report)); obj["autoRenewing"] = toJs(env, resp.auto_renewing); @@ -394,7 +402,8 @@ class ProWrapper : public Napi::ObjectWrap { for (size_t i = 0; i < resp.items.size(); i++) { const auto& src = resp.items[i]; auto item = Napi::Object::New(env); - // status is now an opaque string code (unredeemed/redeemed/expired/revoked; pass through) + // status is now an opaque string code (unredeemed/redeemed/expired/revoked; pass + // through) item["status"] = toJs(env, src.status); item["plan"] = toJs(env, src.plan); item["paymentProvider"] = toJs(env, src.payment_provider); diff --git a/include/pro/types.hpp b/include/pro/types.hpp index 912462a..954b9d7 100644 --- a/include/pro/types.hpp +++ b/include/pro/types.hpp @@ -16,8 +16,8 @@ struct toJs_impl { auto obj = Napi::Object::New(env); obj["version"] = toJs(env, pro_proof.version); - // `revocation_tag` (renamed from `revocation_tag`); kept the JS key `revocationTagB64` to avoid - // churning every desktop consumer. + // `revocation_tag` (renamed from `revocation_tag`); kept the JS key `revocationTagB64` to + // avoid churning every desktop consumer. obj["revocationTagB64"] = toJs(env, oxenc::to_base64(pro_proof.revocation_tag)); obj["rotatingPubkeyHex"] = toJs(env, oxenc::to_hex(pro_proof.rotating_pubkey)); obj["expiryMs"] = toJsMs(env, pro_proof.expiry_at); diff --git a/src/constants.cpp b/src/constants.cpp index 2ed73a6..fd48fad 100644 --- a/src/constants.cpp +++ b/src/constants.cpp @@ -27,9 +27,9 @@ Napi::Object ConstantsWrapper::Init(Napi::Env env, Napi::Object exports) { pro_urls["support_url"] = toJs(env, SESSION_PROTOCOL_STRINGS.url_pro_support); // Provider display metadata (store/platform/account NAMES) is no longer shipped by libsession — - // those are translation data owned by each client (keyed on the provider slug). The per-provider - // support/management URLs are still libsession-owned but are now fetched on demand via - // ProWrapper.providerUrls(code) rather than baked into a constants table here. + // those are translation data owned by each client (keyed on the provider slug). The + // per-provider support/management URLs are still libsession-owned but are now fetched on demand + // via ProWrapper.providerUrls(code) rather than baked into a constants table here. // construct javascript constants object Napi::Function cls = DefineClass( diff --git a/src/convo_info_volatile_config.cpp b/src/convo_info_volatile_config.cpp index fcfb0c3..570b315 100644 --- a/src/convo_info_volatile_config.cpp +++ b/src/convo_info_volatile_config.cpp @@ -200,7 +200,8 @@ void ConvoInfoVolatileWrapper::set1o1(const Napi::CallbackInfo& info) { } } if (proExpiryUnixTsMsCpp.has_value()) { - // if the field is set (not null), we want to write the change as is (ms -> whole seconds) + // if the field is set (not null), we want to write the change as is (ms -> whole + // seconds) convo.pro_expiry_at = std::chrono::floor( std::chrono::sys_time( std::chrono::milliseconds(*proExpiryUnixTsMsCpp))); diff --git a/src/pro/pro.cpp b/src/pro/pro.cpp index 8c8674d..d439fe8 100644 --- a/src/pro/pro.cpp +++ b/src/pro/pro.cpp @@ -3,9 +3,9 @@ namespace session::nodeapi { // Provider, plan and payment-status are no longer fixed enums on the wire: provider/plan are opaque -// strings (emitted verbatim) and the response structs expose payment/user status as small enums that -// we surface to JS as their raw integer values (they line up 1:1 with the desktop numeric enums), so -// the only enum->string mapping still needed is for the pro-features result status. +// strings (emitted verbatim) and the response structs expose payment/user status as small enums +// that we surface to JS as their raw integer values (they line up 1:1 with the desktop numeric +// enums), so the only enum->string mapping still needed is for the pro-features result status. std::string_view proBackendEnumToString(session::ProFeaturesForMsgStatus v) { switch (v) { case session::ProFeaturesForMsgStatus::Success: return "SUCCESS"; From 922a33de2e37190e9d08b4111c1f2bca12bcda94 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 22 Jul 2026 15:52:48 -0300 Subject: [PATCH 13/19] pro: note that desktop persists GetProDetailsResponse (required-field caveat) session-desktop persists this shape to local storage and reads it back cast to this type. Adding an OPTIONAL field is always safe; adding a REQUIRED field needs a transition on the desktop read side (stale caches from older builds). Comment only; points at the reminder in session-desktop's getProDetailsFromStorage. Co-Authored-By: Claude Opus 4.8 (1M context) --- types/pro/pro.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts index e3b0066..f289cc4 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -182,6 +182,11 @@ declare module 'libsession_util_nodejs' { paymentId: string; }; + // NOTE: session-desktop persists this shape verbatim to local storage and reads it back cast to this + // type (getProDetailsFromStorage). The glue and desktop ship in lockstep so a single build can't + // drift, but an older build's cache can. If you add a REQUIRED field here, the desktop read side needs + // a transition (drop the stale cache, or treat the field as optional there) — see the reminder at + // getProDetailsFromStorage in session-desktop. Adding OPTIONAL fields is always safe. type GetProDetailsResponse = WithProResponseHeader & { /** * Opaque account-status code slug: "never"/"active"/"expired" (unknowns pass through). From 08a1f1eb718609e39332a5cb52c24daa17cec083 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 22 Jul 2026 22:18:29 -0300 Subject: [PATCH 14/19] pro: re-pin to 3f8aace0, wire get_pro_status split, drop string8, plan {count,unit} Re-pin the libsession submodule to 3f8aace0 (stable) and absorb its changes: - get_pro_details split (Delta #15): proStatusRequest now builds pro_status_request (no count); parsePaymentDetailsResponse -> parseProStatusResponse (parse_pro_status), emitting userStatus/expiry/grace/refund + a single optional latestPayment (was an items[] array + paymentsTotal). get_payment_details (history) is not wired. - plan is now parsed: emit planCount + planUnit ("second".."lifetime") instead of the raw "1m"/"1y" slug. - string8 removed from libsession: drop the toJs_impl specialisation (the URL bundle fields are now const char*, handled by the string_view overload). - .d.ts: GetProDetailsResponse -> GetProStatusResponse (latestPayment, no items/total), ProPaymentItem plan -> planCount/planUnit, parse rename. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/pro/pro.hpp | 87 ++++++++++++++++++++++++++----------------- include/utilities.hpp | 6 --- libsession-util | 2 +- types/pro/pro.d.ts | 28 +++++++------- 4 files changed, 68 insertions(+), 55 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index ef6184b..ca8b63e 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -70,8 +70,8 @@ class ProWrapper : public Napi::ObjectWrap { "parseRevocationsResponse", static_cast( napi_writable | napi_configurable)), - StaticMethod<&ProWrapper::parsePaymentDetailsResponse>( - "parsePaymentDetailsResponse", + StaticMethod<&ProWrapper::parseProStatusResponse>( + "parseProStatusResponse", static_cast( napi_writable | napi_configurable)), @@ -297,16 +297,15 @@ class ProWrapper : public Napi::ObjectWrap { throw std::invalid_argument("proStatusRequest first received empty"); assertIsNumber(first.Get("unixTsMs"), "proStatusRequest.unixTsMs"); - assertIsNumber(first.Get("count"), "proStatusRequest.count"); auto unix_ts = unixTsMsToSeconds(first.Get("unixTsMs"), "proStatusRequest.unixTsMs"); - auto count = toCppInteger(first.Get("count"), "proStatusRequest.count"); assertIsString(first.Get("masterPrivKeyHex"), "proStatusRequest.masterPrivKeyHex"); auto master_privkey = from_hex(toCppString( first.Get("masterPrivKeyHex"), "proStatusRequest.masterPrivKeyHex")); - auto req = session::pro_backend::payment_details_request( - to_span(master_privkey), unix_ts, static_cast(count)); + // get_pro_status: the light "am I Pro?" query (no count/limit — a single latest payment + // comes back in the response). Payment history (get_payment_details) is not wired. + auto req = session::pro_backend::pro_status_request(to_span(master_privkey), unix_ts); return proRequestToJs(env, req); }); @@ -379,48 +378,66 @@ class ProWrapper : public Napi::ObjectWrap { }); }; - static Napi::Value parsePaymentDetailsResponse(const Napi::CallbackInfo& info) { + // Parsed plan unit -> lowercase slug for the JS domain to localize (Delta #14). + static std::string_view planUnitToString(session::pro_backend::ProPlanUnit u) { + using U = session::pro_backend::ProPlanUnit; + switch (u) { + case U::second: return "second"; + case U::day: return "day"; + case U::week: return "week"; + case U::month: return "month"; + case U::year: return "year"; + case U::lifetime: return "lifetime"; + } + return ""; + } + + // Emit a single ProPaymentItem: ms timestamps, plan as {planCount, planUnit}, opaque slugs. + static Napi::Object paymentItemToJs( + const Napi::Env& env, const session::pro_backend::ProPaymentItem& src) { + auto item = Napi::Object::New(env); + item["status"] = toJs(env, src.status); // opaque status slug; pass through + item["planCount"] = toJs(env, src.plan.count); + item["planUnit"] = toJs(env, planUnitToString(src.plan.unit)); + item["paymentProvider"] = toJs(env, src.payment_provider); + item["autoRenewing"] = toJs(env, src.auto_renewing); + // purchased/revoked carry sub-second (ms) precision; the rest are whole seconds. toJsMs + // normalises every one of them to the ms JS domain (see utilities.hpp). + item["purchasedTsMs"] = toJsMs(env, src.purchased_at); + item["revokedTsMs"] = toJsMs(env, src.revoked_at); + item["redeemedTsMs"] = toJsMs(env, src.redeemed_at); + item["expiryTsMs"] = toJsMs(env, src.expiry_at); + item["gracePeriodDurationMs"] = toJsMs(env, src.grace_period_duration); + item["platformRefundExpiryTsMs"] = toJsMs(env, src.platform_refund_expiry_at); + item["refundRequestedTsMs"] = toJsMs(env, src.refund_requested_at); + item["paymentId"] = toJs(env, src.payment_id); + return item; + } + + // get_pro_status: the light entitlement query. Carries user_status + expiry/grace/refund + a + // single most-recent payment (latestPayment, or null). Payment history (get_payment_details) is + // a separate library-only endpoint, not wired here. + static Napi::Value parseProStatusResponse(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { auto env = info.Env(); - auto body = requestBodyBytes(info, "parsePaymentDetailsResponse"); + auto body = requestBodyBytes(info, "parseProStatusResponse"); - auto resp = session::pro_backend::parse_payment_details(asJsonView(body)); + auto resp = session::pro_backend::parse_pro_status(asJsonView(body)); auto obj = Napi::Object::New(env); emitResponseHeader(env, obj, resp); - // user_status is now an opaque string code (never/active/expired; unknowns pass - // through) + // user_status: opaque string code (never/active/expired; unknowns pass through) obj["userStatus"] = toJs(env, resp.user_status); obj["errorReport"] = toJs(env, static_cast(resp.error_report)); obj["autoRenewing"] = toJs(env, resp.auto_renewing); obj["expiryMs"] = toJsMs(env, resp.expiry_at); obj["gracePeriodDurationMs"] = toJsMs(env, resp.grace_period_duration); obj["refundRequestedTsMs"] = toJsMs(env, resp.refund_requested_at); - obj["paymentsTotal"] = toJs(env, resp.payments_total); - - auto items = Napi::Array::New(env, resp.items.size()); - for (size_t i = 0; i < resp.items.size(); i++) { - const auto& src = resp.items[i]; - auto item = Napi::Object::New(env); - // status is now an opaque string code (unredeemed/redeemed/expired/revoked; pass - // through) - item["status"] = toJs(env, src.status); - item["plan"] = toJs(env, src.plan); - item["paymentProvider"] = toJs(env, src.payment_provider); - item["autoRenewing"] = toJs(env, src.auto_renewing); - // purchased/revoked carry sub-second (ms) precision; the rest are whole seconds. - // toJsMs normalises every one of them to the ms JS domain (see utilities.hpp). - item["purchasedTsMs"] = toJsMs(env, src.purchased_at); - item["revokedTsMs"] = toJsMs(env, src.revoked_at); - item["redeemedTsMs"] = toJsMs(env, src.redeemed_at); - item["expiryTsMs"] = toJsMs(env, src.expiry_at); - item["gracePeriodDurationMs"] = toJsMs(env, src.grace_period_duration); - item["platformRefundExpiryTsMs"] = toJsMs(env, src.platform_refund_expiry_at); - item["refundRequestedTsMs"] = toJsMs(env, src.refund_requested_at); - item["paymentId"] = toJs(env, src.payment_id); - items.Set(i, item); + if (resp.latest_payment) { + obj["latestPayment"] = paymentItemToJs(env, *resp.latest_payment); + } else { + obj["latestPayment"] = env.Null(); } - obj["items"] = items; return obj; }); }; diff --git a/include/utilities.hpp b/include/utilities.hpp index d340bab..d801310 100644 --- a/include/utilities.hpp +++ b/include/utilities.hpp @@ -163,12 +163,6 @@ struct toJs_impl> } }; -template <> -struct toJs_impl { - auto operator()(const Napi::Env& env, string8 s) const { - return Napi::String::New(env, s.data, s.size); - } -}; template struct toJs_impl< diff --git a/libsession-util b/libsession-util index c6e5a21..3f8aace 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit c6e5a2107b6c8adc6fba9afc6bb92f2e77101249 +Subproject commit 3f8aace0ea118da3e3102959e4b7c991f25f4b53 diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts index f289cc4..86d6b6b 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -161,9 +161,12 @@ declare module 'libsession_util_nodejs' { */ status: string; /** - * Billing-period slug, e.g. "1m"/"3m"/"1y" (opaque). + * Parsed billing period (libsession parses the closed `plan` grammar, Delta #14). `planUnit` is + * one of "second"/"day"/"week"/"month"/"year"/"lifetime" (preserved as transmitted, not + * canonicalized). `planCount` is >= 1 for periodic units and 0 for "lifetime". Client localizes. */ - plan: string; + planCount: number; + planUnit: string; /** * Provider slug, e.g. "google_play"/"app_store" (opaque). */ @@ -183,11 +186,11 @@ declare module 'libsession_util_nodejs' { }; // NOTE: session-desktop persists this shape verbatim to local storage and reads it back cast to this - // type (getProDetailsFromStorage). The glue and desktop ship in lockstep so a single build can't + // type (getProStatusFromStorage). The glue and desktop ship in lockstep so a single build can't // drift, but an older build's cache can. If you add a REQUIRED field here, the desktop read side needs // a transition (drop the stale cache, or treat the field as optional there) — see the reminder at - // getProDetailsFromStorage in session-desktop. Adding OPTIONAL fields is always safe. - type GetProDetailsResponse = WithProResponseHeader & { + // getProStatusFromStorage in session-desktop. Adding OPTIONAL fields is always safe. + type GetProStatusResponse = WithProResponseHeader & { /** * Opaque account-status code slug: "never"/"active"/"expired" (unknowns pass through). */ @@ -200,8 +203,9 @@ declare module 'libsession_util_nodejs' { expiryMs: number; gracePeriodDurationMs: number; refundRequestedTsMs: number; - paymentsTotal: number; - items: Array; + /** The single most-recent payment, or null when the account has no payments. (Full history is a + * separate library-only query — not wired.) */ + latestPayment: ProPaymentItem | null; }; type ProWrapper = { @@ -224,9 +228,7 @@ declare module 'libsession_util_nodejs' { */ proRevocationsRequest: (args: WithTicket) => ProRequest; - proStatusRequest: ( - args: WithMasterPrivKeyHex & WithUnixTsMs & { count: number } - ) => ProRequest; + proStatusRequest: (args: WithMasterPrivKeyHex & WithUnixTsMs) => ProRequest; /** * Parse a backend reply. The `body` is the RAW response bytes relayed from the network — the wire @@ -235,7 +237,7 @@ declare module 'libsession_util_nodejs' { */ parseProProofResponse: (args: { body: Uint8Array }) => GenerateProProofResponse; parseRevocationsResponse: (args: { body: Uint8Array }) => GetProRevocationsResponse; - parsePaymentDetailsResponse: (args: { body: Uint8Array }) => GetProDetailsResponse; + parseProStatusResponse: (args: { body: Uint8Array }) => GetProStatusResponse; /** * Support/management URLs for a provider slug, or null if none apply. @@ -264,7 +266,7 @@ declare module 'libsession_util_nodejs' { public static proStatusRequest: ProWrapper['proStatusRequest']; public static parseProProofResponse: ProWrapper['parseProProofResponse']; public static parseRevocationsResponse: ProWrapper['parseRevocationsResponse']; - public static parsePaymentDetailsResponse: ProWrapper['parsePaymentDetailsResponse']; + public static parseProStatusResponse: ProWrapper['parseProStatusResponse']; public static providerUrls: ProWrapper['providerUrls']; } @@ -282,6 +284,6 @@ declare module 'libsession_util_nodejs' { | MakeActionCall | MakeActionCall | MakeActionCall - | MakeActionCall + | MakeActionCall | MakeActionCall; } From b809e68fef59eba7f4778bd13e16995f36276ca1 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 23 Jul 2026 14:51:06 -0300 Subject: [PATCH 15/19] =?UTF-8?q?Drop=20stale=20Delta-N=20spec=20citations?= =?UTF-8?q?;=20reference=20current=20=C2=A7=20sections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wire spec (docs/pro-wire-protocol.md) dropped its Delta-N change-history annotations, so every 'Delta #N' citation was a dangling reference. Reworded each to the current section (envelope -> §5/§5.1/§5.2, plan grammar -> §1, get_pro_status -> §3.4) or dropped the parenthetical where there is no section. Verified all remaining pro-wire-protocol § references resolve. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/pro/pro.hpp | 5 ++--- types/pro/pro.d.ts | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index ca8b63e..ea8a7df 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -107,8 +107,7 @@ class ProWrapper : public Napi::ObjectWrap { return obj; } - // Delta #12: the response envelope is a CLOSED status enum + an optional machine slug - // (error_code) + // §5: the response envelope is a CLOSED status enum + an optional machine slug (error_code, §5.1) // + an optional English diagnostic (error) — no more errors[] array. Render status as its wire // string ("ok"/"fail"/"error") for JS consumers; error_code/error are null on success. static std::string_view responseStatusToJs(session::pro_backend::ResponseStatus s) { @@ -378,7 +377,7 @@ class ProWrapper : public Napi::ObjectWrap { }); }; - // Parsed plan unit -> lowercase slug for the JS domain to localize (Delta #14). + // Parsed plan unit -> lowercase slug for the JS domain to localize (plan grammar, §1). static std::string_view planUnitToString(session::pro_backend::ProPlanUnit u) { using U = session::pro_backend::ProPlanUnit; switch (u) { diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts index 86d6b6b..ba434be 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -105,7 +105,7 @@ declare module 'libsession_util_nodejs' { }; /** - * A parsed backend response (Delta #12). Check `status === 'ok'` before using the typed payload. + * A parsed backend response (§5). Check `status === 'ok'` before using the typed payload. */ type WithProResponseHeader = { /** @@ -161,7 +161,7 @@ declare module 'libsession_util_nodejs' { */ status: string; /** - * Parsed billing period (libsession parses the closed `plan` grammar, Delta #14). `planUnit` is + * Parsed billing period (libsession parses the closed `plan` grammar, §1). `planUnit` is * one of "second"/"day"/"week"/"month"/"year"/"lifetime" (preserved as transmitted, not * canonicalized). `planCount` is >= 1 for periodic units and 0 for "lifetime". Client localizes. */ From 242e6af3cfa17fddef2073c84317d97004f659f3 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 23 Jul 2026 23:05:10 -0300 Subject: [PATCH 16/19] Re-pin libsession-util to merged dev (d63deea5) The refactor merged upstream (session-foundation/libsession-util dev, to become v1.7.0). Move the submodule off the now-orphaned WIP commit 3f8aace0 (rebased away before merge) to the current dev tip d63deea5, which includes the refactor plus the Pro backend integration tests + CI. The pro API is identical to 3f8aace0 (only Delta-citation comments differ), so no glue code changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- libsession-util | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsession-util b/libsession-util index 3f8aace..d63deea 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 3f8aace0ea118da3e3102959e4b7c991f25f4b53 +Subproject commit d63deea51c7bf399f1764d88fe207871fd6c4d97 From 1ec3179089b289899122b69391ab822d8d861f90 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 27 Jul 2026 20:54:26 -0300 Subject: [PATCH 17/19] chore: lint --- include/pro/pro.hpp | 3 ++- include/utilities.hpp | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index ea8a7df..000b25f 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -107,7 +107,8 @@ class ProWrapper : public Napi::ObjectWrap { return obj; } - // §5: the response envelope is a CLOSED status enum + an optional machine slug (error_code, §5.1) + // §5: the response envelope is a CLOSED status enum + an optional machine slug (error_code, + // §5.1) // + an optional English diagnostic (error) — no more errors[] array. Render status as its wire // string ("ok"/"fail"/"error") for JS consumers; error_code/error are null on success. static std::string_view responseStatusToJs(session::pro_backend::ResponseStatus s) { diff --git a/include/utilities.hpp b/include/utilities.hpp index d801310..3432f4d 100644 --- a/include/utilities.hpp +++ b/include/utilities.hpp @@ -163,7 +163,6 @@ struct toJs_impl> } }; - template struct toJs_impl< T, From 6babc6dc0d2bb04c8439b624d2df7dfc8f74ec65 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 27 Jul 2026 21:29:25 -0300 Subject: [PATCH 18/19] Bump libsession-util pin d63deea5 -> 7e4d7641 (revocation retry/retain clamp) Pulls in the revocation-list retry_in/retain_for clamp (7d422cfd) now done in libsession's parse_revocations, plus its merge (#102) and the seed_payment obfuscated-account-id test fix. Lets clients drop their own client-side clamps. Co-Authored-By: Claude Opus 4.8 (1M context) --- libsession-util | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsession-util b/libsession-util index d63deea..7e4d764 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit d63deea51c7bf399f1764d88fe207871fd6c4d97 +Subproject commit 7e4d7641a8243baa9ca6ec3a749f251fdd18488b From ee2cf6fea92e65a79583e2e90bd222bd06c197c6 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 27 Jul 2026 11:32:59 +0200 Subject: [PATCH 19/19] fix: rely on revocation retry_in from libsession --- include/pro/pro.hpp | 11 +++++------ types/pro/pro.d.ts | 3 ++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index 000b25f..38bfd1a 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -354,12 +354,11 @@ class ProWrapper : public Napi::ObjectWrap { auto obj = Napi::Object::New(env); emitResponseHeader(env, obj, resp); obj["ticket"] = toJs(env, resp.ticket); - // The backend returns a retry *delay*; resolve it to the absolute unix instant (ms) at - // which the revocation list may next be polled, clamped so it is never in the past. - // Handing back an absolute instant lets callers schedule the next poll without needing - // a clock of their own. - auto retryIn = std::max(resp.retry_in, 0s); - auto retryAt = std::chrono::system_clock::now() + retryIn; + // The backend returns a retry *delay*, already sanity-clamped by libsession-util; we + // just resolve it to the absolute unix instant (ms) at which the revocation list may + // next be polled. Handing back an absolute instant lets callers schedule the next poll + // without needing a clock of their own. + auto retryAt = std::chrono::system_clock::now() + resp.retry_in; obj["retryAtMs"] = toJsMs(env, std::chrono::floor(retryAt)); // retain_for stays a duration (applied per item as seen + retain_for); milliseconds for // nodejs. diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts index ba434be..bfcd31c 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -140,7 +140,8 @@ declare module 'libsession_util_nodejs' { ticket: number; /** * Absolute unix instant (ms) at which to next poll the revocation list — already `now + retry_in` - * (clamped ≥ now), so callers can feed it straight to a next-run scheduler without any arithmetic. + * (libsession clamps the delay to a sane range), so callers can feed it straight to a next-run + * scheduler without any arithmetic. */ retryAtMs: number; /**