@aalis/plugin-webui-server 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +6 -22
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +29 -140
- package/dist/auth.js.map +1 -1
- package/dist/client-discovery.d.ts +8 -0
- package/dist/client-discovery.d.ts.map +1 -1
- package/dist/client-discovery.js +26 -0
- package/dist/client-discovery.js.map +1 -1
- package/dist/client-switch-page.d.ts +3 -0
- package/dist/client-switch-page.d.ts.map +1 -0
- package/dist/client-switch-page.js +106 -0
- package/dist/client-switch-page.js.map +1 -0
- package/dist/gate.d.ts +9 -15
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +11 -29
- package/dist/gate.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +128 -148
- package/dist/index.js.map +1 -1
- package/dist/routes/files.js +5 -5
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/marketplace.d.ts +45 -18
- package/dist/routes/marketplace.d.ts.map +1 -1
- package/dist/routes/marketplace.js +141 -84
- package/dist/routes/marketplace.js.map +1 -1
- package/dist/routes/plugins.d.ts.map +1 -1
- package/dist/routes/plugins.js +27 -48
- package/dist/routes/plugins.js.map +1 -1
- package/dist/routes/proxy.d.ts.map +1 -1
- package/dist/routes/proxy.js +8 -25
- package/dist/routes/proxy.js.map +1 -1
- package/dist/routes/uploaded-files.js +3 -3
- package/dist/routes/uploaded-files.js.map +1 -1
- package/package.json +39 -27
package/dist/auth.d.ts
CHANGED
|
@@ -3,38 +3,22 @@ import type { Logger } from '@aalis/core';
|
|
|
3
3
|
import type { UserIdentity } from '@aalis/plugin-authority-api';
|
|
4
4
|
import type { ProcessService } from '@aalis/plugin-process-api';
|
|
5
5
|
import type { RequestHandler } from 'express';
|
|
6
|
-
/**
|
|
7
|
-
* 账户校验器 —— auth 系统对 authority 服务的最小依赖面。
|
|
8
|
-
*
|
|
9
|
-
* 以函数注入而非服务引用,调用方应惰性解析(authority 可能晚于 webui-server
|
|
10
|
-
* 激活或被热替换)。
|
|
11
|
-
*/
|
|
12
|
-
export interface AccountVerifier {
|
|
13
|
-
/** 校验账户密码(无账户/不匹配均 false) */
|
|
14
|
-
verify(username: string, password: string): boolean | Promise<boolean>;
|
|
15
|
-
/** 是否存在任何可登录账户(决定登录页是否显示账户表单) */
|
|
16
|
-
hasAccounts(): boolean;
|
|
17
|
-
}
|
|
18
6
|
export interface AuthSystem {
|
|
19
7
|
/** Express 中间件:处理 ?token= 重定向、登录端点、未认证拦截、登录页 */
|
|
20
8
|
middleware: RequestHandler;
|
|
21
9
|
/** 校验 WebSocket 升级请求是否已认证(cookie 读取) */
|
|
22
10
|
verifyWsClient(req: IncomingMessage): boolean;
|
|
23
|
-
/**
|
|
24
|
-
* 解析请求的调用者身份:账户 session → `webui:<username>`;
|
|
25
|
-
* 单 token 模式 → `webui:console`(owner 级,单人模式语义);未认证 → undefined。
|
|
26
|
-
*/
|
|
11
|
+
/** 解析调用者身份:单 token 模式 → `webui:console`(owner 级,单人语义);未认证 → undefined。 */
|
|
27
12
|
identify(req: Pick<IncomingMessage, 'headers'>): UserIdentity | undefined;
|
|
28
13
|
}
|
|
29
14
|
/**
|
|
30
|
-
*
|
|
15
|
+
* 创建单 token 认证系统(单 owner)。
|
|
31
16
|
*
|
|
32
|
-
* -
|
|
33
|
-
*
|
|
34
|
-
* -
|
|
35
|
-
* - 两者并存时 session 身份优先
|
|
17
|
+
* - token 由调用方传入(参见 index.ts resolveAuthToken,支持 ephemeral / persist / fixed 三种模式)
|
|
18
|
+
* - HttpOnly + SameSite=Strict cookie;`?token=` 一键登录;同源 POST /api/auth/login 提交
|
|
19
|
+
* - 身份恒为 `webui:console`(owner 级,单人模式语义)
|
|
36
20
|
*/
|
|
37
|
-
export declare function createAuthSystem(token: string,
|
|
21
|
+
export declare function createAuthSystem(token: string, _logger: Logger): AuthSystem;
|
|
38
22
|
/**
|
|
39
23
|
* 跨平台打开默认浏览器到指定 URL(无外部依赖)
|
|
40
24
|
* 失败静默;此为便利功能,不影响服务启动。
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AA0E9C,MAAM,WAAW,UAAU;IACzB,gDAAgD;IAChD,UAAU,EAAE,cAAc,CAAC;IAC3B,wCAAwC;IACxC,cAAc,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC;IAC9C,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,CAAC,GAAG,YAAY,GAAG,SAAS,CAAC;CAC3E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,CA+E3E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,IAAI,CAenE"}
|
package/dist/auth.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const TOKEN_COOKIE = 'aalis_webui_token';
|
|
2
|
-
const SESSION_COOKIE = 'aalis_webui_session';
|
|
3
2
|
function parseCookieValue(header, name) {
|
|
4
3
|
if (!header)
|
|
5
4
|
return undefined;
|
|
@@ -25,29 +24,9 @@ function buildSetCookie(name, value, maxAgeSec) {
|
|
|
25
24
|
function buildClearCookie(name) {
|
|
26
25
|
return `${name}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`;
|
|
27
26
|
}
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
*
|
|
31
|
-
* 存在账户时默认显示「账户登录」表单(用户名+密码),token 登录折叠为次选
|
|
32
|
-
* (tokenMode=disabled 时整体隐藏);无账户时只显示 token 表单(兜底,防锁死)。
|
|
33
|
-
* 两种提交都打 POST /api/auth/login。
|
|
34
|
-
*/
|
|
35
|
-
function loginPageHtml(hasAccounts, showToken, reason) {
|
|
27
|
+
/** 登录页:自包含 HTML,不引入外部资源;提交 POST /api/auth/login 后刷新到 / */
|
|
28
|
+
function loginPageHtml(reason) {
|
|
36
29
|
const note = reason ? `<p class="note">${escapeHtml(reason)}</p>` : '';
|
|
37
|
-
const tokenDetails = showToken
|
|
38
|
-
? `<details style="margin-top:14px"><summary>使用访问 token 登录</summary>
|
|
39
|
-
<input id="t" type="password" autocomplete="off" placeholder="访问 token" style="margin-top:8px">
|
|
40
|
-
<button type="button" id="tb" style="background:#2a3038">Token 登录</button>
|
|
41
|
-
</details>`
|
|
42
|
-
: '';
|
|
43
|
-
const accountForm = hasAccounts
|
|
44
|
-
? `<input id="u" type="text" autocomplete="username" placeholder="用户名" autofocus required>
|
|
45
|
-
<input id="p" type="password" autocomplete="current-password" placeholder="密码" required style="margin-top:8px">
|
|
46
|
-
<button type="submit" id="b">登录</button>
|
|
47
|
-
${tokenDetails}`
|
|
48
|
-
: `<p>请输入启动日志或 data/webui/access.txt 中显示的访问 token。</p>
|
|
49
|
-
<input id="t" type="password" autocomplete="off" placeholder="访问 token" autofocus required>
|
|
50
|
-
<button type="submit" id="b">登录</button>`;
|
|
51
30
|
return `<!DOCTYPE html><html lang="zh-CN"><head>
|
|
52
31
|
<meta charset="utf-8"><title>Aalis WebUI 登录</title>
|
|
53
32
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
@@ -59,155 +38,68 @@ body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:c
|
|
|
59
38
|
h1{margin:0 0 8px;font-size:22px;font-weight:600}
|
|
60
39
|
p{margin:0 0 16px;color:#8b95a1;font-size:14px;line-height:1.5}
|
|
61
40
|
p.note{color:#ffb300}
|
|
62
|
-
summary{color:#8b95a1;font-size:13px;cursor:pointer}
|
|
63
41
|
input{width:100%;padding:10px 12px;font-size:14px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#0b0d10;color:#e6e8eb;border:1px solid #2a3038;border-radius:6px;outline:none}
|
|
64
42
|
input:focus{border-color:#5b8def}
|
|
65
43
|
button{width:100%;margin-top:12px;padding:10px;font-size:14px;font-weight:500;background:#5b8def;color:#fff;border:0;border-radius:6px;cursor:pointer}
|
|
66
|
-
button:hover{
|
|
44
|
+
button:hover{background:#4a7de0}
|
|
67
45
|
button:disabled{opacity:.5;cursor:wait}
|
|
68
46
|
.err{margin-top:10px;color:#ff6b6b;font-size:13px;min-height:18px}
|
|
69
47
|
</style></head>
|
|
70
48
|
<body><form class="card" id="f">
|
|
71
49
|
<h1>Aalis WebUI</h1>
|
|
50
|
+
<p>请输入启动日志或 data/webui/access.txt 中显示的访问 token。</p>
|
|
72
51
|
${note}
|
|
73
|
-
|
|
52
|
+
<input id="t" type="password" autocomplete="off" placeholder="访问 token" autofocus required>
|
|
53
|
+
<button type="submit" id="b">登录</button>
|
|
74
54
|
<div class="err" id="e"></div>
|
|
75
55
|
</form>
|
|
76
56
|
<script>
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
e.textContent='';btn.disabled=true;
|
|
57
|
+
const f=document.getElementById('f'),t=document.getElementById('t'),b=document.getElementById('b'),e=document.getElementById('e');
|
|
58
|
+
f.addEventListener('submit',async ev=>{
|
|
59
|
+
ev.preventDefault();e.textContent='';b.disabled=true;
|
|
81
60
|
try{
|
|
82
|
-
const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(
|
|
83
|
-
if(r.ok){location.replace('/');}else{const j=await r.json().catch(()=>({}));e.textContent=j.error||'
|
|
84
|
-
}catch(err){e.textContent='网络错误';
|
|
85
|
-
}
|
|
86
|
-
f.addEventListener('submit',ev=>{
|
|
87
|
-
ev.preventDefault();
|
|
88
|
-
const u=$('u'),t=$('t');
|
|
89
|
-
if(u&&u.value.trim()){login({username:u.value.trim(),password:$('p').value},$('b'));}
|
|
90
|
-
else if(t&&t.value.trim()){login({token:t.value.trim()},$('b'));}
|
|
61
|
+
const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:t.value.trim()})});
|
|
62
|
+
if(r.ok){location.replace('/');}else{const j=await r.json().catch(()=>({}));e.textContent=j.error||'token 不正确';b.disabled=false;t.select();}
|
|
63
|
+
}catch(err){e.textContent='网络错误';b.disabled=false;}
|
|
91
64
|
});
|
|
92
|
-
const tb=$('tb');
|
|
93
|
-
if(tb)tb.addEventListener('click',()=>{const t=$('t');if(t.value.trim())login({token:t.value.trim()},tb);});
|
|
94
65
|
</script></body></html>`;
|
|
95
66
|
}
|
|
96
67
|
function escapeHtml(s) {
|
|
97
68
|
return s.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
98
69
|
}
|
|
99
|
-
const MAX_FAILURES = 5;
|
|
100
|
-
const LOCK_MS = 60_000;
|
|
101
70
|
/**
|
|
102
|
-
*
|
|
71
|
+
* 创建单 token 认证系统(单 owner)。
|
|
103
72
|
*
|
|
104
|
-
* -
|
|
105
|
-
*
|
|
106
|
-
* -
|
|
107
|
-
* - 两者并存时 session 身份优先
|
|
73
|
+
* - token 由调用方传入(参见 index.ts resolveAuthToken,支持 ephemeral / persist / fixed 三种模式)
|
|
74
|
+
* - HttpOnly + SameSite=Strict cookie;`?token=` 一键登录;同源 POST /api/auth/login 提交
|
|
75
|
+
* - 身份恒为 `webui:console`(owner 级,单人模式语义)
|
|
108
76
|
*/
|
|
109
|
-
export function createAuthSystem(token,
|
|
110
|
-
const cookieMaxAge = 30 * 24 * 3600; // 30d;进程重启 token
|
|
111
|
-
const sessions = new Map();
|
|
112
|
-
const failures = new Map();
|
|
113
|
-
/** token 登录是否可用:宿主开关(tokenMode!=disabled)|| 无账户兜底(防锁死) */
|
|
114
|
-
function tokenAllowed() {
|
|
115
|
-
return tokenLogin() || !(accounts?.hasAccounts() ?? false);
|
|
116
|
-
}
|
|
117
|
-
function newSession(username) {
|
|
118
|
-
// 顺带清扫过期 session(登录是低频操作,无需独立 timer——审计 MEDIUM #10)
|
|
119
|
-
const now = Date.now();
|
|
120
|
-
for (const [id, s] of sessions)
|
|
121
|
-
if (s.expiresAt <= now)
|
|
122
|
-
sessions.delete(id);
|
|
123
|
-
const id = Buffer.from(crypto.getRandomValues(new Uint8Array(24))).toString('hex');
|
|
124
|
-
sessions.set(id, { username, expiresAt: now + cookieMaxAge * 1000 });
|
|
125
|
-
return id;
|
|
126
|
-
}
|
|
127
|
-
function sessionUser(req) {
|
|
128
|
-
const id = parseCookieValue(req.headers.cookie, SESSION_COOKIE);
|
|
129
|
-
if (!id)
|
|
130
|
-
return undefined;
|
|
131
|
-
const s = sessions.get(id);
|
|
132
|
-
if (!s)
|
|
133
|
-
return undefined;
|
|
134
|
-
if (s.expiresAt <= Date.now()) {
|
|
135
|
-
sessions.delete(id);
|
|
136
|
-
return undefined;
|
|
137
|
-
}
|
|
138
|
-
return s.username;
|
|
139
|
-
}
|
|
77
|
+
export function createAuthSystem(token, _logger) {
|
|
78
|
+
const cookieMaxAge = 30 * 24 * 3600; // 30d;进程重启 token 轮换后无效
|
|
140
79
|
function tokenAuthed(req) {
|
|
141
|
-
if (!tokenAllowed())
|
|
142
|
-
return false;
|
|
143
80
|
const cookieToken = parseCookieValue(req.headers.cookie, TOKEN_COOKIE);
|
|
144
81
|
return !!cookieToken && cookieToken === token;
|
|
145
82
|
}
|
|
146
83
|
function identify(req) {
|
|
147
|
-
|
|
148
|
-
if (username)
|
|
149
|
-
return { platform: 'webui', userId: username };
|
|
150
|
-
if (tokenAuthed(req))
|
|
151
|
-
return { platform: 'webui', userId: 'console' };
|
|
152
|
-
return undefined;
|
|
153
|
-
}
|
|
154
|
-
function checkLocked(username) {
|
|
155
|
-
const rec = failures.get(username);
|
|
156
|
-
return !!rec && rec.count >= MAX_FAILURES && rec.lockedUntil > Date.now();
|
|
157
|
-
}
|
|
158
|
-
function recordFailure(username) {
|
|
159
|
-
const rec = failures.get(username);
|
|
160
|
-
if (rec && rec.lockedUntil > Date.now()) {
|
|
161
|
-
rec.count += 1;
|
|
162
|
-
rec.lockedUntil = Date.now() + LOCK_MS;
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
failures.set(username, { count: 1, lockedUntil: Date.now() + LOCK_MS });
|
|
166
|
-
}
|
|
84
|
+
return tokenAuthed(req) ? { platform: 'webui', userId: 'console' } : undefined;
|
|
167
85
|
}
|
|
168
|
-
const middleware =
|
|
86
|
+
const middleware = (req, res, next) => {
|
|
169
87
|
const url = req.path;
|
|
170
88
|
const qToken = typeof req.query?.token === 'string' ? req.query.token : undefined;
|
|
171
|
-
const hasAccounts = accounts?.hasAccounts() ?? false;
|
|
172
89
|
// 1. ?token= 命中 → 设置 cookie → 302 到不带 query 的同路径
|
|
173
|
-
if (req.method === 'GET' && qToken
|
|
90
|
+
if (req.method === 'GET' && qToken) {
|
|
174
91
|
if (qToken === token) {
|
|
175
92
|
res.setHeader('Set-Cookie', buildSetCookie(TOKEN_COOKIE, token, cookieMaxAge));
|
|
176
93
|
res.redirect(302, url);
|
|
177
94
|
return;
|
|
178
95
|
}
|
|
179
|
-
|
|
180
|
-
res
|
|
181
|
-
.status(401)
|
|
182
|
-
.type('html')
|
|
183
|
-
.send(loginPageHtml(hasAccounts, true, 'URL 中的 token 不正确,请手动输入。'));
|
|
96
|
+
res.status(401).type('html').send(loginPageHtml('URL 中的 token 不正确,请手动输入。'));
|
|
184
97
|
return;
|
|
185
98
|
}
|
|
186
|
-
// 2. 登录端点:{
|
|
99
|
+
// 2. 登录端点:{token}
|
|
187
100
|
if (url === '/api/auth/login' && req.method === 'POST') {
|
|
188
101
|
const body = req.body ?? {};
|
|
189
|
-
if (typeof body.
|
|
190
|
-
const username = body.username.trim();
|
|
191
|
-
if (checkLocked(username)) {
|
|
192
|
-
res.status(429).json({ ok: false, error: '尝试次数过多,请稍后再试' });
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
if (username && (await accounts?.verify(username, body.password))) {
|
|
196
|
-
failures.delete(username);
|
|
197
|
-
res.setHeader('Set-Cookie', buildSetCookie(SESSION_COOKIE, newSession(username), cookieMaxAge));
|
|
198
|
-
logger.info(`账户登录: webui:${username}`);
|
|
199
|
-
res.json({ ok: true, identity: { platform: 'webui', userId: username } });
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
recordFailure(username);
|
|
203
|
-
res.status(401).json({ ok: false, error: '用户名或密码不正确' });
|
|
204
|
-
}
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
if (typeof body.token === 'string' && !tokenAllowed()) {
|
|
208
|
-
res.status(401).json({ ok: false, error: 'token 登录已禁用,请使用账户密码' });
|
|
209
|
-
}
|
|
210
|
-
else if (typeof body.token === 'string' && body.token === token) {
|
|
102
|
+
if (typeof body.token === 'string' && body.token === token) {
|
|
211
103
|
res.setHeader('Set-Cookie', buildSetCookie(TOKEN_COOKIE, token, cookieMaxAge));
|
|
212
104
|
res.json({ ok: true, identity: { platform: 'webui', userId: 'console' } });
|
|
213
105
|
}
|
|
@@ -222,17 +114,14 @@ export function createAuthSystem(token, logger, accounts, tokenLogin = () => tru
|
|
|
222
114
|
res.json({ authed: !!identity, identity });
|
|
223
115
|
return;
|
|
224
116
|
}
|
|
225
|
-
// 4. 注销:清
|
|
117
|
+
// 4. 注销:清 token cookie
|
|
226
118
|
if (url === '/api/auth/logout' && req.method === 'POST') {
|
|
227
|
-
|
|
228
|
-
if (sid)
|
|
229
|
-
sessions.delete(sid);
|
|
230
|
-
res.setHeader('Set-Cookie', [buildClearCookie(SESSION_COOKIE), buildClearCookie(TOKEN_COOKIE)]);
|
|
119
|
+
res.setHeader('Set-Cookie', buildClearCookie(TOKEN_COOKIE));
|
|
231
120
|
res.json({ ok: true });
|
|
232
121
|
return;
|
|
233
122
|
}
|
|
234
123
|
// 5. 已认证 → 放行
|
|
235
|
-
if (
|
|
124
|
+
if (tokenAuthed(req)) {
|
|
236
125
|
next();
|
|
237
126
|
return;
|
|
238
127
|
}
|
|
@@ -243,13 +132,13 @@ export function createAuthSystem(token, logger, accounts, tokenLogin = () => tru
|
|
|
243
132
|
}
|
|
244
133
|
// 7. 未认证 GET:返回内联登录页(覆盖 SPA shell,不暴露任何静态资源)
|
|
245
134
|
if (req.method === 'GET') {
|
|
246
|
-
res.status(401).type('html').send(loginPageHtml(
|
|
135
|
+
res.status(401).type('html').send(loginPageHtml());
|
|
247
136
|
return;
|
|
248
137
|
}
|
|
249
138
|
res.status(401).end();
|
|
250
139
|
};
|
|
251
140
|
function verifyWsClient(req) {
|
|
252
|
-
return
|
|
141
|
+
return tokenAuthed(req);
|
|
253
142
|
}
|
|
254
143
|
return { middleware, verifyWsClient, identify };
|
|
255
144
|
}
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAMA,MAAM,YAAY,GAAG,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,5 +20,13 @@ export interface DiscoveryEnv {
|
|
|
20
20
|
* 默认活跃前端由调用方按 config 或「排序后第一个」决定——本函数不做偏好判断、不认任何具体名字。
|
|
21
21
|
*/
|
|
22
22
|
export declare function discoverClients(scanDirs: string[], depIds: string[], env: DiscoveryEnv): ClientCandidate[];
|
|
23
|
+
/**
|
|
24
|
+
* 扫描本地物理存在的包 → `name → 依赖名[]`(与 discoverClients 共用 scanDirs/env)。
|
|
25
|
+
* 两用,一次扫盘:① keys 即「本地已装包名」,补市场已装判定(require.resolve 在 pnpm 工作区
|
|
26
|
+
* 从仓库根解析不到未提升的工作区包);② values 即各包 `dependencies/peer/optional` 的并集名单,
|
|
27
|
+
* 供依赖图(谁依赖谁)。**只取依赖名、不看版本区间**(`workspace:^`/`^1.0.0` 一律按名),故无需 semver。
|
|
28
|
+
* scope 无关(不挑 @aalis)。纯函数便于单测。
|
|
29
|
+
*/
|
|
30
|
+
export declare function collectLocalPackageDeps(scanDirs: string[], env: Pick<DiscoveryEnv, 'existsSync' | 'readdirSync' | 'readJson' | 'join'>): Map<string, string[]>;
|
|
23
31
|
export {};
|
|
24
32
|
//# sourceMappingURL=client-discovery.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-discovery.d.ts","sourceRoot":"","sources":["../src/client-discovery.ts"],"names":[],"mappings":"AAMA,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IACnC,qBAAqB;IACrB,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IACrC,oCAAoC;IACpC,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IACjC,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,KAAK,MAAM,CAAC;IACrC,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/B,mDAAmD;IACnD,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CACpD;AASD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,YAAY,GAAG,eAAe,EAAE,CA2B1G"}
|
|
1
|
+
{"version":3,"file":"client-discovery.d.ts","sourceRoot":"","sources":["../src/client-discovery.ts"],"names":[],"mappings":"AAMA,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IACnC,qBAAqB;IACrB,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IACrC,oCAAoC;IACpC,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IACjC,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,KAAK,MAAM,CAAC;IACrC,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/B,mDAAmD;IACnD,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CACpD;AASD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,YAAY,GAAG,eAAe,EAAE,CA2B1G;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAAE,EAClB,GAAG,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,GAAG,aAAa,GAAG,UAAU,GAAG,MAAM,CAAC,GAC1E,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAuBvB"}
|
package/dist/client-discovery.js
CHANGED
|
@@ -38,4 +38,30 @@ export function discoverClients(scanDirs, depIds, env) {
|
|
|
38
38
|
consider(env.resolvePkgJson(id));
|
|
39
39
|
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* 扫描本地物理存在的包 → `name → 依赖名[]`(与 discoverClients 共用 scanDirs/env)。
|
|
43
|
+
* 两用,一次扫盘:① keys 即「本地已装包名」,补市场已装判定(require.resolve 在 pnpm 工作区
|
|
44
|
+
* 从仓库根解析不到未提升的工作区包);② values 即各包 `dependencies/peer/optional` 的并集名单,
|
|
45
|
+
* 供依赖图(谁依赖谁)。**只取依赖名、不看版本区间**(`workspace:^`/`^1.0.0` 一律按名),故无需 semver。
|
|
46
|
+
* scope 无关(不挑 @aalis)。纯函数便于单测。
|
|
47
|
+
*/
|
|
48
|
+
export function collectLocalPackageDeps(scanDirs, env) {
|
|
49
|
+
const out = new Map();
|
|
50
|
+
for (const base of scanDirs) {
|
|
51
|
+
if (!env.existsSync(base))
|
|
52
|
+
continue;
|
|
53
|
+
for (const entry of env.readdirSync(base)) {
|
|
54
|
+
const pkg = env.readJson(env.join(base, entry, 'package.json'));
|
|
55
|
+
if (!pkg || typeof pkg.name !== 'string')
|
|
56
|
+
continue;
|
|
57
|
+
const deps = new Set([
|
|
58
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
59
|
+
...Object.keys(pkg.peerDependencies ?? {}),
|
|
60
|
+
...Object.keys(pkg.optionalDependencies ?? {}),
|
|
61
|
+
]);
|
|
62
|
+
out.set(pkg.name, [...deps]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
41
67
|
//# sourceMappingURL=client-discovery.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-discovery.js","sourceRoot":"","sources":["../src/client-discovery.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,EAAE;AACF,0EAA0E;AAC1E,2DAA2D;AAC3D,8EAA8E;AA2B9E;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,QAAkB,EAAE,MAAgB,EAAE,GAAiB;IACrF,MAAM,GAAG,GAAsB,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAE/B,MAAM,QAAQ,GAAG,CAAC,WAA+B,EAAQ,EAAE;QACzD,IAAI,CAAC,WAAW,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC;YAAE,OAAO;QACzD,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAwB,CAAC;QAC7D,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO;QACrG,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC;QACvD,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;YAAE,OAAO;QACzD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACnB,MAAM,KAAK,GACT,CAAC,OAAO,GAAG,CAAC,WAAW,KAAK,QAAQ,IAAI,GAAG,CAAC,WAAW,CAAC;YACxD,CAAC,OAAO,GAAG,CAAC,WAAW,KAAK,QAAQ,IAAI,GAAG,CAAC,WAAW,CAAC;YACxD,GAAG,CAAC,IAAI,CAAC;QACX,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IACzC,CAAC,CAAC;IAEF,kEAAkE;IAClE,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,SAAS;QACpC,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC;YAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC;IAC7F,CAAC;IACD,wDAAwD;IACxD,KAAK,MAAM,EAAE,IAAI,MAAM;QAAE,QAAQ,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC;IAE1D,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACtD,CAAC"}
|
|
1
|
+
{"version":3,"file":"client-discovery.js","sourceRoot":"","sources":["../src/client-discovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-switch-page.d.ts","sourceRoot":"","sources":["../src/client-switch-page.ts"],"names":[],"mappings":"AAYA,kDAAkD;AAClD,wBAAgB,sBAAsB,IAAI,MAAM,CA4F/C"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// 前端切换「逃生页」——由 webui-server 直出的极简恢复页,独立于任何「可被切换的前端」。
|
|
2
|
+
//
|
|
3
|
+
// 背景:前端切换统一走「服务偏好」下拉框,而该下拉框住在前端里。一旦切到不含该 UI 的极简
|
|
4
|
+
// 前端(如第三方裸前端),就没有切回去的入口(只能改 aalis.config.yaml + 重启)。本页由
|
|
5
|
+
// webui-server 直接托管在 `/__clients`,无论当前活跃前端多裸都可达,是「永不卡死」的恢复入口。
|
|
6
|
+
//
|
|
7
|
+
// 零新增后端逻辑:纯静态 HTML + 同源 fetch 复用既有接口(同源 cookie 自动鉴权)——
|
|
8
|
+
// · 列表:GET /api/services (取 webui-client 服务的 providers + preferred)
|
|
9
|
+
// · 切换:POST /api/services/webui-client/prefer (owner 闸,body {contextId})
|
|
10
|
+
// 全局 auth 中间件已要求登录方可到达本路由(未认证 GET 直接回登录页),故本页无需自加 gate;
|
|
11
|
+
// 切换动作仍受既有 owner 闸约束。抽成纯函数仅为便于单测——route 只 res.type('html').send(它)。
|
|
12
|
+
/** 渲染前端切换恢复页的完整 HTML 文档(无运行时入参,数据由页面自身 fetch)。 */
|
|
13
|
+
export function renderClientSwitchPage() {
|
|
14
|
+
return `<!doctype html>
|
|
15
|
+
<html lang="zh-CN">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="utf-8" />
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
19
|
+
<title>切换前端 · Aalis</title>
|
|
20
|
+
<style>
|
|
21
|
+
:root { color-scheme: light dark; }
|
|
22
|
+
body { font: 15px/1.6 system-ui, -apple-system, sans-serif; max-width: 32rem; margin: 8vh auto; padding: 0 1.2rem; }
|
|
23
|
+
h1 { font-size: 1.25rem; margin: 0 0 .25rem; }
|
|
24
|
+
p.hint { color: #888; margin: .25rem 0 1.25rem; }
|
|
25
|
+
label { display: block; font-weight: 600; margin-bottom: .4rem; }
|
|
26
|
+
select, button { font: inherit; padding: .55rem .7rem; border-radius: .5rem; }
|
|
27
|
+
select { width: 100%; box-sizing: border-box; }
|
|
28
|
+
button { margin-top: 1rem; cursor: pointer; border: 1px solid currentColor; background: transparent; }
|
|
29
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
30
|
+
#msg { margin-top: 1rem; min-height: 1.4em; }
|
|
31
|
+
#msg.err { color: #c0392b; }
|
|
32
|
+
#msg.ok { color: #2e7d32; }
|
|
33
|
+
code { font-family: ui-monospace, monospace; }
|
|
34
|
+
</style>
|
|
35
|
+
</head>
|
|
36
|
+
<body>
|
|
37
|
+
<h1>切换前端</h1>
|
|
38
|
+
<p class="hint">这是 Aalis 的前端恢复入口,由 webui-server 直接提供、独立于当前前端——即使当前前端没有切换界面,你也能在这里切回去。</p>
|
|
39
|
+
<label for="client">可用前端(webui-client)</label>
|
|
40
|
+
<select id="client"><option>加载中…</option></select>
|
|
41
|
+
<button id="go" type="button">切换并刷新</button>
|
|
42
|
+
<div id="msg"></div>
|
|
43
|
+
<p class="hint">切回原来的前端:在上面选中它,再点「切换并刷新」。</p>
|
|
44
|
+
<script>
|
|
45
|
+
(function () {
|
|
46
|
+
var SVC = 'webui-client';
|
|
47
|
+
var PREFER = '/api/services/' + SVC + '/prefer';
|
|
48
|
+
var sel = document.getElementById('client');
|
|
49
|
+
var btn = document.getElementById('go');
|
|
50
|
+
var msg = document.getElementById('msg');
|
|
51
|
+
function setMsg(t, err) { msg.textContent = t; msg.className = err ? 'err' : 'ok'; }
|
|
52
|
+
function load() {
|
|
53
|
+
fetch('/api/services', { headers: { Accept: 'application/json' } })
|
|
54
|
+
.then(function (r) {
|
|
55
|
+
if (!r.ok) throw new Error('读取服务列表失败(HTTP ' + r.status + ')');
|
|
56
|
+
return r.json();
|
|
57
|
+
})
|
|
58
|
+
.then(function (data) {
|
|
59
|
+
var svc = data && data.services && data.services[SVC];
|
|
60
|
+
var providers = (svc && svc.providers) || [];
|
|
61
|
+
if (!providers.length) {
|
|
62
|
+
setMsg('未发现任何前端(webui-client provider)。请先安装一个 aalis.client 包。', true);
|
|
63
|
+
btn.disabled = true;
|
|
64
|
+
sel.innerHTML = '';
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
var active = (svc && svc.preferred) || providers[0].contextId;
|
|
68
|
+
sel.innerHTML = '';
|
|
69
|
+
providers.forEach(function (p) {
|
|
70
|
+
var o = document.createElement('option');
|
|
71
|
+
o.value = p.contextId;
|
|
72
|
+
o.textContent = (p.displayName || p.label || p.contextId) + (p.contextId === active ? '(当前)' : '');
|
|
73
|
+
if (p.contextId === active) o.selected = true;
|
|
74
|
+
sel.appendChild(o);
|
|
75
|
+
});
|
|
76
|
+
})
|
|
77
|
+
.catch(function (e) { setMsg(String((e && e.message) || e), true); });
|
|
78
|
+
}
|
|
79
|
+
btn.addEventListener('click', function () {
|
|
80
|
+
var contextId = sel.value;
|
|
81
|
+
if (!contextId) return;
|
|
82
|
+
btn.disabled = true;
|
|
83
|
+
setMsg('切换中…', false);
|
|
84
|
+
fetch(PREFER, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'Content-Type': 'application/json' },
|
|
87
|
+
body: JSON.stringify({ contextId: contextId })
|
|
88
|
+
})
|
|
89
|
+
.then(function (r) {
|
|
90
|
+
if (r.ok) { setMsg('已切换,正在跳转…', false); location.href = '/'; return; }
|
|
91
|
+
return r.json().catch(function () { return {}; }).then(function (j) {
|
|
92
|
+
var detail = (j && j.error) || ('HTTP ' + r.status);
|
|
93
|
+
var hint = (r.status === 401 || r.status === 403) ? '(需要 owner 权限:请用一键登录链接打开 WebUI 后再试)' : '';
|
|
94
|
+
setMsg('切换失败:' + detail + hint, true);
|
|
95
|
+
btn.disabled = false;
|
|
96
|
+
});
|
|
97
|
+
})
|
|
98
|
+
.catch(function (e) { setMsg('切换请求出错:' + String((e && e.message) || e), true); btn.disabled = false; });
|
|
99
|
+
});
|
|
100
|
+
load();
|
|
101
|
+
})();
|
|
102
|
+
</script>
|
|
103
|
+
</body>
|
|
104
|
+
</html>`;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=client-switch-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-switch-page.js","sourceRoot":"","sources":["../src/client-switch-page.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,EAAE;AACF,gDAAgD;AAChD,wDAAwD;AACxD,8DAA8D;AAC9D,EAAE;AACF,uDAAuD;AACvD,6FAA6F;AAC7F,8EAA8E;AAC9E,wDAAwD;AACxD,oEAAoE;AAEpE,kDAAkD;AAClD,MAAM,UAAU,sBAAsB;IACpC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA0FD,CAAC;AACT,CAAC"}
|
package/dist/gate.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { CapabilityVisibility, UserIdentity } from '@aalis/plugin-authority-api';
|
|
1
|
+
import type { UserIdentity } from '@aalis/plugin-authority-api';
|
|
3
2
|
/** 从请求解析调用者身份(auth.ts 的 AuthSystem.identify) */
|
|
4
3
|
type IdentifyFn = (req: {
|
|
5
4
|
headers: {
|
|
@@ -18,22 +17,17 @@ interface GateResponse {
|
|
|
18
17
|
}
|
|
19
18
|
type GateMiddleware = (req: GateRequest, res: GateResponse, next: () => void) => void;
|
|
20
19
|
/**
|
|
21
|
-
* REST
|
|
20
|
+
* REST 路由 owner 闸工厂(单 owner 终态)。
|
|
22
21
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* restricted capability(如 `webui:files:read`)做细粒度委托。
|
|
22
|
+
* 单 token 模式下「持 token ⟺ webui:console ⟺ owner」,故连接级认证(auth.middleware
|
|
23
|
+
* 校验 token,未认证一律 401)即等价于 owner 鉴权。本闸是 REST 层的显式 owner 复核
|
|
24
|
+
* (防御纵深,与 auth.middleware 同义):身份解析得到即放行,否则 403。
|
|
27
25
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* 一切变更操作(配置写、插件启停装卸、服务偏好、文件删改);默认禁,须 owner /
|
|
32
|
-
* 被委托授予。
|
|
33
|
-
*
|
|
34
|
-
* authority 服务缺席时:public 放行、restricted 503(fail-closed,不在权限服务缺位时裸奔)。
|
|
26
|
+
* 多账户 / 能力委托已剥离,故不再有 per-route capability 与档位裁决——所有需登录的
|
|
27
|
+
* REST 路由统一是 owner-only。页面/状态等只读路由同样在 auth.middleware 之后,对
|
|
28
|
+
* 单 owner 无区别。
|
|
35
29
|
*/
|
|
36
|
-
export declare function createRouteGate(
|
|
30
|
+
export declare function createRouteGate(identify: IdentifyFn): () => GateMiddleware;
|
|
37
31
|
/** createRouteGate 返回的闸生成函数类型(传给各 routes/ 注册器) */
|
|
38
32
|
export type RouteGate = ReturnType<typeof createRouteGate>;
|
|
39
33
|
export {};
|
package/dist/gate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAEhE,gDAAgD;AAChD,KAAK,UAAU,GAAG,CAAC,GAAG,EAAE;IAAE,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,KAAK,YAAY,GAAG,SAAS,CAAC;AAKtF,UAAU,WAAW;IACnB,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9B;AACD,UAAU,YAAY;IACpB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;KAAE,CAAC;CACxD;AACD,KAAK,cAAc,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;AAEtF;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,UAAU,SACvC,cAAc,CAO1B;AAED,kDAAkD;AAClD,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC"}
|
package/dist/gate.js
CHANGED
|
@@ -1,36 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* REST
|
|
2
|
+
* REST 路由 owner 闸工厂(单 owner 终态)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* restricted capability(如 `webui:files:read`)做细粒度委托。
|
|
4
|
+
* 单 token 模式下「持 token ⟺ webui:console ⟺ owner」,故连接级认证(auth.middleware
|
|
5
|
+
* 校验 token,未认证一律 401)即等价于 owner 鉴权。本闸是 REST 层的显式 owner 复核
|
|
6
|
+
* (防御纵深,与 auth.middleware 同义):身份解析得到即放行,否则 403。
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 一切变更操作(配置写、插件启停装卸、服务偏好、文件删改);默认禁,须 owner /
|
|
13
|
-
* 被委托授予。
|
|
14
|
-
*
|
|
15
|
-
* authority 服务缺席时:public 放行、restricted 503(fail-closed,不在权限服务缺位时裸奔)。
|
|
8
|
+
* 多账户 / 能力委托已剥离,故不再有 per-route capability 与档位裁决——所有需登录的
|
|
9
|
+
* REST 路由统一是 owner-only。页面/状态等只读路由同样在 auth.middleware 之后,对
|
|
10
|
+
* 单 owner 无区别。
|
|
16
11
|
*/
|
|
17
|
-
export function createRouteGate(
|
|
18
|
-
return (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// authority 缺席(未配置/启动失败/bounce 窗口期)→ 无法裁决。
|
|
22
|
-
// 公共读放行,受限操作 503(区别于 403:服务暂不可用,非权限不足)。
|
|
23
|
-
if (visibility === 'restricted') {
|
|
24
|
-
res.status(503).json({ error: '权限服务不可用,受限操作暂时被拒绝' });
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
next();
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const caller = identify(req) ?? { platform: 'webui', userId: 'console' };
|
|
31
|
-
const denied = authority.authorize(caller, { capability, visibility });
|
|
32
|
-
if (denied) {
|
|
33
|
-
res.status(403).json({ error: denied });
|
|
12
|
+
export function createRouteGate(identify) {
|
|
13
|
+
return () => (req, res, next) => {
|
|
14
|
+
if (!identify(req)) {
|
|
15
|
+
res.status(403).json({ error: '需要 owner 权限' });
|
|
34
16
|
return;
|
|
35
17
|
}
|
|
36
18
|
next();
|