fix(connection): keep the Happ add-subscription button working on Remnawave 2.8.0

Panel 2.8.0 removed the server-side happ-encrypt endpoint, so for users
without a stored crypto link the backend leaves {{HAPP_CRYPT4_LINK}}
unresolved and the subscriptionLink button silently disappeared
(isValidDeepLink requires '://').

- BlockButtons: resolve button templates client-side at render (with username,
  like the panel's own subpage) instead of dropping the button; collapse
  double-prefixed happ://crypt4/happ://cryptN/... resolvedUrl from old backends
- templateEngine: collapse a hardcoded happ://cryptN/ prefix before
  {{HAPP_CRYPT[34]_LINK}}; cache crypt-link generation (jsencrypt RSA-4096 is
  slow on weak devices and re-randomizes every call)
- Connection: in happ_cryptolink mode force the crypt link only when the button
  fell back to the plain subscription link or kept unresolved templates - an
  explicit Subpage link (e.g. happ://add/...) now wins, so Subpage edits apply
This commit is contained in:
Fringg
2026-07-02 16:12:18 +03:00
parent 984d1b0776
commit 8f31311112
4 changed files with 71 additions and 9 deletions

View File

@@ -6,6 +6,31 @@ export function hasTemplates(url: string): boolean {
return TEMPLATE_RE.test(url);
}
/**
* Old bot backends resolve prefix-hardcoded Subpage templates
* (happ://crypt4/{{HAPP_CRYPT4_LINK}}) into happ://crypt4/happ://cryptN/... —
* strip the outer prefix so the deep link stays openable.
*/
export function collapseDoubledCryptPrefix(url: string): string {
return url.replace(/^happ:\/\/crypt\d+\/(?=happ:\/\/crypt)/i, '');
}
// jsencrypt's RSA-4096 takes up to tens of ms on weak devices and its padding is
// random (a new string every call) — cache per subscription URL so render-time
// resolution is cheap and stable within a session. Any single ciphertext is valid.
const cryptLinkCache = new Map<string, string | null>();
function cachedHappCryptoLink(url: string, version: 'v3' | 'v4'): string | null {
const key = `${version}|${url}`;
let link = cryptLinkCache.get(key);
if (link === undefined) {
if (cryptLinkCache.size >= 100) cryptLinkCache.clear();
link = createHappCryptoLink(url, version, true);
cryptLinkCache.set(key, link);
}
return link;
}
interface ResolveContext {
subscriptionUrl: string;
username?: string;
@@ -14,6 +39,11 @@ interface ResolveContext {
export function resolveTemplate(template: string, ctx: ResolveContext): string {
let result = template;
// {{HAPP_CRYPT*_LINK}} resolves to a FULL happ://crypt.../ deep link; when the
// template also hardcodes the prefix (happ://crypt4/{{HAPP_CRYPT4_LINK}}),
// collapse it so we don't produce happ://crypt4/happ://crypt5/...
result = result.replace(/happ:\/\/crypt\d+\/(?=\{\{HAPP_CRYPT[34]_LINK\}\})/gi, '');
result = result.replace(/\{\{SUBSCRIPTION_LINK\}\}/g, ctx.subscriptionUrl);
if (ctx.username) {
@@ -21,11 +51,11 @@ export function resolveTemplate(template: string, ctx: ResolveContext): string {
}
result = result.replace(/\{\{HAPP_CRYPT3_LINK\}\}/g, () => {
return createHappCryptoLink(ctx.subscriptionUrl, 'v3', true) ?? ctx.subscriptionUrl;
return cachedHappCryptoLink(ctx.subscriptionUrl, 'v3') ?? ctx.subscriptionUrl;
});
result = result.replace(/\{\{HAPP_CRYPT4_LINK\}\}/g, () => {
return createHappCryptoLink(ctx.subscriptionUrl, 'v4', true) ?? ctx.subscriptionUrl;
return cachedHappCryptoLink(ctx.subscriptionUrl, 'v4') ?? ctx.subscriptionUrl;
});
return result;