shennian 0.2.89 → 0.2.90
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/assets/wechat-channel/macos/manifest.json +13 -4
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +548 -0
- package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
- package/dist/src/agent-env.js +4 -105
- package/dist/src/agents/adapter.js +1 -19
- package/dist/src/agents/claude.js +8 -305
- package/dist/src/agents/codex-control.js +2 -188
- package/dist/src/agents/codex-utils.js +7 -200
- package/dist/src/agents/codex.js +15 -916
- package/dist/src/agents/command-spec.js +2 -413
- package/dist/src/agents/config-status.js +1 -226
- package/dist/src/agents/cursor.js +1 -249
- package/dist/src/agents/custom.js +4 -271
- package/dist/src/agents/detect.js +1 -56
- package/dist/src/agents/external-channel-instructions.js +10 -94
- package/dist/src/agents/gemini.js +1 -173
- package/dist/src/agents/manager.js +13 -157
- package/dist/src/agents/model-registry/cache.js +1 -37
- package/dist/src/agents/model-registry/discovery.js +2 -187
- package/dist/src/agents/model-registry/parsers.js +4 -447
- package/dist/src/agents/model-registry/runner.js +1 -30
- package/dist/src/agents/model-registry/service.js +1 -78
- package/dist/src/agents/model-registry/types.js +1 -8
- package/dist/src/agents/model-registry.js +1 -18
- package/dist/src/agents/openclaw.js +2 -275
- package/dist/src/agents/opencode.js +1 -231
- package/dist/src/agents/pi-context.js +12 -217
- package/dist/src/agents/pi.js +14 -723
- package/dist/src/agents/platform-instructions.js +9 -54
- package/dist/src/channels/base.js +1 -3
- package/dist/src/channels/registry.js +1 -30
- package/dist/src/channels/reply-split.js +10 -89
- package/dist/src/channels/runtime.js +5 -564
- package/dist/src/channels/secret-registry.js +1 -46
- package/dist/src/channels/websocket.js +8 -378
- package/dist/src/channels/wechat-channel/anchor.js +1 -65
- package/dist/src/channels/wechat-channel/client.js +1 -96
- package/dist/src/channels/wechat-channel/cooldown.js +1 -38
- package/dist/src/channels/wechat-channel/fingerprint.js +1 -71
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +10 -1
- package/dist/src/channels/wechat-channel/helper-assets.js +1 -68
- package/dist/src/channels/wechat-channel/helper-client.js +3 -149
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +1 -1
- package/dist/src/channels/wechat-channel/helper-protocol.js +1 -115
- package/dist/src/channels/wechat-channel/index.d.ts +1 -0
- package/dist/src/channels/wechat-channel/index.js +1 -19
- package/dist/src/channels/wechat-channel/ledger.js +1 -54
- package/dist/src/channels/wechat-channel/media-resolver.js +1 -181
- package/dist/src/channels/wechat-channel/message-key.js +1 -105
- package/dist/src/channels/wechat-channel/observer.js +1 -118
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +3 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -112
- package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
- package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
- package/dist/src/channels/wechat-channel/preflight.js +1 -48
- package/dist/src/channels/wechat-channel/runner.js +1 -84
- package/dist/src/channels/wechat-channel/runtime.js +1 -66
- package/dist/src/channels/wechat-channel/scheduler.d.ts +5 -0
- package/dist/src/channels/wechat-channel/scheduler.js +1 -152
- package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
- package/dist/src/channels/wechat-rpa/macos.js +6 -48
- package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
- package/dist/src/channels/wechat-rpa.js +6 -1028
- package/dist/src/channels/wecom.js +4 -357
- package/dist/src/commands/agent.js +6 -131
- package/dist/src/commands/daemon-windows.js +8 -48
- package/dist/src/commands/daemon.js +19 -1013
- package/dist/src/commands/external-attachments.js +1 -51
- package/dist/src/commands/external.js +1 -137
- package/dist/src/commands/manager.js +2 -391
- package/dist/src/commands/pair-qr.js +1 -6
- package/dist/src/commands/pair.js +9 -287
- package/dist/src/commands/tools.js +1 -34
- package/dist/src/commands/upgrade.js +1 -198
- package/dist/src/config/index.js +1 -35
- package/dist/src/daemon-log.js +6 -58
- package/dist/src/env-path.js +1 -64
- package/dist/src/fs/boundary.js +1 -126
- package/dist/src/fs/handler.js +1 -130
- package/dist/src/fs/security.js +1 -32
- package/dist/src/fs/text-decoder.js +1 -110
- package/dist/src/index.js +2 -404
- package/dist/src/log-reporter.js +1 -16
- package/dist/src/manager/prompt.js +29 -34
- package/dist/src/manager/registry.js +2 -269
- package/dist/src/manager/runtime.js +19 -1007
- package/dist/src/native-fusion/config.js +1 -5
- package/dist/src/native-fusion/opencode-parser.js +3 -123
- package/dist/src/native-fusion/parser-common.js +8 -264
- package/dist/src/native-fusion/parsers.js +8 -729
- package/dist/src/native-fusion/service.js +2 -225
- package/dist/src/native-fusion/state.js +1 -22
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/region.js +1 -88
- package/dist/src/relay/client.js +1 -343
- package/dist/src/session/archive-zip.js +1 -220
- package/dist/src/session/handlers/agent-config.js +1 -150
- package/dist/src/session/handlers/agents.js +1 -55
- package/dist/src/session/handlers/chat.js +2 -751
- package/dist/src/session/handlers/control.js +1 -55
- package/dist/src/session/handlers/fs.js +1 -783
- package/dist/src/session/handlers/session-refresh.js +1 -47
- package/dist/src/session/handlers/skills.js +1 -121
- package/dist/src/session/handlers/title.js +1 -60
- package/dist/src/session/handlers/tool-detail.js +1 -218
- package/dist/src/session/manager.js +1 -319
- package/dist/src/session/projection.js +1 -54
- package/dist/src/session/queue.js +4 -317
- package/dist/src/session/remote-attachments.js +1 -72
- package/dist/src/session/store.js +3 -109
- package/dist/src/session/types.js +1 -4
- package/dist/src/skills/registry.js +15 -148
- package/dist/src/skills/setup.js +1 -101
- package/dist/src/tools/markdown-to-pdf.js +10 -346
- package/dist/src/upgrade/engine.js +3 -347
- package/package.json +3 -2
package/dist/src/env-path.js
CHANGED
|
@@ -1,64 +1 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
const DEFAULT_POSIX_PATH = '/usr/local/bin:/usr/bin:/bin';
|
|
5
|
-
function uniquePush(parts, value, front = false) {
|
|
6
|
-
if (!value || parts.includes(value))
|
|
7
|
-
return;
|
|
8
|
-
if (front)
|
|
9
|
-
parts.unshift(value);
|
|
10
|
-
else
|
|
11
|
-
parts.push(value);
|
|
12
|
-
}
|
|
13
|
-
export function getUserBinPathCandidates(input = {}) {
|
|
14
|
-
const env = input.env ?? process.env;
|
|
15
|
-
const home = input.homedir ?? os.homedir();
|
|
16
|
-
const exists = input.exists ?? fs.existsSync;
|
|
17
|
-
const readdir = input.readdir ?? ((filePath) => fs.readdirSync(filePath));
|
|
18
|
-
const candidates = [];
|
|
19
|
-
if (process.platform === 'win32') {
|
|
20
|
-
const appData = env.APPDATA?.trim() || path.join(home, 'AppData', 'Roaming');
|
|
21
|
-
const localAppData = env.LOCALAPPDATA?.trim() || path.join(home, 'AppData', 'Local');
|
|
22
|
-
uniquePush(candidates, path.join(appData, 'npm'));
|
|
23
|
-
uniquePush(candidates, path.join(localAppData, 'pnpm'));
|
|
24
|
-
uniquePush(candidates, path.join(home, 'scoop', 'shims'));
|
|
25
|
-
uniquePush(candidates, path.join('C:\\', 'Program Files', 'nodejs'));
|
|
26
|
-
return candidates;
|
|
27
|
-
}
|
|
28
|
-
uniquePush(candidates, path.join(home, '.local', 'bin'));
|
|
29
|
-
uniquePush(candidates, path.join(home, '.bun', 'bin'));
|
|
30
|
-
uniquePush(candidates, path.join(home, '.npm-global', 'bin'));
|
|
31
|
-
uniquePush(candidates, path.join(home, '.npm', 'bin'));
|
|
32
|
-
uniquePush(candidates, path.join(home, '.cargo', 'bin'));
|
|
33
|
-
uniquePush(candidates, path.join(home, '.maestro', 'bin'));
|
|
34
|
-
const nvmDir = env.NVM_DIR?.trim() || path.join(home, '.nvm');
|
|
35
|
-
const nvmVersionsDir = path.join(nvmDir, 'versions', 'node');
|
|
36
|
-
try {
|
|
37
|
-
const nvmBins = readdir(nvmVersionsDir)
|
|
38
|
-
.sort((left, right) => right.localeCompare(left, undefined, { numeric: true }))
|
|
39
|
-
.map((version) => path.join(nvmVersionsDir, version, 'bin'))
|
|
40
|
-
.filter((candidate) => exists(candidate));
|
|
41
|
-
for (const candidate of nvmBins)
|
|
42
|
-
uniquePush(candidates, candidate);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
// nvm is optional.
|
|
46
|
-
}
|
|
47
|
-
uniquePush(candidates, '/opt/homebrew/bin');
|
|
48
|
-
uniquePush(candidates, '/usr/local/bin');
|
|
49
|
-
uniquePush(candidates, '/usr/bin');
|
|
50
|
-
uniquePush(candidates, '/bin');
|
|
51
|
-
uniquePush(candidates, '/usr/sbin');
|
|
52
|
-
uniquePush(candidates, '/sbin');
|
|
53
|
-
return candidates;
|
|
54
|
-
}
|
|
55
|
-
export function buildAugmentedPath(input = {}) {
|
|
56
|
-
const basePath = input.pathValue ?? input.env?.PATH ?? process.env.PATH ?? DEFAULT_POSIX_PATH;
|
|
57
|
-
const parts = basePath.split(path.delimiter).filter(Boolean);
|
|
58
|
-
for (const candidate of getUserBinPathCandidates(input))
|
|
59
|
-
uniquePush(parts, candidate);
|
|
60
|
-
return parts.join(path.delimiter);
|
|
61
|
-
}
|
|
62
|
-
export function augmentProcessPath() {
|
|
63
|
-
process.env.PATH = buildAugmentedPath();
|
|
64
|
-
}
|
|
1
|
+
import p from"node:fs";import f from"node:os";import i from"node:path";const u="/usr/local/bin:/usr/bin:/bin";function o(e,t,s=!1){!t||e.includes(t)||(s?e.unshift(t):e.push(t))}function j(e={}){const t=e.env??process.env,s=e.homedir??f.homedir(),c=e.exists??p.existsSync,d=e.readdir??(a=>p.readdirSync(a)),n=[];if(process.platform==="win32"){const a=t.APPDATA?.trim()||i.join(s,"AppData","Roaming"),r=t.LOCALAPPDATA?.trim()||i.join(s,"AppData","Local");return o(n,i.join(a,"npm")),o(n,i.join(r,"pnpm")),o(n,i.join(s,"scoop","shims")),o(n,i.join("C:\\","Program Files","nodejs")),n}o(n,i.join(s,".local","bin")),o(n,i.join(s,".bun","bin")),o(n,i.join(s,".npm-global","bin")),o(n,i.join(s,".npm","bin")),o(n,i.join(s,".cargo","bin")),o(n,i.join(s,".maestro","bin"));const l=t.NVM_DIR?.trim()||i.join(s,".nvm"),m=i.join(l,"versions","node");try{const a=d(m).sort((r,b)=>b.localeCompare(r,void 0,{numeric:!0})).map(r=>i.join(m,r,"bin")).filter(r=>c(r));for(const r of a)o(n,r)}catch{}return o(n,"/opt/homebrew/bin"),o(n,"/usr/local/bin"),o(n,"/usr/bin"),o(n,"/bin"),o(n,"/usr/sbin"),o(n,"/sbin"),n}function P(e={}){const s=(e.pathValue??e.env?.PATH??process.env.PATH??u).split(i.delimiter).filter(Boolean);for(const c of j(e))o(s,c);return s.join(i.delimiter)}function v(){process.env.PATH=P()}export{v as augmentProcessPath,P as buildAugmentedPath,j as getUserBinPathCandidates};
|
package/dist/src/fs/boundary.js
CHANGED
|
@@ -1,126 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// @test src/__tests__/fs-boundary.test.ts
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
const WINDOWS_ABSOLUTE_RE = /^[A-Za-z]:([\\/]|$)/;
|
|
7
|
-
const WINDOWS_UNC_RE = /^\\\\[^\\]+\\[^\\]+/;
|
|
8
|
-
const WINDOWS_DRIVE_RELATIVE_RE = /^[A-Za-z]:(?![\\/])/;
|
|
9
|
-
function isWindowsAbsolutePath(input) {
|
|
10
|
-
return WINDOWS_ABSOLUTE_RE.test(input) || WINDOWS_UNC_RE.test(input);
|
|
11
|
-
}
|
|
12
|
-
function hasWindowsDriveRelativePrefix(input) {
|
|
13
|
-
return WINDOWS_DRIVE_RELATIVE_RE.test(input);
|
|
14
|
-
}
|
|
15
|
-
function normalizeWindowsAbsolutePath(input) {
|
|
16
|
-
return input.replace(/^[/\\]([A-Za-z]:[\\/].*)$/, '$1');
|
|
17
|
-
}
|
|
18
|
-
function isHomeRelativePath(input) {
|
|
19
|
-
return input === '~' || input.startsWith('~/') || input.startsWith('~\\');
|
|
20
|
-
}
|
|
21
|
-
function pathFlavor(input) {
|
|
22
|
-
return isWindowsAbsolutePath(normalizeWindowsAbsolutePath(input)) ? 'win32' : 'posix';
|
|
23
|
-
}
|
|
24
|
-
function pathApi(flavor) {
|
|
25
|
-
return flavor === 'win32' ? path.win32 : path.posix;
|
|
26
|
-
}
|
|
27
|
-
function normalizeForCompare(input, flavor) {
|
|
28
|
-
const api = pathApi(flavor);
|
|
29
|
-
const normalized = api.normalize(input);
|
|
30
|
-
const trimmed = normalized.length > api.parse(normalized).root.length
|
|
31
|
-
? normalized.replace(/[\\/]+$/, '')
|
|
32
|
-
: normalized;
|
|
33
|
-
return flavor === 'win32' ? trimmed.toLowerCase() : trimmed;
|
|
34
|
-
}
|
|
35
|
-
function isWithinOrEqual(candidate, root, flavor) {
|
|
36
|
-
const api = pathApi(flavor);
|
|
37
|
-
const normalizedCandidate = normalizeForCompare(candidate, flavor);
|
|
38
|
-
const normalizedRoot = normalizeForCompare(root, flavor);
|
|
39
|
-
if (normalizedCandidate === normalizedRoot)
|
|
40
|
-
return true;
|
|
41
|
-
const relative = api.relative(normalizedRoot, normalizedCandidate);
|
|
42
|
-
return Boolean(relative) && !relative.startsWith('..') && !api.isAbsolute(relative);
|
|
43
|
-
}
|
|
44
|
-
function safeRealpath(pathValue) {
|
|
45
|
-
try {
|
|
46
|
-
return fs.realpathSync.native(pathValue);
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
export function resolveSessionWorkDir(input) {
|
|
53
|
-
if (!input)
|
|
54
|
-
return os.homedir();
|
|
55
|
-
const normalizedInput = normalizeWindowsAbsolutePath(input);
|
|
56
|
-
const flavor = pathFlavor(normalizedInput);
|
|
57
|
-
const api = pathApi(flavor);
|
|
58
|
-
if (isHomeRelativePath(normalizedInput)) {
|
|
59
|
-
return path.join(os.homedir(), normalizedInput.slice(1).replace(/^[/\\]+/, ''));
|
|
60
|
-
}
|
|
61
|
-
if (process.platform === 'win32') {
|
|
62
|
-
const normalized = normalizedInput.replace(/\\/g, '/');
|
|
63
|
-
if (normalized === '/tmp' || normalized.startsWith('/tmp/')) {
|
|
64
|
-
const suffix = normalized.slice('/tmp'.length).replace(/^\/+/, '');
|
|
65
|
-
return suffix ? path.win32.join(os.tmpdir(), ...suffix.split('/')) : os.tmpdir();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (isWindowsAbsolutePath(normalizedInput)) {
|
|
69
|
-
return path.win32.resolve(normalizedInput);
|
|
70
|
-
}
|
|
71
|
-
return api.resolve(normalizedInput);
|
|
72
|
-
}
|
|
73
|
-
export function createAuthorizedFsRoot(rawRoot) {
|
|
74
|
-
const normalized = resolveSessionWorkDir(rawRoot);
|
|
75
|
-
const flavor = pathFlavor(normalized);
|
|
76
|
-
const stat = safeStat(normalized);
|
|
77
|
-
const file = typeof stat?.isFile === 'function' ? stat.isFile() : false;
|
|
78
|
-
const boundary = file ? pathApi(flavor).dirname(normalized) : normalized;
|
|
79
|
-
return {
|
|
80
|
-
raw: rawRoot,
|
|
81
|
-
normalized,
|
|
82
|
-
boundary,
|
|
83
|
-
real: safeRealpath(boundary),
|
|
84
|
-
flavor,
|
|
85
|
-
file,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
export function resolveAuthorizedPath(rawPath, root) {
|
|
89
|
-
const input = String(rawPath || '').trim() || root.normalized;
|
|
90
|
-
const normalizedInput = isHomeRelativePath(input)
|
|
91
|
-
? resolveSessionWorkDir(input)
|
|
92
|
-
: normalizeWindowsAbsolutePath(input);
|
|
93
|
-
const api = pathApi(root.flavor);
|
|
94
|
-
if (hasWindowsDriveRelativePrefix(normalizedInput)) {
|
|
95
|
-
return { ok: false, error: `Access denied: ${rawPath}` };
|
|
96
|
-
}
|
|
97
|
-
const candidate = api.isAbsolute(normalizedInput)
|
|
98
|
-
? api.resolve(normalizedInput)
|
|
99
|
-
: api.resolve(root.boundary, normalizedInput);
|
|
100
|
-
if (!isWithinOrEqual(candidate, root.boundary, root.flavor)) {
|
|
101
|
-
return { ok: false, error: `Access denied: ${rawPath}` };
|
|
102
|
-
}
|
|
103
|
-
if (root.file && normalizeForCompare(candidate, root.flavor) !== normalizeForCompare(root.normalized, root.flavor)) {
|
|
104
|
-
return { ok: false, error: `Access denied: ${rawPath}` };
|
|
105
|
-
}
|
|
106
|
-
const realCandidate = safeRealpath(candidate);
|
|
107
|
-
if (realCandidate) {
|
|
108
|
-
const realRoot = root.real ?? safeRealpath(root.boundary);
|
|
109
|
-
if (realRoot && !isWithinOrEqual(realCandidate, realRoot, pathFlavor(realRoot))) {
|
|
110
|
-
return { ok: false, error: `Access denied: ${rawPath}` };
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return { ok: true, path: candidate, root };
|
|
114
|
-
}
|
|
115
|
-
export function isAuthorizedRootPath(pathValue, root) {
|
|
116
|
-
return isWithinOrEqual(pathValue, root.boundary, root.flavor) &&
|
|
117
|
-
normalizeForCompare(pathValue, root.flavor) === normalizeForCompare(root.boundary, root.flavor);
|
|
118
|
-
}
|
|
119
|
-
function safeStat(pathValue) {
|
|
120
|
-
try {
|
|
121
|
-
return fs.statSync(pathValue);
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
1
|
+
import v from"node:fs";import u from"node:os";import c from"node:path";const y=/^[A-Za-z]:([\\/]|$)/,R=/^\\\\[^\\]+\\[^\\]+/,b=/^[A-Za-z]:(?![\\/])/;function z(e){return y.test(e)||R.test(e)}function S(e){return b.test(e)}function d(e){return e.replace(/^[/\\]([A-Za-z]:[\\/].*)$/,"$1")}function A(e){return e==="~"||e.startsWith("~/")||e.startsWith("~\\")}function p(e){return z(d(e))?"win32":"posix"}function l(e){return e==="win32"?c.win32:c.posix}function a(e,n){const i=l(n),t=i.normalize(e),r=t.length>i.parse(t).root.length?t.replace(/[\\/]+$/,""):t;return n==="win32"?r.toLowerCase():r}function m(e,n,i){const t=l(i),r=a(e,i),o=a(n,i);if(r===o)return!0;const s=t.relative(o,r);return!!s&&!s.startsWith("..")&&!t.isAbsolute(s)}function h(e){try{return v.realpathSync.native(e)}catch{return null}}function W(e){if(!e)return u.homedir();const n=d(e),i=p(n),t=l(i);if(A(n))return c.join(u.homedir(),n.slice(1).replace(/^[/\\]+/,""));if(process.platform==="win32"){const r=n.replace(/\\/g,"/");if(r==="/tmp"||r.startsWith("/tmp/")){const o=r.slice(4).replace(/^\/+/,"");return o?c.win32.join(u.tmpdir(),...o.split("/")):u.tmpdir()}}return z(n)?c.win32.resolve(n):t.resolve(n)}function I(e){const n=W(e),i=p(n),t=x(n),r=typeof t?.isFile=="function"?t.isFile():!1,o=r?l(i).dirname(n):n;return{raw:e,normalized:n,boundary:o,real:h(o),flavor:i,file:r}}function _(e,n){const i=String(e||"").trim()||n.normalized,t=A(i)?W(i):d(i),r=l(n.flavor);if(S(t))return{ok:!1,error:`Access denied: ${e}`};const o=r.isAbsolute(t)?r.resolve(t):r.resolve(n.boundary,t);if(!m(o,n.boundary,n.flavor))return{ok:!1,error:`Access denied: ${e}`};if(n.file&&a(o,n.flavor)!==a(n.normalized,n.flavor))return{ok:!1,error:`Access denied: ${e}`};const s=h(o);if(s){const f=n.real??h(n.boundary);if(f&&!m(s,f,p(f)))return{ok:!1,error:`Access denied: ${e}`}}return{ok:!0,path:o,root:n}}function k(e,n){return m(e,n.boundary,n.flavor)&&a(e,n.flavor)===a(n.boundary,n.flavor)}function x(e){try{return v.statSync(e)}catch{return null}}export{I as createAuthorizedFsRoot,k as isAuthorizedRootPath,_ as resolveAuthorizedPath,W as resolveSessionWorkDir};
|
package/dist/src/fs/handler.js
CHANGED
|
@@ -1,130 +1 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { normalizePath, isPathSafe } from './security.js';
|
|
5
|
-
const FILE_SYSTEM_ROOTS_PATH = '__roots__';
|
|
6
|
-
const MAX_READ_SIZE = 100 * 1024;
|
|
7
|
-
const MAX_TRANSFER_SIZE = 20 * 1024 * 1024;
|
|
8
|
-
const CHUNK_SIZE = 64 * 1024;
|
|
9
|
-
const RATE_LIMIT_BYTES_PER_SEC = 5 * 1024 * 1024;
|
|
10
|
-
function sleep(ms) {
|
|
11
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
12
|
-
}
|
|
13
|
-
function listFileSystemRoots() {
|
|
14
|
-
if (os.platform() === 'win32') {
|
|
15
|
-
const entries = [];
|
|
16
|
-
for (let code = 65; code <= 90; code += 1) {
|
|
17
|
-
const drive = `${String.fromCharCode(code)}:\\`;
|
|
18
|
-
if (!fs.existsSync(drive))
|
|
19
|
-
continue;
|
|
20
|
-
entries.push({ name: drive, path: drive, isDir: true });
|
|
21
|
-
}
|
|
22
|
-
return { entries };
|
|
23
|
-
}
|
|
24
|
-
return { entries: [{ name: '/', path: '/', isDir: true }] };
|
|
25
|
-
}
|
|
26
|
-
export function handleFsLs(reqPath) {
|
|
27
|
-
if (reqPath === FILE_SYSTEM_ROOTS_PATH) {
|
|
28
|
-
return listFileSystemRoots();
|
|
29
|
-
}
|
|
30
|
-
const normalized = normalizePath(reqPath);
|
|
31
|
-
if (!isPathSafe(normalized)) {
|
|
32
|
-
return { error: `Access denied: ${reqPath}` };
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
const dirents = fs.readdirSync(normalized, { withFileTypes: true });
|
|
36
|
-
const entries = dirents.map((d) => {
|
|
37
|
-
const fullPath = path.join(normalized, d.name);
|
|
38
|
-
const isDir = d.isDirectory();
|
|
39
|
-
const entry = { name: d.name, path: fullPath, isDir };
|
|
40
|
-
try {
|
|
41
|
-
const stat = fs.statSync(fullPath);
|
|
42
|
-
entry.size = isDir ? undefined : stat.size;
|
|
43
|
-
entry.modifiedAt = stat.mtimeMs;
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
// stat failure is non-fatal
|
|
47
|
-
}
|
|
48
|
-
return entry;
|
|
49
|
-
});
|
|
50
|
-
return { entries };
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
return { error: `Failed to list ${reqPath}: ${err.message}` };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
export function handleFsRead(reqPath) {
|
|
57
|
-
const normalized = normalizePath(reqPath);
|
|
58
|
-
if (!isPathSafe(normalized)) {
|
|
59
|
-
return { error: `Access denied: ${reqPath}` };
|
|
60
|
-
}
|
|
61
|
-
try {
|
|
62
|
-
const stat = fs.statSync(normalized);
|
|
63
|
-
if (stat.isDirectory()) {
|
|
64
|
-
return { error: `Not a file: ${reqPath}` };
|
|
65
|
-
}
|
|
66
|
-
if (stat.size > MAX_READ_SIZE) {
|
|
67
|
-
return { error: `File too large (${stat.size} bytes, max ${MAX_READ_SIZE})` };
|
|
68
|
-
}
|
|
69
|
-
const content = fs.readFileSync(normalized, 'utf-8');
|
|
70
|
-
return { path: normalized, content };
|
|
71
|
-
}
|
|
72
|
-
catch (err) {
|
|
73
|
-
return { error: `Failed to read ${reqPath}: ${err.message}` };
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
export async function* handleFsDownload(req, onProgress) {
|
|
77
|
-
const filePath = req.path;
|
|
78
|
-
if (!filePath) {
|
|
79
|
-
throw new Error('Missing path for download');
|
|
80
|
-
}
|
|
81
|
-
const normalized = normalizePath(filePath);
|
|
82
|
-
if (!isPathSafe(normalized)) {
|
|
83
|
-
throw new Error(`Access denied: ${filePath}`);
|
|
84
|
-
}
|
|
85
|
-
const stat = fs.statSync(normalized);
|
|
86
|
-
if (stat.size > MAX_TRANSFER_SIZE) {
|
|
87
|
-
throw new Error(`File too large (${stat.size} bytes, max ${MAX_TRANSFER_SIZE})`);
|
|
88
|
-
}
|
|
89
|
-
const buf = fs.readFileSync(normalized);
|
|
90
|
-
const total = Math.ceil(buf.length / CHUNK_SIZE);
|
|
91
|
-
const chunksPerSec = Math.floor(RATE_LIMIT_BYTES_PER_SEC / CHUNK_SIZE);
|
|
92
|
-
const delayMs = chunksPerSec > 0 ? Math.ceil(1000 / chunksPerSec) : 0;
|
|
93
|
-
for (let seq = 0; seq < total; seq++) {
|
|
94
|
-
const start = seq * CHUNK_SIZE;
|
|
95
|
-
const end = Math.min(start + CHUNK_SIZE, buf.length);
|
|
96
|
-
const data = buf.subarray(start, end).toString('base64');
|
|
97
|
-
yield { transferId: req.transferId, seq, total, data };
|
|
98
|
-
onProgress?.({
|
|
99
|
-
transferId: req.transferId,
|
|
100
|
-
seq,
|
|
101
|
-
total,
|
|
102
|
-
bytesSent: end,
|
|
103
|
-
bytesTotal: buf.length,
|
|
104
|
-
});
|
|
105
|
-
if (delayMs > 0 && seq < total - 1) {
|
|
106
|
-
await sleep(delayMs);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
export function handleFsUploadInit(req) {
|
|
111
|
-
const targetPath = req.targetPath ?? req.path;
|
|
112
|
-
if (!targetPath) {
|
|
113
|
-
return { error: 'Missing targetPath for upload' };
|
|
114
|
-
}
|
|
115
|
-
const normalized = normalizePath(targetPath);
|
|
116
|
-
if (!isPathSafe(normalized)) {
|
|
117
|
-
return { error: `Access denied: ${targetPath}` };
|
|
118
|
-
}
|
|
119
|
-
if (req.size != null && req.size > MAX_TRANSFER_SIZE) {
|
|
120
|
-
return { error: `File too large (${req.size} bytes, max ${MAX_TRANSFER_SIZE})` };
|
|
121
|
-
}
|
|
122
|
-
const dir = path.dirname(normalized);
|
|
123
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
124
|
-
return { targetPath: normalized };
|
|
125
|
-
}
|
|
126
|
-
export function handleFsUploadChunk(targetPath, chunk) {
|
|
127
|
-
const buf = Buffer.from(chunk.data, 'base64');
|
|
128
|
-
const flag = chunk.seq === 0 ? 'w' : 'a';
|
|
129
|
-
fs.writeFileSync(targetPath, buf, { flag });
|
|
130
|
-
}
|
|
1
|
+
import o from"node:fs";import _ from"node:os";import S from"node:path";import{normalizePath as f,isPathSafe as u}from"./security.js";const $="__roots__",g=100*1024,m=20*1024*1024,h=64*1024,w=5*1024*1024;function z(t){return new Promise(e=>setTimeout(e,t))}function E(){if(_.platform()==="win32"){const t=[];for(let e=65;e<=90;e+=1){const r=`${String.fromCharCode(e)}:\\`;o.existsSync(r)&&t.push({name:r,path:r,isDir:!0})}return{entries:t}}return{entries:[{name:"/",path:"/",isDir:!0}]}}function T(t){if(t===$)return E();const e=f(t);if(!u(e))return{error:`Access denied: ${t}`};try{return{entries:o.readdirSync(e,{withFileTypes:!0}).map(a=>{const s=S.join(e,a.name),i=a.isDirectory(),c={name:a.name,path:s,isDir:i};try{const d=o.statSync(s);c.size=i?void 0:d.size,c.modifiedAt=d.mtimeMs}catch{}return c})}}catch(r){return{error:`Failed to list ${t}: ${r.message}`}}}function x(t){const e=f(t);if(!u(e))return{error:`Access denied: ${t}`};try{const r=o.statSync(e);if(r.isDirectory())return{error:`Not a file: ${t}`};if(r.size>g)return{error:`File too large (${r.size} bytes, max ${g})`};const n=o.readFileSync(e,"utf-8");return{path:e,content:n}}catch(r){return{error:`Failed to read ${t}: ${r.message}`}}}async function*R(t,e){const r=t.path;if(!r)throw new Error("Missing path for download");const n=f(r);if(!u(n))throw new Error(`Access denied: ${r}`);const a=o.statSync(n);if(a.size>m)throw new Error(`File too large (${a.size} bytes, max ${m})`);const s=o.readFileSync(n),i=Math.ceil(s.length/h),c=Math.floor(w/h),d=c>0?Math.ceil(1e3/c):0;for(let l=0;l<i;l++){const y=l*h,p=Math.min(y+h,s.length),F=s.subarray(y,p).toString("base64");yield{transferId:t.transferId,seq:l,total:i,data:F},e?.({transferId:t.transferId,seq:l,total:i,bytesSent:p,bytesTotal:s.length}),d>0&&l<i-1&&await z(d)}}function D(t){const e=t.targetPath??t.path;if(!e)return{error:"Missing targetPath for upload"};const r=f(e);if(!u(r))return{error:`Access denied: ${e}`};if(t.size!=null&&t.size>m)return{error:`File too large (${t.size} bytes, max ${m})`};const n=S.dirname(r);return o.mkdirSync(n,{recursive:!0}),{targetPath:r}}function P(t,e){const r=Buffer.from(e.data,"base64"),n=e.seq===0?"w":"a";o.writeFileSync(t,r,{flag:n})}export{R as handleFsDownload,T as handleFsLs,x as handleFsRead,P as handleFsUploadChunk,D as handleFsUploadInit};
|
package/dist/src/fs/security.js
CHANGED
|
@@ -1,32 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
const FILE_SYSTEM_ROOTS_PATH = '__roots__';
|
|
4
|
-
const ALLOWED_ROOTS = [os.homedir()];
|
|
5
|
-
const BLOCKED_PATTERNS = [
|
|
6
|
-
/\.\./, // directory traversal
|
|
7
|
-
/\/\.ssh\//,
|
|
8
|
-
/\/\.gnupg\//,
|
|
9
|
-
/\/\.aws\/credentials/,
|
|
10
|
-
/\/\.env$/,
|
|
11
|
-
/\/\.env\./,
|
|
12
|
-
];
|
|
13
|
-
export function normalizePath(inputPath) {
|
|
14
|
-
if (inputPath === FILE_SYSTEM_ROOTS_PATH)
|
|
15
|
-
return inputPath;
|
|
16
|
-
const expanded = inputPath.startsWith('~')
|
|
17
|
-
? path.join(os.homedir(), inputPath.slice(1))
|
|
18
|
-
: inputPath;
|
|
19
|
-
return path.resolve(expanded);
|
|
20
|
-
}
|
|
21
|
-
export function isPathSafe(normalizedPath) {
|
|
22
|
-
if (normalizedPath === FILE_SYSTEM_ROOTS_PATH)
|
|
23
|
-
return true;
|
|
24
|
-
const withinAllowed = ALLOWED_ROOTS.some((root) => normalizedPath === root || normalizedPath.startsWith(root + path.sep));
|
|
25
|
-
if (!withinAllowed)
|
|
26
|
-
return false;
|
|
27
|
-
for (const pattern of BLOCKED_PATTERNS) {
|
|
28
|
-
if (pattern.test(normalizedPath))
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
1
|
+
import t from"node:path";import o from"node:os";const n="__roots__",i=[o.homedir()],f=[/\.\./,/\/\.ssh\//,/\/\.gnupg\//,/\/\.aws\/credentials/,/\/\.env$/,/\/\.env\./];function l(e){if(e===n)return e;const s=e.startsWith("~")?t.join(o.homedir(),e.slice(1)):e;return t.resolve(s)}function u(e){if(e===n)return!0;if(!i.some(r=>e===r||e.startsWith(r+t.sep)))return!1;for(const r of f)if(r.test(e))return!1;return!0}export{u as isPathSafe,l as normalizePath};
|
|
@@ -1,110 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// @test src/__tests__/session-manager.test.ts
|
|
3
|
-
import { TextDecoder } from 'node:util';
|
|
4
|
-
const AUTO_TEXT_ENCODINGS = new Set(['auto', 'utf8-auto', 'utf-8-auto', 'text-auto']);
|
|
5
|
-
const TEXT_SAMPLE_BYTES = 8192;
|
|
6
|
-
const MAX_SUSPICIOUS_CONTROL_RATIO = 0.01;
|
|
7
|
-
export class BinaryTextPreviewError extends Error {
|
|
8
|
-
constructor() {
|
|
9
|
-
super('File appears to be binary; use encoding=base64');
|
|
10
|
-
this.name = 'BinaryTextPreviewError';
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
export function isAutoTextEncoding(value) {
|
|
14
|
-
return AUTO_TEXT_ENCODINGS.has(value.trim().toLowerCase());
|
|
15
|
-
}
|
|
16
|
-
function startsWithBytes(buffer, bytes) {
|
|
17
|
-
if (buffer.length < bytes.length)
|
|
18
|
-
return false;
|
|
19
|
-
return bytes.every((byte, index) => buffer[index] === byte);
|
|
20
|
-
}
|
|
21
|
-
function isAllowedTextControlByte(byte) {
|
|
22
|
-
return byte === 0x09 || byte === 0x0a || byte === 0x0c || byte === 0x0d || byte === 0x1b;
|
|
23
|
-
}
|
|
24
|
-
function hasBinaryMagic(buffer) {
|
|
25
|
-
return (startsWithBytes(buffer, [0x25, 0x50, 0x44, 0x46]) || // PDF
|
|
26
|
-
startsWithBytes(buffer, [0x50, 0x4b, 0x03, 0x04]) || // ZIP / Office Open XML
|
|
27
|
-
startsWithBytes(buffer, [0x50, 0x4b, 0x05, 0x06]) ||
|
|
28
|
-
startsWithBytes(buffer, [0x50, 0x4b, 0x07, 0x08]) ||
|
|
29
|
-
startsWithBytes(buffer, [0x89, 0x50, 0x4e, 0x47]) || // PNG
|
|
30
|
-
startsWithBytes(buffer, [0xff, 0xd8, 0xff]) || // JPEG
|
|
31
|
-
startsWithBytes(buffer, [0x47, 0x49, 0x46, 0x38]) || // GIF
|
|
32
|
-
startsWithBytes(buffer, [0x52, 0x49, 0x46, 0x46]) || // RIFF / WebP / WAV / AVI
|
|
33
|
-
startsWithBytes(buffer, [0x1f, 0x8b]) || // gzip
|
|
34
|
-
startsWithBytes(buffer, [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c]) || // 7z
|
|
35
|
-
startsWithBytes(buffer, [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07]) || // RAR
|
|
36
|
-
startsWithBytes(buffer, [0x7f, 0x45, 0x4c, 0x46]) || // ELF
|
|
37
|
-
startsWithBytes(buffer, [0xcf, 0xfa, 0xed, 0xfe]) || // Mach-O
|
|
38
|
-
startsWithBytes(buffer, [0xca, 0xfe, 0xba, 0xbe]) ||
|
|
39
|
-
startsWithBytes(buffer, [0x4d, 0x5a]) || // Windows PE
|
|
40
|
-
startsWithBytes(buffer, [0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66]));
|
|
41
|
-
}
|
|
42
|
-
function looksLikeBinaryBuffer(buffer) {
|
|
43
|
-
if (hasBinaryMagic(buffer))
|
|
44
|
-
return true;
|
|
45
|
-
const sample = buffer.subarray(0, Math.min(buffer.length, TEXT_SAMPLE_BYTES));
|
|
46
|
-
if (sample.length === 0)
|
|
47
|
-
return false;
|
|
48
|
-
let suspiciousControls = 0;
|
|
49
|
-
for (const byte of sample) {
|
|
50
|
-
if (byte === 0x00)
|
|
51
|
-
return true;
|
|
52
|
-
if (byte < 0x20 && !isAllowedTextControlByte(byte))
|
|
53
|
-
suspiciousControls += 1;
|
|
54
|
-
}
|
|
55
|
-
return suspiciousControls / sample.length > MAX_SUSPICIOUS_CONTROL_RATIO;
|
|
56
|
-
}
|
|
57
|
-
function isAllowedTextControlCode(codePoint) {
|
|
58
|
-
return codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0c || codePoint === 0x0d || codePoint === 0x1b;
|
|
59
|
-
}
|
|
60
|
-
function hasDecodedControlNoise(content) {
|
|
61
|
-
if (!content)
|
|
62
|
-
return false;
|
|
63
|
-
let suspiciousControls = 0;
|
|
64
|
-
let total = 0;
|
|
65
|
-
for (const char of content.slice(0, TEXT_SAMPLE_BYTES)) {
|
|
66
|
-
const codePoint = char.codePointAt(0) ?? 0;
|
|
67
|
-
total += 1;
|
|
68
|
-
if (((codePoint >= 0x00 && codePoint < 0x20) || (codePoint >= 0x7f && codePoint <= 0x9f)) &&
|
|
69
|
-
!isAllowedTextControlCode(codePoint)) {
|
|
70
|
-
suspiciousControls += 1;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return total > 0 && suspiciousControls / total > MAX_SUSPICIOUS_CONTROL_RATIO;
|
|
74
|
-
}
|
|
75
|
-
function decodeStrict(buffer, decoderEncoding, encoding, fallback) {
|
|
76
|
-
const content = new TextDecoder(decoderEncoding, { fatal: true }).decode(buffer);
|
|
77
|
-
if (hasDecodedControlNoise(content))
|
|
78
|
-
throw new BinaryTextPreviewError();
|
|
79
|
-
return { content, encoding, fallback };
|
|
80
|
-
}
|
|
81
|
-
export function decodeTextBufferAuto(buffer) {
|
|
82
|
-
if (buffer.length === 0)
|
|
83
|
-
return { content: '', encoding: 'utf8', fallback: false };
|
|
84
|
-
if (startsWithBytes(buffer, [0xef, 0xbb, 0xbf])) {
|
|
85
|
-
return decodeStrict(buffer, 'utf-8', 'utf8', false);
|
|
86
|
-
}
|
|
87
|
-
if (startsWithBytes(buffer, [0xff, 0xfe])) {
|
|
88
|
-
return decodeStrict(buffer, 'utf-16le', 'utf16le', true);
|
|
89
|
-
}
|
|
90
|
-
if (startsWithBytes(buffer, [0xfe, 0xff])) {
|
|
91
|
-
return decodeStrict(buffer, 'utf-16be', 'utf16be', true);
|
|
92
|
-
}
|
|
93
|
-
if (looksLikeBinaryBuffer(buffer))
|
|
94
|
-
throw new BinaryTextPreviewError();
|
|
95
|
-
try {
|
|
96
|
-
return decodeStrict(buffer, 'utf-8', 'utf8', false);
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
if (error instanceof BinaryTextPreviewError)
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
102
|
-
try {
|
|
103
|
-
return decodeStrict(buffer, 'gb18030', 'gb18030', true);
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
if (error instanceof BinaryTextPreviewError)
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
109
|
-
return decodeStrict(buffer, 'windows-1252', 'windows-1252', true);
|
|
110
|
-
}
|
|
1
|
+
import{TextDecoder as l}from"node:util";const u=new Set(["auto","utf8-auto","utf-8-auto","text-auto"]),s=8192,c=.01;class a extends Error{constructor(){super("File appears to be binary; use encoding=base64"),this.name="BinaryTextPreviewError"}}function p(t){return u.has(t.trim().toLowerCase())}function x(t,n){return t.length<n.length?!1:n.every((e,o)=>t[o]===e)}function d(t){return t===9||t===10||t===12||t===13||t===27}function f(t){return x(t,[37,80,68,70])||x(t,[80,75,3,4])||x(t,[80,75,5,6])||x(t,[80,75,7,8])||x(t,[137,80,78,71])||x(t,[255,216,255])||x(t,[71,73,70,56])||x(t,[82,73,70,70])||x(t,[31,139])||x(t,[55,122,188,175,39,28])||x(t,[82,97,114,33,26,7])||x(t,[127,69,76,70])||x(t,[207,250,237,254])||x(t,[202,254,186,190])||x(t,[77,90])||x(t,[83,81,76,105,116,101,32,102])}function h(t){if(f(t))return!0;const n=t.subarray(0,Math.min(t.length,s));if(n.length===0)return!1;let e=0;for(const o of n){if(o===0)return!0;o<32&&!d(o)&&(e+=1)}return e/n.length>c}function w(t){return t===9||t===10||t===12||t===13||t===27}function T(t){if(!t)return!1;let n=0,e=0;for(const o of t.slice(0,s)){const r=o.codePointAt(0)??0;e+=1,(r>=0&&r<32||r>=127&&r<=159)&&!w(r)&&(n+=1)}return e>0&&n/e>c}function i(t,n,e,o){const r=new l(n,{fatal:!0}).decode(t);if(T(r))throw new a;return{content:r,encoding:e,fallback:o}}function C(t){if(t.length===0)return{content:"",encoding:"utf8",fallback:!1};if(x(t,[239,187,191]))return i(t,"utf-8","utf8",!1);if(x(t,[255,254]))return i(t,"utf-16le","utf16le",!0);if(x(t,[254,255]))return i(t,"utf-16be","utf16be",!0);if(h(t))throw new a;try{return i(t,"utf-8","utf8",!1)}catch(n){if(n instanceof a)throw n}try{return i(t,"gb18030","gb18030",!0)}catch(n){if(n instanceof a)throw n}return i(t,"windows-1252","windows-1252",!0)}export{a as BinaryTextPreviewError,C as decodeTextBufferAuto,p as isAutoTextEncoding};
|