@aalis/plugin-webui-server 0.14.1 → 0.16.0

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.
Files changed (118) hide show
  1. package/README.md +5 -1
  2. package/dist/admin-channel.d.ts +10 -0
  3. package/dist/admin-channel.d.ts.map +1 -0
  4. package/dist/admin-channel.js +57 -0
  5. package/dist/admin-channel.js.map +1 -0
  6. package/dist/attention.d.ts +36 -0
  7. package/dist/attention.d.ts.map +1 -0
  8. package/dist/attention.js +74 -0
  9. package/dist/attention.js.map +1 -0
  10. package/dist/auth.d.ts +32 -4
  11. package/dist/auth.d.ts.map +1 -1
  12. package/dist/auth.js +253 -79
  13. package/dist/auth.js.map +1 -1
  14. package/dist/client-discovery.d.ts +7 -3
  15. package/dist/client-discovery.d.ts.map +1 -1
  16. package/dist/client-discovery.js +9 -3
  17. package/dist/client-discovery.js.map +1 -1
  18. package/dist/client-switch-page.d.ts +26 -2
  19. package/dist/client-switch-page.d.ts.map +1 -1
  20. package/dist/client-switch-page.js +76 -45
  21. package/dist/client-switch-page.js.map +1 -1
  22. package/dist/gate.d.ts +3 -3
  23. package/dist/gate.d.ts.map +1 -1
  24. package/dist/gate.js +5 -4
  25. package/dist/gate.js.map +1 -1
  26. package/dist/html.d.ts +15 -0
  27. package/dist/html.d.ts.map +1 -0
  28. package/dist/html.js +38 -0
  29. package/dist/html.js.map +1 -0
  30. package/dist/index.d.ts +24 -26
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +627 -1102
  33. package/dist/index.js.map +1 -1
  34. package/dist/layout.d.ts +19 -0
  35. package/dist/layout.d.ts.map +1 -0
  36. package/dist/layout.js +200 -0
  37. package/dist/layout.js.map +1 -0
  38. package/dist/locales/en.d.ts +191 -0
  39. package/dist/locales/en.d.ts.map +1 -0
  40. package/dist/locales/en.js +191 -0
  41. package/dist/locales/en.js.map +1 -0
  42. package/dist/localize.d.ts +24 -0
  43. package/dist/localize.d.ts.map +1 -0
  44. package/dist/localize.js +68 -0
  45. package/dist/localize.js.map +1 -0
  46. package/dist/logs.d.ts +10 -0
  47. package/dist/logs.d.ts.map +1 -0
  48. package/dist/logs.js +50 -0
  49. package/dist/logs.js.map +1 -0
  50. package/dist/page-check.d.ts +56 -0
  51. package/dist/page-check.d.ts.map +1 -0
  52. package/dist/page-check.js +376 -0
  53. package/dist/page-check.js.map +1 -0
  54. package/dist/page-files.d.ts +13 -0
  55. package/dist/page-files.d.ts.map +1 -0
  56. package/dist/page-files.js +39 -0
  57. package/dist/page-files.js.map +1 -0
  58. package/dist/pages.d.ts +62 -0
  59. package/dist/pages.d.ts.map +1 -0
  60. package/dist/pages.js +486 -0
  61. package/dist/pages.js.map +1 -0
  62. package/dist/route.d.ts +14 -0
  63. package/dist/route.d.ts.map +1 -0
  64. package/dist/route.js +16 -0
  65. package/dist/route.js.map +1 -0
  66. package/dist/routes/files.d.ts +8 -9
  67. package/dist/routes/files.d.ts.map +1 -1
  68. package/dist/routes/files.js +33 -119
  69. package/dist/routes/files.js.map +1 -1
  70. package/dist/routes/frames.d.ts +4 -0
  71. package/dist/routes/frames.d.ts.map +1 -0
  72. package/dist/routes/frames.js +103 -0
  73. package/dist/routes/frames.js.map +1 -0
  74. package/dist/routes/i18n.d.ts +20 -0
  75. package/dist/routes/i18n.d.ts.map +1 -0
  76. package/dist/routes/i18n.js +78 -0
  77. package/dist/routes/i18n.js.map +1 -0
  78. package/dist/routes/marketplace.d.ts +46 -129
  79. package/dist/routes/marketplace.d.ts.map +1 -1
  80. package/dist/routes/marketplace.js +156 -106
  81. package/dist/routes/marketplace.js.map +1 -1
  82. package/dist/routes/media.d.ts +10 -0
  83. package/dist/routes/media.d.ts.map +1 -0
  84. package/dist/routes/media.js +66 -0
  85. package/dist/routes/media.js.map +1 -0
  86. package/dist/routes/pages.d.ts +22 -0
  87. package/dist/routes/pages.d.ts.map +1 -0
  88. package/dist/routes/pages.js +104 -0
  89. package/dist/routes/pages.js.map +1 -0
  90. package/dist/routes/pipe-to-response.d.ts +9 -0
  91. package/dist/routes/pipe-to-response.d.ts.map +1 -0
  92. package/dist/routes/pipe-to-response.js +16 -0
  93. package/dist/routes/pipe-to-response.js.map +1 -0
  94. package/dist/routes/plugins.d.ts +22 -28
  95. package/dist/routes/plugins.d.ts.map +1 -1
  96. package/dist/routes/plugins.js +339 -344
  97. package/dist/routes/plugins.js.map +1 -1
  98. package/dist/routes/proxy.d.ts +3 -2
  99. package/dist/routes/proxy.d.ts.map +1 -1
  100. package/dist/routes/proxy.js +64 -42
  101. package/dist/routes/proxy.js.map +1 -1
  102. package/dist/session-transport.d.ts +6 -0
  103. package/dist/session-transport.d.ts.map +1 -0
  104. package/dist/session-transport.js +32 -0
  105. package/dist/session-transport.js.map +1 -0
  106. package/dist/texts.d.ts +381 -0
  107. package/dist/texts.d.ts.map +1 -0
  108. package/dist/texts.js +194 -0
  109. package/dist/texts.js.map +1 -0
  110. package/package.json +38 -32
  111. package/dist/protocol.d.ts +0 -113
  112. package/dist/protocol.d.ts.map +0 -1
  113. package/dist/protocol.js +0 -59
  114. package/dist/protocol.js.map +0 -1
  115. package/dist/routes/uploaded-files.d.ts +0 -20
  116. package/dist/routes/uploaded-files.d.ts.map +0 -1
  117. package/dist/routes/uploaded-files.js +0 -161
  118. package/dist/routes/uploaded-files.js.map +0 -1
