@swimmingliu/autovpn 2.2.2 → 2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -8,6 +8,7 @@ import { AccountStore, maskAccount } from '../pool/account-store.js';
|
|
|
8
8
|
import { BRANDS, brandFromSite, brandSite, normalizeBrand } from '../pool/brands.js';
|
|
9
9
|
import { FingerprintStore, resolveFingerprintCacheDir } from './brand-cache.js';
|
|
10
10
|
import { resolveUserRuntimeRoot } from '../runtime/paths.js';
|
|
11
|
+
import { profilePayload } from '../config/profile.js';
|
|
11
12
|
import { redactText } from '../runtime/redaction.js';
|
|
12
13
|
/**
|
|
13
14
|
* How many pooled accounts one source may try before it reports failure.
|
|
@@ -1986,12 +1987,17 @@ export function classifyMembershipRefresh(facts, today) {
|
|
|
1986
1987
|
if (expires && expires < today)
|
|
1987
1988
|
return { status: 'expired', reason: 'membership expired' };
|
|
1988
1989
|
const level = facts.membership_level;
|
|
1989
|
-
|
|
1990
|
+
// Links, not routes: the route table is filtered by flags, not by the
|
|
1991
|
+
// account's grade, so a free row can list paid-looking areas and still get
|
|
1992
|
+
// nothing but placeholders. The run path uses links for the same reason.
|
|
1993
|
+
const produced = Number(facts.produced_links ?? 0) > 0;
|
|
1990
1994
|
// A free-tier row (level 0/1) only ever yields placeholders. Retire it only
|
|
1991
1995
|
// when it produced nothing, exactly as the run path does, so a paid row the
|
|
1992
|
-
// vendor misreports as level 1 keeps its place.
|
|
1993
|
-
|
|
1996
|
+
// vendor misreports as level 1 keeps its place. A redirected row is exempt
|
|
1997
|
+
// for the run path's reason: the level is the storefront's grading.
|
|
1998
|
+
if (level !== undefined && level < 2 && !produced && !facts.redirected) {
|
|
1994
1999
|
return { status: 'retired', reason: 'no paid membership' };
|
|
2000
|
+
}
|
|
1995
2001
|
return { status: 'updated', reason: '' };
|
|
1996
2002
|
}
|
|
1997
2003
|
if (facts.membership_expired)
|
|
@@ -2010,6 +2016,38 @@ export function classifyMembershipRefresh(facts, today) {
|
|
|
2010
2016
|
* active rows deliberately, reusing the normal credential flow (cached
|
|
2011
2017
|
* fingerprint, one node request) so the login path is the same one a run uses.
|
|
2012
2018
|
*/
|
|
2019
|
+
/**
|
|
2020
|
+
* Where a membership sweep should log in for each brand.
|
|
2021
|
+
*
|
|
2022
|
+
* The sweep must reach the same host a run reaches, or its verdicts describe a
|
|
2023
|
+
* different account than the one the pool rotates: on a redirected row the run
|
|
2024
|
+
* rewrites `official_site` to the backend before any request, and the sweep
|
|
2025
|
+
* used to probe the storefront the run never touches. Read from the profile so
|
|
2026
|
+
* a redirected row is known here exactly as the run knows it.
|
|
2027
|
+
*/
|
|
2028
|
+
export function resolveExpiryProbeTargets(env, readSources) {
|
|
2029
|
+
const targets = new Map();
|
|
2030
|
+
let sources = {};
|
|
2031
|
+
try {
|
|
2032
|
+
// The profile path comes from the environment, so the project root argument
|
|
2033
|
+
// is not consulted; `profilePayload` reads the same file a run does.
|
|
2034
|
+
sources = readSources ? readSources() : (profilePayload(process.cwd(), env).sources ?? {});
|
|
2035
|
+
}
|
|
2036
|
+
catch {
|
|
2037
|
+
// No readable profile: every brand falls back to its own storefront.
|
|
2038
|
+
}
|
|
2039
|
+
for (const [name, config] of Object.entries(sources)) {
|
|
2040
|
+
const brand = normalizeBrand(name);
|
|
2041
|
+
if (!brand)
|
|
2042
|
+
continue;
|
|
2043
|
+
const backend = normalizeBrand(String(config?.backend_brand ?? '').trim());
|
|
2044
|
+
targets.set(brand, {
|
|
2045
|
+
officialSite: String(config?.official_site ?? '').trim() || brandSite(brand),
|
|
2046
|
+
...(backend && backend !== brand ? { backendBrand: backend } : {})
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
return targets;
|
|
2050
|
+
}
|
|
2013
2051
|
export async function refreshAccountExpiry(input = {}) {
|
|
2014
2052
|
const env = input.env ?? process.env;
|
|
2015
2053
|
const now = input.now ?? (() => new Date());
|
|
@@ -2026,6 +2064,15 @@ export async function refreshAccountExpiry(input = {}) {
|
|
|
2026
2064
|
const requested = input.brand ? normalizeBrand(input.brand) : null;
|
|
2027
2065
|
// Every brand the pool knows, from the registry rather than a second list.
|
|
2028
2066
|
const brands = requested ? [requested] : BRANDS.map((brand) => brand.key);
|
|
2067
|
+
// How a run would reach each brand. A redirected row is graded by its own
|
|
2068
|
+
// storefront but logs in — and is graded again — against the backend it
|
|
2069
|
+
// reads, with the backend's APK. Probing the storefront instead put the sweep
|
|
2070
|
+
// on a host the run never touches (the deployment host cannot even resolve
|
|
2071
|
+
// 黑洞's storefront) and graded the account by a backend it never uses, so its
|
|
2072
|
+
// verdicts described a different account than the one the pool rotates.
|
|
2073
|
+
const probeFor = resolveExpiryProbeTargets(env);
|
|
2074
|
+
const probeSite = (brand) => probeFor.get(brand)?.officialSite ?? brandSite(brand);
|
|
2075
|
+
const probeBackend = (brand) => probeFor.get(brand)?.backendBrand;
|
|
2029
2076
|
try {
|
|
2030
2077
|
for (const brand of brands) {
|
|
2031
2078
|
let accounts = store.leaseAll(brand);
|
|
@@ -2034,10 +2081,13 @@ export async function refreshAccountExpiry(input = {}) {
|
|
|
2034
2081
|
if (accounts.length === 0)
|
|
2035
2082
|
continue;
|
|
2036
2083
|
// One memo per brand: the APK is inspected at most once for the whole
|
|
2037
|
-
// sweep, not once per account. The
|
|
2038
|
-
//
|
|
2039
|
-
//
|
|
2040
|
-
const brandKey =
|
|
2084
|
+
// sweep, not once per account. The key comes from the same function a run
|
|
2085
|
+
// uses, against the same probe source, so a redirected row is inspected
|
|
2086
|
+
// under the backend's prefix and shares the entry the run just cached.
|
|
2087
|
+
const brandKey = fingerprintKeyFor(brand, {
|
|
2088
|
+
official_site: probeSite(brand),
|
|
2089
|
+
...(probeBackend(brand) ? { brand_override: probeBackend(brand) } : {})
|
|
2090
|
+
});
|
|
2041
2091
|
const memo = createFingerprintMemo(fingerprintStore, brandKey);
|
|
2042
2092
|
await mapWithConcurrency(accounts, concurrency, async (leased) => {
|
|
2043
2093
|
const entry = {
|
|
@@ -2052,7 +2102,8 @@ export async function refreshAccountExpiry(input = {}) {
|
|
|
2052
2102
|
source_name: brand,
|
|
2053
2103
|
source: {
|
|
2054
2104
|
mode: 'credential',
|
|
2055
|
-
official_site:
|
|
2105
|
+
official_site: probeSite(brand),
|
|
2106
|
+
...(probeBackend(brand) ? { backend_brand: probeBackend(brand) } : {}),
|
|
2056
2107
|
account: leased.account,
|
|
2057
2108
|
password: leased.password,
|
|
2058
2109
|
platforms: ['android'],
|
|
@@ -2065,7 +2116,8 @@ export async function refreshAccountExpiry(input = {}) {
|
|
|
2065
2116
|
entry.expires_at = expires;
|
|
2066
2117
|
const verdict = classifyMembershipRefresh({
|
|
2067
2118
|
...result.diagnostics,
|
|
2068
|
-
produced_links: result.links.length
|
|
2119
|
+
produced_links: result.links.length,
|
|
2120
|
+
redirected: Boolean(probeBackend(brand))
|
|
2069
2121
|
}, todayText);
|
|
2070
2122
|
entry.status = verdict.status;
|
|
2071
2123
|
if (verdict.status === 'updated' || verdict.status === 'expired') {
|
package/dist/server/runtime.js
CHANGED
|
@@ -219,7 +219,18 @@ function artifactEventLog(artifactDir) {
|
|
|
219
219
|
try {
|
|
220
220
|
const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/).filter(Boolean);
|
|
221
221
|
const events = lines.map((line) => parseJsonLine(line)).filter(Boolean);
|
|
222
|
-
|
|
222
|
+
// Only the LAST run in the file decides whether it is still going. A resume
|
|
223
|
+
// appends to the same artifact log, so an earlier run's terminal line would
|
|
224
|
+
// otherwise mark the live resume finished and the page would never follow it.
|
|
225
|
+
let finished = false;
|
|
226
|
+
for (const event of events) {
|
|
227
|
+
if (event.type === 'run_started' || event.type === 'resume_latest_state' || event.type === 'resume_pipeline_state') {
|
|
228
|
+
finished = false;
|
|
229
|
+
}
|
|
230
|
+
else if (event.type === 'summary' || event.type === 'run_failed' || event.type === 'finished') {
|
|
231
|
+
finished = true;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
223
234
|
let fresh = false;
|
|
224
235
|
try {
|
|
225
236
|
fresh = Date.now() - fs.statSync(filePath).mtimeMs < 2 * 60 * 1000;
|
|
@@ -1252,7 +1252,10 @@ export function buildSourceIterationDraft(sources = {}, sourceCounts = {}) {
|
|
|
1252
1252
|
const storedDry = hasStoredLapValue(source.dry_laps) ? coercePositiveInteger(source.dry_laps, SOURCE_LAP_DEFAULTS.dry_laps) : null;
|
|
1253
1253
|
if (storedLaps !== null || storedDry !== null) {
|
|
1254
1254
|
source.max_laps = storedLaps ?? SOURCE_LAP_DEFAULTS.max_laps;
|
|
1255
|
-
|
|
1255
|
+
// Same clamp as the conversion below: a profile written by an older
|
|
1256
|
+
// build (or hand-edited) can already hold dry > max, and the drawer's
|
|
1257
|
+
// validator refuses that pair — an untouched row must stay savable.
|
|
1258
|
+
source.dry_laps = Math.min(source.max_laps, storedDry ?? SOURCE_LAP_DEFAULTS.dry_laps);
|
|
1256
1259
|
continue;
|
|
1257
1260
|
}
|
|
1258
1261
|
// Only a row that actually stored a legacy budget is converted; a fresh
|