File size: 6,018 Bytes
67d18ac | 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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | /**
* Wiring between the claim subsystem and the rest of the proxy: builds a
* scheduler from a loaded `ProxyConfig` + `AuthManager` (serve path) and
* implements the one-shot CLI flow (`zcode-proxy claim [list|now]`).
*/
import type { AuthManager } from "../auth/manager.js";
import type { ProxyConfig } from "../config/types.js";
import type { ClaimablePlan, ClaimOutcome } from "./types.js";
import { createClaimClient, ClaimPreviewError } from "./client.js";
import { ClaimScheduler } from "./scheduler.js";
import { getCaptchaToken } from "../proxy/captcha.js";
import { loadCredential } from "../auth/store.js";
/** `${process.platform}-${process.arch}` — mirrors the client's `TH()`. */
export function claimPlatform(): string {
return `${process.platform}-${process.arch}`;
}
export function startAutoClaim(config: ProxyConfig, auth: AuthManager): ClaimScheduler {
const scheduler = new ClaimScheduler({
// AuthManager first (fresh), then the encrypted store — on Android the
// login can land in the store after boot while auth hasn't been reloaded.
getJwt: async () => {
try {
const cred = await auth.getCredential();
if (cred.jwt) return cred.jwt;
} catch { /* fall through to the store */ }
const stored = await loadCredential().catch(() => null);
return stored?.jwt;
},
createClient: (jwt) =>
createClaimClient({
origin: config.claim.origin,
jwt,
appVersion: config.identity.appVersion,
platform: claimPlatform(),
identity: config.identity,
}),
getCaptcha: async () => {
const { verifyParam, region } = await getCaptchaToken(config.identity.appVersion);
return { verifyParam, region: region || undefined };
},
config: {
planId: config.claim.planId || undefined,
pollIntervalMs: config.claim.pollIntervalMs,
cooldownMs: config.claim.cooldownMs,
},
log: (message) => console.log(`[claim] ${message}`),
});
scheduler.start();
return scheduler;
}
const FAILURE_LABELS: Record<string, string> = {
not_found: "plan does not exist",
unavailable: "campaign ended or not claimable yet",
already_claimed: "already claimed on this account",
ineligible: "account or client version not eligible (needs appVersion >= campaign minimum)",
quota_exhausted: "daily claim quota exhausted",
invalid_request: "invalid request",
captcha: "captcha verification failed",
login_required: "not logged in",
http_error: "HTTP error",
unknown: "unknown failure",
};
/** One-shot CLI: `list` prints previews; `now` claims the target plan. */
export async function runClaimCli(config: ProxyConfig, mode: "list" | "now"): Promise<void> {
const cred = await loadCredential();
const jwt = cred?.jwt;
if (!jwt) {
console.error("Claim requires a logged-in oauth credential (no JWT stored). Run: zcode-proxy auth login <zai|bigmodel>");
process.exit(1);
}
const client = createClaimClient({
origin: config.claim.origin,
jwt,
appVersion: config.identity.appVersion,
platform: claimPlatform(),
identity: config.identity,
});
let plans: ClaimablePlan[];
try {
plans = await client.getPreviews();
} catch (err) {
if (err instanceof ClaimPreviewError && err.status === 404) {
console.log("No claimable plans: the campaign endpoint is not deployed yet (404).");
console.log("Weekend campaigns typically go live shortly before the window — keep the proxy");
console.log("serving with claim.enabled, or re-run this command later.");
return;
}
throw err;
}
if (plans.length === 0) {
console.log("No claimable plans right now.");
return;
}
printPlans(plans);
if (mode === "list") return;
const wanted = config.claim.planId.trim();
const target = wanted ? plans.find((p) => p.planId === wanted) : [...plans].sort((a, b) => b.priority - a.priority)[0];
if (!target) {
console.error(`Configured claim.planId "${wanted}" not in the preview list.`);
process.exit(1);
}
if (target.planId !== plans[0].planId) console.log(`Claiming configured plan: ${target.planId}`);
const captcha = await getCaptchaToken(config.identity.appVersion);
const outcome = await client.claim(target.planId, { verifyParam: captcha.verifyParam, region: captcha.region || undefined });
printOutcome(outcome);
if (!outcome.ok) process.exit(1);
}
function printPlans(plans: ClaimablePlan[]): void {
console.log(`Claimable plans (${plans.length}):`);
for (const p of plans) {
const window = [fmtTime(p.startsAt), fmtTime(p.endsAt)].filter(Boolean).join(" → ");
console.log(` - ${p.planId} "${p.name}" priority=${p.priority}${window ? ` ${window}` : ""}`);
for (const e of p.entitlements) {
const quota = e.grantUnits > 0 ? ` ${e.grantUnits} ${e.unitType}` : "";
const activate = e.effectiveAt !== undefined ? ` (activates ${new Date(e.effectiveAt * 1000).toISOString()})` : "";
console.log(` · ${e.showName || e.entitlementId}${quota}${activate}`);
}
}
}
function printOutcome(outcome: ClaimOutcome): void {
if (outcome.ok) {
console.log(`\nClaimed: ${outcome.planId}`);
if (outcome.startsAt !== undefined) console.log(` activates: ${new Date(outcome.startsAt * 1000).toISOString()}`);
if (outcome.endsAt !== undefined) console.log(` expires: ${new Date(outcome.endsAt * 1000).toISOString()}`);
if (outcome.startsAt === undefined && outcome.endsAt === undefined) console.log(" active immediately");
return;
}
const label = FAILURE_LABELS[outcome.failureKind] ?? FAILURE_LABELS.unknown;
console.error(`\nClaim failed: ${label} (code ${String(outcome.code)}) — ${outcome.message}`);
if (outcome.failureEndsAt !== undefined) {
console.error(` retry window opens: ${new Date(outcome.failureEndsAt * 1000).toISOString()}`);
}
}
function fmtTime(sec: number | undefined): string {
return sec === undefined ? "" : new Date(sec * 1000).toISOString();
}
|