diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index 5ea7f40..38bfd1a 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -4,24 +4,20 @@ #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); +using namespace std::literals; + std::string_view proBackendEnumToString(session::ProFeaturesForMsgStatus v); class ProWrapper : public Napi::ObjectWrap { @@ -51,23 +47,89 @@ 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::proRevocationsRequest>( + "proRevocationsRequest", + static_cast( + napi_writable | napi_configurable)), + 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::parseProStatusResponse>( + "parseProStatusResponse", static_cast( napi_writable | napi_configurable)), - StaticMethod<&ProWrapper::proRevocationsRequestBody>( - "proRevocationsRequestBody", + + // Per-provider support/management URLs (or null) + StaticMethod<&ProWrapper::providerUrls>( + "providerUrls", static_cast( napi_writable | napi_configurable)), - StaticMethod<&ProWrapper::proStatusRequestBody>( - "proStatusRequestBody", + + // Purchasable payment-provider slugs to surface to users + StaticMethod<&ProWrapper::visiblePlatforms>( + "visiblePlatforms", 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["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; + } + + // §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) { + 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) { return wrapResult(info, [&] { // we expect one argument that matches: @@ -84,8 +146,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 +180,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 +224,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 +238,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 +268,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 +293,196 @@ 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"); + auto unix_ts = unixTsMsToSeconds(first.Get("unixTsMs"), "proStatusRequest.unixTsMs"); - 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); + // 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); - 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; + // 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"); + 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 body = requestBodyBytes(info, "parseProProofResponse"); + + auto resp = session::pro_backend::parse_pro_proof(asJsonView(body)); + + auto obj = Napi::Object::New(env); + emitResponseHeader(env, obj, resp); + obj["proof"] = toJs(env, resp.proof); + return obj; + }); + }; + + static Napi::Value parseRevocationsResponse(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + auto env = info.Env(); + auto body = requestBodyBytes(info, "parseRevocationsResponse"); + + auto resp = session::pro_backend::parse_revocations(asJsonView(body)); + + auto obj = Napi::Object::New(env); + emitResponseHeader(env, obj, resp); + obj["ticket"] = toJs(env, resp.ticket); + // 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. + 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["effectiveMs"] = toJsMs(env, resp.items[i].effective_at); + items.Set(i, item); + } + obj["items"] = items; + return obj; + }); + }; + + // 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) { + 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, "parseProStatusResponse"); + + auto resp = session::pro_backend::parse_pro_status(asJsonView(body)); + + auto obj = Napi::Object::New(env); + emitResponseHeader(env, obj, resp); + // 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); + if (resp.latest_payment) { + obj["latestPayment"] = paymentItemToJs(env, *resp.latest_payment); + } else { + obj["latestPayment"] = env.Null(); + } + 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; + }); + }; + + // 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; }); }; }; diff --git a/include/pro/types.hpp b/include/pro/types.hpp index ed717b8..954b9d7 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 `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"] = toJs(env, pro_proof.expiry_unix_ts.time_since_epoch().count()); + 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..3432f4d 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 { @@ -162,13 +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< T, @@ -275,6 +269,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 <> diff --git a/libsession-util b/libsession-util index 99f1b52..7e4d764 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 99f1b5201c68a48f700ecb58c284d1a584f0699a +Subproject commit 7e4d7641a8243baa9ca6ec3a749f251fdd18488b diff --git a/src/constants.cpp b/src/constants.cpp index 6e403ae..fd48fad 100644 --- a/src/constants.cpp +++ b/src/constants.cpp @@ -1,10 +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.h" +#include "session/pro_backend.hpp" #include "session/session_protocol.h" #include "session/version.h" #include "utilities.hpp" @@ -24,43 +26,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 +56,21 @@ 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), + // 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), + // 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 74dc6d1..570b315 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() || - !info_1o1.pro_expiry_unix_ts.time_since_epoch().count()) { - obj["proGenIndexHashB64"] = env.Null(); + if (info_1o1.pro_revocation_tag->empty() || + !info_1o1.pro_expiry_at.time_since_epoch().count()) { + obj["proRevocationTagB64"] = 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["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); } return obj; @@ -176,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"); @@ -188,19 +190,21 @@ 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_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>(*proRevocationTagB64Cpp); } } 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_at = std::chrono::floor( + std::chrono::sys_time( + std::chrono::milliseconds(*proExpiryUnixTsMsCpp))); } config.set(convo); 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/src/pro/pro.cpp b/src/pro/pro.cpp index 01e1e0b..d439fe8 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..6d1b488 100644 --- a/src/user_config.cpp +++ b/src/user_config.cpp @@ -35,16 +35,16 @@ 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(), - pro_config.proof.gen_index_hash.begin()); + revocation_tag_cpp.begin(), + revocation_tag_cpp.end(), + 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_at = 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..bfcd31c 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -13,16 +13,18 @@ declare module 'libsession_util_nodejs' { type ProStatus = 'ValidOrExpired' | 'Invalid'; type WithProProfileBitset = { proProfileBitset: bigint }; type WithProMessageBitset = { proMessageBitset: bigint }; - type WithGenIndexHash = { genIndexHashB64: string }; + /** + * base64 of the proof's revocation tag (historically the "gen index hash") + */ + type WithRevocationTag = { revocationTagB64: string }; - type WithRequestVersion = { requestVersion: number }; type WithTicket = { ticket: number }; type WithUnixTsMs = { unixTsMs: number; }; - type ProProof = WithGenIndexHash & { + type ProProof = WithRevocationTag & { version: number; /** * HexString, 64 chars @@ -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,142 @@ 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 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; + }; + + /** + * A parsed backend response (§5). Check `status === 'ok'` before using the typed payload. + */ + type WithProResponseHeader = { + /** + * Outcome category (closed set): 'ok' = success; 'fail' = client input / precondition rejected; + * 'error' = backend fault (retryable). + */ + status: 'ok' | 'fail' | 'error'; + /** + * 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. + */ + 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 & { + proof: ProProof; + }; + + type ProRevocationItem = WithRevocationTag & { + /** + * A matching proof is revoked once the client clock reaches this unix instant (milliseconds). + */ + effectiveMs: number; + }; + + type GetProRevocationsResponse = WithProResponseHeader & { + ticket: number; + /** + * Absolute unix instant (ms) at which to next poll the revocation list — already `now + retry_in` + * (libsession clamps the delay to a sane range), so callers can feed it straight to a next-run + * scheduler without any arithmetic. + */ + retryAtMs: number; + /** + * 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. + */ + retainForMs: 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 = { + /** + * Opaque payment-status code slug: "unredeemed"/"redeemed"/"expired"/"revoked" (unknowns pass through). + */ + status: string; + /** + * 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. + */ + planCount: number; + planUnit: 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; + }; + + // NOTE: session-desktop persists this shape verbatim to local storage and reads it back cast to this + // 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 + // getProStatusFromStorage in session-desktop. Adding OPTIONAL fields is always safe. + type GetProStatusResponse = WithProResponseHeader & { + /** + * Opaque account-status code slug: "never"/"active"/"expired" (unknowns pass through). + */ + userStatus: string; + /** + * numeric error-report enum (0=Success,1=GenericError) + */ + errorReport: number; + autoRenewing: boolean; + expiryMs: number; + gracePeriodDurationMs: number; + refundRequestedTsMs: number; + /** 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 = { proFeaturesForMessage: (args: { utf16: string }) => WithProMessageBitset & { @@ -102,25 +218,39 @@ 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) => ProRequest; - proStatusRequestBody: ( - args: WithMasterPrivKeyHex & - WithRequestVersion & - WithUnixTsMs & { - count: number; - } - ) => string; + /** + * 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: { body: Uint8Array }) => GenerateProProofResponse; + parseRevocationsResponse: (args: { body: Uint8Array }) => GetProRevocationsResponse; + parseProStatusResponse: (args: { body: Uint8Array }) => GetProStatusResponse; + + /** + * 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; @@ -132,9 +262,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 parseProStatusResponse: ProWrapper['parseProStatusResponse']; + public static providerUrls: ProWrapper['providerUrls']; } /** @@ -146,7 +280,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..780a017 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -178,8 +178,12 @@ 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; + /** 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; + /** Session Pro backend X25519 pubkey for onion routing (hex, 64 chars) */ + LIBSESSION_PRO_BACKEND_PUBKEY_X25519_HEX: string; }; export const CONSTANTS: ConstantsType; 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;