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
21 changes: 20 additions & 1 deletion include/pro/pro.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ 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.
// Advisory account (subscription) expiry — the account's true paid-through expiry, with
// coverage running to expiry + grace_period rather than ending here.
// 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.
Expand All @@ -281,6 +282,24 @@ class ProWrapper : public Napi::ObjectWrap<ProWrapper> {
std::chrono::duration_cast<std::chrono::milliseconds>(
resp.account_expiry->time_since_epoch()));
obj["accountExpiryMs"] = toJs(env, account_expiry_ms);

// The account's auto-renewing flag and grace period, advisory like the expiry above, so
// a proof fetch can refresh config keys `A` and `G` alongside `E` rather than leaving
// them to desync. Non-null: core requires all three on a successful parse, so a
// response missing one is a parse error rather than a defaulted value.
//
// ⚠️ Only MEANINGFUL on a success. On a failure outcome core never fills them and the
// struct's own defaults (`false` / `0s`) come through, indistinguishable from a backend
// that really said "not renewing, no grace". Read them only where `status == 'ok'`.
obj["accountAutoRenewing"] = toJs(env, resp.account_auto_renewing);

// Seconds in core, milliseconds in the JS domain, matching getProGracePeriod and the
// `gracePeriodDurationMs` field on the get_pro_status response.
obj["accountGracePeriodMs"] =
toJs(env,
static_cast<int64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
resp.account_grace_period)
.count()));
return obj;
});
};
Expand Down
12 changes: 12 additions & 0 deletions include/user_config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ class UserConfigWrapper : public ConfigBaseImpl, public Napi::ObjectWrap<UserCon
void setRefundRequested(const Napi::CallbackInfo& info);
Napi::Value getProPrepaid(const Napi::CallbackInfo& info);
void setProPrepaid(const Napi::CallbackInfo& info);
// Auto-renewing (config key A). Presence-only in core: set_pro_auto_renewing uses
// set_nonzero_int, so writing false ERASES the key — absent means terminal/unknown, and a
// caller must compare the getter's value rather than the key's presence.
Napi::Value getProAutoRenewing(const Napi::CallbackInfo& info);
void setProAutoRenewing(const Napi::CallbackInfo& info);
// Grace period (config key G), in ms on the JS side. Synced alongside E so any linked device can
// derive when coverage ends as E + G: E is the account's true paid-through expiry, and the backend
// keeps serving for G past it, so [E, E + G) is expired-but-still-served. This is the
// ACCOUNT-level grace, not the per-payment field of the same name. Deliberately not optional --
// the backend sends 0 when not auto-renewing, and E + 0 == E.
Napi::Value getProGracePeriod(const Napi::CallbackInfo& info);
void setProGracePeriod(const Napi::CallbackInfo& info);
Napi::Value getProRenewalTarget(const Napi::CallbackInfo& info);
};
}; // namespace session::nodeapi
42 changes: 42 additions & 0 deletions src/user_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ void UserConfigWrapper::Init(Napi::Env env, Napi::Object exports) {
InstanceMethod("setRefundRequested", &UserConfigWrapper::setRefundRequested),
InstanceMethod("getProPrepaid", &UserConfigWrapper::getProPrepaid),
InstanceMethod("setProPrepaid", &UserConfigWrapper::setProPrepaid),
InstanceMethod("getProAutoRenewing", &UserConfigWrapper::getProAutoRenewing),
InstanceMethod("setProAutoRenewing", &UserConfigWrapper::setProAutoRenewing),
InstanceMethod("getProGracePeriod", &UserConfigWrapper::getProGracePeriod),
InstanceMethod("setProGracePeriod", &UserConfigWrapper::setProGracePeriod),
InstanceMethod("getProRenewalTarget", &UserConfigWrapper::getProRenewalTarget),
});
}
Expand Down Expand Up @@ -462,6 +466,44 @@ void UserConfigWrapper::setProPrepaid(const Napi::CallbackInfo& info) {
});
}

Napi::Value UserConfigWrapper::getProAutoRenewing(const Napi::CallbackInfo& info) {
return wrapResult(info, [&] { return toJs(info.Env(), config.get_pro_auto_renewing()); });
}

void UserConfigWrapper::setProAutoRenewing(const Napi::CallbackInfo& info) {
wrapExceptions(info, [&] {
assertInfoLength(info, 1);
assertIsBoolean(info[0], "setProAutoRenewing");
auto auto_renewing = toCppBoolean(info[0], "UserConfigWrapper::setProAutoRenewing");
config.set_pro_auto_renewing(auto_renewing);
});
}

Napi::Value UserConfigWrapper::getProGracePeriod(const Napi::CallbackInfo& info) {
return wrapResult(info, [&] {
// libsession stores whole seconds; the JS domain is milliseconds, matching the rest of the
// Pro accessors (and the `gracePeriodDurationMs` field callers get from get_pro_status).
// Not optional, unlike the auto-renewing pair: the backend sends 0 when the subscription
// isn't auto-renewing, and `E - 0 == E`, so an absent key and a stored zero are the same
// account.
auto grace_s = config.get_pro_grace_period();
return static_cast<int64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(grace_s).count());
});
}

