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();
}