Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 16 additions & 77 deletions include/pro/pro.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,6 @@ class ProWrapper : public Napi::ObjectWrap<ProWrapper> {
"ProWrapperNode",
{
// Pro features
StaticMethod<&ProWrapper::utf16CountTruncatedToCodepoints>(
"utf16CountTruncatedToCodepoints",
static_cast<napi_property_attributes>(
napi_writable | napi_configurable)),
StaticMethod<&ProWrapper::utf16Count>(
"utf16Count",
static_cast<napi_property_attributes>(
napi_writable | napi_configurable)),
StaticMethod<&ProWrapper::proFeaturesForMessage>(
"proFeaturesForMessage",
static_cast<napi_property_attributes>(
Expand Down Expand Up @@ -134,8 +126,10 @@ class ProWrapper : public Napi::ObjectWrap<ProWrapper> {
return wrapResult(info, [&] {
// we expect one argument that matches:
// first: {
// "utf16": string,
// "codepointCount": number,
// }
// The caller counts codepoints natively (JS: [...text].length) and passes the count;
// libsession owns the count -> feature policy.

assertInfoLength(info, 1);
assertIsObject(info[0]);
Expand All @@ -146,84 +140,21 @@ class ProWrapper : public Napi::ObjectWrap<ProWrapper> {
if (first.IsEmpty())
throw std::invalid_argument("proFeaturesForMessage first received empty");

assertIsString(first.Get("utf16"), "proFeaturesForMessage.utf16");
std::u16string utf16 = first.Get("utf16").As<Napi::String>().Utf16Value();
ProFeaturesForMsg pro_features_msg =
session::pro_features_for_utf16((utf16.data()), utf16.length());
assertIsNumber(first.Get("codepointCount"), "proFeaturesForMessage.codepointCount");
size_t codepoint_count = first.Get("codepointCount").As<Napi::Number>().Uint32Value();
ProFeaturesForMsg pro_features_msg = session::pro_features_for_message(codepoint_count);

auto obj = Napi::Object::New(env);

obj["status"] = toJs(env, proBackendEnumToString(pro_features_msg.status));
obj["error"] =
pro_features_msg.error.size() ? toJs(env, pro_features_msg.error) : env.Null();
obj["codepointCount"] = toJs(env, pro_features_msg.codepoint_count);
obj["proMessageBitset"] = proMessageBitsetToJS(env, pro_features_msg.bitset);

return obj;
});
};

static Napi::Value utf16Count(const Napi::CallbackInfo& info) {
return wrapResult(info, [&] {
// we expect one argument that matches:
// first: {
// "utf16": string,
// }
// we return an object with a single property {`codepointCount: number`}

assertInfoLength(info, 1);
assertIsObject(info[0]);
auto env = info.Env();

auto first = info[0].As<Napi::Object>();

if (first.IsEmpty())
throw std::invalid_argument("utf16Count first received empty");

assertIsString(first.Get("utf16"), "utf16Count.utf16");
std::u16string utf16 = first.Get("utf16").As<Napi::String>().Utf16Value();
size_t codepoint_count = session::utf16_count(utf16);

auto obj = Napi::Object::New(env);
obj["codepointCount"] = toJs(env, codepoint_count);

return obj;
});
};

static Napi::Value utf16CountTruncatedToCodepoints(const Napi::CallbackInfo& info) {
return wrapResult(info, [&] {
// we expect one argument that matches:
// first: {
// "utf16": string,
// "codepointLen": number,
// }
// we return an object with a single property {`truncateAt: number`}

assertInfoLength(info, 1);
assertIsObject(info[0]);
auto env = info.Env();

auto first = info[0].As<Napi::Object>();

if (first.IsEmpty())
throw std::invalid_argument("utf16CountTruncatedToCodepoints first received empty");

assertIsString(first.Get("utf16"), "utf16CountTruncatedToCodepoints.utf16");
std::u16string utf16 = first.Get("utf16").As<Napi::String>().Utf16Value();
assertIsNumber(
first.Get("codepointLen"), "utf16CountTruncatedToCodepoints.codepointLen");
size_t codepointLen = first.Get("codepointLen").As<Napi::Number>().Uint32Value();

size_t truncate_at = session::utf16_count_truncated_to_codepoints(utf16, codepointLen);

auto obj = Napi::Object::New(env);
obj["truncateAt"] = toJs(env, truncate_at);

return obj;
});
};

static Napi::Value proProofRequest(const Napi::CallbackInfo& info) {
return wrapResult(info, [&] {
// first: {
Expand Down Expand Up @@ -340,6 +271,16 @@ class ProWrapper : public Napi::ObjectWrap<ProWrapper> {
auto obj = Napi::Object::New(env);
emitResponseHeader(env, obj, resp);
obj["proof"] = toJs(env, resp.proof);
// Advisory account (subscription) expiry — grace-inclusive true entitlement end.
// Present on success + subscription_expired (a past value there), null otherwise.
// Distinct from the proof's own clamped expiry; unsigned/not-in-M, for display + `E`
// refresh only.
std::optional<std::chrono::sys_time<std::chrono::milliseconds>> account_expiry_ms;
if (resp.account_expiry)
account_expiry_ms = std::chrono::sys_time<std::chrono::milliseconds>(
std::chrono::duration_cast<std::chrono::milliseconds>(
resp.account_expiry->time_since_epoch()));
obj["accountExpiryMs"] = toJs(env, account_expiry_ms);
return obj;
});
};
Expand Down Expand Up @@ -408,7 +349,6 @@ class ProWrapper : public Napi::ObjectWrap<ProWrapper> {
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;
}
Expand All @@ -431,7 +371,6 @@ class ProWrapper : public Napi::ObjectWrap<ProWrapper> {
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 {
Expand Down
13 changes: 13 additions & 0 deletions include/user_config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,18 @@ class UserConfigWrapper : public ConfigBaseImpl, public Napi::ObjectWrap<UserCon

Napi::Value generateProMasterKey(const Napi::CallbackInfo& info);
Napi::Value generateRotatingPrivKeyHex(const Napi::CallbackInfo& info);

// Derive the rotating seed (and its ed25519 keypair) for `now` from the Pro master key
// (libsession owns the rotation schedule), so every device converges on the same key. Replaces
// ad-hoc/random rotating-key generation.
Napi::Value deriveProRotatingKey(const Napi::CallbackInfo& info);

// Refund-requested (config key R) and pro-prepaid / purchase-in-flight (config key I) markers,
// and the renewal-target poll that decides when to (re)request a proof.
Napi::Value getRefundRequested(const Napi::CallbackInfo& info);
void setRefundRequested(const Napi::CallbackInfo& info);
Napi::Value getProPrepaid(const Napi::CallbackInfo& info);
void setProPrepaid(const Napi::CallbackInfo& info);
Napi::Value getProRenewalTarget(const Napi::CallbackInfo& info);
};
}; // namespace session::nodeapi
2 changes: 1 addition & 1 deletion libsession-util
Submodule libsession-util updated 42 files
+1 −0 CMakeLists.txt
+17 −11 include/session/config/pro.hpp
+66 −0 include/session/config/user_profile.h
+90 −2 include/session/config/user_profile.hpp
+1 −1 include/session/network/transport/network_transport.hpp
+0 −1 include/session/network/transport/quic_transport.hpp
+14 −84 include/session/pro_backend.h
+41 −130 include/session/pro_backend.hpp
+0 −2 include/session/session_encrypt.h
+29 −39 include/session/session_protocol.h
+62 −54 include/session/session_protocol.hpp
+10 −11 include/session/util.hpp
+6 −0 src/CMakeLists.txt
+2 −2 src/attachments.cpp
+3 −7 src/config/convo_info_volatile.cpp
+1 −1 src/config/groups/info.cpp
+1 −1 src/config/groups/keys.cpp
+8 −0 src/config/internal.cpp
+5 −0 src/config/internal.hpp
+37 −43 src/config/pro.cpp
+1 −1 src/config/user_groups.cpp
+176 −15 src/config/user_profile.cpp
+5 −5 src/network/network_config.cpp
+1 −1 src/network/routing/direct_router.cpp
+1 −1 src/network/routing/onion_request_router.cpp
+1 −1 src/network/routing/session_router_router.cpp
+6 −6 src/network/session_network.cpp
+1 −1 src/network/snode_pool.cpp
+6 −12 src/network/transport/quic_transport.cpp
+26 −309 src/pro_backend.cpp
+4 −2 src/pro_message.hpp
+2 −4 src/session_encrypt.cpp
+93 −85 src/session_protocol.cpp
+26 −45 src/util.cpp
+1 −4 tests/pro_backend/gen_kat.py
+11 −36 tests/test_config_pro.cpp
+126 −1 tests/test_config_userprofile.cpp
+14 −14 tests/test_onion_request_router.cpp
+74 −256 tests/test_pro_backend.cpp
+36 −30 tests/test_session_protocol.cpp
+2 −2 tests/test_snode_pool.cpp
+19 −26 tests/test_unicode_operations.cpp
10 changes: 10 additions & 0 deletions src/constants.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ Napi::Object ConstantsWrapper::Init(Napi::Env env, Napi::Object exports) {
"COMMUNITY_FULL_URL_MAX_LENGTH",
Napi::Number::New(env, session::config::community::FULL_URL_MAX_LENGTH),
napi_enumerable),
// Pro message character (codepoint) limits — single-sourced from libsession so clients
// don't hard-code them (the count -> feature decision goes via proFeaturesForMessage).
ObjectWrap::StaticValue(
"MESSAGE_CHARACTER_LIMIT_STANDARD",
Napi::Number::New(env, SESSION_PROTOCOL_STANDARD_CHARACTER_LIMIT),
napi_enumerable),
ObjectWrap::StaticValue(
"MESSAGE_CHARACTER_LIMIT_PRO",
Napi::Number::New(env, SESSION_PROTOCOL_PRO_HIGHER_CHARACTER_LIMIT),
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).
Expand Down
3 changes: 2 additions & 1 deletion src/encrypt_decrypt/encrypt_decrypt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,8 @@ Napi::Value MultiEncryptWrapper::encryptForCommunityInbox(const Napi::CallbackIn
extractPlaintext(obj, "encryptForCommunityInbox.obj.plaintext"),
extractSenderEd25519SeedAsVector(
obj, "encryptForCommunityInbox.obj.senderEd25519Seed"),
extractSentTimestampMs(obj, "encryptForCommunityInbox.obj.sentTimestampMs"),
// §4: sent_timestamp_ms removed — community-inbox messages carry no envelope
// ts.
extractRecipientPubkeyAsArray(
obj, "encryptForCommunityInbox.obj.recipientPubkey"),
extractCommunityPubkeyAsArray(
Expand Down
1 change: 0 additions & 1 deletion src/pro/pro.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace session::nodeapi {
std::string_view proBackendEnumToString(session::ProFeaturesForMsgStatus v) {
switch (v) {
case session::ProFeaturesForMsgStatus::Success: return "SUCCESS";
case session::ProFeaturesForMsgStatus::UTFDecodingError: return "UTF_DECODING_ERROR";
case session::ProFeaturesForMsgStatus::ExceedsCharacterLimit:
return "EXCEEDS_CHARACTER_LIMIT";
}
Expand Down
101 changes: 101 additions & 0 deletions src/user_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "profile_pic.hpp"
#include "session/config/user_profile.hpp"
#include "session/ed25519.hpp"
#include "session/session_protocol.hpp"
#include "utilities.hpp"

namespace session::nodeapi {
Expand Down Expand Up @@ -102,6 +103,13 @@ void UserConfigWrapper::Init(Napi::Env env, Napi::Object exports) {
InstanceMethod(
"generateRotatingPrivKeyHex",
&UserConfigWrapper::generateRotatingPrivKeyHex),
InstanceMethod(
"deriveProRotatingKey", &UserConfigWrapper::deriveProRotatingKey),
InstanceMethod("getRefundRequested", &UserConfigWrapper::getRefundRequested),
InstanceMethod("setRefundRequested", &UserConfigWrapper::setRefundRequested),
InstanceMethod("getProPrepaid", &UserConfigWrapper::getProPrepaid),
InstanceMethod("setProPrepaid", &UserConfigWrapper::setProPrepaid),
InstanceMethod("getProRenewalTarget", &UserConfigWrapper::getProRenewalTarget),
});
}

Expand Down Expand Up @@ -376,4 +384,97 @@ Napi::Value UserConfigWrapper::generateRotatingPrivKeyHex(const Napi::CallbackIn
});
}

Napi::Value UserConfigWrapper::deriveProRotatingKey(const Napi::CallbackInfo& info) {
return wrapResult(info, [&] {
assertInfoLength(info, 1);
auto input = info[0];
assertIsObject(input);
auto obj_in = input.As<Napi::Object>();

auto master_key_js_hex = obj_in.Get("proMasterKeyHex");
assertIsString(master_key_js_hex, "deriveProRotatingKey.proMasterKeyHex");
auto master_key_hex =
toCppString(master_key_js_hex, "deriveProRotatingKey.proMasterKeyHex");
auto master_key = from_hex_to_vector(master_key_hex);

auto now = std::chrono::floor<std::chrono::seconds>(
toCppSysMs(obj_in.Get("nowMs"), "deriveProRotatingKey.nowMs"));

// Deterministic seed for `now` (shared across the account's devices), then its ed25519
// keypair.
auto rotating_seed = session::ProProof::rotating_seed(master_key, now);
auto [rotating_pk, rotating_sk] = session::ed25519::ed25519_key_pair(rotating_seed);

auto obj = Napi::Object::New(info.Env());
obj["rotatingSeedHex"] = toJs(info.Env(), to_hex(rotating_seed));
obj["rotatingPrivKeyHex"] = toJs(info.Env(), to_hex(rotating_sk));
return obj;
});
}

Napi::Value UserConfigWrapper::getRefundRequested(const Napi::CallbackInfo& info) {
return wrapResult(info, [&] {
// libsession stores whole seconds; the JS domain is milliseconds. Null when unset (or
// gated).
auto refund_s = config.get_refund_requested();
std::optional<std::chrono::sys_time<std::chrono::milliseconds>> refund_ms;
if (refund_s)
refund_ms = std::chrono::sys_time<std::chrono::milliseconds>(
std::chrono::duration_cast<std::chrono::milliseconds>(
refund_s->time_since_epoch()));
return toJs(info.Env(), refund_ms);
});
}

void UserConfigWrapper::setRefundRequested(const Napi::CallbackInfo& info) {
wrapExceptions(info, [&] {
assertInfoLength(info, 1);
assertIsNumberOrNull(info[0], "setRefundRequested");
auto refund_ms = maybeNonemptyTimeMs(info[0], "UserConfigWrapper::setRefundRequested");
std::optional<std::chrono::sys_seconds> refund;
if (refund_ms)
refund = std::chrono::floor<std::chrono::seconds>(*refund_ms);
config.set_refund_requested(refund);
});
}

Napi::Value UserConfigWrapper::getProPrepaid(const Napi::CallbackInfo& info) {
return wrapResult(info, [&] {
auto prepaid_s = config.get_pro_prepaid();
std::optional<std::chrono::sys_time<std::chrono::milliseconds>> prepaid_ms;
if (prepaid_s)
prepaid_ms = std::chrono::sys_time<std::chrono::milliseconds>(
std::chrono::duration_cast<std::chrono::milliseconds>(
prepaid_s->time_since_epoch()));
return toJs(info.Env(), prepaid_ms);
});
}

void UserConfigWrapper::setProPrepaid(const Napi::CallbackInfo& info) {
wrapExceptions(info, [&] {
assertInfoLength(info, 1);
assertIsNumberOrNull(info[0], "setProPrepaid");
auto prepaid_ms = maybeNonemptyTimeMs(info[0], "UserConfigWrapper::setProPrepaid");
std::optional<std::chrono::sys_seconds> prepaid;
if (prepaid_ms)
prepaid = std::chrono::floor<std::chrono::seconds>(*prepaid_ms);
config.set_pro_prepaid(prepaid);
});
}

Napi::Value UserConfigWrapper::getProRenewalTarget(const Napi::CallbackInfo& info) {
return wrapResult(info, [&] {
assertInfoLength(info, 1);
auto now = std::chrono::floor<std::chrono::seconds>(
toCppSysMs(info[0], "UserConfigWrapper::getProRenewalTarget"));
auto target_s = config.pro_renewal_target(now);
std::optional<std::chrono::sys_time<std::chrono::milliseconds>> target_ms;
if (target_s)
target_ms = std::chrono::sys_time<std::chrono::milliseconds>(
std::chrono::duration_cast<std::chrono::milliseconds>(
target_s->time_since_epoch()));
return toJs(info.Env(), target_ms);
});
}

} // namespace session::nodeapi
21 changes: 9 additions & 12 deletions types/pro/pro.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ declare module 'libsession_util_nodejs' {

type GenerateProProofResponse = WithProResponseHeader & {
proof: ProProof;
/**
* Advisory account (subscription) expiry (ms) — grace-inclusive true entitlement end; present on
* a successful proof and on a `subscription_expired` failure (a past value), null otherwise.
* Distinct from the proof's own clamped expiry; unsigned / not-in-M — use for the "Pro until X"
* display and to refresh the cached access expiry (`E`), never for entitlement gating.
*/
accountExpiryMs: number | null;
};

type ProRevocationItem = WithRevocationTag & {
Expand Down Expand Up @@ -179,7 +186,6 @@ declare module 'libsession_util_nodejs' {
expiryTsMs: number;
gracePeriodDurationMs: number;
platformRefundExpiryTsMs: number;
refundRequestedTsMs: number;
/**
* Opaque payment identifier (confidential).
*/
Expand All @@ -203,19 +209,14 @@ declare module 'libsession_util_nodejs' {
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 & {
status: 'SUCCESS' | 'UTF_DECODING_ERROR' | 'EXCEEDS_CHARACTER_LIMIT';
};
utf16Count: (args: { utf16: string }) => { codepointCount: number };
utf16CountTruncatedToCodepoints: (args: { utf16: string; codepointLen: number }) => {
truncateAt: number;
proFeaturesForMessage: (args: { codepointCount: number }) => WithProMessageBitset & {
status: 'SUCCESS' | 'EXCEEDS_CHARACTER_LIMIT';
};

proProofRequest: (
Expand Down Expand Up @@ -260,8 +261,6 @@ declare module 'libsession_util_nodejs' {
*/
export class ProWrapperNode {
public static proFeaturesForMessage: ProWrapper['proFeaturesForMessage'];
public static utf16Count: ProWrapper['utf16Count'];
public static utf16CountTruncatedToCodepoints: ProWrapper['utf16CountTruncatedToCodepoints'];
public static proProofRequest: ProWrapper['proProofRequest'];
public static proRevocationsRequest: ProWrapper['proRevocationsRequest'];
public static proStatusRequest: ProWrapper['proStatusRequest'];
Expand All @@ -279,8 +278,6 @@ declare module 'libsession_util_nodejs' {
*/
export type ProActionsType =
| MakeActionCall<ProWrapper, 'proFeaturesForMessage'>
| MakeActionCall<ProWrapper, 'utf16Count'>
| MakeActionCall<ProWrapper, 'utf16CountTruncatedToCodepoints'>
| MakeActionCall<ProWrapper, 'proProofRequest'>
| MakeActionCall<ProWrapper, 'proRevocationsRequest'>
| MakeActionCall<ProWrapper, 'proStatusRequest'>
Expand Down
4 changes: 4 additions & 0 deletions types/shared.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ declare module 'libsession_util_nodejs' {
* BASE_URL_MAX_LENGTH + '/r/' + ROOM_MAX_LENGTH + qs_pubkey.size() + hex pubkey + null terminator
*/
COMMUNITY_FULL_URL_MAX_LENGTH: number;
/** Max message codepoints for a standard (non-Pro) message */
MESSAGE_CHARACTER_LIMIT_STANDARD: number;
/** Max message codepoints for a Pro message */
MESSAGE_CHARACTER_LIMIT_PRO: number;
/**
* A string that looks like libsession-util v1.2.0-nogit
*/
Expand Down
Loading
Loading