@luckypiggy/zhihu-ai-cli 0.1.0-test.1 → 0.2.1-test.4
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.
- package/README.md +32 -20
- package/assets/codex-skills/zhihu-aiops/SKILL.md +32 -16
- package/assets/codex-skills/zhihu-aiops/references/commands.md +28 -14
- package/assets/codex-skills/zhihu-aiops/references/errors.md +1 -1
- package/assets/codex-skills/zhihu-aiops/references/safety.md +6 -4
- package/dist/auth-service.js +71 -83
- package/dist/cli.js +226 -22
- package/dist/codex-skill.js +3 -3
- package/dist/config.js +14 -3
- package/dist/contracts.js +3 -1
- package/dist/credentials.js +152 -29
- package/dist/http.js +413 -67
- package/dist/install.js +310 -59
- package/dist/profile-lock.js +32 -3
- package/dist/release.js +24 -0
- package/dist/terminal-prompt.js +191 -0
- package/dist/token-manager.js +36 -15
- package/package.json +4 -2
- package/dist/browser.js +0 -49
- package/dist/pkce.js +0 -15
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as defaultInput, stderr as defaultOutput } from "node:process";
|
|
3
|
+
export class TerminalPromptCancelledError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super("Login cancelled");
|
|
6
|
+
this.name = "TerminalPromptCancelledError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class TerminalPromptInteractiveRequiredError extends Error {
|
|
10
|
+
constructor() {
|
|
11
|
+
super("Interactive terminal input is required");
|
|
12
|
+
this.name = "TerminalPromptInteractiveRequiredError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function validateValue(value, label, maxLength, required = true) {
|
|
16
|
+
const normalized = value.trim();
|
|
17
|
+
if ((!normalized && required) || normalized.length > maxLength || /[\u0000-\u001f\u007f]/.test(normalized)) {
|
|
18
|
+
throw new Error(`${label} is invalid`);
|
|
19
|
+
}
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
function isTty(stream) {
|
|
23
|
+
return Boolean(stream.isTTY);
|
|
24
|
+
}
|
|
25
|
+
function throwIfAborted(signal) {
|
|
26
|
+
if (signal?.aborted)
|
|
27
|
+
throw new TerminalPromptCancelledError();
|
|
28
|
+
}
|
|
29
|
+
async function ask(rl, label, defaultValue, output, signal, required = true) {
|
|
30
|
+
throwIfAborted(signal);
|
|
31
|
+
const prompt = `${label}${defaultValue === undefined ? "" : ` [${defaultValue}]`}: `;
|
|
32
|
+
let remove = () => { };
|
|
33
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
34
|
+
const cancel = () => reject(new TerminalPromptCancelledError());
|
|
35
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
36
|
+
remove = () => signal?.removeEventListener("abort", cancel);
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
const value = await Promise.race([rl.question(prompt), cancellation]);
|
|
40
|
+
const answer = value.trim() || defaultValue || "";
|
|
41
|
+
if (!answer && required) {
|
|
42
|
+
output.write("A value is required. Please try again.\n");
|
|
43
|
+
return ask(rl, label, defaultValue, output, signal, required);
|
|
44
|
+
}
|
|
45
|
+
return answer;
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
remove();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function promptHiddenPassword(input, output, label, signal) {
|
|
52
|
+
const rawInput = input;
|
|
53
|
+
if (typeof rawInput.setRawMode !== "function" || typeof input.on !== "function") {
|
|
54
|
+
throw new TerminalPromptInteractiveRequiredError();
|
|
55
|
+
}
|
|
56
|
+
throwIfAborted(signal);
|
|
57
|
+
output.write(label);
|
|
58
|
+
let answer = "";
|
|
59
|
+
let settled = false;
|
|
60
|
+
const previousRaw = rawInput.isRaw === true;
|
|
61
|
+
const previousPaused = typeof rawInput.isPaused === "function" ? rawInput.isPaused() : false;
|
|
62
|
+
const finish = (resolve, reject, error) => {
|
|
63
|
+
if (settled)
|
|
64
|
+
return;
|
|
65
|
+
settled = true;
|
|
66
|
+
input.removeListener?.("data", onData);
|
|
67
|
+
input.removeListener?.("error", onError);
|
|
68
|
+
input.removeListener?.("end", onEnd);
|
|
69
|
+
input.removeListener?.("close", onClose);
|
|
70
|
+
signal?.removeEventListener("abort", onAbort);
|
|
71
|
+
try {
|
|
72
|
+
rawInput.setRawMode?.(previousRaw);
|
|
73
|
+
}
|
|
74
|
+
catch { /* best effort */ }
|
|
75
|
+
if (previousPaused)
|
|
76
|
+
rawInput.pause?.();
|
|
77
|
+
else
|
|
78
|
+
rawInput.resume?.();
|
|
79
|
+
output.write("\n");
|
|
80
|
+
if (error)
|
|
81
|
+
reject(error);
|
|
82
|
+
else
|
|
83
|
+
resolve(answer);
|
|
84
|
+
};
|
|
85
|
+
let resolveValue;
|
|
86
|
+
let rejectValue;
|
|
87
|
+
const promise = new Promise((resolve, reject) => {
|
|
88
|
+
resolveValue = resolve;
|
|
89
|
+
rejectValue = reject;
|
|
90
|
+
});
|
|
91
|
+
const onAbort = () => finish(resolveValue, rejectValue, new TerminalPromptCancelledError());
|
|
92
|
+
const onError = () => finish(resolveValue, rejectValue, new TerminalPromptCancelledError());
|
|
93
|
+
const onEnd = () => finish(resolveValue, rejectValue, new TerminalPromptCancelledError());
|
|
94
|
+
const onClose = () => finish(resolveValue, rejectValue, new TerminalPromptCancelledError());
|
|
95
|
+
const onData = (chunk) => {
|
|
96
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
|
|
97
|
+
for (const character of text) {
|
|
98
|
+
if (character === "\u0003" || character === "\u001b") {
|
|
99
|
+
finish(resolveValue, rejectValue, new TerminalPromptCancelledError());
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (character === "\r" || character === "\n") {
|
|
103
|
+
if (!answer) {
|
|
104
|
+
output.write("\nA value is required. Please try again.\n");
|
|
105
|
+
output.write(label);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
finish(resolveValue, rejectValue);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (character === "\u007f" || character === "\b") {
|
|
112
|
+
answer = answer.slice(0, -1);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (character < " " || character === "\u007f") {
|
|
116
|
+
finish(resolveValue, rejectValue, new Error("Password is invalid"));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (character >= " " && character !== "\u007f") {
|
|
120
|
+
if (answer.length >= 4096) {
|
|
121
|
+
finish(resolveValue, rejectValue, new Error("Password is invalid"));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
answer += character;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
input.on("data", onData);
|
|
129
|
+
input.once?.("error", onError);
|
|
130
|
+
input.once?.("end", onEnd);
|
|
131
|
+
input.once?.("close", onClose);
|
|
132
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
133
|
+
try {
|
|
134
|
+
rawInput.setRawMode(true);
|
|
135
|
+
rawInput.resume?.();
|
|
136
|
+
return await promise;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
finish(resolveValue, rejectValue, error instanceof Error ? error : new TerminalPromptCancelledError());
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function secret(input, output, label, signal, injected) {
|
|
144
|
+
throwIfAborted(signal);
|
|
145
|
+
const value = injected
|
|
146
|
+
? await injected(label)
|
|
147
|
+
: isTty(input)
|
|
148
|
+
? await promptHiddenPassword(input, output, label, signal)
|
|
149
|
+
: (() => { throw new TerminalPromptInteractiveRequiredError(); })();
|
|
150
|
+
// Passwords are opaque credentials: preserve leading/trailing spaces and
|
|
151
|
+
// validate only the same boundaries enforced by the HTTP contract.
|
|
152
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096 || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
153
|
+
throw new Error("Password is invalid");
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
export async function promptTerminalCredentials(options = {}) {
|
|
158
|
+
const input = options.input ?? defaultInput;
|
|
159
|
+
const output = options.output ?? defaultOutput;
|
|
160
|
+
const rl = createInterface({ input, output });
|
|
161
|
+
try {
|
|
162
|
+
const usernameLabel = options.usernameLabel ?? "Zhihu username";
|
|
163
|
+
const tenantLabel = options.tenantLabel ?? "Tenant id (optional)";
|
|
164
|
+
const captchaLabel = options.captchaLabel ?? "Captcha verification (optional)";
|
|
165
|
+
const passwordLabel = options.passwordLabel ?? "Zhihu password: ";
|
|
166
|
+
const usernameRaw = options.valuePrompt
|
|
167
|
+
? await options.valuePrompt(usernameLabel, options.defaultUsername)
|
|
168
|
+
: await ask(rl, usernameLabel, options.defaultUsername, output, options.signal);
|
|
169
|
+
const username = validateValue(usernameRaw, "Username", 256);
|
|
170
|
+
const tenantRaw = options.valuePrompt
|
|
171
|
+
? await options.valuePrompt(tenantLabel, options.defaultTenantId)
|
|
172
|
+
: await ask(rl, tenantLabel, options.defaultTenantId, output, options.signal, false);
|
|
173
|
+
const tenant_id = tenantRaw.trim() ? validateValue(tenantRaw, "Tenant id", 128) : undefined;
|
|
174
|
+
const captchaRaw = options.valuePrompt
|
|
175
|
+
? await options.valuePrompt(captchaLabel)
|
|
176
|
+
: await ask(rl, captchaLabel, undefined, output, options.signal, false);
|
|
177
|
+
const captcha_verification = captchaRaw.trim() ? validateValue(captchaRaw, "Captcha verification", 4096) : undefined;
|
|
178
|
+
if (isTty(input) && !options.secretPrompt)
|
|
179
|
+
rl.close();
|
|
180
|
+
const password = await secret(input, output, passwordLabel, options.signal, options.secretPrompt);
|
|
181
|
+
return {
|
|
182
|
+
username,
|
|
183
|
+
password,
|
|
184
|
+
...(tenant_id ? { tenant_id } : {}),
|
|
185
|
+
...(captcha_verification ? { captcha_verification } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
rl.close();
|
|
190
|
+
}
|
|
191
|
+
}
|
package/dist/token-manager.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CredentialStoreError } from "./credentials.js";
|
|
2
2
|
import { HttpError } from "./http.js";
|
|
3
3
|
import { withProfileLock, ProfileLockError } from "./profile-lock.js";
|
|
4
|
+
import { cliCommand } from "./release.js";
|
|
4
5
|
const CLOCK_SKEW_SECONDS = 60;
|
|
5
6
|
export class CliAuthError extends Error {
|
|
6
7
|
code;
|
|
@@ -64,21 +65,39 @@ function mapFailure(error) {
|
|
|
64
65
|
: error.code === "FORBIDDEN"
|
|
65
66
|
|| error.code === "CAPABILITY_SCOPE_REQUIRED"
|
|
66
67
|
|| error.code === "CAPABILITY_NOT_AVAILABLE"
|
|
68
|
+
|| error.code === "A2A_DISABLED"
|
|
69
|
+
|| error.code === "A2A_CAPABILITY_NOT_FOUND"
|
|
70
|
+
|| error.code === "A2A_CAPABILITY_DISABLED"
|
|
71
|
+
|| error.code === "A2A_TASK_NOT_FOUND"
|
|
72
|
+
|| error.code === "A2A_FORBIDDEN"
|
|
73
|
+
|| error.code === "A2A_CANCELLED"
|
|
74
|
+
|| error.code === "A2A_EMPTY_RESPONSE"
|
|
75
|
+
|| error.code === "A2A_EXECUTION_INDETERMINATE"
|
|
67
76
|
|| error.code === "EMPLOYEE_NOT_FOUND"
|
|
68
77
|
|| error.code === "EMPLOYEE_NOT_CHATABLE"
|
|
69
78
|
|| error.code === "CONVERSATION_NOT_FOUND"
|
|
70
79
|
? 5
|
|
71
80
|
: error.code === "SERVER_ERROR"
|
|
81
|
+
|| error.code === "A2A_RUNTIME_ERROR"
|
|
82
|
+
|| error.code === "A2A_TIMEOUT"
|
|
83
|
+
|| error.code === "A2A_DUPLICATE"
|
|
84
|
+
|| error.code === "A2A_TASK_START_FAILED"
|
|
72
85
|
? 6
|
|
73
86
|
: error.code === "VERSION_INCOMPATIBLE"
|
|
74
87
|
|| error.code === "SERVICE_CONFIGURATION_REQUIRED"
|
|
88
|
+
|| error.code === "TERMINAL_LOGIN_REQUIRED"
|
|
75
89
|
? 7
|
|
76
|
-
: error.code === "
|
|
77
|
-
|| error.code === "
|
|
78
|
-
|| error.code === "
|
|
79
|
-
|| error.code === "
|
|
80
|
-
?
|
|
81
|
-
:
|
|
90
|
+
: error.code === "AUTH_LOGIN_FAILED"
|
|
91
|
+
|| error.code === "AUTH_ACCOUNT_LOCKED"
|
|
92
|
+
|| error.code === "AUTH_CAPTCHA_REQUIRED"
|
|
93
|
+
|| error.code === "INTERACTIVE_REQUIRED"
|
|
94
|
+
? 2
|
|
95
|
+
: error.code === "PROTOCOL_ERROR"
|
|
96
|
+
|| error.code === "RESPONSE_TOO_LARGE"
|
|
97
|
+
|| error.code === "UPSTREAM_PROTOCOL_ERROR"
|
|
98
|
+
|| error.code === "UPSTREAM_RESPONSE_TOO_LARGE"
|
|
99
|
+
? 8
|
|
100
|
+
: 4;
|
|
82
101
|
return new CliAuthError(error.code, error.message, { retryable: error.retryable, exitCode });
|
|
83
102
|
}
|
|
84
103
|
return new CliAuthError("NETWORK_ERROR", "Unable to reach the Zhihu service", { retryable: true, exitCode: 4 });
|
|
@@ -87,13 +106,15 @@ export class TokenManager {
|
|
|
87
106
|
client;
|
|
88
107
|
credentials;
|
|
89
108
|
profile;
|
|
109
|
+
environment;
|
|
90
110
|
operationTail = Promise.resolve();
|
|
91
111
|
accessTokenValue;
|
|
92
112
|
accessInfoValue;
|
|
93
|
-
constructor(client, credentials, profile) {
|
|
113
|
+
constructor(client, credentials, profile, options = {}) {
|
|
94
114
|
this.client = client;
|
|
95
115
|
this.credentials = credentials;
|
|
96
116
|
this.profile = profile;
|
|
117
|
+
this.environment = options.environment;
|
|
97
118
|
}
|
|
98
119
|
get accessInfo() {
|
|
99
120
|
return this.accessInfoValue;
|
|
@@ -110,7 +131,7 @@ export class TokenManager {
|
|
|
110
131
|
}
|
|
111
132
|
async refreshWithProfileLock() {
|
|
112
133
|
try {
|
|
113
|
-
return await withProfileLock(this.profile, () => this.refreshWithinLock());
|
|
134
|
+
return await withProfileLock(this.profile, () => this.refreshWithinLock(), { environment: this.environment });
|
|
114
135
|
}
|
|
115
136
|
catch (error) {
|
|
116
137
|
throw mapFailure(error);
|
|
@@ -119,7 +140,7 @@ export class TokenManager {
|
|
|
119
140
|
async refreshWithinLock() {
|
|
120
141
|
const oldCredential = await this.credentials.get();
|
|
121
142
|
if (!oldCredential) {
|
|
122
|
-
throw new CliAuthError("AUTH_RELOGIN_REQUIRED",
|
|
143
|
+
throw new CliAuthError("AUTH_RELOGIN_REQUIRED", `No active login; run ${cliCommand("auth", "login")}.`, { exitCode: 3 });
|
|
123
144
|
}
|
|
124
145
|
let refreshed;
|
|
125
146
|
try {
|
|
@@ -157,7 +178,7 @@ export class TokenManager {
|
|
|
157
178
|
// Keep the primary credential-store failure stable.
|
|
158
179
|
}
|
|
159
180
|
const mapped = mapFailure(error);
|
|
160
|
-
throw new CliAuthError("AUTH_RELOGIN_REQUIRED",
|
|
181
|
+
throw new CliAuthError("AUTH_RELOGIN_REQUIRED", `Credential storage failed; run ${cliCommand("auth", "login")} again.`, { retryable: mapped.retryable, exitCode: 3 });
|
|
161
182
|
}
|
|
162
183
|
this.accessTokenValue = refreshed.access_token;
|
|
163
184
|
this.accessInfoValue = {
|
|
@@ -210,9 +231,9 @@ export class TokenManager {
|
|
|
210
231
|
if (!isAuthenticationFailure(error))
|
|
211
232
|
throw mapFailure(error);
|
|
212
233
|
await this.clearDeadCredentialWithinLock();
|
|
213
|
-
throw new CliAuthError("AUTH_RELOGIN_REQUIRED",
|
|
234
|
+
throw new CliAuthError("AUTH_RELOGIN_REQUIRED", `Your Zhihu session has expired. Run \`${cliCommand("auth", "login")}\`.`, { exitCode: 3 });
|
|
214
235
|
}
|
|
215
|
-
}));
|
|
236
|
+
}, { environment: this.environment }));
|
|
216
237
|
}
|
|
217
238
|
catch (error) {
|
|
218
239
|
throw mapFailure(error);
|
|
@@ -251,7 +272,7 @@ export class TokenManager {
|
|
|
251
272
|
if (!isAuthenticationFailure(error) || observedEvent)
|
|
252
273
|
throw mapFailure(error);
|
|
253
274
|
await this.clearDeadCredential();
|
|
254
|
-
throw new CliAuthError("AUTH_RELOGIN_REQUIRED",
|
|
275
|
+
throw new CliAuthError("AUTH_RELOGIN_REQUIRED", `Your Zhihu session has expired. Run \`${cliCommand("auth", "login")}\`.`, { exitCode: 3 });
|
|
255
276
|
}
|
|
256
277
|
}
|
|
257
278
|
catch (error) {
|
|
@@ -259,7 +280,7 @@ export class TokenManager {
|
|
|
259
280
|
}
|
|
260
281
|
}
|
|
261
282
|
async clearDeadCredential() {
|
|
262
|
-
await this.exclusive(() => withProfileLock(this.profile, () => this.clearDeadCredentialWithinLock()));
|
|
283
|
+
await this.exclusive(() => withProfileLock(this.profile, () => this.clearDeadCredentialWithinLock(), { environment: this.environment }));
|
|
263
284
|
}
|
|
264
285
|
async clearDeadCredentialWithinLock() {
|
|
265
286
|
let currentCredential = null;
|
|
@@ -307,7 +328,7 @@ export class TokenManager {
|
|
|
307
328
|
this.accessTokenValue = undefined;
|
|
308
329
|
this.accessInfoValue = undefined;
|
|
309
330
|
return "revoked";
|
|
310
|
-
}));
|
|
331
|
+
}, { environment: this.environment }));
|
|
311
332
|
}
|
|
312
333
|
catch (error) {
|
|
313
334
|
throw mapFailure(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luckypiggy/zhihu-ai-cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1-test.4",
|
|
4
4
|
"description": "智护 Pilot CLI:认证、数字员工、资产、指标、告警、指定告警诊断和巡检只读能力(P1 Read-only)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"zhihu-pilot",
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
"test": "npm run build && node --test tests/*.mjs",
|
|
36
36
|
"pack:dry-run": "npm_config_cache=\"${TMPDIR:-/tmp}/zhihu-ai-cli-npm-cache\" npm pack --dry-run --json --loglevel=error",
|
|
37
37
|
"build:offline": "node scripts/build-offline-release.mjs",
|
|
38
|
-
"
|
|
38
|
+
"build:public-test": "node scripts/build-public-test-package.mjs",
|
|
39
|
+
"verify:offline": "node scripts/verify-offline-release.mjs",
|
|
40
|
+
"verify:public-test": "node scripts/verify-public-test-release.mjs"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
43
|
"@types/node": "22.15.0",
|
package/dist/browser.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
export function validAuthorizationUrl(value) {
|
|
3
|
-
try {
|
|
4
|
-
const parsed = new URL(value);
|
|
5
|
-
const loopback = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]" || parsed.hostname === "::1";
|
|
6
|
-
return /^https?:$/.test(parsed.protocol)
|
|
7
|
-
&& (parsed.protocol === "https:" || loopback)
|
|
8
|
-
&& !parsed.username
|
|
9
|
-
&& !parsed.password
|
|
10
|
-
&& !/[\u0000-\u001f\u007f]/.test(value);
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
/** Launch a browser without a shell; the URL is never logged by this helper. */
|
|
17
|
-
export const openSystemBrowser = (authorizationUrl) => new Promise((resolve) => {
|
|
18
|
-
if (!validAuthorizationUrl(authorizationUrl)) {
|
|
19
|
-
resolve(false);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const command = process.platform === "darwin" ? "/usr/bin/open" : process.platform === "linux" ? "/usr/bin/xdg-open" : undefined;
|
|
23
|
-
if (!command) {
|
|
24
|
-
resolve(false);
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
let child;
|
|
28
|
-
try {
|
|
29
|
-
child = spawn(command, [authorizationUrl], { shell: false, stdio: ["ignore", "ignore", "ignore"] });
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
resolve(false);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
let settled = false;
|
|
36
|
-
const timer = setTimeout(() => {
|
|
37
|
-
child.kill();
|
|
38
|
-
finish(false);
|
|
39
|
-
}, 5000);
|
|
40
|
-
const finish = (ok) => {
|
|
41
|
-
if (settled)
|
|
42
|
-
return;
|
|
43
|
-
settled = true;
|
|
44
|
-
clearTimeout(timer);
|
|
45
|
-
resolve(ok);
|
|
46
|
-
};
|
|
47
|
-
child.once("error", () => finish(false));
|
|
48
|
-
child.once("close", (code) => finish(code === 0));
|
|
49
|
-
});
|
package/dist/pkce.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
const VERIFIER_PATTERN = /^[A-Za-z0-9._~-]{43,128}$/;
|
|
3
|
-
function base64Url(value) {
|
|
4
|
-
return value.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
5
|
-
}
|
|
6
|
-
/** Generate RFC 7636 S256 material without retaining it outside the caller. */
|
|
7
|
-
export function createPkcePair() {
|
|
8
|
-
const verifier = base64Url(randomBytes(32));
|
|
9
|
-
const challenge = base64Url(createHash("sha256").update(verifier, "ascii").digest());
|
|
10
|
-
return Object.freeze({ verifier, challenge, method: "S256" });
|
|
11
|
-
}
|
|
12
|
-
export function isValidCodeVerifier(value) {
|
|
13
|
-
return typeof value === "string" && VERIFIER_PATTERN.test(value);
|
|
14
|
-
}
|
|
15
|
-
export const generatePkce = createPkcePair;
|