@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
@@ -1,40 +1,274 @@
1
- import { execFile } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
1
+ import { execFile, spawnSync } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { constants as fsConstants, readFileSync } from "node:fs";
3
4
  import fs, {} from "node:fs/promises";
4
5
  import path from "node:path";
6
+ import { performance } from "node:perf_hooks";
5
7
  import { promisify } from "node:util";
8
+ import { applyOwnerOnlyWindowsDirectoryAcl as applyOwnerOnlyWindowsDirectoryAclByHandle, createOwnerOnlyWindowsDirectoryAncestry as createOwnerOnlyWindowsDirectoryAncestryByHandle, } from "../platform/windows-acl.js";
6
9
  const execFileAsync = promisify(execFile);
7
- const WINDOWS_OWNER_ONLY_ACL_SCRIPT = String.raw `
10
+ const WINDOWS_ACL_COMMAND_TIMEOUT_MS = 15_000;
11
+ const WINDOWS_CREATE_OWNER_ONLY_FILE_SCRIPT = String.raw `
8
12
  $ErrorActionPreference = "Stop"
9
13
  $filePath = $env:CRABLINE_PRIVATE_FILE_PATH
10
14
  if ([string]::IsNullOrWhiteSpace($filePath)) {
11
15
  throw "CRABLINE_PRIVATE_FILE_PATH is required."
12
16
  }
13
17
 
18
+ Add-Type -TypeDefinition @"
19
+ using System;
20
+ using System.ComponentModel;
21
+ using System.Runtime.InteropServices;
22
+ using Microsoft.Win32.SafeHandles;
23
+
24
+ public static class CrablinePrivateFileIdentity
25
+ {
26
+ private const int FileInternalInformationClass = 6;
27
+ private const int FileFsVolumeInformationClass = 1;
28
+ private const int FileDispositionInfoClass = 4;
29
+
30
+ [StructLayout(LayoutKind.Sequential)]
31
+ private struct IoStatusBlock
32
+ {
33
+ public IntPtr Status;
34
+ public UIntPtr Information;
35
+ }
36
+
37
+ [StructLayout(LayoutKind.Sequential)]
38
+ private struct FileInternalInformation
39
+ {
40
+ public long IndexNumber;
41
+ }
42
+
43
+ [StructLayout(LayoutKind.Sequential)]
44
+ private struct FileDispositionInfo
45
+ {
46
+ [MarshalAs(UnmanagedType.Bool)]
47
+ public bool DeleteFile;
48
+ }
49
+
50
+ [DllImport("ntdll.dll")]
51
+ private static extern int NtQueryInformationFile(
52
+ SafeFileHandle file,
53
+ out IoStatusBlock ioStatus,
54
+ out FileInternalInformation fileInformation,
55
+ uint length,
56
+ int fileInformationClass
57
+ );
58
+
59
+ [DllImport("ntdll.dll")]
60
+ private static extern int NtQueryVolumeInformationFile(
61
+ SafeFileHandle file,
62
+ out IoStatusBlock ioStatus,
63
+ [Out] byte[] volumeInformation,
64
+ uint length,
65
+ int volumeInformationClass
66
+ );
67
+
68
+ [DllImport("kernel32.dll", SetLastError = true)]
69
+ [return: MarshalAs(UnmanagedType.Bool)]
70
+ private static extern bool SetFileInformationByHandle(
71
+ SafeFileHandle file,
72
+ int informationClass,
73
+ ref FileDispositionInfo information,
74
+ uint bufferSize
75
+ );
76
+
77
+ [DllImport("ntdll.dll")]
78
+ private static extern uint RtlNtStatusToDosError(int status);
79
+
80
+ private static void ThrowIfFailed(int status)
81
+ {
82
+ if (status != 0) {
83
+ throw new Win32Exception(
84
+ unchecked((int)RtlNtStatusToDosError(status))
85
+ );
86
+ }
87
+ }
88
+
89
+ public static string Read(SafeFileHandle file)
90
+ {
91
+ IoStatusBlock ioStatus;
92
+ FileInternalInformation fileInfo;
93
+ int status = NtQueryInformationFile(
94
+ file,
95
+ out ioStatus,
96
+ out fileInfo,
97
+ (uint)Marshal.SizeOf(typeof(FileInternalInformation)),
98
+ FileInternalInformationClass
99
+ );
100
+ ThrowIfFailed(status);
101
+
102
+ byte[] volumeInfo = new byte[1024];
103
+ status = NtQueryVolumeInformationFile(
104
+ file,
105
+ out ioStatus,
106
+ volumeInfo,
107
+ (uint)volumeInfo.Length,
108
+ FileFsVolumeInformationClass
109
+ );
110
+ ThrowIfFailed(status);
111
+
112
+ uint device = BitConverter.ToUInt32(volumeInfo, 8);
113
+ ulong inode = unchecked((ulong)fileInfo.IndexNumber);
114
+ return device.ToString(
115
+ System.Globalization.CultureInfo.InvariantCulture
116
+ ) + ":" + inode.ToString(
117
+ System.Globalization.CultureInfo.InvariantCulture
118
+ );
119
+ }
120
+
121
+ public static void MarkDelete(SafeFileHandle file)
122
+ {
123
+ FileDispositionInfo disposition = new FileDispositionInfo {
124
+ DeleteFile = true
125
+ };
126
+ if (!SetFileInformationByHandle(
127
+ file,
128
+ FileDispositionInfoClass,
129
+ ref disposition,
130
+ (uint)Marshal.SizeOf(typeof(FileDispositionInfo))
131
+ )) {
132
+ throw new Win32Exception(Marshal.GetLastWin32Error());
133
+ }
134
+ }
135
+ }
136
+ "@
137
+
14
138
  $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
15
139
  $sid = $identity.User
16
140
  if ($null -eq $sid) {
17
141
  throw "Could not resolve the current Windows user SID."
18
142
  }
19
143
 
20
- $acl = Get-Acl -LiteralPath $filePath
144
+ $acl = [System.Security.AccessControl.FileSecurity]::new()
21
145
  $acl.SetOwner($sid)
22
146
  $acl.SetAccessRuleProtection($true, $false)
23
- $existingRules = @($acl.GetAccessRules(
24
- $true,
25
- $false,
26
- [System.Security.Principal.SecurityIdentifier]
27
- ))
28
- foreach ($existingRule in $existingRules) {
29
- [void]$acl.RemoveAccessRuleSpecific($existingRule)
30
- }
31
147
  $rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
32
148
  $sid,
33
149
  [System.Security.AccessControl.FileSystemRights]::FullControl,
34
150
  [System.Security.AccessControl.AccessControlType]::Allow
35
151
  )
36
152
  $acl.SetAccessRule($rule)
