@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.
- package/README.md +137 -23
- package/dist/src/bin/crabline.js +3 -1
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +5 -1
- package/dist/src/cli/program.js +281 -52
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +39 -3
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +20 -0
- package/dist/src/config/schema.js +163 -35
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.d.ts +1 -0
- package/dist/src/core/errors.js +6 -3
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/http-path.d.ts +1 -0
- package/dist/src/core/http-path.js +14 -0
- package/dist/src/core/http-path.js.map +1 -0
- package/dist/src/core/matcher.d.ts +3 -1
- package/dist/src/core/matcher.js +58 -2
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/nonces.d.ts +2 -0
- package/dist/src/core/nonces.js +11 -1
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +5 -0
- package/dist/src/core/reporters.js +26 -2
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -1
- package/dist/src/core/run.js +178 -66
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/index.d.ts +12 -10
- package/dist/src/index.js +6 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/matrix-ids.d.ts +5 -0
- package/dist/src/matrix-ids.js +83 -0
- package/dist/src/matrix-ids.js.map +1 -0
- package/dist/src/openclaw/artifact-generation.d.ts +7 -6
- package/dist/src/openclaw/artifact-generation.js +385 -41
- package/dist/src/openclaw/artifact-generation.js.map +1 -1
- package/dist/src/openclaw/bridges/discord.d.ts +1 -0
- package/dist/src/openclaw/bridges/discord.js +149 -0
- package/dist/src/openclaw/bridges/discord.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +22 -5
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +14 -9
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
- package/dist/src/openclaw/bridges/probe-response.js +5 -0
- package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
- package/dist/src/openclaw/bridges/signal.js +16 -7
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +117 -24
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +46 -20
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +33 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +9 -4
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.js +3 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -1
- package/dist/src/openclaw/private-file.d.ts +38 -3
- package/dist/src/openclaw/private-file.js +2008 -87
- package/dist/src/openclaw/private-file.js.map +1 -1
- package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
- package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
- package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +10 -17
- package/dist/src/openclaw/shared.js +53 -19
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw.d.ts +3 -5
- package/dist/src/openclaw.js +245 -60
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/platform/process-owned-lock.d.ts +11 -0
- package/dist/src/platform/process-owned-lock.js +1698 -0
- package/dist/src/platform/process-owned-lock.js.map +1 -0
- package/dist/src/platform/recorder-directory.d.ts +1 -0
- package/dist/src/platform/recorder-directory.js +14 -0
- package/dist/src/platform/recorder-directory.js.map +1 -0
- package/dist/src/platform/windows-acl.d.ts +18 -0
- package/dist/src/platform/windows-acl.js +1367 -0
- package/dist/src/platform/windows-acl.js.map +1 -0
- package/dist/src/platform/windows-lock-root.d.ts +13 -0
- package/dist/src/platform/windows-lock-root.js +98 -0
- package/dist/src/platform/windows-lock-root.js.map +1 -0
- package/dist/src/providers/builtin/discord.d.ts +4 -0
- package/dist/src/providers/builtin/discord.js +52 -9
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
- package/dist/src/providers/builtin/feishu.d.ts +1 -0
- package/dist/src/providers/builtin/feishu.js +96 -26
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +1 -0
- package/dist/src/providers/builtin/googlechat.js +95 -26
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.js +60 -17
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +88 -35
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +2 -2
- package/dist/src/providers/builtin/matrix.js +78 -21
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +11 -7
- package/dist/src/providers/builtin/mattermost.js +138 -24
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +1 -0
- package/dist/src/providers/builtin/msteams.js +118 -38
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.js +4 -4
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.js +1057 -146
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +8 -1
- package/dist/src/providers/builtin/slack.js +327 -24
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +1 -1
- package/dist/src/providers/builtin/telegram.js +55 -14
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
- package/dist/src/providers/builtin/whatsapp.js +157 -29
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.d.ts +5 -2
- package/dist/src/providers/builtin/zalo.js +31 -7
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +1 -1
- package/dist/src/providers/catalog.js +1 -1
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +4 -0
- package/dist/src/providers/local-mock.js +162 -35
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +2 -0
- package/dist/src/providers/native-ids.js +3 -0
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +14 -3
- package/dist/src/providers/recorder.js +521 -41
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +1 -0
- package/dist/src/providers/registry.js +65 -20
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +6 -1
- package/dist/src/providers/signed-jwt.js +241 -50
- package/dist/src/providers/signed-jwt.js.map +1 -1
- package/dist/src/providers/slack-ids.d.ts +1 -0
- package/dist/src/providers/slack-ids.js +5 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +9 -1
- package/dist/src/providers/target-normalizers.js +177 -58
- package/dist/src/providers/target-normalizers.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +3 -0
- package/dist/src/providers/webhook-server.js +268 -47
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/discord.d.ts +40 -0
- package/dist/src/servers/discord.js +842 -0
- package/dist/src/servers/discord.js.map +1 -0
- package/dist/src/servers/http.d.ts +14 -2
- package/dist/src/servers/http.js +229 -58
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/index.d.ts +10 -4
- package/dist/src/servers/index.js +4 -0
- package/dist/src/servers/index.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +3 -0
- package/dist/src/servers/matrix.js +268 -110
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +389 -91
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/recorder.d.ts +11 -0
- package/dist/src/servers/recorder.js +1072 -69
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +1 -0
- package/dist/src/servers/signal.js +279 -44
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +15 -0
- package/dist/src/servers/slack.js +270 -54
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram-identity.d.ts +6 -0
- package/dist/src/servers/telegram-identity.js +27 -0
- package/dist/src/servers/telegram-identity.js.map +1 -0
- package/dist/src/servers/telegram.d.ts +17 -6
- package/dist/src/servers/telegram.js +1218 -107
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +12 -4
- package/dist/src/servers/webhook-target.js +226 -28
- package/dist/src/servers/webhook-target.js.map +1 -1
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +1 -0
- package/dist/src/servers/whatsapp-jid.js +9 -5
- package/dist/src/servers/whatsapp-jid.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
- package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
- package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js +3 -3
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +7 -1
- package/dist/src/servers/whatsapp.js +149 -25
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +1 -0
- package/dist/src/servers/zalo.js +193 -82
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +251 -64
- package/fixtures/examples/crabline.example.yaml +3 -2
- package/package.json +18 -10
- package/dist/src/openclaw/smoke-lock.js +0 -1004
- package/dist/src/openclaw/smoke-lock.js.map +0 -1
- package/fixtures/examples/openclaw-bridge.yaml +0 -125
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windows-acl.js","sourceRoot":"","sources":["../../../src/platform/windows-acl.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFile } from \"node:child_process\";\nimport path from \"node:path\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\nconst WINDOWS_ACL_COMMAND_TIMEOUT_MS = 15_000;\nconst WINDOWS_DIRECTORY_HANDLE_HELPER = String.raw`\nAdd-Type -TypeDefinition @\"\nusing System;\nusing System.ComponentModel;\nusing System.IO;\nusing System.Runtime.InteropServices;\nusing Microsoft.Win32.SafeHandles;\n\npublic static class CrablineWindowsDirectoryHandle\n{\n private const uint FileReadAttributes = 0x00000080;\n private const uint FileTraverse = 0x00000020;\n private const uint Delete = 0x00010000;\n private const uint ReadControl = 0x00020000;\n private const uint WriteDac = 0x00040000;\n private const uint WriteOwner = 0x00080000;\n private const uint ShareRead = 0x00000001;\n private const uint ShareWrite = 0x00000002;\n private const uint Synchronize = 0x00100000;\n private const uint OpenExisting = 3;\n private const uint FileFlagBackupSemantics = 0x02000000;\n private const uint FileFlagOpenReparsePoint = 0x00200000;\n private const uint FileOpen = 1;\n private const uint FileCreate = 2;\n private const uint FileDirectoryFile = 0x00000001;\n private const uint FileSynchronousIoNonAlert = 0x00000020;\n private const uint FileOpenReparsePoint = 0x00200000;\n private const uint ObjectCaseInsensitive = 0x00000040;\n private const uint FileAttributeNormal = 0x00000080;\n private const uint DirectoryAttribute = 0x00000010;\n private const uint ReparsePointAttribute = 0x00000400;\n private const int FileIdInfoClass = 18;\n private const int FileDispositionInfoClass = 4;\n private const uint OwnerSecurityInformation = 0x00000001;\n private const uint DaclSecurityInformation = 0x00000004;\n private const uint ProtectedDaclSecurityInformation = 0x80000000;\n private const int SeFileObject = 1;\n\n [StructLayout(LayoutKind.Sequential)]\n private struct FileTime\n {\n public uint Low;\n public uint High;\n }\n\n [StructLayout(LayoutKind.Sequential)]\n private struct ByHandleFileInformation\n {\n public uint FileAttributes;\n public FileTime CreationTime;\n public FileTime LastAccessTime;\n public FileTime LastWriteTime;\n public uint VolumeSerialNumber;\n public uint FileSizeHigh;\n public uint FileSizeLow;\n public uint NumberOfLinks;\n public uint FileIndexHigh;\n public uint FileIndexLow;\n }\n\n [StructLayout(LayoutKind.Sequential)]\n private struct FileId128\n {\n [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]\n public byte[] Identifier;\n }\n\n [StructLayout(LayoutKind.Sequential)]\n private struct FileIdInfo\n {\n public ulong VolumeSerialNumber;\n public FileId128 FileId;\n }\n\n [StructLayout(LayoutKind.Sequential)]\n private struct FileDispositionInfo\n {\n [MarshalAs(UnmanagedType.Bool)]\n public bool DeleteFile;\n }\n\n [StructLayout(LayoutKind.Sequential)]\n private struct IoStatusBlock\n {\n public IntPtr Status;\n public UIntPtr Information;\n }\n\n [StructLayout(LayoutKind.Sequential)]\n private struct UnicodeString\n {\n public ushort Length;\n public ushort MaximumLength;\n public IntPtr Buffer;\n }\n\n [StructLayout(LayoutKind.Sequential)]\n private struct ObjectAttributes\n {\n public int Length;\n public IntPtr RootDirectory;\n public IntPtr ObjectName;\n public uint Attributes;\n public IntPtr SecurityDescriptor;\n public IntPtr SecurityQualityOfService;\n }\n\n [DllImport(\n \"kernel32.dll\",\n CharSet = CharSet.Unicode,\n SetLastError = true\n )]\n private static extern SafeFileHandle CreateFile(\n string fileName,\n uint desiredAccess,\n uint shareMode,\n IntPtr securityAttributes,\n uint creationDisposition,\n uint flagsAndAttributes,\n IntPtr templateFile\n );\n\n [DllImport(\"ntdll.dll\")]\n private static extern int NtCreateFile(\n out SafeFileHandle file,\n uint desiredAccess,\n ref ObjectAttributes objectAttributes,\n out IoStatusBlock ioStatus,\n IntPtr allocationSize,\n uint fileAttributes,\n uint shareAccess,\n uint createDisposition,\n uint createOptions,\n IntPtr eaBuffer,\n uint eaLength\n );\n\n [DllImport(\"ntdll.dll\")]\n private static extern uint RtlNtStatusToDosError(int status);\n\n [DllImport(\"kernel32.dll\", SetLastError = true)]\n [return: MarshalAs(UnmanagedType.Bool)]\n private static extern bool GetFileInformationByHandle(\n SafeFileHandle file,\n out ByHandleFileInformation information\n );\n\n [DllImport(\"kernel32.dll\", SetLastError = true)]\n [return: MarshalAs(UnmanagedType.Bool)]\n private static extern bool GetFileInformationByHandleEx(\n SafeFileHandle file,\n int informationClass,\n out FileIdInfo information,\n uint bufferSize\n );\n\n [DllImport(\"kernel32.dll\", SetLastError = true)]\n [return: MarshalAs(UnmanagedType.Bool)]\n private static extern bool SetFileInformationByHandle(\n SafeFileHandle file,\n int informationClass,\n ref FileDispositionInfo information,\n uint bufferSize\n );\n\n [DllImport(\"advapi32.dll\")]\n private static extern uint GetSecurityInfo(\n SafeFileHandle file,\n int objectType,\n uint securityInformation,\n out IntPtr owner,\n out IntPtr group,\n out IntPtr dacl,\n out IntPtr sacl,\n out IntPtr securityDescriptor\n );\n\n [DllImport(\"advapi32.dll\")]\n private static extern uint SetSecurityInfo(\n SafeFileHandle file,\n int objectType,\n uint securityInformation,\n IntPtr owner,\n IntPtr group,\n IntPtr dacl,\n IntPtr sacl\n );\n\n [DllImport(\"advapi32.dll\", SetLastError = true)]\n [return: MarshalAs(UnmanagedType.Bool)]\n private static extern bool GetSecurityDescriptorOwner(\n IntPtr securityDescriptor,\n out IntPtr owner,\n [MarshalAs(UnmanagedType.Bool)] out bool ownerDefaulted\n );\n\n [DllImport(\"advapi32.dll\", SetLastError = true)]\n [return: MarshalAs(UnmanagedType.Bool)]\n private static extern bool GetSecurityDescriptorDacl(\n IntPtr securityDescriptor,\n [MarshalAs(UnmanagedType.Bool)] out bool daclPresent,\n out IntPtr dacl,\n [MarshalAs(UnmanagedType.Bool)] out bool daclDefaulted\n );\n\n [DllImport(\"advapi32.dll\")]\n private static extern uint GetSecurityDescriptorLength(\n IntPtr securityDescriptor\n );\n\n [DllImport(\"kernel32.dll\")]\n private static extern IntPtr LocalFree(\n IntPtr memory\n );\n\n private static uint AccessMask(\n bool writeDacl,\n bool writeOwner,\n bool traverse\n )\n {\n uint access = FileReadAttributes | ReadControl;\n if (traverse) {\n access |= FileTraverse;\n }\n if (writeDacl) {\n access |= WriteDac;\n }\n if (writeOwner) {\n access |= WriteOwner;\n }\n return access;\n }\n\n public static SafeFileHandle Open(\n string path,\n bool writeDacl,\n bool writeOwner,\n bool traverse\n )\n {\n SafeFileHandle file = CreateFile(\n path,\n AccessMask(writeDacl, writeOwner, traverse),\n ShareRead | ShareWrite,\n IntPtr.Zero,\n OpenExisting,\n FileFlagBackupSemantics | FileFlagOpenReparsePoint,\n IntPtr.Zero\n );\n if (file.IsInvalid) {\n throw new Win32Exception(Marshal.GetLastWin32Error());\n }\n try {\n ReadIdentity(file);\n return file;\n } catch {\n file.Dispose();\n throw;\n }\n }\n\n private static SafeFileHandle OpenRelativeCore(\n SafeFileHandle parent,\n string name,\n bool writeDacl,\n bool writeOwner,\n bool traverse,\n uint createDisposition,\n byte[] securityDescriptor,\n bool deleteAccess\n )\n {\n if (\n String.IsNullOrEmpty(name) ||\n name == \".\" ||\n name == \"..\" ||\n name.IndexOf(Path.DirectorySeparatorChar) >= 0 ||\n name.IndexOf(Path.AltDirectorySeparatorChar) >= 0\n ) {\n throw new ArgumentException(\n \"Private directory child name must be one path component.\",\n \"name\"\n );\n }\n int pathBytes = name.Length * 2;\n if (pathBytes > UInt16.MaxValue - 2) {\n throw new PathTooLongException();\n }\n\n GCHandle descriptorHandle = default(GCHandle);\n IntPtr pathBuffer = IntPtr.Zero;\n IntPtr objectNameBuffer = IntPtr.Zero;\n bool parentAddRef = false;\n try {\n if (securityDescriptor != null) {\n descriptorHandle = GCHandle.Alloc(\n securityDescriptor,\n GCHandleType.Pinned\n );\n }\n parent.DangerousAddRef(ref parentAddRef);\n pathBuffer = Marshal.StringToHGlobalUni(name);\n UnicodeString objectName = new UnicodeString {\n Length = (ushort)pathBytes,\n MaximumLength = (ushort)(pathBytes + 2),\n Buffer = pathBuffer\n };\n objectNameBuffer = Marshal.AllocHGlobal(\n Marshal.SizeOf(typeof(UnicodeString))\n );\n Marshal.StructureToPtr(objectName, objectNameBuffer, false);\n ObjectAttributes attributes = new ObjectAttributes {\n Length = Marshal.SizeOf(typeof(ObjectAttributes)),\n RootDirectory = parent.DangerousGetHandle(),\n ObjectName = objectNameBuffer,\n Attributes = ObjectCaseInsensitive,\n SecurityDescriptor = securityDescriptor == null\n ? IntPtr.Zero\n : descriptorHandle.AddrOfPinnedObject(),\n SecurityQualityOfService = IntPtr.Zero\n };\n IoStatusBlock ioStatus;\n SafeFileHandle file;\n int status = NtCreateFile(\n out file,\n AccessMask(writeDacl, writeOwner, traverse) |\n (deleteAccess ? Delete : 0) |\n Synchronize,\n ref attributes,\n out ioStatus,\n IntPtr.Zero,\n FileAttributeNormal,\n ShareRead | ShareWrite,\n createDisposition,\n FileDirectoryFile |\n FileSynchronousIoNonAlert |\n FileOpenReparsePoint,\n IntPtr.Zero,\n 0\n );\n if (status < 0) {\n throw new Win32Exception(\n unchecked((int)RtlNtStatusToDosError(status))\n );\n }\n if (file == null || file.IsInvalid) {\n throw new InvalidOperationException(\n \"Windows did not return the directory handle.\"\n );\n }\n try {\n ReadIdentity(file);\n return file;\n } catch {\n file.Dispose();\n throw;\n }\n } finally {\n if (objectNameBuffer != IntPtr.Zero) {\n Marshal.FreeHGlobal(objectNameBuffer);\n }\n if (pathBuffer != IntPtr.Zero) {\n Marshal.FreeHGlobal(pathBuffer);\n }\n if (parentAddRef) {\n parent.DangerousRelease();\n }\n if (descriptorHandle.IsAllocated) {\n descriptorHandle.Free();\n }\n }\n }\n\n public static SafeFileHandle CreateRelative(\n SafeFileHandle parent,\n string name,\n byte[] securityDescriptor\n )\n {\n return OpenRelativeCore(\n parent,\n name,\n true,\n false,\n true,\n FileCreate,\n securityDescriptor,\n true\n );\n }\n\n public static SafeFileHandle OpenRelative(\n SafeFileHandle parent,\n string name,\n bool writeDacl,\n bool writeOwner,\n bool traverse\n )\n {\n return OpenRelativeCore(\n parent,\n name,\n writeDacl,\n writeOwner,\n traverse,\n FileOpen,\n null,\n false\n );\n }\n\n public static void MarkDelete(SafeFileHandle file)\n {\n FileDispositionInfo disposition = new FileDispositionInfo {\n DeleteFile = true\n };\n if (!SetFileInformationByHandle(\n file,\n FileDispositionInfoClass,\n ref disposition,\n (uint)Marshal.SizeOf(typeof(FileDispositionInfo))\n )) {\n throw new Win32Exception(Marshal.GetLastWin32Error());\n }\n }\n\n public static string ReadIdentity(SafeFileHandle file)\n {\n ByHandleFileInformation information;\n if (!GetFileInformationByHandle(file, out information)) {\n throw new Win32Exception(Marshal.GetLastWin32Error());\n }\n if ((information.FileAttributes & ReparsePointAttribute) != 0) {\n throw new InvalidOperationException(\n \"Private directory must not be a reparse point.\"\n );\n }\n if ((information.FileAttributes & DirectoryAttribute) == 0) {\n throw new InvalidOperationException(\n \"Private directory handle does not reference a directory.\"\n );\n }\n FileIdInfo identity;\n uint identitySize = (uint)Marshal.SizeOf(typeof(FileIdInfo));\n if (!GetFileInformationByHandleEx(\n file,\n FileIdInfoClass,\n out identity,\n identitySize\n )) {\n throw new Win32Exception(Marshal.GetLastWin32Error());\n }\n byte[] identifier = identity.FileId.Identifier;\n bool hasIdentity = identifier != null && identifier.Length == 16;\n if (hasIdentity) {\n hasIdentity = false;\n foreach (byte value in identifier) {\n if (value != 0) {\n hasIdentity = true;\n break;\n }\n }\n }\n if (!hasIdentity) {\n throw new InvalidOperationException(\n \"Windows did not return a stable directory identity.\"\n );\n }\n return identity.VolumeSerialNumber.ToString(\n System.Globalization.CultureInfo.InvariantCulture\n ) + \":\" + BitConverter.ToString(identifier).Replace(\"-\", String.Empty);\n }\n\n public static byte[] ReadSecurityDescriptor(SafeFileHandle file)\n {\n IntPtr owner;\n IntPtr group;\n IntPtr dacl;\n IntPtr sacl;\n IntPtr securityDescriptor;\n uint error = GetSecurityInfo(\n file,\n SeFileObject,\n OwnerSecurityInformation | DaclSecurityInformation,\n out owner,\n out group,\n out dacl,\n out sacl,\n out securityDescriptor\n );\n if (error != 0) {\n throw new Win32Exception((int)error);\n }\n try {\n uint length = GetSecurityDescriptorLength(securityDescriptor);\n if (length == 0 || length > Int32.MaxValue) {\n throw new InvalidOperationException(\n \"Windows returned an invalid directory security descriptor.\"\n );\n }\n byte[] descriptor = new byte[(int)length];\n Marshal.Copy(securityDescriptor, descriptor, 0, descriptor.Length);\n return descriptor;\n } finally {\n LocalFree(securityDescriptor);\n }\n }\n\n public static void WriteSecurityDescriptor(\n SafeFileHandle file,\n byte[] securityDescriptor,\n bool writeOwner\n )\n {\n GCHandle descriptorHandle = GCHandle.Alloc(\n securityDescriptor,\n GCHandleType.Pinned\n );\n try {\n IntPtr descriptor = descriptorHandle.AddrOfPinnedObject();\n IntPtr owner = IntPtr.Zero;\n if (writeOwner) {\n bool ownerDefaulted;\n if (!GetSecurityDescriptorOwner(\n descriptor,\n out owner,\n out ownerDefaulted\n )) {\n throw new Win32Exception(Marshal.GetLastWin32Error());\n }\n }\n bool daclPresent;\n IntPtr dacl;\n bool daclDefaulted;\n if (!GetSecurityDescriptorDacl(\n descriptor,\n out daclPresent,\n out dacl,\n out daclDefaulted\n )) {\n throw new Win32Exception(Marshal.GetLastWin32Error());\n }\n if (!daclPresent || dacl == IntPtr.Zero) {\n throw new InvalidOperationException(\n \"Private directory security descriptor has no usable DACL.\"\n );\n }\n uint securityInformation =\n DaclSecurityInformation |\n ProtectedDaclSecurityInformation;\n if (writeOwner) {\n securityInformation |= OwnerSecurityInformation;\n }\n uint error = SetSecurityInfo(\n file,\n SeFileObject,\n securityInformation,\n owner,\n IntPtr.Zero,\n dacl,\n IntPtr.Zero\n );\n if (error != 0) {\n throw new Win32Exception((int)error);\n }\n } finally {\n descriptorHandle.Free();\n }\n }\n\n public static void AssertSamePathIdentity(string path, string expected)\n {\n if (!String.Equals(\n ReadPathIdentity(path),\n expected,\n StringComparison.Ordinal\n )) {\n throw new InvalidOperationException(\n \"Private directory identity changed during ACL mutation.\"\n );\n }\n }\n\n public static string ReadPathIdentity(string path)\n {\n using (SafeFileHandle current = Open(path, false, false, false)) {\n return ReadIdentity(current);\n }\n }\n}\n\"@\n`;\n\nconst WINDOWS_OWNER_ONLY_DIRECTORY_ACL_SCRIPT = String.raw`\n$ErrorActionPreference = \"Stop\"\n$directoryPath = $env:CRABLINE_PRIVATE_DIRECTORY_PATH\nif ([string]::IsNullOrWhiteSpace($directoryPath)) {\n throw \"CRABLINE_PRIVATE_DIRECTORY_PATH is required.\"\n}\n$directoryPath = [System.IO.Path]::GetFullPath($directoryPath)\n$rootPath = [System.IO.Path]::GetPathRoot($directoryPath)\nif ($directoryPath.Length -gt $rootPath.Length) {\n $directoryPath = $directoryPath.TrimEnd(\n [char[]]@(\n [System.IO.Path]::DirectorySeparatorChar,\n [System.IO.Path]::AltDirectorySeparatorChar\n )\n )\n}\n\n${WINDOWS_DIRECTORY_HANDLE_HELPER}\n\n$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()\n$sid = $identity.User\nif ($null -eq $sid) {\n throw \"Could not resolve the current Windows user SID.\"\n}\n\n$inspection = [CrablineWindowsDirectoryHandle]::Open(\n $directoryPath,\n $false,\n $false,\n $false\n)\n$directory = $null\ntry {\n $directoryIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity($inspection)\n $acl = [System.Security.AccessControl.DirectorySecurity]::new()\n $acl.SetSecurityDescriptorBinaryForm(\n [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor($inspection)\n )\n $currentOwner = $acl.GetOwner(\n [System.Security.Principal.SecurityIdentifier]\n )\n $writeOwner = $currentOwner.Value -ne $sid.Value\n $directory = [CrablineWindowsDirectoryHandle]::Open(\n $directoryPath,\n $true,\n $writeOwner,\n $false\n )\n if (\n [CrablineWindowsDirectoryHandle]::ReadIdentity($directory) -ne\n $directoryIdentity\n ) {\n throw \"Private directory identity changed before ACL repair.\"\n }\n $acl.SetOwner($sid)\n $acl.SetAccessRuleProtection($true, $false)\n $existingRules = @($acl.GetAccessRules(\n $true,\n $false,\n [System.Security.Principal.SecurityIdentifier]\n ))\n foreach ($existingRule in $existingRules) {\n [void]$acl.RemoveAccessRuleSpecific($existingRule)\n }\n $inheritance = (\n [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor\n [System.Security.AccessControl.InheritanceFlags]::ObjectInherit\n )\n $rule = [System.Security.AccessControl.FileSystemAccessRule]::new(\n $sid,\n [System.Security.AccessControl.FileSystemRights]::FullControl,\n $inheritance,\n [System.Security.AccessControl.PropagationFlags]::None,\n [System.Security.AccessControl.AccessControlType]::Allow\n )\n $acl.SetAccessRule($rule)\n [CrablineWindowsDirectoryHandle]::WriteSecurityDescriptor(\n $directory,\n $acl.GetSecurityDescriptorBinaryForm(),\n $writeOwner\n )\n\n $actual = [System.Security.AccessControl.DirectorySecurity]::new()\n $actual.SetSecurityDescriptorBinaryForm(\n [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor($directory)\n )\n $ownerSid = $actual.GetOwner([System.Security.Principal.SecurityIdentifier])\n $rules = @($actual.GetAccessRules(\n $true,\n $true,\n [System.Security.Principal.SecurityIdentifier]\n ))\n if (-not $actual.AreAccessRulesProtected) {\n throw \"Private directory DACL still inherits permissions.\"\n }\n if ($ownerSid.Value -ne $sid.Value) {\n throw \"Private directory owner SID does not match the current user.\"\n }\n if ($rules.Count -ne 1) {\n throw \"Private directory DACL must contain exactly one access rule.\"\n }\n $actualRule = $rules[0]\n $requiredInheritance = (\n [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor\n [System.Security.AccessControl.InheritanceFlags]::ObjectInherit\n )\n if (\n $actualRule.IsInherited -or\n $actualRule.IdentityReference.Value -ne $sid.Value -or\n $actualRule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or\n (($actualRule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl) -or\n (($actualRule.InheritanceFlags -band $requiredInheritance) -ne $requiredInheritance)\n ) {\n throw \"Private directory DACL is not owner-only inheritable full control.\"\n }\n [CrablineWindowsDirectoryHandle]::AssertSamePathIdentity(\n $directoryPath,\n $directoryIdentity\n )\n} finally {\n if ($null -ne $directory) {\n $directory.Dispose()\n }\n $inspection.Dispose()\n}\n`;\n\nconst WINDOWS_SAFE_PARENT_NAMESPACE_HELPER = String.raw`\n$trustedSids = [System.Collections.Generic.HashSet[string]]::new(\n [System.StringComparer]::OrdinalIgnoreCase\n)\n[void]$trustedSids.Add($sid.Value)\n[void]$trustedSids.Add(\"S-1-5-18\")\n[void]$trustedSids.Add(\"S-1-5-32-544\")\n[void]$trustedSids.Add(\n \"S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464\"\n)\n$replacementRights = (\n [int64][System.Security.AccessControl.FileSystemRights]::WriteData -bor\n [int64][System.Security.AccessControl.FileSystemRights]::WriteAttributes -bor\n [int64][System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor\n [int64][System.Security.AccessControl.FileSystemRights]::Delete -bor\n [int64][System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor\n [int64][System.Security.AccessControl.FileSystemRights]::TakeOwnership -bor\n [int64]0x40000000 -bor\n [int64]0x10000000\n)\n\nfunction Assert-SafeParentNamespace(\n [string]$candidate,\n [bool]$rejectChildCreation\n) {\n $parentDirectory = [CrablineWindowsDirectoryHandle]::Open(\n $candidate,\n $false,\n $false,\n $false\n )\n try {\n # ReadIdentity rejects reparse points, so the lexical walk cannot cross a junction.\n $parentIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity($parentDirectory)\n $parentDescriptor = [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor(\n $parentDirectory\n )\n $parentRawDescriptor = [System.Security.AccessControl.RawSecurityDescriptor]::new(\n $parentDescriptor,\n 0\n )\n if (\n (($parentRawDescriptor.ControlFlags -band\n [System.Security.AccessControl.ControlFlags]::DiscretionaryAclPresent) -eq 0) -or\n $null -eq $parentRawDescriptor.DiscretionaryAcl\n ) {\n throw \"Private directory parent namespace has a null DACL.\"\n }\n $parentAcl = [System.Security.AccessControl.DirectorySecurity]::new()\n $parentAcl.SetSecurityDescriptorBinaryForm($parentDescriptor)\n $parentOwner = $parentAcl.GetOwner(\n [System.Security.Principal.SecurityIdentifier]\n )\n if (-not $trustedSids.Contains($parentOwner.Value)) {\n throw \"Private directory parent namespace has an untrusted owner.\"\n }\n $parentRules = @($parentAcl.GetAccessRules(\n $true,\n $true,\n [System.Security.Principal.SecurityIdentifier]\n ))\n $inheritOnly = [System.Security.AccessControl.PropagationFlags]::InheritOnly\n $unsafeRights = $replacementRights\n if ($rejectChildCreation) {\n $unsafeRights = (\n $unsafeRights -bor\n [int64][System.Security.AccessControl.FileSystemRights]::CreateDirectories\n )\n }\n foreach ($parentRule in $parentRules) {\n if (\n $parentRule.AccessControlType -ne\n [System.Security.AccessControl.AccessControlType]::Allow -or\n $trustedSids.Contains($parentRule.IdentityReference.Value) -or\n (($parentRule.PropagationFlags -band $inheritOnly) -ne 0)\n ) {\n continue\n }\n if (([int64]$parentRule.FileSystemRights -band $unsafeRights) -ne 0) {\n throw \"Private directory parent namespace permits untrusted replacement.\"\n }\n }\n [CrablineWindowsDirectoryHandle]::AssertSamePathIdentity(\n $candidate,\n $parentIdentity\n )\n } finally {\n $parentDirectory.Dispose()\n }\n}\n\nfunction Assert-SafeNamespaceChain(\n [string]$candidate,\n [bool]$rejectChildCreationAtCandidate\n) {\n $current = $candidate\n while ($null -ne $current) {\n Assert-SafeParentNamespace $current $rejectChildCreationAtCandidate\n $rejectChildCreationAtCandidate = $false\n $parent = [System.IO.Directory]::GetParent($current)\n $current = if ($null -eq $parent) { $null } else { $parent.FullName }\n }\n}\n`;\n\nconst WINDOWS_CREATE_OWNER_ONLY_DIRECTORY_SCRIPT = String.raw`\n$ErrorActionPreference = \"Stop\"\n$directoryPath = $env:CRABLINE_PRIVATE_DIRECTORY_PATH\nif ([string]::IsNullOrWhiteSpace($directoryPath)) {\n throw \"CRABLINE_PRIVATE_DIRECTORY_PATH is required.\"\n}\n$directoryPath = [System.IO.Path]::GetFullPath($directoryPath)\n$rootPath = [System.IO.Path]::GetPathRoot($directoryPath)\nif ($directoryPath.Length -gt $rootPath.Length) {\n $directoryPath = $directoryPath.TrimEnd(\n [char[]]@(\n [System.IO.Path]::DirectorySeparatorChar,\n [System.IO.Path]::AltDirectorySeparatorChar\n )\n )\n}\n\n${WINDOWS_DIRECTORY_HANDLE_HELPER}\n\n$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()\n$sid = $identity.User\nif ($null -eq $sid) {\n throw \"Could not resolve the current Windows user SID.\"\n}\n\n$acl = [System.Security.AccessControl.DirectorySecurity]::new()\n$acl.SetOwner($sid)\n$acl.SetAccessRuleProtection($true, $false)\n$inheritance = (\n [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor\n [System.Security.AccessControl.InheritanceFlags]::ObjectInherit\n)\n$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(\n $sid,\n [System.Security.AccessControl.FileSystemRights]::FullControl,\n $inheritance,\n [System.Security.AccessControl.PropagationFlags]::None,\n [System.Security.AccessControl.AccessControlType]::Allow\n)\n$acl.SetAccessRule($rule)\n$descriptor = $acl.GetSecurityDescriptorBinaryForm()\n\n${WINDOWS_SAFE_PARENT_NAMESPACE_HELPER}\n\n$missing = [System.Collections.Generic.List[string]]::new()\n$current = $directoryPath\nwhile (-not [System.IO.Directory]::Exists($current)) {\n if ([System.IO.File]::Exists($current)) {\n throw \"Private directory ancestry contains a non-directory entry.\"\n }\n $missing.Add($current)\n $parent = [System.IO.Directory]::GetParent($current)\n if ($null -eq $parent) {\n throw \"Private directory ancestry has no existing parent.\"\n }\n $current = $parent.FullName\n}\n\nif ($missing.Count -eq 0) {\n $parent = [System.IO.Directory]::GetParent($directoryPath)\n if ($null -eq $parent) {\n throw \"Private directory must have a parent.\"\n }\n $current = $parent.FullName\n}\n\n$directory = $null\n$heldDirectories = [System.Collections.Generic.List[Microsoft.Win32.SafeHandles.SafeFileHandle]]::new()\n$createdDirectories = [System.Collections.Generic.List[Microsoft.Win32.SafeHandles.SafeFileHandle]]::new()\n$firstCreatedDirectory = $null\ntry {\n if ($missing.Count -eq 0) {\n $inspection = [CrablineWindowsDirectoryHandle]::Open(\n $directoryPath,\n $false,\n $false,\n $false\n )\n $heldDirectories.Add($inspection)\n $directoryIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity($inspection)\n Assert-SafeNamespaceChain $current $false\n [CrablineWindowsDirectoryHandle]::AssertSamePathIdentity(\n $directoryPath,\n $directoryIdentity\n )\n $currentAcl = [System.Security.AccessControl.DirectorySecurity]::new()\n $currentAcl.SetSecurityDescriptorBinaryForm(\n [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor($inspection)\n )\n $currentOwner = $currentAcl.GetOwner(\n [System.Security.Principal.SecurityIdentifier]\n )\n $writeOwner = $currentOwner.Value -ne $sid.Value\n $directory = [CrablineWindowsDirectoryHandle]::Open(\n $directoryPath,\n $true,\n $writeOwner,\n $false\n )\n $heldDirectories.Add($directory)\n if (\n [CrablineWindowsDirectoryHandle]::ReadIdentity($directory) -ne\n $directoryIdentity\n ) {\n throw \"Private directory identity changed before ACL repair.\"\n }\n [CrablineWindowsDirectoryHandle]::WriteSecurityDescriptor(\n $directory,\n $descriptor,\n $writeOwner\n )\n } else {\n $paths = $missing.ToArray()\n [Array]::Reverse($paths)\n $parentDirectory = [CrablineWindowsDirectoryHandle]::Open(\n $current,\n $false,\n $false,\n $true\n )\n $heldDirectories.Add($parentDirectory)\n $existingParentIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity(\n $parentDirectory\n )\n Assert-SafeNamespaceChain $current $true\n [CrablineWindowsDirectoryHandle]::AssertSamePathIdentity(\n $current,\n $existingParentIdentity\n )\n\n foreach ($candidate in $paths) {\n $childName = [System.IO.Path]::GetFileName($candidate)\n $createdDirectory = $null\n try {\n $createdDirectory = [CrablineWindowsDirectoryHandle]::CreateRelative(\n $parentDirectory,\n $childName,\n $descriptor\n )\n $createdDirectories.Add($createdDirectory)\n if ($null -eq $firstCreatedDirectory) {\n $firstCreatedDirectory = $candidate\n }\n } catch [System.ComponentModel.Win32Exception] {\n if (\n $_.Exception.NativeErrorCode -ne 80 -and\n $_.Exception.NativeErrorCode -ne 183\n ) {\n throw\n }\n $inspection = [CrablineWindowsDirectoryHandle]::OpenRelative(\n $parentDirectory,\n $childName,\n $false,\n $false,\n $false\n )\n $heldDirectories.Add($inspection)\n $childIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity($inspection)\n $currentAcl = [System.Security.AccessControl.DirectorySecurity]::new()\n $currentAcl.SetSecurityDescriptorBinaryForm(\n [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor($inspection)\n )\n $currentOwner = $currentAcl.GetOwner(\n [System.Security.Principal.SecurityIdentifier]\n )\n $writeOwner = $currentOwner.Value -ne $sid.Value\n $createdDirectory = [CrablineWindowsDirectoryHandle]::OpenRelative(\n $parentDirectory,\n $childName,\n $true,\n $writeOwner,\n $true\n )\n if (\n [CrablineWindowsDirectoryHandle]::ReadIdentity($createdDirectory) -ne\n $childIdentity\n ) {\n $createdDirectory.Dispose()\n throw \"Private directory identity changed before ACL repair.\"\n }\n [CrablineWindowsDirectoryHandle]::WriteSecurityDescriptor(\n $createdDirectory,\n $descriptor,\n $writeOwner\n )\n }\n $heldDirectories.Add($createdDirectory)\n $parentDirectory = $createdDirectory\n }\n $directory = $parentDirectory\n [CrablineWindowsDirectoryHandle]::AssertSamePathIdentity(\n $current,\n $existingParentIdentity\n )\n }\n if ($null -eq $directory) {\n throw \"Windows did not return the private directory handle.\"\n }\n $directoryIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity($directory)\n $actual = [System.Security.AccessControl.DirectorySecurity]::new()\n $actual.SetSecurityDescriptorBinaryForm(\n [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor($directory)\n )\n $ownerSid = $actual.GetOwner([System.Security.Principal.SecurityIdentifier])\n $rules = @($actual.GetAccessRules(\n $true,\n $true,\n [System.Security.Principal.SecurityIdentifier]\n ))\n if (-not $actual.AreAccessRulesProtected) {\n throw \"Private directory DACL still inherits permissions.\"\n }\n if ($ownerSid.Value -ne $sid.Value) {\n throw \"Private directory owner SID does not match the current user.\"\n }\n if ($rules.Count -ne 1) {\n throw \"Private directory DACL must contain exactly one access rule.\"\n }\n $actualRule = $rules[0]\n if (\n $actualRule.IsInherited -or\n $actualRule.IdentityReference.Value -ne $sid.Value -or\n $actualRule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or\n (($actualRule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl) -or\n (($actualRule.InheritanceFlags -band $inheritance) -ne $inheritance)\n ) {\n throw \"Private directory DACL is not owner-only inheritable full control.\"\n }\n [CrablineWindowsDirectoryHandle]::AssertSamePathIdentity(\n $directoryPath,\n $directoryIdentity\n )\n if ($null -ne $firstCreatedDirectory) {\n [Console]::Out.Write($firstCreatedDirectory)\n }\n} catch {\n $primaryError = $_.Exception\n $cleanupErrors = [System.Collections.Generic.List[System.Exception]]::new()\n for ($index = $createdDirectories.Count - 1; $index -ge 0; $index--) {\n try {\n [CrablineWindowsDirectoryHandle]::MarkDelete(\n $createdDirectories[$index]\n )\n $createdDirectories[$index].Dispose()\n } catch {\n $cleanupErrors.Add($_.Exception)\n }\n }\n if ($cleanupErrors.Count -gt 0) {\n $aggregateErrors = [System.Collections.Generic.List[System.Exception]]::new()\n $aggregateErrors.Add($primaryError)\n $aggregateErrors.AddRange($cleanupErrors)\n throw [System.AggregateException]::new(\n \"Private directory ancestry creation and rollback cleanup failed.\",\n $aggregateErrors\n )\n }\n throw $primaryError\n} finally {\n for ($index = $heldDirectories.Count - 1; $index -ge 0; $index--) {\n $heldDirectories[$index].Dispose()\n }\n}\n`;\n\nconst WINDOWS_DIRECTORY_SECURITY_DESCRIPTOR_SCRIPT = String.raw`\n$ErrorActionPreference = \"Stop\"\n$directoryPath = $env:CRABLINE_PRIVATE_DIRECTORY_PATH\nif ([string]::IsNullOrWhiteSpace($directoryPath)) {\n throw \"CRABLINE_PRIVATE_DIRECTORY_PATH is required.\"\n}\n$directoryPath = [System.IO.Path]::GetFullPath($directoryPath)\n$rootPath = [System.IO.Path]::GetPathRoot($directoryPath)\nif ($directoryPath.Length -gt $rootPath.Length) {\n $directoryPath = $directoryPath.TrimEnd(\n [char[]]@(\n [System.IO.Path]::DirectorySeparatorChar,\n [System.IO.Path]::AltDirectorySeparatorChar\n )\n )\n}\n\n${WINDOWS_DIRECTORY_HANDLE_HELPER}\n\n$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()\n$sid = $identity.User\nif ($null -eq $sid) {\n throw \"Could not resolve the current Windows user SID.\"\n}\n\n${WINDOWS_SAFE_PARENT_NAMESPACE_HELPER}\n\n$parent = [System.IO.Directory]::GetParent($directoryPath)\nif ($null -eq $parent) {\n throw \"Private directory must have a parent.\"\n}\nAssert-SafeNamespaceChain $parent.FullName $false\n\n$directory = [CrablineWindowsDirectoryHandle]::Open(\n $directoryPath,\n $false,\n $false,\n $false\n)\ntry {\n $directoryIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity($directory)\n $descriptor = [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor($directory)\n $acl = [System.Security.AccessControl.DirectorySecurity]::new()\n $acl.SetSecurityDescriptorBinaryForm($descriptor)\n $ownerSid = $acl.GetOwner([System.Security.Principal.SecurityIdentifier])\n $rules = @($acl.GetAccessRules(\n $true,\n $true,\n [System.Security.Principal.SecurityIdentifier]\n ))\n $requiredInheritance = (\n [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor\n [System.Security.AccessControl.InheritanceFlags]::ObjectInherit\n )\n if (\n -not $acl.AreAccessRulesProtected -or\n $ownerSid.Value -ne $sid.Value -or\n $rules.Count -ne 1\n ) {\n throw \"Windows directory security descriptor is not owner-only.\"\n }\n $rule = $rules[0]\n if (\n $rule.IsInherited -or\n $rule.IdentityReference.Value -ne $sid.Value -or\n $rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or\n (($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl) -or\n (($rule.InheritanceFlags -band $requiredInheritance) -ne $requiredInheritance)\n ) {\n throw \"Windows directory security descriptor is not owner-only.\"\n }\n $pathIdentity = [CrablineWindowsDirectoryHandle]::ReadPathIdentity($directoryPath)\n if ($pathIdentity -ne $directoryIdentity) {\n throw \"Private directory identity changed during security inspection.\"\n }\n [Console]::Out.Write($directoryIdentity)\n [Console]::Out.Write([Environment]::NewLine)\n [Console]::Out.Write($pathIdentity)\n [Console]::Out.Write([Environment]::NewLine)\n [Console]::Out.Write([Convert]::ToBase64String($descriptor))\n} finally {\n $directory.Dispose()\n}\n`;\n\nconst WINDOWS_DIRECTORY_NAMESPACE_SECURITY_DESCRIPTOR_SCRIPT = String.raw`\n$ErrorActionPreference = \"Stop\"\n$directoryPath = $env:CRABLINE_PRIVATE_DIRECTORY_PATH\nif ([string]::IsNullOrWhiteSpace($directoryPath)) {\n throw \"CRABLINE_PRIVATE_DIRECTORY_PATH is required.\"\n}\n$directoryPath = [System.IO.Path]::GetFullPath($directoryPath)\n$rootPath = [System.IO.Path]::GetPathRoot($directoryPath)\nif ($directoryPath.Length -gt $rootPath.Length) {\n $directoryPath = $directoryPath.TrimEnd(\n [char[]]@(\n [System.IO.Path]::DirectorySeparatorChar,\n [System.IO.Path]::AltDirectorySeparatorChar\n )\n )\n}\n\n${WINDOWS_DIRECTORY_HANDLE_HELPER}\n\n$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()\n$sid = $identity.User\nif ($null -eq $sid) {\n throw \"Could not resolve the current Windows user SID.\"\n}\n\n${WINDOWS_SAFE_PARENT_NAMESPACE_HELPER}\n\nAssert-SafeNamespaceChain $directoryPath $true\n\n$directory = [CrablineWindowsDirectoryHandle]::Open(\n $directoryPath,\n $false,\n $false,\n $false\n)\ntry {\n $directoryIdentity = [CrablineWindowsDirectoryHandle]::ReadIdentity($directory)\n $descriptor = [CrablineWindowsDirectoryHandle]::ReadSecurityDescriptor($directory)\n $pathIdentity = [CrablineWindowsDirectoryHandle]::ReadPathIdentity($directoryPath)\n if ($pathIdentity -ne $directoryIdentity) {\n throw \"Private directory identity changed during security inspection.\"\n }\n [Console]::Out.Write($directoryIdentity)\n [Console]::Out.Write([Environment]::NewLine)\n [Console]::Out.Write($pathIdentity)\n [Console]::Out.Write([Environment]::NewLine)\n [Console]::Out.Write([Convert]::ToBase64String($descriptor))\n} finally {\n $directory.Dispose()\n}\n`;\n\nexport type WindowsAclRunner = (\n command: string,\n args: string[],\n options: {\n env: NodeJS.ProcessEnv;\n killSignal: NodeJS.Signals;\n timeout: number;\n windowsHide: boolean;\n },\n) => Promise<string>;\n\nexport type WindowsDirectorySecuritySnapshot = {\n identity: string;\n pathIdentity: string;\n securityDescriptor: string;\n};\n\nconst runWindowsAclCommand: WindowsAclRunner = async (command, args, options) => {\n const result = await execFileAsync(command, args, { ...options, encoding: \"utf8\" });\n return result.stdout;\n};\n\nexport function resolveWindowsPowerShellPath(systemRoot: string | null | undefined): string {\n const normalizedRoot = systemRoot?.trim() ? path.win32.normalize(systemRoot.trim()) : undefined;\n if (!normalizedRoot || !/^[A-Za-z]:\\\\/u.test(normalizedRoot)) {\n throw new Error(\"SystemRoot must be an absolute local Windows path.\");\n }\n return path.win32.join(normalizedRoot, \"System32\", \"WindowsPowerShell\", \"v1.0\", \"powershell.exe\");\n}\n\nexport async function applyOwnerOnlyWindowsDirectoryAcl(\n directoryPath: string,\n run: WindowsAclRunner = runWindowsAclCommand,\n systemRoot: string | null | undefined = process.env.SystemRoot,\n): Promise<void> {\n try {\n const powershellPath = resolveWindowsPowerShellPath(systemRoot);\n await run(\n powershellPath,\n [\n \"-NoLogo\",\n \"-NoProfile\",\n \"-NonInteractive\",\n \"-Command\",\n WINDOWS_OWNER_ONLY_DIRECTORY_ACL_SCRIPT,\n ],\n {\n env: {\n ...process.env,\n CRABLINE_PRIVATE_DIRECTORY_PATH: path.resolve(directoryPath),\n },\n killSignal: \"SIGKILL\",\n timeout: WINDOWS_ACL_COMMAND_TIMEOUT_MS,\n windowsHide: true,\n },\n );\n } catch (error) {\n throw new Error(\n \"Could not apply and verify an owner-only Windows directory ACL; Windows PowerShell ACL support is required.\",\n { cause: error },\n );\n }\n}\n\nexport async function createOwnerOnlyWindowsDirectoryAncestry(\n directoryPath: string,\n run: WindowsAclRunner = runWindowsAclCommand,\n systemRoot: string | null | undefined = process.env.SystemRoot,\n): Promise<string | undefined> {\n const powershellPath = resolveWindowsPowerShellPath(systemRoot);\n const output = await run(\n powershellPath,\n [\n \"-NoLogo\",\n \"-NoProfile\",\n \"-NonInteractive\",\n \"-Command\",\n WINDOWS_CREATE_OWNER_ONLY_DIRECTORY_SCRIPT,\n ],\n {\n env: {\n ...process.env,\n CRABLINE_PRIVATE_DIRECTORY_PATH: path.resolve(directoryPath),\n },\n killSignal: \"SIGKILL\",\n timeout: WINDOWS_ACL_COMMAND_TIMEOUT_MS,\n windowsHide: true,\n },\n );\n const firstCreatedDirectory = output.trim();\n return firstCreatedDirectory.length > 0 ? firstCreatedDirectory : undefined;\n}\n\nexport async function createOwnerOnlyWindowsDirectory(\n directoryPath: string,\n run: WindowsAclRunner = runWindowsAclCommand,\n systemRoot: string | null | undefined = process.env.SystemRoot,\n): Promise<void> {\n try {\n await createOwnerOnlyWindowsDirectoryAncestry(directoryPath, run, systemRoot);\n } catch (error) {\n throw new Error(\n \"Could not atomically create or verify an owner-only Windows directory; Windows PowerShell ACL support is required.\",\n { cause: error },\n );\n }\n}\n\nexport async function readWindowsDirectorySecuritySnapshot(\n directoryPath: string,\n run: WindowsAclRunner = runWindowsAclCommand,\n systemRoot: string | null | undefined = process.env.SystemRoot,\n): Promise<WindowsDirectorySecuritySnapshot> {\n try {\n const powershellPath = resolveWindowsPowerShellPath(systemRoot);\n const output = await run(\n powershellPath,\n [\n \"-NoLogo\",\n \"-NoProfile\",\n \"-NonInteractive\",\n \"-Command\",\n WINDOWS_DIRECTORY_SECURITY_DESCRIPTOR_SCRIPT,\n ],\n {\n env: {\n ...process.env,\n CRABLINE_PRIVATE_DIRECTORY_PATH: path.resolve(directoryPath),\n },\n killSignal: \"SIGKILL\",\n timeout: WINDOWS_ACL_COMMAND_TIMEOUT_MS,\n windowsHide: true,\n },\n );\n const [identity, pathIdentity, securityDescriptor, ...extraLines] = output\n .trim()\n .split(/\\r?\\n/u);\n if (\n !identity ||\n !/^\\d+:[0-9A-F]{32}$/u.test(identity) ||\n !pathIdentity ||\n !/^\\d+:[0-9A-F]{32}$/u.test(pathIdentity) ||\n pathIdentity !== identity ||\n !securityDescriptor ||\n extraLines.length > 0\n ) {\n throw new Error(\"Windows directory security snapshot was invalid.\");\n }\n return { identity, pathIdentity, securityDescriptor };\n } catch (error) {\n throw new Error(\n \"Could not read the Windows directory security descriptor through a stable no-follow handle; Windows PowerShell ACL support is required.\",\n { cause: error },\n );\n }\n}\n\nexport async function readWindowsDirectoryNamespaceSecuritySnapshot(\n directoryPath: string,\n run: WindowsAclRunner = runWindowsAclCommand,\n systemRoot: string | null | undefined = process.env.SystemRoot,\n): Promise<WindowsDirectorySecuritySnapshot> {\n try {\n const powershellPath = resolveWindowsPowerShellPath(systemRoot);\n const output = await run(\n powershellPath,\n [\n \"-NoLogo\",\n \"-NoProfile\",\n \"-NonInteractive\",\n \"-Command\",\n WINDOWS_DIRECTORY_NAMESPACE_SECURITY_DESCRIPTOR_SCRIPT,\n ],\n {\n env: {\n ...process.env,\n CRABLINE_PRIVATE_DIRECTORY_PATH: path.resolve(directoryPath),\n },\n killSignal: \"SIGKILL\",\n timeout: WINDOWS_ACL_COMMAND_TIMEOUT_MS,\n windowsHide: true,\n },\n );\n const [identity, pathIdentity, securityDescriptor, ...extraLines] = output\n .trim()\n .split(/\\r?\\n/u);\n if (\n !identity ||\n !/^\\d+:[0-9A-F]{32}$/u.test(identity) ||\n !pathIdentity ||\n !/^\\d+:[0-9A-F]{32}$/u.test(pathIdentity) ||\n pathIdentity !== identity ||\n !securityDescriptor ||\n extraLines.length > 0\n ) {\n throw new Error(\"Windows directory namespace security snapshot was invalid.\");\n }\n return { identity, pathIdentity, securityDescriptor };\n } catch (error) {\n throw new Error(\n \"Could not validate the Windows directory namespace through stable no-follow handles; Windows PowerShell ACL support is required.\",\n { cause: error },\n );\n }\n}\n\nexport async function readWindowsDirectorySecurityDescriptor(\n directoryPath: string,\n run: WindowsAclRunner = runWindowsAclCommand,\n systemRoot: string | null | undefined = process.env.SystemRoot,\n): Promise<string> {\n return (await readWindowsDirectorySecuritySnapshot(directoryPath, run, systemRoot))\n .securityDescriptor;\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WindowsDirectorySecuritySnapshot } from "./windows-acl.js";
|
|
2
|
+
export type WindowsLockRootIdentity = {
|
|
3
|
+
handleIdentity: string;
|
|
4
|
+
securityDescriptor: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function secureCachedWindowsLockRoot(options: {
|
|
7
|
+
cache: Map<string, Promise<WindowsLockRootIdentity>>;
|
|
8
|
+
cacheKey: string;
|
|
9
|
+
createDirectory: () => Promise<void>;
|
|
10
|
+
errorPrefix: string;
|
|
11
|
+
readSecuritySnapshot: () => Promise<WindowsDirectorySecuritySnapshot>;
|
|
12
|
+
root: string;
|
|
13
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
const MAX_WINDOWS_LOCK_ROOT_RECOVERIES = 1;
|
|
3
|
+
const MAX_WINDOWS_LOCK_ROOT_SNAPSHOT_ATTEMPTS = 2;
|
|
4
|
+
function isTransientPathError(error) {
|
|
5
|
+
const code = error.code;
|
|
6
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
7
|
+
}
|
|
8
|
+
function isPrivateWindowsLockRoot(stats) {
|
|
9
|
+
return stats.isDirectory() && !stats.isSymbolicLink();
|
|
10
|
+
}
|
|
11
|
+
async function readStableWindowsLockRootIdentity(options) {
|
|
12
|
+
for (let attempt = 0; attempt < MAX_WINDOWS_LOCK_ROOT_SNAPSHOT_ATTEMPTS; attempt += 1) {
|
|
13
|
+
let pathStats;
|
|
14
|
+
try {
|
|
15
|
+
pathStats = await lstat(options.root, { bigint: true });
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (isTransientPathError(error)) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
if (!isPrivateWindowsLockRoot(pathStats)) {
|
|
24
|
+
throw new Error(`${options.errorPrefix} is not a private directory.`);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const securitySnapshot = await options.readSecuritySnapshot();
|
|
28
|
+
if (securitySnapshot.pathIdentity !== securitySnapshot.identity) {
|
|
29
|
+
throw new Error(`${options.errorPrefix} path identity does not match its secure handle.`);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
handleIdentity: securitySnapshot.identity,
|
|
33
|
+
securityDescriptor: securitySnapshot.securityDescriptor,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (isTransientPathError(error)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`${options.errorPrefix} could not be stabilized.`);
|
|
44
|
+
}
|
|
45
|
+
export async function secureCachedWindowsLockRoot(options) {
|
|
46
|
+
let recoveryAttempts = 0;
|
|
47
|
+
for (;;) {
|
|
48
|
+
let secured = options.cache.get(options.cacheKey);
|
|
49
|
+
if (!secured) {
|
|
50
|
+
secured = (async () => {
|
|
51
|
+
await options.createDirectory();
|
|
52
|
+
return readStableWindowsLockRootIdentity(options);
|
|
53
|
+
})();
|
|
54
|
+
options.cache.set(options.cacheKey, secured);
|
|
55
|
+
void secured.catch(() => {
|
|
56
|
+
if (options.cache.get(options.cacheKey) === secured) {
|
|
57
|
+
options.cache.delete(options.cacheKey);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
let expected;
|
|
62
|
+
try {
|
|
63
|
+
expected = await secured;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (options.cache.get(options.cacheKey) === secured) {
|
|
67
|
+
options.cache.delete(options.cacheKey);
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
let current;
|
|
72
|
+
try {
|
|
73
|
+
current = await readStableWindowsLockRootIdentity(options);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (options.cache.get(options.cacheKey) === secured) {
|
|
77
|
+
options.cache.delete(options.cacheKey);
|
|
78
|
+
}
|
|
79
|
+
if (recoveryAttempts >= MAX_WINDOWS_LOCK_ROOT_RECOVERIES) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
recoveryAttempts += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (current.handleIdentity === expected.handleIdentity &&
|
|
86
|
+
current.securityDescriptor === expected.securityDescriptor) {
|
|
87
|
+
return options.root;
|
|
88
|
+
}
|
|
89
|
+
if (options.cache.get(options.cacheKey) === secured) {
|
|
90
|
+
options.cache.delete(options.cacheKey);
|
|
91
|
+
}
|
|
92
|
+
if (recoveryAttempts >= MAX_WINDOWS_LOCK_ROOT_RECOVERIES) {
|
|
93
|
+
throw new Error(`${options.errorPrefix} could not be stabilized.`);
|
|
94
|
+
}
|
|
95
|
+
recoveryAttempts += 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=windows-lock-root.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windows-lock-root.js","sourceRoot":"","sources":["../../../src/platform/windows-lock-root.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BigIntStats } from \"node:fs\";\nimport { lstat } from \"node:fs/promises\";\nimport type { WindowsDirectorySecuritySnapshot } from \"./windows-acl.js\";\n\nexport type WindowsLockRootIdentity = {\n handleIdentity: string;\n securityDescriptor: string;\n};\n\nconst MAX_WINDOWS_LOCK_ROOT_RECOVERIES = 1;\nconst MAX_WINDOWS_LOCK_ROOT_SNAPSHOT_ATTEMPTS = 2;\n\ntype WindowsLockRootStats = BigIntStats;\n\nfunction isTransientPathError(error: unknown): boolean {\n const code = (error as NodeJS.ErrnoException).code;\n return code === \"ENOENT\" || code === \"ENOTDIR\";\n}\n\nfunction isPrivateWindowsLockRoot(stats: WindowsLockRootStats): boolean {\n return stats.isDirectory() && !stats.isSymbolicLink();\n}\n\nasync function readStableWindowsLockRootIdentity(options: {\n errorPrefix: string;\n readSecuritySnapshot: () => Promise<WindowsDirectorySecuritySnapshot>;\n root: string;\n}): Promise<WindowsLockRootIdentity> {\n for (let attempt = 0; attempt < MAX_WINDOWS_LOCK_ROOT_SNAPSHOT_ATTEMPTS; attempt += 1) {\n let pathStats: WindowsLockRootStats;\n try {\n pathStats = await lstat(options.root, { bigint: true });\n } catch (error) {\n if (isTransientPathError(error)) {\n continue;\n }\n throw error;\n }\n if (!isPrivateWindowsLockRoot(pathStats)) {\n throw new Error(`${options.errorPrefix} is not a private directory.`);\n }\n try {\n const securitySnapshot = await options.readSecuritySnapshot();\n if (securitySnapshot.pathIdentity !== securitySnapshot.identity) {\n throw new Error(`${options.errorPrefix} path identity does not match its secure handle.`);\n }\n return {\n handleIdentity: securitySnapshot.identity,\n securityDescriptor: securitySnapshot.securityDescriptor,\n };\n } catch (error) {\n if (isTransientPathError(error)) {\n continue;\n }\n throw error;\n }\n }\n throw new Error(`${options.errorPrefix} could not be stabilized.`);\n}\n\nexport async function secureCachedWindowsLockRoot(options: {\n cache: Map<string, Promise<WindowsLockRootIdentity>>;\n cacheKey: string;\n createDirectory: () => Promise<void>;\n errorPrefix: string;\n readSecuritySnapshot: () => Promise<WindowsDirectorySecuritySnapshot>;\n root: string;\n}): Promise<string> {\n let recoveryAttempts = 0;\n for (;;) {\n let secured = options.cache.get(options.cacheKey);\n if (!secured) {\n secured = (async () => {\n await options.createDirectory();\n return readStableWindowsLockRootIdentity(options);\n })();\n options.cache.set(options.cacheKey, secured);\n void secured.catch(() => {\n if (options.cache.get(options.cacheKey) === secured) {\n options.cache.delete(options.cacheKey);\n }\n });\n }\n let expected: WindowsLockRootIdentity;\n try {\n expected = await secured;\n } catch (error) {\n if (options.cache.get(options.cacheKey) === secured) {\n options.cache.delete(options.cacheKey);\n }\n throw error;\n }\n let current: WindowsLockRootIdentity;\n try {\n current = await readStableWindowsLockRootIdentity(options);\n } catch (error) {\n if (options.cache.get(options.cacheKey) === secured) {\n options.cache.delete(options.cacheKey);\n }\n if (recoveryAttempts >= MAX_WINDOWS_LOCK_ROOT_RECOVERIES) {\n throw error;\n }\n recoveryAttempts += 1;\n continue;\n }\n if (\n current.handleIdentity === expected.handleIdentity &&\n current.securityDescriptor === expected.securityDescriptor\n ) {\n return options.root;\n }\n if (options.cache.get(options.cacheKey) === secured) {\n options.cache.delete(options.cacheKey);\n }\n if (recoveryAttempts >= MAX_WINDOWS_LOCK_ROOT_RECOVERIES) {\n throw new Error(`${options.errorPrefix} could not be stabilized.`);\n }\n recoveryAttempts += 1;\n }\n}\n"]}
|
|
@@ -16,6 +16,10 @@ export declare function normalizeDiscordWebhookPayload(payload: unknown): {
|
|
|
16
16
|
raw: {};
|
|
17
17
|
text?: string | undefined;
|
|
18
18
|
};
|
|
19
|
+
export declare function matchesDiscordThread(candidateThreadId: string, expectedThreadId: string | undefined, target: {
|
|
20
|
+
channelId?: string | undefined;
|
|
21
|
+
threadId?: string | undefined;
|
|
22
|
+
}, raw?: unknown): boolean;
|
|
19
23
|
export declare function createDiscordInteractionResponse(payload: unknown): Response;
|
|
20
24
|
export declare function handleDiscordWebhookPayload(payload: unknown): Response | undefined;
|
|
21
25
|
export declare class DiscordProviderAdapter extends LocalMockProviderAdapter implements ProviderAdapter {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createPublicKey, verify } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { CrablineError } from "../../core/errors.js";
|
|
4
|
-
import { LocalMockProviderAdapter } from "../local-mock.js";
|
|
4
|
+
import { LocalMockProviderAdapter, resolveGeneratedLocalMockRecorderPath } from "../local-mock.js";
|
|
5
5
|
import { DISCORD_SNOWFLAKE_RULE, getBuiltinTargetCodec } from "../target-normalizers.js";
|
|
6
6
|
import { authorFromBotFlag, genericMockPayloadWithNativeThread, isRecord, optionalRecord, optionalString, requireNativeInboundId, } from "./native-local-mock.js";
|
|
7
7
|
import { requireExternalWebhookAuthentication } from "./external-webhook-auth.js";
|
|
8
|
+
const DISCORD_SIGNATURE_MAX_SKEW_SECONDS = 5 * 60;
|
|
9
|
+
const DISCORD_THREAD_CHANNEL_TYPES = new Set([10, 11, 12]);
|
|
8
10
|
function resolveDiscordAdapterConfigValue(config, userName, env = process.env) {
|
|
9
11
|
return {
|
|
10
12
|
...((config.discord?.applicationId ?? env.DISCORD_APPLICATION_ID)
|
|
@@ -37,12 +39,15 @@ function discordPublicKey(value) {
|
|
|
37
39
|
type: "spki",
|
|
38
40
|
});
|
|
39
41
|
}
|
|
40
|
-
function authenticateDiscordWebhook(publicKey, request, rawBody) {
|
|
42
|
+
function authenticateDiscordWebhook(publicKey, request, rawBody, now = Date.now) {
|
|
41
43
|
const signature = request.headers.get("x-signature-ed25519");
|
|
42
44
|
const timestamp = request.headers.get("x-signature-timestamp");
|
|
45
|
+
const timestampSeconds = timestamp && /^\d+$/u.test(timestamp) ? Number(timestamp) : Number.NaN;
|
|
43
46
|
if (!signature ||
|
|
44
47
|
!timestamp ||
|
|
45
48
|
!/^[0-9a-f]{128}$/iu.test(signature) ||
|
|
49
|
+
!Number.isSafeInteger(timestampSeconds) ||
|
|
50
|
+
Math.abs(now() / 1_000 - timestampSeconds) > DISCORD_SIGNATURE_MAX_SKEW_SECONDS ||
|
|
46
51
|
!verify(null, Buffer.from(timestamp + rawBody), publicKey, Buffer.from(signature, "hex"))) {
|
|
47
52
|
return new Response("invalid request signature", { status: 401 });
|
|
48
53
|
}
|
|
@@ -73,7 +78,15 @@ function discordApplicationCommandText(data) {
|
|
|
73
78
|
}
|
|
74
79
|
};
|
|
75
80
|
collectValues(data.options);
|
|
76
|
-
|
|
81
|
+
const targetId = optionalString(data, "target_id");
|
|
82
|
+
const resolved = optionalRecord(data, "resolved");
|
|
83
|
+
const context = targetId === undefined
|
|
84
|
+
? undefined
|
|
85
|
+
: JSON.stringify({
|
|
86
|
+
target_id: targetId,
|
|
87
|
+
...(resolved ? { resolved } : {}),
|
|
88
|
+
});
|
|
89
|
+
return [name, ...values, ...(context ? [context] : [])].join(" ");
|
|
77
90
|
}
|
|
78
91
|
function discordInteractionText(data) {
|
|
79
92
|
const command = discordApplicationCommandText(data);
|
|
@@ -114,25 +127,33 @@ function toRecorderPath(providerId, config) {
|
|
|
114
127
|
const configuredPath = config.discord?.recorder.path;
|
|
115
128
|
return configuredPath
|
|
116
129
|
? path.resolve(configuredPath)
|
|
117
|
-
:
|
|
130
|
+
: resolveGeneratedLocalMockRecorderPath(providerId);
|
|
118
131
|
}
|
|
119
132
|
export function normalizeDiscordWebhookPayload(payload) {
|
|
120
133
|
if (!isRecord(payload)) {
|
|
121
134
|
throw new CrablineError("Discord webhook payload must be an object", { kind: "inbound" });
|
|
122
135
|
}
|
|
136
|
+
const { token: _token, ...safePayload } = payload;
|
|
123
137
|
const message = optionalRecord(payload, "message");
|
|
124
138
|
if ((message && ("text" in message || "threadId" in message)) ||
|
|
125
139
|
"text" in payload ||
|
|
126
140
|
"threadId" in payload) {
|
|
127
141
|
return genericMockPayloadWithNativeThread({
|
|
128
142
|
channelRule: DISCORD_SNOWFLAKE_RULE,
|
|
129
|
-
payload,
|
|
143
|
+
payload: safePayload,
|
|
130
144
|
threadRule: DISCORD_SNOWFLAKE_RULE,
|
|
131
145
|
});
|
|
132
146
|
}
|
|
133
147
|
const data = optionalRecord(payload, "data");
|
|
134
148
|
const author = optionalRecord(payload, "author") ?? optionalRecord(payload, "member")?.user;
|
|
135
149
|
const channelId = optionalString(payload, "channel_id");
|
|
150
|
+
const channel = optionalRecord(payload, "channel");
|
|
151
|
+
const embeddedChannelId = channel ? optionalString(channel, "id") : undefined;
|
|
152
|
+
if (embeddedChannelId && channelId && embeddedChannelId !== channelId) {
|
|
153
|
+
throw new CrablineError("Discord interaction channel.id must match channel_id", {
|
|
154
|
+
kind: "inbound",
|
|
155
|
+
});
|
|
156
|
+
}
|
|
136
157
|
const text = optionalString(payload, "content") ??
|
|
137
158
|
(data ? (optionalString(data, "content") ?? discordInteractionText(data)) : undefined) ??
|
|
138
159
|
(message ? optionalString(message, "content") : undefined);
|
|
@@ -142,15 +163,34 @@ export function normalizeDiscordWebhookPayload(payload) {
|
|
|
142
163
|
});
|
|
143
164
|
}
|
|
144
165
|
const authorRecord = isRecord(author) ? author : undefined;
|
|
145
|
-
const
|
|
166
|
+
const nativeChannelId = requireNativeInboundId(channelId, DISCORD_SNOWFLAKE_RULE, "Discord channel_id");
|
|
167
|
+
const threadId = optionalString(payload, "thread_id") ?? nativeChannelId;
|
|
146
168
|
return {
|
|
147
169
|
author: authorFromBotFlag(authorRecord?.bot === true),
|
|
148
170
|
...(optionalString(payload, "id") ? { id: optionalString(payload, "id") } : {}),
|
|
149
|
-
raw:
|
|
171
|
+
raw: safePayload,
|
|
150
172
|
text,
|
|
151
173
|
threadId: requireNativeInboundId(threadId, DISCORD_SNOWFLAKE_RULE, "Discord thread_id"),
|
|
152
174
|
};
|
|
153
175
|
}
|
|
176
|
+
export function matchesDiscordThread(candidateThreadId, expectedThreadId, target, raw) {
|
|
177
|
+
if (!expectedThreadId || candidateThreadId === expectedThreadId) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
if (target.threadId ||
|
|
181
|
+
!target.channelId ||
|
|
182
|
+
expectedThreadId !== target.channelId ||
|
|
183
|
+
!isRecord(raw)) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const channel = optionalRecord(raw, "channel");
|
|
187
|
+
const channelType = channel?.type;
|
|
188
|
+
return Boolean(channel &&
|
|
189
|
+
typeof channelType === "number" &&
|
|
190
|
+
DISCORD_THREAD_CHANNEL_TYPES.has(channelType) &&
|
|
191
|
+
optionalString(channel, "id") === candidateThreadId &&
|
|
192
|
+
optionalString(channel, "parent_id") === target.channelId);
|
|
193
|
+
}
|
|
154
194
|
export function createDiscordInteractionResponse(payload) {
|
|
155
195
|
if (!isRecord(payload)) {
|
|
156
196
|
return Response.json({ type: 5 }, { status: 200 });
|
|
@@ -177,7 +217,8 @@ export function handleDiscordWebhookPayload(payload) {
|
|
|
177
217
|
}
|
|
178
218
|
export class DiscordProviderAdapter extends LocalMockProviderAdapter {
|
|
179
219
|
constructor(id, config, userName, runtime) {
|
|
180
|
-
const
|
|
220
|
+
const authRuntime = runtime ?? {};
|
|
221
|
+
const env = authRuntime.env ?? process.env;
|
|
181
222
|
const resolvedConfig = resolveDiscordAdapterConfigValue(config, userName, env);
|
|
182
223
|
const publicKey = resolvedConfig.publicKey
|
|
183
224
|
? discordPublicKey(resolvedConfig.publicKey)
|
|
@@ -195,7 +236,7 @@ export class DiscordProviderAdapter extends LocalMockProviderAdapter {
|
|
|
195
236
|
options: {
|
|
196
237
|
...(publicKey
|
|
197
238
|
? {
|
|
198
|
-
authenticateWebhookRequest: (request, rawBody) => authenticateDiscordWebhook(publicKey, request, rawBody),
|
|
239
|
+
authenticateWebhookRequest: (request, rawBody) => authenticateDiscordWebhook(publicKey, request, rawBody, authRuntime.now),
|
|
199
240
|
}
|
|
200
241
|
: {}),
|
|
201
242
|
defaultWebhook: {
|
|
@@ -206,11 +247,13 @@ export class DiscordProviderAdapter extends LocalMockProviderAdapter {
|
|
|
206
247
|
createWebhookSuccessResponse: createDiscordInteractionResponse,
|
|
207
248
|
endpointLabel: "interactions endpoint",
|
|
208
249
|
handleWebhookPayload: handleDiscordWebhookPayload,
|
|
250
|
+
matchesThread: matchesDiscordThread,
|
|
209
251
|
normalizeWebhookPayload: normalizeDiscordWebhookPayload,
|
|
210
252
|
platform: "discord",
|
|
211
253
|
publicUrl: config.discord?.webhook.publicUrl,
|
|
212
254
|
recorderPath: toRecorderPath(id, config),
|
|
213
255
|
webhook: config.discord?.webhook,
|
|
256
|
+
webhookMethods: ["POST"],
|
|
214
257
|
},
|
|
215
258
|
});
|
|
216
259
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discord.js","sourceRoot":"","sources":["../../../../src/providers/builtin/discord.ts"],"names":[],"mappings":"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","sourcesContent":["import { createPublicKey, verify, type KeyObject } from \"node:crypto\";\nimport path from \"node:path\";\nimport { CrablineError } from \"../../core/errors.js\";\nimport type { ProviderConfig } from \"../../config/schema.js\";\nimport { LocalMockProviderAdapter } from \"../local-mock.js\";\nimport type { ProviderAdapter } from \"../types.js\";\nimport { DISCORD_SNOWFLAKE_RULE, getBuiltinTargetCodec } from \"../target-normalizers.js\";\nimport {\n authorFromBotFlag,\n genericMockPayloadWithNativeThread,\n isRecord,\n optionalRecord,\n optionalString,\n requireNativeInboundId,\n} from \"./native-local-mock.js\";\nimport { requireExternalWebhookAuthentication } from \"./external-webhook-auth.js\";\n\ntype DiscordEnvironment = Partial<\n Pick<NodeJS.ProcessEnv, \"DISCORD_APPLICATION_ID\" | \"DISCORD_BOT_TOKEN\" | \"DISCORD_PUBLIC_KEY\">\n>;\n\ntype DiscordRuntime = {\n env?: DiscordEnvironment | undefined;\n};\n\nfunction resolveDiscordAdapterConfigValue(\n config: ProviderConfig,\n userName: string,\n env: DiscordEnvironment = process.env,\n) {\n return {\n ...((config.discord?.applicationId ?? env.DISCORD_APPLICATION_ID)\n ? { applicationId: config.discord?.applicationId ?? env.DISCORD_APPLICATION_ID! }\n : {}),\n ...((config.discord?.botToken ?? env.DISCORD_BOT_TOKEN)\n ? { botToken: config.discord?.botToken ?? env.DISCORD_BOT_TOKEN! }\n : {}),\n ...((config.discord?.mentionRoleIds?.length ?? 0) > 0\n ? { mentionRoleIds: config.discord?.mentionRoleIds }\n : {}),\n ...((config.discord?.publicKey ?? env.DISCORD_PUBLIC_KEY)\n ? { publicKey: config.discord?.publicKey ?? env.DISCORD_PUBLIC_KEY! }\n : {}),\n userName,\n };\n}\n\nexport async function resolveDiscordAdapterConfig(\n config: ProviderConfig,\n userName: string,\n env: DiscordEnvironment = process.env,\n) {\n return resolveDiscordAdapterConfigValue(config, userName, env);\n}\n\nfunction discordPublicKey(value: string): KeyObject {\n if (!/^[0-9a-f]{64}$/iu.test(value)) {\n throw new CrablineError(\"Discord publicKey must be a 32-byte hexadecimal Ed25519 key.\", {\n kind: \"config\",\n });\n }\n return createPublicKey({\n format: \"der\",\n key: Buffer.concat([Buffer.from(\"302a300506032b6570032100\", \"hex\"), Buffer.from(value, \"hex\")]),\n type: \"spki\",\n });\n}\n\nfunction authenticateDiscordWebhook(publicKey: KeyObject, request: Request, rawBody: string) {\n const signature = request.headers.get(\"x-signature-ed25519\");\n const timestamp = request.headers.get(\"x-signature-timestamp\");\n if (\n !signature ||\n !timestamp ||\n !/^[0-9a-f]{128}$/iu.test(signature) ||\n !verify(null, Buffer.from(timestamp + rawBody), publicKey, Buffer.from(signature, \"hex\"))\n ) {\n return new Response(\"invalid request signature\", { status: 401 });\n }\n return undefined;\n}\n\nfunction discordApplicationCommandText(data: Record<string, unknown>): string | undefined {\n const name = optionalString(data, \"name\");\n if (!name) {\n return undefined;\n }\n\n const values: string[] = [];\n const collectValues = (options: unknown): void => {\n if (!Array.isArray(options)) {\n return;\n }\n for (const option of options) {\n if (!isRecord(option)) {\n continue;\n }\n if ((option.type === 1 || option.type === 2) && typeof option.name === \"string\") {\n values.push(option.name);\n }\n const value = option.value;\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n values.push(String(value));\n }\n collectValues(option.options);\n }\n };\n collectValues(data.options);\n return [name, ...values].join(\" \");\n}\n\nfunction discordInteractionText(data: Record<string, unknown>): string | undefined {\n const command = discordApplicationCommandText(data);\n if (command) {\n return command;\n }\n\n const values: string[] = [];\n const collectValues = (value: unknown): void => {\n if (Array.isArray(value)) {\n for (const child of value) {\n collectValues(child);\n }\n return;\n }\n if (!isRecord(value)) {\n return;\n }\n if (typeof value.value === \"string\") {\n values.push(value.value);\n }\n if (Array.isArray(value.values)) {\n for (const selected of value.values) {\n if (\n typeof selected === \"string\" ||\n typeof selected === \"number\" ||\n typeof selected === \"boolean\"\n ) {\n values.push(String(selected));\n }\n }\n }\n collectValues(value.components);\n };\n collectValues(data);\n const customId = optionalString(data, \"custom_id\");\n const parts = [...(customId ? [customId] : []), ...values];\n return parts.length > 0 ? parts.join(\" \") : undefined;\n}\n\nfunction toRecorderPath(providerId: string, config: ProviderConfig): string {\n const configuredPath = config.discord?.recorder.path;\n return configuredPath\n ? path.resolve(configuredPath)\n : path.resolve(\".crabline\", \"recorders\", `${providerId}.jsonl`);\n}\n\nexport function normalizeDiscordWebhookPayload(payload: unknown) {\n if (!isRecord(payload)) {\n throw new CrablineError(\"Discord webhook payload must be an object\", { kind: \"inbound\" });\n }\n\n const message = optionalRecord(payload, \"message\");\n if (\n (message && (\"text\" in message || \"threadId\" in message)) ||\n \"text\" in payload ||\n \"threadId\" in payload\n ) {\n return genericMockPayloadWithNativeThread({\n channelRule: DISCORD_SNOWFLAKE_RULE,\n payload,\n threadRule: DISCORD_SNOWFLAKE_RULE,\n });\n }\n\n const data = optionalRecord(payload, \"data\");\n const author = optionalRecord(payload, \"author\") ?? optionalRecord(payload, \"member\")?.user;\n const channelId = optionalString(payload, \"channel_id\");\n const text =\n optionalString(payload, \"content\") ??\n (data ? (optionalString(data, \"content\") ?? discordInteractionText(data)) : undefined) ??\n (message ? optionalString(message, \"content\") : undefined);\n if (!channelId || !text) {\n throw new CrablineError(\"Discord event payload requires channel_id and content\", {\n kind: \"inbound\",\n });\n }\n\n const authorRecord = isRecord(author) ? author : undefined;\n const threadId = optionalString(payload, \"thread_id\") ?? channelId;\n return {\n author: authorFromBotFlag(authorRecord?.bot === true),\n ...(optionalString(payload, \"id\") ? { id: optionalString(payload, \"id\") } : {}),\n raw: payload,\n text,\n threadId: requireNativeInboundId(threadId, DISCORD_SNOWFLAKE_RULE, \"Discord thread_id\"),\n };\n}\n\nexport function createDiscordInteractionResponse(payload: unknown): Response {\n if (!isRecord(payload)) {\n return Response.json({ type: 5 }, { status: 200 });\n }\n if (payload.type === 3) {\n return Response.json({ type: 6 }, { status: 200 });\n }\n if (payload.type === 4) {\n return Response.json({ data: { choices: [] }, type: 8 }, { status: 200 });\n }\n return Response.json({ type: 5 }, { status: 200 });\n}\n\nexport function handleDiscordWebhookPayload(payload: unknown): Response | undefined {\n if (!isRecord(payload)) {\n return undefined;\n }\n if (payload.type === 1) {\n return Response.json({ type: 1 }, { status: 200 });\n }\n if (payload.type === 4) {\n return createDiscordInteractionResponse(payload);\n }\n return undefined;\n}\n\nexport class DiscordProviderAdapter extends LocalMockProviderAdapter implements ProviderAdapter {\n constructor(id: string, config: ProviderConfig, userName: string, runtime?: unknown) {\n const env = (runtime as DiscordRuntime | undefined)?.env ?? process.env;\n const resolvedConfig = resolveDiscordAdapterConfigValue(config, userName, env);\n const publicKey = resolvedConfig.publicKey\n ? discordPublicKey(resolvedConfig.publicKey)\n : undefined;\n requireExternalWebhookAuthentication({\n authenticated: Boolean(publicKey),\n provider: \"Discord\",\n requirement: \"discord.publicKey or DISCORD_PUBLIC_KEY\",\n webhook: config.discord?.webhook,\n });\n super({\n codec: getBuiltinTargetCodec(\"discord\"),\n config,\n id,\n options: {\n ...(publicKey\n ? {\n authenticateWebhookRequest: (request: Request, rawBody: string) =>\n authenticateDiscordWebhook(publicKey, request, rawBody),\n }\n : {}),\n defaultWebhook: {\n host: \"127.0.0.1\",\n path: \"/discord/interactions\",\n port: 8788,\n },\n createWebhookSuccessResponse: createDiscordInteractionResponse,\n endpointLabel: \"interactions endpoint\",\n handleWebhookPayload: handleDiscordWebhookPayload,\n normalizeWebhookPayload: normalizeDiscordWebhookPayload,\n platform: \"discord\",\n publicUrl: config.discord?.webhook.publicUrl,\n recorderPath: toRecorderPath(id, config),\n webhook: config.discord?.webhook,\n },\n });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"discord.js","sourceRoot":"","sources":["../../../../src/providers/builtin/discord.ts"],"names":[],"mappings":"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","sourcesContent":["import { createPublicKey, verify, type KeyObject } from \"node:crypto\";\nimport path from \"node:path\";\nimport { CrablineError } from \"../../core/errors.js\";\nimport type { ProviderConfig } from \"../../config/schema.js\";\nimport { LocalMockProviderAdapter, resolveGeneratedLocalMockRecorderPath } from \"../local-mock.js\";\nimport type { ProviderAdapter } from \"../types.js\";\nimport { DISCORD_SNOWFLAKE_RULE, getBuiltinTargetCodec } from \"../target-normalizers.js\";\nimport {\n authorFromBotFlag,\n genericMockPayloadWithNativeThread,\n isRecord,\n optionalRecord,\n optionalString,\n requireNativeInboundId,\n} from \"./native-local-mock.js\";\nimport { requireExternalWebhookAuthentication } from \"./external-webhook-auth.js\";\n\ntype DiscordEnvironment = Partial<\n Pick<NodeJS.ProcessEnv, \"DISCORD_APPLICATION_ID\" | \"DISCORD_BOT_TOKEN\" | \"DISCORD_PUBLIC_KEY\">\n>;\n\ntype DiscordRuntime = {\n env?: DiscordEnvironment | undefined;\n now?: (() => number) | undefined;\n};\n\nconst DISCORD_SIGNATURE_MAX_SKEW_SECONDS = 5 * 60;\nconst DISCORD_THREAD_CHANNEL_TYPES = new Set([10, 11, 12]);\n\nfunction resolveDiscordAdapterConfigValue(\n config: ProviderConfig,\n userName: string,\n env: DiscordEnvironment = process.env,\n) {\n return {\n ...((config.discord?.applicationId ?? env.DISCORD_APPLICATION_ID)\n ? { applicationId: config.discord?.applicationId ?? env.DISCORD_APPLICATION_ID! }\n : {}),\n ...((config.discord?.botToken ?? env.DISCORD_BOT_TOKEN)\n ? { botToken: config.discord?.botToken ?? env.DISCORD_BOT_TOKEN! }\n : {}),\n ...((config.discord?.mentionRoleIds?.length ?? 0) > 0\n ? { mentionRoleIds: config.discord?.mentionRoleIds }\n : {}),\n ...((config.discord?.publicKey ?? env.DISCORD_PUBLIC_KEY)\n ? { publicKey: config.discord?.publicKey ?? env.DISCORD_PUBLIC_KEY! }\n : {}),\n userName,\n };\n}\n\nexport async function resolveDiscordAdapterConfig(\n config: ProviderConfig,\n userName: string,\n env: DiscordEnvironment = process.env,\n) {\n return resolveDiscordAdapterConfigValue(config, userName, env);\n}\n\nfunction discordPublicKey(value: string): KeyObject {\n if (!/^[0-9a-f]{64}$/iu.test(value)) {\n throw new CrablineError(\"Discord publicKey must be a 32-byte hexadecimal Ed25519 key.\", {\n kind: \"config\",\n });\n }\n return createPublicKey({\n format: \"der\",\n key: Buffer.concat([Buffer.from(\"302a300506032b6570032100\", \"hex\"), Buffer.from(value, \"hex\")]),\n type: \"spki\",\n });\n}\n\nfunction authenticateDiscordWebhook(\n publicKey: KeyObject,\n request: Request,\n rawBody: string,\n now: () => number = Date.now,\n) {\n const signature = request.headers.get(\"x-signature-ed25519\");\n const timestamp = request.headers.get(\"x-signature-timestamp\");\n const timestampSeconds = timestamp && /^\\d+$/u.test(timestamp) ? Number(timestamp) : Number.NaN;\n if (\n !signature ||\n !timestamp ||\n !/^[0-9a-f]{128}$/iu.test(signature) ||\n !Number.isSafeInteger(timestampSeconds) ||\n Math.abs(now() / 1_000 - timestampSeconds) > DISCORD_SIGNATURE_MAX_SKEW_SECONDS ||\n !verify(null, Buffer.from(timestamp + rawBody), publicKey, Buffer.from(signature, \"hex\"))\n ) {\n return new Response(\"invalid request signature\", { status: 401 });\n }\n return undefined;\n}\n\nfunction discordApplicationCommandText(data: Record<string, unknown>): string | undefined {\n const name = optionalString(data, \"name\");\n if (!name) {\n return undefined;\n }\n\n const values: string[] = [];\n const collectValues = (options: unknown): void => {\n if (!Array.isArray(options)) {\n return;\n }\n for (const option of options) {\n if (!isRecord(option)) {\n continue;\n }\n if ((option.type === 1 || option.type === 2) && typeof option.name === \"string\") {\n values.push(option.name);\n }\n const value = option.value;\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n values.push(String(value));\n }\n collectValues(option.options);\n }\n };\n collectValues(data.options);\n const targetId = optionalString(data, \"target_id\");\n const resolved = optionalRecord(data, \"resolved\");\n const context =\n targetId === undefined\n ? undefined\n : JSON.stringify({\n target_id: targetId,\n ...(resolved ? { resolved } : {}),\n });\n return [name, ...values, ...(context ? [context] : [])].join(\" \");\n}\n\nfunction discordInteractionText(data: Record<string, unknown>): string | undefined {\n const command = discordApplicationCommandText(data);\n if (command) {\n return command;\n }\n\n const values: string[] = [];\n const collectValues = (value: unknown): void => {\n if (Array.isArray(value)) {\n for (const child of value) {\n collectValues(child);\n }\n return;\n }\n if (!isRecord(value)) {\n return;\n }\n if (typeof value.value === \"string\") {\n values.push(value.value);\n }\n if (Array.isArray(value.values)) {\n for (const selected of value.values) {\n if (\n typeof selected === \"string\" ||\n typeof selected === \"number\" ||\n typeof selected === \"boolean\"\n ) {\n values.push(String(selected));\n }\n }\n }\n collectValues(value.components);\n };\n collectValues(data);\n const customId = optionalString(data, \"custom_id\");\n const parts = [...(customId ? [customId] : []), ...values];\n return parts.length > 0 ? parts.join(\" \") : undefined;\n}\n\nfunction toRecorderPath(providerId: string, config: ProviderConfig): string {\n const configuredPath = config.discord?.recorder.path;\n return configuredPath\n ? path.resolve(configuredPath)\n : resolveGeneratedLocalMockRecorderPath(providerId);\n}\n\nexport function normalizeDiscordWebhookPayload(payload: unknown) {\n if (!isRecord(payload)) {\n throw new CrablineError(\"Discord webhook payload must be an object\", { kind: \"inbound\" });\n }\n const { token: _token, ...safePayload } = payload;\n\n const message = optionalRecord(payload, \"message\");\n if (\n (message && (\"text\" in message || \"threadId\" in message)) ||\n \"text\" in payload ||\n \"threadId\" in payload\n ) {\n return genericMockPayloadWithNativeThread({\n channelRule: DISCORD_SNOWFLAKE_RULE,\n payload: safePayload,\n threadRule: DISCORD_SNOWFLAKE_RULE,\n });\n }\n\n const data = optionalRecord(payload, \"data\");\n const author = optionalRecord(payload, \"author\") ?? optionalRecord(payload, \"member\")?.user;\n const channelId = optionalString(payload, \"channel_id\");\n const channel = optionalRecord(payload, \"channel\");\n const embeddedChannelId = channel ? optionalString(channel, \"id\") : undefined;\n if (embeddedChannelId && channelId && embeddedChannelId !== channelId) {\n throw new CrablineError(\"Discord interaction channel.id must match channel_id\", {\n kind: \"inbound\",\n });\n }\n const text =\n optionalString(payload, \"content\") ??\n (data ? (optionalString(data, \"content\") ?? discordInteractionText(data)) : undefined) ??\n (message ? optionalString(message, \"content\") : undefined);\n if (!channelId || !text) {\n throw new CrablineError(\"Discord event payload requires channel_id and content\", {\n kind: \"inbound\",\n });\n }\n\n const authorRecord = isRecord(author) ? author : undefined;\n const nativeChannelId = requireNativeInboundId(\n channelId,\n DISCORD_SNOWFLAKE_RULE,\n \"Discord channel_id\",\n );\n const threadId = optionalString(payload, \"thread_id\") ?? nativeChannelId;\n return {\n author: authorFromBotFlag(authorRecord?.bot === true),\n ...(optionalString(payload, \"id\") ? { id: optionalString(payload, \"id\") } : {}),\n raw: safePayload,\n text,\n threadId: requireNativeInboundId(threadId, DISCORD_SNOWFLAKE_RULE, \"Discord thread_id\"),\n };\n}\n\nexport function matchesDiscordThread(\n candidateThreadId: string,\n expectedThreadId: string | undefined,\n target: { channelId?: string | undefined; threadId?: string | undefined },\n raw?: unknown,\n): boolean {\n if (!expectedThreadId || candidateThreadId === expectedThreadId) {\n return true;\n }\n if (\n target.threadId ||\n !target.channelId ||\n expectedThreadId !== target.channelId ||\n !isRecord(raw)\n ) {\n return false;\n }\n const channel = optionalRecord(raw, \"channel\");\n const channelType = channel?.type;\n return Boolean(\n channel &&\n typeof channelType === \"number\" &&\n DISCORD_THREAD_CHANNEL_TYPES.has(channelType) &&\n optionalString(channel, \"id\") === candidateThreadId &&\n optionalString(channel, \"parent_id\") === target.channelId,\n );\n}\n\nexport function createDiscordInteractionResponse(payload: unknown): Response {\n if (!isRecord(payload)) {\n return Response.json({ type: 5 }, { status: 200 });\n }\n if (payload.type === 3) {\n return Response.json({ type: 6 }, { status: 200 });\n }\n if (payload.type === 4) {\n return Response.json({ data: { choices: [] }, type: 8 }, { status: 200 });\n }\n return Response.json({ type: 5 }, { status: 200 });\n}\n\nexport function handleDiscordWebhookPayload(payload: unknown): Response | undefined {\n if (!isRecord(payload)) {\n return undefined;\n }\n if (payload.type === 1) {\n return Response.json({ type: 1 }, { status: 200 });\n }\n if (payload.type === 4) {\n return createDiscordInteractionResponse(payload);\n }\n return undefined;\n}\n\nexport class DiscordProviderAdapter extends LocalMockProviderAdapter implements ProviderAdapter {\n constructor(id: string, config: ProviderConfig, userName: string, runtime?: unknown) {\n const authRuntime = (runtime as DiscordRuntime | undefined) ?? {};\n const env = authRuntime.env ?? process.env;\n const resolvedConfig = resolveDiscordAdapterConfigValue(config, userName, env);\n const publicKey = resolvedConfig.publicKey\n ? discordPublicKey(resolvedConfig.publicKey)\n : undefined;\n requireExternalWebhookAuthentication({\n authenticated: Boolean(publicKey),\n provider: \"Discord\",\n requirement: \"discord.publicKey or DISCORD_PUBLIC_KEY\",\n webhook: config.discord?.webhook,\n });\n super({\n codec: getBuiltinTargetCodec(\"discord\"),\n config,\n id,\n options: {\n ...(publicKey\n ? {\n authenticateWebhookRequest: (request: Request, rawBody: string) =>\n authenticateDiscordWebhook(publicKey, request, rawBody, authRuntime.now),\n }\n : {}),\n defaultWebhook: {\n host: \"127.0.0.1\",\n path: \"/discord/interactions\",\n port: 8788,\n },\n createWebhookSuccessResponse: createDiscordInteractionResponse,\n endpointLabel: \"interactions endpoint\",\n handleWebhookPayload: handleDiscordWebhookPayload,\n matchesThread: matchesDiscordThread,\n normalizeWebhookPayload: normalizeDiscordWebhookPayload,\n platform: \"discord\",\n publicUrl: config.discord?.webhook.publicUrl,\n recorderPath: toRecorderPath(id, config),\n webhook: config.discord?.webhook,\n webhookMethods: [\"POST\"],\n },\n });\n }\n}\n"]}
|
|
@@ -2,9 +2,46 @@ import { CrablineError } from "../../core/errors.js";
|
|
|
2
2
|
import { isLoopbackHost } from "../../servers/http.js";
|
|
3
3
|
export function requireExternalWebhookAuthentication(params) {
|
|
4
4
|
const host = params.webhook?.host ?? "127.0.0.1";
|
|
5
|
-
const
|
|
5
|
+
const hostIsLoopback = isLoopbackHost(host);
|
|
6
|
+
const publicUrls = [
|
|
7
|
+
...(params.authenticatedIngressUrls ?? []),
|
|
8
|
+
...(params.webhook?.publicUrl ? [params.webhook.publicUrl] : []),
|
|
9
|
+
];
|
|
10
|
+
const callbackUrls = publicUrls.map((publicUrl) => {
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL(publicUrl);
|
|
13
|
+
return {
|
|
14
|
+
isLoopback: hostIsLoopback && isLoopbackHost(url.hostname),
|
|
15
|
+
url,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new CrablineError(`${params.provider} public callback URL is invalid.`, {
|
|
20
|
+
cause: error,
|
|
21
|
+
kind: "config",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
for (const callbackUrl of callbackUrls) {
|
|
26
|
+
if (callbackUrl.url.protocol !== "http:" && callbackUrl.url.protocol !== "https:") {
|
|
27
|
+
throw new CrablineError(`${params.provider} authenticated external webhooks require HTTPS; plain HTTP is allowed only for loopback-local ingress.`, { kind: "config" });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const externallyReachable = !hostIsLoopback || callbackUrls.some((callbackUrl) => !callbackUrl.isLoopback);
|
|
6
31
|
if (externallyReachable && !params.authenticated) {
|
|
7
32
|
throw new CrablineError(`${params.provider} externally reachable webhooks require ${params.requirement}.`, { kind: "config" });
|
|
8
33
|
}
|
|
34
|
+
if (!externallyReachable) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (publicUrls.length === 0) {
|
|
38
|
+
throw new CrablineError(`${params.provider} authenticated external webhooks require a public callback URL with HTTPS.`, { kind: "config" });
|
|
39
|
+
}
|
|
40
|
+
for (const callbackUrl of callbackUrls) {
|
|
41
|
+
const safeLoopbackHttp = callbackUrl.url.protocol === "http:" && callbackUrl.isLoopback;
|
|
42
|
+
if (callbackUrl.url.protocol !== "https:" && !safeLoopbackHttp) {
|
|
43
|
+
throw new CrablineError(`${params.provider} authenticated external webhooks require HTTPS; plain HTTP is allowed only for loopback-local ingress.`, { kind: "config" });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
9
46
|
}
|
|
10
47
|
//# sourceMappingURL=external-webhook-auth.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"external-webhook-auth.js","sourceRoot":"","sources":["../../../../src/providers/builtin/external-webhook-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAOvD,MAAM,UAAU,oCAAoC,CAAC,
|
|
1
|
+
{"version":3,"file":"external-webhook-auth.js","sourceRoot":"","sources":["../../../../src/providers/builtin/external-webhook-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAOvD,MAAM,UAAU,oCAAoC,CAAC,MAMpD;IACC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,EAAE,IAAI,IAAI,WAAW,CAAC;IACjD,MAAM,cAAc,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,UAAU,GAAG;QACjB,GAAG,CAAC,MAAM,CAAC,wBAAwB,IAAI,EAAE,CAAC;QAC1C,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KACjE,CAAC;IACF,MAAM,YAAY,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE;QAChD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;YAC/B,OAAO;gBACL,UAAU,EAAE,cAAc,IAAI,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAC1D,GAAG;aACJ,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,aAAa,CAAC,GAAG,MAAM,CAAC,QAAQ,kCAAkC,EAAE;gBAC5E,KAAK,EAAE,KAAK;gBACZ,IAAI,EAAE,QAAQ;aACf,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC,CAAC;IACH,KAAK,MAAM,WAAW,IAAI,YAAY,EAAE,CAAC;QACvC,IAAI,WAAW,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,WAAW,CAAC,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAClF,MAAM,IAAI,aAAa,CACrB,GAAG,MAAM,CAAC,QAAQ,wGAAwG,EAC1H,EAAE,IAAI,EAAE,QAAQ,EAAE,CACnB,CAAC;QACJ,CAAC;IACH,CAAC;IACD,MAAM,mBAAmB,GACvB,CAAC,cAAc,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;IACjF,IAAI,mBAAmB,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;QACjD,MAAM,IAAI,aAAa,CACrB,GAAG,MAAM,CAAC,QAAQ,0CAA0C,MAAM,CAAC,WAAW,GAAG,EACjF,EAAE,IAAI,EAAE,QAAQ,EAAE,CACnB,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACzB,OAAO;IACT,CAAC;IACD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,aAAa,CACrB,GAAG,MAAM,CAAC,QAAQ,4EAA4E,EAC9F,EAAE,IAAI,EAAE,QAAQ,EAAE,CACnB,CAAC;IACJ,CAAC;IAED,KAAK,MAAM,WAAW,IAAI,YAAY,EAAE,CAAC;QACvC,MAAM,gBAAgB,GAAG,WAAW,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,WAAW,CAAC,UAAU,CAAC;QACxF,IAAI,WAAW,CAAC,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC/D,MAAM,IAAI,aAAa,CACrB,GAAG,MAAM,CAAC,QAAQ,wGAAwG,EAC1H,EAAE,IAAI,EAAE,QAAQ,EAAE,CACnB,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["import { CrablineError } from \"../../core/errors.js\";\nimport { isLoopbackHost } from \"../../servers/http.js\";\n\ntype WebhookConfig = {\n host?: string | undefined;\n publicUrl?: string | undefined;\n};\n\nexport function requireExternalWebhookAuthentication(params: {\n authenticated: boolean;\n authenticatedIngressUrls?: readonly string[] | undefined;\n provider: string;\n requirement: string;\n webhook: WebhookConfig | undefined;\n}): void {\n const host = params.webhook?.host ?? \"127.0.0.1\";\n const hostIsLoopback = isLoopbackHost(host);\n const publicUrls = [\n ...(params.authenticatedIngressUrls ?? []),\n ...(params.webhook?.publicUrl ? [params.webhook.publicUrl] : []),\n ];\n const callbackUrls = publicUrls.map((publicUrl) => {\n try {\n const url = new URL(publicUrl);\n return {\n isLoopback: hostIsLoopback && isLoopbackHost(url.hostname),\n url,\n };\n } catch (error) {\n throw new CrablineError(`${params.provider} public callback URL is invalid.`, {\n cause: error,\n kind: \"config\",\n });\n }\n });\n for (const callbackUrl of callbackUrls) {\n if (callbackUrl.url.protocol !== \"http:\" && callbackUrl.url.protocol !== \"https:\") {\n throw new CrablineError(\n `${params.provider} authenticated external webhooks require HTTPS; plain HTTP is allowed only for loopback-local ingress.`,\n { kind: \"config\" },\n );\n }\n }\n const externallyReachable =\n !hostIsLoopback || callbackUrls.some((callbackUrl) => !callbackUrl.isLoopback);\n if (externallyReachable && !params.authenticated) {\n throw new CrablineError(\n `${params.provider} externally reachable webhooks require ${params.requirement}.`,\n { kind: \"config\" },\n );\n }\n if (!externallyReachable) {\n return;\n }\n if (publicUrls.length === 0) {\n throw new CrablineError(\n `${params.provider} authenticated external webhooks require a public callback URL with HTTPS.`,\n { kind: \"config\" },\n );\n }\n\n for (const callbackUrl of callbackUrls) {\n const safeLoopbackHttp = callbackUrl.url.protocol === \"http:\" && callbackUrl.isLoopback;\n if (callbackUrl.url.protocol !== \"https:\" && !safeLoopbackHttp) {\n throw new CrablineError(\n `${params.provider} authenticated external webhooks require HTTPS; plain HTTP is allowed only for loopback-local ingress.`,\n { kind: \"config\" },\n );\n }\n }\n}\n"]}
|
|
@@ -4,6 +4,7 @@ import type { ProviderAdapter } from "../types.js";
|
|
|
4
4
|
type FeishuEnvironment = Partial<Pick<NodeJS.ProcessEnv, "FEISHU_ENCRYPT_KEY" | "FEISHU_VERIFICATION_TOKEN">>;
|
|
5
5
|
type FeishuAuthRuntime = {
|
|
6
6
|
now?: (() => number) | undefined;
|
|
7
|
+
replayCacheLimit?: number | undefined;
|
|
7
8
|
};
|
|
8
9
|
export declare function resolveFeishuAdapterConfig(config: ProviderConfig, env?: FeishuEnvironment): {
|
|
9
10
|
appId: string;
|