@openclaw/crabline 0.1.11 → 0.1.13

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 (214) hide show
  1. package/README.md +137 -23
  2. package/dist/src/bin/crabline.js +3 -1
  3. package/dist/src/bin/crabline.js.map +1 -1
  4. package/dist/src/cli/program.d.ts +5 -1
  5. package/dist/src/cli/program.js +281 -52
  6. package/dist/src/cli/program.js.map +1 -1
  7. package/dist/src/config/load.js +39 -3
  8. package/dist/src/config/load.js.map +1 -1
  9. package/dist/src/config/schema.d.ts +20 -0
  10. package/dist/src/config/schema.js +163 -35
  11. package/dist/src/config/schema.js.map +1 -1
  12. package/dist/src/core/errors.d.ts +1 -0
  13. package/dist/src/core/errors.js +6 -3
  14. package/dist/src/core/errors.js.map +1 -1
  15. package/dist/src/core/http-path.d.ts +1 -0
  16. package/dist/src/core/http-path.js +14 -0
  17. package/dist/src/core/http-path.js.map +1 -0
  18. package/dist/src/core/matcher.d.ts +3 -1
  19. package/dist/src/core/matcher.js +58 -2
  20. package/dist/src/core/matcher.js.map +1 -1
  21. package/dist/src/core/nonces.d.ts +2 -0
  22. package/dist/src/core/nonces.js +11 -1
  23. package/dist/src/core/nonces.js.map +1 -1
  24. package/dist/src/core/reporters.d.ts +5 -0
  25. package/dist/src/core/reporters.js +26 -2
  26. package/dist/src/core/reporters.js.map +1 -1
  27. package/dist/src/core/run.d.ts +3 -1
  28. package/dist/src/core/run.js +178 -66
  29. package/dist/src/core/run.js.map +1 -1
  30. package/dist/src/index.d.ts +12 -10
  31. package/dist/src/index.js +6 -5
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/matrix-ids.d.ts +5 -0
  34. package/dist/src/matrix-ids.js +83 -0
  35. package/dist/src/matrix-ids.js.map +1 -0
  36. package/dist/src/openclaw/artifact-generation.d.ts +7 -6
  37. package/dist/src/openclaw/artifact-generation.js +385 -41
  38. package/dist/src/openclaw/artifact-generation.js.map +1 -1
  39. package/dist/src/openclaw/bridges/discord.d.ts +1 -0
  40. package/dist/src/openclaw/bridges/discord.js +149 -0
  41. package/dist/src/openclaw/bridges/discord.js.map +1 -0
  42. package/dist/src/openclaw/bridges/matrix.js +22 -5
  43. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  44. package/dist/src/openclaw/bridges/mattermost.js +14 -9
  45. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  46. package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
  47. package/dist/src/openclaw/bridges/probe-response.js +5 -0
  48. package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
  49. package/dist/src/openclaw/bridges/signal.js +16 -7
  50. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  51. package/dist/src/openclaw/bridges/slack.js +117 -24
  52. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  53. package/dist/src/openclaw/bridges/telegram.js +46 -20
  54. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  55. package/dist/src/openclaw/bridges/whatsapp.js +33 -17
  56. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  57. package/dist/src/openclaw/bridges/zalo.js +9 -4
  58. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  59. package/dist/src/openclaw/outbound-contract.js +3 -0
  60. package/dist/src/openclaw/outbound-contract.js.map +1 -1
  61. package/dist/src/openclaw/private-file.d.ts +38 -3
  62. package/dist/src/openclaw/private-file.js +2008 -87
  63. package/dist/src/openclaw/private-file.js.map +1 -1
  64. package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
  65. package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
  66. package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
  67. package/dist/src/openclaw/shared.d.ts +10 -17
  68. package/dist/src/openclaw/shared.js +53 -19
  69. package/dist/src/openclaw/shared.js.map +1 -1
  70. package/dist/src/openclaw.d.ts +3 -5
  71. package/dist/src/openclaw.js +245 -60
  72. package/dist/src/openclaw.js.map +1 -1
  73. package/dist/src/platform/process-owned-lock.d.ts +11 -0
  74. package/dist/src/platform/process-owned-lock.js +1698 -0
  75. package/dist/src/platform/process-owned-lock.js.map +1 -0
  76. package/dist/src/platform/recorder-directory.d.ts +1 -0
  77. package/dist/src/platform/recorder-directory.js +14 -0
  78. package/dist/src/platform/recorder-directory.js.map +1 -0
  79. package/dist/src/platform/windows-acl.d.ts +18 -0
  80. package/dist/src/platform/windows-acl.js +1367 -0
  81. package/dist/src/platform/windows-acl.js.map +1 -0
  82. package/dist/src/platform/windows-lock-root.d.ts +13 -0
  83. package/dist/src/platform/windows-lock-root.js +98 -0
  84. package/dist/src/platform/windows-lock-root.js.map +1 -0
  85. package/dist/src/providers/builtin/discord.d.ts +4 -0
  86. package/dist/src/providers/builtin/discord.js +52 -9
  87. package/dist/src/providers/builtin/discord.js.map +1 -1
  88. package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
  89. package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
  90. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
  91. package/dist/src/providers/builtin/feishu.d.ts +1 -0
  92. package/dist/src/providers/builtin/feishu.js +96 -26
  93. package/dist/src/providers/builtin/feishu.js.map +1 -1
  94. package/dist/src/providers/builtin/googlechat.d.ts +1 -0
  95. package/dist/src/providers/builtin/googlechat.js +95 -26
  96. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  97. package/dist/src/providers/builtin/imessage.js +60 -17
  98. package/dist/src/providers/builtin/imessage.js.map +1 -1
  99. package/dist/src/providers/builtin/loopback.js +88 -35
  100. package/dist/src/providers/builtin/loopback.js.map +1 -1
  101. package/dist/src/providers/builtin/matrix.d.ts +2 -2
  102. package/dist/src/providers/builtin/matrix.js +78 -21
  103. package/dist/src/providers/builtin/matrix.js.map +1 -1
  104. package/dist/src/providers/builtin/mattermost.d.ts +11 -7
  105. package/dist/src/providers/builtin/mattermost.js +138 -24
  106. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  107. package/dist/src/providers/builtin/msteams.d.ts +1 -0
  108. package/dist/src/providers/builtin/msteams.js +118 -38
  109. package/dist/src/providers/builtin/msteams.js.map +1 -1
  110. package/dist/src/providers/builtin/native-local-mock.js +4 -4
  111. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  112. package/dist/src/providers/builtin/script.js +1057 -146
  113. package/dist/src/providers/builtin/script.js.map +1 -1
  114. package/dist/src/providers/builtin/slack.d.ts +8 -1
  115. package/dist/src/providers/builtin/slack.js +327 -24
  116. package/dist/src/providers/builtin/slack.js.map +1 -1
  117. package/dist/src/providers/builtin/telegram.d.ts +1 -1
  118. package/dist/src/providers/builtin/telegram.js +55 -14
  119. package/dist/src/providers/builtin/telegram.js.map +1 -1
  120. package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
  121. package/dist/src/providers/builtin/whatsapp.js +157 -29
  122. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  123. package/dist/src/providers/builtin/zalo.d.ts +5 -2
  124. package/dist/src/providers/builtin/zalo.js +31 -7
  125. package/dist/src/providers/builtin/zalo.js.map +1 -1
  126. package/dist/src/providers/catalog.d.ts +1 -1
  127. package/dist/src/providers/catalog.js +1 -1
  128. package/dist/src/providers/catalog.js.map +1 -1
  129. package/dist/src/providers/local-mock.d.ts +4 -0
  130. package/dist/src/providers/local-mock.js +162 -35
  131. package/dist/src/providers/local-mock.js.map +1 -1
  132. package/dist/src/providers/native-ids.d.ts +2 -0
  133. package/dist/src/providers/native-ids.js +3 -0
  134. package/dist/src/providers/native-ids.js.map +1 -1
  135. package/dist/src/providers/recorder.d.ts +14 -3
  136. package/dist/src/providers/recorder.js +521 -41
  137. package/dist/src/providers/recorder.js.map +1 -1
  138. package/dist/src/providers/registry.d.ts +1 -0
  139. package/dist/src/providers/registry.js +65 -20
  140. package/dist/src/providers/registry.js.map +1 -1
  141. package/dist/src/providers/signed-jwt.d.ts +6 -1
  142. package/dist/src/providers/signed-jwt.js +241 -50
  143. package/dist/src/providers/signed-jwt.js.map +1 -1
  144. package/dist/src/providers/slack-ids.d.ts +1 -0
  145. package/dist/src/providers/slack-ids.js +5 -0
  146. package/dist/src/providers/slack-ids.js.map +1 -1
  147. package/dist/src/providers/target-normalizers.d.ts +9 -1
  148. package/dist/src/providers/target-normalizers.js +177 -58
  149. package/dist/src/providers/target-normalizers.js.map +1 -1
  150. package/dist/src/providers/webhook-server.d.ts +3 -0
  151. package/dist/src/providers/webhook-server.js +268 -47
  152. package/dist/src/providers/webhook-server.js.map +1 -1
  153. package/dist/src/servers/discord.d.ts +40 -0
  154. package/dist/src/servers/discord.js +842 -0
  155. package/dist/src/servers/discord.js.map +1 -0
  156. package/dist/src/servers/http.d.ts +14 -2
  157. package/dist/src/servers/http.js +229 -58
  158. package/dist/src/servers/http.js.map +1 -1
  159. package/dist/src/servers/index.d.ts +10 -4
  160. package/dist/src/servers/index.js +4 -0
  161. package/dist/src/servers/index.js.map +1 -1
  162. package/dist/src/servers/matrix.d.ts +3 -0
  163. package/dist/src/servers/matrix.js +268 -110
  164. package/dist/src/servers/matrix.js.map +1 -1
  165. package/dist/src/servers/mattermost.d.ts +5 -0
  166. package/dist/src/servers/mattermost.js +389 -91
  167. package/dist/src/servers/mattermost.js.map +1 -1
  168. package/dist/src/servers/recorder.d.ts +11 -0
  169. package/dist/src/servers/recorder.js +1072 -69
  170. package/dist/src/servers/recorder.js.map +1 -1
  171. package/dist/src/servers/signal.d.ts +1 -0
  172. package/dist/src/servers/signal.js +279 -44
  173. package/dist/src/servers/signal.js.map +1 -1
  174. package/dist/src/servers/slack.d.ts +15 -0
  175. package/dist/src/servers/slack.js +270 -54
  176. package/dist/src/servers/slack.js.map +1 -1
  177. package/dist/src/servers/telegram-identity.d.ts +6 -0
  178. package/dist/src/servers/telegram-identity.js +27 -0
  179. package/dist/src/servers/telegram-identity.js.map +1 -0
  180. package/dist/src/servers/telegram.d.ts +17 -6
  181. package/dist/src/servers/telegram.js +1218 -107
  182. package/dist/src/servers/telegram.js.map +1 -1
  183. package/dist/src/servers/webhook-target.d.ts +12 -4
  184. package/dist/src/servers/webhook-target.js +226 -28
  185. package/dist/src/servers/webhook-target.js.map +1 -1
  186. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
  187. package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
  188. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  189. package/dist/src/servers/whatsapp-jid.d.ts +1 -0
  190. package/dist/src/servers/whatsapp-jid.js +9 -5
  191. package/dist/src/servers/whatsapp-jid.js.map +1 -1
  192. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
  193. package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
  194. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  195. package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
  196. package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
  197. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  198. package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
  199. package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
  200. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  201. package/dist/src/servers/whatsapp-wire/signal.js +3 -3
  202. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  203. package/dist/src/servers/whatsapp.d.ts +7 -1
  204. package/dist/src/servers/whatsapp.js +149 -25
  205. package/dist/src/servers/whatsapp.js.map +1 -1
  206. package/dist/src/servers/zalo.d.ts +1 -0
  207. package/dist/src/servers/zalo.js +193 -82
  208. package/dist/src/servers/zalo.js.map +1 -1
  209. package/docs/channel-setup.md +251 -64
  210. package/fixtures/examples/crabline.example.yaml +3 -2
  211. package/package.json +18 -10
  212. package/dist/src/openclaw/smoke-lock.js +0 -1004
  213. package/dist/src/openclaw/smoke-lock.js.map +0 -1
  214. package/fixtures/examples/openclaw-bridge.yaml +0 -125