37
- Set-Acl -LiteralPath $filePath -AclObject $acl
153
+
154
+ $rights = (
155
+ [System.Security.AccessControl.FileSystemRights]::Read -bor
156
+ [System.Security.AccessControl.FileSystemRights]::Write -bor
157
+ [System.Security.AccessControl.FileSystemRights]::ReadPermissions -bor
158
+ [System.Security.AccessControl.FileSystemRights]::Delete
159
+ )
160
+ $stream = [System.IO.FileStream]::new(
161
+ $filePath,
162
+ [System.IO.FileMode]::CreateNew,
163
+ $rights,
164
+ [System.IO.FileShare]::ReadWrite,
165
+ 4096,
166
+ [System.IO.FileOptions]::None,
167
+ $acl
168
+ )
169
+ try {
170
+ $actual = $stream.GetAccessControl()
171
+ $ownerSid = $actual.GetOwner([System.Security.Principal.SecurityIdentifier])
172
+ $rules = @($actual.GetAccessRules(
173
+ $true,
174
+ $true,
175
+ [System.Security.Principal.SecurityIdentifier]
176
+ ))
177
+ if (-not $actual.AreAccessRulesProtected) {
178
+ throw "Private file DACL still inherits permissions."
179
+ }
180
+ if ($ownerSid.Value -ne $sid.Value) {
181
+ throw "Private file owner SID does not match the current user."
182
+ }
183
+ if ($rules.Count -ne 1) {
184
+ throw "Private file DACL must contain exactly one access rule."
185
+ }
186
+ $actualRule = $rules[0]
187
+ if (
188
+ $actualRule.IsInherited -or
189
+ $actualRule.IdentityReference.Value -ne $sid.Value -or
190
+ $actualRule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or
191
+ (($actualRule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl)
192
+ ) {
193
+ throw "Private file DACL is not owner-only full control."
194
+ }
195
+ [Console]::Out.Write(
196
+ [CrablinePrivateFileIdentity]::Read($stream.SafeFileHandle)
197
+ )
198
+ } catch {
199
+ $primaryError = $_.Exception
200
+ try {
201
+ [CrablinePrivateFileIdentity]::MarkDelete($stream.SafeFileHandle)
202
+ } catch {
203
+ $aggregateErrors = [System.Collections.Generic.List[System.Exception]]::new()
204
+ $aggregateErrors.Add($primaryError)
205
+ $aggregateErrors.Add($_.Exception)
206
+ throw [System.AggregateException]::new(
207
+ "Private file creation and handle-bound cleanup failed.",
208
+ $aggregateErrors
209
+ )
210
+ }
211
+ throw $primaryError
212
+ } finally {
213
+ $stream.Dispose()
214
+ }
215
+ `;
216
+ const WINDOWS_VERIFY_OWNER_ONLY_DIRECTORY_ACL_SCRIPT = String.raw `
217
+ $ErrorActionPreference = "Stop"
218
+ $directoryPath = $env:CRABLINE_PRIVATE_DIRECTORY_PATH
219
+ if ([string]::IsNullOrWhiteSpace($directoryPath)) {
220
+ throw "CRABLINE_PRIVATE_DIRECTORY_PATH is required."
221
+ }
222
+
223
+ $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
224
+ $sid = $identity.User
225
+ if ($null -eq $sid) {
226
+ throw "Could not resolve the current Windows user SID."
227
+ }
228
+
229
+ $actual = Get-Acl -LiteralPath $directoryPath
230
+ $ownerSid = $actual.GetOwner([System.Security.Principal.SecurityIdentifier])
231
+ $rules = @($actual.GetAccessRules(
232
+ $true,
233
+ $true,
234
+ [System.Security.Principal.SecurityIdentifier]
235
+ ))
236
+ $requiredInheritance = (
237
+ [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
238
+ [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
239
+ )
240
+ if (-not $actual.AreAccessRulesProtected) {
241
+ throw "Private directory DACL still inherits permissions."
242
+ }
243
+ if ($ownerSid.Value -ne $sid.Value) {
244
+ throw "Private directory owner SID does not match the current user."
245
+ }
246
+ if ($rules.Count -ne 1) {
247
+ throw "Private directory DACL must contain exactly one access rule."
248
+ }
249
+ $actualRule = $rules[0]
250
+ if (
251
+ $actualRule.IsInherited -or
252
+ $actualRule.IdentityReference.Value -ne $sid.Value -or
253
+ $actualRule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or
254
+ (($actualRule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl) -or
255
+ (($actualRule.InheritanceFlags -band $requiredInheritance) -ne $requiredInheritance)
256
+ ) {
257
+ throw "Private directory DACL is not owner-only inheritable full control."
258
+ }
259
+ `;
260
+ const WINDOWS_VERIFY_OWNER_ONLY_FILE_ACL_SCRIPT = String.raw `
261
+ $ErrorActionPreference = "Stop"
262
+ $filePath = $env:CRABLINE_PRIVATE_FILE_PATH
263
+ if ([string]::IsNullOrWhiteSpace($filePath)) {
264
+ throw "CRABLINE_PRIVATE_FILE_PATH is required."
265
+ }
266
+
267
+ $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
268
+ $sid = $identity.User
269
+ if ($null -eq $sid) {
270
+ throw "Could not resolve the current Windows user SID."
271
+ }
38
272
 
39
273
  $actual = Get-Acl -LiteralPath $filePath
40
274
  $ownerSid = $actual.GetOwner([System.Security.Principal.SecurityIdentifier])
@@ -62,7 +296,7 @@ if (
62
296
  throw "Private file DACL is not owner-only full control."
63
297
  }
64
298
  `;
65
- const WINDOWS_OWNER_ONLY_DIRECTORY_ACL_SCRIPT = String.raw `
299
+ const WINDOWS_VERIFY_SAFE_DIRECTORY_ENTRY_PARENT_SCRIPT = String.raw `
66
300
  $ErrorActionPreference = "Stop"
67
301
  $directoryPath = $env:CRABLINE_PRIVATE_DIRECTORY_PATH
68
302
  if ([string]::IsNullOrWhiteSpace($directoryPath)) {
@@ -75,64 +309,138 @@ if ($null -eq $sid) {
75
309
  throw "Could not resolve the current Windows user SID."
76
310
  }
77
311
 
78
- $acl = Get-Acl -LiteralPath $directoryPath
79
- $acl.SetOwner($sid)
80
- $acl.SetAccessRuleProtection($true, $false)
81
- $existingRules = @($acl.GetAccessRules(
82
- $true,
83
- $false,
84
- [System.Security.Principal.SecurityIdentifier]
85
- ))
86
- foreach ($existingRule in $existingRules) {
87
- [void]$acl.RemoveAccessRuleSpecific($existingRule)
88
- }
89
- $inheritance = (
90
- [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
91
- [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
312
+ $trustedSids = @(
313
+ $sid.Value,
314
+ "S-1-5-18",
315
+ "S-1-5-32-544",
316
+ "S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464"
92
317
  )
93
- $rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
94
- $sid,
95
- [System.Security.AccessControl.FileSystemRights]::FullControl,
96
- $inheritance,
97
- [System.Security.AccessControl.PropagationFlags]::None,
98
- [System.Security.AccessControl.AccessControlType]::Allow
318
+ $ancestorReplacementRights = (
319
+ [System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
320
+ [System.Security.AccessControl.FileSystemRights]::Delete -bor
321
+ [System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor
322
+ [System.Security.AccessControl.FileSystemRights]::TakeOwnership
99
323
  )
100
- $acl.SetAccessRule($rule)
101
- Set-Acl -LiteralPath $directoryPath -AclObject $acl
324
+ $ancestorReplacementMask = [BitConverter]::ToUInt32(
325
+ [BitConverter]::GetBytes([int32]$ancestorReplacementRights),
326
+ 0
327
+ )
328
+ $genericAll = [uint32]0x10000000
329
+ $inheritOnly = [System.Security.AccessControl.PropagationFlags]::InheritOnly
102
330
 
103
331
  $actual = Get-Acl -LiteralPath $directoryPath
332
+ $rawDescriptor = [System.Security.AccessControl.RawSecurityDescriptor]::new(
333
+ $actual.GetSecurityDescriptorBinaryForm(),
334
+ 0
335
+ )
336
+ if (
337
+ (($rawDescriptor.ControlFlags -band
338
+ [System.Security.AccessControl.ControlFlags]::DiscretionaryAclPresent) -eq 0) -or
339
+ $null -eq $rawDescriptor.DiscretionaryAcl
340
+ ) {
341
+ throw "Directory has an absent or null DACL in private mutation ancestry."
342
+ }
104
343
  $ownerSid = $actual.GetOwner([System.Security.Principal.SecurityIdentifier])
344
+ if ($trustedSids -notcontains $ownerSid.Value) {
345
+ throw "Directory owner is not trusted for private mutation ancestry."
346
+ }
105
347
  $rules = @($actual.GetAccessRules(
106
348
  $true,
107
349
  $true,
108
350
  [System.Security.Principal.SecurityIdentifier]
109
351
  ))
110
- if (-not $actual.AreAccessRulesProtected) {
111
- throw "Private directory DACL still inherits permissions."
352
+ foreach ($rule in $rules) {
353
+ $ruleAccessMask = [BitConverter]::ToUInt32(
354
+ [BitConverter]::GetBytes([int32]$rule.FileSystemRights),
355
+ 0
356
+ )
357
+ $appliesToDirectory = ($rule.PropagationFlags -band $inheritOnly) -eq 0
358
+ if (
359
+ $appliesToDirectory -and
360
+ $rule.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Allow -and
361
+ $trustedSids -notcontains $rule.IdentityReference.Value -and
362
+ ($ruleAccessMask -band ($ancestorReplacementMask -bor $genericAll)) -ne 0
363
+ ) {
364
+ throw "Directory grants child-deletion rights to an untrusted principal."
365
+ }
112
366
  }
113
- if ($ownerSid.Value -ne $sid.Value) {
114
- throw "Private directory owner SID does not match the current user."
367
+ `;
368
+ const WINDOWS_VERIFY_SAFE_DIRECTORY_MUTATION_BOUNDARY_SCRIPT = String.raw `
369
+ $ErrorActionPreference = "Stop"
370
+ $directoryPath = $env:CRABLINE_PRIVATE_DIRECTORY_PATH
371
+ if ([string]::IsNullOrWhiteSpace($directoryPath)) {
372
+ throw "CRABLINE_PRIVATE_DIRECTORY_PATH is required."
115
373
  }
116
- if ($rules.Count -ne 1) {
117
- throw "Private directory DACL must contain exactly one access rule."
374
+
375
+ $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
376
+ $sid = $identity.User
377
+ if ($null -eq $sid) {
378
+ throw "Could not resolve the current Windows user SID."
118
379
  }
119
- $actualRule = $rules[0]
120
- $requiredInheritance = (
121
- [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
122
- [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
380
+
381
+ $trustedSids = @(
382
+ $sid.Value,
383
+ "S-1-5-18",
384
+ "S-1-5-32-544",
385
+ "S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464"
386
+ )
387
+ $mutationRights = (
388
+ [System.Security.AccessControl.FileSystemRights]::WriteData -bor
389
+ [System.Security.AccessControl.FileSystemRights]::AppendData -bor
390
+ [System.Security.AccessControl.FileSystemRights]::WriteExtendedAttributes -bor
391
+ [System.Security.AccessControl.FileSystemRights]::WriteAttributes -bor
392
+ [System.Security.AccessControl.FileSystemRights]::Delete -bor
393
+ [System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
394
+ [System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor
395
+ [System.Security.AccessControl.FileSystemRights]::TakeOwnership
396
+ )
397
+ $genericMutationRights = [uint32]0x50000000
398
+ $inheritOnly = [System.Security.AccessControl.PropagationFlags]::InheritOnly
399
+ $mutationAccessMask = [BitConverter]::ToUInt32(
400
+ [BitConverter]::GetBytes([int32]$mutationRights),
401
+ 0
402
+ )
403
+
404
+ $actual = Get-Acl -LiteralPath $directoryPath
405
+ $rawDescriptor = [System.Security.AccessControl.RawSecurityDescriptor]::new(
406
+ $actual.GetSecurityDescriptorBinaryForm(),
407
+ 0
123
408
  )
124
409
  if (
125
- $actualRule.IsInherited -or
126
- $actualRule.IdentityReference.Value -ne $sid.Value -or
127
- $actualRule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or
128
- (($actualRule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl) -or
129
- (($actualRule.InheritanceFlags -band $requiredInheritance) -ne $requiredInheritance)
410
+ (($rawDescriptor.ControlFlags -band
411
+ [System.Security.AccessControl.ControlFlags]::DiscretionaryAclPresent) -eq 0) -or
412
+ $null -eq $rawDescriptor.DiscretionaryAcl
130
413
  ) {
131
- throw "Private directory DACL is not owner-only inheritable full control."
414
+ throw "Directory has an absent or null DACL in private mutation ancestry."
415
+ }
416
+ $ownerSid = $actual.GetOwner([System.Security.Principal.SecurityIdentifier])
417
+ if ($trustedSids -notcontains $ownerSid.Value) {
418
+ throw "Directory owner is not trusted for private mutation ancestry."
419
+ }
420
+ $rules = @($actual.GetAccessRules(
421
+ $true,
422
+ $true,
423
+ [System.Security.Principal.SecurityIdentifier]
424
+ ))
425
+ foreach ($rule in $rules) {
426
+ $ruleAccessMask = [BitConverter]::ToUInt32(
427
+ [BitConverter]::GetBytes([int32]$rule.FileSystemRights),
428
+ 0
429
+ )
430
+ $appliesToDirectory = ($rule.PropagationFlags -band $inheritOnly) -eq 0
431
+ if (
432
+ $appliesToDirectory -and
433
+ $rule.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Allow -and
434
+ $trustedSids -notcontains $rule.IdentityReference.Value -and
435
+ ($ruleAccessMask -band ($mutationAccessMask -bor $genericMutationRights)) -ne 0
436
+ ) {
437
+ throw "Directory grants mutation rights to an untrusted principal."
438
+ }
132
439
  }
133
440
  `;
134
441
  const runWindowsAclCommand = async (command, args, options) => {
135
- await execFileAsync(command, args, options);
442
+ const result = await execFileAsync(command, args, { ...options, encoding: "utf8" });
443
+ return result.stdout;
136
444
  };
137
445
  export function resolveWindowsPowerShellPath(systemRoot) {
138
446
  const normalizedRoot = systemRoot?.trim() ? path.win32.normalize(systemRoot.trim()) : undefined;
@@ -141,22 +449,128 @@ export function resolveWindowsPowerShellPath(systemRoot) {
141
449
  }
142
450
  return path.win32.join(normalizedRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
143
451
  }
144
- export async function applyOwnerOnlyWindowsAcl(filePath, run = runWindowsAclCommand, systemRoot = process.env.SystemRoot) {
452
+ export async function createOwnerOnlyWindowsFile(filePath, run = runWindowsAclCommand, systemRoot = process.env.SystemRoot) {
145
453
  try {
146
454
  const powershellPath = resolveWindowsPowerShellPath(systemRoot);
147
- await run(powershellPath, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", WINDOWS_OWNER_ONLY_ACL_SCRIPT], {
455
+ const output = await run(powershellPath, [
456
+ "-NoLogo",
457
+ "-NoProfile",
458
+ "-NonInteractive",
459
+ "-Command",
460
+ WINDOWS_CREATE_OWNER_ONLY_FILE_SCRIPT,
461
+ ], {
148
462
  env: {
149
463
  ...process.env,
150
464
  CRABLINE_PRIVATE_FILE_PATH: path.resolve(filePath),
151
465
  },
466
+ killSignal: "SIGKILL",
467
+ timeout: WINDOWS_ACL_COMMAND_TIMEOUT_MS,
152
468
  windowsHide: true,
153
469
  });
470
+ const match = /^(\d+):(\d+)$/u.exec(output.trim());
471
+ if (!match) {
472
+ throw new Error("Windows did not return a stable private file identity.");
473
+ }
474
+ const identity = {
475
+ device: BigInt(match[1]),
476
+ inode: BigInt(match[2]),
477
+ };
478
+ if (identity.inode <= 0n) {
479
+ throw new Error("Windows did not return a stable private file identity.");
480
+ }
481
+ return identity;
154
482
  }
155
483
  catch (error) {
156
- throw new Error("Could not apply and verify an owner-only Windows ACL; powershell.exe with Set-Acl is required.", { cause: error });
484
+ throw new Error("Could not atomically create and verify an owner-only Windows private file; Windows PowerShell ACL support is required.", { cause: error });
157
485
  }
158
486
  }
159
487
  export async function applyOwnerOnlyWindowsDirectoryAcl(directoryPath, run = runWindowsAclCommand, systemRoot = process.env.SystemRoot) {
488
+ await applyOwnerOnlyWindowsDirectoryAclByHandle(directoryPath, run, systemRoot);
489
+ }
490
+ export async function createOwnerOnlyWindowsDirectoryAncestry(directoryPath, run = runWindowsAclCommand, systemRoot = process.env.SystemRoot) {
491
+ try {
492
+ return await createOwnerOnlyWindowsDirectoryAncestryByHandle(directoryPath, run, systemRoot);
493
+ }
494
+ catch (error) {
495
+ throw new Error("Could not atomically create and verify owner-only Windows private directory ancestry; Windows PowerShell security descriptor support is required.", { cause: error });
496
+ }
497
+ }
498
+ export async function verifyOwnerOnlyWindowsDirectoryAcl(directoryPath, run = runWindowsAclCommand, systemRoot = process.env.SystemRoot) {
499
+ try {
500
+ const powershellPath = resolveWindowsPowerShellPath(systemRoot);
501
+ await run(powershellPath, [
502
+ "-NoLogo",
503
+ "-NoProfile",
504
+ "-NonInteractive",
505
+ "-Command",
506
+ WINDOWS_VERIFY_OWNER_ONLY_DIRECTORY_ACL_SCRIPT,
507
+ ], {
508
+ env: {
509
+ ...process.env,
510
+ CRABLINE_PRIVATE_DIRECTORY_PATH: path.resolve(directoryPath),
511
+ },
512
+ killSignal: "SIGKILL",
513
+ timeout: WINDOWS_ACL_COMMAND_TIMEOUT_MS,
514
+ windowsHide: true,
515
+ });
516
+ }
517
+ catch (error) {
518
+ throw new Error("Private mutation parent must have an owner-only protected Windows ACL.", {
519
+ cause: error,
520
+ });
521
+ }
522
+ }
523
+ export async function verifyOwnerOnlyWindowsFileAcl(filePath, run = runWindowsAclCommand, systemRoot = process.env.SystemRoot) {
524
+ try {
525
+ const powershellPath = resolveWindowsPowerShellPath(systemRoot);
526
+ await run(powershellPath, [
527
+ "-NoLogo",
528
+ "-NoProfile",
529
+ "-NonInteractive",
530
+ "-Command",
531
+ WINDOWS_VERIFY_OWNER_ONLY_FILE_ACL_SCRIPT,
532
+ ], {
533
+ env: {
534
+ ...process.env,
535
+ CRABLINE_PRIVATE_FILE_PATH: path.resolve(filePath),
536
+ },
537
+ killSignal: "SIGKILL",
538
+ timeout: WINDOWS_ACL_COMMAND_TIMEOUT_MS,
539
+ windowsHide: true,
540
+ });
541
+ }
542
+ catch (error) {
543
+ throw new Error("Private mutation claim must have an owner-only protected Windows ACL.", {
544
+ cause: error,
545
+ });
546
+ }
547
+ }
548
+ export async function verifySafeWindowsDirectoryEntryParent(directoryPath, run = runWindowsAclCommand, systemRoot = process.env.SystemRoot) {
549
+ try {
550
+ const powershellPath = resolveWindowsPowerShellPath(systemRoot);
551
+ await run(powershellPath, [
552
+ "-NoLogo",
553
+ "-NoProfile",
554
+ "-NonInteractive",
555
+ "-Command",
556
+ WINDOWS_VERIFY_SAFE_DIRECTORY_ENTRY_PARENT_SCRIPT,
557
+ ], {
558
+ env: {
559
+ ...process.env,
560
+ CRABLINE_PRIVATE_DIRECTORY_PATH: path.resolve(directoryPath),
561
+ },
562
+ killSignal: "SIGKILL",
563
+ timeout: WINDOWS_ACL_COMMAND_TIMEOUT_MS,
564
+ windowsHide: true,
565
+ });
566
+ }
567
+ catch (error) {
568
+ throw new Error("Private mutation boundary has a replaceable Windows ancestor.", {
569
+ cause: error,
570
+ });
571
+ }
572
+ }
573
+ export async function verifySafeWindowsDirectoryMutationBoundary(directoryPath, run = runWindowsAclCommand, systemRoot = process.env.SystemRoot) {
160
574
  try {
161
575
  const powershellPath = resolveWindowsPowerShellPath(systemRoot);
162
576
  await run(powershellPath, [
@@ -164,17 +578,26 @@ export async function applyOwnerOnlyWindowsDirectoryAcl(directoryPath, run = run
164
578
  "-NoProfile",
165
579
  "-NonInteractive",
166
580
  "-Command",
167
- WINDOWS_OWNER_ONLY_DIRECTORY_ACL_SCRIPT,
581
+ WINDOWS_VERIFY_SAFE_DIRECTORY_MUTATION_BOUNDARY_SCRIPT,
168
582
  ], {
169
583
  env: {
170
584
  ...process.env,
171
585
  CRABLINE_PRIVATE_DIRECTORY_PATH: path.resolve(directoryPath),
172
586
  },
587
+ killSignal: "SIGKILL",
588
+ timeout: WINDOWS_ACL_COMMAND_TIMEOUT_MS,
173
589
  windowsHide: true,
174
590
  });
175
591
  }
176
592
  catch (error) {
177
- throw new Error("Could not apply and verify an owner-only Windows directory ACL; powershell.exe with Set-Acl is required.", { cause: error });
593
+ throw new Error("Private mutation ancestry has an unsafe Windows ACL.", {
594
+ cause: error,
595
+ });
596
+ }
597
+ }
598
+ function assertSameFileIdentity(actual, expected) {
599
+ if (actual.device !== expected.device || actual.inode !== expected.inode) {
600
+ throw new Error("Private file path identity changed during publication.");
178
601
  }
179
602
  }
180
603
  async function readHandleIdentity(handle) {
@@ -202,6 +625,21 @@ async function assertPathIdentity(filePath, expected) {
202
625
  }
203
626
  throw new Error("Private file path identity changed during publication.");
204
627
  }
628
+ async function pathHasFileIdentity(filePath, expected) {
629
+ try {
630
+ const stats = await fs.lstat(filePath, { bigint: true });
631
+ return (stats.isFile() &&
632
+ stats.nlink === 1n &&
633
+ stats.dev === expected.device &&
634
+ stats.ino === expected.inode);
635
+ }
636
+ catch (error) {
637
+ if (error.code === "ENOENT") {
638
+ return false;
639
+ }
640
+ throw error;
641
+ }
642
+ }
205
643
  async function readDirectoryHandleIdentity(handle) {
206
644
  const stats = await handle.stat({ bigint: true });
207
645
  if (!stats.isDirectory() || stats.ino <= 0n) {
@@ -291,30 +729,282 @@ async function syncPathAncestry(filePath, syncParent, platform, firstCreatedDire
291
729
  currentPath = parentPath;
292
730
  }
293
731
  }
294
- export async function removeSecuredPrivateDirectory(secured, currentPath = secured.directoryPath, quarantineBaseName = path.basename(currentPath)) {
295
- if (path.basename(quarantineBaseName) !== quarantineBaseName || quarantineBaseName.length === 0) {
296
- throw new Error("Private directory quarantine basename is malformed.");
732
+ async function darwinDirectoryHasExtendedAcl(directoryPath) {
733
+ if (process.platform !== "darwin") {
734
+ return false;
297
735
  }
298
- const quarantinePath = path.join(path.dirname(currentPath), `.${quarantineBaseName}.${process.pid}.${randomUUID()}.remove`);
299
- await secured.assertIdentityAt(currentPath);
300
- await fs.rename(currentPath, quarantinePath);
301
- await secured.assertIdentityAt(quarantinePath);
302
- await syncParentDirectory(quarantinePath);
303
- await fs.rm(quarantinePath, { force: true, recursive: true });
304
- await syncParentDirectory(quarantinePath);
736
+ let output;
737
+ try {
738
+ const result = await execFileAsync("/bin/ls", ["-lde", directoryPath], {
739
+ encoding: "utf8",
740
+ env: { ...process.env, LC_ALL: "C" },
741
+ maxBuffer: 64 * 1024,
742
+ });
743
+ output = result.stdout;
744
+ }
745
+ catch (error) {
746
+ throw new Error("Could not verify the private directory macOS ACL.", { cause: error });
747
+ }
748
+ const mode = output.trimStart().split(/\s+/u, 1)[0] ?? "";
749
+ return mode.includes("+");
305
750
  }
306
- export async function securePrivateDirectory(directoryPath, options = {}) {
307
- let created = false;
751
+ async function assertDarwinDirectoryHasNoExtendedAcl(directoryPath) {
752
+ if (await darwinDirectoryHasExtendedAcl(directoryPath)) {
753
+ throw new Error("Private directory must not have a macOS extended ACL.");
754
+ }
755
+ }
756
+ async function removeDarwinExtendedAcl(directoryPath) {
757
+ if (process.platform !== "darwin") {
758
+ return;
759
+ }
308
760
  try {
309
- await fs.mkdir(directoryPath, { mode: 0o700 });
310
- created = true;
761
+ await execFileAsync("/bin/chmod", ["-N", directoryPath], {
762
+ encoding: "utf8",
763
+ env: { ...process.env, LC_ALL: "C" },
764
+ maxBuffer: 64 * 1024,
765
+ });
311
766
  }
312
767
  catch (error) {
313
- if (error.code !== "EEXIST") {
314
- throw error;
315
- }
768
+ throw new Error("Could not remove the legacy private directory macOS ACL.", {
769
+ cause: error,
770
+ });
316
771
  }
317
- const handle = await fs.open(directoryPath, "r");
772
+ }
773
+ async function nearestExistingDirectory(directoryPath) {
774
+ let currentPath = path.resolve(directoryPath);
775
+ for (;;) {
776
+ try {
777
+ const stats = await fs.lstat(currentPath);
778
+ if (!stats.isDirectory()) {
779
+ throw new Error("Private directory ancestry contains a non-directory entry.");
780
+ }
781
+ return currentPath;
782
+ }
783
+ catch (error) {
784
+ if (error.code !== "ENOENT") {
785
+ throw error;
786
+ }
787
+ }
788
+ const parentPath = path.dirname(currentPath);
789
+ if (parentPath === currentPath) {
790
+ throw new Error("Private directory ancestry has no existing parent.");
791
+ }
792
+ currentPath = parentPath;
793
+ }
794
+ }
795
+ async function assertDarwinCreatedAncestryHasNoExtendedAcl(firstCreatedDirectory, finalDirectory) {
796
+ if (process.platform !== "darwin" || firstCreatedDirectory === undefined) {
797
+ return;
798
+ }
799
+ const firstCreatedPath = path.resolve(firstCreatedDirectory);
800
+ let currentPath = path.resolve(finalDirectory);
801
+ const createdPaths = [];
802
+ for (;;) {
803
+ createdPaths.push(currentPath);
804
+ if (currentPath === firstCreatedPath) {
805
+ break;
806
+ }
807
+ const parentPath = path.dirname(currentPath);
808
+ if (parentPath === currentPath) {
809
+ throw new Error("Private directory ancestry creation boundary is invalid.");
810
+ }
811
+ currentPath = parentPath;
812
+ }
813
+ for (const createdPath of createdPaths.reverse()) {
814
+ await assertDarwinDirectoryHasNoExtendedAcl(createdPath);
815
+ }
816
+ }
817
+ async function captureSingleSafePrivateMutationBoundary(directoryPath, platform, stickyTargetOwnedByCurrentUser) {
818
+ if (platform === "win32" && process.platform === "win32") {
819
+ await verifySafeWindowsDirectoryMutationBoundary(directoryPath);
820
+ return await captureDirectoryIdentity(directoryPath);
821
+ }
822
+ const currentUserId = process.geteuid?.();
823
+ if (currentUserId === undefined || !Number.isSafeInteger(currentUserId) || currentUserId < 0) {
824
+ throw new Error("Could not resolve the current POSIX user for private mutation.");
825
+ }
826
+ const stats = await fs.lstat(directoryPath, { bigint: true });
827
+ if (!stats.isDirectory()) {
828
+ throw new Error("Private mutation boundary is not a directory.");
829
+ }
830
+ const writableByAnotherPrincipal = (stats.mode & 18n) !== 0n;
831
+ const trustedOwner = stats.uid === BigInt(currentUserId) || stats.uid === 0n;
832
+ if (!trustedOwner) {
833
+ throw new Error("Private mutation boundary is not owned by the current POSIX user or root.");
834
+ }
835
+ const protectedByStickyOwnership = trustedOwner && (stats.mode & 512n) !== 0n && stickyTargetOwnedByCurrentUser;
836
+ if (writableByAnotherPrincipal && !protectedByStickyOwnership) {
837
+ throw new Error("Private mutation boundary is writable by another POSIX principal.");
838
+ }
839
+ await assertDarwinDirectoryHasNoExtendedAcl(directoryPath);
840
+ return await captureDirectoryIdentity(directoryPath);
841
+ }
842
+ async function captureSafePrivateMutationBoundary(directoryPath, platform, stickyTargetOwnedByCurrentUser) {
843
+ const resolvedBoundaryPath = path.resolve(directoryPath);
844
+ const boundary = await captureSingleSafePrivateMutationBoundary(resolvedBoundaryPath, platform, stickyTargetOwnedByCurrentUser);
845
+ return await capturePrivateMutationPathAncestry(resolvedBoundaryPath, platform, boundary);
846
+ }
847
+ async function capturePrivateMutationPathAncestry(directoryPath, platform, boundary) {
848
+ const resolvedBoundaryPath = path.resolve(directoryPath);
849
+ const capturedBoundary = boundary ?? (await captureDirectoryIdentity(resolvedBoundaryPath));
850
+ const ancestors = [];
851
+ let childPath = await fs.realpath(resolvedBoundaryPath);
852
+ for (;;) {
853
+ const parentPath = path.dirname(childPath);
854
+ if (parentPath === childPath) {
855
+ break;
856
+ }
857
+ if (platform === "win32" && process.platform === "win32") {
858
+ await verifySafeWindowsDirectoryEntryParent(parentPath);
859
+ ancestors.push(await captureDirectoryIdentity(parentPath));
860
+ }
861
+ else {
862
+ const childStats = await fs.lstat(childPath, { bigint: true });
863
+ const currentUserId = process.geteuid?.();
864
+ const childOwnedByTrustedPrincipal = currentUserId !== undefined &&
865
+ (childStats.uid === BigInt(currentUserId) || childStats.uid === 0n);
866
+ ancestors.push(await captureSingleSafePrivateMutationBoundary(parentPath, platform, childOwnedByTrustedPrincipal));
867
+ }
868
+ childPath = parentPath;
869
+ }
870
+ return {
871
+ async assertIdentityAt(currentPath = resolvedBoundaryPath) {
872
+ for (const ancestor of [...ancestors].reverse()) {
873
+ await ancestor.assertIdentityAt();
874
+ }
875
+ await capturedBoundary.assertIdentityAt(currentPath);
876
+ },
877
+ directoryPath: resolvedBoundaryPath,
878
+ };
879
+ }
880
+ async function captureSafePrivateDirectoryMutationParent(currentPath, platform) {
881
+ if (platform === "win32" && process.platform === "win32") {
882
+ return await captureSafePrivateMutationBoundary(path.dirname(currentPath), platform, true);
883
+ }
884
+ const currentUserId = process.geteuid?.();
885
+ const targetStats = await fs.lstat(currentPath, { bigint: true });
886
+ return await captureSafePrivateMutationBoundary(path.dirname(currentPath), platform, currentUserId !== undefined &&
887
+ targetStats.isDirectory() &&
888
+ targetStats.uid === BigInt(currentUserId));
889
+ }
890
+ export async function removeSecuredPrivateDirectory(secured, currentPath = secured.directoryPath, quarantineBaseName = path.basename(currentPath), options = {}) {
891
+ if (path.basename(quarantineBaseName) !== quarantineBaseName || quarantineBaseName.length === 0) {
892
+ throw new Error("Private directory quarantine basename is malformed.");
893
+ }
894
+ const quarantinePath = path.join(path.dirname(currentPath), `.${quarantineBaseName}.${process.pid}.${randomUUID()}.remove`);
895
+ const platform = options.platform ?? process.platform;
896
+ const parent = await captureSafePrivateDirectoryMutationParent(currentPath, platform);
897
+ const claimParent = {
898
+ assertIdentityAt: (candidatePath = currentPath) => secured.assertIdentityAt(candidatePath),
899
+ directoryPath: currentPath,
900
+ };
901
+ const claim = await acquirePrivateMutationClaimChain(claimParent, {
902
+ ...(options.claimRuntime ? { runtime: options.claimRuntime } : {}),
903
+ ...(options.createWindowsFile ? { createWindowsFile: options.createWindowsFile } : {}),
904
+ ...(options.platform ? { platform: options.platform } : {}),
905
+ });
906
+ let removalFailed = false;
907
+ let primaryError;
908
+ try {
909
+ await parent.assertIdentityAt();
910
+ await claim.assertOwned();
911
+ await secured.assertIdentityAt(currentPath);
912
+ await options.beforeRename?.(currentPath);
913
+ await parent.assertIdentityAt();
914
+ await claim.assertOwned();
915
+ await secured.assertIdentityAt(currentPath);
916
+ await fs.rename(currentPath, quarantinePath);
917
+ await parent.assertIdentityAt();
918
+ await secured.assertIdentityAt(quarantinePath);
919
+ await claim.relocateParent(quarantinePath);
920
+ await claim.assertOwned();
921
+ const syncParent = options.syncParent ?? syncParentDirectory;
922
+ await syncParent(quarantinePath, options.platform);
923
+ await options.beforeRecursiveRemove?.(quarantinePath);
924
+ await parent.assertIdentityAt();
925
+ await claim.assertOwned();
926
+ await secured.assertIdentityAt(quarantinePath);
927
+ await claim.prepareContainerRemoval();
928
+ await (options.removeDirectory ??
929
+ ((candidatePath) => fs.rm(candidatePath, { force: true, recursive: true })))(quarantinePath);
930
+ await claim.completeContainerRemoval();
931
+ await parent.assertIdentityAt();
932
+ await syncParent(quarantinePath, options.platform);
933
+ }
934
+ catch (error) {
935
+ removalFailed = true;
936
+ primaryError = error;
937
+ }
938
+ try {
939
+ await claim.release();
940
+ }
941
+ catch (releaseError) {
942
+ if (removalFailed) {
943
+ const aggregateError = new AggregateError([primaryError, releaseError], "Private directory removal failed and its mutation claim could not be released.");
944
+ aggregateError.cause = primaryError;
945
+ throw aggregateError;
946
+ }
947
+ throw releaseError;
948
+ }
949
+ if (removalFailed) {
950
+ throw primaryError;
951
+ }
952
+ }
953
+ export async function securePrivateDirectory(directoryPath, options = {}) {
954
+ const platform = options.platform ?? process.platform;
955
+ let created = false;
956
+ let createdAtomically = false;
957
+ if (platform === "win32") {
958
+ try {
959
+ await fs.lstat(directoryPath);
960
+ }
961
+ catch (error) {
962
+ if (error.code !== "ENOENT") {
963
+ throw error;
964
+ }
965
+ const createWindowsDirectories = options.createWindowsDirectories ??
966
+ (process.platform === "win32" || options.secureWindowsDirectory === undefined
967
+ ? createOwnerOnlyWindowsDirectoryAncestry
968
+ : async (candidatePath) => {
969
+ await fs.mkdir(candidatePath);
970
+ return candidatePath;
971
+ });
972
+ const firstCreatedDirectory = await createWindowsDirectories(directoryPath);
973
+ if (firstCreatedDirectory === undefined) {
974
+ throw new Error("Windows private directory creation did not create the requested path.", {
975
+ cause: error,
976
+ });
977
+ }
978
+ created = true;
979
+ createdAtomically =
980
+ options.createWindowsDirectories !== undefined || process.platform === "win32";
981
+ }
982
+ }
983
+ else {
984
+ let existed = false;
985
+ try {
986
+ await fs.lstat(directoryPath);
987
+ existed = true;
988
+ }
989
+ catch (error) {
990
+ if (error.code !== "ENOENT") {
991
+ throw error;
992
+ }
993
+ }
994
+ if (!existed) {
995
+ await assertDarwinDirectoryHasNoExtendedAcl(path.dirname(directoryPath));
996
+ }
997
+ try {
998
+ await fs.mkdir(directoryPath, { mode: 0o700 });
999
+ created = true;
1000
+ }
1001
+ catch (error) {
1002
+ if (error.code !== "EEXIST") {
1003
+ throw error;
1004
+ }
1005
+ }
1006
+ }
1007
+ const handle = await fs.open(directoryPath, "r");
318
1008
  let handleOpen = true;
319
1009
  const closeHandle = async () => {
320
1010
  if (!handleOpen) {
@@ -346,8 +1036,13 @@ export async function securePrivateDirectory(directoryPath, options = {}) {
346
1036
  };
347
1037
  try {
348
1038
  await secured.assertIdentityAt();
349
- if ((options.platform ?? process.platform) === "win32") {
350
- await (options.secureWindowsDirectory ?? applyOwnerOnlyWindowsDirectoryAcl)(directoryPath);
1039
+ if (platform === "win32") {
1040
+ if (!createdAtomically) {
1041
+ await (options.secureWindowsDirectory ?? applyOwnerOnlyWindowsDirectoryAcl)(directoryPath);
1042
+ }
1043
+ if (process.platform !== "win32") {
1044
+ await handle.chmod(0o700);
1045
+ }
351
1046
  }
352
1047
  else {
353
1048
  const currentUserId = options.currentUserId ?? process.geteuid?.();
@@ -357,7 +1052,14 @@ export async function securePrivateDirectory(directoryPath, options = {}) {
357
1052
  identity.userId !== BigInt(currentUserId)) {
358
1053
  throw new Error("Private directory must be owned by the current POSIX user.");
359
1054
  }
360
- await handle.chmod(0o700);
1055
+ if (!created) {
1056
+ await removeDarwinExtendedAcl(directoryPath);
1057
+ }
1058
+ const currentStats = await handle.stat({ bigint: true });
1059
+ const mutationRootMode = options.markMutationRoot === false ? currentStats.mode & 512n : 512n;
1060
+ await handle.chmod(Number(448n | mutationRootMode));
1061
+ await assertDarwinDirectoryHasNoExtendedAcl(directoryPath);
1062
+ await (options.syncDirectory ?? (() => handle.sync()))();
361
1063
  }
362
1064
  await secured.assertIdentityAt();
363
1065
  const syncParent = options.syncParent ?? syncParentDirectory;
@@ -395,31 +1097,1242 @@ export async function securePrivateDirectory(directoryPath, options = {}) {
395
1097
  }
396
1098
  return secured;
397
1099
  }
1100
+ const PRIVATE_MUTATION_CLAIM_ROOT_FILE = ".crabline-private-mutation.claim";
1101
+ const PRIVATE_MUTATION_CLAIM_OWNER_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
1102
+ const PRIVATE_MUTATION_CLAIM_METADATA_MAX_BYTES = 4096;
1103
+ const PRIVATE_MUTATION_RESERVED_BASENAME_PATTERN = /^\.crabline-private-mutation(?:\.|$)/iu;
1104
+ function assertNotReservedPrivateMutationPath(targetPath) {
1105
+ if (PRIVATE_MUTATION_RESERVED_BASENAME_PATTERN.test(path.basename(targetPath))) {
1106
+ throw new Error("Private path uses Crabline's reserved mutation claim namespace.");
1107
+ }
1108
+ }
1109
+ function isProcessAlive(pid) {
1110
+ if (!Number.isSafeInteger(pid) || pid <= 0) {
1111
+ return false;
1112
+ }
1113
+ try {
1114
+ process.kill(pid, 0);
1115
+ return true;
1116
+ }
1117
+ catch (error) {
1118
+ return error.code === "EPERM";
1119
+ }
1120
+ }
1121
+ function processIdentityFromLinuxStat(value, bootId) {
1122
+ const normalizedBootId = bootId.trim();
1123
+ if (!/^[0-9a-f-]{16,64}$/iu.test(normalizedBootId)) {
1124
+ return null;
1125
+ }
1126
+ const commandEnd = value.lastIndexOf(") ");
1127
+ if (commandEnd < 0) {
1128
+ return null;
1129
+ }
1130
+ const fields = value
1131
+ .slice(commandEnd + 2)
1132
+ .trim()
1133
+ .split(/\s+/u);
1134
+ const startTicks = fields[19];
1135
+ return startTicks && /^\d+$/u.test(startTicks) ? `linux:${normalizedBootId}:${startTicks}` : null;
1136
+ }
1137
+ function processIdentityFromDarwin(processDetails, bootTime) {
1138
+ const bootMatch = /\bsec = (\d+), usec = (\d+)\b/u.exec(bootTime);
1139
+ const launchMatch = /^Launch Time:\s*(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})\.(\d{3,6}) ([+-])(\d{2})(\d{2})$/mu.exec(processDetails);
1140
+ if (!bootMatch || !launchMatch) {
1141
+ return null;
1142
+ }
1143
+ const [, year, month, day, hour, minute, second, fraction, sign, offsetHour, offsetMinute] = launchMatch;
1144
+ const offsetMs = (Number(offsetHour) * 60 + Number(offsetMinute)) * 60_000 * (sign === "+" ? 1 : -1);
1145
+ const fractionMicros = Number(fraction.padEnd(6, "0"));
1146
+ const utcMilliseconds = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second), Math.floor(fractionMicros / 1_000)) - offsetMs;
1147
+ if (!Number.isSafeInteger(utcMilliseconds) || utcMilliseconds <= 0) {
1148
+ return null;
1149
+ }
1150
+ const startedAtMicros = BigInt(utcMilliseconds) * 1000n + BigInt(fractionMicros % 1_000);
1151
+ return `darwin:${bootMatch[1]}.${bootMatch[2]}:us:${startedAtMicros}`;
1152
+ }
1153
+ function getProcessIdentity(pid) {
1154
+ if (!Number.isSafeInteger(pid) || pid <= 0) {
1155
+ return null;
1156
+ }
1157
+ if (process.platform === "linux") {
1158
+ try {
1159
+ return processIdentityFromLinuxStat(readFileSync(`/proc/${pid}/stat`, "utf8"), readFileSync("/proc/sys/kernel/random/boot_id", "utf8"));
1160
+ }
1161
+ catch {
1162
+ return null;
1163
+ }
1164
+ }
1165
+ if (process.platform === "darwin") {
1166
+ const options = {
1167
+ encoding: "utf8",
1168
+ env: { ...process.env, LC_ALL: "C", TZ: "UTC" },
1169
+ maxBuffer: 512 * 1024,
1170
+ timeout: 3_000,
1171
+ };
1172
+ const bootTime = spawnSync("/usr/sbin/sysctl", ["-n", "kern.boottime"], options);
1173
+ const processDetails = spawnSync("/usr/bin/vmmap", ["-summary", String(pid)], options);
1174
+ return bootTime.status === 0 && processDetails.status === 0
1175
+ ? processIdentityFromDarwin(processDetails.stdout, bootTime.stdout)
1176
+ : null;
1177
+ }
1178
+ if (process.platform !== "win32") {
1179
+ return null;
1180
+ }
1181
+ let powershellPath;
1182
+ try {
1183
+ powershellPath = resolveWindowsPowerShellPath(process.env.SystemRoot);
1184
+ }
1185
+ catch {
1186
+ return null;
1187
+ }
1188
+ const result = spawnSync(powershellPath, [
1189
+ "-NoProfile",
1190
+ "-NonInteractive",
1191
+ "-Command",
1192
+ `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks.ToString()`,
1193
+ ], { encoding: "utf8", timeout: 1_000, windowsHide: true });
1194
+ const ticks = result.status === 0 ? result.stdout.trim() : "";
1195
+ return /^\d+$/u.test(ticks) ? `windows:${ticks}` : null;
1196
+ }
1197
+ let cachedCurrentProcessIdentity;
1198
+ function defaultPrivateMutationClaimRuntime() {
1199
+ if (cachedCurrentProcessIdentity === undefined) {
1200
+ cachedCurrentProcessIdentity = getProcessIdentity(process.pid);
1201
+ }
1202
+ return {
1203
+ getProcessIdentity,
1204
+ isProcessAlive,
1205
+ ownerId: randomUUID(),
1206
+ pid: process.pid,
1207
+ ...(cachedCurrentProcessIdentity ? { processIdentity: cachedCurrentProcessIdentity } : {}),
1208
+ processStartedAtMs: Math.floor(performance.timeOrigin),
1209
+ };
1210
+ }
1211
+ function parsePrivateMutationClaimOwner(contents) {
1212
+ let owner;
1213
+ try {
1214
+ owner = JSON.parse(contents);
1215
+ }
1216
+ catch (error) {
1217
+ throw new Error("Private path mutation claim owner metadata is malformed.", { cause: error });
1218
+ }
1219
+ if (!Number.isSafeInteger(owner.pid) ||
1220
+ Number(owner.pid) <= 0 ||
1221
+ typeof owner.ownerId !== "string" ||
1222
+ !PRIVATE_MUTATION_CLAIM_OWNER_ID_PATTERN.test(owner.ownerId) ||
1223
+ !Number.isSafeInteger(owner.processStartedAtMs) ||
1224
+ Number(owner.processStartedAtMs) <= 0 ||
1225
+ (owner.processIdentity !== undefined &&
1226
+ (typeof owner.processIdentity !== "string" ||
1227
+ owner.processIdentity.length === 0 ||
1228
+ owner.processIdentity.length > 256))) {
1229
+ throw new Error("Private path mutation claim owner metadata is malformed.");
1230
+ }
1231
+ return owner;
1232
+ }
1233
+ async function assertClaimPathIdentity(claimPath, expected, expectedLinkCount) {
1234
+ try {
1235
+ const stats = await fs.lstat(claimPath, { bigint: true });
1236
+ if (stats.isFile() &&
1237
+ (expectedLinkCount === undefined ? stats.nlink >= 1n : stats.nlink === expectedLinkCount) &&
1238
+ stats.dev === expected.device &&
1239
+ stats.ino === expected.inode) {
1240
+ return;
1241
+ }
1242
+ }
1243
+ catch (error) {
1244
+ throw new Error("Private path mutation claim identity changed.", { cause: error });
1245
+ }
1246
+ throw new Error("Private path mutation claim identity changed.");
1247
+ }
1248
+ function assertOwnerOnlyPosixMutationClaim(stats, kind) {
1249
+ const currentUserId = process.geteuid?.();
1250
+ if (currentUserId === undefined || !Number.isSafeInteger(currentUserId) || currentUserId < 0) {
1251
+ throw new Error("Could not resolve the current POSIX user for private mutation claims.");
1252
+ }
1253
+ if (stats.uid !== BigInt(currentUserId) || (stats.mode & 63n) !== 0n) {
1254
+ throw new Error(`Private mutation claim ${kind} must be owner-only.`);
1255
+ }
1256
+ }
1257
+ async function readPrivateMutationClaim(claimPath) {
1258
+ let handle;
1259
+ try {
1260
+ handle = await fs.open(claimPath, process.platform === "win32"
1261
+ ? "r"
1262
+ : fsConstants.O_RDONLY | fsConstants.O_NONBLOCK | fsConstants.O_NOFOLLOW);
1263
+ const stats = await handle.stat({ bigint: true });
1264
+ if (!stats.isFile() ||
1265
+ stats.ino <= 0n ||
1266
+ stats.size <= 0n ||
1267
+ stats.size > BigInt(PRIVATE_MUTATION_CLAIM_METADATA_MAX_BYTES)) {
1268
+ throw new Error("Private path mutation claim metadata size is invalid.");
1269
+ }
1270
+ if (process.platform === "win32") {
1271
+ await verifyOwnerOnlyWindowsFileAcl(claimPath);
1272
+ }
1273
+ else {
1274
+ assertOwnerOnlyPosixMutationClaim(stats, "file");
1275
+ }
1276
+ const identity = { device: stats.dev, inode: stats.ino };
1277
+ const buffer = Buffer.alloc(Number(stats.size));
1278
+ let offset = 0;
1279
+ while (offset < buffer.length) {
1280
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
1281
+ if (bytesRead === 0) {
1282
+ throw new Error("Private path mutation claim metadata was truncated while reading.");
1283
+ }
1284
+ offset += bytesRead;
1285
+ }
1286
+ const finalStats = await handle.stat({ bigint: true });
1287
+ if (process.platform === "win32") {
1288
+ await verifyOwnerOnlyWindowsFileAcl(claimPath);
1289
+ }
1290
+ else {
1291
+ assertOwnerOnlyPosixMutationClaim(finalStats, "file");
1292
+ }
1293
+ if (!finalStats.isFile() ||
1294
+ finalStats.dev !== stats.dev ||
1295
+ finalStats.ino !== stats.ino ||
1296
+ finalStats.size !== stats.size) {
1297
+ throw new Error("Private path mutation claim metadata changed while reading.");
1298
+ }
1299
+ const contents = buffer.toString("utf8");
1300
+ const owner = parsePrivateMutationClaimOwner(contents);
1301
+ await assertClaimPathIdentity(claimPath, identity);
1302
+ return { contents, identity, owner };
1303
+ }
1304
+ catch (error) {
1305
+ if (error.code === "ENOENT") {
1306
+ return null;
1307
+ }
1308
+ throw error;
1309
+ }
1310
+ finally {
1311
+ await handle?.close();
1312
+ }
1313
+ }
1314
+ function nextPrivateMutationClaimPath(parentDirectory, ownerContents) {
1315
+ const digest = createHash("sha256").update(ownerContents).digest("hex");
1316
+ return path.join(parentDirectory, `.crabline-private-mutation.${digest}.claim`);
1317
+ }
1318
+ async function removeStalePrivateMutationClaimAliases(parentDirectory, staleClaims, currentClaimPath) {
1319
+ const staleIdentities = new Set(staleClaims.map(({ identity }) => `${identity.device}:${identity.inode}`));
1320
+ for (const entry of await fs.readdir(parentDirectory)) {
1321
+ if (!PRIVATE_MUTATION_RESERVED_BASENAME_PATTERN.test(entry)) {
1322
+ continue;
1323
+ }
1324
+ const entryPath = path.join(parentDirectory, entry);
1325
+ if (entryPath === currentClaimPath) {
1326
+ continue;
1327
+ }
1328
+ let stats;
1329
+ try {
1330
+ stats = await fs.lstat(entryPath, { bigint: true });
1331
+ }
1332
+ catch (error) {
1333
+ if (error.code === "ENOENT") {
1334
+ continue;
1335
+ }
1336
+ throw error;
1337
+ }
1338
+ if (stats.isFile() && staleIdentities.has(`${stats.dev}:${stats.ino}`)) {
1339
+ await fs.unlink(entryPath);
1340
+ }
1341
+ }
1342
+ }
1343
+ class UnsupportedPrivateMutationHardLinkError extends Error {
1344
+ constructor(cause) {
1345
+ super("Private mutation claims require a non-hard-link fallback.", { cause });
1346
+ }
1347
+ }
1348
+ function isUnsupportedHardLinkError(error) {
1349
+ const code = error.code;
1350
+ return code === "EPERM" || code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "ENOSYS";
1351
+ }
1352
+ async function acquireHardLinkPrivateMutationClaim(parent, options) {
1353
+ const platform = options.platform ?? process.platform;
1354
+ const runtime = options.runtime ?? defaultPrivateMutationClaimRuntime();
1355
+ if (!Number.isSafeInteger(runtime.pid) ||
1356
+ runtime.pid <= 0 ||
1357
+ !PRIVATE_MUTATION_CLAIM_OWNER_ID_PATTERN.test(runtime.ownerId) ||
1358
+ !Number.isSafeInteger(runtime.processStartedAtMs) ||
1359
+ runtime.processStartedAtMs <= 0 ||
1360
+ (runtime.processIdentity !== undefined &&
1361
+ (runtime.processIdentity.length === 0 || runtime.processIdentity.length > 256))) {
1362
+ throw new Error("Private path mutation claim runtime is invalid.");
1363
+ }
1364
+ if (!Number.isSafeInteger(runtime.pid) ||
1365
+ runtime.pid <= 0 ||
1366
+ !PRIVATE_MUTATION_CLAIM_OWNER_ID_PATTERN.test(runtime.ownerId) ||
1367
+ !Number.isSafeInteger(runtime.processStartedAtMs) ||
1368
+ runtime.processStartedAtMs <= 0 ||
1369
+ (runtime.processIdentity !== undefined &&
1370
+ (runtime.processIdentity.length === 0 || runtime.processIdentity.length > 256))) {
1371
+ throw new Error("Private path mutation claim runtime is invalid.");
1372
+ }
1373
+ const ownerContents = `${JSON.stringify({
1374
+ ownerId: runtime.ownerId,
1375
+ pid: runtime.pid,
1376
+ ...(runtime.processIdentity ? { processIdentity: runtime.processIdentity } : {}),
1377
+ processStartedAtMs: runtime.processStartedAtMs,
1378
+ })}\n`;
1379
+ const rootClaimPath = path.join(parent.directoryPath, PRIVATE_MUTATION_CLAIM_ROOT_FILE);
1380
+ const candidatePath = `${rootClaimPath}.${runtime.pid}.${randomUUID()}.candidate`;
1381
+ let handle;
1382
+ let identity;
1383
+ const linkedClaimPaths = new Set();
1384
+ try {
1385
+ if (platform === "win32" && process.platform === "win32") {
1386
+ const createdIdentity = await (options.createWindowsFile ?? createOwnerOnlyWindowsFile)(candidatePath);
1387
+ handle = await fs.open(candidatePath, "r+");
1388
+ identity = await readHandleIdentity(handle);
1389
+ assertSameFileIdentity(identity, createdIdentity);
1390
+ }
1391
+ else {
1392
+ handle = await fs.open(candidatePath, "wx+", 0o600);
1393
+ identity = await readHandleIdentity(handle);
1394
+ await handle.chmod(0o600);
1395
+ }
1396
+ await handle.writeFile(ownerContents, "utf8");
1397
+ await handle.sync();
1398
+ await assertClaimPathIdentity(candidatePath, identity, 1n);
1399
+ let claimPath = rootClaimPath;
1400
+ const visitedClaimPaths = new Set();
1401
+ const staleClaims = [];
1402
+ for (;;) {
1403
+ if (visitedClaimPaths.has(claimPath)) {
1404
+ throw new Error("Private path mutation claim chain contains a cycle.");
1405
+ }
1406
+ visitedClaimPaths.add(claimPath);
1407
+ try {
1408
+ await fs.link(candidatePath, claimPath);
1409
+ linkedClaimPaths.add(claimPath);
1410
+ }
1411
+ catch (error) {
1412
+ if (isUnsupportedHardLinkError(error)) {
1413
+ throw new UnsupportedPrivateMutationHardLinkError(error);
1414
+ }
1415
+ if (error.code !== "EEXIST") {
1416
+ throw error;
1417
+ }
1418
+ await parent.assertIdentityAt();
1419
+ const observed = await readPrivateMutationClaim(claimPath);
1420
+ if (observed === null) {
1421
+ claimPath = rootClaimPath;
1422
+ visitedClaimPaths.clear();
1423
+ staleClaims.length = 0;
1424
+ continue;
1425
+ }
1426
+ if (runtime.isProcessAlive(observed.owner.pid)) {
1427
+ if (observed.owner.pid === runtime.pid &&
1428
+ observed.owner.processStartedAtMs === runtime.processStartedAtMs) {
1429
+ throw new Error("Private path mutation is already claimed.", { cause: error });
1430
+ }
1431
+ else if (observed.owner.pid !== runtime.pid) {
1432
+ if (observed.owner.processIdentity === undefined) {
1433
+ throw new Error("Private path mutation is already claimed.", { cause: error });
1434
+ }
1435
+ const actualIdentity = runtime.getProcessIdentity(observed.owner.pid);
1436
+ if (actualIdentity === null || actualIdentity === observed.owner.processIdentity) {
1437
+ throw new Error("Private path mutation is already claimed.", { cause: error });
1438
+ }
1439
+ }
1440
+ }
1441
+ const revalidated = await readPrivateMutationClaim(claimPath);
1442
+ if (revalidated === null ||
1443
+ revalidated.identity.device !== observed.identity.device ||
1444
+ revalidated.identity.inode !== observed.identity.inode ||
1445
+ revalidated.contents !== observed.contents) {
1446
+ claimPath = rootClaimPath;
1447
+ visitedClaimPaths.clear();
1448
+ staleClaims.length = 0;
1449
+ continue;
1450
+ }
1451
+ const duplicateIdentity = staleClaims.find(({ identity: staleIdentity }) => staleIdentity.device === revalidated.identity.device &&
1452
+ staleIdentity.inode === revalidated.identity.inode);
1453
+ if (duplicateIdentity !== undefined) {
1454
+ await parent.assertIdentityAt();
1455
+ await assertClaimPathIdentity(claimPath, revalidated.identity);
1456
+ await fs.unlink(claimPath);
1457
+ await syncParentDirectory(claimPath, options.platform);
1458
+ claimPath = rootClaimPath;
1459
+ visitedClaimPaths.clear();
1460
+ staleClaims.length = 0;
1461
+ continue;
1462
+ }
1463
+ staleClaims.push({ claimPath, ...revalidated });
1464
+ claimPath = nextPrivateMutationClaimPath(parent.directoryPath, revalidated.contents);
1465
+ continue;
1466
+ }
1467
+ await assertClaimPathIdentity(candidatePath, identity, 2n);
1468
+ await assertClaimPathIdentity(claimPath, identity, 2n);
1469
+ if (staleClaims.length > 0) {
1470
+ const staleRoot = await readPrivateMutationClaim(rootClaimPath);
1471
+ const expectedRoot = staleClaims[0];
1472
+ if (staleRoot === null ||
1473
+ staleRoot.identity.device !== expectedRoot.identity.device ||
1474
+ staleRoot.identity.inode !== expectedRoot.identity.inode ||
1475
+ staleRoot.contents !== expectedRoot.contents) {
1476
+ throw new Error("Private path mutation claim root changed during recovery.");
1477
+ }
1478
+ await fs.rename(candidatePath, rootClaimPath);
1479
+ linkedClaimPaths.add(rootClaimPath);
1480
+ await assertClaimPathIdentity(rootClaimPath, identity, 2n);
1481
+ await assertClaimPathIdentity(claimPath, identity, 2n);
1482
+ await fs.unlink(claimPath);
1483
+ linkedClaimPaths.delete(claimPath);
1484
+ claimPath = rootClaimPath;
1485
+ await assertClaimPathIdentity(claimPath, identity, 1n);
1486
+ await removeStalePrivateMutationClaimAliases(parent.directoryPath, staleClaims, claimPath);
1487
+ }
1488
+ else {
1489
+ await fs.unlink(candidatePath);
1490
+ }
1491
+ await assertClaimPathIdentity(claimPath, identity, 1n);
1492
+ await parent.assertIdentityAt();
1493
+ await syncParentDirectory(claimPath, options.platform);
1494
+ linkedClaimPaths.clear();
1495
+ let released = false;
1496
+ const claimHandle = handle;
1497
+ let claimHandleOpen = true;
1498
+ let ownedClaimPath = claimPath;
1499
+ let ownedParentPath = parent.directoryPath;
1500
+ const claimIdentity = identity;
1501
+ const closeClaimHandle = async () => {
1502
+ if (!claimHandleOpen) {
1503
+ return;
1504
+ }
1505
+ claimHandleOpen = false;
1506
+ await claimHandle.close();
1507
+ };
1508
+ const assertOwned = async () => {
1509
+ await parent.assertIdentityAt(ownedParentPath);
1510
+ await assertClaimPathIdentity(ownedClaimPath, claimIdentity, 1n);
1511
+ const observed = await readPrivateMutationClaim(ownedClaimPath);
1512
+ if (observed === null ||
1513
+ observed.identity.device !== claimIdentity.device ||
1514
+ observed.identity.inode !== claimIdentity.inode ||
1515
+ observed.contents !== ownerContents) {
1516
+ throw new Error("Private path mutation claim owner metadata changed.");
1517
+ }
1518
+ };
1519
+ return {
1520
+ assertOwned,
1521
+ async completeContainerRemoval() {
1522
+ if (released) {
1523
+ return;
1524
+ }
1525
+ let completionError;
1526
+ try {
1527
+ await fs.lstat(ownedParentPath);
1528
+ completionError = new Error("Private directory path still exists after recursive removal.");
1529
+ }
1530
+ catch (error) {
1531
+ if (error.code !== "ENOENT") {
1532
+ completionError = error;
1533
+ }
1534
+ }
1535
+ try {
1536
+ await closeClaimHandle();
1537
+ }
1538
+ catch (closeError) {
1539
+ if (completionError !== undefined) {
1540
+ const aggregateError = new AggregateError([completionError, closeError], "Private directory removal verification failed and its claim handle could not be closed.");
1541
+ aggregateError.cause = completionError;
1542
+ throw aggregateError;
1543
+ }
1544
+ throw closeError;
1545
+ }
1546
+ if (completionError !== undefined) {
1547
+ throw completionError;
1548
+ }
1549
+ released = true;
1550
+ },
1551
+ async prepareContainerRemoval() {
1552
+ if (released) {
1553
+ throw new Error("Private path mutation claim has already been released.");
1554
+ }
1555
+ await assertOwned();
1556
+ await closeClaimHandle();
1557
+ await assertOwned();
1558
+ },
1559
+ async relocateParent(directoryPath) {
1560
+ if (released) {
1561
+ throw new Error("Private path mutation claim has already been released.");
1562
+ }
1563
+ const relocatedClaimPath = path.join(directoryPath, path.basename(ownedClaimPath));
1564
+ await parent.assertIdentityAt(directoryPath);
1565
+ const observed = await readPrivateMutationClaim(relocatedClaimPath);
1566
+ if (observed === null ||
1567
+ observed.identity.device !== claimIdentity.device ||
1568
+ observed.identity.inode !== claimIdentity.inode ||
1569
+ observed.contents !== ownerContents) {
1570
+ throw new Error("Private path mutation claim identity changed during relocation.");
1571
+ }
1572
+ ownedParentPath = directoryPath;
1573
+ ownedClaimPath = relocatedClaimPath;
1574
+ },
1575
+ async release() {
1576
+ if (released) {
1577
+ return;
1578
+ }
1579
+ const releasePath = `${ownedClaimPath}.${randomUUID()}.release`;
1580
+ let releaseError;
1581
+ try {
1582
+ await assertOwned();
1583
+ await fs.rename(ownedClaimPath, releasePath);
1584
+ await assertClaimPathIdentity(releasePath, claimIdentity, 1n);
1585
+ if ((await fs.readFile(releasePath, "utf8")) !== ownerContents) {
1586
+ throw new Error("Private path mutation claim owner metadata changed.");
1587
+ }
1588
+ await fs.unlink(releasePath);
1589
+ released = true;
1590
+ await parent.assertIdentityAt(ownedParentPath);
1591
+ await syncParentDirectory(ownedClaimPath, options.platform);
1592
+ }
1593
+ catch (error) {
1594
+ releaseError = error;
1595
+ }
1596
+ try {
1597
+ await closeClaimHandle();
1598
+ }
1599
+ catch (closeError) {
1600
+ if (releaseError !== undefined) {
1601
+ const aggregateError = new AggregateError([releaseError, closeError], "Private path mutation claim release failed and its handle could not be closed.");
1602
+ aggregateError.cause = releaseError;
1603
+ throw aggregateError;
1604
+ }
1605
+ throw closeError;
1606
+ }
1607
+ if (releaseError !== undefined) {
1608
+ throw releaseError;
1609
+ }
1610
+ },
1611
+ };
1612
+ }
1613
+ }
1614
+ catch (error) {
1615
+ const cleanupErrors = [];
1616
+ if (identity !== undefined) {
1617
+ for (const linkedClaimPath of linkedClaimPaths) {
1618
+ try {
1619
+ await assertClaimPathIdentity(linkedClaimPath, identity);
1620
+ await fs.unlink(linkedClaimPath);
1621
+ await syncParentDirectory(linkedClaimPath, options.platform);
1622
+ }
1623
+ catch (cleanupError) {
1624
+ cleanupErrors.push(cleanupError);
1625
+ }
1626
+ }
1627
+ }
1628
+ try {
1629
+ await handle?.close();
1630
+ }
1631
+ catch (closeError) {
1632
+ cleanupErrors.push(closeError);
1633
+ }
1634
+ try {
1635
+ await fs.rm(candidatePath, { force: true });
1636
+ }
1637
+ catch (cleanupError) {
1638
+ cleanupErrors.push(cleanupError);
1639
+ }
1640
+ if (cleanupErrors.length > 0) {
1641
+ const aggregateError = new AggregateError([error, ...cleanupErrors], "Private path mutation claim acquisition failed and rollback cleanup also failed.");
1642
+ aggregateError.cause = error;
1643
+ throw aggregateError;
1644
+ }
1645
+ throw error;
1646
+ }
1647
+ }
1648
+ const PRIVATE_MUTATION_DIRECTORY_OWNER_FILE = "owner.json";
1649
+ function samePrivateMutationDirectoryClaim(actual, expected) {
1650
+ return (actual.identity.device === expected.identity.device &&
1651
+ actual.identity.inode === expected.identity.inode &&
1652
+ actual.metadata.contents === expected.metadata.contents);
1653
+ }
1654
+ async function readPrivateMutationDirectoryClaim(claimPath) {
1655
+ let stats;
1656
+ try {
1657
+ stats = await fs.lstat(claimPath, { bigint: true });
1658
+ }
1659
+ catch (error) {
1660
+ if (error.code === "ENOENT") {
1661
+ return null;
1662
+ }
1663
+ throw error;
1664
+ }
1665
+ if (!stats.isDirectory() || stats.ino <= 0n) {
1666
+ throw new Error("Private mutation directory claim is malformed.");
1667
+ }
1668
+ if (process.platform === "win32") {
1669
+ await verifyOwnerOnlyWindowsDirectoryAcl(claimPath);
1670
+ }
1671
+ else {
1672
+ assertOwnerOnlyPosixMutationClaim(stats, "directory");
1673
+ }
1674
+ const metadata = await readPrivateMutationClaim(path.join(claimPath, PRIVATE_MUTATION_DIRECTORY_OWNER_FILE));
1675
+ if (metadata === null) {
1676
+ throw new Error("Private mutation directory claim is missing owner metadata.");
1677
+ }
1678
+ const finalStats = await fs.lstat(claimPath, { bigint: true });
1679
+ if (process.platform === "win32") {
1680
+ await verifyOwnerOnlyWindowsDirectoryAcl(claimPath);
1681
+ }
1682
+ else {
1683
+ assertOwnerOnlyPosixMutationClaim(finalStats, "directory");
1684
+ }
1685
+ if (!finalStats.isDirectory() || finalStats.dev !== stats.dev || finalStats.ino !== stats.ino) {
1686
+ throw new Error("Private mutation directory claim identity changed.");
1687
+ }
1688
+ return {
1689
+ identity: { device: stats.dev, inode: stats.ino },
1690
+ metadata,
1691
+ };
1692
+ }
1693
+ function privateMutationClaimOwnerIsActive(owner, runtime) {
1694
+ if (!runtime.isProcessAlive(owner.pid)) {
1695
+ return false;
1696
+ }
1697
+ if (owner.pid === runtime.pid && owner.processStartedAtMs === runtime.processStartedAtMs) {
1698
+ return true;
1699
+ }
1700
+ if (owner.pid === runtime.pid) {
1701
+ return false;
1702
+ }
1703
+ if (owner.processIdentity === undefined) {
1704
+ return true;
1705
+ }
1706
+ const actualIdentity = runtime.getProcessIdentity(owner.pid);
1707
+ return actualIdentity === null || actualIdentity === owner.processIdentity;
1708
+ }
1709
+ async function createPrivateMutationClaimDirectory(directoryPath, ownerContents, options) {
1710
+ if (options.platform === "win32" && process.platform === "win32") {
1711
+ const firstCreated = await createOwnerOnlyWindowsDirectoryAncestry(directoryPath);
1712
+ if (firstCreated === undefined) {
1713
+ throw new Error("Windows mutation claim directory creation did not create its path.");
1714
+ }
1715
+ }
1716
+ else {
1717
+ await fs.mkdir(directoryPath, { mode: 0o700 });
1718
+ await fs.chmod(directoryPath, 0o700);
1719
+ }
1720
+ const metadataPath = path.join(directoryPath, PRIVATE_MUTATION_DIRECTORY_OWNER_FILE);
1721
+ let handle;
1722
+ try {
1723
+ if (options.platform === "win32" && process.platform === "win32") {
1724
+ await (options.createWindowsFile ?? createOwnerOnlyWindowsFile)(metadataPath);
1725
+ handle = await fs.open(metadataPath, "r+");
1726
+ }
1727
+ else {
1728
+ handle = await fs.open(metadataPath, "wx+", 0o600);
1729
+ await handle.chmod(0o600);
1730
+ }
1731
+ await handle.writeFile(ownerContents, "utf8");
1732
+ await handle.sync();
1733
+ }
1734
+ finally {
1735
+ await handle?.close();
1736
+ }
1737
+ await syncParentDirectory(metadataPath, options.platform);
1738
+ }
1739
+ async function removeInstalledPrivateMutationDirectoryClaim(claimPath, expectedIdentity, ownerContents, rootClaimPath, platform) {
1740
+ const observed = await readPrivateMutationDirectoryClaim(claimPath);
1741
+ if (observed === null ||
1742
+ observed.identity.device !== expectedIdentity.device ||
1743
+ observed.identity.inode !== expectedIdentity.inode ||
1744
+ observed.metadata.contents !== ownerContents) {
1745
+ throw new Error("Private mutation directory claim changed before rollback.");
1746
+ }
1747
+ const rollbackPath = `${rootClaimPath}.${randomUUID()}.rollback-dir`;
1748
+ await fs.rename(claimPath, rollbackPath);
1749
+ const moved = await readPrivateMutationDirectoryClaim(rollbackPath);
1750
+ if (moved === null ||
1751
+ moved.identity.device !== expectedIdentity.device ||
1752
+ moved.identity.inode !== expectedIdentity.inode ||
1753
+ moved.metadata.contents !== ownerContents) {
1754
+ throw new Error("Private mutation directory claim changed during rollback.");
1755
+ }
1756
+ await fs.rm(rollbackPath, { force: true, recursive: true });
1757
+ await syncParentDirectory(claimPath, platform);
1758
+ }
1759
+ async function acquireDirectoryPrivateMutationClaim(parent, options) {
1760
+ const platform = options.platform ?? process.platform;
1761
+ const runtime = options.runtime ?? defaultPrivateMutationClaimRuntime();
1762
+ const ownerContents = `${JSON.stringify({
1763
+ ownerId: runtime.ownerId,
1764
+ pid: runtime.pid,
1765
+ ...(runtime.processIdentity ? { processIdentity: runtime.processIdentity } : {}),
1766
+ processStartedAtMs: runtime.processStartedAtMs,
1767
+ })}\n`;
1768
+ parsePrivateMutationClaimOwner(ownerContents);
1769
+ const rootClaimPath = path.join(parent.directoryPath, PRIVATE_MUTATION_CLAIM_ROOT_FILE);
1770
+ const candidatePath = `${rootClaimPath}.${runtime.pid}.${randomUUID()}.candidate-dir`;
1771
+ const createCandidate = async () => {
1772
+ await createPrivateMutationClaimDirectory(candidatePath, ownerContents, {
1773
+ ...(options.createWindowsFile ? { createWindowsFile: options.createWindowsFile } : {}),
1774
+ platform,
1775
+ });
1776
+ const candidate = await readPrivateMutationDirectoryClaim(candidatePath);
1777
+ if (candidate === null || candidate.metadata.contents !== ownerContents) {
1778
+ throw new Error("Private mutation directory claim candidate could not be verified.");
1779
+ }
1780
+ return candidate;
1781
+ };
1782
+ let candidate;
1783
+ let claimPath = rootClaimPath;
1784
+ let installedClaimPath;
1785
+ let expectedRootClaim;
1786
+ try {
1787
+ candidate = await createCandidate();
1788
+ for (;;) {
1789
+ try {
1790
+ await fs.rename(candidatePath, claimPath);
1791
+ installedClaimPath = claimPath;
1792
+ }
1793
+ catch (error) {
1794
+ const observed = await readPrivateMutationDirectoryClaim(claimPath);
1795
+ if (observed === null) {
1796
+ throw error;
1797
+ }
1798
+ if (privateMutationClaimOwnerIsActive(observed.metadata.owner, runtime)) {
1799
+ throw new Error("Private path mutation is already claimed.", { cause: error });
1800
+ }
1801
+ await parent.assertIdentityAt();
1802
+ const revalidated = await readPrivateMutationDirectoryClaim(claimPath);
1803
+ if (revalidated === null ||
1804
+ revalidated.identity.device !== observed.identity.device ||
1805
+ revalidated.identity.inode !== observed.identity.inode ||
1806
+ revalidated.metadata.contents !== observed.metadata.contents) {
1807
+ continue;
1808
+ }
1809
+ if (claimPath === rootClaimPath) {
1810
+ expectedRootClaim = revalidated;
1811
+ }
1812
+ else {
1813
+ const revalidatedRoot = await readPrivateMutationDirectoryClaim(rootClaimPath);
1814
+ if (expectedRootClaim === undefined ||
1815
+ revalidatedRoot === null ||
1816
+ !samePrivateMutationDirectoryClaim(revalidatedRoot, expectedRootClaim)) {
1817
+ claimPath = rootClaimPath;
1818
+ expectedRootClaim = undefined;
1819
+ continue;
1820
+ }
1821
+ }
1822
+ claimPath = path.join(claimPath, ".next");
1823
+ continue;
1824
+ }
1825
+ const claimed = await readPrivateMutationDirectoryClaim(claimPath);
1826
+ if (claimed === null ||
1827
+ claimed.identity.device !== candidate.identity.device ||
1828
+ claimed.identity.inode !== candidate.identity.inode ||
1829
+ claimed.metadata.contents !== ownerContents) {
1830
+ throw new Error("Private mutation directory claim ownership could not be verified.");
1831
+ }
1832
+ if (claimPath !== rootClaimPath) {
1833
+ const revalidatedRoot = await readPrivateMutationDirectoryClaim(rootClaimPath);
1834
+ if (expectedRootClaim === undefined ||
1835
+ revalidatedRoot === null ||
1836
+ !samePrivateMutationDirectoryClaim(revalidatedRoot, expectedRootClaim)) {
1837
+ await removeInstalledPrivateMutationDirectoryClaim(claimPath, candidate.identity, ownerContents, rootClaimPath, platform);
1838
+ installedClaimPath = undefined;
1839
+ candidate = await createCandidate();
1840
+ claimPath = rootClaimPath;
1841
+ expectedRootClaim = undefined;
1842
+ continue;
1843
+ }
1844
+ const terminalRelativePath = path.relative(rootClaimPath, claimPath);
1845
+ const staleTreePath = `${rootClaimPath}.${randomUUID()}.stale-dir`;
1846
+ await parent.assertIdentityAt();
1847
+ await fs.rename(rootClaimPath, staleTreePath);
1848
+ const relocatedClaimPath = path.join(staleTreePath, terminalRelativePath);
1849
+ installedClaimPath = relocatedClaimPath;
1850
+ const [movedRoot, movedClaim] = await Promise.all([
1851
+ readPrivateMutationDirectoryClaim(staleTreePath),
1852
+ readPrivateMutationDirectoryClaim(relocatedClaimPath),
1853
+ ]);
1854
+ if (movedRoot === null ||
1855
+ !samePrivateMutationDirectoryClaim(movedRoot, revalidatedRoot) ||
1856
+ movedClaim === null ||
1857
+ movedClaim.identity.device !== candidate.identity.device ||
1858
+ movedClaim.identity.inode !== candidate.identity.inode ||
1859
+ movedClaim.metadata.contents !== ownerContents) {
1860
+ const compactionError = new Error("Private mutation directory claim changed during compaction.");
1861
+ try {
1862
+ await fs.rename(staleTreePath, rootClaimPath);
1863
+ installedClaimPath = claimPath;
1864
+ }
1865
+ catch (rollbackError) {
1866
+ const aggregateError = new AggregateError([compactionError, rollbackError], "Private mutation directory claim compaction failed and its stale tree could not be restored.");
1867
+ aggregateError.cause = compactionError;
1868
+ throw aggregateError;
1869
+ }
1870
+ throw compactionError;
1871
+ }
1872
+ try {
1873
+ await fs.rename(relocatedClaimPath, rootClaimPath);
1874
+ }
1875
+ catch (error) {
1876
+ const code = error.code;
1877
+ if (code !== "EEXIST" && code !== "ENOTEMPTY") {
1878
+ try {
1879
+ await fs.rename(staleTreePath, rootClaimPath);
1880
+ installedClaimPath = claimPath;
1881
+ }
1882
+ catch (rollbackError) {
1883
+ const aggregateError = new AggregateError([error, rollbackError], "Private mutation directory claim compaction failed and its stale tree could not be restored.");
1884
+ aggregateError.cause = error;
1885
+ throw aggregateError;
1886
+ }
1887
+ throw error;
1888
+ }
1889
+ const retainedClaim = await readPrivateMutationDirectoryClaim(relocatedClaimPath);
1890
+ if (retainedClaim === null ||
1891
+ retainedClaim.identity.device !== candidate.identity.device ||
1892
+ retainedClaim.identity.inode !== candidate.identity.inode ||
1893
+ retainedClaim.metadata.contents !== ownerContents) {
1894
+ throw new Error("Private mutation directory claim changed during compaction.", {
1895
+ cause: error,
1896
+ });
1897
+ }
1898
+ await fs.rm(staleTreePath, { force: true, recursive: true });
1899
+ await syncParentDirectory(rootClaimPath, platform);
1900
+ installedClaimPath = undefined;
1901
+ candidate = await createCandidate();
1902
+ claimPath = rootClaimPath;
1903
+ expectedRootClaim = undefined;
1904
+ continue;
1905
+ }
1906
+ installedClaimPath = rootClaimPath;
1907
+ const promotedClaim = await readPrivateMutationDirectoryClaim(rootClaimPath);
1908
+ if (promotedClaim === null ||
1909
+ promotedClaim.identity.device !== candidate.identity.device ||
1910
+ promotedClaim.identity.inode !== candidate.identity.inode ||
1911
+ promotedClaim.metadata.contents !== ownerContents) {
1912
+ throw new Error("Private mutation directory claim changed during compaction.");
1913
+ }
1914
+ await fs.rm(staleTreePath, { force: true, recursive: true });
1915
+ await syncParentDirectory(rootClaimPath, platform);
1916
+ claimPath = rootClaimPath;
1917
+ expectedRootClaim = undefined;
1918
+ }
1919
+ break;
1920
+ }
1921
+ const [rootClaim, claimed] = await Promise.all([
1922
+ readPrivateMutationDirectoryClaim(rootClaimPath),
1923
+ readPrivateMutationDirectoryClaim(claimPath),
1924
+ ]);
1925
+ if (rootClaim === null) {
1926
+ throw new Error("Private mutation directory claim root disappeared.");
1927
+ }
1928
+ if (claimed === null ||
1929
+ claimed.identity.device !== candidate.identity.device ||
1930
+ claimed.identity.inode !== candidate.identity.inode ||
1931
+ claimed.metadata.contents !== ownerContents) {
1932
+ throw new Error("Private mutation directory claim ownership could not be verified.");
1933
+ }
1934
+ if (claimPath !== rootClaimPath &&
1935
+ (expectedRootClaim === undefined ||
1936
+ !samePrivateMutationDirectoryClaim(rootClaim, expectedRootClaim))) {
1937
+ throw new Error("Private mutation directory claim root changed during acquisition.");
1938
+ }
1939
+ const terminalRelativePath = path.relative(rootClaimPath, claimPath);
1940
+ await parent.assertIdentityAt();
1941
+ await syncParentDirectory(claimPath, platform);
1942
+ let released = false;
1943
+ let claimedParentPath = parent.directoryPath;
1944
+ let ownedRootClaimPath = rootClaimPath;
1945
+ let ownedClaimPath = claimPath;
1946
+ const assertOwned = async () => {
1947
+ await parent.assertIdentityAt(claimedParentPath);
1948
+ const [observedRoot, observed] = await Promise.all([
1949
+ readPrivateMutationDirectoryClaim(ownedRootClaimPath),
1950
+ readPrivateMutationDirectoryClaim(ownedClaimPath),
1951
+ ]);
1952
+ if (observedRoot === null ||
1953
+ !samePrivateMutationDirectoryClaim(observedRoot, rootClaim) ||
1954
+ observed === null ||
1955
+ observed.identity.device !== claimed.identity.device ||
1956
+ observed.identity.inode !== claimed.identity.inode ||
1957
+ observed.metadata.contents !== ownerContents) {
1958
+ throw new Error("Private mutation directory claim owner changed.");
1959
+ }
1960
+ };
1961
+ return {
1962
+ assertOwned,
1963
+ async completeContainerRemoval() {
1964
+ if (released) {
1965
+ return;
1966
+ }
1967
+ try {
1968
+ await fs.lstat(claimedParentPath);
1969
+ throw new Error("Private directory path still exists after recursive removal.");
1970
+ }
1971
+ catch (error) {
1972
+ if (error.code !== "ENOENT") {
1973
+ throw error;
1974
+ }
1975
+ }
1976
+ released = true;
1977
+ },
1978
+ async prepareContainerRemoval() {
1979
+ await assertOwned();
1980
+ },
1981
+ async relocateParent(directoryPath) {
1982
+ const relocatedRootClaimPath = path.join(directoryPath, path.basename(ownedRootClaimPath));
1983
+ const relocatedClaimPath = path.join(relocatedRootClaimPath, terminalRelativePath);
1984
+ await parent.assertIdentityAt(directoryPath);
1985
+ const [observedRoot, observed] = await Promise.all([
1986
+ readPrivateMutationDirectoryClaim(relocatedRootClaimPath),
1987
+ readPrivateMutationDirectoryClaim(relocatedClaimPath),
1988
+ ]);
1989
+ if (observedRoot === null ||
1990
+ !samePrivateMutationDirectoryClaim(observedRoot, rootClaim) ||
1991
+ observed === null ||
1992
+ observed.identity.device !== claimed.identity.device ||
1993
+ observed.identity.inode !== claimed.identity.inode ||
1994
+ observed.metadata.contents !== ownerContents) {
1995
+ throw new Error("Private mutation directory claim changed during relocation.");
1996
+ }
1997
+ claimedParentPath = directoryPath;
1998
+ ownedRootClaimPath = relocatedRootClaimPath;
1999
+ ownedClaimPath = relocatedClaimPath;
2000
+ },
2001
+ async release() {
2002
+ if (released) {
2003
+ return;
2004
+ }
2005
+ await assertOwned();
2006
+ const releasePath = `${ownedRootClaimPath}.${randomUUID()}.release-dir`;
2007
+ await fs.rename(ownedRootClaimPath, releasePath);
2008
+ const [movedRoot, moved] = await Promise.all([
2009
+ readPrivateMutationDirectoryClaim(releasePath),
2010
+ readPrivateMutationDirectoryClaim(path.join(releasePath, terminalRelativePath)),
2011
+ ]);
2012
+ if (movedRoot === null ||
2013
+ !samePrivateMutationDirectoryClaim(movedRoot, rootClaim) ||
2014
+ moved === null ||
2015
+ moved.identity.device !== claimed.identity.device ||
2016
+ moved.identity.inode !== claimed.identity.inode ||
2017
+ moved.metadata.contents !== ownerContents) {
2018
+ throw new Error("Private mutation directory claim changed during release.");
2019
+ }
2020
+ await fs.rm(releasePath, { force: true, recursive: true });
2021
+ released = true;
2022
+ await parent.assertIdentityAt(claimedParentPath);
2023
+ await syncParentDirectory(ownedRootClaimPath, platform);
2024
+ },
2025
+ };
2026
+ }
2027
+ catch (error) {
2028
+ const cleanupErrors = [];
2029
+ if (installedClaimPath !== undefined && candidate !== undefined) {
2030
+ try {
2031
+ await removeInstalledPrivateMutationDirectoryClaim(installedClaimPath, candidate.identity, ownerContents, rootClaimPath, platform);
2032
+ }
2033
+ catch (cleanupError) {
2034
+ cleanupErrors.push(cleanupError);
2035
+ }
2036
+ }
2037
+ if (cleanupErrors.length > 0) {
2038
+ const aggregateError = new AggregateError([error, ...cleanupErrors], "Private mutation directory claim acquisition failed and rollback cleanup also failed.");
2039
+ aggregateError.cause = error;
2040
+ throw aggregateError;
2041
+ }
2042
+ throw error;
2043
+ }
2044
+ finally {
2045
+ await fs.rm(candidatePath, { force: true, recursive: true });
2046
+ }
2047
+ }
2048
+ async function acquirePrivateMutationClaim(parent, options) {
2049
+ const rootClaimPath = path.join(parent.directoryPath, PRIVATE_MUTATION_CLAIM_ROOT_FILE);
2050
+ try {
2051
+ const stats = await fs.lstat(rootClaimPath);
2052
+ if (stats.isDirectory()) {
2053
+ return await acquireDirectoryPrivateMutationClaim(parent, options);
2054
+ }
2055
+ }
2056
+ catch (error) {
2057
+ if (error.code !== "ENOENT") {
2058
+ throw error;
2059
+ }
2060
+ }
2061
+ try {
2062
+ return await acquireHardLinkPrivateMutationClaim(parent, options);
2063
+ }
2064
+ catch (error) {
2065
+ let directoryClaimExists = false;
2066
+ try {
2067
+ directoryClaimExists = (await fs.lstat(rootClaimPath)).isDirectory();
2068
+ }
2069
+ catch { }
2070
+ if (!(error instanceof UnsupportedPrivateMutationHardLinkError) && !directoryClaimExists) {
2071
+ throw error;
2072
+ }
2073
+ return await acquireDirectoryPrivateMutationClaim(parent, options);
2074
+ }
2075
+ }
2076
+ async function captureOwnerOnlyPrivateClaimAncestor(directoryPath, platform) {
2077
+ if (platform === "win32" && process.platform === "win32") {
2078
+ try {
2079
+ await verifyOwnerOnlyWindowsDirectoryAcl(directoryPath);
2080
+ }
2081
+ catch {
2082
+ return null;
2083
+ }
2084
+ return {
2085
+ directory: await captureDirectoryIdentity(directoryPath),
2086
+ mutationRoot: false,
2087
+ };
2088
+ }
2089
+ const currentUserId = process.geteuid?.();
2090
+ if (currentUserId === undefined || !Number.isSafeInteger(currentUserId) || currentUserId < 0) {
2091
+ throw new Error("Could not resolve the current POSIX user for private mutation claims.");
2092
+ }
2093
+ const stats = await fs.lstat(directoryPath, { bigint: true });
2094
+ if (!stats.isDirectory()) {
2095
+ throw new Error("Private mutation claim ancestry contains a non-directory entry.");
2096
+ }
2097
+ if (stats.uid !== BigInt(currentUserId) ||
2098
+ (stats.mode & 18n) !== 0n ||
2099
+ (await darwinDirectoryHasExtendedAcl(directoryPath))) {
2100
+ return null;
2101
+ }
2102
+ return {
2103
+ directory: await captureDirectoryIdentity(directoryPath),
2104
+ mutationRoot: (stats.mode & 512n) !== 0n,
2105
+ };
2106
+ }
2107
+ async function ownerOnlyPrivateClaimAncestry(leaf, platform) {
2108
+ const ancestry = [leaf];
2109
+ let highestMutationRootIndex = platform !== "win32" &&
2110
+ process.platform !== "win32" &&
2111
+ ((await fs.lstat(leaf.directoryPath, { bigint: true })).mode & 512n) !== 0n
2112
+ ? 0
2113
+ : -1;
2114
+ let directoryPath = path.dirname(leaf.directoryPath);
2115
+ while (directoryPath !== path.dirname(directoryPath)) {
2116
+ const ancestor = await captureOwnerOnlyPrivateClaimAncestor(directoryPath, platform);
2117
+ if (ancestor === null) {
2118
+ break;
2119
+ }
2120
+ ancestry.push(ancestor.directory);
2121
+ if (ancestor.mutationRoot) {
2122
+ highestMutationRootIndex = ancestry.length - 1;
2123
+ }
2124
+ directoryPath = path.dirname(directoryPath);
2125
+ }
2126
+ if (platform !== "win32" && process.platform !== "win32") {
2127
+ return highestMutationRootIndex < 0 ? [leaf] : ancestry.slice(0, highestMutationRootIndex + 1);
2128
+ }
2129
+ return ancestry;
2130
+ }
2131
+ async function acquirePrivateMutationClaimChain(leaf, options) {
2132
+ const platform = options.platform ?? process.platform;
2133
+ const ancestry = await ownerOnlyPrivateClaimAncestry(leaf, platform);
2134
+ const highestClaimDirectory = ancestry.at(-1);
2135
+ const outerBoundary = await captureSafePrivateDirectoryMutationParent(highestClaimDirectory.directoryPath, platform);
2136
+ const claims = [];
2137
+ let leafClaim;
2138
+ try {
2139
+ for (const directory of ancestry) {
2140
+ const claim = await acquirePrivateMutationClaim(directory, options);
2141
+ claims.push(claim);
2142
+ if (directory === leaf) {
2143
+ leafClaim = claim;
2144
+ }
2145
+ }
2146
+ }
2147
+ catch (error) {
2148
+ const cleanupErrors = [];
2149
+ for (const claim of claims.reverse()) {
2150
+ try {
2151
+ await claim.release();
2152
+ }
2153
+ catch (cleanupError) {
2154
+ cleanupErrors.push(cleanupError);
2155
+ }
2156
+ }
2157
+ if (cleanupErrors.length > 0) {
2158
+ const aggregateError = new AggregateError([error, ...cleanupErrors], "Private mutation claim-chain acquisition failed and rollback cleanup also failed.");
2159
+ aggregateError.cause = error;
2160
+ throw aggregateError;
2161
+ }
2162
+ throw error;
2163
+ }
2164
+ let released = false;
2165
+ let leafRemoved = false;
2166
+ const releaseClaims = async (claimsToRelease) => {
2167
+ const releaseErrors = [];
2168
+ for (const claim of claimsToRelease) {
2169
+ try {
2170
+ await claim.release();
2171
+ }
2172
+ catch (error) {
2173
+ releaseErrors.push(error);
2174
+ }
2175
+ }
2176
+ if (releaseErrors.length === 1) {
2177
+ throw releaseErrors[0];
2178
+ }
2179
+ if (releaseErrors.length > 1) {
2180
+ throw new AggregateError(releaseErrors, "Private mutation claim-chain release failed.");
2181
+ }
2182
+ };
2183
+ return {
2184
+ async assertOwned() {
2185
+ await outerBoundary.assertIdentityAt();
2186
+ for (const claim of claims) {
2187
+ await claim.assertOwned();
2188
+ }
2189
+ },
2190
+ async completeContainerRemoval() {
2191
+ if (released) {
2192
+ return;
2193
+ }
2194
+ if (leafClaim === undefined) {
2195
+ throw new Error("Private directory removal did not acquire its leaf mutation claim.");
2196
+ }
2197
+ let primaryError;
2198
+ try {
2199
+ await leafClaim.completeContainerRemoval();
2200
+ leafRemoved = true;
2201
+ }
2202
+ catch (error) {
2203
+ primaryError = error;
2204
+ }
2205
+ try {
2206
+ await releaseClaims(claims.slice(1).reverse());
2207
+ }
2208
+ catch (releaseError) {
2209
+ if (primaryError !== undefined) {
2210
+ const aggregateError = new AggregateError([primaryError, releaseError], "Private directory removal completion and ancestor claim release both failed.");
2211
+ aggregateError.cause = primaryError;
2212
+ throw aggregateError;
2213
+ }
2214
+ throw releaseError;
2215
+ }
2216
+ if (primaryError !== undefined) {
2217
+ throw primaryError;
2218
+ }
2219
+ released = true;
2220
+ },
2221
+ async prepareContainerRemoval() {
2222
+ if (leafClaim === undefined) {
2223
+ throw new Error("Private directory removal did not acquire its leaf mutation claim.");
2224
+ }
2225
+ await leafClaim.prepareContainerRemoval();
2226
+ },
2227
+ async relocateParent(directoryPath) {
2228
+ if (leafClaim === undefined) {
2229
+ throw new Error("Private directory removal did not acquire its leaf mutation claim.");
2230
+ }
2231
+ await leafClaim.relocateParent(directoryPath);
2232
+ },
2233
+ async release() {
2234
+ if (released) {
2235
+ return;
2236
+ }
2237
+ await releaseClaims(leafRemoved ? claims.slice(1).reverse() : [...claims].reverse());
2238
+ released = true;
2239
+ },
2240
+ };
2241
+ }
2242
+ async function secureOwnerOnlyMutationParent(directoryPath) {
2243
+ return await securePrivateDirectory(directoryPath, {
2244
+ markMutationRoot: false,
2245
+ platform: process.platform,
2246
+ });
2247
+ }
398
2248
  export async function publishPrivateFileAtomically(filePath, contents, options = {}) {
2249
+ assertNotReservedPrivateMutationPath(filePath);
399
2250
  const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
400
2251
  const parentDirectory = path.resolve(path.dirname(filePath));
401
- const firstCreatedDirectory = await fs.mkdir(parentDirectory, { recursive: true });
2252
+ const platform = options.platform ?? process.platform;
2253
+ let parentExists = false;
2254
+ try {
2255
+ const parentStats = await fs.lstat(parentDirectory);
2256
+ if (!parentStats.isDirectory()) {
2257
+ throw new Error("Private publication parent is not a directory.");
2258
+ }
2259
+ parentExists = true;
2260
+ }
2261
+ catch (error) {
2262
+ if (error.code !== "ENOENT") {
2263
+ throw error;
2264
+ }
2265
+ }
2266
+ const creationBoundary = parentExists
2267
+ ? undefined
2268
+ : await captureSafePrivateMutationBoundary(await nearestExistingDirectory(parentDirectory), platform, true);
2269
+ const migrationBoundary = parentExists
2270
+ ? await capturePrivateMutationPathAncestry(parentDirectory, platform)
2271
+ : undefined;
2272
+ const firstCreatedDirectory = platform === "win32"
2273
+ ? parentExists
2274
+ ? undefined
2275
+ : await (options.createWindowsDirectories ?? createOwnerOnlyWindowsDirectoryAncestry)(parentDirectory)
2276
+ : await fs.mkdir(parentDirectory, { recursive: true, mode: 0o700 });
2277
+ await creationBoundary?.assertIdentityAt();
2278
+ await assertDarwinCreatedAncestryHasNoExtendedAcl(firstCreatedDirectory, parentDirectory);
2279
+ if (firstCreatedDirectory !== undefined && platform !== "win32") {
2280
+ await securePrivateDirectory(firstCreatedDirectory, {
2281
+ markMutationRoot: true,
2282
+ platform,
2283
+ });
2284
+ }
2285
+ await migrationBoundary?.assertIdentityAt();
2286
+ const parent = await secureOwnerOnlyMutationParent(parentDirectory);
2287
+ await migrationBoundary?.assertIdentityAt();
2288
+ const claim = await acquirePrivateMutationClaimChain(parent, {
2289
+ ...(options.claimRuntime ? { runtime: options.claimRuntime } : {}),
2290
+ ...(options.createWindowsFile ? { createWindowsFile: options.createWindowsFile } : {}),
2291
+ ...(options.platform ? { platform: options.platform } : {}),
2292
+ });
402
2293
  let handle;
2294
+ let identity;
403
2295
  let publicationFailed = false;
404
2296
  let primaryError;
405
2297
  try {
406
- handle = await fs.open(temporaryPath, "wx+", 0o600);
407
- const identity = await readHandleIdentity(handle);
408
- if ((options.platform ?? process.platform) === "win32") {
409
- await (options.secureWindowsFile ?? applyOwnerOnlyWindowsAcl)(temporaryPath);
2298
+ if (platform === "win32" && options.secureWindowsFile === undefined) {
2299
+ const createdIdentity = await (options.createWindowsFile ?? createOwnerOnlyWindowsFile)(temporaryPath);
2300
+ identity = createdIdentity;
2301
+ handle = await fs.open(temporaryPath, "r+");
2302
+ const openedIdentity = await readHandleIdentity(handle);
2303
+ assertSameFileIdentity(openedIdentity, createdIdentity);
410
2304
  }
411
2305
  else {
412
- await handle.chmod(0o600);
2306
+ handle = await fs.open(temporaryPath, "wx+", 0o600);
2307
+ identity = await readHandleIdentity(handle);
2308
+ if (platform === "win32") {
2309
+ await options.secureWindowsFile(temporaryPath);
2310
+ }
2311
+ else {
2312
+ await handle.chmod(0o600);
2313
+ }
413
2314
  }
414
2315
  await assertPathIdentity(temporaryPath, identity);
415
2316
  await handle.writeFile(contents, "utf8");
416
2317
  await handle.sync();
417
2318
  await assertPathIdentity(temporaryPath, identity);
418
2319
  await options.beforeRename?.(temporaryPath);
2320
+ await parent.assertIdentityAt();
2321
+ await claim.assertOwned();
2322
+ await assertPathIdentity(temporaryPath, identity);
2323
+ await options.beforeCommitRename?.(temporaryPath);
2324
+ await parent.assertIdentityAt();
2325
+ await claim.assertOwned();
419
2326
  await assertPathIdentity(temporaryPath, identity);
2327
+ // Node has no portable renameat API. The owner-only parent and parent-wide claim chain fence
2328
+ // every supported same-user writer while these path identities are revalidated.
420
2329
  await fs.rename(temporaryPath, filePath);
2330
+ await parent.assertIdentityAt();
2331
+ await claim.assertOwned();
421
2332
  await syncPathAncestry(filePath, options.syncParent ?? syncParentDirectory, options.platform, firstCreatedDirectory);
422
2333
  await options.afterRename?.(filePath);
2334
+ await parent.assertIdentityAt();
2335
+ await claim.assertOwned();
423
2336
  await assertPathIdentity(filePath, identity);
424
2337
  }
425
2338
  catch (error) {
@@ -434,8 +2347,16 @@ export async function publishPrivateFileAtomically(filePath, contents, options =
434
2347
  cleanupErrors.push(error);
435
2348
  }
436
2349
  try {
437
- await (options.removeTemporaryFile ??
438
- ((candidatePath) => fs.rm(candidatePath, { force: true })))(temporaryPath);
2350
+ if (identity !== undefined && (await pathHasFileIdentity(temporaryPath, identity))) {
2351
+ await (options.removeTemporaryFile ??
2352
+ ((candidatePath) => fs.rm(candidatePath, { force: true })))(temporaryPath);
2353
+ }
2354
+ }
2355
+ catch (error) {
2356
+ cleanupErrors.push(error);
2357
+ }
2358
+ try {
2359
+ await claim.release();
439
2360
  }
440
2361
  catch (error) {
441
2362
  cleanupErrors.push(error);