@skillbuilder/minitool-cli-auth 0.0.0-beta-20261027053041051

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,302 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DeviceAuthService = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const promises_1 = require("node:timers/promises");
6
+ const minitool_cli_config_1 = require("@skillbuilder/minitool-cli-config");
7
+ const auth_state_1 = require("./auth-state");
8
+ const device_auth_api_1 = require("./device-auth-api");
9
+ const REFRESH_BUFFER_MS = 5 * 60 * 1000;
10
+ const defaultClock = {
11
+ now: Date.now,
12
+ sleep: async (milliseconds, signal) => {
13
+ await (0, promises_1.setTimeout)(milliseconds, undefined, { signal });
14
+ },
15
+ };
16
+ /** 扫码 PKCE 与续期生命周期;UI 回调只接收公开授权链接,不接收设备码和 verifier。 */
17
+ class DeviceAuthService {
18
+ store;
19
+ api;
20
+ storage;
21
+ pending;
22
+ secretAuth;
23
+ clock;
24
+ refreshPromise;
25
+ constructor(store, api, storage, pending, secretAuth, clock = defaultClock) {
26
+ this.store = store;
27
+ this.api = api;
28
+ this.storage = storage;
29
+ this.pending = pending;
30
+ this.secretAuth = secretAuth;
31
+ this.clock = clock;
32
+ }
33
+ loginWithSecret(id, key) {
34
+ if (!this.secretAuth)
35
+ throw new Error("当前宿主未启用 Secret 登录");
36
+ return this.secretAuth.loginWithSecret(id, key);
37
+ }
38
+ read() {
39
+ try {
40
+ return this.store.get();
41
+ }
42
+ catch {
43
+ throw new auth_state_1.CredentialStoreUnavailableError();
44
+ }
45
+ }
46
+ owns(credential) {
47
+ return credential.apiBaseUrl === this.api.apiBaseUrl;
48
+ }
49
+ canRefresh(credential) {
50
+ return (this.owns(credential) &&
51
+ Date.parse(credential.refreshExpiresAt) > this.clock.now() + REFRESH_BUFFER_MS);
52
+ }
53
+ identity(credential) {
54
+ if (!credential.openId)
55
+ return null;
56
+ return {
57
+ creatorId: credential.openId,
58
+ displayName: credential.creator?.displayName ?? credential.openId,
59
+ avatarUrl: credential.creator?.avatarUrl,
60
+ description: credential.creator?.description,
61
+ };
62
+ }
63
+ async getAuthState() {
64
+ let credential;
65
+ let source;
66
+ try {
67
+ credential = this.read();
68
+ source = this.storage.getMode() === "keychain" ? "os_keychain" : "local_file";
69
+ }
70
+ catch {
71
+ return { status: "unavailable", reason: "credential_store_error" };
72
+ }
73
+ if (!credential)
74
+ return { status: "anonymous", reason: "not_logged_in" };
75
+ if (credential.method === "secret")
76
+ return this.secretAuth
77
+ ? this.secretAuth.getAuthState()
78
+ : { status: "anonymous", reason: "not_logged_in" };
79
+ if (!this.owns(credential) ||
80
+ (Date.parse(credential.expiresAt) <= this.clock.now() && !this.canRefresh(credential)))
81
+ return { status: "anonymous", reason: "expired" };
82
+ return {
83
+ status: "authenticated",
84
+ method: "device",
85
+ source,
86
+ identitySnapshot: this.identity(credential),
87
+ expiresAt: credential.expiresAt,
88
+ };
89
+ }
90
+ async getAccessToken() {
91
+ const credential = this.read();
92
+ if (!credential)
93
+ throw new auth_state_1.AuthenticationRequiredError();
94
+ if (credential.method === "secret") {
95
+ if (!this.secretAuth)
96
+ throw new auth_state_1.AuthenticationRequiredError();
97
+ return this.secretAuth.getAccessToken();
98
+ }
99
+ if (!this.owns(credential))
100
+ throw new auth_state_1.AuthenticationRequiredError("登录环境与当前服务不一致,请重新扫码登录");
101
+ if (Date.parse(credential.expiresAt) > this.clock.now() + REFRESH_BUFFER_MS)
102
+ return credential.accessToken;
103
+ if (!this.canRefresh(credential)) {
104
+ if (Date.parse(credential.expiresAt) > this.clock.now())
105
+ return credential.accessToken;
106
+ throw new auth_state_1.AuthenticationRequiredError("登录态已过期,请重新扫码登录");
107
+ }
108
+ if (!this.refreshPromise) {
109
+ this.refreshPromise = this.refresh(credential).finally(() => {
110
+ this.refreshPromise = undefined;
111
+ });
112
+ }
113
+ return this.refreshPromise;
114
+ }
115
+ tokenCredential(data, previous) {
116
+ const now = this.clock.now();
117
+ const expires = data.expire_time_ms ?? data.expireTimeMs ?? now + Number(data.expires_in ?? 7200) * 1000;
118
+ const refreshExpires = data.refresh_expire_time_ms ?? data.refreshExpireTimeMs ?? now + 180 * 24 * 3600_000;
119
+ try {
120
+ if (Number(expires) <= now || Number(refreshExpires) <= now)
121
+ throw new Error();
122
+ const openId = (0, device_auth_api_1.authText)(data.open_id ?? data.openId).trim() || previous?.openId;
123
+ const info = (0, device_auth_api_1.authRecord)(data.userInfo) ? data.userInfo : {};
124
+ const credential = (0, minitool_cli_config_1.parseAuthFileConfig)({
125
+ schemaVersion: 1,
126
+ method: "device",
127
+ apiBaseUrl: this.api.apiBaseUrl,
128
+ accessToken: data.access_token ?? data.accessToken,
129
+ refreshToken: data.refresh_token ?? data.refreshToken,
130
+ issuedAt: new Date(now).toISOString(),
131
+ expiresAt: new Date(Number(expires)).toISOString(),
132
+ refreshExpiresAt: new Date(Number(refreshExpires)).toISOString(),
133
+ openId,
134
+ scope: (0, device_auth_api_1.authText)(data.scope) || previous?.scope || "base_info,skill_publish",
135
+ creator: openId
136
+ ? {
137
+ creatorId: openId,
138
+ displayName: (0, device_auth_api_1.authText)(info.nick ?? info.nickname) || previous?.creator?.displayName || openId,
139
+ avatarUrl: (0, device_auth_api_1.authText)(info.avatar) || previous?.creator?.avatarUrl,
140
+ }
141
+ : undefined,
142
+ });
143
+ if (credential.method !== "device")
144
+ throw new Error();
145
+ return credential;
146
+ }
147
+ catch {
148
+ throw new device_auth_api_1.DeviceAuthError("AUTH_INVALID_RESPONSE", "Token 响应无效,本次登录态未保存");
149
+ }
150
+ }
151
+ async refresh(original) {
152
+ let data;
153
+ try {
154
+ data = await this.api.refresh(original.refreshToken);
155
+ }
156
+ catch (error) {
157
+ const current = this.read();
158
+ if (error instanceof device_auth_api_1.DeviceAuthError &&
159
+ error.code === "AUTH_EXPIRED" &&
160
+ current?.method === "device" &&
161
+ current.refreshToken === original.refreshToken &&
162
+ current.accessToken === original.accessToken)
163
+ this.store.delete();
164
+ throw error;
165
+ }
166
+ const credential = this.tokenCredential(data, original);
167
+ if (original.openId && credential.openId !== original.openId)
168
+ throw new device_auth_api_1.DeviceAuthError("AUTH_INVALID_RESPONSE", "续期返回了不同账号,本次登录态未保存");
169
+ const current = this.read();
170
+ if (current?.method !== "device" ||
171
+ current.refreshToken !== original.refreshToken ||
172
+ current.accessToken !== original.accessToken)
173
+ throw new device_auth_api_1.DeviceAuthError("AUTH_CHANGED", "登录态已变化,请重新执行命令");
174
+ this.store.update(credential);
175
+ return credential.accessToken;
176
+ }
177
+ async loginWithQr(options) {
178
+ const state = await this.getAuthState();
179
+ if (state.status === "unavailable")
180
+ throw new auth_state_1.CredentialStoreUnavailableError();
181
+ if (state.status === "authenticated" && state.method === "device") {
182
+ try {
183
+ await this.getAccessToken();
184
+ const current = this.read();
185
+ if (current?.method === "device")
186
+ return { creator: this.identity(current) ?? undefined, expiresAt: current.expiresAt };
187
+ }
188
+ catch (error) {
189
+ if (!(error instanceof device_auth_api_1.DeviceAuthError) ||
190
+ !["AUTH_EXPIRED", "AUTH_REJECTED"].includes(error.code))
191
+ throw error;
192
+ }
193
+ }
194
+ if (options.signal?.aborted)
195
+ throw new device_auth_api_1.DeviceAuthError("AUTH_CANCELLED", "已取消扫码登录");
196
+ let pending = this.pending.readPendingAuth();
197
+ if (!pending ||
198
+ pending.apiBaseUrl !== this.api.apiBaseUrl ||
199
+ Date.parse(pending.expiresAt) <= this.clock.now()) {
200
+ const verifier = (0, node_crypto_1.randomBytes)(48).toString("base64url").replace(/-/g, ".");
201
+ const challenge = (0, node_crypto_1.createHash)("sha256").update(verifier).digest("base64url");
202
+ const device = await this.api.create(challenge, options.signal);
203
+ const common = new URL(device.authorizeCommonUrl);
204
+ const h5 = new URL(device.authorizeH5Url);
205
+ if (!["https:", "xhsopen:", "xhsdiscover:"].includes(common.protocol) ||
206
+ common.username ||
207
+ common.password ||
208
+ h5.protocol !== "https:" ||
209
+ h5.username ||
210
+ h5.password ||
211
+ !device.deviceCode ||
212
+ !Number.isSafeInteger(device.expiresInSeconds) ||
213
+ device.expiresInSeconds < 1 ||
214
+ device.expiresInSeconds > 3600 ||
215
+ !Number.isInteger(device.pollIntervalSeconds) ||
216
+ device.pollIntervalSeconds < 1 ||
217
+ device.pollIntervalSeconds > 60)
218
+ throw new device_auth_api_1.DeviceAuthError("AUTH_INVALID_RESPONSE", "扫码授权响应格式无效");
219
+ pending = {
220
+ apiBaseUrl: this.api.apiBaseUrl,
221
+ deviceCode: device.deviceCode,
222
+ codeVerifier: verifier,
223
+ authorizeCommonUrl: common.toString(),
224
+ authorizeH5Url: h5.toString(),
225
+ userCode: device.userCode,
226
+ pollIntervalSeconds: device.pollIntervalSeconds,
227
+ expiresAt: new Date(this.clock.now() + device.expiresInSeconds * 1000).toISOString(),
228
+ };
229
+ this.pending.writePendingAuth(pending);
230
+ }
231
+ const active = pending;
232
+ const checkActive = () => {
233
+ if (options.signal?.aborted ||
234
+ this.pending.readPendingAuth()?.deviceCode !== active.deviceCode)
235
+ throw new device_auth_api_1.DeviceAuthError("AUTH_CANCELLED", "已取消扫码登录");
236
+ };
237
+ try {
238
+ await options.onPrompt({
239
+ authorizeCommonUrl: active.authorizeCommonUrl,
240
+ authorizeH5Url: active.authorizeH5Url,
241
+ userCode: active.userCode,
242
+ expiresAt: active.expiresAt,
243
+ });
244
+ while (this.clock.now() < Date.parse(active.expiresAt)) {
245
+ checkActive();
246
+ await this.clock.sleep(Math.min(active.pollIntervalSeconds * 1000, Date.parse(active.expiresAt) - this.clock.now()), options.signal);
247
+ checkActive();
248
+ if (this.clock.now() >= Date.parse(active.expiresAt))
249
+ break;
250
+ const polled = await this.api.poll(active.deviceCode, options.signal);
251
+ if (polled.status === "approved") {
252
+ if (!polled.authorizationCode)
253
+ throw new device_auth_api_1.DeviceAuthError("AUTH_INVALID_RESPONSE", "授权响应缺少 authorizationCode");
254
+ const data = await this.api.exchange(polled.authorizationCode, active.codeVerifier, options.signal);
255
+ checkActive();
256
+ const credential = this.tokenCredential(data);
257
+ this.store.update(credential);
258
+ this.pending.clearPendingAuth();
259
+ return {
260
+ creator: this.identity(credential) ?? undefined,
261
+ expiresAt: credential.expiresAt,
262
+ };
263
+ }
264
+ if (["denied", "cancelled"].includes(polled.status))
265
+ throw new device_auth_api_1.DeviceAuthError("AUTH_DENIED", "用户已拒绝或取消授权");
266
+ if (polled.status === "expired")
267
+ break;
268
+ }
269
+ throw new device_auth_api_1.DeviceAuthError("AUTH_TIMEOUT", "扫码授权已超时,请重新登录");
270
+ }
271
+ catch (error) {
272
+ const cancelled = options.signal?.aborted;
273
+ // 网络故障保留有时效的等待状态供下次恢复;终止、拒绝和异常响应清理。
274
+ if ((cancelled || !(error instanceof device_auth_api_1.DeviceAuthError) || error.code !== "AUTH_UNAVAILABLE") &&
275
+ this.pending.readPendingAuth()?.deviceCode === active.deviceCode)
276
+ this.pending.clearPendingAuth();
277
+ if (cancelled)
278
+ throw new device_auth_api_1.DeviceAuthError("AUTH_CANCELLED", "已取消扫码登录");
279
+ throw error;
280
+ }
281
+ }
282
+ async cancelLogin() {
283
+ this.pending.clearPendingAuth();
284
+ }
285
+ async whoAmI() {
286
+ await this.getAccessToken();
287
+ const credential = this.read();
288
+ if (credential?.method === "secret" && this.secretAuth)
289
+ return this.secretAuth.whoAmI();
290
+ if (!credential || credential.method !== "device")
291
+ throw new auth_state_1.AuthenticationRequiredError();
292
+ const identity = this.identity(credential);
293
+ if (!identity)
294
+ throw new auth_state_1.CreatorIdentityUnavailableError();
295
+ return identity;
296
+ }
297
+ async logout() {
298
+ this.pending.clearPendingAuth();
299
+ this.store.delete();
300
+ }
301
+ }
302
+ exports.DeviceAuthService = DeviceAuthService;
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const node_crypto_1 = require("node:crypto");
8
+ const node_test_1 = __importDefault(require("node:test"));
9
+ const device_auth_1 = require("./device-auth");
10
+ const device_auth_api_1 = require("./device-auth-api");
11
+ function fixture() {
12
+ let now = Date.parse("2026-10-26T00:00:00Z");
13
+ let credential = null;
14
+ let pending = null;
15
+ let challenge = "";
16
+ let refreshCalls = 0;
17
+ let createCalls = 0;
18
+ const token = () => ({
19
+ access_token: "private-access",
20
+ refresh_token: "private-refresh",
21
+ open_id: "creator",
22
+ expire_time_ms: now + 7200_000,
23
+ refresh_expire_time_ms: now + 86400_000,
24
+ });
25
+ const api = {
26
+ apiBaseUrl: "https://auth.example",
27
+ async create(value) {
28
+ createCalls++;
29
+ challenge = value;
30
+ return {
31
+ deviceCode: "private-device",
32
+ authorizeCommonUrl: "xhsopen://account/oauth",
33
+ authorizeH5Url: "https://auth.example/web",
34
+ userCode: "CODE",
35
+ pollIntervalSeconds: 1,
36
+ expiresInSeconds: 60,
37
+ };
38
+ },
39
+ async poll() {
40
+ return { status: "approved", authorizationCode: "private-code" };
41
+ },
42
+ async exchange(_code, verifier) {
43
+ strict_1.default.equal((0, node_crypto_1.createHash)("sha256").update(verifier).digest("base64url"), challenge);
44
+ return token();
45
+ },
46
+ async refresh() {
47
+ refreshCalls++;
48
+ return { ...token(), access_token: "renewed-access", refresh_token: "renewed-refresh" };
49
+ },
50
+ };
51
+ const store = {
52
+ get: () => credential,
53
+ update: (value) => {
54
+ credential = value;
55
+ },
56
+ delete: () => {
57
+ credential = null;
58
+ },
59
+ };
60
+ const waiting = {
61
+ readPendingAuth: () => pending,
62
+ writePendingAuth: (value) => {
63
+ pending = value;
64
+ },
65
+ clearPendingAuth: () => {
66
+ pending = null;
67
+ },
68
+ };
69
+ const clock = {
70
+ now: () => now,
71
+ sleep: async (ms) => {
72
+ now += ms;
73
+ },
74
+ };
75
+ const service = new device_auth_1.DeviceAuthService(store, api, { getMode: () => "file" }, waiting, undefined, clock);
76
+ return {
77
+ service,
78
+ api,
79
+ store,
80
+ waiting,
81
+ clock,
82
+ token,
83
+ advance(ms) {
84
+ now += ms;
85
+ },
86
+ get createCalls() {
87
+ return createCalls;
88
+ },
89
+ get refreshCalls() {
90
+ return refreshCalls;
91
+ },
92
+ };
93
+ }
94
+ (0, node_test_1.default)("扫码使用 S256 PKCE,持久化 Token,输出身份不泄漏凭据", async () => {
95
+ const f = fixture();
96
+ let prompt;
97
+ const result = await f.service.loginWithQr({
98
+ onPrompt: (value) => {
99
+ prompt = value;
100
+ },
101
+ });
102
+ strict_1.default.equal(result.creator?.creatorId, "creator");
103
+ strict_1.default.equal(f.store.get()?.method, "device");
104
+ strict_1.default.equal(f.waiting.readPendingAuth(), null);
105
+ strict_1.default.equal(await f.service.getAccessToken(), "private-access");
106
+ const publicOutput = JSON.stringify([
107
+ prompt,
108
+ result,
109
+ await f.service.getAuthState(),
110
+ await f.service.whoAmI(),
111
+ ]);
112
+ for (const secret of [
113
+ "private-access",
114
+ "private-refresh",
115
+ "private-device",
116
+ "private-code",
117
+ "codeVerifier",
118
+ ])
119
+ strict_1.default.equal(publicOutput.includes(secret), false);
120
+ });
121
+ (0, node_test_1.default)("过期前并发请求只续期一次;登录环境不一致时不发 Token", async () => {
122
+ const f = fixture();
123
+ await f.service.loginWithQr({ onPrompt() { } });
124
+ f.advance(7100_000);
125
+ strict_1.default.deepEqual(await Promise.all([f.service.getAccessToken(), f.service.getAccessToken()]), [
126
+ "renewed-access",
127
+ "renewed-access",
128
+ ]);
129
+ strict_1.default.equal(f.refreshCalls, 1);
130
+ const foreign = new device_auth_1.DeviceAuthService(f.store, { ...f.api, apiBaseUrl: "https://other.example" }, { getMode: () => "file" }, f.waiting, undefined, f.clock);
131
+ await strict_1.default.rejects(foreign.getAccessToken(), { code: "AUTHENTICATION_REQUIRED" });
132
+ });
133
+ (0, node_test_1.default)("拒绝、超时、外部取消均清理等待状态且不覆盖已有凭据", async () => {
134
+ for (const status of ["denied", "expired"]) {
135
+ const f = fixture();
136
+ f.api.poll = async () => ({ status });
137
+ await strict_1.default.rejects(f.service.loginWithQr({ onPrompt() { } }), {
138
+ code: status === "denied" ? "AUTH_DENIED" : "AUTH_TIMEOUT",
139
+ });
140
+ strict_1.default.equal(f.waiting.readPendingAuth(), null);
141
+ strict_1.default.equal(f.store.get(), null);
142
+ }
143
+ const f = fixture();
144
+ await strict_1.default.rejects(f.service.loginWithQr({
145
+ onPrompt() {
146
+ f.waiting.clearPendingAuth();
147
+ },
148
+ }), { code: "AUTH_CANCELLED" });
149
+ strict_1.default.equal(f.store.get(), null);
150
+ });
151
+ (0, node_test_1.default)("网络中断恢复同一设备码;abort 清理;非法 Token 不落盘", async () => {
152
+ const f = fixture();
153
+ f.api.poll = async () => {
154
+ throw new device_auth_api_1.DeviceAuthError("AUTH_UNAVAILABLE", "暂不可用");
155
+ };
156
+ await strict_1.default.rejects(f.service.loginWithQr({ onPrompt() { } }), { code: "AUTH_UNAVAILABLE" });
157
+ strict_1.default.ok(f.waiting.readPendingAuth());
158
+ f.api.poll = async () => ({
159
+ status: "approved",
160
+ authorizationCode: "private-code",
161
+ });
162
+ await f.service.loginWithQr({ onPrompt() { } });
163
+ strict_1.default.equal(f.createCalls, 1);
164
+ await f.service.logout();
165
+ const controller = new AbortController();
166
+ await strict_1.default.rejects(f.service.loginWithQr({
167
+ signal: controller.signal,
168
+ onPrompt() {
169
+ controller.abort();
170
+ },
171
+ }), { code: "AUTH_CANCELLED" });
172
+ strict_1.default.equal(f.waiting.readPendingAuth(), null);
173
+ f.api.exchange = async () => ({ access_token: "private", expires_in: -1 });
174
+ await strict_1.default.rejects(f.service.loginWithQr({ onPrompt() { } }), { code: "AUTH_INVALID_RESPONSE" });
175
+ strict_1.default.equal(f.store.get(), null);
176
+ });
177
+ (0, node_test_1.default)("续期不会复活并发登出的账号;失效续期可重新扫码", async () => {
178
+ const f = fixture();
179
+ await f.service.loginWithQr({ onPrompt() { } });
180
+ f.advance(7100_000);
181
+ f.api.refresh = async () => {
182
+ await f.service.logout();
183
+ return f.token();
184
+ };
185
+ await strict_1.default.rejects(f.service.getAccessToken(), { code: "AUTH_CHANGED" });
186
+ strict_1.default.equal(f.store.get(), null);
187
+ await f.service.loginWithQr({ onPrompt() { } });
188
+ f.advance(7100_000);
189
+ f.api.refresh = async () => {
190
+ throw new device_auth_api_1.DeviceAuthError("AUTH_EXPIRED", "expired");
191
+ };
192
+ await f.service.loginWithQr({ onPrompt() { } });
193
+ strict_1.default.equal(f.createCalls, 3);
194
+ });
195
+ (0, node_test_1.default)("HTTP 扫码协议发送正确字段,拒绝业务失败且不回显私密响应", async () => {
196
+ const seen = [];
197
+ const api = new device_auth_api_1.HttpDeviceAuthApi("https://auth.example", async (url, init) => {
198
+ seen.push({ url: new URL(String(url)), init });
199
+ return new Response(JSON.stringify({ code: 0, data: { auth_status: 2, authorization_code: "code" } }));
200
+ });
201
+ await api.create("challenge");
202
+ strict_1.default.deepEqual(JSON.parse(String(seen[0].init?.body)), {
203
+ code_challenge: "challenge",
204
+ code_challenge_method: "S256",
205
+ scopes: ["base_info", "skill_publish"],
206
+ });
207
+ strict_1.default.deepEqual(await api.poll("device&private"), {
208
+ status: "approved",
209
+ authorizationCode: "code",
210
+ });
211
+ strict_1.default.equal(seen[1].url.searchParams.get("device_code"), "device&private");
212
+ await api.exchange("code", "verifier");
213
+ strict_1.default.deepEqual(JSON.parse(String(seen[2].init?.body)), {
214
+ authorization_code: "code",
215
+ code_verifier: "verifier",
216
+ });
217
+ await api.refresh("refresh");
218
+ strict_1.default.deepEqual(JSON.parse(String(seen[3].init?.body)), { refresh_token: "refresh" });
219
+ strict_1.default.equal(seen[3].init?.redirect, "error");
220
+ const denied = new device_auth_api_1.HttpDeviceAuthApi("https://auth.example", async () => new Response(JSON.stringify({ code: 9, msg: "private-token", data: {} })));
221
+ await strict_1.default.rejects(denied.refresh("private-token"), (error) => error instanceof device_auth_api_1.DeviceAuthError &&
222
+ error.code === "AUTH_REJECTED" &&
223
+ !error.message.includes("private-token"));
224
+ });
225
+ (0, node_test_1.default)("无身份字段的 Token 可登录;续期不回传身份时保留原有身份", async () => {
226
+ const f = fixture();
227
+ f.api.exchange = async () => ({ ...f.token(), open_id: undefined });
228
+ const login = await f.service.loginWithQr({ onPrompt() { } });
229
+ strict_1.default.equal(login.creator, undefined);
230
+ strict_1.default.equal((await f.service.getAuthState()).status, "authenticated");
231
+ await strict_1.default.rejects(f.service.whoAmI(), { code: "CREATOR_IDENTITY_UNAVAILABLE" });
232
+ await f.service.logout();
233
+ f.api.exchange = async () => f.token();
234
+ await f.service.loginWithQr({ onPrompt() { } });
235
+ f.advance(7100_000);
236
+ f.api.refresh = async () => ({ ...f.token(), open_id: undefined });
237
+ strict_1.default.equal((await f.service.whoAmI()).creatorId, "creator");
238
+ });
package/lib/index.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEVICE_AUTH_PATHS = exports.DeviceAuthError = exports.HttpDeviceAuthApi = exports.DeviceAuthService = exports.ManagedCredentialStore = exports.SecretAuthService = exports.HttpSecretAuthApi = exports.requireAuthenticatedAuthState = exports.CreatorIdentityUnavailableError = exports.CredentialStoreUnavailableError = exports.AuthenticationRequiredError = void 0;
4
+ var auth_state_1 = require("./auth-state");
5
+ Object.defineProperty(exports, "AuthenticationRequiredError", { enumerable: true, get: function () { return auth_state_1.AuthenticationRequiredError; } });
6
+ Object.defineProperty(exports, "CredentialStoreUnavailableError", { enumerable: true, get: function () { return auth_state_1.CredentialStoreUnavailableError; } });
7
+ Object.defineProperty(exports, "CreatorIdentityUnavailableError", { enumerable: true, get: function () { return auth_state_1.CreatorIdentityUnavailableError; } });
8
+ Object.defineProperty(exports, "requireAuthenticatedAuthState", { enumerable: true, get: function () { return auth_state_1.requireAuthenticatedAuthState; } });
9
+ var secret_auth_1 = require("./secret-auth");
10
+ Object.defineProperty(exports, "HttpSecretAuthApi", { enumerable: true, get: function () { return secret_auth_1.HttpSecretAuthApi; } });
11
+ Object.defineProperty(exports, "SecretAuthService", { enumerable: true, get: function () { return secret_auth_1.SecretAuthService; } });
12
+ var managed_credential_store_1 = require("./managed-credential-store");
13
+ Object.defineProperty(exports, "ManagedCredentialStore", { enumerable: true, get: function () { return managed_credential_store_1.ManagedCredentialStore; } });
14
+ var device_auth_1 = require("./device-auth");
15
+ Object.defineProperty(exports, "DeviceAuthService", { enumerable: true, get: function () { return device_auth_1.DeviceAuthService; } });
16
+ var device_auth_api_1 = require("./device-auth-api");
17
+ Object.defineProperty(exports, "HttpDeviceAuthApi", { enumerable: true, get: function () { return device_auth_api_1.HttpDeviceAuthApi; } });
18
+ Object.defineProperty(exports, "DeviceAuthError", { enumerable: true, get: function () { return device_auth_api_1.DeviceAuthError; } });
19
+ Object.defineProperty(exports, "DEVICE_AUTH_PATHS", { enumerable: true, get: function () { return device_auth_api_1.DEVICE_AUTH_PATHS; } });
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KeyringCredentialStore = exports.KeyringAccessError = exports.KeyringUnavailableError = void 0;
4
+ exports.getSystemCredentialStoreName = getSystemCredentialStoreName;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const minitool_cli_config_1 = require("@skillbuilder/minitool-cli-config");
7
+ const KEYRING_SERVICE = "com.xiaohongshu.redminitool";
8
+ class KeyringUnavailableError extends Error {
9
+ code = "KEYRING_UNAVAILABLE";
10
+ constructor() {
11
+ super("系统凭据库不可用;凭据未降级保存到 auth.json");
12
+ this.name = "KeyringUnavailableError";
13
+ }
14
+ }
15
+ exports.KeyringUnavailableError = KeyringUnavailableError;
16
+ class KeyringAccessError extends Error {
17
+ code = "KEYRING_ACCESS_FAILED";
18
+ constructor(operation) {
19
+ super(`系统凭据库${operation}失败;未执行明文文件降级`);
20
+ this.name = "KeyringAccessError";
21
+ }
22
+ }
23
+ exports.KeyringAccessError = KeyringAccessError;
24
+ function getSystemCredentialStoreName(platform = process.platform) {
25
+ if (platform === "darwin")
26
+ return "macOS Keychain";
27
+ if (platform === "win32")
28
+ return "Windows Credential Manager";
29
+ if (platform === "linux")
30
+ return "Linux Secret Service / Keyutils";
31
+ return "OS Keyring";
32
+ }
33
+ function createNativeEntry(service, account) {
34
+ try {
35
+ const { Entry } = require("@napi-rs/keyring");
36
+ return new Entry(service, account);
37
+ }
38
+ catch {
39
+ throw new KeyringUnavailableError();
40
+ }
41
+ }
42
+ function execute(operation, task) {
43
+ try {
44
+ return task();
45
+ }
46
+ catch (error) {
47
+ if (error instanceof KeyringUnavailableError)
48
+ throw error;
49
+ throw new KeyringAccessError(operation);
50
+ }
51
+ }
52
+ /** 每个配置目录使用独立系统凭据条目。 */
53
+ class KeyringCredentialStore {
54
+ entryFactory;
55
+ account;
56
+ constructor(authPath, entryFactory = createNativeEntry) {
57
+ this.entryFactory = entryFactory;
58
+ this.account = `cli:${(0, node_crypto_1.createHash)("sha256").update(authPath).digest("hex").slice(0, 24)}`;
59
+ }
60
+ probe() {
61
+ execute("读取", () => this.entryFactory(KEYRING_SERVICE, this.account).getPassword());
62
+ }
63
+ get() {
64
+ return execute("读取", () => {
65
+ const password = this.entryFactory(KEYRING_SERVICE, this.account).getPassword();
66
+ return password === null ? null : (0, minitool_cli_config_1.parseAuthFileConfig)(JSON.parse(password));
67
+ });
68
+ }
69
+ update(config) {
70
+ const password = JSON.stringify((0, minitool_cli_config_1.parseAuthFileConfig)(config));
71
+ execute("写入", () => this.entryFactory(KEYRING_SERVICE, this.account).setPassword(password));
72
+ }
73
+ delete() {
74
+ execute("删除", () => this.entryFactory(KEYRING_SERVICE, this.account).deleteCredential());
75
+ }
76
+ }
77
+ exports.KeyringCredentialStore = KeyringCredentialStore;