1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// KV store resolution for hosts with no native binding (Vercel, Netlify).
//
// Cloudflare has KV; Render/Fly/Railway/Koyeb/Docker have a mounted disk. Vercel
// and Netlify have neither: their functions are stateless, so anything the panel
// writes vanishes and the VLESS UUID regenerates on every cold start, silently
// breaking every subscription link already handed to a client.
//
// The portable fix is a Redis REST endpoint, which is plain fetch() and so works
// identically on Node, Deno and workerd. Vercel's Marketplace KV (Upstash) sets
// KV_REST_API_URL/KV_REST_API_TOKEN for you; a standalone Upstash database sets
// UPSTASH_REDIS_REST_URL/UPSTASH_REDIS_REST_TOKEN. Either is picked up here.
//
// With no endpoint configured we fall back to per-instance memory plus env-var
// identities, which keeps subscriptions stable even though panel edits will not
// survive. `createKVStore` reports which mode it chose so the caller can warn.
const ENV_FALLBACK = {
'config:admin_password': 'PANEL_PASSWORD',
'config:jwt_secret': 'JWT_SECRET',
'config:sub_token': 'SUB_TOKEN',
'config:vless_uuid': 'VLESS_UUID',
'config:trojan_password': 'TROJAN_PASSWORD',
'config:proxy_path': 'PROXY_PATH',
'config:node_share_token': 'NODE_SHARE_TOKEN'
};
const REST_URL_VARS = ['KV_REST_API_URL', 'UPSTASH_REDIS_REST_URL', 'REDIS_REST_URL'];
const REST_TOKEN_VARS = ['KV_REST_API_TOKEN', 'UPSTASH_REDIS_REST_TOKEN', 'REDIS_REST_TOKEN'];
function firstSet(readEnv, names) {
for (const n of names) {
const v = readEnv(n);
if (v) return String(v).trim();
}
return null;
}
/**
* Redis-over-HTTP store. Commands go in the POST body as a JSON array so keys
* containing ':' need no path escaping.
*/
export function createRestKV(url, token, { fetchImpl = fetch } = {}) {
const endpoint = url.replace(/\/+$/, '');
async function cmd(args) {
const res = await fetchImpl(endpoint, {
method: 'POST',
headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify(args)
});
if (!res.ok) {
throw new Error('KV REST ' + res.status + ': ' + (await res.text()).slice(0, 200));
}
const data = await res.json();
if (data && data.error) throw new Error('KV REST: ' + data.error);
return data ? data.result : null;
}
return {
mode: 'rest',
async get(key) {
const v = await cmd(['GET', key]);
return v === null || v === undefined ? null : String(v);
},
async put(key, value) {
await cmd(['SET', key, String(value)]);
return true;
},
async delete(key) {
await cmd(['DEL', key]);
return true;
}
};
}
/** Per-instance memory, with env vars supplying stable identities. */
export function createMemoryKV(readEnv) {
const store = new Map();
return {
mode: 'memory',
async get(key) {
if (store.has(key)) return store.get(key);
const envName = ENV_FALLBACK[key];
const fromEnv = envName ? readEnv(envName) : undefined;
return fromEnv ? String(fromEnv) : null;
},
async put(key, value) {
store.set(key, String(value));
return true;
},
async delete(key) {
return store.delete(key);
}
};
}
/**
* Pick the best store the environment allows. Returns { kv, mode, warning }.
* A REST store that cannot be reached degrades to memory rather than taking the
* whole panel down, but says so loudly.
*/
export function createKVStore(readEnv, options = {}) {
const url = firstSet(readEnv, REST_URL_VARS);
const token = firstSet(readEnv, REST_TOKEN_VARS);
if (url && token) {
return {
kv: createRestKV(url, token, options),
mode: 'rest',
warning: null
};
}
return {
kv: createMemoryKV(readEnv),
mode: 'memory',
warning:
'No durable KV configured. Panel changes will not survive a cold start. ' +
'Set KV_REST_API_URL + KV_REST_API_TOKEN (Vercel Marketplace KV) or ' +
'UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN, and set PANEL_PASSWORD, ' +
'JWT_SECRET, SUB_TOKEN, VLESS_UUID and TROJAN_PASSWORD so identities stay stable.'
};
}
// Kept for callers that only want the volatile behaviour.
export const createEnvKV = createMemoryKV;