package/dist/auth.js CHANGED
@@ -1,4 +1,74 @@
1
+ import { createHash, timingSafeEqual } from 'node:crypto';
2
+ import { isIP } from 'node:net';
3
+ import { BUILTIN_THEMES, DEFAULT_THEMES } from '@aalis/api-webui';
4
+ import { escapeHtml, pageLanguageOf, textsBlock } from './html.js';
5
+ import { route } from './route.js';
6
+ import { t } from './texts.js';
1
7
  const TOKEN_COOKIE = 'aalis_webui_token';
8
+ /** 同一来源地址在一个窗口内登录失败满这么多次,到窗口结束前它的登录一律回 429(正确的 token 也不收) */
9
+ const LOGIN_MAX_FAILURES = 10;
10
+ /** 登录失败的计数窗口,从这个地址的第一次失败算起;窗口结束自动解除,计数归零 */
11
+ const LOGIN_FAILURE_WINDOW_MS = 10 * 60 * 1000;
12
+ /** 失败计数表最多记这么多个地址:满了再记新地址时丢掉最早的那条,内存有界,也不因此变成全局锁定 */
13
+ export const LOGIN_FAILURE_ADDRESSES_MAX = 10_000;
14
+ /**
15
+ * token 的定时安全比较:两边先取 SHA-256 摘要再逐字节比,比较耗时与 token 的内容和长度都无关,
16
+ * 逐字节试探与按长度试探都得不到信号。
17
+ */
18
+ function tokenEquals(given, expected) {
19
+ const digest = (s) => createHash('sha256').update(s).digest();
20
+ return timingSafeEqual(digest(given), digest(expected));
21
+ }
22
+ /** Host 头(或配置里同样写法)的主机名:小写,国际化域名转成 punycode,IPv6 带方括号;解析不了的没有 */
23
+ function hostnameOf(host) {
24
+ const url = `http://${host}`;
25
+ return URL.canParse(url) ? new URL(url).hostname : undefined;
26
+ }
27
+ /**
28
+ * 配置里的一项是不是主机名(allowedHosts 的写法):可带端口,不带协议、用户名、路径、查询与片段,去掉首尾空白后解析得出。
29
+ * 带协议的照 Host 头的写法拼出来会把协议名当主机名(`https://x` 成了 `https`),真正要认的主机名照旧被拒
30
+ */
31
+ export function isHostName(entry) {
32
+ return !/[/\\?#@]/.test(entry) && hostnameOf(entry.trim()) !== undefined;
33
+ }
34
+ /**
35
+ * 请求的 Host 能不能收:主机名是 IP 地址、localhost 或 names 里的一个(监听地址与 allowedHosts)。DNS 重绑定的网页把自己的
36
+ * 域名解析到本机后,借 owner 的浏览器发来的请求 Origin 与 Host 都是那个域名,同源判定拦不住,靠这一关拦下;
37
+ * IP 地址重绑定不了,局域网按 IP 访问照常。
38
+ */
39
+ function hostAllowed(req, names) {
40
+ const name = req.headers.host === undefined ? undefined : hostnameOf(req.headers.host);
41
+ if (name === undefined)
42
+ return false;
43
+ return name === 'localhost' || isIP(name.replace(/^\[(.*)\]$/, '$1')) !== 0 || names.has(name);
44
+ }
45
+ /** 不改状态的请求方法:不校验来源 */
46
+ const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
47
+ /**
48
+ * 请求的来源能不能收,WebSocket 升级与改状态的请求(非 GET)共用。浏览器发起的握手与 POST 一律带 Origin,只收与本服务同源的:
49
+ * Origin 的主机与端口等于请求的 Host。同一主机别的端口上的网页与本服务同站,SameSite cookie 会随它的请求发出,
50
+ * 靠这一关拦下。没有 Origin 的是浏览器之外的客户端(脚本、命令行),不会被别的网页借用户的浏览器发起,交给 token。
51
+ * Host 按 Origin 的协议解析后再比,两边的默认端口都去掉(代理转来的 Host 可能写成 `x:443`)。
52
+ * 经反向代理访问时,代理要把原始的 Host 转给本服务。
53
+ */
54
+ export function originAllowed(req) {
55
+ const { origin, host } = req.headers;
56
+ if (origin === undefined)
57
+ return true;
58
+ if (host === undefined || !URL.canParse(origin))
59
+ return false;
60
+ const { protocol, host: originHost } = new URL(origin);
61
+ const target = `${protocol}//${host}`;
62
+ return URL.canParse(target) && new URL(target).host === originHost;
63
+ }
64
+ /**
65
+ * 这次导航是不是别的网页发起的:Sec-Fetch-Site 为 cross-site 或 same-site。粘贴链接、书签与启动时自动打开的
66
+ * 一键登录链接是 none,本服务自己页面里的跳转是 same-origin;不带这个头的(浏览器之外的客户端、旧浏览器)不算
67
+ */
68
+ function navigatedFromOtherSite(req) {
69
+ const site = req.headers['sec-fetch-site'];
70
+ return site === 'cross-site' || site === 'same-site';
71
+ }
2
72
  function parseCookieValue(header, name) {
3
73
  if (!header)
4
74
  return undefined;
@@ -24,48 +94,69 @@ function buildSetCookie(name, value, maxAgeSec) {
24
94
  function buildClearCookie(name) {
25
95
  return `${name}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`;
26
96
  }
27
- /** 登录页:自包含 HTML,不引入外部资源;提交 POST /api/auth/login 后刷新到 / */
28
- function loginPageHtml(reason) {
29
- const note = reason ? `<p class="note">${escapeHtml(reason)}</p>` : '';
30
- return `<!DOCTYPE html><html lang="zh-CN"><head>
31
- <meta charset="utf-8"><title>Aalis WebUI 登录</title>
97
+ /** 登录页的脚本:文档级 CSP 只许同源脚本,不内联,由这个地址给出(不需登录) */
98
+ const LOGIN_SCRIPT_PATH = '/api/auth/login.js';
99
+ /** 提交 POST /api/auth/login,成功后刷新到 /;给人看的文字取自页面的 texts 块 */
100
+ const LOGIN_SCRIPT = `const T=JSON.parse(document.getElementById('texts').textContent);
101
+ const f=document.getElementById('f'),t=document.getElementById('t'),b=document.getElementById('b'),e=document.getElementById('e');
102
+ f.addEventListener('submit',async ev=>{
103
+ ev.preventDefault();e.textContent='';b.disabled=true;
104
+ try{
105
+ const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:t.value.trim()})});
106
+ if(r.ok){location.replace('/');}else{const j=await r.json().catch(()=>({}));e.textContent=j.error||T.tokenWrong;b.disabled=false;t.select();}
107
+ }catch(err){e.textContent=T.networkError;b.disabled=false;}
108
+ });
109
+ `;
110
+ /** 一个内置主题的颜色令牌,写成 CSS 自定义属性的声明 */
111
+ const themeTokens = (id) => Object.entries(BUILTIN_THEMES[id].colors)
112
+ .map(([name, value]) => `${name}:${value}`)
113
+ .join(';');
114
+ /**
115
+ * 登录页的样式:颜色取默认主题的令牌,深浅跟随系统(界面在登录之后才读得到本浏览器选的主题)。与界面同一套写法:
116
+ * 不用卡片、不加阴影,圆角用小档
117
+ */
118
+ const LOGIN_STYLE = `:root{color-scheme:light dark;${themeTokens(DEFAULT_THEMES.dark)}}
119
+ @media (prefers-color-scheme: light){:root{${themeTokens(DEFAULT_THEMES.light)}}}
120
+ *{box-sizing:border-box}
121
+ body{margin:0;min-height:100vh;min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:1rem;font-family:system-ui,-apple-system,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;font-size:0.875rem;line-height:1.375rem;background:var(--color-bg);color:var(--color-text)}
122
+ form{width:min(22.5rem,100%);display:flex;flex-direction:column;gap:0.75rem}
123
+ h1{margin:0;font-size:1rem;line-height:1.5rem;font-weight:600}
124
+ p{margin:0;color:var(--color-text-2)}
125
+ p.note{color:var(--color-warning-text)}
126
+ input{width:100%;height:2.25rem;padding:0 0.75rem;font-family:ui-monospace,"SF Mono",Menlo,Consolas,monospace;font-size:1rem;color:var(--color-text);background:transparent;border:1px solid var(--color-border-control);border-radius:4px}
127
+ input:focus-visible,button:focus-visible{outline:2px solid var(--color-focus);outline-offset:2px}
128
+ button{height:2.25rem;font:inherit;font-weight:500;color:var(--color-on-accent);background:var(--color-accent);border:0;border-radius:4px;cursor:pointer}
129
+ button:hover{background:var(--color-accent-hover)}
130
+ button:disabled{opacity:0.6;cursor:wait}
131
+ .err{min-height:1.375rem;color:var(--color-danger-text)}`;
132
+ /** 登录页的文字;pageLanguage 是渲染时实际用的语言,写进 <html lang> */
133
+ const LOGIN_TEXTS = {
134
+ lang: t('pageLanguage'),
135
+ title: t('loginTitle'),
136
+ hint: t('loginHint'),
137
+ placeholder: t('tokenPlaceholder'),
138
+ button: t('loginButton'),
139
+ script: { tokenWrong: t('tokenWrong'), networkError: t('networkError') },
140
+ };
141
+ /** 登录页:只引用同源的登录脚本,不引入别的资源。text 是渲染过的文字,note 是这次要说明的一句 */
142
+ function loginPageHtml(text) {
143
+ const note = text.note ? `<p class="note">${escapeHtml(text.note)}</p>` : '';
144
+ return `<!DOCTYPE html><html lang="${escapeHtml(text.lang)}"><head>
145
+ <meta charset="utf-8"><title>${escapeHtml(text.title)}</title>
32
146
  <meta name="viewport" content="width=device-width,initial-scale=1">
