apifm-admin-mcp 26.5.4 → 26.5.5
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 +42 -33
- package/package.json +1 -1
- package/src/browser-auth.js +67 -234
- package/src/index.js +5 -4
- package/src/self-check.js +7 -1
package/README.md
CHANGED
|
@@ -1,32 +1,26 @@
|
|
|
1
1
|
# apifm-admin-mcp
|
|
2
2
|
|
|
3
|
-
`apifm-admin-mcp`
|
|
3
|
+
`apifm-admin-mcp` 是一个通过 `apifm-admin` SDK 调用 APIFM 后台接口的 MCP Server,可用于 Kiro、Cursor、Claude Code、Codex、Windsurf、Trae、Qoder 等支持 MCP 的 Agent。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 安全原则
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
密码、商户秘钥、X-Token 等敏感信息不要粘贴到聊天窗口。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
本 MCP 通过 `apifm_admin_start_auth` 返回一个本机 `127.0.0.1` 授权页面。用户在浏览器里填写敏感信息,信息只保存在当前 MCP 进程内存中,不会作为工具参数发送给大模型。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
如果尚未授权就调用后台 API,`apifm_admin_call` 和 `apifm_admin_find_and_call` 会直接返回 `authUrl`,Agent 应该立即把这个 URL 展示给用户打开。
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
The API callers reject payloads and headers containing sensitive field names such as `pwd`, `password`, `token`, `x-token`, `authorization`, `secret`, or `key`.
|
|
16
|
-
|
|
17
|
-
Accounts are in-memory only. Restarting the MCP server clears all tokens and credentials.
|
|
18
|
-
|
|
19
|
-
## Install
|
|
13
|
+
## 安装
|
|
20
14
|
|
|
21
15
|
```bash
|
|
22
16
|
npm install -g apifm-admin-mcp
|
|
23
17
|
```
|
|
24
18
|
|
|
25
|
-
|
|
19
|
+
本包依赖 `apifm-admin`,但不会把 SDK 源码打包进 MCP。`apifm-admin` 后续升级新增的方法,会在运行时动态发现。
|
|
26
20
|
|
|
27
|
-
## MCP
|
|
21
|
+
## MCP 配置
|
|
28
22
|
|
|
29
|
-
|
|
23
|
+
使用 `npx`:
|
|
30
24
|
|
|
31
25
|
```json
|
|
32
26
|
{
|
|
@@ -39,7 +33,7 @@ Use stdio:
|
|
|
39
33
|
}
|
|
40
34
|
```
|
|
41
35
|
|
|
42
|
-
|
|
36
|
+
全局安装后:
|
|
43
37
|
|
|
44
38
|
```json
|
|
45
39
|
{
|
|
@@ -51,28 +45,43 @@ If installed globally:
|
|
|
51
45
|
}
|
|
52
46
|
```
|
|
53
47
|
|
|
54
|
-
##
|
|
48
|
+
## 授权方式
|
|
49
|
+
|
|
50
|
+
授权页面分为“登录现有账号”和“注册新账号”。
|
|
51
|
+
|
|
52
|
+
登录现有账号:
|
|
53
|
+
|
|
54
|
+
- 手机号登录:调用 `loginAdminMobile`,也就是“手机号码登录”方法,成功后取返回的 X-Token 作为后续 API 凭证。
|
|
55
|
+
- 邮箱登录:调用 `loginAdminEmail`,也就是“邮箱登录获取X-TOKEN”方法,成功后取返回的 X-Token 作为后续 API 凭证。
|
|
56
|
+
- 直接填写 X-Token:不调用登录接口,直接把填写的 X-Token 作为后续 API 凭证。
|
|
57
|
+
- Basic Authentication:填写商户号和商户秘钥,后续请求同时写入 `Authentication` 和 `Authorization` 请求头。
|
|
58
|
+
|
|
59
|
+
注册新账号:
|
|
60
|
+
|
|
61
|
+
- 邮箱注册:调用邮箱注册保存接口,成功后尝试邮箱登录获取 X-Token。
|
|
62
|
+
- 手机号注册:调用手机号注册保存接口,成功后尝试手机号登录获取 X-Token。
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
- `apifm_admin_accounts`: Lists local account aliases without revealing secrets.
|
|
58
|
-
- `apifm_admin_switch_account`: Switches the active account alias.
|
|
59
|
-
- `apifm_admin_remove_account`: Clears an account alias and its in-memory secrets.
|
|
60
|
-
- `apifm_admin_find_and_call`: Searches for the best matching SDK method, calls it, and returns the live backend response in `apiResult`.
|
|
61
|
-
- `apifm_admin_call`: Calls an exact SDK method using the selected local account and returns the live backend response in `apiResult`.
|
|
62
|
-
- `apifm_admin_search_methods`: Planning helper only. Searches methods dynamically from the installed `apifm-admin` SDK, but does not call the API.
|
|
63
|
-
- `apifm_admin_method_info`: Planning helper only. Shows route, method, parameters, and usage for one SDK method, but does not call the API.
|
|
64
|
+
## 工具
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
- `apifm_admin_start_auth`:启动本机授权页面,支持简体中文、繁体中文、英文。
|
|
67
|
+
- `apifm_admin_accounts`:查看当前 MCP 进程内已授权账号,不返回密钥。
|
|
68
|
+
- `apifm_admin_switch_account`:切换当前使用的账号别名。
|
|
69
|
+
- `apifm_admin_remove_account`:删除当前 MCP 进程内保存的账号凭证。
|
|
70
|
+
- `apifm_admin_find_and_call`:按自然语言搜索 SDK 方法并调用,真实接口返回在 `apiResult`。
|
|
71
|
+
- `apifm_admin_call`:按指定 SDK 方法名调用后台 API,真实接口返回在 `apiResult`。
|
|
72
|
+
- `apifm_admin_search_methods`:只搜索 SDK 方法和参数说明,不会调用 API。
|
|
73
|
+
- `apifm_admin_method_info`:只查看某个 SDK 方法说明,不会调用 API。
|
|
66
74
|
|
|
67
|
-
##
|
|
75
|
+
## 使用流程
|
|
68
76
|
|
|
69
|
-
1.
|
|
70
|
-
2. Agent
|
|
71
|
-
3.
|
|
72
|
-
4.
|
|
73
|
-
5. Agent
|
|
77
|
+
1. 用户让 Agent 读取或操作后台数据。
|
|
78
|
+
2. Agent 调用 `apifm_admin_find_and_call` 或 `apifm_admin_call`。
|
|
79
|
+
3. 如果还没授权,工具返回 `authUrl`。
|
|
80
|
+
4. 用户打开 `authUrl`,选择手机号登录、邮箱登录、X-Token 或 Basic Authentication。
|
|
81
|
+
5. 授权成功后,Agent 重新调用刚才的 API。
|
|
82
|
+
6. Agent 使用 `apiResult` 中的真实接口返回回答用户。
|
|
74
83
|
|
|
75
|
-
##
|
|
84
|
+
## 本地检查
|
|
76
85
|
|
|
77
86
|
```bash
|
|
78
87
|
npm run check
|
package/package.json
CHANGED
package/src/browser-auth.js
CHANGED
|
@@ -10,7 +10,8 @@ const AUTH_TYPES = new Set([
|
|
|
10
10
|
'all',
|
|
11
11
|
'basic',
|
|
12
12
|
'x-token',
|
|
13
|
-
'
|
|
13
|
+
'password',
|
|
14
|
+
'mobile-password',
|
|
14
15
|
'email-password',
|
|
15
16
|
'register-email',
|
|
16
17
|
'register-mobile'
|
|
@@ -18,7 +19,6 @@ const AUTH_TYPES = new Set([
|
|
|
18
19
|
|
|
19
20
|
const OPTIONAL_FIELDS = new Set([
|
|
20
21
|
'alias',
|
|
21
|
-
'pdomain',
|
|
22
22
|
'imgcode',
|
|
23
23
|
'k',
|
|
24
24
|
'registerName',
|
|
@@ -26,9 +26,7 @@ const OPTIONAL_FIELDS = new Set([
|
|
|
26
26
|
'referrer',
|
|
27
27
|
'agentKey',
|
|
28
28
|
'mailCode',
|
|
29
|
-
'smsCode'
|
|
30
|
-
'smsImgCode',
|
|
31
|
-
'smsK'
|
|
29
|
+
'smsCode'
|
|
32
30
|
])
|
|
33
31
|
|
|
34
32
|
const I18N = {
|
|
@@ -46,8 +44,8 @@ const I18N = {
|
|
|
46
44
|
aliasPh: 'default-admin',
|
|
47
45
|
tabLogin: '登录现有账号',
|
|
48
46
|
tabRegister: '注册新账号',
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
methodMobile: '手机号登录',
|
|
48
|
+
methodMobileSub: '手机号码 + 密码',
|
|
51
49
|
methodEmail: '邮箱登录',
|
|
52
50
|
methodEmailSub: '邮箱 + 密码',
|
|
53
51
|
methodBasic: 'Basic Auth',
|
|
@@ -58,18 +56,14 @@ const I18N = {
|
|
|
58
56
|
methodRegisterEmailSub: '邮箱验证码',
|
|
59
57
|
methodRegisterMobile: '手机号注册',
|
|
60
58
|
methodRegisterMobileSub: '短信验证码',
|
|
61
|
-
|
|
62
|
-
emailHint: '使用后台邮箱和密码登录,MCP
|
|
63
|
-
basicHint: '填写 Basic Authentication
|
|
64
|
-
tokenHint: '直接填写管理员登录后的 X-Token
|
|
65
|
-
registerEmailHint: '使用邮箱注册开通新后台账号,提交成功后 MCP
|
|
66
|
-
registerMobileHint: '使用手机号注册开通新后台账号,提交成功后 MCP
|
|
67
|
-
username: '用户名',
|
|
68
|
-
usernamePh: '请输入后台用户名',
|
|
59
|
+
mobileHint: '使用后台手机号码和密码登录,MCP 会调用 loginAdminMobile 手机号码登录方法获取 X-Token。',
|
|
60
|
+
emailHint: '使用后台邮箱和密码登录,MCP 会调用 loginAdminEmail 邮箱登录获取 X-TOKEN 方法。',
|
|
61
|
+
basicHint: '填写 Basic Authentication 信息,后续请求会写入 Authentication 和 Authorization 请求头。',
|
|
62
|
+
tokenHint: '直接填写管理员登录后的 X-Token,MCP 会把它作为后续调用后台 API 的凭证。',
|
|
63
|
+
registerEmailHint: '使用邮箱注册开通新后台账号,提交成功后 MCP 会尝试邮箱登录获取 X-Token。',
|
|
64
|
+
registerMobileHint: '使用手机号注册开通新后台账号,提交成功后 MCP 会尝试手机号登录获取 X-Token。',
|
|
69
65
|
password: '密码',
|
|
70
66
|
passwordPh: '请输入登录密码',
|
|
71
|
-
pdomain: '专属域名,可选',
|
|
72
|
-
pdomainPh: '例如 yourdomain',
|
|
73
67
|
imgcode: '图形验证码,可选',
|
|
74
68
|
imgcodePh: '请输入图形验证码',
|
|
75
69
|
k: '验证码随机数,可选',
|
|
@@ -116,8 +110,8 @@ const I18N = {
|
|
|
116
110
|
aliasPh: 'default-admin',
|
|
117
111
|
tabLogin: '登入現有帳號',
|
|
118
112
|
tabRegister: '註冊新帳號',
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
methodMobile: '手機號登入',
|
|
114
|
+
methodMobileSub: '手機號碼 + 密碼',
|
|
121
115
|
methodEmail: '郵箱登入',
|
|
122
116
|
methodEmailSub: '郵箱 + 密碼',
|
|
123
117
|
methodBasic: 'Basic Auth',
|
|
@@ -128,18 +122,14 @@ const I18N = {
|
|
|
128
122
|
methodRegisterEmailSub: '郵箱驗證碼',
|
|
129
123
|
methodRegisterMobile: '手機號註冊',
|
|
130
124
|
methodRegisterMobileSub: '簡訊驗證碼',
|
|
131
|
-
|
|
132
|
-
emailHint: '使用後台郵箱和密碼登入,MCP
|
|
133
|
-
basicHint: '填寫 Basic Authentication
|
|
134
|
-
tokenHint: '直接填寫管理員登入後的 X-Token
|
|
135
|
-
registerEmailHint: '使用郵箱註冊開通新後台帳號,提交成功後 MCP
|
|
136
|
-
registerMobileHint: '使用手機號註冊開通新後台帳號,提交成功後 MCP
|
|
137
|
-
username: '使用者名稱',
|
|
138
|
-
usernamePh: '請輸入後台使用者名稱',
|
|
125
|
+
mobileHint: '使用後台手機號碼和密碼登入,MCP 會呼叫 loginAdminMobile 手機號碼登入方法取得 X-Token。',
|
|
126
|
+
emailHint: '使用後台郵箱和密碼登入,MCP 會呼叫 loginAdminEmail 郵箱登入取得 X-TOKEN 方法。',
|
|
127
|
+
basicHint: '填寫 Basic Authentication 資訊,後續請求會寫入 Authentication 和 Authorization 請求頭。',
|
|
128
|
+
tokenHint: '直接填寫管理員登入後的 X-Token,MCP 會把它作為後續呼叫後台 API 的憑證。',
|
|
129
|
+
registerEmailHint: '使用郵箱註冊開通新後台帳號,提交成功後 MCP 會嘗試郵箱登入取得 X-Token。',
|
|
130
|
+
registerMobileHint: '使用手機號註冊開通新後台帳號,提交成功後 MCP 會嘗試手機號登入取得 X-Token。',
|
|
139
131
|
password: '密碼',
|
|
140
132
|
passwordPh: '請輸入登入密碼',
|
|
141
|
-
pdomain: '專屬網域,可選',
|
|
142
|
-
pdomainPh: '例如 yourdomain',
|
|
143
133
|
imgcode: '圖形驗證碼,可選',
|
|
144
134
|
imgcodePh: '請輸入圖形驗證碼',
|
|
145
135
|
k: '驗證碼隨機數,可選',
|
|
@@ -186,8 +176,8 @@ const I18N = {
|
|
|
186
176
|
aliasPh: 'default-admin',
|
|
187
177
|
tabLogin: 'Log in',
|
|
188
178
|
tabRegister: 'Register',
|
|
189
|
-
|
|
190
|
-
|
|
179
|
+
methodMobile: 'Mobile login',
|
|
180
|
+
methodMobileSub: 'Mobile + password',
|
|
191
181
|
methodEmail: 'Email login',
|
|
192
182
|
methodEmailSub: 'Email + password',
|
|
193
183
|
methodBasic: 'Basic Auth',
|
|
@@ -198,18 +188,14 @@ const I18N = {
|
|
|
198
188
|
methodRegisterEmailSub: 'Email code',
|
|
199
189
|
methodRegisterMobile: 'Mobile signup',
|
|
200
190
|
methodRegisterMobileSub: 'SMS code',
|
|
201
|
-
|
|
202
|
-
emailHint: 'Log in with an admin email and password. The MCP
|
|
203
|
-
basicHint: 'Enter Basic Authentication credentials.
|
|
204
|
-
tokenHint: 'Enter an existing admin X-Token for later
|
|
205
|
-
registerEmailHint: 'Create a new admin account by email. After signup, the MCP will try to
|
|
206
|
-
registerMobileHint: 'Create a new admin account by mobile. After signup, the MCP will try to
|
|
207
|
-
username: 'Username',
|
|
208
|
-
usernamePh: 'Enter admin username',
|
|
191
|
+
mobileHint: 'Log in with an admin mobile number and password. The MCP calls the mobile login SDK method to get X-Token.',
|
|
192
|
+
emailHint: 'Log in with an admin email and password. The MCP calls the email login SDK method to get X-Token.',
|
|
193
|
+
basicHint: 'Enter Basic Authentication credentials. API calls will include both Authentication and Authorization headers.',
|
|
194
|
+
tokenHint: 'Enter an existing admin X-Token. The MCP will use it as the credential for later admin API calls.',
|
|
195
|
+
registerEmailHint: 'Create a new admin account by email. After signup, the MCP will try email login to get X-Token.',
|
|
196
|
+
registerMobileHint: 'Create a new admin account by mobile. After signup, the MCP will try mobile login to get X-Token.',
|
|
209
197
|
password: 'Password',
|
|
210
198
|
passwordPh: 'Enter login password',
|
|
211
|
-
pdomain: 'Private domain, optional',
|
|
212
|
-
pdomainPh: 'Example: yourdomain',
|
|
213
199
|
imgcode: 'Image code, optional',
|
|
214
200
|
imgcodePh: 'Enter image code',
|
|
215
201
|
k: 'Captcha nonce, optional',
|
|
@@ -327,7 +313,7 @@ async function finishAuth({ authType, alias, domains, fields }) {
|
|
|
327
313
|
})
|
|
328
314
|
}
|
|
329
315
|
|
|
330
|
-
if (selectedAuthType === '
|
|
316
|
+
if (selectedAuthType === 'mobile-password' || selectedAuthType === 'email-password') {
|
|
331
317
|
const token = await loginAndExtractToken({ authType: selectedAuthType, domains, fields })
|
|
332
318
|
return upsertAccount({
|
|
333
319
|
alias: fields.alias || alias,
|
|
@@ -386,17 +372,6 @@ async function finishAuth({ authType, alias, domains, fields }) {
|
|
|
386
372
|
|
|
387
373
|
async function loginAndExtractToken({ authType, domains, fields }) {
|
|
388
374
|
const loginConfig = {
|
|
389
|
-
'username-password': {
|
|
390
|
-
methodName: 'loginAdminUserName',
|
|
391
|
-
payload: {
|
|
392
|
-
userName: required(fields.loginUsername, 'username'),
|
|
393
|
-
pwd: required(fields.loginPassword, 'password'),
|
|
394
|
-
rememberMe: true,
|
|
395
|
-
pdomain: fields.pdomain || undefined,
|
|
396
|
-
imgcode: fields.imgcode || undefined,
|
|
397
|
-
k: fields.k || undefined
|
|
398
|
-
}
|
|
399
|
-
},
|
|
400
375
|
'email-password': {
|
|
401
376
|
methodName: 'loginAdminEmail',
|
|
402
377
|
payload: {
|
|
@@ -410,11 +385,11 @@ async function loginAndExtractToken({ authType, domains, fields }) {
|
|
|
410
385
|
'mobile-password': {
|
|
411
386
|
methodName: 'loginAdminMobile',
|
|
412
387
|
payload: {
|
|
413
|
-
mobile: required(fields.registerMobile, 'mobile'),
|
|
414
|
-
pwd: required(fields.registerPassword, 'password'),
|
|
388
|
+
mobile: required(fields.loginMobile || fields.registerMobile, 'mobile'),
|
|
389
|
+
pwd: required(fields.loginPassword || fields.registerPassword, 'password'),
|
|
415
390
|
rememberMe: true,
|
|
416
|
-
imgcode: fields.
|
|
417
|
-
k: fields.
|
|
391
|
+
imgcode: fields.imgcode || undefined,
|
|
392
|
+
k: fields.k || undefined
|
|
418
393
|
}
|
|
419
394
|
}
|
|
420
395
|
}[authType]
|
|
@@ -450,7 +425,7 @@ function compactObject(input) {
|
|
|
450
425
|
}
|
|
451
426
|
|
|
452
427
|
function normalizeAuthType(authType) {
|
|
453
|
-
if (authType === 'password') return '
|
|
428
|
+
if (authType === 'password' || authType === 'username-password') return 'mobile-password'
|
|
454
429
|
if (AUTH_TYPES.has(authType)) return authType
|
|
455
430
|
return 'all'
|
|
456
431
|
}
|
|
@@ -478,8 +453,7 @@ function extractToken(response) {
|
|
|
478
453
|
if (typeof value === 'string' && value.trim()) return value.trim()
|
|
479
454
|
}
|
|
480
455
|
|
|
481
|
-
|
|
482
|
-
return found || ''
|
|
456
|
+
return findTokenDeep(response) || ''
|
|
483
457
|
}
|
|
484
458
|
|
|
485
459
|
function getPath(value, path) {
|
|
@@ -500,7 +474,7 @@ function findTokenDeep(value, depth = 0) {
|
|
|
500
474
|
if (
|
|
501
475
|
typeof child === 'string' &&
|
|
502
476
|
child.trim() &&
|
|
503
|
-
['token', 'xtoken', '
|
|
477
|
+
['token', 'xtoken', 'xstoken', 'admintoken', 'logintoken'].includes(compactKey)
|
|
504
478
|
) {
|
|
505
479
|
return child.trim()
|
|
506
480
|
}
|
|
@@ -563,7 +537,7 @@ function escapeHtml(value) {
|
|
|
563
537
|
}
|
|
564
538
|
|
|
565
539
|
function renderForm({ authType, alias, expiresAt }) {
|
|
566
|
-
const initialType = authType === 'all' ? '
|
|
540
|
+
const initialType = authType === 'all' ? 'mobile-password' : authType
|
|
567
541
|
const initialGroup = initialType.startsWith('register-') ? 'register' : 'login'
|
|
568
542
|
return `<!doctype html>
|
|
569
543
|
<html lang="zh-CN">
|
|
@@ -583,7 +557,6 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
583
557
|
--accent: oklch(0.56 0.19 255);
|
|
584
558
|
--accent-ink: oklch(1 0 0);
|
|
585
559
|
--accent-soft: oklch(0.94 0.035 255);
|
|
586
|
-
--success: oklch(0.58 0.15 155);
|
|
587
560
|
}
|
|
588
561
|
* { box-sizing: border-box; }
|
|
589
562
|
body {
|
|
@@ -595,44 +568,11 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
595
568
|
linear-gradient(135deg, var(--bg), oklch(0.955 0.011 230));
|
|
596
569
|
color: var(--ink);
|
|
597
570
|
}
|
|
598
|
-
.shell {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
margin: 0 auto;
|
|
602
|
-
display: grid;
|
|
603
|
-
place-items: center;
|
|
604
|
-
padding: 28px 0;
|
|
605
|
-
}
|
|
606
|
-
.panel {
|
|
607
|
-
width: 100%;
|
|
608
|
-
display: grid;
|
|
609
|
-
grid-template-columns: minmax(280px, 0.78fr) minmax(340px, 1.22fr);
|
|
610
|
-
background: var(--surface);
|
|
611
|
-
border: 1px solid var(--line);
|
|
612
|
-
border-radius: 16px;
|
|
613
|
-
overflow: hidden;
|
|
614
|
-
box-shadow: 0 8px 28px oklch(0.25 0.02 255 / 0.12);
|
|
615
|
-
}
|
|
616
|
-
.aside {
|
|
617
|
-
padding: 34px;
|
|
618
|
-
background: linear-gradient(180deg, oklch(0.26 0.07 255), oklch(0.19 0.04 255));
|
|
619
|
-
color: white;
|
|
620
|
-
display: flex;
|
|
621
|
-
flex-direction: column;
|
|
622
|
-
justify-content: space-between;
|
|
623
|
-
gap: 32px;
|
|
624
|
-
}
|
|
571
|
+
.shell { width: min(1120px, calc(100vw - 32px)); min-height: 100vh; margin: 0 auto; display: grid; place-items: center; padding: 28px 0; }
|
|
572
|
+
.panel { width: 100%; display: grid; grid-template-columns: minmax(280px, 0.78fr) minmax(340px, 1.22fr); background: var(--surface); border: 1px solid var(--line); border-radius: 16px; overflow: hidden; box-shadow: 0 8px 28px oklch(0.25 0.02 255 / 0.12); }
|
|
573
|
+
.aside { padding: 34px; background: linear-gradient(180deg, oklch(0.26 0.07 255), oklch(0.19 0.04 255)); color: white; display: flex; flex-direction: column; justify-content: space-between; gap: 32px; }
|
|
625
574
|
.brand { display: flex; align-items: center; gap: 12px; font-weight: 760; }
|
|
626
|
-
.mark {
|
|
627
|
-
width: 38px;
|
|
628
|
-
height: 38px;
|
|
629
|
-
display: grid;
|
|
630
|
-
place-items: center;
|
|
631
|
-
border-radius: 10px;
|
|
632
|
-
background: oklch(0.74 0.16 210);
|
|
633
|
-
color: oklch(0.18 0.04 255);
|
|
634
|
-
font-weight: 850;
|
|
635
|
-
}
|
|
575
|
+
.mark { width: 38px; height: 38px; display: grid; place-items: center; border-radius: 10px; background: oklch(0.74 0.16 210); color: oklch(0.18 0.04 255); font-weight: 850; }
|
|
636
576
|
h1 { margin: 28px 0 12px; font-size: 2rem; line-height: 1.12; text-wrap: balance; letter-spacing: 0; }
|
|
637
577
|
.lead { margin: 0; color: oklch(0.88 0.018 250); max-width: 48ch; }
|
|
638
578
|
.trust { display: grid; gap: 10px; margin: 28px 0 0; padding: 0; list-style: none; }
|
|
@@ -641,41 +581,14 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
641
581
|
.content { padding: 30px; }
|
|
642
582
|
.topbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 20px; }
|
|
643
583
|
.meta { color: var(--muted); font-size: 0.92rem; }
|
|
644
|
-
.lang, .tabs {
|
|
645
|
-
|
|
646
|
-
grid-auto-flow: column;
|
|
647
|
-
gap: 4px;
|
|
648
|
-
padding: 4px;
|
|
649
|
-
background: var(--surface-2);
|
|
650
|
-
border: 1px solid var(--line);
|
|
651
|
-
border-radius: 999px;
|
|
652
|
-
}
|
|
653
|
-
.lang button, .tabs button, .method button {
|
|
654
|
-
appearance: none;
|
|
655
|
-
border: 0;
|
|
656
|
-
font: inherit;
|
|
657
|
-
cursor: pointer;
|
|
658
|
-
}
|
|
584
|
+
.lang, .tabs { display: inline-grid; grid-auto-flow: column; gap: 4px; padding: 4px; background: var(--surface-2); border: 1px solid var(--line); border-radius: 999px; }
|
|
585
|
+
.lang button, .tabs button, .method button { appearance: none; border: 0; font: inherit; cursor: pointer; }
|
|
659
586
|
.lang button, .tabs button { padding: 7px 12px; border-radius: 999px; color: var(--muted); background: transparent; }
|
|
660
587
|
.lang button[aria-pressed="true"], .tabs button[aria-pressed="true"] { background: var(--ink); color: white; }
|
|
661
588
|
.tabs { margin-bottom: 14px; }
|
|
662
|
-
.method {
|
|
663
|
-
display: grid;
|
|
664
|
-
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
665
|
-
gap: 8px;
|
|
666
|
-
margin-bottom: 22px;
|
|
667
|
-
}
|
|
589
|
+
.method { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-bottom: 22px; }
|
|
668
590
|
.method[data-group="register"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
669
|
-
.method button {
|
|
670
|
-
min-height: 62px;
|
|
671
|
-
padding: 10px;
|
|
672
|
-
border: 1px solid var(--line);
|
|
673
|
-
border-radius: 10px;
|
|
674
|
-
background: var(--surface-2);
|
|
675
|
-
color: var(--ink);
|
|
676
|
-
text-align: left;
|
|
677
|
-
transition: border-color 160ms ease, background 160ms ease, transform 160ms ease;
|
|
678
|
-
}
|
|
591
|
+
.method button { min-height: 62px; padding: 10px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); color: var(--ink); text-align: left; transition: border-color 160ms ease, background 160ms ease, transform 160ms ease; }
|
|
679
592
|
.method button:hover { border-color: oklch(0.72 0.04 255); transform: translateY(-1px); }
|
|
680
593
|
.method button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
|
|
681
594
|
.method strong { display: block; font-size: 0.92rem; line-height: 1.2; }
|
|
@@ -684,67 +597,20 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
684
597
|
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
|
685
598
|
.field { display: grid; gap: 7px; }
|
|
686
599
|
label { color: var(--ink); font-weight: 660; }
|
|
687
|
-
input {
|
|
688
|
-
width: 100%;
|
|
689
|
-
min-height: 44px;
|
|
690
|
-
padding: 10px 12px;
|
|
691
|
-
border: 1px solid var(--line);
|
|
692
|
-
border-radius: 8px;
|
|
693
|
-
background: white;
|
|
694
|
-
color: var(--ink);
|
|
695
|
-
font: inherit;
|
|
696
|
-
outline: none;
|
|
697
|
-
transition: border-color 160ms ease, box-shadow 160ms ease;
|
|
698
|
-
}
|
|
600
|
+
input { width: 100%; min-height: 44px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; background: white; color: var(--ink); font: inherit; outline: none; transition: border-color 160ms ease, box-shadow 160ms ease; }
|
|
699
601
|
input::placeholder { color: oklch(0.48 0.025 255); }
|
|
700
602
|
input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px oklch(0.62 0.16 255 / 0.18); }
|
|
701
|
-
.section {
|
|
702
|
-
display: none;
|
|
703
|
-
padding: 18px;
|
|
704
|
-
border: 1px solid var(--line);
|
|
705
|
-
border-radius: 12px;
|
|
706
|
-
background: var(--surface-2);
|
|
707
|
-
}
|
|
603
|
+
.section { display: none; padding: 18px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface-2); }
|
|
708
604
|
.section.active { display: block; }
|
|
709
605
|
.section-head { margin: 0 0 14px; color: var(--muted); }
|
|
710
|
-
.submit {
|
|
711
|
-
min-height: 46px;
|
|
712
|
-
border: 0;
|
|
713
|
-
border-radius: 8px;
|
|
714
|
-
background: var(--accent);
|
|
715
|
-
color: var(--accent-ink);
|
|
716
|
-
font: inherit;
|
|
717
|
-
font-weight: 760;
|
|
718
|
-
cursor: pointer;
|
|
719
|
-
transition: transform 160ms ease, filter 160ms ease;
|
|
720
|
-
}
|
|
606
|
+
.submit { min-height: 46px; border: 0; border-radius: 8px; background: var(--accent); color: var(--accent-ink); font: inherit; font-weight: 760; cursor: pointer; transition: transform 160ms ease, filter 160ms ease; }
|
|
721
607
|
.submit:hover { filter: brightness(1.03); transform: translateY(-1px); }
|
|
722
608
|
.note { margin: 12px 0 0; color: var(--muted); font-size: 0.9rem; }
|
|
723
|
-
.security {
|
|
724
|
-
margin-top: 20px;
|
|
725
|
-
padding: 12px 14px;
|
|
726
|
-
border-radius: 10px;
|
|
727
|
-
background: oklch(0.96 0.025 155);
|
|
728
|
-
color: oklch(0.31 0.07 155);
|
|
729
|
-
font-size: 0.9rem;
|
|
730
|
-
}
|
|
609
|
+
.security { margin-top: 20px; padding: 12px 14px; border-radius: 10px; background: oklch(0.96 0.025 155); color: oklch(0.31 0.07 155); font-size: 0.9rem; }
|
|
731
610
|
[hidden] { display: none !important; }
|
|
732
|
-
@media (max-width: 900px) {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
.method { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
736
|
-
.grid { grid-template-columns: 1fr; }
|
|
737
|
-
}
|
|
738
|
-
@media (max-width: 520px) {
|
|
739
|
-
.shell { width: min(100vw - 20px, 1120px); padding: 10px 0; }
|
|
740
|
-
.content { padding: 20px; }
|
|
741
|
-
.topbar { align-items: flex-start; flex-direction: column; }
|
|
742
|
-
.method, .method[data-group="register"] { grid-template-columns: 1fr; }
|
|
743
|
-
h1 { font-size: 1.55rem; }
|
|
744
|
-
}
|
|
745
|
-
@media (prefers-reduced-motion: reduce) {
|
|
746
|
-
*, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
|
|
747
|
-
}
|
|
611
|
+
@media (max-width: 900px) { .panel { grid-template-columns: 1fr; } .aside { padding: 26px; } .method { grid-template-columns: repeat(2, minmax(0, 1fr)); } .grid { grid-template-columns: 1fr; } }
|
|
612
|
+
@media (max-width: 520px) { .shell { width: min(100vw - 20px, 1120px); padding: 10px 0; } .content { padding: 20px; } .topbar { align-items: flex-start; flex-direction: column; } .method, .method[data-group="register"] { grid-template-columns: 1fr; } h1 { font-size: 1.55rem; } }
|
|
613
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }
|
|
748
614
|
</style>
|
|
749
615
|
</head>
|
|
750
616
|
<body>
|
|
@@ -775,14 +641,12 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
775
641
|
<button type="button" data-lang="en" aria-pressed="false">EN</button>
|
|
776
642
|
</div>
|
|
777
643
|
</div>
|
|
778
|
-
|
|
779
644
|
<div class="tabs" role="tablist" aria-label="Account mode">
|
|
780
645
|
<button type="button" data-group-tab="login" aria-pressed="${initialGroup === 'login'}" data-i18n="tabLogin"></button>
|
|
781
646
|
<button type="button" data-group-tab="register" aria-pressed="${initialGroup === 'register'}" data-i18n="tabRegister"></button>
|
|
782
647
|
</div>
|
|
783
|
-
|
|
784
648
|
<div class="method" data-group="login">
|
|
785
|
-
${renderMethodButton('
|
|
649
|
+
${renderMethodButton('mobile-password', initialType)}
|
|
786
650
|
${renderMethodButton('email-password', initialType)}
|
|
787
651
|
${renderMethodButton('basic', initialType)}
|
|
788
652
|
${renderMethodButton('x-token', initialType)}
|
|
@@ -791,16 +655,13 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
791
655
|
${renderMethodButton('register-email', initialType)}
|
|
792
656
|
${renderMethodButton('register-mobile', initialType)}
|
|
793
657
|
</div>
|
|
794
|
-
|
|
795
658
|
<form method="post" autocomplete="off" id="authForm">
|
|
796
659
|
<input type="hidden" id="authType" name="authType" value="${escapeHtml(initialType)}">
|
|
797
660
|
<div class="field">
|
|
798
661
|
<label for="alias" data-i18n="alias"></label>
|
|
799
662
|
<input id="alias" name="alias" value="${escapeHtml(alias)}" data-i18n-placeholder="aliasPh">
|
|
800
663
|
</div>
|
|
801
|
-
|
|
802
664
|
${renderSections()}
|
|
803
|
-
|
|
804
665
|
<button class="submit" type="submit" data-i18n="submit"></button>
|
|
805
666
|
</form>
|
|
806
667
|
<div class="security" data-i18n="security"></div>
|
|
@@ -812,7 +673,7 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
812
673
|
const messages = ${JSON.stringify(I18N)}
|
|
813
674
|
const initialType = ${JSON.stringify(initialType)}
|
|
814
675
|
const methodGroups = {
|
|
815
|
-
'
|
|
676
|
+
'mobile-password': 'login',
|
|
816
677
|
'email-password': 'login',
|
|
817
678
|
basic: 'login',
|
|
818
679
|
'x-token': 'login',
|
|
@@ -821,53 +682,36 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
821
682
|
}
|
|
822
683
|
const optionalFields = new Set(${JSON.stringify([...OPTIONAL_FIELDS])})
|
|
823
684
|
let activeLang = localStorage.getItem('apifm-auth-lang') || 'zh-CN'
|
|
824
|
-
|
|
825
685
|
const authTypeInput = document.querySelector('#authType')
|
|
826
686
|
const sections = [...document.querySelectorAll('[data-section]')]
|
|
827
687
|
const methodButtons = [...document.querySelectorAll('[data-method]')]
|
|
828
688
|
const groupTabs = [...document.querySelectorAll('[data-group-tab]')]
|
|
829
689
|
const methodGroupsEl = [...document.querySelectorAll('.method[data-group]')]
|
|
830
690
|
const langButtons = [...document.querySelectorAll('[data-lang]')]
|
|
831
|
-
|
|
832
|
-
function t(key) {
|
|
833
|
-
return messages[activeLang][key] || messages['zh-CN'][key] || key
|
|
834
|
-
}
|
|
835
|
-
|
|
691
|
+
function t(key) { return messages[activeLang][key] || messages['zh-CN'][key] || key }
|
|
836
692
|
function setLang(lang) {
|
|
837
693
|
activeLang = messages[lang] ? lang : 'zh-CN'
|
|
838
694
|
localStorage.setItem('apifm-auth-lang', activeLang)
|
|
839
695
|
document.documentElement.lang = activeLang
|
|
840
|
-
document.querySelectorAll('[data-i18n]').forEach((node) => {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
document.querySelectorAll('[data-i18n-placeholder]').forEach((node) => {
|
|
844
|
-
node.placeholder = t(node.dataset.i18nPlaceholder)
|
|
845
|
-
})
|
|
846
|
-
langButtons.forEach((button) => {
|
|
847
|
-
button.setAttribute('aria-pressed', String(button.dataset.lang === activeLang))
|
|
848
|
-
})
|
|
696
|
+
document.querySelectorAll('[data-i18n]').forEach((node) => { node.textContent = t(node.dataset.i18n) })
|
|
697
|
+
document.querySelectorAll('[data-i18n-placeholder]').forEach((node) => { node.placeholder = t(node.dataset.i18nPlaceholder) })
|
|
698
|
+
langButtons.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.lang === activeLang)))
|
|
849
699
|
}
|
|
850
|
-
|
|
851
700
|
function setGroup(group) {
|
|
852
701
|
groupTabs.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.groupTab === group)))
|
|
853
|
-
methodGroupsEl.forEach((node) => {
|
|
854
|
-
node.hidden = node.dataset.group !== group
|
|
855
|
-
})
|
|
702
|
+
methodGroupsEl.forEach((node) => { node.hidden = node.dataset.group !== group })
|
|
856
703
|
const currentMethod = authTypeInput.value
|
|
857
704
|
if (methodGroups[currentMethod] !== group) {
|
|
858
705
|
const firstMethod = methodButtons.find((button) => methodGroups[button.dataset.method] === group)?.dataset.method
|
|
859
706
|
setMethod(firstMethod)
|
|
860
707
|
}
|
|
861
708
|
}
|
|
862
|
-
|
|
863
709
|
function setMethod(method) {
|
|
864
710
|
if (!method) return
|
|
865
711
|
authTypeInput.value = method
|
|
866
712
|
const group = methodGroups[method]
|
|
867
713
|
groupTabs.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.groupTab === group)))
|
|
868
|
-
methodGroupsEl.forEach((node) => {
|
|
869
|
-
node.hidden = node.dataset.group !== group
|
|
870
|
-
})
|
|
714
|
+
methodGroupsEl.forEach((node) => { node.hidden = node.dataset.group !== group })
|
|
871
715
|
sections.forEach((section) => {
|
|
872
716
|
const active = section.dataset.section === method
|
|
873
717
|
section.classList.toggle('active', active)
|
|
@@ -876,11 +720,8 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
876
720
|
input.required = active && !optionalFields.has(input.name)
|
|
877
721
|
})
|
|
878
722
|
})
|
|
879
|
-
methodButtons.forEach((button) =>
|
|
880
|
-
button.setAttribute('aria-pressed', String(button.dataset.method === method))
|
|
881
|
-
})
|
|
723
|
+
methodButtons.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.method === method)))
|
|
882
724
|
}
|
|
883
|
-
|
|
884
725
|
methodButtons.forEach((button) => button.addEventListener('click', () => setMethod(button.dataset.method)))
|
|
885
726
|
groupTabs.forEach((button) => button.addEventListener('click', () => setGroup(button.dataset.groupTab)))
|
|
886
727
|
langButtons.forEach((button) => button.addEventListener('click', () => setLang(button.dataset.lang)))
|
|
@@ -893,17 +734,15 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
893
734
|
|
|
894
735
|
function renderSections() {
|
|
895
736
|
return `
|
|
896
|
-
<section class="section" data-section="
|
|
897
|
-
<p class="section-head" data-i18n="
|
|
737
|
+
<section class="section" data-section="mobile-password">
|
|
738
|
+
<p class="section-head" data-i18n="mobileHint"></p>
|
|
898
739
|
<div class="grid">
|
|
899
|
-
${field('
|
|
900
|
-
${field('
|
|
901
|
-
${field('
|
|
902
|
-
${field('
|
|
903
|
-
${field('k', 'k', 'k', 'kPh')}
|
|
740
|
+
${field('loginMobile', 'loginMobile', 'mobile', 'mobilePh', 'tel')}
|
|
741
|
+
${field('loginPasswordMobile', 'loginPassword', 'password', 'passwordPh', 'password')}
|
|
742
|
+
${field('imgcodeMobile', 'imgcode', 'imgcode', 'imgcodePh')}
|
|
743
|
+
${field('kMobile', 'k', 'k', 'kPh')}
|
|
904
744
|
</div>
|
|
905
745
|
</section>
|
|
906
|
-
|
|
907
746
|
<section class="section" data-section="email-password">
|
|
908
747
|
<p class="section-head" data-i18n="emailHint"></p>
|
|
909
748
|
<div class="grid">
|
|
@@ -913,7 +752,6 @@ function renderSections() {
|
|
|
913
752
|
${field('kEmail', 'k', 'k', 'kPh')}
|
|
914
753
|
</div>
|
|
915
754
|
</section>
|
|
916
|
-
|
|
917
755
|
<section class="section" data-section="basic">
|
|
918
756
|
<p class="section-head" data-i18n="basicHint"></p>
|
|
919
757
|
<div class="grid">
|
|
@@ -921,12 +759,10 @@ function renderSections() {
|
|
|
921
759
|
${field('basicPassword', 'basicPassword', 'merchantKey', 'merchantKeyPh', 'password')}
|
|
922
760
|
</div>
|
|
923
761
|
</section>
|
|
924
|
-
|
|
925
762
|
<section class="section" data-section="x-token">
|
|
926
763
|
<p class="section-head" data-i18n="tokenHint"></p>
|
|
927
764
|
${field('xToken', 'xToken', 'xToken', 'xTokenPh', 'password')}
|
|
928
765
|
</section>
|
|
929
|
-
|
|
930
766
|
<section class="section" data-section="register-email">
|
|
931
767
|
<p class="section-head" data-i18n="registerEmailHint"></p>
|
|
932
768
|
<div class="grid">
|
|
@@ -937,7 +773,6 @@ function renderSections() {
|
|
|
937
773
|
${field('referrerEmail', 'referrer', 'referrer', 'referrerPh')}
|
|
938
774
|
</div>
|
|
939
775
|
</section>
|
|
940
|
-
|
|
941
776
|
<section class="section" data-section="register-mobile">
|
|
942
777
|
<p class="section-head" data-i18n="registerMobileHint"></p>
|
|
943
778
|
<div class="grid">
|
|
@@ -948,8 +783,6 @@ function renderSections() {
|
|
|
948
783
|
${field('registerType', 'registerType', 'type', 'typePh')}
|
|
949
784
|
${field('referrerMobile', 'referrer', 'referrer', 'referrerPh')}
|
|
950
785
|
${field('agentKey', 'agentKey', 'agentKey', 'agentKeyPh')}
|
|
951
|
-
${field('smsImgCode', 'smsImgCode', 'imgcode', 'imgcodePh')}
|
|
952
|
-
${field('smsK', 'smsK', 'k', 'kPh')}
|
|
953
786
|
</div>
|
|
954
787
|
</section>`
|
|
955
788
|
}
|
|
@@ -963,7 +796,7 @@ function field(id, name, labelKey, placeholderKey, type = 'text') {
|
|
|
963
796
|
|
|
964
797
|
function renderMethodButton(method, activeType) {
|
|
965
798
|
const keys = {
|
|
966
|
-
'
|
|
799
|
+
'mobile-password': ['methodMobile', 'methodMobileSub'],
|
|
967
800
|
'email-password': ['methodEmail', 'methodEmailSub'],
|
|
968
801
|
basic: ['methodBasic', 'methodBasicSub'],
|
|
969
802
|
'x-token': ['methodToken', 'methodTokenSub'],
|
package/src/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
summarizeMetadata
|
|
22
22
|
} from './sdk.js'
|
|
23
23
|
|
|
24
|
-
const VERSION = '26.5.
|
|
24
|
+
const VERSION = '26.5.5'
|
|
25
25
|
|
|
26
26
|
const server = new McpServer(
|
|
27
27
|
{
|
|
@@ -44,13 +44,13 @@ server.registerTool(
|
|
|
44
44
|
{
|
|
45
45
|
title: 'Start secure APIFM admin authorization',
|
|
46
46
|
description:
|
|
47
|
-
'Creates a localhost browser page for login, Basic Auth, X-Token, email registration, or mobile registration. Secrets are stored only in this MCP process memory and must not be pasted into chat.',
|
|
47
|
+
'Creates a localhost browser page for mobile login, email login, Basic Auth, X-Token, email registration, or mobile registration. Secrets are stored only in this MCP process memory and must not be pasted into chat.',
|
|
48
48
|
inputSchema: z.object({
|
|
49
49
|
authType: z
|
|
50
|
-
.enum(['all', 'basic', 'x-token', 'password', '
|
|
50
|
+
.enum(['all', 'basic', 'x-token', 'password', 'mobile-password', 'email-password', 'register-email', 'register-mobile'])
|
|
51
51
|
.optional()
|
|
52
52
|
.describe(
|
|
53
|
-
'Optional initial auth method. all shows every method in one browser page. basic = Basic Authentication, x-token = existing admin X-Token,
|
|
53
|
+
'Optional initial auth method. all shows every method in one browser page. basic = Basic Authentication, x-token = existing admin X-Token, mobile-password/email-password = SDK login, register-email/register-mobile = create admin account.'
|
|
54
54
|
),
|
|
55
55
|
alias: z.string().optional().describe('Friendly local account alias, for example prod or test-shop.'),
|
|
56
56
|
domains: z
|
|
@@ -304,6 +304,7 @@ async function callToolResult({
|
|
|
304
304
|
const sdkHeaders = { ...headers }
|
|
305
305
|
if (account.basicAuth) {
|
|
306
306
|
sdkHeaders.Authorization = account.basicAuth
|
|
307
|
+
sdkHeaders.Authentication = account.basicAuth
|
|
307
308
|
}
|
|
308
309
|
|
|
309
310
|
sdk.setConfig({
|
package/src/self-check.js
CHANGED
|
@@ -47,16 +47,22 @@ try {
|
|
|
47
47
|
'data-lang="en"',
|
|
48
48
|
'data-group-tab="login"',
|
|
49
49
|
'data-group-tab="register"',
|
|
50
|
+
'data-method="mobile-password"',
|
|
50
51
|
'data-method="basic"',
|
|
51
52
|
'data-method="x-token"',
|
|
52
53
|
'data-method="register-mobile"',
|
|
53
54
|
'merchantNo',
|
|
54
|
-
'merchantKey'
|
|
55
|
+
'merchantKey',
|
|
56
|
+
'loginAdminMobile',
|
|
57
|
+
'Authentication'
|
|
55
58
|
]) {
|
|
56
59
|
if (!authPage.includes(expected)) {
|
|
57
60
|
throw new Error(`Authorization page is missing ${expected}.`)
|
|
58
61
|
}
|
|
59
62
|
}
|
|
63
|
+
if (authPage.includes('data-method="username-password"')) {
|
|
64
|
+
throw new Error('Authorization page must not expose username-password login.')
|
|
65
|
+
}
|
|
60
66
|
|
|
61
67
|
const mobileAuth = await client.callTool({
|
|
62
68
|
name: 'apifm_admin_start_auth',
|