@@ -0,0 +1,1367 @@
1
+ import { execFile } from "node:child_process";
2
+ import path from "node:path";
3
+ import { promisify } from "node:util";
4
+ const execFileAsync = promisify(execFile);
5
+ const WINDOWS_ACL_COMMAND_TIMEOUT_MS = 15_000;
6
+ const WINDOWS_DIRECTORY_HANDLE_HELPER = String.raw `
7
+ Add-Type -TypeDefinition @"
8
+ using System;
9
+ using System.ComponentModel;
10
+ using System.IO;
11
+ using System.Runtime.InteropServices;
12
+ using Microsoft.Win32.SafeHandles;
13
+
14
+ public static class CrablineWindowsDirectoryHandle
15
+ {
16
+ private const uint FileReadAttributes = 0x00000080;
17
+ private const uint FileTraverse = 0x00000020;
18
+ private const uint Delete = 0x00010000;
19
+ private const uint ReadControl = 0x00020000;
20
+ private const uint WriteDac = 0x00040000;
21
+ private const uint WriteOwner = 0x00080000;
22
+ private const uint ShareRead = 0x00000001;
23
+ private const uint ShareWrite = 0x00000002;
24
+ private const uint Synchronize = 0x00100000;
25
+ private const uint OpenExisting = 3;
26
+ private const uint FileFlagBackupSemantics = 0x02000000;
27
+ private const uint FileFlagOpenReparsePoint = 0x00200000;
28
+ private const uint FileOpen = 1;
29
+ private const uint FileCreate = 2;
30
+ private const uint FileDirectoryFile = 0x00000001;
31
+ private const uint FileSynchronousIoNonAlert = 0x00000020;
32
+ private const uint FileOpenReparsePoint = 0x00200000;
33
+ private const uint ObjectCaseInsensitive = 0x00000040;
34
+ private const uint FileAttributeNormal = 0x00000080;
35
+ private const uint DirectoryAttribute = 0x00000010;
36
+ private const uint ReparsePointAttribute = 0x00000400;
37
+ private const int FileIdInfoClass = 18;
38
+ private const int FileDispositionInfoClass = 4;
39
+ private const uint OwnerSecurityInformation = 0x00000001;
40
+ private const uint DaclSecurityInformation = 0x00000004;
41
+ private const uint ProtectedDaclSecurityInformation = 0x80000000;
42
+ private const int SeFileObject = 1;
43
+
44
+ [StructLayout(LayoutKind.Sequential)]
45
+ private struct FileTime
46
+ {
47
+ public uint Low;
48
+ public uint High;
49
+ }
50
+
51
+ [StructLayout(LayoutKind.Sequential)]
52
+ private struct ByHandleFileInformation
53
+ {
54
+ public uint FileAttributes;
55
+ public FileTime CreationTime;
56
+ public FileTime LastAccessTime;
57
+ public FileTime LastWriteTime;
58
+ public uint VolumeSerialNumber;
59
+ public uint FileSizeHigh;
60
+ public uint FileSizeLow;
61
+ public uint NumberOfLinks;
62
+ public uint FileIndexHigh;
63
+ public uint FileIndexLow;
64
+ }
65
+
66
+ [StructLayout(LayoutKind.Sequential)]
67
+ private struct FileId128
68
+ {
69
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
70
+ public byte[] Identifier;
71
+ }
72
+
73
+ [StructLayout(LayoutKind.Sequential)]
74
+ private struct FileIdInfo
75
+ {
76
+ public ulong VolumeSerialNumber;
77
+ public FileId128 FileId;
78
+ }
79
+
80
+ [StructLayout(LayoutKind.Sequential)]
81
+ private struct FileDispositionInfo
82
+ {
83
+ [MarshalAs(UnmanagedType.Bool)]
84
+ public bool DeleteFile;
85
+ }
86
+
87
+ [StructLayout(LayoutKind.Sequential)]
88
+ private struct IoStatusBlock
89
+ {
90
+ public IntPtr Status;
91
+ public UIntPtr Information;
92
+ }
93
+
94
+ [StructLayout(LayoutKind.Sequential)]
95
+ private struct UnicodeString
96
+ {
97
+ public ushort Length;
98
+ public ushort MaximumLength;
99
+ public IntPtr Buffer;
100
+ }
101
+
102
+ [StructLayout(LayoutKind.Sequential)]
103
+ private struct ObjectAttributes
104
+ {
105
+ public int Length;
106
+ public IntPtr RootDirectory;
107
+ public IntPtr ObjectName;
108
+ public uint Attributes;
109
+ public IntPtr SecurityDescriptor;
110
+ public IntPtr SecurityQualityOfService;
111
+ }
112
+
113
+ [DllImport(
114
+ "kernel32.dll",
115
+ CharSet = CharSet.Unicode,
116
+ SetLastError = true
117
+ )]
118
+ private static extern SafeFileHandle CreateFile(
119
+ string fileName,
120
+ uint desiredAccess,
121
+ uint shareMode,
122
+ IntPtr securityAttributes,
123
+ uint creationDisposition,
124
+ uint flagsAndAttributes,
125
+ IntPtr templateFile
126
+ );
127
+
128
+ [DllImport("ntdll.dll")]
129
+ private static extern int NtCreateFile(
130
+ out SafeFileHandle file,
131
+ uint desiredAccess,
132
+ ref ObjectAttributes objectAttributes,
133
+ out IoStatusBlock ioStatus,
134
+ IntPtr allocationSize,
135
+ uint fileAttributes,
136
+ uint shareAccess,
137
+ uint createDisposition,
138
+ uint createOptions,
139
+ IntPtr eaBuffer,
140
+ uint eaLength
141
+ );
142
+
143
+ [DllImport("ntdll.dll")]
144
+ private static extern uint RtlNtStatusToDosError(int status);
145
+
146
+ [DllImport("kernel32.dll", SetLastError = true)]
147
+ [return: MarshalAs(UnmanagedType.Bool)]
148
+ private static extern bool GetFileInformationByHandle(
149
+ SafeFileHandle file,
150
+ out ByHandleFileInformation information
151
+ );
152
+
153
+ [DllImport("kernel32.dll", SetLastError = true)]
154
+ [return: MarshalAs(UnmanagedType.Bool)]
155
+ private static extern bool GetFileInformationByHandleEx(
156
+ SafeFileHandle file,
157
+ int informationClass,
158
+ out FileIdInfo information,
159
+ uint bufferSize
160
+ );
161
+
162
+ [DllImport("kernel32.dll", SetLastError = true)]
163
+ [return: MarshalAs(UnmanagedType.Bool)]
164
+ private static extern bool SetFileInformationByHandle(
165
+ SafeFileHandle file,
166
+ int informationClass,
167
+ ref FileDispositionInfo information,
168
+ uint bufferSize
169
+ );
170
+
171
+ [DllImport("advapi32.dll")]
172
+ private static extern uint GetSecurityInfo(
173
+ SafeFileHandle file,
174
+ int objectType,
175
+ uint securityInformation,
176
+ out IntPtr owner,
177
+ out IntPtr group,
178
+ out IntPtr dacl,
179
+ out IntPtr sacl,
180
+ out IntPtr securityDescriptor
181
+ );
182
+
183
+ [DllImport("advapi32.dll")]
184
+ private static extern uint SetSecurityInfo(
185
+ SafeFileHandle file,
186
+ int objectType,
187
+ uint securityInformation,
188
+ IntPtr owner,
189
+ IntPtr group,
190
+ IntPtr dacl,
191
+ IntPtr sacl
192
+ );
193
+
194
+ [DllImport("advapi32.dll", SetLastError = true)]
195
+ [return: MarshalAs(UnmanagedType.Bool)]
196
+ private static extern bool GetSecurityDescriptorOwner(
197
+ IntPtr securityDescriptor,
198
+ out IntPtr owner,
199
+ [MarshalAs(UnmanagedType.Bool)] out bool ownerDefaulted
200
+ );
201
+
202
+ [DllImport("advapi32.dll", SetLastError = true)]
203
+ [return: MarshalAs(UnmanagedType.Bool)]
204
+ private static extern bool GetSecurityDescriptorDacl(
205
+ IntPtr securityDescriptor,
206
+ [MarshalAs(UnmanagedType.Bool)] out bool daclPresent,
207
+ out IntPtr dacl,
208
+ [MarshalAs(UnmanagedType.Bool)] out bool daclDefaulted
209
+ );
210
+
211
+ [DllImport("advapi32.dll")]
212
+ private static extern uint GetSecurityDescriptorLength(
213
+ IntPtr securityDescriptor
214
+ );
215
+
216
+ [DllImport("kernel32.dll")]
217
+ private static extern IntPtr LocalFree(
218
+ IntPtr memory
219
+ );
220
+
221
+ private static uint AccessMask(
222
+ bool writeDacl,
223
+ bool writeOwner,
224
+ bool traverse
225
+ )
226
+ {
227
+ uint access = FileReadAttributes | ReadControl;
228
+ if (traverse) {
229
+ access |= FileTraverse;
230
+ }
231
+ if (writeDacl) {
232
+ access |= WriteDac;
233
+ }
234
+ if (writeOwner) {
235
+ access |= WriteOwner;
236
+ }
237
+ return access;
238
+ }
239
+
240
+ public static SafeFileHandle Open(
241
+ string path,
242
+ bool writeDacl,
243
+ bool writeOwner,
244
+ bool traverse
245
+ )
246
+ {
247
+ SafeFileHandle file = CreateFile(
248
+ path,
249
+ AccessMask(writeDacl, writeOwner, traverse),
250
+ ShareRead | ShareWrite,
251
+ IntPtr.Zero,
252
+ OpenExisting,
253
+ FileFlagBackupSemantics | FileFlagOpenReparsePoint,
254
+ IntPtr.Zero
255
+ );
256
+ if (file.IsInvalid) {
257
+ throw new Win32Exception(Marshal.GetLastWin32Error());
258
+ }
259
+ try {
260
+ ReadIdentity(file);
261
+ return file;
262
+ } catch {
263
+ file.Dispose();
264
+ throw;
265
+ }
266
+ }
267
+
268
+ private static SafeFileHandle OpenRelativeCore(
269
+ SafeFileHandle parent,
270
+ string name,
271
+ bool writeDacl,
272
+ bool writeOwner,
273
+ bool traverse,
274
+ uint createDisposition,
275
+ byte[] securityDescriptor,
276
+ bool deleteAccess
277
+ )
278
+ {
279
+ if (
280
+ String.IsNullOrEmpty(name) ||
281
+ name == "." ||
282
+ name == ".." ||
283
+ name.IndexOf(Path.DirectorySeparatorChar) >= 0 ||
284
+ name.IndexOf(Path.AltDirectorySeparatorChar) >= 0
285
+ ) {
286
+ throw new ArgumentException(
287
+ "Private directory child name must be one path component.",
288
+ "name"
289
+ );
290
+ }
291
+ int pathBytes = name.Length * 2;
292
+ if (pathBytes > UInt16.MaxValue - 2) {
293
+ throw new PathTooLongException();
294
+ }
295
+
296
+ GCHandle descriptorHandle = default(GCHandle);
297
+ IntPtr pathBuffer = IntPtr.Zero;
298
+ IntPtr objectNameBuffer = IntPtr.Zero;
299
+ bool parentAddRef = false;
300
+ try {
301
+ if (securityDescriptor != null) {
302
+ descriptorHandle = GCHandle.Alloc(
303
+ securityDescriptor,
304
+ GCHandleType.Pinned
305
+ );
306
+ }
307
+ parent.DangerousAddRef(ref parentAddRef);
308
+ pathBuffer = Marshal.StringToHGlobalUni(name);
309
+ UnicodeString objectName = new UnicodeString {
310
+ Length = (ushort)pathBytes,
311
+ MaximumLength = (ushort)(pathBytes + 2),
312
+ Buffer = pathBuffer
313
+ };
314
+ objectNameBuffer = Marshal.AllocHGlobal(
315
+ Marshal.SizeOf(typeof(UnicodeString))
316
+ );
317
+ Marshal.StructureToPtr(objectName, objectNameBuffer, false);
318
+ ObjectAttributes attributes = new ObjectAttributes {
319
+ Length = Marshal.SizeOf(typeof(ObjectAttributes)),
320
+ RootDirectory = parent.DangerousGetHandle(),
321
+ ObjectName = objectNameBuffer,
322
+ Attributes = ObjectCaseInsensitive,
323
+ SecurityDescriptor = securityDescriptor == null
324
+ ? IntPtr.Zero
325
+ : descriptorHandle.AddrOfPinnedObject(),
326
+ SecurityQualityOfService = IntPtr.Zero
327
+ };
328
+ IoStatusBlock ioStatus;
329
+ SafeFileHandle file;
330
+ int status = NtCreateFile(
331
+ out file,
332
+ AccessMask(writeDacl, writeOwner, traverse) |
333
+ (deleteAccess ? Delete : 0) |
334
+ Synchronize,
335
+ ref attributes,
336
+ out ioStatus,
337
+ IntPtr.Zero,
338
+ FileAttributeNormal,
339
+ ShareRead | ShareWrite,
340
+ createDisposition,
341
+ FileDirectoryFile |
342
+ FileSynchronousIoNonAlert |
343
+ FileOpenReparsePoint,
344
+ IntPtr.Zero,
345
+ 0
346
+ );
347
+ if (status < 0) {
348
+ throw new Win32Exception(
349
+ unchecked((int)RtlNtStatusToDosError(status))
350
+ );
351
+ }
352
+ if (file == null || file.IsInvalid) {
353
+ throw new InvalidOperationException(
354
+ "Windows did not return the directory handle."
355
+ );
356
+ }
357
+ try {
358
+ ReadIdentity(file);
359
+ return file;
360
+ } catch {
361
+ file.Dispose();
362
+ throw;
363
+ }
364
+ } finally {
365
+ if (objectNameBuffer != IntPtr.Zero) {
366
+ Marshal.FreeHGlobal(objectNameBuffer);
367
+ }
368
+ if (pathBuffer != IntPtr.Zero) {
369
+ Marshal.FreeHGlobal(pathBuffer);
370
+ }
371
+ if (parentAddRef) {
372
+ parent.DangerousRelease();
373
+ }
374
+ if (descriptorHandle.IsAllocated) {
375
+ descriptorHandle.Free();
376
+ }
377
+ }
378
+ }
379
+
380
+ public static SafeFileHandle CreateRelative(
381
+ SafeFileHandle parent,
382
+ string name,
383
+ byte[] securityDescriptor
384
+ )
385
+ {
386
+ return OpenRelativeCore(
387
+ parent,
388
+ name,
389
+ true,
390
+ false,
391
+ true,
392
+ FileCreate,
393
+ securityDescriptor,
394
+ true
395
+ );
396
+ }
397
+
398
+ public static SafeFileHandle OpenRelative(
399
+ SafeFileHandle parent,
400
+ string name,
401
+ bool writeDacl,
402
+ bool writeOwner,
403
+ bool traverse
404
+ )
405
+ {
406
+ return OpenRelativeCore(
407
+ parent,
408
+ name,
409
+ writeDacl,
410
+ writeOwner,
411
+ traverse,
412
+ FileOpen,
413
+ null,
414
+ false
415
+ );
416
+ }
417
+
418
+ public static void MarkDelete(SafeFileHandle file)
419
+ {
420
+ FileDispositionInfo disposition = new FileDispositionInfo {
421
+ DeleteFile = true
422
+ };
423
+ if (!SetFileInformationByHandle(
424
+ file,
425
+ FileDispositionInfoClass,
426
+ ref disposition,
427
+ (uint)Marshal.SizeOf(typeof(FileDispositionInfo))
428
+ )) {
429
+ throw new Win32Exception(Marshal.GetLastWin32Error());
430
+ }
431
+ }
432
+
433
+ public static string ReadIdentity(SafeFileHandle file)
434
+ {
435
+ ByHandleFileInformation information;
436
+ if (!GetFileInformationByHandle(file, out information)) {
437
+ throw new Win32Exception(Marshal.GetLastWin32Error());
438
+ }
439
+ if ((information.FileAttributes & ReparsePointAttribute) != 0) {
440
+ throw new InvalidOperationException(
441
+ "Private directory must not be a reparse point."
442
+ );
443
+ }
444
+ if ((information.FileAttributes & DirectoryAttribute) == 0) {
445
+ throw new InvalidOperationException(
446
+ "Private directory handle does not reference a directory."
447
+ );
448
+ }
449
+ FileIdInfo identity;
450
+ uint identitySize = (uint)Marshal.SizeOf(typeof(FileIdInfo));
451
+ if (!GetFileInformationByHandleEx(
452
+ file,
453
+ FileIdInfoClass,
454
+ out identity,
455
+ identitySize
456
+ )) {
457
+ throw new Win32Exception(Marshal.GetLastWin32Error());
458
+ }
459
+ byte[] identifier = identity.FileId.Identifier;
460
+ bool hasIdentity = identifier != null && identifier.Length == 16;
461
+ if (hasIdentity) {
462
+ hasIdentity = false;
463
+ foreach (byte value in identifier) {
464
+ if (value != 0) {
465
+ hasIdentity = true;
466
+ break;
467
+ }
468
+ }
469
+ }
470
+ if (!hasIdentity) {
471
+ throw new InvalidOperationException(
472
+ "Windows did not return a stable directory identity."
473
+ );
474
+ }
475
+ return identity.VolumeSerialNumber.ToString(
476
+ System.Globalization.CultureInfo.InvariantCulture
477
+ ) + ":" + BitConverter.ToString(identifier).Replace("-", String.Empty);
478
+ }
479
+
480
+ public static byte[] ReadSecurityDescriptor(SafeFileHandle file)
481
+ {
482
+ IntPtr owner;
483
+ IntPtr group;
484
+ IntPtr dacl;
485
+ IntPtr sacl;
486
+ IntPtr securityDescriptor;
487
+ uint error = GetSecurityInfo(
488
+ file,
489
+ SeFileObject,
490
+ OwnerSecurityInformation | DaclSecurityInformation,
491
+ out owner,
492
+ out group,
493
+ out dacl,
494
+ out sacl,
495
+ out securityDescriptor
496
+ );
497
+ if (error != 0) {
498
+ throw new Win32Exception((int)error);
499
+ }
500
+ try {
501
+ uint length = GetSecurityDescriptorLength(securityDescriptor);
502
+ if (length == 0 || length > Int32.MaxValue) {
503
+ throw new InvalidOperationException(
504
+ "Windows returned an invalid directory security descriptor."
505
+ );
506
+ }
507
+ byte[] descriptor = new byte[(int)length];
508
+ Marshal.Copy(securityDescriptor, descriptor, 0, descriptor.Length);
509
+ return descriptor;
510
+ } finally {
511
+ LocalFree(securityDescriptor);
512
+ }
513
+ }
514
+
515
+ public static void WriteSecurityDescriptor(
516
+ SafeFileHandle file,
517
+ byte[] securityDescriptor,
518
+ bool writeOwner
519
+ )
520
+ {
521
+ GCHandle descriptorHandle = GCHandle.Alloc(
522
+ securityDescriptor,
523
+ GCHandleType.Pinned
524
+ );
525
+ try {
526
+ IntPtr descriptor = descriptorHandle.AddrOfPinnedObject();
527
+ IntPtr owner = IntPtr.Zero;
528
+ if (writeOwner) {
529
+ bool ownerDefaulted;
530
+ if (!GetSecurityDescriptorOwner(
531
+ descriptor,
532
+ out owner,
533
+ out ownerDefaulted
534
+ )) {
535
+ throw new Win32Exception(Marshal.GetLastWin32Error());
536
+ }
537
+ }
538
+ bool daclPresent;
539
+ IntPtr dacl;
540
+ bool daclDefaulted;
541
+ if (!GetSecurityDescriptorDacl(
542
+ descriptor,
543
+ out daclPresent,
544
+ out dacl,
545
+ out daclDefaulted
546
+ )) {
547
+ throw new Win32Exception(Marshal.GetLastWin32Error());
548
+ }
549
+ if (!daclPresent || dacl == IntPtr.Zero) {
550
+ throw new InvalidOperationException(
551
+ "Private directory security descriptor has no usable DACL."
552
+ );
553
+ }
554
+ uint securityInformation =
555
+ DaclSecurityInformation |
556
+ ProtectedDaclSecurityInformation;
557
+ if (writeOwner) {
558
+ securityInformation |= OwnerSecurityInformation;
559
+ }
560
+ uint error = SetSecurityInfo(
561
+ file,
562
+ SeFileObject,
563
+ securityInformation,
564
+ owner,
565
+ IntPtr.Zero,
566
+ dacl,
567
+ IntPtr.Zero
568
+ );
569
+ if (error != 0) {
570
+ throw new Win32Exception((int)error);
571
+ }
572
+ } finally {
573
+ descriptorHandle.Free();
574
+ }
575
+ }
576
+
577
+ public static void AssertSamePathIdentity(string path, string expected)
578
+ {
579
+ if (!String.Equals(
580
+ ReadPathIdentity(path),
581
+ expected,
582
+ StringComparison.Ordinal
583
+ )) {
584
+ throw new InvalidOperationException(
585
+ "Private directory identity changed during ACL mutation."
586
+ );
587
+ }
588
+ }
589
+
590
+ public static string ReadPathIdentity(string path)
591
+ {
592
+ using (SafeFileHandle current = Open(path, false, false, false)) {
593
+ return ReadIdentity(current);
594
+ }
595
+ }
596
+ }
597
+ "@
598
+ `;
599
+ const WINDOWS_OWNER_ONLY_DIRECTORY_ACL_SCRIPT = String.raw `
600
+ $ErrorActionPreference = "Stop"
601
+ $directoryPath = $env:CRABLINE_PRIVATE_DIRECTORY_PATH
602
+ if ([string]::IsNullOrWhiteSpace($directoryPath)) {
603
+ throw "CRABLINE_PRIVATE_DIRECTORY_PATH is required."
604
+ }
605
+ $directoryPath = [System.IO.Path]::GetFullPath($directoryPath)
606
+ $rootPath = [System.IO.Path]::GetPathRoot($directoryPath)
607
+ if ($directoryPath.Length -gt $rootPath.Length) {
608
+ $directoryPath = $directoryPath.TrimEnd(
609
+ [char[]]@(
610
+ [System.IO.Path]::DirectorySeparatorChar,
611
+ [System.IO.Path]::AltDirectorySeparatorChar
612
+ )
613
+ )
614
+ }
615
+
616
+ ${WINDOWS_DIRECTORY_HANDLE_HELPER}
617
+
618
+ $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
619
+ $sid = $identity.User
620
+ if ($null -eq $sid) {
621
+ throw "Could not resolve the current Windows user SID."
622
+ }
623
+
624
+ $inspection = [CrablineWindowsDirectoryHandle]::Open(
625
+ $directoryPath,
626
+ $false,
627
+ $false,
628
+ $false
629
+ )
630
+ $directory = $null
631
+ try {
632
+ $directoryIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity($inspection)
633
+ $acl = [System.Security.AccessControl.DirectorySecurity]::new()
634
+ $acl.SetSecurityDescriptorBinaryForm(
635
+ [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor($inspection)
636
+ )
637
+ $currentOwner = $acl.GetOwner(
638
+ [System.Security.Principal.SecurityIdentifier]
639
+ )
640
+ $writeOwner = $currentOwner.Value -ne $sid.Value
641
+ $directory = [CrablineWindowsDirectoryHandle]::Open(
642
+ $directoryPath,
643
+ $true,
644
+ $writeOwner,
645
+ $false
646
+ )
647
+ if (
648
+ [CrablineWindowsDirectoryHandle]::ReadIdentity($directory) -ne
649
+ $directoryIdentity
650
+ ) {
651
+ throw "Private directory identity changed before ACL repair."
652
+ }
653
+ $acl.SetOwner($sid)
654
+ $acl.SetAccessRuleProtection($true, $false)
655
+ $existingRules = @($acl.GetAccessRules(
656
+ $true,
657
+ $false,
658
+ [System.Security.Principal.SecurityIdentifier]
659
+ ))
660
+ foreach ($existingRule in $existingRules) {
661
+ [void]$acl.RemoveAccessRuleSpecific($existingRule)
662
+ }
663
+ $inheritance = (
664
+ [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
665
+ [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
666
+ )
667
+ $rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
668
+ $sid,
669
+ [System.Security.AccessControl.FileSystemRights]::FullControl,
670
+ $inheritance,
671
+ [System.Security.AccessControl.PropagationFlags]::None,
672
+ [System.Security.AccessControl.AccessControlType]::Allow
673
+ )
674
+ $acl.SetAccessRule($rule)
675
+ [CrablineWindowsDirectoryHandle]::WriteSecurityDescriptor(
676
+ $directory,
677
+ $acl.GetSecurityDescriptorBinaryForm(),
678
+ $writeOwner
679
+ )
680
+
681
+ $actual = [System.Security.AccessControl.DirectorySecurity]::new()
682
+ $actual.SetSecurityDescriptorBinaryForm(
683
+ [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor($directory)
684
+ )
685
+ $ownerSid = $actual.GetOwner([System.Security.Principal.SecurityIdentifier])
686
+ $rules = @($actual.GetAccessRules(
687
+ $true,
688
+ $true,
689
+ [System.Security.Principal.SecurityIdentifier]
690
+ ))
691
+ if (-not $actual.AreAccessRulesProtected) {
692
+ throw "Private directory DACL still inherits permissions."
693
+ }
694
+ if ($ownerSid.Value -ne $sid.Value) {
695
+ throw "Private directory owner SID does not match the current user."
696
+ }
697
+ if ($rules.Count -ne 1) {
698
+ throw "Private directory DACL must contain exactly one access rule."
699
+ }
700
+ $actualRule = $rules[0]
701
+ $requiredInheritance = (
702
+ [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
703
+ [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
704
+ )
705
+ if (
706
+ $actualRule.IsInherited -or
707
+ $actualRule.IdentityReference.Value -ne $sid.Value -or
708
+ $actualRule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or
709
+ (($actualRule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl) -or
710
+ (($actualRule.InheritanceFlags -band $requiredInheritance) -ne $requiredInheritance)
711
+ ) {
712
+ throw "Private directory DACL is not owner-only inheritable full control."
713
+ }
714
+ [CrablineWindowsDirectoryHandle]::AssertSamePathIdentity(
715
+ $directoryPath,
716
+ $directoryIdentity
717
+ )
718
+ } finally {
719
+ if ($null -ne $directory) {
720
+ $directory.Dispose()
721
+ }
722
+ $inspection.Dispose()
723
+ }
724
+ `;
725
+ const WINDOWS_SAFE_PARENT_NAMESPACE_HELPER = String.raw `
726
+ $trustedSids = [System.Collections.Generic.HashSet[string]]::new(
727
+ [System.StringComparer]::OrdinalIgnoreCase
728
+ )
729
+ [void]$trustedSids.Add($sid.Value)
730
+ [void]$trustedSids.Add("S-1-5-18")
731
+ [void]$trustedSids.Add("S-1-5-32-544")
732
+ [void]$trustedSids.Add(
733
+ "S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464"
734
+ )
735
+ $replacementRights = (
736
+ [int64][System.Security.AccessControl.FileSystemRights]::WriteData -bor
737
+ [int64][System.Security.AccessControl.FileSystemRights]::WriteAttributes -bor
738
+ [int64][System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
739
+ [int64][System.Security.AccessControl.FileSystemRights]::Delete -bor
740
+ [int64][System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor
741
+ [int64][System.Security.AccessControl.FileSystemRights]::TakeOwnership -bor
742
+ [int64]0x40000000 -bor
743
+ [int64]0x10000000
744
+ )
745
+
746
+ function Assert-SafeParentNamespace(
747
+ [string]$candidate,
748
+ [bool]$rejectChildCreation
749
+ ) {
750
+ $parentDirectory = [CrablineWindowsDirectoryHandle]::Open(
751
+ $candidate,
752
+ $false,
753
+ $false,
754
+ $false
755
+ )
756
+ try {
757
+ # ReadIdentity rejects reparse points, so the lexical walk cannot cross a junction.
758
+ $parentIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity($parentDirectory)
759
+ $parentDescriptor = [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor(
760
+ $parentDirectory
761
+ )
762
+ $parentRawDescriptor = [System.Security.AccessControl.RawSecurityDescriptor]::new(
763
+ $parentDescriptor,
764
+ 0
765
+ )
766
+ if (
767
+ (($parentRawDescriptor.ControlFlags -band
768
+ [System.Security.AccessControl.ControlFlags]::DiscretionaryAclPresent) -eq 0) -or
769
+ $null -eq $parentRawDescriptor.DiscretionaryAcl
770
+ ) {
771
+ throw "Private directory parent namespace has a null DACL."
772
+ }
773
+ $parentAcl = [System.Security.AccessControl.DirectorySecurity]::new()
774
+ $parentAcl.SetSecurityDescriptorBinaryForm($parentDescriptor)
775
+ $parentOwner = $parentAcl.GetOwner(
776
+ [System.Security.Principal.SecurityIdentifier]
777
+ )
778
+ if (-not $trustedSids.Contains($parentOwner.Value)) {
779
+ throw "Private directory parent namespace has an untrusted owner."
780
+ }
781
+ $parentRules = @($parentAcl.GetAccessRules(
782
+ $true,
783
+ $true,
784
+ [System.Security.Principal.SecurityIdentifier]
785
+ ))
786
+ $inheritOnly = [System.Security.AccessControl.PropagationFlags]::InheritOnly
787
+ $unsafeRights = $replacementRights
788
+ if ($rejectChildCreation) {
789
+ $unsafeRights = (
790
+ $unsafeRights -bor
791
+ [int64][System.Security.AccessControl.FileSystemRights]::CreateDirectories
792
+ )
793
+ }
794
+ foreach ($parentRule in $parentRules) {
795
+ if (
796
+ $parentRule.AccessControlType -ne
797
+ [System.Security.AccessControl.AccessControlType]::Allow -or
798
+ $trustedSids.Contains($parentRule.IdentityReference.Value) -or
799
+ (($parentRule.PropagationFlags -band $inheritOnly) -ne 0)
800
+ ) {
801
+ continue
802
+ }
803
+ if (([int64]$parentRule.FileSystemRights -band $unsafeRights) -ne 0) {
804
+ throw "Private directory parent namespace permits untrusted replacement."
805
+ }
806
+ }
807
+ [CrablineWindowsDirectoryHandle]::AssertSamePathIdentity(
808
+ $candidate,
809
+ $parentIdentity
810
+ )
811
+ } finally {
812
+ $parentDirectory.Dispose()
813
+ }
814
+ }
815
+
816
+ function Assert-SafeNamespaceChain(
817
+ [string]$candidate,
818
+ [bool]$rejectChildCreationAtCandidate
819
+ ) {
820
+ $current = $candidate
821
+ while ($null -ne $current) {
822
+ Assert-SafeParentNamespace $current $rejectChildCreationAtCandidate
823
+ $rejectChildCreationAtCandidate = $false
824
+ $parent = [System.IO.Directory]::GetParent($current)
825
+ $current = if ($null -eq $parent) { $null } else { $parent.FullName }
826
+ }
827
+ }
828
+ `;
829
+ const WINDOWS_CREATE_OWNER_ONLY_DIRECTORY_SCRIPT = String.raw `
830
+ $ErrorActionPreference = "Stop"
831
+ $directoryPath = $env:CRABLINE_PRIVATE_DIRECTORY_PATH
832
+ if ([string]::IsNullOrWhiteSpace($directoryPath)) {
833
+ throw "CRABLINE_PRIVATE_DIRECTORY_PATH is required."
834
+ }
835
+ $directoryPath = [System.IO.Path]::GetFullPath($directoryPath)
836
+ $rootPath = [System.IO.Path]::GetPathRoot($directoryPath)
837
+ if ($directoryPath.Length -gt $rootPath.Length) {
838
+ $directoryPath = $directoryPath.TrimEnd(
839
+ [char[]]@(
840
+ [System.IO.Path]::DirectorySeparatorChar,
841
+ [System.IO.Path]::AltDirectorySeparatorChar
842
+ )
843
+ )
844
+ }
845
+
846
+ ${WINDOWS_DIRECTORY_HANDLE_HELPER}
847
+
848
+ $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
849
+ $sid = $identity.User
850
+ if ($null -eq $sid) {
851
+ throw "Could not resolve the current Windows user SID."
852
+ }
853
+
854
+ $acl = [System.Security.AccessControl.DirectorySecurity]::new()
855
+ $acl.SetOwner($sid)
856
+ $acl.SetAccessRuleProtection($true, $false)
857
+ $inheritance = (
858
+ [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
859
+ [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
860
+ )
861
+ $rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
862
+ $sid,
863
+ [System.Security.AccessControl.FileSystemRights]::FullControl,
864
+ $inheritance,
865
+ [System.Security.AccessControl.PropagationFlags]::None,
866
+ [System.Security.AccessControl.AccessControlType]::Allow
867
+ )
868
+ $acl.SetAccessRule($rule)
869
+ $descriptor = $acl.GetSecurityDescriptorBinaryForm()
870
+
871
+ ${WINDOWS_SAFE_PARENT_NAMESPACE_HELPER}
872
+
873
+ $missing = [System.Collections.Generic.List[string]]::new()
874
+ $current = $directoryPath
875
+ while (-not [System.IO.Directory]::Exists($current)) {
876
+ if ([System.IO.File]::Exists($current)) {
877
+ throw "Private directory ancestry contains a non-directory entry."
878
+ }
879
+ $missing.Add($current)
880
+ $parent = [System.IO.Directory]::GetParent($current)
881
+ if ($null -eq $parent) {
882
+ throw "Private directory ancestry has no existing parent."
883
+ }
884
+ $current = $parent.FullName
885
+ }
886
+
887
+ if ($missing.Count -eq 0) {
888
+ $parent = [System.IO.Directory]::GetParent($directoryPath)
889
+ if ($null -eq $parent) {
890
+ throw "Private directory must have a parent."
891
+ }
892
+ $current = $parent.FullName
893
+ }
894
+
895
+ $directory = $null
896
+ $heldDirectories = [System.Collections.Generic.List[Microsoft.Win32.SafeHandles.SafeFileHandle]]::new()
897
+ $createdDirectories = [System.Collections.Generic.List[Microsoft.Win32.SafeHandles.SafeFileHandle]]::new()
898
+ $firstCreatedDirectory = $null
899
+ try {
900
+ if ($missing.Count -eq 0) {
901
+ $inspection = [CrablineWindowsDirectoryHandle]::Open(
902
+ $directoryPath,
903
+ $false,
904
+ $false,
905
+ $false
906
+ )
907
+ $heldDirectories.Add($inspection)
908
+ $directoryIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity($inspection)
909
+ Assert-SafeNamespaceChain $current $false
910
+ [CrablineWindowsDirectoryHandle]::AssertSamePathIdentity(
911
+ $directoryPath,
912
+ $directoryIdentity
913
+ )
914
+ $currentAcl = [System.Security.AccessControl.DirectorySecurity]::new()
915
+ $currentAcl.SetSecurityDescriptorBinaryForm(
916
+ [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor($inspection)
917
+ )
918
+ $currentOwner = $currentAcl.GetOwner(
919
+ [System.Security.Principal.SecurityIdentifier]
920
+ )
921
+ $writeOwner = $currentOwner.Value -ne $sid.Value
922
+ $directory = [CrablineWindowsDirectoryHandle]::Open(
923
+ $directoryPath,
924
+ $true,
925
+ $writeOwner,
926
+ $false
927
+ )
928
+ $heldDirectories.Add($directory)
929
+ if (
930
+ [CrablineWindowsDirectoryHandle]::ReadIdentity($directory) -ne
931
+ $directoryIdentity
932
+ ) {
933
+ throw "Private directory identity changed before ACL repair."
934
+ }
935
+ [CrablineWindowsDirectoryHandle]::WriteSecurityDescriptor(
936
+ $directory,
937
+ $descriptor,
938
+ $writeOwner
939
+ )
940
+ } else {
941
+ $paths = $missing.ToArray()
942
+ [Array]::Reverse($paths)
943
+ $parentDirectory = [CrablineWindowsDirectoryHandle]::Open(
944
+ $current,
945
+ $false,
946
+ $false,
947
+ $true
948
+ )
949
+ $heldDirectories.Add($parentDirectory)
950
+ $existingParentIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity(
951
+ $parentDirectory
952
+ )
953
+ Assert-SafeNamespaceChain $current $true
954
+ [CrablineWindowsDirectoryHandle]::AssertSamePathIdentity(
955
+ $current,
956
+ $existingParentIdentity
957
+ )
958
+
959
+ foreach ($candidate in $paths) {
960
+ $childName = [System.IO.Path]::GetFileName($candidate)
961
+ $createdDirectory = $null
962
+ try {
963
+ $createdDirectory = [CrablineWindowsDirectoryHandle]::CreateRelative(
964
+ $parentDirectory,
965
+ $childName,
966
+ $descriptor
967
+ )
968
+ $createdDirectories.Add($createdDirectory)
969
+ if ($null -eq $firstCreatedDirectory) {
970
+ $firstCreatedDirectory = $candidate
971
+ }
972
+ } catch [System.ComponentModel.Win32Exception] {
973
+ if (
974
+ $_.Exception.NativeErrorCode -ne 80 -and
975
+ $_.Exception.NativeErrorCode -ne 183
976
+ ) {
977
+ throw
978
+ }
979
+ $inspection = [CrablineWindowsDirectoryHandle]::OpenRelative(
980
+ $parentDirectory,
981
+ $childName,
982
+ $false,
983
+ $false,
984
+ $false
985
+ )
986
+ $heldDirectories.Add($inspection)
987
+ $childIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity($inspection)
988
+ $currentAcl = [System.Security.AccessControl.DirectorySecurity]::new()
989
+ $currentAcl.SetSecurityDescriptorBinaryForm(
990
+ [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor($inspection)
991
+ )
992
+ $currentOwner = $currentAcl.GetOwner(
993
+ [System.Security.Principal.SecurityIdentifier]
994
+ )
995
+ $writeOwner = $currentOwner.Value -ne $sid.Value
996
+ $createdDirectory = [CrablineWindowsDirectoryHandle]::OpenRelative(
997
+ $parentDirectory,
998
+ $childName,
999
+ $true,
1000
+ $writeOwner,
1001
+ $true
1002
+ )
1003
+ if (
1004
+ [CrablineWindowsDirectoryHandle]::ReadIdentity($createdDirectory) -ne
1005
+ $childIdentity
1006
+ ) {
1007
+ $createdDirectory.Dispose()
1008
+ throw "Private directory identity changed before ACL repair."
1009
+ }
1010
+ [CrablineWindowsDirectoryHandle]::WriteSecurityDescriptor(
1011
+ $createdDirectory,
1012
+ $descriptor,
1013
+ $writeOwner
1014
+ )
1015
+ }
1016
+ $heldDirectories.Add($createdDirectory)
1017
+ $parentDirectory = $createdDirectory
1018
+ }
1019
+ $directory = $parentDirectory
1020
+ [CrablineWindowsDirectoryHandle]::AssertSamePathIdentity(
1021
+ $current,
1022
+ $existingParentIdentity
1023
+ )
1024
+ }
1025
+ if ($null -eq $directory) {
1026
+ throw "Windows did not return the private directory handle."
1027
+ }
1028
+ $directoryIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity($directory)
1029
+ $actual = [System.Security.AccessControl.DirectorySecurity]::new()
1030
+ $actual.SetSecurityDescriptorBinaryForm(
1031
+ [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor($directory)
1032
+ )
1033
+ $ownerSid = $actual.GetOwner([System.Security.Principal.SecurityIdentifier])
1034
+ $rules = @($actual.GetAccessRules(
1035
+ $true,
1036
+ $true,
1037
+ [System.Security.Principal.SecurityIdentifier]
1038
+ ))
1039
+ if (-not $actual.AreAccessRulesProtected) {
1040
+ throw "Private directory DACL still inherits permissions."
1041
+ }
1042
+ if ($ownerSid.Value -ne $sid.Value) {
1043
+ throw "Private directory owner SID does not match the current user."
1044
+ }
1045
+ if ($rules.Count -ne 1) {
1046
+ throw "Private directory DACL must contain exactly one access rule."
1047
+ }
1048
+ $actualRule = $rules[0]
1049
+ if (
1050
+ $actualRule.IsInherited -or
1051
+ $actualRule.IdentityReference.Value -ne $sid.Value -or
1052
+ $actualRule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or
1053
+ (($actualRule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl) -or
1054
+ (($actualRule.InheritanceFlags -band $inheritance) -ne $inheritance)
1055
+ ) {
1056
+ throw "Private directory DACL is not owner-only inheritable full control."
1057
+ }
1058
+ [CrablineWindowsDirectoryHandle]::AssertSamePathIdentity(
1059
+ $directoryPath,
1060
+ $directoryIdentity
1061
+ )
1062
+ if ($null -ne $firstCreatedDirectory) {
1063
+ [Console]::Out.Write($firstCreatedDirectory)
1064
+ }
1065
+ } catch {
1066
+ $primaryError = $_.Exception
1067
+ $cleanupErrors = [System.Collections.Generic.List[System.Exception]]::new()
1068
+ for ($index = $createdDirectories.Count - 1; $index -ge 0; $index--) {
1069
+ try {
1070
+ [CrablineWindowsDirectoryHandle]::MarkDelete(
1071
+ $createdDirectories[$index]
1072
+ )
1073
+ $createdDirectories[$index].Dispose()
1074
+ } catch {
1075
+ $cleanupErrors.Add($_.Exception)
1076
+ }
1077
+ }
1078
+ if ($cleanupErrors.Count -gt 0) {
1079
+ $aggregateErrors = [System.Collections.Generic.List[System.Exception]]::new()
1080
+ $aggregateErrors.Add($primaryError)
1081
+ $aggregateErrors.AddRange($cleanupErrors)
1082
+ throw [System.AggregateException]::new(
1083
+ "Private directory ancestry creation and rollback cleanup failed.",
1084
+ $aggregateErrors
1085
+ )
1086
+ }
1087
+ throw $primaryError
1088
+ } finally {
1089
+ for ($index = $heldDirectories.Count - 1; $index -ge 0; $index--) {
1090
+ $heldDirectories[$index].Dispose()
1091
+ }
1092
+ }
1093
+ `;
1094
+ const WINDOWS_DIRECTORY_SECURITY_DESCRIPTOR_SCRIPT = String.raw `
1095
+ $ErrorActionPreference = "Stop"
1096
+ $directoryPath = $env:CRABLINE_PRIVATE_DIRECTORY_PATH
1097
+ if ([string]::IsNullOrWhiteSpace($directoryPath)) {
1098
+ throw "CRABLINE_PRIVATE_DIRECTORY_PATH is required."
1099
+ }
1100
+ $directoryPath = [System.IO.Path]::GetFullPath($directoryPath)
1101
+ $rootPath = [System.IO.Path]::GetPathRoot($directoryPath)
1102
+ if ($directoryPath.Length -gt $rootPath.Length) {
1103
+ $directoryPath = $directoryPath.TrimEnd(
1104
+ [char[]]@(
1105
+ [System.IO.Path]::DirectorySeparatorChar,
1106
+ [System.IO.Path]::AltDirectorySeparatorChar
1107
+ )
1108
+ )
1109
+ }
1110
+
1111
+ ${WINDOWS_DIRECTORY_HANDLE_HELPER}
1112
+
1113
+ $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
1114
+ $sid = $identity.User
1115
+ if ($null -eq $sid) {
1116
+ throw "Could not resolve the current Windows user SID."
1117
+ }
1118
+
1119
+ ${WINDOWS_SAFE_PARENT_NAMESPACE_HELPER}
1120
+
1121
+ $parent = [System.IO.Directory]::GetParent($directoryPath)
1122
+ if ($null -eq $parent) {
1123
+ throw "Private directory must have a parent."
1124
+ }
1125
+ Assert-SafeNamespaceChain $parent.FullName $false
1126
+
1127
+ $directory = [CrablineWindowsDirectoryHandle]::Open(
1128
+ $directoryPath,
1129
+ $false,
1130
+ $false,
1131
+ $false
1132
+ )
1133
+ try {
1134
+ $directoryIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity($directory)
1135
+ $descriptor = [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor($directory)
1136
+ $acl = [System.Security.AccessControl.DirectorySecurity]::new()
1137
+ $acl.SetSecurityDescriptorBinaryForm($descriptor)
1138
+ $ownerSid = $acl.GetOwner([System.Security.Principal.SecurityIdentifier])
1139
+ $rules = @($acl.GetAccessRules(
1140
+ $true,
1141
+ $true,
1142
+ [System.Security.Principal.SecurityIdentifier]
1143
+ ))
1144
+ $requiredInheritance = (
1145
+ [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
1146
+ [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
1147
+ )
1148
+ if (
1149
+ -not $acl.AreAccessRulesProtected -or
1150
+ $ownerSid.Value -ne $sid.Value -or
1151
+ $rules.Count -ne 1
1152
+ ) {
1153
+ throw "Windows directory security descriptor is not owner-only."
1154
+ }
1155
+ $rule = $rules[0]
1156
+ if (
1157
+ $rule.IsInherited -or
1158
+ $rule.IdentityReference.Value -ne $sid.Value -or
1159
+ $rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or
1160
+ (($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl) -or
1161
+ (($rule.InheritanceFlags -band $requiredInheritance) -ne $requiredInheritance)
1162
+ ) {
1163
+ throw "Windows directory security descriptor is not owner-only."
1164
+ }
1165
+ $pathIdentity = [CrablineWindowsDirectoryHandle]::ReadPathIdentity($directoryPath)
1166
+ if ($pathIdentity -ne $directoryIdentity) {
1167
+ throw "Private directory identity changed during security inspection."
1168
+ }
1169
+ [Console]::Out.Write($directoryIdentity)
1170
+ [Console]::Out.Write([Environment]::NewLine)
1171
+ [Console]::Out.Write($pathIdentity)
1172
+ [Console]::Out.Write([Environment]::NewLine)
1173
+ [Console]::Out.Write([Convert]::ToBase64String($descriptor))
1174
+ } finally {
1175
+ $directory.Dispose()
1176
+ }
1177
+ `;
1178
+ const WINDOWS_DIRECTORY_NAMESPACE_SECURITY_DESCRIPTOR_SCRIPT = String.raw `
1179
+ $ErrorActionPreference = "Stop"
1180
+ $directoryPath = $env:CRABLINE_PRIVATE_DIRECTORY_PATH
1181
+ if ([string]::IsNullOrWhiteSpace($directoryPath)) {
1182
+ throw "CRABLINE_PRIVATE_DIRECTORY_PATH is required."
1183
+ }
1184
+ $directoryPath = [System.IO.Path]::GetFullPath($directoryPath)
1185
+ $rootPath = [System.IO.Path]::GetPathRoot($directoryPath)
1186
+ if ($directoryPath.Length -gt $rootPath.Length) {
1187
+ $directoryPath = $directoryPath.TrimEnd(
1188
+ [char[]]@(
1189
+ [System.IO.Path]::DirectorySeparatorChar,
1190
+ [System.IO.Path]::AltDirectorySeparatorChar
1191
+ )
1192
+ )
1193
+ }
1194
+
1195
+ ${WINDOWS_DIRECTORY_HANDLE_HELPER}
1196
+
1197
+ $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
1198
+ $sid = $identity.User
1199
+ if ($null -eq $sid) {
1200
+ throw "Could not resolve the current Windows user SID."
1201
+ }
1202
+
1203
+ ${WINDOWS_SAFE_PARENT_NAMESPACE_HELPER}
1204
+
1205
+ Assert-SafeNamespaceChain $directoryPath $true
1206
+
1207
+ $directory = [CrablineWindowsDirectoryHandle]::Open(
1208
+ $directoryPath,
1209
+ $false,
1210
+ $false,
1211
+ $false
1212
+ )
1213
+ try {
1214
+ $directoryIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity($directory)
1215
+ $descriptor = [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor($directory)
1216
+ $pathIdentity = [CrablineWindowsDirectoryHandle]::ReadPathIdentity($directoryPath)
1217
+ if ($pathIdentity -ne $directoryIdentity) {
1218
+ throw "Private directory identity changed during security inspection."
1219
+ }
1220
+ [Console]::Out.Write($directoryIdentity)
1221
+ [Console]::Out.Write([Environment]::NewLine)
1222
+ [Console]::Out.Write($pathIdentity)
1223
+ [Console]::Out.Write([Environment]::NewLine)
1224
+ [Console]::Out.Write([Convert]::ToBase64String($descriptor))
1225
+ } finally {
1226
+ $directory.Dispose()
1227
+ }
1228
+ `;
1229
+ const runWindowsAclCommand = async (command, args, options) => {
1230
+ const result = await execFileAsync(command, args, { ...options, encoding: "utf8" });
1231
+ return result.stdout;
1232
+ };
1233
+ export function resolveWindowsPowerShellPath(systemRoot) {
1234
+ const normalizedRoot = systemRoot?.trim() ? path.win32.normalize(systemRoot.trim()) : undefined;
1235
+ if (!normalizedRoot || !/^[A-Za-z]:\\/u.test(normalizedRoot)) {
1236
+ throw new Error("SystemRoot must be an absolute local Windows path.");
1237
+ }
1238
+ return path.win32.join(normalizedRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
1239
+ }
1240
+ export async function applyOwnerOnlyWindowsDirectoryAcl(directoryPath, run = runWindowsAclCommand, systemRoot = process.env.SystemRoot) {
1241
+ try {
1242
+ const powershellPath = resolveWindowsPowerShellPath(systemRoot);
1243
+ await run(powershellPath, [
1244
+ "-NoLogo",
1245
+ "-NoProfile",
1246
+ "-NonInteractive",
1247
+ "-Command",
1248
+ WINDOWS_OWNER_ONLY_DIRECTORY_ACL_SCRIPT,
1249
+ ], {
1250
+ env: {
1251
+ ...process.env,
1252
+ CRABLINE_PRIVATE_DIRECTORY_PATH: path.resolve(directoryPath),
1253
+ },
1254
+ killSignal: "SIGKILL",
1255
+ timeout: WINDOWS_ACL_COMMAND_TIMEOUT_MS,
1256
+ windowsHide: true,
1257
+ });
1258
+ }
1259
+ catch (error) {
1260
+ throw new Error("Could not apply and verify an owner-only Windows directory ACL; Windows PowerShell ACL support is required.", { cause: error });
1261
+ }
1262
+ }
1263
+ export async function createOwnerOnlyWindowsDirectoryAncestry(directoryPath, run = runWindowsAclCommand, systemRoot = process.env.SystemRoot) {
1264
+ const powershellPath = resolveWindowsPowerShellPath(systemRoot);
1265
+ const output = await run(powershellPath, [
1266
+ "-NoLogo",
1267
+ "-NoProfile",
1268
+ "-NonInteractive",
1269
+ "-Command",
1270
+ WINDOWS_CREATE_OWNER_ONLY_DIRECTORY_SCRIPT,
1271
+ ], {
1272
+ env: {
1273
+ ...process.env,
1274
+ CRABLINE_PRIVATE_DIRECTORY_PATH: path.resolve(directoryPath),
1275
+ },
1276
+ killSignal: "SIGKILL",
1277
+ timeout: WINDOWS_ACL_COMMAND_TIMEOUT_MS,
1278
+ windowsHide: true,
1279
+ });
1280
+ const firstCreatedDirectory = output.trim();
1281
+ return firstCreatedDirectory.length > 0 ? firstCreatedDirectory : undefined;
1282
+ }
1283
+ export async function createOwnerOnlyWindowsDirectory(directoryPath, run = runWindowsAclCommand, systemRoot = process.env.SystemRoot) {
1284
+ try {
1285
+ await createOwnerOnlyWindowsDirectoryAncestry(directoryPath, run, systemRoot);
1286
+ }
1287
+ catch (error) {
1288
+ throw new Error("Could not atomically create or verify an owner-only Windows directory; Windows PowerShell ACL support is required.", { cause: error });
1289
+ }
1290
+ }
1291
+ export async function readWindowsDirectorySecuritySnapshot(directoryPath, run = runWindowsAclCommand, systemRoot = process.env.SystemRoot) {
1292
+ try {
1293
+ const powershellPath = resolveWindowsPowerShellPath(systemRoot);
1294
+ const output = await run(powershellPath, [
1295
+ "-NoLogo",
1296
+ "-NoProfile",
1297
+ "-NonInteractive",
1298
+ "-Command",
1299
+ WINDOWS_DIRECTORY_SECURITY_DESCRIPTOR_SCRIPT,
1300
+ ], {
1301
+ env: {
1302
+ ...process.env,
1303
+ CRABLINE_PRIVATE_DIRECTORY_PATH: path.resolve(directoryPath),
1304
+ },
1305
+ killSignal: "SIGKILL",
1306
+ timeout: WINDOWS_ACL_COMMAND_TIMEOUT_MS,
1307
+ windowsHide: true,
1308
+ });
1309
+ const [identity, pathIdentity, securityDescriptor, ...extraLines] = output
1310
+ .trim()
1311
+ .split(/\r?\n/u);
1312
+ if (!identity ||
1313
+ !/^\d+:[0-9A-F]{32}$/u.test(identity) ||
1314
+ !pathIdentity ||
1315
+ !/^\d+:[0-9A-F]{32}$/u.test(pathIdentity) ||
1316
+ pathIdentity !== identity ||
1317
+ !securityDescriptor ||
1318
+ extraLines.length > 0) {
1319
+ throw new Error("Windows directory security snapshot was invalid.");
1320
+ }
1321
+ return { identity, pathIdentity, securityDescriptor };
1322
+ }
1323
+ catch (error) {
1324
+ throw new Error("Could not read the Windows directory security descriptor through a stable no-follow handle; Windows PowerShell ACL support is required.", { cause: error });
1325
+ }
1326
+ }
1327
+ export async function readWindowsDirectoryNamespaceSecuritySnapshot(directoryPath, run = runWindowsAclCommand, systemRoot = process.env.SystemRoot) {
1328
+ try {
1329
+ const powershellPath = resolveWindowsPowerShellPath(systemRoot);
1330
+ const output = await run(powershellPath, [
1331
+ "-NoLogo",
1332
+ "-NoProfile",
1333
+ "-NonInteractive",
1334
+ "-Command",
1335
+ WINDOWS_DIRECTORY_NAMESPACE_SECURITY_DESCRIPTOR_SCRIPT,
1336
+ ], {
1337
+ env: {
1338
+ ...process.env,
1339
+ CRABLINE_PRIVATE_DIRECTORY_PATH: path.resolve(directoryPath),
1340
+ },
1341
+ killSignal: "SIGKILL",
1342
+ timeout: WINDOWS_ACL_COMMAND_TIMEOUT_MS,
1343
+ windowsHide: true,
1344
+ });
1345
+ const [identity, pathIdentity, securityDescriptor, ...extraLines] = output
1346
+ .trim()
1347
+ .split(/\r?\n/u);
1348
+ if (!identity ||
1349
+ !/^\d+:[0-9A-F]{32}$/u.test(identity) ||
1350
+ !pathIdentity ||
1351
+ !/^\d+:[0-9A-F]{32}$/u.test(pathIdentity) ||
1352
+ pathIdentity !== identity ||
1353
+ !securityDescriptor ||
1354
+ extraLines.length > 0) {
1355
+ throw new Error("Windows directory namespace security snapshot was invalid.");
1356
+ }
1357
+ return { identity, pathIdentity, securityDescriptor };
1358
+ }
1359
+ catch (error) {
1360
+ throw new Error("Could not validate the Windows directory namespace through stable no-follow handles; Windows PowerShell ACL support is required.", { cause: error });
1361
+ }
1362
+ }
1363
+ export async function readWindowsDirectorySecurityDescriptor(directoryPath, run = runWindowsAclCommand, systemRoot = process.env.SystemRoot) {
1364
+ return (await readWindowsDirectorySecuritySnapshot(directoryPath, run, systemRoot))
1365
+ .securityDescriptor;
1366
+ }
1367
+ //# sourceMappingURL=windows-acl.js.map