@skillbuilder/minitool-cli-auth 0.0.0-beta-20261027053041051 → 0.0.0-stage
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 +2 -35
- package/package.json +4 -26
- package/lib/auth-state.js +0 -36
- package/lib/credential-store.js +0 -20
- package/lib/credential-store.test.js +0 -204
- package/lib/device-auth-api.js +0 -122
- package/lib/device-auth.js +0 -302
- package/lib/device-auth.test.js +0 -238
- package/lib/index.js +0 -19
- package/lib/keyring-credential-store.js +0 -77
- package/lib/managed-credential-store.js +0 -135
- package/lib/secret-auth.js +0 -228
- package/lib/secret-auth.test.js +0 -248
- package/types/auth-state.d.ts +0 -63
- package/types/credential-store.d.ts +0 -32
- package/types/credential-store.test.d.ts +0 -1
- package/types/device-auth-api.d.ts +0 -40
- package/types/device-auth.d.ts +0 -34
- package/types/device-auth.test.d.ts +0 -1
- package/types/index.d.ts +0 -11
- package/types/keyring-credential-store.d.ts +0 -30
- package/types/managed-credential-store.d.ts +0 -21
- package/types/secret-auth.d.ts +0 -52
- package/types/secret-auth.test.d.ts +0 -1
package/README.md
CHANGED
|
@@ -1,36 +1,3 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Temporary Holding Version
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
申请设备码、S256 PKCE、授权轮询、Token 交换和刷新。`HttpDeviceAuthApi` 由宿主注入网关与请求函数,协议路径集中定义;
|
|
5
|
-
公开提示仅含扫码链接、备用链接、用户码和到期时间,设备码/verifier 不进入 UI DTO。
|
|
6
|
-
`ConfigFiles` 安全保存有时效的等待状态,支持重启恢复、信号中断与跨进程取消;成功或终止后清理。
|
|
7
|
-
Token 按所属 API origin 校验,临近过期时进行进程内合并续期;并发 logout 或账号切换后,旧续期结果不能写回。
|
|
8
|
-
确定失效的刷新凭据清理并要求重新扫码,临时网络失败保留凭据。`getAuthState` 不发网络请求,access token 过期但 refresh 仍有效时
|
|
9
|
-
表示可续期的 authenticated 状态,业务实际使用前由 `getAccessToken` 续期。prod/beta 由宿主使用独立 `ConfigFiles` 目录隔离。
|
|
10
|
-
|
|
11
|
-
原有 `SecretAuthService` 使用 Secret ID / Secret Key、随机 nonce 和分钟
|
|
12
|
-
时间生成 MD5 登录证明,请求平台 `cli_auth` 接口;Secret Key 不出网、不落盘。真实接口成功后返回的
|
|
13
|
-
`data.cli_token` 当前就是服务端确认后的该证明,按 7200 秒短期可重放凭据管理;响应不含 `secret_id`,
|
|
14
|
-
持久化时使用本次已验证的请求值补齐。若未来响应包含 `secret_id`,则必须与请求身份一致。
|
|
15
|
-
登录会校验 token、服务端 `create_at` 和 TTL 可安全表示后再执行持久化。
|
|
16
|
-
真实认证接口由宿主传入完整 HTTPS URL;本包不内置旧云开发接口路径。
|
|
17
|
-
`ManagedCredentialStore` 支持文件与系统 Keyring,默认文件,显式切换时写入并回读目标、提交存储模式,
|
|
18
|
-
最后清理旧位置;任一步失败都会保持或恢复原模式,不会从残留明文文件静默恢复 Keyring 凭据。
|
|
19
|
-
Keyring 通过可选且固定版本的 `@napi-rs/keyring@2.0.0` 原生模块按配置路径隔离条目,在 macOS 映射
|
|
20
|
-
Keychain、Windows 映射 Credential Manager、Linux 映射 Secret Service / Keyutils。原生模块仅在使用或
|
|
21
|
-
探测 Keyring 时延迟加载;读取、写入和删除错误会转换为不含凭据的稳定错误,正常读写失败时不会静默
|
|
22
|
-
写回 `auth.json`。`inspect()` 只返回后端、可用性和失败类别,不返回条目名或凭据。宿主显式执行
|
|
23
|
-
`auth storage --check` 时只做只读探测;无论原生包/平台二进制不可加载,还是 Keyring 被锁定或访问失败,
|
|
24
|
-
都只报错并提示开发者自行执行 `auth storage --set file`,不会修改配置或凭据。显式切换时若原 Keyring
|
|
25
|
-
无法读取,则仅将配置切为文件模式,不删除无法访问的旧条目,也不会伪造或恢复登录态,开发者需重新登录。
|
|
26
|
-
Keyring → 文件切换若旧条目删除失败,会回滚新文件并保持原模式;配置内容损坏时 logout 仍尝试同时
|
|
27
|
-
清理文件和 Keyring,并将配置重置为无凭据的文件模式;路径安全错误及无法保证清理完成的 Keyring
|
|
28
|
-
错误仍会显式失败。
|
|
29
|
-
|
|
30
|
-
本包统一定义 `AuthState`、`CreatorIdentity`、认证方式(device / secret)与凭据来源;`getAuthState()` 只返回脱敏的
|
|
31
|
-
`authenticated / anonymous / unavailable` 三态,并区分未登录、过期与凭据存储故障。对业务层只暴露
|
|
32
|
-
`AccessTokenProvider#getAccessToken()` 窄接口。`AuthService` 负责登录、登出、脱敏登录态与当前创作者身份,
|
|
33
|
-
由 CLI bootstrap 组装。业务 core 不读取 `auth.json`,也不感知 Keyring、Secret 或登录接口。
|
|
34
|
-
|
|
35
|
-
本包不包含 CLI 输出、创作者小工具业务规则或资源权限判定。过期的当前 Secret 会话会删除并要求重新登录;
|
|
36
|
-
设备码 OAuth 则使用参考分支已有的 refresh 协议续期。
|
|
3
|
+
This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
|
package/package.json
CHANGED
|
@@ -1,28 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillbuilder/minitool-cli-auth",
|
|
3
|
-
"version": "0.0.0-
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"exports": {
|
|
8
|
-
".": {
|
|
9
|
-
"types": "./types/index.d.ts",
|
|
10
|
-
"require": "./lib/index.js",
|
|
11
|
-
"default": "./lib/index.js"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"lib",
|
|
16
|
-
"types"
|
|
17
|
-
],
|
|
18
|
-
"engines": {
|
|
19
|
-
"node": ">=20.0.0"
|
|
20
|
-
},
|
|
21
|
-
"dependencies": {
|
|
22
|
-
"@skillbuilder/minitool-cli-config": "0.0.0-beta-20261027053041051"
|
|
23
|
-
},
|
|
24
|
-
"optionalDependencies": {
|
|
25
|
-
"@napi-rs/keyring": "2.0.0"
|
|
26
|
-
},
|
|
27
|
-
"license": "ISC"
|
|
28
|
-
}
|
|
3
|
+
"version": "0.0.0-stage",
|
|
4
|
+
"stub": true,
|
|
5
|
+
"description": "Temporary package placeholder for staged publishing"
|
|
6
|
+
}
|
package/lib/auth-state.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CreatorIdentityUnavailableError = exports.AuthenticationRequiredError = exports.CredentialStoreUnavailableError = void 0;
|
|
4
|
-
exports.requireAuthenticatedAuthState = requireAuthenticatedAuthState;
|
|
5
|
-
class CredentialStoreUnavailableError extends Error {
|
|
6
|
-
code = "CREDENTIAL_STORE_UNAVAILABLE";
|
|
7
|
-
constructor(message = "无法读取本地凭据存储,请检查系统凭据库或配置文件。") {
|
|
8
|
-
super(message);
|
|
9
|
-
this.name = "CredentialStoreUnavailableError";
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
exports.CredentialStoreUnavailableError = CredentialStoreUnavailableError;
|
|
13
|
-
class AuthenticationRequiredError extends Error {
|
|
14
|
-
code = "AUTHENTICATION_REQUIRED";
|
|
15
|
-
constructor(message = "未登录,请先完成小红书小工具认证。") {
|
|
16
|
-
super(message);
|
|
17
|
-
this.name = "AuthenticationRequiredError";
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
exports.AuthenticationRequiredError = AuthenticationRequiredError;
|
|
21
|
-
class CreatorIdentityUnavailableError extends Error {
|
|
22
|
-
code = "CREATOR_IDENTITY_UNAVAILABLE";
|
|
23
|
-
constructor(message = "登录态有效,但未能获取创作者身份。") {
|
|
24
|
-
super(message);
|
|
25
|
-
this.name = "CreatorIdentityUnavailableError";
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
exports.CreatorIdentityUnavailableError = CreatorIdentityUnavailableError;
|
|
29
|
-
function requireAuthenticatedAuthState(state) {
|
|
30
|
-
if (state.status === "unavailable")
|
|
31
|
-
throw new CredentialStoreUnavailableError();
|
|
32
|
-
if (state.status === "anonymous") {
|
|
33
|
-
throw new AuthenticationRequiredError(state.reason === "expired" ? "登录态已过期,请重新登录。" : undefined);
|
|
34
|
-
}
|
|
35
|
-
return state;
|
|
36
|
-
}
|
package/lib/credential-store.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FileCredentialStore = void 0;
|
|
4
|
-
class FileCredentialStore {
|
|
5
|
-
files;
|
|
6
|
-
constructor(files) {
|
|
7
|
-
this.files = files;
|
|
8
|
-
}
|
|
9
|
-
get() {
|
|
10
|
-
return this.files.readAuth();
|
|
11
|
-
}
|
|
12
|
-
update(config) {
|
|
13
|
-
this.files.writeGlobal(this.files.readGlobal());
|
|
14
|
-
this.files.writeAuth(config);
|
|
15
|
-
}
|
|
16
|
-
delete() {
|
|
17
|
-
this.files.clearAuth();
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
exports.FileCredentialStore = FileCredentialStore;
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
40
|
-
const fs = __importStar(require("node:fs"));
|
|
41
|
-
const os = __importStar(require("node:os"));
|
|
42
|
-
const path = __importStar(require("node:path"));
|
|
43
|
-
const node_test_1 = __importDefault(require("node:test"));
|
|
44
|
-
const minitool_cli_config_1 = require("@skillbuilder/minitool-cli-config");
|
|
45
|
-
const credential_store_1 = require("./credential-store");
|
|
46
|
-
const keyring_credential_store_1 = require("./keyring-credential-store");
|
|
47
|
-
const managed_credential_store_1 = require("./managed-credential-store");
|
|
48
|
-
const credential = {
|
|
49
|
-
schemaVersion: 1,
|
|
50
|
-
method: "secret",
|
|
51
|
-
secretId: "creator-id",
|
|
52
|
-
sessionToken: "server-session-token",
|
|
53
|
-
issuedAt: "2026-09-01T00:00:00.000Z",
|
|
54
|
-
expiresAt: "2030-09-01T00:00:00.000Z",
|
|
55
|
-
};
|
|
56
|
-
function fixture(t, behavior = {}) {
|
|
57
|
-
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "redminitool-keyring-test-"));
|
|
58
|
-
t.after(() => fs.rmSync(homeDir, { recursive: true, force: true }));
|
|
59
|
-
const state = { password: null };
|
|
60
|
-
const entryFactory = () => {
|
|
61
|
-
if (behavior.packageUnavailable)
|
|
62
|
-
throw new keyring_credential_store_1.KeyringUnavailableError();
|
|
63
|
-
if (behavior.unavailable)
|
|
64
|
-
throw new Error("keyring unavailable");
|
|
65
|
-
return {
|
|
66
|
-
getPassword: () => state.password,
|
|
67
|
-
setPassword: (value) => {
|
|
68
|
-
state.password = behavior.transformWrite?.(value) ?? value;
|
|
69
|
-
},
|
|
70
|
-
deleteCredential: () => {
|
|
71
|
-
if (behavior.failDelete)
|
|
72
|
-
throw new Error("keyring delete failed");
|
|
73
|
-
const existed = state.password !== null;
|
|
74
|
-
state.password = null;
|
|
75
|
-
return existed;
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
};
|
|
79
|
-
const files = new minitool_cli_config_1.ConfigFiles(homeDir);
|
|
80
|
-
const file = new credential_store_1.FileCredentialStore(files);
|
|
81
|
-
const keyring = new keyring_credential_store_1.KeyringCredentialStore(files.paths.auth, entryFactory);
|
|
82
|
-
const store = new managed_credential_store_1.ManagedCredentialStore(files, file, keyring);
|
|
83
|
-
return { files, file, keyring, state, store };
|
|
84
|
-
}
|
|
85
|
-
(0, node_test_1.default)("文件与 Keyring 双向切换只保留目标副本", (t) => {
|
|
86
|
-
const { files, state, store } = fixture(t);
|
|
87
|
-
store.update(credential);
|
|
88
|
-
store.use("keychain");
|
|
89
|
-
strict_1.default.equal(store.getMode(), "keychain");
|
|
90
|
-
strict_1.default.notEqual(state.password, null);
|
|
91
|
-
strict_1.default.equal(fs.existsSync(files.paths.auth), false);
|
|
92
|
-
store.use("file");
|
|
93
|
-
strict_1.default.equal(store.getMode(), "file");
|
|
94
|
-
strict_1.default.equal(state.password, null);
|
|
95
|
-
strict_1.default.deepEqual(store.get(), credential);
|
|
96
|
-
});
|
|
97
|
-
(0, node_test_1.default)("Keyring 不可用时不切换或降级凭据", (t) => {
|
|
98
|
-
const { store } = fixture(t, { unavailable: true });
|
|
99
|
-
store.update(credential);
|
|
100
|
-
strict_1.default.throws(() => store.use("keychain"), /系统凭据库写入失败/);
|
|
101
|
-
strict_1.default.equal(store.getMode(), "file");
|
|
102
|
-
strict_1.default.deepEqual(store.get(), credential);
|
|
103
|
-
});
|
|
104
|
-
(0, node_test_1.default)("显式检查只报告故障,人工切换文件模式时保留不可读的 Keyring 条目", (t) => {
|
|
105
|
-
const behavior = {};
|
|
106
|
-
const { files, state, store } = fixture(t, behavior);
|
|
107
|
-
store.use("keychain");
|
|
108
|
-
store.update(credential);
|
|
109
|
-
behavior.packageUnavailable = true;
|
|
110
|
-
strict_1.default.deepEqual(store.inspect(true), {
|
|
111
|
-
configuredMode: "keychain",
|
|
112
|
-
provider: (0, keyring_credential_store_1.getSystemCredentialStoreName)(),
|
|
113
|
-
protection: "os_protected_store",
|
|
114
|
-
keyringAvailable: false,
|
|
115
|
-
keyringIssue: "package_unavailable",
|
|
116
|
-
});
|
|
117
|
-
strict_1.default.equal(store.getMode(), "keychain");
|
|
118
|
-
strict_1.default.deepEqual(store.use("file"), {
|
|
119
|
-
changed: true,
|
|
120
|
-
migrated: false,
|
|
121
|
-
sourceRetained: true,
|
|
122
|
-
});
|
|
123
|
-
strict_1.default.equal(store.getMode(), "file");
|
|
124
|
-
strict_1.default.equal(files.readAuth(), null);
|
|
125
|
-
strict_1.default.notEqual(state.password, null);
|
|
126
|
-
const accessBehavior = {};
|
|
127
|
-
const access = fixture(t, accessBehavior);
|
|
128
|
-
access.store.use("keychain");
|
|
129
|
-
access.store.update(credential);
|
|
130
|
-
accessBehavior.unavailable = true;
|
|
131
|
-
strict_1.default.equal(access.store.inspect(true).keyringIssue, "access_error");
|
|
132
|
-
strict_1.default.deepEqual(access.store.use("file"), {
|
|
133
|
-
changed: true,
|
|
134
|
-
migrated: false,
|
|
135
|
-
sourceRetained: true,
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
(0, node_test_1.default)("Keyring 模式清理残留文件且不从中恢复凭据", (t) => {
|
|
139
|
-
const { files, state, store } = fixture(t);
|
|
140
|
-
store.update(credential);
|
|
141
|
-
store.use("keychain");
|
|
142
|
-
files.writeAuth(credential);
|
|
143
|
-
strict_1.default.deepEqual(store.get(), credential);
|
|
144
|
-
strict_1.default.equal(fs.existsSync(files.paths.auth), false);
|
|
145
|
-
state.password = null;
|
|
146
|
-
files.writeAuth(credential);
|
|
147
|
-
strict_1.default.equal(store.get(), null);
|
|
148
|
-
strict_1.default.equal(fs.existsSync(files.paths.auth), false);
|
|
149
|
-
});
|
|
150
|
-
(0, node_test_1.default)("存储状态探测不暴露凭据", (t) => {
|
|
151
|
-
const { store } = fixture(t);
|
|
152
|
-
store.update(credential);
|
|
153
|
-
store.use("keychain");
|
|
154
|
-
const status = store.inspect();
|
|
155
|
-
strict_1.default.equal(status.configuredMode, "keychain");
|
|
156
|
-
strict_1.default.equal(status.protection, "os_protected_store");
|
|
157
|
-
strict_1.default.equal(status.keyringAvailable, true);
|
|
158
|
-
strict_1.default.equal(JSON.stringify(status).includes(credential.sessionToken), false);
|
|
159
|
-
});
|
|
160
|
-
(0, node_test_1.default)("损坏的 Keyring 内容只返回稳定错误", () => {
|
|
161
|
-
const store = new keyring_credential_store_1.KeyringCredentialStore("/tmp/redminitool-test-auth.json", () => ({
|
|
162
|
-
getPassword: () => '{"sessionToken":"sensitive-fragment"',
|
|
163
|
-
setPassword: () => undefined,
|
|
164
|
-
deleteCredential: () => true,
|
|
165
|
-
}));
|
|
166
|
-
strict_1.default.throws(() => store.get(), (error) => {
|
|
167
|
-
strict_1.default.equal(String(error), "KeyringAccessError: 系统凭据库读取失败;未执行明文文件降级");
|
|
168
|
-
strict_1.default.equal(String(error).includes("sensitive-fragment"), false);
|
|
169
|
-
return true;
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
(0, node_test_1.default)("回读校验比较完整凭据并清理无效目标", (t) => {
|
|
173
|
-
const { state, store } = fixture(t, {
|
|
174
|
-
transformWrite(value) {
|
|
175
|
-
return JSON.stringify({ ...JSON.parse(value), secretId: "tampered-id" });
|
|
176
|
-
},
|
|
177
|
-
});
|
|
178
|
-
store.update(credential);
|
|
179
|
-
strict_1.default.throws(() => store.use("keychain"), /凭据存储切换校验失败/);
|
|
180
|
-
strict_1.default.equal(store.getMode(), "file");
|
|
181
|
-
strict_1.default.equal(state.password, null);
|
|
182
|
-
strict_1.default.deepEqual(store.get(), credential);
|
|
183
|
-
});
|
|
184
|
-
(0, node_test_1.default)("切换后清理来源失败时回滚模式和目标副本", (t) => {
|
|
185
|
-
const behavior = {};
|
|
186
|
-
const { files, state, store } = fixture(t, behavior);
|
|
187
|
-
store.use("keychain");
|
|
188
|
-
store.update(credential);
|
|
189
|
-
behavior.failDelete = true;
|
|
190
|
-
strict_1.default.throws(() => store.use("file"), /系统凭据库删除失败/);
|
|
191
|
-
strict_1.default.equal(store.getMode(), "keychain");
|
|
192
|
-
strict_1.default.equal(fs.existsSync(files.paths.auth), false);
|
|
193
|
-
strict_1.default.notEqual(state.password, null);
|
|
194
|
-
});
|
|
195
|
-
(0, node_test_1.default)("配置损坏时登出仍清理文件与 Keyring", (t) => {
|
|
196
|
-
const { files, file, keyring, state, store } = fixture(t);
|
|
197
|
-
file.update(credential);
|
|
198
|
-
keyring.update(credential);
|
|
199
|
-
fs.writeFileSync(files.paths.config, "{broken-config");
|
|
200
|
-
store.delete();
|
|
201
|
-
strict_1.default.equal(files.readAuth(), null);
|
|
202
|
-
strict_1.default.equal(state.password, null);
|
|
203
|
-
strict_1.default.deepEqual(files.readGlobal(), { schemaVersion: 1, credentialStore: "file" });
|
|
204
|
-
});
|
package/lib/device-auth-api.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HttpDeviceAuthApi = exports.DeviceAuthError = exports.DEVICE_AUTH_SCOPES = exports.DEVICE_AUTH_PATHS = void 0;
|
|
4
|
-
exports.authRecord = authRecord;
|
|
5
|
-
exports.authText = authText;
|
|
6
|
-
const PREFIX = "/api/sns/v1/creator/red_skill";
|
|
7
|
-
exports.DEVICE_AUTH_PATHS = Object.freeze({
|
|
8
|
-
create: `${PREFIX}/create_cli_oauth_device_code`,
|
|
9
|
-
poll: `${PREFIX}/poll_cli_oauth_device_token`,
|
|
10
|
-
exchange: `${PREFIX}/exchange_cli_oauth_token`,
|
|
11
|
-
refresh: `${PREFIX}/refresh_cli_oauth_token`,
|
|
12
|
-
});
|
|
13
|
-
exports.DEVICE_AUTH_SCOPES = ["base_info", "skill_publish"];
|
|
14
|
-
class DeviceAuthError extends Error {
|
|
15
|
-
code;
|
|
16
|
-
constructor(code, message) {
|
|
17
|
-
super(message);
|
|
18
|
-
this.code = code;
|
|
19
|
-
this.name = "DeviceAuthError";
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
exports.DeviceAuthError = DeviceAuthError;
|
|
23
|
-
function authRecord(value) {
|
|
24
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
25
|
-
}
|
|
26
|
-
function authText(value) {
|
|
27
|
-
return typeof value === "string" ? value : "";
|
|
28
|
-
}
|
|
29
|
-
class HttpDeviceAuthApi {
|
|
30
|
-
request;
|
|
31
|
-
apiBaseUrl;
|
|
32
|
-
constructor(baseUrl, request = fetch) {
|
|
33
|
-
this.request = request;
|
|
34
|
-
const url = new URL(baseUrl);
|
|
35
|
-
if ((url.protocol !== "https:" &&
|
|
36
|
-
!(url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname))) ||
|
|
37
|
-
url.username ||
|
|
38
|
-
url.password ||
|
|
39
|
-
url.search ||
|
|
40
|
-
url.hash ||
|
|
41
|
-
url.pathname !== "/")
|
|
42
|
-
throw new Error("扫码鉴权地址须为 HTTPS origin(本机测试服务除外)");
|
|
43
|
-
this.apiBaseUrl = url.origin;
|
|
44
|
-
}
|
|
45
|
-
async call(operation, payload, signal) {
|
|
46
|
-
const url = new URL(exports.DEVICE_AUTH_PATHS[operation], this.apiBaseUrl);
|
|
47
|
-
const poll = operation === "poll";
|
|
48
|
-
if (poll)
|
|
49
|
-
url.searchParams.set("device_code", String(payload.device_code));
|
|
50
|
-
let response;
|
|
51
|
-
try {
|
|
52
|
-
response = await this.request(url, {
|
|
53
|
-
method: poll ? "GET" : "POST",
|
|
54
|
-
headers: poll ? undefined : { "content-type": "application/json" },
|
|
55
|
-
body: poll ? undefined : JSON.stringify(payload),
|
|
56
|
-
redirect: "error",
|
|
57
|
-
signal: signal
|
|
58
|
-
? AbortSignal.any([signal, AbortSignal.timeout(20_000)])
|
|
59
|
-
: AbortSignal.timeout(20_000),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
if (signal?.aborted)
|
|
64
|
-
throw new DeviceAuthError("AUTH_CANCELLED", "已取消扫码登录");
|
|
65
|
-
throw new DeviceAuthError("AUTH_UNAVAILABLE", "扫码鉴权服务暂不可用,请稍后重试");
|
|
66
|
-
}
|
|
67
|
-
if ([401, 403].includes(response.status) && operation === "refresh")
|
|
68
|
-
throw new DeviceAuthError("AUTH_EXPIRED", "登录态已失效,请重新扫码登录");
|
|
69
|
-
if (!response.ok)
|
|
70
|
-
throw new DeviceAuthError("AUTH_UNAVAILABLE", `扫码鉴权请求失败(HTTP ${response.status})`);
|
|
71
|
-
let body;
|
|
72
|
-
try {
|
|
73
|
-
body = await response.json();
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
throw new DeviceAuthError("AUTH_INVALID_RESPONSE", "扫码鉴权响应格式无效");
|
|
77
|
-
}
|
|
78
|
-
if (!authRecord(body))
|
|
79
|
-
throw new DeviceAuthError("AUTH_INVALID_RESPONSE", "扫码鉴权响应格式无效");
|
|
80
|
-
const data = authRecord(body.data) ? body.data : body;
|
|
81
|
-
if (body.success === false ||
|
|
82
|
-
data.success === false ||
|
|
83
|
-
(body.code !== undefined && ![0, "0", "OK"].includes(body.code)))
|
|
84
|
-
throw new DeviceAuthError(operation === "refresh" &&
|
|
85
|
-
["invalid_grant", "invalid_token", "token_expired"].includes(String(data.error ?? body.error))
|
|
86
|
-
? "AUTH_EXPIRED"
|
|
87
|
-
: "AUTH_REJECTED", "扫码鉴权请求被拒绝,请重新登录");
|
|
88
|
-
return data;
|
|
89
|
-
}
|
|
90
|
-
async create(codeChallenge, signal) {
|
|
91
|
-
const data = await this.call("create", { code_challenge: codeChallenge, code_challenge_method: "S256", scopes: exports.DEVICE_AUTH_SCOPES }, signal);
|
|
92
|
-
const fallback = data.authorizeUrl ?? data.authorize_url;
|
|
93
|
-
return {
|
|
94
|
-
deviceCode: authText(data.deviceCode ?? data.device_code),
|
|
95
|
-
userCode: authText(data.userCode ?? data.user_code),
|
|
96
|
-
authorizeCommonUrl: authText(data.authorizeCommonUrl ?? data.authorize_common_url ?? fallback),
|
|
97
|
-
authorizeH5Url: authText(data.authorizeH5Url ?? data.authorize_h5_url ?? fallback),
|
|
98
|
-
expiresInSeconds: Number(data.expiresInSeconds ?? data.expires_in_seconds ?? 600),
|
|
99
|
-
pollIntervalSeconds: Number(data.pollIntervalSeconds ?? data.poll_interval_seconds ?? 5),
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
async poll(deviceCode, signal) {
|
|
103
|
-
const data = await this.call("poll", { device_code: deviceCode }, signal);
|
|
104
|
-
const raw = data.authStatus ?? data.auth_status ?? data.status;
|
|
105
|
-
const status = typeof raw === "number"
|
|
106
|
-
? ["", "pending", "approved", "denied", "expired", "cancelled"][raw]
|
|
107
|
-
: String(raw).toLowerCase();
|
|
108
|
-
if (!["pending", "approved", "denied", "expired", "cancelled", "rejected"].includes(status))
|
|
109
|
-
throw new DeviceAuthError("AUTH_INVALID_RESPONSE", "未知的扫码授权状态");
|
|
110
|
-
return {
|
|
111
|
-
status: status === "rejected" ? "denied" : status,
|
|
112
|
-
authorizationCode: authText(data.authorizationCode ?? data.authorization_code) || undefined,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
exchange(code, verifier, signal) {
|
|
116
|
-
return this.call("exchange", { authorization_code: code, code_verifier: verifier }, signal);
|
|
117
|
-
}
|
|
118
|
-
refresh(token) {
|
|
119
|
-
return this.call("refresh", { refresh_token: token });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
exports.HttpDeviceAuthApi = HttpDeviceAuthApi;
|