Pro backend 0.2 + libsession 1.7 compatibility updates#1966
Conversation
… wire Start migrating the desktop app off hand-rolled zod wire parsing onto the libsession (nodejs glue) parsers + structs. - types.ts: provider/plan and now account/payment status are all opaque wire slugs (libsession emits strings). Replaced the numeric enums with slug constants (ProStatus never/active/expired, ProItemStatus unredeemed/…/revoked, ProAccessVariant 1m/3m/1y, ProPaymentProvider google_play/app_store/rangeproof) + open string types; deleted the ProOriginatingPlatform<->numeric bridges. - schemas.ts: deleted the zod request/response wire schemas; re-export the response types from libsession_util_nodejs (GetProDetailsResponse, etc.) so import sites stay stable. Kept a small DB schema for the revocation cache (glue shape: genIndexHashB64 + effectiveMs). - libsession_constants.ts: dropped LIBSESSION_PRO_PROVIDERS (metadata table gone); added LIBSESSION_PRO_BACKEND_URL + _PUBKEY_HEX (single source of truth). Remaining (next commits): rewire ProBackendAPI request/response flow to the glue parsers, then the field-rename ripple through ducks/selectors/revocation/UI + persistence migration + ProBackendTarget URL/pubkey. WIP, non-compiling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o client parsing
Desktop no longer parses (or assumes the format of) the Pro backend response —
that wire format is a libsession<->backend contract. Desktop just relays bytes.
- session_backend_server: new makeRequestReturningRawBody() returns { status_code,
bodyBinary } straight from the onion V4 response — transport status from the V4
metadata, body as the raw bytes (no content-type parsing). The existing
makeRequestWithSchema/_makeRequest JSON path is untouched (still used by NetworkApi).
- ProBackendAPI: requests are built by libsession ({ endpoint, body }); the endpoint
is no longer hardcoded and requestVersion is gone. Responses relay bodyBinary to the
glue parsers (parseProProofResponse / parsePaymentDetailsResponse /
parseRevocationsResponse) and return the typed struct. Transport failure -> null;
app-level errors come back in the struct's errors/status.
- worker interface: ProWrapperActions updated to the new glue methods (proProofRequest/
proStatusRequest/proRevocationsRequest returning {endpoint,body}; parse*Response taking
{ body: Uint8Array }; providerUrls). Worker dispatch is dynamic so no switch change.
Remaining: field-rename ripple through ducks/selectors/revocation/UI + persistence +
ProBackendTarget URL/pubkey. WIP, non-compiling until the glue is rebuilt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relay libsession's Content-Type through the onion request path (makeRequestReturningRawBody takes contentType, sets the header; ProBackendAPI.sendAndParse threads request.contentType) so the client never assumes the wire format. Point the DEFAULT pro backend target at the libsession constants (url + ed/x25519 pubkeys) instead of hand-carried copies, so there is a single source of truth; add LIBSESSION_PRO_BACKEND_PUBKEY_X25519_HEX to the desktop constants mirror. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ProBackendAPI now returns libsession's parsed response structs directly
(flat `{ errors, ...camelCase ms fields }`) instead of an HTTP-wrapper
`{ status_code, t, result }`. Adapt the consumers:
- proBackendData duck: fetch thunk keys success off `errors` (not
status_code) and stores the struct as-is; handleNewProProof relays the
ready-made `proof`; callback reads userStatus/expiryMs/autoRenewing/
errorReport; unknown status slug no longer hard-fails.
- UpdateProRevocationListJob: use libsession's absolute `retryAtMs`
(arithmetic subsumed into the glue) and the flat ticket/items fields.
- pro_revocation_list cache + its test: item shape is
{ genIndexHashB64, effectiveMs } (matches the glue's ProRevocationItem).
- FeatureFlags debug proof button: relay the ready-made proof.
Provider-model selectors/UI (LIBSESSION_PRO_PROVIDERS -> providerUrls +
client-owned display names) are intentionally not touched here; that is a
follow-up needing a localization decision.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
status field The nodejs glue no longer emits an `errors` array — the response header is now `status` ('ok'/'fail'/'error') + optional `errorCode` slug + diagnostic `error`. Success is `status === 'ok'` (was `errors.length === 0`); the fetch thunk derives its error from `error`/`errorCode`. Fixes the earlier response-consumer commit, which keyed off the removed `errors` field. (Provider display-name i18n and the error_code->localized-string mapping remain, pending Crowdin tokens.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
libsession no longer supplies provider display names or (synchronously) URLs. Rework the provider-constants model: - Display names come from client i18n: getProProviderConstantsWithFallbacks(slug) returns store/platform/device/platform_account/store_other via tr() tokens (proProvider*). Those strings are a TEMP stopgap injected into the generated localization (english.ts/locales.ts) so they resolve now but are wiped by the next Crowdin sync — upstream them first. - URLs are fetched on demand at click time via ProWrapperActions.providerUrls(slug) in ProNonOriginatingPage (openProviderUrl helper), not threaded through the synchronous selector. Also finish the selector's Delta session-foundation#12 field renames (expiryMs/userStatus/ autoRenewing/paymentProvider/platformRefundExpiryTsMs/refundRequestedTsMs), replace the deleted ProDetailsResultSchema with a light storage check, make proAccessVariantToString/provider handling tolerate slug/unknown values (drop assertUnreachable), and fix stale ProPaymentProvider member names (iOSAppStore->AppStore, GooglePlayStore->GooglePlay, Nil->''). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e messages
Resolve provider display from pro_provider_<slug>_<suffix> tokens; build the {pro_stores}
purchasable-store list from ProWrapperActions.visiblePlatforms() (gated on the store token existing);
map error_code slugs to localized pro_error_<slug> strings. Adds ProWrapperActions.visiblePlatforms;
removes the now-unused store_other provider constant.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n_tag; no migration, Pro unshipped) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ProProof expiry travels on the wire as whole seconds — that is what the Session Pro backend signs over and what libsession parses via `as_sys_seconds` (session_protocol.cpp). Our JS domain keeps proof timestamps in milliseconds (the nodejs wrapper multiplies by 1000 on the way in), so the outgoing proof proto was serialising a millisecond value into a seconds field, ~1000x too large. Any receiver reconstructs `expiry_at = as_sys_seconds(<ms value>)` and the proof signature check fails. Convert back to seconds at the proto boundary and rename the field `expireAtMs` -> `expiryUnixTs` (field number 4 unchanged, wire-compatible) to match the canonical libsession proto and the android/iOS copies, and to stop the name lying about the unit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The genIndexHash->revocationTag rename edited migration v50 in place, changing the column it adds from `proGenIndexHashB64` to `proRevocationTagB64`. Existing installs already ran v50 and so still have `proGenIndexHashB64`; editing v50 does not re-run for them, and the app now reads `proRevocationTagB64` -> "SQLITE_ERROR: no such column". (Pro is dormant but its schema is built into shipped clients, so v50 has been applied in the wild.) Restore v50 to its shipped form (`proGenIndexHashB64`) and add a new schema version 57 that renames the column in place, preserving data. The rename is guarded on the actual columns present so it is idempotent for internal builds that already ran the edited v50 (new column already present) or somehow have neither column. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| const urls = await ProWrapperActions.providerUrls({ code: provider }); | ||
| if (urls) { | ||
| showLinkVisitWarningDialog(pick(urls), dispatch); | ||
| } |
There was a problem hiding this comment.
[] is truthy in js, so I believe we could show that dialog without an url?
| static async getProDetails(args: WithMasterPrivKeyHex): Promise<GetProDetailsResponseType | null> { | ||
| const request = await ProWrapperActions.proStatusRequest({ | ||
| ...args, | ||
| unixTsMs: NetworkTime.now(), | ||
| // NOTE: the latest payment is the only one required for state derivation | ||
| count: 1, | ||
| }); | ||
| return request; | ||
| } | ||
|
|
||
| private static async getRevocationListBody(args: WithTicket) { | ||
| const body = await ProWrapperActions.proRevocationsRequestBody({ requestVersion: 0, ...args }); | ||
| return body; | ||
| } | ||
|
|
||
| static async getProDetails( | ||
| args: WithMasterPrivKeyHex | ||
| ): Promise<GetProDetailsResponseType | null> { | ||
| return ProBackendAPI.getServer().makeRequestWithSchema({ | ||
| path: '/get_pro_details', | ||
| method: 'POST', | ||
| bodyGetter: () => ProBackendAPI.getProDetailsBody(args), | ||
| withZodSchema: GetProDetailsResponseSchema, | ||
| }); | ||
| return ProBackendAPI.sendAndParse(request, body => | ||
| ProWrapperActions.parsePaymentDetailsResponse({ body }) | ||
| ); |
There was a problem hiding this comment.
We call
ProWrapperActions.parsePaymentDetailsResponse
for a ProWrapperActions.proStatusRequest
is this expected?
There was a problem hiding this comment.
Yes, it is correct, but the names are indeed misleading and need fixing. The pro-endpoint is poorly named:
/get_pro_details
Description
Retrieve the list of current and historical payments associated with the Session Pro master
public key. The returned list is in descending order from the date that the payment was
registered (e.g.: newest payment to oldest).
I renamed the actual structs in libsession to reflect what it actually is (payment details), but it seems there are still some places where it is called a vague "pro details", and I'll push fixes through for that. (Also I'm going to rename that endpoint itself).
There was a problem hiding this comment.
Actually the problem is a bit different: this endpoint is actually two different endpoints in a trenchcoat: one is "give me payment details" and one is "give me current pro status". It's the latter we want and use, but the two got conflated into just one that is awkward to use. I'm separating them and will push the update through. (This will become parseProStatusResponse and getProStatus; the payment details code isn't current used client side, but will be available whenever we want to start using it).
The pro RequestState carried a `t` timestamp copied from the shared shape used by networkData (where `t` is a real server-provided marker read via getInfoTimestamp). For the pro backend it was never populated by the server, and after the raw-relay refactor there's no `response.t` at all — it was just a local "fetched at" stamp that nothing downstream reads (verified: every consumer destructures only `data`). Rather than keep a dead field that had already drifted to the wrong unit (ms vs the seconds it should be), drop it from RequestState, defaultRequestState, the fetch results, and ProcessedProDetails. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The get-details handler cleared the pro proof for any non-active userStatus, including the `default` (unknown/future slug) case. handleClearProProof() writes the SYNCED user config (removeProConfig + setProAccessExpiry(null)), so an older client that doesn't recognise a new status slug would erase a still-valid proof and propagate that deletion to all the user's devices. Make the unknown case non-destructive: log and leave the proof. Entitlement is governed by the proof's own signature + expiry (hasValidCurrentProProof); a genuinely-lapsed account simply won't get its proof refreshed (or gets revoked). Explicit Expired/NeverBeenPro still clear as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openProviderUrl only checked that the providerUrls object was non-null before opening the link-visit warning dialog. A provider may expose only some URLs, and libsession returns an empty string for an absent one, so picking (e.g.) cancelSubscriptionUrl could hand the dialog '' and open it pointing at nothing. Guard on the picked URL, not just the container. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getProDetailsFromStorage read the persisted libsession struct back with only a
top-level object/items[] check and cast it to the current ProDetailsResultType.
That meant a future libsession change adding a required field couldn't be made
safely: an older client's cache lacks the field but the type assumes it. (No live
crash today — processProBackendData reads every field defensively and the cache
is re-fetched next poll — but a real forward-compat trap.)
Wrap the cache as { version, data } and drop it on a version mismatch, so a
stale-shape cache is re-fetched rather than mis-read. Bumping
PRO_DETAILS_CACHE_VERSION on any shape change now makes adding a required field
safe. The pre-versioning format has no version and is dropped the same way.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(no versioning) Reverts the preemptive cache-versioning: session-desktop and libsession-util-nodejs ship in lockstep, so within a build the glue-produced shape, the type, and this consumer can't drift. The only cross-version exposure is a cache written by an older build, and that is owned by whatever future change adds a required field (handle the cache then, like a migration). Pre-building versioning now would just be guessing at the future shape. Instead, leave a clear reminder at getProDetailsFromStorage (and putProDetailsInStorage) that adding a REQUIRED field to the persisted shape needs a transition here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The DEV pro-backend target hard-copied a URL + Ed25519/X25519 pubkeys that were byte-identical to libsession's own pro_backend constants — duplicated single- source data that shouldn't exist in the client (the production DEFAULT target already reads them from libsession). There is no distinct dev backend today (and a future one likely can't carry full signing keys), so rather than keep stale copies or silently fall back to the default backend, the DEV target now holds non-functional placeholders and getServer() throws a clear error if `useTestProBackend` is enabled. Wire real values in — sourced from libsession — if/when a dev backend exists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reviewer flagged isSimpleTokenNoArgs. It's the correct check — pro_error_* strings take no args, so membership is exactly "does a translation exist for this slug." Say that at the check itself, and drop the docstring's unrelated cross-references/backstory. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The account-status enum lives in a Pro-only namespace and the wire value is already "never"; NeverBeenPro was a redundant/awkward member name. Identifier-only rename, value unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the desktop app to the split endpoint (libsession Delta session-foundation#15, glue b9f5893): - ProBackendAPI.getProDetails -> getProStatus; proStatusRequest drops count; parsePaymentDetailsResponse -> parseProStatusResponse; return ProStatusResultType (fixes a pre-existing broken import of the never-exported GetProDetailsResponseType). - schemas: GetProDetailsResponse -> GetProStatusResponse, ProDetailsResultType -> ProStatusResultType. - selector: items[0] -> latestPayment; storage shape-guard keys off userStatus (was Array.isArray(items)); plan display now derives from {planCount, planUnit} via planPeriodToString (minimal English; Crowdin-localized version is separate). - worker interface + storage + debug menu renamed accordingly. tsc clean. Localized plan strings and the remaining internal *ProDetails* identifier purge are follow-ups. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename the endpoint-tied identifiers to match the get_pro_status split: ProcessedProDetails->ProcessedProStatus, getProBackendProDetails->getProBackendProStatus, useProBackendProDetails->useProBackendProStatus (+ Internal/Local), fetch/refreshGetProDetailsFromProBackend->...ProStatus..., firstFetchProDetailsHappened. Deliberately NOT touched: the separate per-contact/message pro-proof-metadata concept (ProDetailsContact, processProDetailsForMsg, applyProDetailsChange, dbContactProDetails, emptyProDetails, WithProDetailsContact, the `proDetails` locals/`proDetailsChanged` flag), the SettingsKey.proDetails storage key, and DecodedPro.proStatus — none of those are the endpoint. tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace planPeriodToString's hardcoded English with date-fns formatDuration (new formatPlanDuration helper), so any parsed plan period renders localized and pluralized with the unit preserved (12 months stays "12 months", never rewritten to 1 year). "lifetime" isn't a duration: resolve proPlanLifetime if the Crowdin key exists yet, else fall back to English "Lifetime". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
When a template contains formatting tags, Localizer HTML-escapes all substitution
args so dynamic values can't inject markup. That also escaped the client-built
{pro_stores} bulleted list (<br/>• …), rendering it as literal text.
Add an opt-in `htmlArgs` prop naming args whose value is trusted, pre-formatted
HTML and should skip that escaping. It stays XSS-safe: SessionHtmlRenderer still
runs DOMPurify with the supportedFormattingTags whitelist on the final string, so
only formatting survives — never scripts or attributes. Wire the three
{pro_stores} call sites (ProNonOriginatingPage) to use it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The get_pro_status backend result was still persisted, surfaced, and logged as
"proDetails"/"Pro Details" even though its types were already renamed to ProStatus
— e.g. getProStatusFromStorage() read SettingsKey.proDetails. Rename the leftover
endpoint-result naming to proStatus: the SettingsKey (constant + value), the
Storage put/get/remove, the debug menu buttons ("Get/Refresh Pro Status") and
logs, and the refresh comments/logs.
The persisted value string changes proDetails -> proStatus; it's a transient,
re-fetched cache so any stale entry is simply orphaned and repopulated.
Note: the remaining `proDetails`/ProDetailsContact identifiers are a separate
concept (a contact's/message's synced pro-proof metadata), not this endpoint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Desktop counterpart of the Pro wire-format + display rework. Consumes the flat, libsession-parsed response structs and relays raw wire bytes both ways (never parses the wire itself). Relies on the updated
libsession-util-nodejsglue (session-foundation/libsession-util-nodejs#63).This is required for desktop compatibility with session-foundation/session-pro-backend#2 and session-foundation/libsession-util#95.
Changes
statusfield ('ok'|'fail'|'error') +errorCode+ singleerror; request-side raw-wire relay finished; backend URL/pubkeys read from libsession (single source).getProProviderConstantsWithFallbacksresolvespro_provider_<slug>_<suffix>tokens (fallback to the raw slug); provider support/management URLs fetched on demand viaProWrapperActions.providerUrls.{pro_stores}store list: built fromProWrapperActions.visiblePlatforms()(gated on the store token existing) and substituted into the "purchase via …" messages; removes the two-storestore_otherconstant.error_codeto a localizedpro_error_<slug>string (falling back to the backend diagnostic).ProWrapperActions.visiblePlatformsto the worker interface.Dependencies