void UserConfigWrapper::setProGracePeriod(const Napi::CallbackInfo& info) {
wrapExceptions(info, [&] {
assertInfoLength(info, 1);
assertIsNumber(info[0], "setProGracePeriod");
auto grace_ms = toCppInteger(info[0], "UserConfigWrapper::setProGracePeriod", false);
// Floor rather than round: a grace period is a coverage window, so truncating keeps
// `E + G` from claiming coverage the backend didn't grant. Zero or negative clears the key.
config.set_pro_grace_period(
std::chrono::floor<std::chrono::seconds>(std::chrono::milliseconds{grace_ms}));
});
}

Napi::Value UserConfigWrapper::getProRenewalTarget(const Napi::CallbackInfo& info) {
return wrapResult(info, [&] {
assertInfoLength(info, 1);
Expand Down
23 changes: 21 additions & 2 deletions types/pro/pro.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,31 @@ 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.
* Advisory account (subscription) expiry (ms) — the account's true paid-through expiry, the same
* value `get_pro_status` reports as `expiryMs`. Coverage runs to `expiryMs + gracePeriodMs`, so this
* is not the instant the backend stops serving. 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;
/**
* Whether the subscription auto-renews. Non-null: core requires this on a successful parse, so a
* response missing it is a parse error rather than a defaulted value.
*
* ⚠️ Only meaningful when `status === 'ok'`. On a failure outcome core never fills it and the
* struct's own default `false` comes through — indistinguishable from a backend that really said
* "not auto-renewing". Config key `A` is presence-only, so writing that false would ERASE what a
* `get_pro_status` fetch had learned. Read it only inside the success branch.
*/
accountAutoRenewing: boolean;
/**
* The account's grace period (ms). Non-null for the same reason; core stores whole seconds.
*
* ⚠️ Same scope caveat: on a failure outcome this is the struct default `0`, and zero erases config
* key `G`. Read it only inside the success branch.
*/
accountGracePeriodMs: number;
};

type ProRevocationItem = WithRevocationTag & {
Expand Down
30 changes: 30 additions & 0 deletions types/user/userconfig.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,28 @@ declare module 'libsession_util_nodejs' {
/** Pro-prepaid / purchase-in-flight marker (config key I), in ms; null when unset or gated. */
getProPrepaid: () => number | null;
setProPrepaid: (prepaidTsMs: number | null) => void;
/**
* Whether the subscription auto-renews (config key A), from get_pro_status.auto_renewing.
*
* Presence-only: core writes it with set_nonzero_int, so `setProAutoRenewing(false)` ERASES the key.
* The getter returns false for "not auto-renewing", "never fetched" and "written by a client
* predating key A" alike — never key a change check on whether the key is present.
*/
getProAutoRenewing: () => boolean;
setProAutoRenewing: (autoRenewing: boolean) => void;
/**
* The account's grace period (config key G), in MILLISECONDS, from the ACCOUNT-level
* get_pro_status.grace_period_duration. 0 when unset. Core stores seconds; the conversion floors.
*
* Coverage ends at `getProAccessExpiry() + getProGracePeriod()` — `E` is the paid-through expiry and
* the backend serves for `G` past it, so `[E, E + G)` is expired-but-still-served.
*
* NOT the `gracePeriodDurationMs` on a response's `latestPayment`: that is one store's declaration
* about a single transaction, is not gated on auto-renewal, and a subscriber who cancels mid-retry
* keeps a nonzero value in it. Only meaningful written from the same response as `E`.
*/
getProGracePeriod: () => number;
setProGracePeriod: (graceMs: number) => void;
/**
* When to (re)request a proof given `nowMs`: nowMs (request now), a future ms (preemptive
* renewal ~1h before expiry), or null (don't renew). Supersedes bespoke auto-renew logic.
Expand Down Expand Up @@ -136,6 +158,10 @@ declare module 'libsession_util_nodejs' {
public setRefundRequested: UserConfigWrapper['setRefundRequested'];
public getProPrepaid: UserConfigWrapper['getProPrepaid'];
public setProPrepaid: UserConfigWrapper['setProPrepaid'];
public getProAutoRenewing: UserConfigWrapper['getProAutoRenewing'];
public setProAutoRenewing: UserConfigWrapper['setProAutoRenewing'];
public getProGracePeriod: UserConfigWrapper['getProGracePeriod'];
public setProGracePeriod: UserConfigWrapper['setProGracePeriod'];
public getProRenewalTarget: UserConfigWrapper['getProRenewalTarget'];
}

Expand Down Expand Up @@ -175,5 +201,9 @@ declare module 'libsession_util_nodejs' {
| MakeActionCall<UserConfigWrapper, 'setRefundRequested'>
| MakeActionCall<UserConfigWrapper, 'getProPrepaid'>
| MakeActionCall<UserConfigWrapper, 'setProPrepaid'>
| MakeActionCall<UserConfigWrapper, 'getProAutoRenewing'>
| MakeActionCall<UserConfigWrapper, 'setProAutoRenewing'>
| MakeActionCall<UserConfigWrapper, 'getProGracePeriod'>
| MakeActionCall<UserConfigWrapper, 'setProGracePeriod'>
| MakeActionCall<UserConfigWrapper, 'getProRenewalTarget'>;
}
Loading