@luckypiggy/zhihu-ai-cli 0.1.0-test.1

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.
@@ -0,0 +1,42 @@
1
+ const SENSITIVE_KEY_PATTERN = /(?:access|refresh|pilot)?[_-]?(?:token|secret|password|cookie)|authorization|api[_-]?key|private[_-]?key|credential|code[_-]?verifier|polling[_-]?secret|oauth[_-]?state|authorization[_-]?url/i;
2
+ const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g;
3
+ const BEARER_PATTERN = /\bBearer\s+[^\s,;]+/gi;
4
+ const KEY_VALUE_PATTERN = /((?:access|refresh|pilot)?[_-]?(?:token|secret|password|cookie)|authorization|api[_-]?key|private[_-]?key|credential)(\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;}]+)/gi;
5
+ const OAUTH_STATE_URL_PATTERN = /([?&](?:oauth_)?state=)[^&#\s]+/gi;
6
+ export const REDACTED_VALUE = "[REDACTED]";
7
+ /** Redact credentials from a string before it reaches a log or error. */
8
+ export function redactString(value) {
9
+ return value
10
+ .replace(BEARER_PATTERN, "Bearer [REDACTED]")
11
+ .replace(KEY_VALUE_PATTERN, (_match, key, separator, rawValue) => {
12
+ const preserveBearer = key.toLowerCase() === "authorization" && rawValue.trim().toLowerCase() === "bearer";
13
+ return `${key}${separator}${preserveBearer ? "Bearer " : ""}${REDACTED_VALUE}`;
14
+ })
15
+ .replace(OAUTH_STATE_URL_PATTERN, "$1[REDACTED]")
16
+ .replace(/\bBearer\s+\[REDACTED\](?:\s+\[REDACTED\])+/gi, "Bearer [REDACTED]")
17
+ .replace(JWT_PATTERN, REDACTED_VALUE);
18
+ }
19
+ /** Recursively redact known secret fields without mutating the input value. */
20
+ export function redactValue(value, key = "") {
21
+ if (SENSITIVE_KEY_PATTERN.test(key)) {
22
+ return REDACTED_VALUE;
23
+ }
24
+ if (typeof value === "string") {
25
+ return redactString(value);
26
+ }
27
+ if (Array.isArray(value)) {
28
+ return value.map((item) => redactValue(item));
29
+ }
30
+ if (value && typeof value === "object") {
31
+ const output = {};
32
+ for (const [childKey, childValue] of Object.entries(value)) {
33
+ output[childKey] = redactValue(childValue, childKey);
34
+ }
35
+ return output;
36
+ }
37
+ return value;
38
+ }
39
+ export function redactErrorMessage(error) {
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ return redactString(message);
42
+ }
@@ -0,0 +1,320 @@
1
+ import { CredentialStoreError } from "./credentials.js";
2
+ import { HttpError } from "./http.js";
3
+ import { withProfileLock, ProfileLockError } from "./profile-lock.js";
4
+ const CLOCK_SKEW_SECONDS = 60;
5
+ export class CliAuthError extends Error {
6
+ code;
7
+ retryable;
8
+ exitCode;
9
+ constructor(code, message, options = {}) {
10
+ super(message);
11
+ this.name = "CliAuthError";
12
+ this.code = code;
13
+ this.retryable = options.retryable ?? false;
14
+ this.exitCode = options.exitCode ?? (code === "AUTH_RELOGIN_REQUIRED" ? 3 : 8);
15
+ }
16
+ }
17
+ function decodeClaims(token) {
18
+ try {
19
+ const pieces = token.split(".");
20
+ const payload = pieces.length === 3 ? pieces[1] : "";
21
+ if (!payload)
22
+ return {};
23
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payload.length / 4) * 4, "=");
24
+ const parsed = JSON.parse(Buffer.from(normalized, "base64").toString("utf8"));
25
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
26
+ }
27
+ catch {
28
+ return {};
29
+ }
30
+ }
31
+ function tokenExpiry(token, fallbackSeconds) {
32
+ const exp = decodeClaims(token).exp;
33
+ if (typeof exp === "number" && Number.isFinite(exp) && exp > 0)
34
+ return exp * 1000;
35
+ return Date.now() + fallbackSeconds * 1000;
36
+ }
37
+ function mapFailure(error) {
38
+ if (error instanceof CliAuthError)
39
+ return error;
40
+ if (error instanceof CredentialStoreError) {
41
+ return new CliAuthError(error.code, "Unable to access the system credential store", { exitCode: 3 });
42
+ }
43
+ if (error instanceof ProfileLockError) {
44
+ return new CliAuthError("AUTH_LOCK_TIMEOUT", "Another process is refreshing this profile; try again.", { retryable: true, exitCode: 4 });
45
+ }
46
+ if (error instanceof HttpError) {
47
+ const exitCode = error.code === "AUTH_RELOGIN_REQUIRED"
48
+ ? 3
49
+ : error.code === "CONFIG_INVALID"
50
+ ? 2
51
+ : error.code === "INVALID_ARGUMENT"
52
+ ? 2
53
+ : error.code === "MODEL_NOT_FOUND"
54
+ ? 2
55
+ : error.code === "ASSET_NOT_FOUND"
56
+ ? 2
57
+ : error.code === "ALARM_NOT_FOUND"
58
+ ? 2
59
+ : error.code === "INCIDENT_ASSET_IDENTITY_UNAVAILABLE"
60
+ ? 2
61
+ : error.code === "METRIC_DATA_NOT_FOUND"
62
+ || error.code === "ASSET_METRIC_IDENTITY_UNAVAILABLE"
63
+ ? 2
64
+ : error.code === "FORBIDDEN"
65
+ || error.code === "CAPABILITY_SCOPE_REQUIRED"
66
+ || error.code === "CAPABILITY_NOT_AVAILABLE"
67
+ || error.code === "EMPLOYEE_NOT_FOUND"
68
+ || error.code === "EMPLOYEE_NOT_CHATABLE"
69
+ || error.code === "CONVERSATION_NOT_FOUND"
70
+ ? 5
71
+ : error.code === "SERVER_ERROR"
72
+ ? 6
73
+ : error.code === "VERSION_INCOMPATIBLE"
74
+ || error.code === "SERVICE_CONFIGURATION_REQUIRED"
75
+ ? 7
76
+ : error.code === "PROTOCOL_ERROR"
77
+ || error.code === "RESPONSE_TOO_LARGE"
78
+ || error.code === "UPSTREAM_PROTOCOL_ERROR"
79
+ || error.code === "UPSTREAM_RESPONSE_TOO_LARGE"
80
+ ? 8
81
+ : 4;
82
+ return new CliAuthError(error.code, error.message, { retryable: error.retryable, exitCode });
83
+ }
84
+ return new CliAuthError("NETWORK_ERROR", "Unable to reach the Zhihu service", { retryable: true, exitCode: 4 });
85
+ }
86
+ export class TokenManager {
87
+ client;
88
+ credentials;
89
+ profile;
90
+ operationTail = Promise.resolve();
91
+ accessTokenValue;
92
+ accessInfoValue;
93
+ constructor(client, credentials, profile) {
94
+ this.client = client;
95
+ this.credentials = credentials;
96
+ this.profile = profile;
97
+ }
98
+ get accessInfo() {
99
+ return this.accessInfoValue;
100
+ }
101
+ async getAccessToken(forceRefresh = false) {
102
+ return this.exclusive(async () => {
103
+ if (!forceRefresh && this.hasFreshAccessToken())
104
+ return this.accessTokenValue;
105
+ return this.refreshWithProfileLock();
106
+ });
107
+ }
108
+ async refreshNow() {
109
+ return this.exclusive(() => this.refreshWithProfileLock());
110
+ }
111
+ async refreshWithProfileLock() {
112
+ try {
113
+ return await withProfileLock(this.profile, () => this.refreshWithinLock());
114
+ }
115
+ catch (error) {
116
+ throw mapFailure(error);
117
+ }
118
+ }
119
+ async refreshWithinLock() {
120
+ const oldCredential = await this.credentials.get();
121
+ if (!oldCredential) {
122
+ throw new CliAuthError("AUTH_RELOGIN_REQUIRED", "No active login; run zhihu auth login.", { exitCode: 3 });
123
+ }
124
+ let refreshed;
125
+ try {
126
+ refreshed = await this.client.refresh(oldCredential);
127
+ }
128
+ catch (error) {
129
+ const mapped = mapFailure(error);
130
+ if (mapped.code === "AUTH_RELOGIN_REQUIRED") {
131
+ try {
132
+ await this.credentials.delete();
133
+ }
134
+ catch {
135
+ // The stable re-login error is safer than retrying a dead secret.
136
+ }
137
+ }
138
+ throw mapped;
139
+ }
140
+ try {
141
+ // The new credential must be durable before its access token is used.
142
+ await this.credentials.set(refreshed.refresh_credential);
143
+ }
144
+ catch (error) {
145
+ // Rotation already invalidated oldCredential. Best effort compensation
146
+ // prevents an orphaned 30-day server session when the keychain fails.
147
+ try {
148
+ await this.client.logout(refreshed.refresh_credential);
149
+ }
150
+ catch {
151
+ // The user is still told to re-login; no secret or provider body is emitted.
152
+ }
153
+ try {
154
+ await this.credentials.delete();
155
+ }
156
+ catch {
157
+ // Keep the primary credential-store failure stable.
158
+ }
159
+ const mapped = mapFailure(error);
160
+ throw new CliAuthError("AUTH_RELOGIN_REQUIRED", "Credential storage failed; run zhihu auth login again.", { retryable: mapped.retryable, exitCode: 3 });
161
+ }
162
+ this.accessTokenValue = refreshed.access_token;
163
+ this.accessInfoValue = {
164
+ expiresAt: tokenExpiry(refreshed.access_token, refreshed.expires_in),
165
+ claims: decodeClaims(refreshed.access_token),
166
+ };
167
+ return refreshed.access_token;
168
+ }
169
+ hasFreshAccessToken() {
170
+ return Boolean(this.accessTokenValue
171
+ && this.accessInfoValue
172
+ && this.accessInfoValue.expiresAt > Date.now() + CLOCK_SKEW_SECONDS * 1000);
173
+ }
174
+ async exclusive(operation) {
175
+ let release;
176
+ const previous = this.operationTail;
177
+ this.operationTail = new Promise((resolve) => {
178
+ release = resolve;
179
+ });
180
+ await previous;
181
+ try {
182
+ return await operation();
183
+ }
184
+ finally {
185
+ release();
186
+ }
187
+ }
188
+ /**
189
+ * Execute one idempotent/read-only request with at most one forced refresh
190
+ * after an HTTP 401. Write operations must not use this helper.
191
+ */
192
+ async callReadOnly(operation) {
193
+ try {
194
+ return await this.exclusive(async () => withProfileLock(this.profile, async () => {
195
+ let accessToken = this.hasFreshAccessToken()
196
+ ? this.accessTokenValue
197
+ : await this.refreshWithinLock();
198
+ try {
199
+ return await operation(accessToken);
200
+ }
201
+ catch (error) {
202
+ if (!isAuthenticationFailure(error))
203
+ throw mapFailure(error);
204
+ }
205
+ accessToken = await this.refreshWithinLock();
206
+ try {
207
+ return await operation(accessToken);
208
+ }
209
+ catch (error) {
210
+ if (!isAuthenticationFailure(error))
211
+ throw mapFailure(error);
212
+ await this.clearDeadCredentialWithinLock();
213
+ throw new CliAuthError("AUTH_RELOGIN_REQUIRED", "Your Zhihu session has expired. Run `zhihu auth login`.", { exitCode: 3 });
214
+ }
215
+ }));
216
+ }
217
+ catch (error) {
218
+ throw mapFailure(error);
219
+ }
220
+ }
221
+ /** Alias used by command adapters that prefer a higher-level name. */
222
+ async withReadOnly(operation) {
223
+ return this.callReadOnly(operation);
224
+ }
225
+ /**
226
+ * Execute a streaming read-only request. A 401 can refresh once only when
227
+ * no response event has reached the caller; replaying after a delta would
228
+ * duplicate a conversation and is therefore forbidden.
229
+ */
230
+ async callStreaming(operation) {
231
+ try {
232
+ let observedEvent = false;
233
+ const markEvent = () => {
234
+ observedEvent = true;
235
+ };
236
+ // Profile locking is intentionally limited to token acquisition. The
237
+ // long-lived SSE operation must not hold another CLI command hostage.
238
+ let accessToken = await this.getAccessToken();
239
+ try {
240
+ return await operation(accessToken, markEvent);
241
+ }
242
+ catch (error) {
243
+ if (!isAuthenticationFailure(error) || observedEvent)
244
+ throw mapFailure(error);
245
+ }
246
+ accessToken = await this.refreshNow();
247
+ try {
248
+ return await operation(accessToken, markEvent);
249
+ }
250
+ catch (error) {
251
+ if (!isAuthenticationFailure(error) || observedEvent)
252
+ throw mapFailure(error);
253
+ await this.clearDeadCredential();
254
+ throw new CliAuthError("AUTH_RELOGIN_REQUIRED", "Your Zhihu session has expired. Run `zhihu auth login`.", { exitCode: 3 });
255
+ }
256
+ }
257
+ catch (error) {
258
+ throw mapFailure(error);
259
+ }
260
+ }
261
+ async clearDeadCredential() {
262
+ await this.exclusive(() => withProfileLock(this.profile, () => this.clearDeadCredentialWithinLock()));
263
+ }
264
+ async clearDeadCredentialWithinLock() {
265
+ let currentCredential = null;
266
+ try {
267
+ currentCredential = await this.credentials.get();
268
+ }
269
+ catch {
270
+ // Continue to local cleanup even when the credential store is unreadable.
271
+ }
272
+ if (currentCredential) {
273
+ try {
274
+ await this.client.logout(currentCredential);
275
+ }
276
+ catch {
277
+ // The local credential is still removed below; no secret/provider body
278
+ // is allowed to replace the stable re-login result.
279
+ }
280
+ }
281
+ try {
282
+ await this.credentials.delete();
283
+ }
284
+ catch {
285
+ // The stable re-login error is safer than surfacing a keychain body.
286
+ }
287
+ finally {
288
+ this.accessTokenValue = undefined;
289
+ this.accessInfoValue = undefined;
290
+ }
291
+ }
292
+ async logout() {
293
+ try {
294
+ return await this.exclusive(() => withProfileLock(this.profile, async () => {
295
+ const credential = await this.credentials.get();
296
+ if (!credential)
297
+ return "already_logged_out";
298
+ try {
299
+ await this.client.logout(credential);
300
+ }
301
+ catch (error) {
302
+ if (!(error instanceof HttpError) || error.code !== "AUTH_RELOGIN_REQUIRED")
303
+ throw mapFailure(error);
304
+ // A server-side invalid/revoked session is already logged out.
305
+ }
306
+ await this.credentials.delete();
307
+ this.accessTokenValue = undefined;
308
+ this.accessInfoValue = undefined;
309
+ return "revoked";
310
+ }));
311
+ }
312
+ catch (error) {
313
+ throw mapFailure(error);
314
+ }
315
+ }
316
+ }
317
+ function isAuthenticationFailure(error) {
318
+ return error instanceof HttpError && (error.status === 401 || (error.status === undefined && error.code === "AUTH_RELOGIN_REQUIRED"));
319
+ }
320
+ export { decodeClaims, mapFailure };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@luckypiggy/zhihu-ai-cli",
3
+ "version": "0.1.0-test.1",
4
+ "description": "智护 Pilot CLI:认证、数字员工、资产、指标、告警、指定告警诊断和巡检只读能力(P1 Read-only)",
5
+ "keywords": [
6
+ "zhihu-pilot",
7
+ "aiops",
8
+ "codex",
9
+ "cli",
10
+ "digital-employee"
11
+ ],
12
+ "author": {
13
+ "name": "Zhihu Pilot"
14
+ },
15
+ "product": "zhihu-pilot",
16
+ "productName": "智护 Pilot",
17
+ "type": "module",
18
+ "bin": {
19
+ "zhihu": "bin/zhihu.js"
20
+ },
21
+ "files": [
22
+ "bin",
23
+ "dist",
24
+ "assets/codex-skills",
25
+ "README.md",
26
+ "package.json"
27
+ ],
28
+ "engines": {
29
+ "node": ">=22.0.0"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.json",
33
+ "prepack": "npm run build",
34
+ "prepublishOnly": "npm test",
35
+ "test": "npm run build && node --test tests/*.mjs",
36
+ "pack:dry-run": "npm_config_cache=\"${TMPDIR:-/tmp}/zhihu-ai-cli-npm-cache\" npm pack --dry-run --json --loglevel=error",
37
+ "build:offline": "node scripts/build-offline-release.mjs",
38
+ "verify:offline": "node scripts/verify-offline-release.mjs"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "22.15.0",
42
+ "typescript": "5.8.3"
43
+ },
44
+ "license": "UNLICENSED",
45
+ "packageManager": "npm@11.4.2",
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "registry": "https://registry.npmjs.org/"
49
+ }
50
+ }