@triflux/core 10.77.0 → 10.79.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/core",
3
- "version": "10.77.0",
3
+ "version": "10.79.0",
4
4
  "description": "triflux core — CLI routing, adapters. Zero native dependencies.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
@@ -112,12 +112,12 @@ function trustedPackageTree(packageRoot, nodePath, tmuxPath) {
112
112
  !existing(resolvedNode)?.isFile()
113
113
  )
114
114
  return false;
115
+ // Homebrew prefix는 git 저장소일 수 있으므로 node_modules까지만 검사한다.
115
116
  let ancestor = packageRoot;
116
117
  while (true) {
117
118
  if (existing(path.join(ancestor, ".git"))) return false;
118
- const parent = path.dirname(ancestor);
119
- if (parent === ancestor) break;
120
- ancestor = parent;
119
+ if (ancestor === path.dirname(packageRoot)) break;
120
+ ancestor = path.dirname(ancestor);
121
121
  }
122
122
  const stack = [packageRoot];
123
123
  while (stack.length) {
@@ -125,6 +125,7 @@ function trustedPackageTree(packageRoot, nodePath, tmuxPath) {
125
125
  const meta = existing(current);
126
126
  if (
127
127
  !meta ||
128
+ path.basename(current).toLowerCase() === ".git" ||
128
129
  meta.isSymbolicLink() ||
129
130
  ![0, process.getuid()].includes(meta.uid) ||
130
131
  (meta.mode & 0o002) !== 0
@@ -232,8 +233,8 @@ export function inspectPeerPermission({
232
233
  tmuxPath,
233
234
  platform = process.platform,
234
235
  } = {}) {
236
+ if (platform === "win32") return { status: "unsupported" };
235
237
  const plan = peerPermissionPlan({ home, packageRoot, nodePath, tmuxPath });
236
- if (platform === "win32") return { status: "unsupported", plan };
237
238
  const effectiveNodePath = nodePath || resolveStableNodeBin();
238
239
  if (
239
240
  !trustedPackageTree(
@@ -334,12 +335,9 @@ export function installPeerPermission({
334
335
  platform,
335
336
  });
336
337
  const { plan, status } = inspected;
338
+ if (status === "unsupported") return { status, changed: false };
337
339
  if (status === "approved") return { status: "approved", changed: false };
338
- if (
339
- status === "conflict" ||
340
- status === "unavailable" ||
341
- status === "unsupported"
342
- )
340
+ if (status === "conflict" || status === "unavailable")
343
341
  return {
344
342
  status,
345
343
  changed: false,
@@ -502,8 +500,10 @@ export async function offerPeerPermission({
502
500
  tmuxPath,
503
501
  platform,
504
502
  });
505
- if (state.status === "unsupported")
503
+ if (state.status === "unsupported") {
504
+ notice("Windows 는 자동 승인 없이 tfx-peer 를 직접 실행한다");
506
505
  return { status: "unsupported", changed: false };
506
+ }
507
507
  if (state.status === "approved")
508
508
  return { status: "approved", changed: false };
509
509
  if (fromUpdate) {
@@ -0,0 +1,137 @@
1
+ import { execFileSync } from "node:child_process";
2
+
3
+ const WINDOWS_POWERSHELL = String.raw`C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`;
4
+
5
+ function windowsPowerShellEnv() {
6
+ // PowerShell 7의 모듈 경로를 빼서 Windows PowerShell 5.1이 기본값을 복원하게 한다.
7
+ return Object.fromEntries(
8
+ Object.entries(process.env).filter(([key]) =>
9
+ ["systemroot", "windir", "temp", "tmp", "userprofile"].includes(
10
+ key.toLowerCase(),
11
+ ),
12
+ ),
13
+ );
14
+ }
15
+
16
+ export function readWindowsPeerState(
17
+ files,
18
+ { pids = [], execFile = execFileSync } = {},
19
+ ) {
20
+ if (!files.length && !pids.length)
21
+ return { acls: new Map(), procStarts: new Map() };
22
+ const script = `
23
+ $ErrorActionPreference = 'Stop'
24
+ [Console]::InputEncoding = [Text.UTF8Encoding]::new($false)
25
+ [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
26
+ $request = ConvertFrom-Json ([Console]::In.ReadToEnd())
27
+ $userSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
28
+ $acls = @(foreach ($p in $request.paths) {
29
+ try {
30
+ $item = if ([System.IO.Directory]::Exists($p)) { [System.IO.DirectoryInfo]::new($p) } else { [System.IO.FileInfo]::new($p) }
31
+ $acl = $item.GetAccessControl()
32
+ [pscustomobject]@{
33
+ path = $p
34
+ userSid = $userSid
35
+ ownerSid = $acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value
36
+ reparsePoint = [bool]($item.Attributes -band [IO.FileAttributes]::ReparsePoint)
37
+ access = @(foreach ($rule in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) {
38
+ [pscustomobject]@{
39
+ sid = $rule.IdentityReference.Value
40
+ rights = [long]$rule.FileSystemRights
41
+ type = $rule.AccessControlType.ToString()
42
+ inheritanceFlags = [int]$rule.InheritanceFlags
43
+ propagationFlags = [int]$rule.PropagationFlags
44
+ }
45
+ })
46
+ }
47
+ } catch {
48
+ [pscustomobject]@{ path = $p; error = $_.Exception.Message }
49
+ }
50
+ })
51
+ $processes = @(foreach ($processId in $request.pids) {
52
+ try {
53
+ $start = [System.Diagnostics.Process]::GetProcessById($processId).StartTime.ToFileTimeUtc().ToString([Globalization.CultureInfo]::InvariantCulture)
54
+ [pscustomobject]@{ pid = $processId; procStart = $start }
55
+ } catch {
56
+ [pscustomobject]@{ pid = $processId; error = $_.Exception.Message }
57
+ }
58
+ })
59
+ ConvertTo-Json -InputObject @{ acls = $acls; processes = $processes } -Depth 5 -Compress
60
+ `;
61
+ const rows = JSON.parse(
62
+ execFile(
63
+ WINDOWS_POWERSHELL,
64
+ ["-NoProfile", "-NonInteractive", "-Command", script],
65
+ {
66
+ input: JSON.stringify({ paths: files, pids }),
67
+ encoding: "utf8",
68
+ timeout: 10000,
69
+ maxBuffer: 32 * 1024 * 1024,
70
+ windowsHide: true,
71
+ env: windowsPowerShellEnv(),
72
+ },
73
+ ),
74
+ );
75
+ if (!Array.isArray(rows.acls) || !Array.isArray(rows.processes))
76
+ throw new Error("Invalid Windows peer response");
77
+ return {
78
+ acls: new Map(
79
+ rows.acls
80
+ .filter((row) => !Object.hasOwn(row, "error"))
81
+ .map((row) => [row.path, row]),
82
+ ),
83
+ procStarts: new Map(
84
+ rows.processes
85
+ .filter(
86
+ (row) =>
87
+ !Object.hasOwn(row, "error") &&
88
+ typeof row.procStart === "string" &&
89
+ /^\d+$/.test(row.procStart),
90
+ )
91
+ .map((row) => [row.pid, row.procStart]),
92
+ ),
93
+ };
94
+ }
95
+
96
+ export function readWindowsAcls(files, options) {
97
+ return readWindowsPeerState(files, options).acls;
98
+ }
99
+
100
+ export function assertWindowsAcl(acl, kind, { privateMode = true } = {}) {
101
+ const trusted = new Set([acl?.userSid, "S-1-5-18", "S-1-5-32-544"]);
102
+ // 개별 쓰기·삭제·ACL 변경과 generic write/all을 함께 거른다.
103
+ const writes =
104
+ 0x2 |
105
+ 0x4 |
106
+ 0x10 |
107
+ 0x40 |
108
+ 0x100 |
109
+ 0x10000 |
110
+ 0x40000 |
111
+ 0x80000 |
112
+ 0x40000000 |
113
+ 0x10000000;
114
+ if (
115
+ !/^S-1-\d+(?:-\d+)+$/.test(acl?.userSid || "") ||
116
+ !trusted.has(acl.ownerSid) ||
117
+ acl.reparsePoint !== false ||
118
+ !Array.isArray(acl.access) ||
119
+ !acl.access.length ||
120
+ acl.access.some(
121
+ (ace) =>
122
+ !/^S-1-\d+(?:-\d+)+$/.test(ace.sid || "") ||
123
+ !["Allow", "Deny"].includes(ace.type) ||
124
+ !Number.isInteger(ace.rights) ||
125
+ ace.rights < -2147483648 ||
126
+ ace.rights > 0xffffffff ||
127
+ (ace.type === "Allow" &&
128
+ !(
129
+ Number.isInteger(ace.propagationFlags) &&
130
+ (ace.propagationFlags & 2) !== 0
131
+ ) &&
132
+ !trusted.has(ace.sid) &&
133
+ (privateMode || (ace.rights & writes) !== 0)),
134
+ )
135
+ )
136
+ throw new Error(`Unsafe ${kind}`);
137
+ }