@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.
- package/README.md +5 -1
- package/dist/admin-channel.d.ts +10 -0
- package/dist/admin-channel.d.ts.map +1 -0
- package/dist/admin-channel.js +57 -0
- package/dist/admin-channel.js.map +1 -0
- package/dist/attention.d.ts +36 -0
- package/dist/attention.d.ts.map +1 -0
- package/dist/attention.js +74 -0
- package/dist/attention.js.map +1 -0
- package/dist/auth.d.ts +32 -4
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +253 -79
- package/dist/auth.js.map +1 -1
- package/dist/client-discovery.d.ts +7 -3
- package/dist/client-discovery.d.ts.map +1 -1
- package/dist/client-discovery.js +9 -3
- package/dist/client-discovery.js.map +1 -1
- package/dist/client-switch-page.d.ts +26 -2
- package/dist/client-switch-page.d.ts.map +1 -1
- package/dist/client-switch-page.js +76 -45
- package/dist/client-switch-page.js.map +1 -1
- package/dist/gate.d.ts +3 -3
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +5 -4
- package/dist/gate.js.map +1 -1
- package/dist/html.d.ts +15 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +38 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +24 -26
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +627 -1102
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +19 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +200 -0
- package/dist/layout.js.map +1 -0
- package/dist/locales/en.d.ts +191 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/en.js +191 -0
- package/dist/locales/en.js.map +1 -0
- package/dist/localize.d.ts +24 -0
- package/dist/localize.d.ts.map +1 -0
- package/dist/localize.js +68 -0
- package/dist/localize.js.map +1 -0
- package/dist/logs.d.ts +10 -0
- package/dist/logs.d.ts.map +1 -0
- package/dist/logs.js +50 -0
- package/dist/logs.js.map +1 -0
- package/dist/page-check.d.ts +56 -0
- package/dist/page-check.d.ts.map +1 -0
- package/dist/page-check.js +376 -0
- package/dist/page-check.js.map +1 -0
- package/dist/page-files.d.ts +13 -0
- package/dist/page-files.d.ts.map +1 -0
- package/dist/page-files.js +39 -0
- package/dist/page-files.js.map +1 -0
- package/dist/pages.d.ts +62 -0
- package/dist/pages.d.ts.map +1 -0
- package/dist/pages.js +486 -0
- package/dist/pages.js.map +1 -0
- package/dist/route.d.ts +14 -0
- package/dist/route.d.ts.map +1 -0
- package/dist/route.js +16 -0
- package/dist/route.js.map +1 -0
- package/dist/routes/files.d.ts +8 -9
- package/dist/routes/files.d.ts.map +1 -1
- package/dist/routes/files.js +33 -119
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/frames.d.ts +4 -0
- package/dist/routes/frames.d.ts.map +1 -0
- package/dist/routes/frames.js +103 -0
- package/dist/routes/frames.js.map +1 -0
- package/dist/routes/i18n.d.ts +20 -0
- package/dist/routes/i18n.d.ts.map +1 -0
- package/dist/routes/i18n.js +78 -0
- package/dist/routes/i18n.js.map +1 -0
- package/dist/routes/marketplace.d.ts +46 -129
- package/dist/routes/marketplace.d.ts.map +1 -1
- package/dist/routes/marketplace.js +156 -106
- package/dist/routes/marketplace.js.map +1 -1
- package/dist/routes/media.d.ts +10 -0
- package/dist/routes/media.d.ts.map +1 -0
- package/dist/routes/media.js +66 -0
- package/dist/routes/media.js.map +1 -0
- package/dist/routes/pages.d.ts +22 -0
- package/dist/routes/pages.d.ts.map +1 -0
- package/dist/routes/pages.js +104 -0
- package/dist/routes/pages.js.map +1 -0
- package/dist/routes/pipe-to-response.d.ts +9 -0
- package/dist/routes/pipe-to-response.d.ts.map +1 -0
- package/dist/routes/pipe-to-response.js +16 -0
- package/dist/routes/pipe-to-response.js.map +1 -0
- package/dist/routes/plugins.d.ts +22 -28
- package/dist/routes/plugins.d.ts.map +1 -1
- package/dist/routes/plugins.js +339 -344
- package/dist/routes/plugins.js.map +1 -1
- package/dist/routes/proxy.d.ts +3 -2
- package/dist/routes/proxy.d.ts.map +1 -1
- package/dist/routes/proxy.js +64 -42
- package/dist/routes/proxy.js.map +1 -1
- package/dist/session-transport.d.ts +6 -0
- package/dist/session-transport.d.ts.map +1 -0
- package/dist/session-transport.js +32 -0
- package/dist/session-transport.js.map +1 -0
- package/dist/texts.d.ts +381 -0
- package/dist/texts.d.ts.map +1 -0
- package/dist/texts.js +194 -0
- package/dist/texts.js.map +1 -0
- package/package.json +38 -32
- package/dist/protocol.d.ts +0 -113
- package/dist/protocol.d.ts.map +0 -1
- package/dist/protocol.js +0 -59
- package/dist/protocol.js.map +0 -1
- package/dist/routes/uploaded-files.d.ts +0 -20
- package/dist/routes/uploaded-files.d.ts.map +0 -1
- package/dist/routes/uploaded-files.js +0 -161
- package/dist/routes/uploaded-files.js.map +0 -1
package/dist/routes/files.js
CHANGED
|
@@ -1,130 +1,44 @@
|
|
|
1
1
|
// ============================================================
|
|
2
|
-
// routes/files.ts —
|
|
2
|
+
// routes/files.ts — 按句柄下载文件
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// GET /api/session-file?sessionId=<会话>&handle=<句柄>:会话条目里的文件。句柄由会话服务签发、绑定会话与文件,先请它核验。
|
|
5
|
+
// GET /api/page-file?handle=<句柄>:页面单元格里的文件。句柄由本插件签发(page-files.ts)。
|
|
6
|
+
// 核验出的存储地址读出后一律当附件以 octet-stream 下发,不在 WebUI 源里内联。只有 owner(cookie 闸)能取。
|
|
7
|
+
// 不接受存储地址:按地址任意读取会把 data 根(家目录的 state/,里面有 token 与审计)整个暴露出去。
|
|
8
|
+
// 句柄核验不过(含签发方不在、重启过)回 403,文件已不在回 404,其余读不出的回 500 并记告警。
|
|
6
9
|
// ============================================================
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const idx = uri.indexOf(':/');
|
|
17
|
-
return idx >= 0 ? uri.slice(idx + 2) : uri;
|
|
18
|
-
}
|
|
19
|
-
function storageRootBrowsable() {
|
|
20
|
-
const root = storage.listRoots().find(r => r.name === fileRoot);
|
|
21
|
-
return Boolean(root?.browsable);
|
|
22
|
-
}
|
|
23
|
-
function sendStorageError(res, err) {
|
|
24
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25
|
-
const status = message.includes('路径不合法') || message.includes('不允许')
|
|
26
|
-
? 403
|
|
27
|
-
: message.includes('不存在') || message.includes('ENOENT')
|
|
28
|
-
? 404
|
|
29
|
-
: message.includes('不是目录') || message.includes('不能') || message.includes('不合法')
|
|
30
|
-
? 400
|
|
31
|
-
: 500;
|
|
32
|
-
res.status(status).json({ error: message });
|
|
33
|
-
}
|
|
34
|
-
function guardBrowsable(res) {
|
|
35
|
-
if (!storageRootBrowsable()) {
|
|
36
|
-
res.status(403).json({ error: '文件根不可浏览' });
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
// 列出目录内容
|
|
42
|
-
expressApp.get('/api/files', gate(), async (req, res) => {
|
|
43
|
-
const dir = String(req.query.path || '');
|
|
44
|
-
if (!guardBrowsable(res))
|
|
45
|
-
return;
|
|
46
|
-
try {
|
|
47
|
-
const result = await storage.list(storageUri(dir));
|
|
48
|
-
res.json({ path: result.path, entries: result.entries });
|
|
49
|
-
}
|
|
50
|
-
catch (err) {
|
|
51
|
-
sendStorageError(res, err);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
// 获取文件/目录详情
|
|
55
|
-
expressApp.get('/api/files/info', gate(), async (req, res) => {
|
|
56
|
-
const filePath = String(req.query.path || '');
|
|
57
|
-
if (!guardBrowsable(res))
|
|
58
|
-
return;
|
|
59
|
-
try {
|
|
60
|
-
const info = await storage.stat(storageUri(filePath));
|
|
61
|
-
res.json(info);
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
sendStorageError(res, err);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
// 下载文件
|
|
68
|
-
expressApp.get('/api/files/download', gate(), async (req, res) => {
|
|
69
|
-
const filePath = String(req.query.path || '');
|
|
70
|
-
if (!guardBrowsable(res))
|
|
10
|
+
import { isStorageNotFound } from '@aalis/api-storage';
|
|
11
|
+
import { route } from '../route.js';
|
|
12
|
+
import { t } from '../texts.js';
|
|
13
|
+
import { pipeToResponse } from './pipe-to-response.js';
|
|
14
|
+
export function registerFileRoutes(expressApp, storage, issuers, logger, gate) {
|
|
15
|
+
/** what 是日志里的叫法:会话文件或页面文件 */
|
|
16
|
+
async function sendAttachment(res, file, what) {
|
|
17
|
+
if (!file) {
|
|
18
|
+
res.status(403).json({ error: t('invalidHandle') });
|
|
71
19
|
return;
|
|
72
|
-
try {
|
|
73
|
-
const result = await storage.createReadStream(storageUri(filePath));
|
|
74
|
-
const fileName = basename(result.stat.name);
|
|
75
|
-
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`);
|
|
76
|
-
res.setHeader('Content-Length', result.stat.size);
|
|
77
|
-
// 客户端中断(取消下载/关标签页/断网)时 res 触发 'close',而 Node 的 pipe 只 unpipe、
|
|
78
|
-
// 不 destroy 源流——fs.ReadStream 会停在 paused 态永不 end,fd 不回收。正常结束时
|
|
79
|
-
// destroy 打在已 autoClose 的流上是 no-op,happy path 行为不变。
|
|
80
|
-
res.on('close', () => result.stream.destroy());
|
|
81
|
-
result.stream.pipe(res);
|
|
82
|
-
}
|
|
83
|
-
catch (err) {
|
|
84
|
-
sendStorageError(res, err);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
// 重命名
|
|
88
|
-
expressApp.post('/api/files/rename', gate(), async (req, res) => {
|
|
89
|
-
const { path: filePath, newName } = req.body ?? {};
|
|
90
|
-
if (!filePath || !newName) {
|
|
91
|
-
res.status(400).json({ error: '缺少参数' });
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
if (typeof newName !== 'string' ||
|
|
95
|
-
newName.includes('/') ||
|
|
96
|
-
newName.includes('\\') ||
|
|
97
|
-
newName === '.' ||
|
|
98
|
-
newName === '..') {
|
|
99
|
-
res.status(400).json({ error: '文件名不合法' });
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
if (!guardBrowsable(res))
|
|
103
|
-
return;
|
|
104
|
-
try {
|
|
105
|
-
const newUri = await storage.rename(storageUri(String(filePath)), String(newName));
|
|
106
|
-
res.json({ ok: true, newPath: pathFromStorageUri(newUri) });
|
|
107
|
-
}
|
|
108
|
-
catch (err) {
|
|
109
|
-
sendStorageError(res, err);
|
|
110
20
|
}
|
|
111
|
-
});
|
|
112
|
-
// 删除
|
|
113
|
-
expressApp.post('/api/files/delete', gate(), async (req, res) => {
|
|
114
|
-
const { path: filePath } = req.body ?? {};
|
|
115
|
-
if (!filePath) {
|
|
116
|
-
res.status(400).json({ error: '缺少参数' });
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (!guardBrowsable(res))
|
|
120
|
-
return;
|
|
121
21
|
try {
|
|
122
|
-
await storage.
|
|
123
|
-
|
|
22
|
+
const { stream, stat } = await storage.createReadStream(file.uri);
|
|
23
|
+
// attachment 按扩展名设了类型,随后一律盖成 octet-stream
|
|
24
|
+
res.attachment(file.name);
|
|
25
|
+
res.setHeader('Content-Type', 'application/octet-stream');
|
|
26
|
+
res.setHeader('Content-Length', stat.size);
|
|
27
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
28
|
+
pipeToResponse(stream, res, logger, `${what} ${file.uri}`);
|
|
124
29
|
}
|
|
125
30
|
catch (err) {
|
|
126
|
-
|
|
31
|
+
if (isStorageNotFound(err)) {
|
|
32
|
+
res.status(404).json({ error: t('fileGone') });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
logger.warn(`读取${what} ${file.uri} 失败:`, err);
|
|
36
|
+
res.status(500).json({ error: t('readFailed') });
|
|
127
37
|
}
|
|
128
|
-
}
|
|
38
|
+
}
|
|
39
|
+
route(expressApp, 'GET /api/session-file', gate(), (req, res) => sendAttachment(res, issuers.sessions.present
|
|
40
|
+
? issuers.sessions.use.resolveFile(String(req.query.sessionId ?? ''), String(req.query.handle ?? ''))
|
|
41
|
+
: undefined, '会话文件'));
|
|
42
|
+
route(expressApp, 'GET /api/page-file', gate(), (req, res) => sendAttachment(res, issuers.pages.resolve(String(req.query.handle ?? '')), '页面文件'));
|
|
129
43
|
}
|
|
130
44
|
//# sourceMappingURL=files.js.map
|
package/dist/routes/files.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/routes/files.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/routes/files.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,4BAA4B;AAC5B,EAAE;AACF,qFAAqF;AACrF,oEAAoE;AACpE,wEAAwE;AACxE,6DAA6D;AAC7D,uDAAuD;AACvD,+DAA+D;AAG/D,OAAO,EAAE,iBAAiB,EAAuB,MAAM,oBAAoB,CAAC;AAM5E,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,CAAC,EAAE,MAAM,aAAa,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,MAAM,UAAU,kBAAkB,CAChC,UAA2B,EAC3B,OAAuB,EACvB,OAA0G,EAC1G,MAAc,EACd,IAAe;IAEf,6BAA6B;IAC7B,KAAK,UAAU,cAAc,CAC3B,GAAoC,EACpC,IAA+C,EAC/C,IAAY;QAEZ,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC;YACpD,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,OAAO,CAAC,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAClE,0CAA0C;YAC1C,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1B,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,0BAA0B,CAAC,CAAC;YAC1D,GAAG,CAAC,SAAS,CAAC,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3C,GAAG,CAAC,SAAS,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAC;YACnD,cAAc,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC7D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC3B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;gBAC/C,OAAO;YACT,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,IAAI,CAAC,GAAG,MAAM,EAAE,GAAG,CAAC,CAAC;YAC9C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,UAAU,EAAE,uBAAuB,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAC9D,cAAc,CACZ,GAAG,EACH,OAAO,CAAC,QAAQ,CAAC,OAAO;QACtB,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;QACrG,CAAC,CAAC,SAAS,EACb,MAAM,CACP,CACF,CAAC;IACF,KAAK,CAAC,UAAU,EAAE,oBAAoB,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAC3D,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CACnF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frames.d.ts","sourceRoot":"","sources":["../../src/routes/frames.ts"],"names":[],"mappings":"AAeA,OAAO,OAAO,MAAM,SAAS,CAAC;AAoD9B,uEAAuE;AACvE,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,CAyCnH"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// routes/frames.ts — 插件前端的文件托管
|
|
3
|
+
//
|
|
4
|
+
// GET /frames/:token/<路径> 给出插件前端 webui/ 里的文件,token 是页面登记时随机生成的(pages.ts),页面撤回或重新
|
|
5
|
+
// 登记后旧的回 404;GET /frames/_base/<路径> 给出 api-webui 随包带的公用文件(助手、样式基线与助手导入的
|
|
6
|
+
// schema-i18n)。注册在认证之前,不看 cookie:token 只出现在要登录的 GET /api/pages 里,文件是随包发布的代码,不含数据。
|
|
7
|
+
// 托管用 express.static,前面另加扩展名白名单与 realpath 两道;不合的一律 404,不回目录、不发 3xx:重定向之后 CSP 只按
|
|
8
|
+
// 来源匹配、不看路径,「只放行这一页的目录」就失效了。只收 GET 与 HEAD。
|
|
9
|
+
// 响应头带框架 CSP,盖过全局的文档级 CSP。CSP 的来源取请求的 Host,写成带路径、不带协议的来源,http 访问与经 TLS 代理的
|
|
10
|
+
// https 访问都成立;Host 只收主机名或 IPv4 地址加可选端口,去掉默认端口。
|
|
11
|
+
// ============================================================
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import { dirname, extname, join } from 'node:path';
|
|
14
|
+
import { FRAME_SANDBOX } from '@aalis/api-webui';
|
|
15
|
+
import express from 'express';
|
|
16
|
+
import { realWithin } from '../pages.js';
|
|
17
|
+
/** 公用文件的地址段 */
|
|
18
|
+
const BASE = '_base';
|
|
19
|
+
/** api-webui 随包带的公用文件所在的目录 */
|
|
20
|
+
const BASE_DIR = join(dirname(createRequire(import.meta.url).resolve('@aalis/api-webui/package.json')), 'dist', 'frame');
|
|
21
|
+
/** 托管的扩展名:网页、脚本、样式、图片与字体 */
|
|
22
|
+
const EXTENSIONS = new Set([
|
|
23
|
+
'.html',
|
|
24
|
+
'.js',
|
|
25
|
+
'.mjs',
|
|
26
|
+
'.css',
|
|
27
|
+
'.svg',
|
|
28
|
+
'.png',
|
|
29
|
+
'.jpg',
|
|
30
|
+
'.jpeg',
|
|
31
|
+
'.gif',
|
|
32
|
+
'.webp',
|
|
33
|
+
'.woff2',
|
|
34
|
+
]);
|
|
35
|
+
/** 主机名或 IPv4 地址,加可选端口。IPv6 字面地址写不进 CSP 的来源,带分号、空白、斜杠的会改写 CSP,都不收 */
|
|
36
|
+
const HOST = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?::\d{1,5})?$/;
|
|
37
|
+
/**
|
|
38
|
+
* 框架 CSP:除脚本外按类型放开、不按来源限,样式、字体、图片、音视频与数据请求都能取外站的。只留两道:脚本只认这一页的
|
|
39
|
+
* 目录与公用目录(不许内联与 eval,worker 经回退链同样只认它们),框架里不能再嵌别的网页(frame 与 object 都不给)。
|
|
40
|
+
* 宿主接口靠不同源挡:框架是不透明来源,请求不带登录状态,改状态的请求与 WebSocket 握手按来源被拒
|
|
41
|
+
*/
|
|
42
|
+
function frameCsp(host, token) {
|
|
43
|
+
return [
|
|
44
|
+
`sandbox ${FRAME_SANDBOX}`,
|
|
45
|
+
`script-src ${host}/frames/${token}/ ${host}/frames/${BASE}/`,
|
|
46
|
+
"frame-src 'none'",
|
|
47
|
+
"object-src 'none'",
|
|
48
|
+
`frame-ancestors ${host}`,
|
|
49
|
+
].join('; ');
|
|
50
|
+
}
|
|
51
|
+
/** 请求里的路径按 %xx 解开,与 express.static 的解法相同;解不开的回 undefined */
|
|
52
|
+
function decoded(path) {
|
|
53
|
+
try {
|
|
54
|
+
return decodeURIComponent(path);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** dirOf 按 token 取插件前端的 webui/ 目录(realpath),没有这个 token 时回 undefined */
|
|
61
|
+
export function registerFrameRoutes(expressApp, dirOf) {
|
|
62
|
+
expressApp.use('/frames/:token', (req, res) => {
|
|
63
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
64
|
+
res.setHeader('Allow', 'GET, HEAD');
|
|
65
|
+
res.status(405).end();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const given = req.headers.host ?? '';
|
|
69
|
+
if (!HOST.test(given)) {
|
|
70
|
+
res.status(400).end();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// 代理转来的 Host 可能写成 x:443,经 TLS 终结的代理还可能写成 x:80,原样写进 CSP 会与页面的端口对不上
|
|
74
|
+
const host = given.replace(/:(?:80|443)$/, '');
|
|
75
|
+
const { token } = req.params;
|
|
76
|
+
const dir = token === BASE ? BASE_DIR : dirOf(token);
|
|
77
|
+
const path = decoded(req.path);
|
|
78
|
+
if (!dir || !path || !EXTENSIONS.has(extname(path).toLowerCase()) || !realWithin(dir, path)) {
|
|
79
|
+
res.status(404).end();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
express.static(dir, {
|
|
83
|
+
dotfiles: 'ignore',
|
|
84
|
+
index: false,
|
|
85
|
+
redirect: false,
|
|
86
|
+
setHeaders: served => {
|
|
87
|
+
served.setHeader('Content-Security-Policy', frameCsp(host, token));
|
|
88
|
+
served.setHeader('X-Content-Type-Options', 'nosniff');
|
|
89
|
+
// 框架能取外站的资源:不把框架地址带给外站
|
|
90
|
+
served.setHeader('Referrer-Policy', 'no-referrer');
|
|
91
|
+
// 框架文档是不透明来源,模块脚本连取自己目录下的文件也走 CORS
|
|
92
|
+
served.setHeader('Access-Control-Allow-Origin', '*');
|
|
93
|
+
served.setHeader('Cache-Control', 'no-cache');
|
|
94
|
+
},
|
|
95
|
+
})(req, res, () => res.status(404).end());
|
|
96
|
+
});
|
|
97
|
+
// token 段的 %xx 解不开时,路由在解参数这一步就抛错、走不到上面;默认的错误处理会回堆栈,这里一律回 404。
|
|
98
|
+
// 紧跟在挂载之后,前面只有渲染中间件(只包 res.json,不抛错)与加 CSP 的中间件,接到的错误只会来自这里
|
|
99
|
+
expressApp.use((_err, _req, res, _next) => {
|
|
100
|
+
res.status(404).end();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=frames.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frames.js","sourceRoot":"","sources":["../../src/routes/frames.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Contributions } from '@aalis/api-contributions';
|
|
2
|
+
import type { I18nService } from '@aalis/api-i18n';
|
|
3
|
+
import type { FrameTexts, PageLanguage } from '@aalis/api-webui';
|
|
4
|
+
import type { ServiceSelection } from '@aalis/core';
|
|
5
|
+
import type express from 'express';
|
|
6
|
+
import type { RouteGate } from '../gate.js';
|
|
7
|
+
import type { Localizer } from '../localize.js';
|
|
8
|
+
/** 界面的语言:全局层的语言链与服务进程的时区。GET /api/i18n/languages 回它,SPA 兜底也写进前端入口的页面 */
|
|
9
|
+
export declare const interfaceLanguage: (localizer: Pick<Localizer, "languages">) => PageLanguage;
|
|
10
|
+
interface I18nDeps {
|
|
11
|
+
i18n: Pick<ServiceSelection<I18nService>, 'present' | 'use'>;
|
|
12
|
+
/** 语言包的来源 */
|
|
13
|
+
contributions: Pick<ServiceSelection<Contributions>, 'present' | 'use'>;
|
|
14
|
+
localizer: Pick<Localizer, 'languages'>;
|
|
15
|
+
}
|
|
16
|
+
/** 插件前端的字典:服务进程目录里这个 ns(插件名)的条目,与语言包给它补的译文 */
|
|
17
|
+
export declare const frameTexts: (deps: I18nDeps, ns: string) => FrameTexts;
|
|
18
|
+
export declare function registerI18nRoutes(expressApp: express.Express, deps: I18nDeps, gate: RouteGate): void;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=i18n.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../src/routes/i18n.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAc,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAuBhD,wEAAwE;AACxE,eAAO,MAAM,iBAAiB,GAAI,WAAW,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,KAAG,YAG1E,CAAC;AAEH,UAAU,QAAQ;IAChB,IAAI,EAAE,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,EAAE,SAAS,GAAG,KAAK,CAAC,CAAC;IAC7D,aAAa;IACb,aAAa,EAAE,IAAI,CAAC,gBAAgB,CAAC,aAAa,CAAC,EAAE,SAAS,GAAG,KAAK,CAAC,CAAC;IACxE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;CACzC;AAuBD,8CAA8C;AAC9C,eAAO,MAAM,UAAU,GAAI,MAAM,QAAQ,EAAE,IAAI,MAAM,KAAG,UAGtD,CAAC;AAEH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAYrG"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// routes/i18n.ts — 多语言的两条路由
|
|
3
|
+
//
|
|
4
|
+
// GET /api/i18n/languages 给有文字可用的语言(供可选项与第三方客户端用;第一方前端只读后两样)、全局层的语言链与服务
|
|
5
|
+
// 进程的时区(前端的文字、格式与时间按它们,不读浏览器的语言与时区;后两样另写进前端入口的页面,见 interfaceLanguage);
|
|
6
|
+
// GET /api/i18n/messages 给前端自带的字典
|
|
7
|
+
// (外壳、契约包)补语言包的译文:取贡献点 i18n:pack 建索引,按 ns 与全局层的语言链筛,取舍(只补缺、指纹)
|
|
8
|
+
// 由前端的 resolveText 做。i18n 缺席时前端按原文显示,语言、语言链与补充都是空的,时区照给。插件前端的字典连同补充经
|
|
9
|
+
// frameTexts 交出(routes/pages.ts)。
|
|
10
|
+
// ============================================================
|
|
11
|
+
import { catalogEntries, indexPacks } from '@aalis/schema-i18n';
|
|
12
|
+
import { route } from '../route.js';
|
|
13
|
+
/** 语言标签的语言部分(zh-Hant-TW → zh)。收的都已规范化(语言链、语言包的语言),不会抛 */
|
|
14
|
+
const baseLanguage = (tag) => new Intl.Locale(tag).language;
|
|
15
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
16
|
+
/**
|
|
17
|
+
* 服务进程的时区。环境变量 TZ 写成 POSIX 串(CST-8)或文件路径(:/etc/localtime)时 Node 取不到时区名(undefined,也见过
|
|
18
|
+
* Etc/Unknown),进程的本地时间照样带着那个偏移:按现在的偏移写成 IANA 的 Etc/GMT±N(这一组的符号与偏移相反,东八区是
|
|
19
|
+
* Etc/GMT-8),不是整点时写 ±HH:MM,偏移为 0 是 UTC。这样写不含夏令时的切换,只与进程现在的偏移一致
|
|
20
|
+
*/
|
|
21
|
+
function processTimeZone() {
|
|
22
|
+
const name = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
23
|
+
if (name !== undefined && name !== 'Etc/Unknown')
|
|
24
|
+
return name;
|
|
25
|
+
const east = -new Date().getTimezoneOffset();
|
|
26
|
+
if (east === 0)
|
|
27
|
+
return 'UTC';
|
|
28
|
+
const minutes = Math.abs(east);
|
|
29
|
+
if (minutes % 60 === 0)
|
|
30
|
+
return `Etc/GMT${east > 0 ? '-' : '+'}${minutes / 60}`;
|
|
31
|
+
return `${east > 0 ? '+' : '-'}${pad(Math.floor(minutes / 60))}:${pad(minutes % 60)}`;
|
|
32
|
+
}
|
|
33
|
+
/** 界面的语言:全局层的语言链与服务进程的时区。GET /api/i18n/languages 回它,SPA 兜底也写进前端入口的页面 */
|
|
34
|
+
export const interfaceLanguage = (localizer) => ({
|
|
35
|
+
chain: [...localizer.languages()],
|
|
36
|
+
timeZone: processTimeZone(),
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* 语言包给这些 ns 补的译文:ns → 键 → 各语言包给的。只留全局层语言链里某种的语言相同的(zh-TW 读者遇到只有简体时用
|
|
40
|
+
* 简体),另留 en(链里都没有时按 en)。i18n 缺席时为空
|
|
41
|
+
*/
|
|
42
|
+
function packedTexts(deps, names) {
|
|
43
|
+
if (!deps.i18n.present)
|
|
44
|
+
return {};
|
|
45
|
+
const wanted = new Set([...deps.localizer.languages(), 'en'].map(baseLanguage));
|
|
46
|
+
const packs = deps.contributions.present ? deps.contributions.use.collect('i18n:pack').map(({ spec }) => spec) : [];
|
|
47
|
+
const index = indexPacks(packs);
|
|
48
|
+
const entries = {};
|
|
49
|
+
for (const name of new Set(names)) {
|
|
50
|
+
const keys = {};
|
|
51
|
+
for (const [key, candidates] of index.get(name) ?? []) {
|
|
52
|
+
const kept = candidates.filter(({ language }) => wanted.has(baseLanguage(language)));
|
|
53
|
+
if (kept.length > 0)
|
|
54
|
+
keys[key] = kept;
|
|
55
|
+
}
|
|
56
|
+
if (Object.keys(keys).length > 0)
|
|
57
|
+
entries[name] = keys;
|
|
58
|
+
}
|
|
59
|
+
return entries;
|
|
60
|
+
}
|
|
61
|
+
/** 插件前端的字典:服务进程目录里这个 ns(插件名)的条目,与语言包给它补的译文 */
|
|
62
|
+
export const frameTexts = (deps, ns) => ({
|
|
63
|
+
texts: catalogEntries().find(entry => entry.ns === ns) ?? null,
|
|
64
|
+
packs: packedTexts(deps, [ns])[ns] ?? {},
|
|
65
|
+
});
|
|
66
|
+
export function registerI18nRoutes(expressApp, deps, gate) {
|
|
67
|
+
route(expressApp, 'GET /api/i18n/languages', gate(), (_req, res) => {
|
|
68
|
+
res.json({
|
|
69
|
+
languages: deps.i18n.present ? [...deps.i18n.use.languages()] : [],
|
|
70
|
+
...interfaceLanguage(deps.localizer),
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
route(expressApp, 'GET /api/i18n/messages', gate(), (req, res) => {
|
|
74
|
+
const { ns } = req.query;
|
|
75
|
+
res.json({ entries: typeof ns === 'string' ? packedTexts(deps, ns.split(',')) : {} });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=i18n.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../../src/routes/i18n.ts"],"names":[],"mappings":"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"}
|