33
147
  <style>
34
- :root{color-scheme:light dark}
35
- *{box-sizing:border-box}
36
- body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#0b0d10;color:#e6e8eb}
37
- .card{width:min(420px,90vw);padding:32px;background:#16191d;border:1px solid #262b32;border-radius:12px;box-shadow:0 12px 36px rgba(0,0,0,.4)}
38
- h1{margin:0 0 8px;font-size:22px;font-weight:600}
39
- p{margin:0 0 16px;color:#8b95a1;font-size:14px;line-height:1.5}
40
- p.note{color:#ffb300}
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}
42
- input:focus{border-color:#5b8def}
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}
44
- button:hover{background:#4a7de0}
45
- button:disabled{opacity:.5;cursor:wait}
46
- .err{margin-top:10px;color:#ff6b6b;font-size:13px;min-height:18px}
148
+ ${LOGIN_STYLE}
47
149
  </style></head>
48
- <body><form class="card" id="f">
150
+ <body><form id="f">
49
151
  <h1>Aalis WebUI</h1>
50
- <p>请输入启动日志或 data/webui/access.txt 中显示的访问 token。</p>
152
+ <p>${escapeHtml(text.hint)}</p>
51
153
  ${note}
52
- <input id="t" type="password" autocomplete="off" placeholder="访问 token" autofocus required>
53
- <button type="submit" id="b">登录</button>
154
+ <input id="t" type="password" autocomplete="off" placeholder="${escapeHtml(text.placeholder)}" autofocus required>
155
+ <button type="submit" id="b">${escapeHtml(text.button)}</button>
54
156
  <div class="err" id="e"></div>
55
157
  </form>
56
- <script>
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;
60
- try{
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;}
64
- });
65
- </script></body></html>`;
66
- }
67
- function escapeHtml(s) {
68
- return s.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
158
+ ${textsBlock(text.script)}
159
+ <script src="${LOGIN_SCRIPT_PATH}"></script></body></html>`;
69
160
  }
70
161
  /**
71
162
  * 创建单 token 认证系统(单 owner)。
@@ -73,77 +164,160 @@ function escapeHtml(s) {
73
164
  * - token 由调用方经 getToken 提供,每次校验现取:persist 模式下 storage 晚上线时,调用方会把
74
165
  * 启动时的临时 token 换成读回的持久化 token(见 index.ts 的 token 解析)
75
166
  * - HttpOnly + SameSite=Strict cookie;`?token=` 一键登录;同源 POST /api/auth/login 提交
167
+ * - 请求的 Host 先按 {@link hostAllowed} 核对,hostNames 是 IP 地址与 localhost 之外认的主机名(监听地址与 allowedHosts);
168
+ * 不认的回 421,不进登录的比对与计数,带着 cookie 也不放行
169
+ * - 改状态的请求(非 GET、HEAD、OPTIONS)按 {@link originAllowed} 校验来源,与 WebSocket 升级同一个判定:SameSite 按站判、
170
+ * 不分端口,同一主机别的端口上的网页发一个简单 POST 就会带上 cookie
171
+ * - `?token=` 与登录接口按来源地址限速:失败计数见 LOGIN_MAX_FAILURES、LOGIN_FAILURE_WINDOW_MS,成功清零。
172
+ * 别的网页借 owner 的浏览器发起的登录(跨源的 POST、别的站点打开的 `?token=`、DNS 重绑定)不比对也不计数,免得别人替 owner 把
173
+ * 地址锁住。带 cookie 的请求不计数:各种模式的 token 都够长(随机生成的 24 字节,固定的至少 32 个字符),
174
+ * 按 cookie 逐个猜猜不中;对 cookie 也锁定的话,共用一个来源地址(同一个反向代理)的已登录页面会被别人一起锁住
76
175
  * - 身份恒为 `webui:console`(owner 级,单人模式语义)
176
+ * - 登录页与回应里的文字按全局层的语言(localizer)
77
177
  */
78
- export function createAuthSystem(getToken) {
178
+ export function createAuthSystem(getToken, localizer, hostNames) {
79
179
  const cookieMaxAge = 30 * 24 * 3600; // 30d;进程重启 token 轮换后无效
180
+ const names = new Set(hostNames.flatMap(name => hostnameOf(name.trim()) ?? []));
181
+ // 登录失败按来源地址计数。本机回环也计数:经同机反向代理来的请求全是回环地址,豁免回环等于不限速;
182
+ // owner 自己输错满额也只等到窗口结束,已登录的页面不受影响。
183
+ // 时间取单调时钟,键的插入顺序就是窗口结束的先后,过期的从表头剪掉;表满时也从表头丢,最多 LOGIN_FAILURE_ADDRESSES_MAX 条。
184
+ const failures = new Map();
185
+ /** 这个地址还要等多少毫秒才能再登录,不受限时为 0;先剪掉窗口已过的计数 */
186
+ function lockedFor(address) {
187
+ const now = performance.now();
188
+ for (const [key, entry] of failures) {
189
+ if (entry.resetAt > now)
190
+ break;
191
+ failures.delete(key);
192
+ }
193
+ const entry = failures.get(address);
194
+ return entry && entry.count >= LOGIN_MAX_FAILURES ? entry.resetAt - now : 0;
195
+ }
196
+ /** 记一次失败;须紧跟在 lockedFor 之后调用,表里留着的计数都还在窗口内 */
197
+ function recordFailure(address) {
198
+ const entry = failures.get(address);
199
+ if (entry) {
200
+ entry.count++;
201
+ return;
202
+ }
203
+ if (failures.size >= LOGIN_FAILURE_ADDRESSES_MAX)
204
+ failures.delete(failures.keys().next().value);
205
+ failures.set(address, { count: 1, resetAt: performance.now() + LOGIN_FAILURE_WINDOW_MS });
206
+ }
207
+ /** 给被限速的登录写好 429 与 Retry-After,返回给人看的说明 */
208
+ function rejectLocked(res, waitMs) {
209
+ res.status(429);
210
+ res.setHeader('Retry-After', String(Math.ceil(waitMs / 1000)));
211
+ return t('tooManyFailures', { minutes: Math.ceil(waitMs / 60_000) });
212
+ }
213
+ /** 登录页,note 是要说明的一句 */
214
+ function loginPage(note) {
215
+ const text = localizer.render({ ...LOGIN_TEXTS, note });
216
+ return loginPageHtml({ ...text, lang: pageLanguageOf(text.lang, localizer.languages()) });
217
+ }
80
218
  function tokenAuthed(req) {
81
219
  const cookieToken = parseCookieValue(req.headers.cookie, TOKEN_COOKIE);
82
- return !!cookieToken && cookieToken === getToken();
220
+ return !!cookieToken && tokenEquals(cookieToken, getToken());
83
221
  }
84
222
  function identify(req) {
85
223
  return tokenAuthed(req) ? { platform: 'webui', userId: 'console' } : undefined;
86
224
  }
87
- const middleware = (req, res, next) => {
88
- const url = req.path;
89
- const qToken = typeof req.query?.token === 'string' ? req.query.token : undefined;
90
- // 1. ?token= 命中 → 设置 cookie → 302 到不带 query 的同路径
91
- if (req.method === 'GET' && qToken) {
225
+ const addressOf = (req) => req.socket.remoteAddress ?? '';
226
+ function install(expressApp) {
227
+ // 先核 Host:DNS 重绑定的网页发来的请求在这里拦下
228
+ expressApp.use((req, res, next) => {
229
+ if (hostAllowed(req, names))
230
+ return next();
231
+ res.status(421).json({ error: t('hostNotAllowed') });
232
+ });
233
+ // 改状态的请求再看来源:别的网页借 owner 的浏览器发来的(带着 cookie)一律拒,登录与注销也一样
234
+ expressApp.use((req, res, next) => {
235
+ if (SAFE_METHODS.has(req.method) || originAllowed(req))
236
+ return next();
237
+ res.status(403).json({ error: t('crossOrigin') });
238
+ });
239
+ // 登录页的脚本不需登录
240
+ route(expressApp, 'GET /api/auth/login.js', (_req, res) => {
241
+ res.type('application/javascript').send(LOGIN_SCRIPT);
242
+ });
243
+ // ?token= 命中 → 设置 cookie → 302 到同一路径,只去掉 token,其余查询串(如日志的筛选)照留。别的站点打开的不认 token,
244
+ // 按没带 token 往下走
245
+ expressApp.use((req, res, next) => {
246
+ const qToken = typeof req.query?.token === 'string' ? req.query.token : undefined;
247
+ if (req.method !== 'GET' || !qToken || navigatedFromOtherSite(req))
248
+ return next();
249
+ const address = addressOf(req);
250
+ const waitMs = lockedFor(address);
251
+ if (waitMs > 0) {
252
+ const note = rejectLocked(res, waitMs);
253
+ res.type('html').send(loginPage(note));
254
+ return;
255
+ }
92
256
  const token = getToken();
93
- if (qToken === token) {
257
+ if (tokenEquals(qToken, token)) {
258
+ failures.delete(address);
94
259
  res.setHeader('Set-Cookie', buildSetCookie(TOKEN_COOKIE, token, cookieMaxAge));
95
- res.redirect(302, url);
260
+ const rest = new URLSearchParams(req.originalUrl.slice(req.originalUrl.indexOf('?') + 1));
261
+ rest.delete('token');
262
+ const search = rest.toString();
263
+ res.redirect(302, search ? `${req.path}?${search}` : req.path);
264
+ return;
265
+ }
266
+ recordFailure(address);
267
+ res
268
+ .status(401)
269
+ .type('html')
270
+ .send(loginPage(t('urlTokenWrong')));
271
+ });
272
+ // 登录:{ token }。跨源的已在上面拒掉,不计数
273
+ route(expressApp, 'POST /api/auth/login', (req, res) => {
274
+ const address = addressOf(req);
275
+ const waitMs = lockedFor(address);
276
+ if (waitMs > 0) {
277
+ res.json({ error: rejectLocked(res, waitMs) });
96
278
  return;
97
279
  }
98
- res.status(401).type('html').send(loginPageHtml('URL 中的 token 不正确,请手动输入。'));
99
- return;
100
- }
101
- // 2. 登录端点:{token}
102
- if (url === '/api/auth/login' && req.method === 'POST') {
103
280
  const body = req.body ?? {};
104
281
  const token = getToken();
105
- if (typeof body.token === 'string' && body.token === token) {
282
+ if (typeof body.token === 'string' && tokenEquals(body.token, token)) {
283
+ failures.delete(address);
106
284
  res.setHeader('Set-Cookie', buildSetCookie(TOKEN_COOKIE, token, cookieMaxAge));
107
285
  res.json({ ok: true, identity: { platform: 'webui', userId: 'console' } });
108
286
  }
109
287
  else {
110
- res.status(401).json({ ok: false, error: 'token 不正确' });
288
+ recordFailure(address);
289
+ res.status(401).json({ error: t('tokenWrong') });
111
290
  }
112
- return;
113
- }
114
- // 3. 状态查询(含当前身份,供前端顶栏展示)
115
- if (url === '/api/auth/status' && req.method === 'GET') {
291
+ });
292
+ // 登录状态(含当前身份,供前端顶栏展示)
293
+ route(expressApp, 'GET /api/auth/status', (req, res) => {
116
294
  const identity = identify(req);
117
295
  res.json({ authed: !!identity, identity });
118
- return;
119
- }
120
- // 4. 注销:清 token cookie
121
- if (url === '/api/auth/logout' && req.method === 'POST') {
296
+ });
297
+ // 注销:清 token cookie
298
+ route(expressApp, 'POST /api/auth/logout', (_req, res) => {
122
299
  res.setHeader('Set-Cookie', buildClearCookie(TOKEN_COOKIE));
123
300
  res.json({ ok: true });
124
- return;
125
- }
126
- // 5. 已认证 → 放行
127
- if (tokenAuthed(req)) {
128
- next();
129
- return;
130
- }
131
- // 6. 未认证:API 一律 401
132
- if (url.startsWith('/api/')) {
133
- res.status(401).json({ error: 'unauthenticated' });
134
- return;
135
- }
136
- // 7. 未认证 GET:返回内联登录页(覆盖 SPA shell,不暴露任何静态资源)
137
- if (req.method === 'GET') {
138
- res.status(401).type('html').send(loginPageHtml());
139
- return;
140
- }
141
- res.status(401).end();
142
- };
301
+ });
302
+ // 已登录的放行;没登录的 API 一律 401,GET 回内联登录页(覆盖 SPA shell,不暴露任何静态资源)
303
+ expressApp.use((req, res, next) => {
304
+ if (tokenAuthed(req))
305
+ return next();
306
+ if (req.path.startsWith('/api/')) {
307
+ res.status(401).json({ error: t('unauthenticated') });
308
+ return;
309
+ }
310
+ if (req.method === 'GET') {
311
+ res.status(401).type('html').send(loginPage());
312
+ return;
313
+ }
314
+ res.status(401).end();
315
+ });
316
+ }
143
317
  function verifyWsClient(req) {
144
318
  return tokenAuthed(req);
145
319
  }
146
- return { middleware, verifyWsClient, identify };
320
+ return { install, verifyWsClient, identify };
147
321
  }
148
322
  /**
149
323
  * 跨平台打开默认浏览器到指定 URL(无外部依赖)
package/dist/auth.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
@@ -28,14 +28,18 @@ export interface LocalScanEntry {
28
28
  /** 该包的 keywords。离线降级路径靠它筛出 aalis 组件——缺了这些包就整批看不见。 */
29
29
  keywords?: string[];
30
30
  description?: string;
31
+ /** 声明提供的服务({@link declaredProvides}):市场按服务搜靠它 */
32
+ provides: string[];
31
33
  /** `dependencies/peer/optional` 的并集**包名**(不含版本区间)。 */
32
34
  deps: string[];
33
35
  }
36
+ /** package.json 的 `aalis.service.provides` 里的字符串;没声明或写法不对时为空。本地与安装源上的清单同一读法 */
37
+ export declare function declaredProvides(pkg: unknown): string[];
34
38
  /**
35
39
  * 扫描本地物理存在的包 → `name → {version, deps}`(与 discoverClients 共用 scanDirs/env)。
36
40
  * 三用,一次扫盘:① keys 即「本地已装包名」,补市场已装判定(require.resolve 在 pnpm 工作区
37
41
  * 从仓库根解析不到未提升的工作区包);② `deps` 供依赖图(谁依赖谁)——**只取依赖名、不看版本
38
- * 区间**(`workspace:^`/`^1.0.0` 一律按名),故无需 semver;③ `version`/`keywords`/`description` 供市场卡片:
42
+ * 区间**(`workspace:^`/`^1.0.0` 一律按名),故无需 semver;③ `version`/`keywords`/`description`/`provides` 供市场卡片:
39
43
  * 工作区包这条路径 require.resolve 走不通,不带 version 就只能退回显示 npm latest;不带
40
44
  * keywords 则**离线降级列表整批看不见它们**(toLocalPackages 按 keywords 筛 aalis 组件)。
41
45
  * scope 无关(不挑 @aalis)。纯函数便于单测。
@@ -44,8 +48,8 @@ export declare function collectLocalPackageDeps(scanDirs: string[], env: Pick<Di
44
48
  /**
45
49
  * 从本轮发现结果里挑出**尚未登记**的候选。
46
50
  *
47
- * 前端发现要能重复跑(装完一个 `aalis-interface` 包后立刻重跑,否则它不出现在服务页的
48
- * 下拉里、必须重启才看得见)。而重复 `fork().provide('webui-client', …)` 会在服务容器里
51
+ * 前端发现要能重复跑(装完一个 `aalis-interface` 包后立刻重跑,否则它不出现在服务偏好的
52
+ * 可选项里、必须重启才看得见)。而重复 `fork().provide('webui-client', …)` 会在服务容器里
49
53
  * 堆同名重复项 —— 所以幂等是硬要求,不是优化。纯函数,便于单测。
50
54
  */
51
55
  export declare function pickFreshClients<T extends {
@@ -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;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,YAAY,GAAG,eAAe,EAAE,CA2B1G;AAED,gCAAgC;AAChC,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sDAAsD;IACtD,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;;;;;;GAQG;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,cAAc,CAAC,CAiC7B;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,EAAE,CAGjH"}
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;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,YAAY,GAAG,eAAe,EAAE,CA2B1G;AAED,gCAAgC;AAChC,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,sDAAsD;IACtD,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,EAAE,CAGvD;AAED;;;;;;;;GAQG;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,cAAc,CAAC,CAkC7B;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,EAAE,CAGjH"}
@@ -39,11 +39,16 @@ export function discoverClients(scanDirs, depIds, env) {
39
39
  consider(env.resolvePkgJson(id));
40
40
  return out.sort((a, b) => a.id.localeCompare(b.id));
41
41
  }
42
+ /** package.json 的 `aalis.service.provides` 里的字符串;没声明或写法不对时为空。本地与安装源上的清单同一读法 */
43
+ export function declaredProvides(pkg) {
44
+ const provides = pkg?.aalis?.service?.provides;
45
+ return Array.isArray(provides) ? provides.filter((item) => typeof item === 'string') : [];
46
+ }
42
47
  /**
43
48
  * 扫描本地物理存在的包 → `name → {version, deps}`(与 discoverClients 共用 scanDirs/env)。
44
49
  * 三用,一次扫盘:① keys 即「本地已装包名」,补市场已装判定(require.resolve 在 pnpm 工作区
45
50
  * 从仓库根解析不到未提升的工作区包);② `deps` 供依赖图(谁依赖谁)——**只取依赖名、不看版本
46
- * 区间**(`workspace:^`/`^1.0.0` 一律按名),故无需 semver;③ `version`/`keywords`/`description` 供市场卡片:
51
+ * 区间**(`workspace:^`/`^1.0.0` 一律按名),故无需 semver;③ `version`/`keywords`/`description`/`provides` 供市场卡片:
47
52
  * 工作区包这条路径 require.resolve 走不通,不带 version 就只能退回显示 npm latest;不带
48
53
  * keywords 则**离线降级列表整批看不见它们**(toLocalPackages 按 keywords 筛 aalis 组件)。
49
54
  * scope 无关(不挑 @aalis)。纯函数便于单测。
@@ -68,6 +73,7 @@ export function collectLocalPackageDeps(scanDirs, env) {
68
73
  ? pkg.keywords.filter((k) => typeof k === 'string')
69
74
  : undefined,
70
75
  description: typeof pkg.description === 'string' ? pkg.description : undefined,
76
+ provides: declaredProvides(pkg),
71
77
  deps: [...deps],
72
78
  });
73
79
  }
@@ -77,8 +83,8 @@ export function collectLocalPackageDeps(scanDirs, env) {
77
83
  /**
78
84
  * 从本轮发现结果里挑出**尚未登记**的候选。
79
85
  *
80
- * 前端发现要能重复跑(装完一个 `aalis-interface` 包后立刻重跑,否则它不出现在服务页的
81
- * 下拉里、必须重启才看得见)。而重复 `fork().provide('webui-client', …)` 会在服务容器里
86
+ * 前端发现要能重复跑(装完一个 `aalis-interface` 包后立刻重跑,否则它不出现在服务偏好的
87
+ * 可选项里、必须重启才看得见)。而重复 `fork().provide('webui-client', …)` 会在服务容器里
82
88
  * 堆同名重复项 —— 所以幂等是硬要求,不是优化。纯函数,便于单测。
83
89
  */
84
90
  export function pickFreshClients(known, discovered) {
@@ -1 +1 @@
1
- {"version":3,"file":"client-discovery.js","sourceRoot":"","sources":["../src/client-discovery.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,EAAE;AACF,yEAAyE;AACzE,iCAAiC;AACjC,8EAA8E;AA2B9E;;;;;GAKG;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;AAaD;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CACrC,QAAkB,EAClB,GAA2E;IAE3E,MAAM,GAAG,GAAG,IAAI,GAAG,EAA0B,CAAC;IAC9C,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,EAAE,CAAC;YAC1C,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,cAAc,CAAC,CAUjD,CAAC;YACd,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ;gBAAE,SAAS;YACnD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAS;gBAC3B,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC;gBACtC,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,gBAAgB,IAAI,EAAE,CAAC;gBAC1C,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,oBAAoB,IAAI,EAAE,CAAC;aAC/C,CAAC,CAAC;YACH,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE;gBAChB,OAAO,EAAE,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;gBAClE,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;oBACnC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;oBAChE,CAAC,CAAC,SAAS;gBACb,WAAW,EAAE,OAAO,GAAG,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;gBAC9E,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;aAChB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAA2B,KAAuB,EAAE,UAAwB;IAC1G,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC5B,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACjD,CAAC"}
1
+ {"version":3,"file":"client-discovery.js","sourceRoot":"","sources":["../src/client-discovery.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,27 @@
1
- /** 渲染前端切换恢复页的完整 HTML 文档(无运行时入参,数据由页面自身 fetch)。 */
2
- export declare function renderClientSwitchPage(): string;
1
+ import type { Localized } from '@aalis/schema-i18n';
2
+ /** 前端切换恢复页的脚本:文档级 CSP 只许同源脚本,由 `/__clients.js` 给出 */
3
+ export declare const CLIENT_SWITCH_SCRIPT = "(function () {\n var SVC = 'webui-client';\n var PREFER = '/api/services/' + SVC + '/prefer';\n var T = JSON.parse(document.getElementById('texts').textContent);\n var sel = document.getElementById('client');\n var btn = document.getElementById('go');\n var msg = document.getElementById('msg');\n function fill(text, values) {\n return text.replace(/\\{(\\w+)\\}/g, function (m, k) { return k in values ? String(values[k]) : m; });\n }\n function setMsg(t, err) { msg.textContent = t; msg.className = err ? 'err' : 'ok'; }\n function load() {\n fetch('/api/services', { headers: { Accept: 'application/json' } })\n .then(function (r) {\n if (!r.ok) throw new Error(fill(T.listFailed, { status: r.status }));\n return r.json();\n })\n .then(function (data) {\n var svc = data && data.services && data.services[SVC];\n var providers = (svc && svc.providers) || [];\n if (!providers.length) {\n setMsg(T.noClients, true);\n btn.disabled = true;\n sel.innerHTML = '';\n return;\n }\n var active = (svc && svc.preferred) || providers[0].contextId;\n sel.innerHTML = '';\n providers.forEach(function (p) {\n var o = document.createElement('option');\n var name = p.displayName || p.label || p.contextId;\n o.value = p.contextId;\n o.textContent = p.contextId === active ? fill(T.current, { name: name }) : name;\n if (p.contextId === active) o.selected = true;\n sel.appendChild(o);\n });\n })\n .catch(function (e) { setMsg(String((e && e.message) || e), true); });\n }\n btn.addEventListener('click', function () {\n var contextId = sel.value;\n if (!contextId) return;\n btn.disabled = true;\n setMsg(T.switching, false);\n fetch(PREFER, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ contextId: contextId })\n })\n .then(function (r) {\n if (r.ok) { setMsg(T.switched, false); location.href = '/'; return; }\n return r.json().catch(function () { return {}; }).then(function (j) {\n var detail = (j && j.error) || ('HTTP ' + r.status);\n var hint = (r.status === 401 || r.status === 403) ? T.ownerHint : '';\n setMsg(fill(T.failed, { detail: detail, hint: hint }), true);\n btn.disabled = false;\n });\n })\n .catch(function (e) { setMsg(fill(T.error, { error: String((e && e.message) || e) }), true); btn.disabled = false; });\n });\n load();\n})();\n";
4
+ /** 恢复页的文字;带参数的以 {名} 占位交给脚本填。pageLanguage 是渲染时实际用的语言,写进 <html lang> */
5
+ export declare const CLIENT_SWITCH_TEXTS: {
6
+ lang: import("@aalis/schema-i18n").TextRef;
7
+ title: import("@aalis/schema-i18n").TextRef;
8
+ heading: import("@aalis/schema-i18n").TextRef;
9
+ intro: import("@aalis/schema-i18n").TextRef;
10
+ label: import("@aalis/schema-i18n").TextRef;
11
+ loading: import("@aalis/schema-i18n").TextRef;
12
+ button: import("@aalis/schema-i18n").TextRef;
13
+ back: import("@aalis/schema-i18n").TextRef;
14
+ script: {
15
+ listFailed: import("@aalis/schema-i18n").TextRef;
16
+ noClients: import("@aalis/schema-i18n").TextRef;
17
+ current: import("@aalis/schema-i18n").TextRef;
18
+ switching: import("@aalis/schema-i18n").TextRef;
19
+ switched: import("@aalis/schema-i18n").TextRef;
20
+ ownerHint: import("@aalis/schema-i18n").TextRef;
21
+ failed: import("@aalis/schema-i18n").TextRef;
22
+ error: import("@aalis/schema-i18n").TextRef;
23
+ };
24
+ };
25
+ /** 渲染前端切换恢复页的完整 HTML 文档:text 是渲染过的文字,数据由页面自身 fetch */
26
+ export declare function renderClientSwitchPage(text: Localized<typeof CLIENT_SWITCH_TEXTS>): string;
3
27
  //# sourceMappingURL=client-switch-page.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"client-switch-page.d.ts","sourceRoot":"","sources":["../src/client-switch-page.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAIpD,qDAAqD;AACrD,eAAO,MAAM,oBAAoB,sjFA8DhC,CAAC;AAEF,sEAAsE;AACtE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;CAmB/B,CAAC;AAEF,sDAAsD;AACtD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,SAAS,CAAC,OAAO,mBAAmB,CAAC,GAAG,MAAM,CAmC1F"}