@joe-broadhead/messaging 0.0.0-stage → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/CHANGELOG.md +220 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +12 -0
  4. package/README.md +36 -2
  5. package/dist/bin/messaging-mcp.js +26524 -0
  6. package/dist/bin/messaging-relay.js +19768 -0
  7. package/dist/bin/messaging.js +15465 -0
  8. package/dist/bin/messagingd.js +29449 -0
  9. package/dist/lib/core-testing.js +1 -0
  10. package/dist/lib/core.js +1 -0
  11. package/dist/lib/ipc-testing.js +1 -0
  12. package/dist/lib/ipc.js +1 -0
  13. package/dist/lib/modules/core/address.js +42 -0
  14. package/dist/lib/modules/core/agent/operations.js +595 -0
  15. package/dist/lib/modules/core/agent/present.js +244 -0
  16. package/dist/lib/modules/core/agent/text.js +79 -0
  17. package/dist/lib/modules/core/agent-tools.js +477 -0
  18. package/dist/lib/modules/core/b64u.js +20 -0
  19. package/dist/lib/modules/core/cards.js +55 -0
  20. package/dist/lib/modules/core/check.js +20 -0
  21. package/dist/lib/modules/core/consent.js +199 -0
  22. package/dist/lib/modules/core/contact-card.js +74 -0
  23. package/dist/lib/modules/core/conversation/context.js +80 -0
  24. package/dist/lib/modules/core/conversation/event.js +100 -0
  25. package/dist/lib/modules/core/conversation/ids.js +38 -0
  26. package/dist/lib/modules/core/conversation/link.js +35 -0
  27. package/dist/lib/modules/core/conversation/log.js +766 -0
  28. package/dist/lib/modules/core/conversation/ratchet.js +243 -0
  29. package/dist/lib/modules/core/conversation/seal.js +198 -0
  30. package/dist/lib/modules/core/delegation.js +174 -0
  31. package/dist/lib/modules/core/directory/device-log.js +258 -0
  32. package/dist/lib/modules/core/directory/epoch.js +103 -0
  33. package/dist/lib/modules/core/directory/handle.js +29 -0
  34. package/dist/lib/modules/core/directory/jelly-device.js +204 -0
  35. package/dist/lib/modules/core/directory/jelly-issuer.js +12 -0
  36. package/dist/lib/modules/core/directory/jelly-token.js +305 -0
  37. package/dist/lib/modules/core/directory/keys-statement.js +43 -0
  38. package/dist/lib/modules/core/directory/link.js +81 -0
  39. package/dist/lib/modules/core/directory/merkle-log.js +250 -0
  40. package/dist/lib/modules/core/directory/person-view.js +197 -0
  41. package/dist/lib/modules/core/directory/person.js +40 -0
  42. package/dist/lib/modules/core/directory/realm.js +59 -0
  43. package/dist/lib/modules/core/directory/smt.js +266 -0
  44. package/dist/lib/modules/core/envelope.js +320 -0
  45. package/dist/lib/modules/core/hash.js +5 -0
  46. package/dist/lib/modules/core/head.js +27 -0
  47. package/dist/lib/modules/core/identity.js +71 -0
  48. package/dist/lib/modules/core/index.js +53 -0
  49. package/dist/lib/modules/core/jcs.js +96 -0
  50. package/dist/lib/modules/core/jelly-host-double.js +35 -0
  51. package/dist/lib/modules/core/jelly-host.js +211 -0
  52. package/dist/lib/modules/core/keys.js +79 -0
  53. package/dist/lib/modules/core/peer-text.js +563 -0
  54. package/dist/lib/modules/core/realm.js +1 -0
  55. package/dist/lib/modules/core/roster.js +454 -0
  56. package/dist/lib/modules/core/schema/a2a.js +85 -0
  57. package/dist/lib/modules/core/schema/common.js +310 -0
  58. package/dist/lib/modules/core/schema/consent.js +120 -0
  59. package/dist/lib/modules/core/schema/conversation.js +292 -0
  60. package/dist/lib/modules/core/schema/delegation.js +58 -0
  61. package/dist/lib/modules/core/schema/directory-frames.js +270 -0
  62. package/dist/lib/modules/core/schema/directory.js +169 -0
  63. package/dist/lib/modules/core/schema/envelope.js +211 -0
  64. package/dist/lib/modules/core/schema/errors.js +104 -0
  65. package/dist/lib/modules/core/schema/index.js +16 -0
  66. package/dist/lib/modules/core/schema/ipc/consent.js +181 -0
  67. package/dist/lib/modules/core/schema/ipc/device.js +276 -0
  68. package/dist/lib/modules/core/schema/ipc/jelly.js +189 -0
  69. package/dist/lib/modules/core/schema/ipc/shapes.js +185 -0
  70. package/dist/lib/modules/core/schema/ipc/surfaces.js +223 -0
  71. package/dist/lib/modules/core/schema/ipc.js +451 -0
  72. package/dist/lib/modules/core/schema/jelly.js +122 -0
  73. package/dist/lib/modules/core/schema/realm.js +27 -0
  74. package/dist/lib/modules/core/schema/relay.js +112 -0
  75. package/dist/lib/modules/core/schema/roster.js +126 -0
  76. package/dist/lib/modules/core/sign.js +43 -0
  77. package/dist/lib/modules/core/task.js +37 -0
  78. package/dist/lib/modules/core/testing.js +21 -0
  79. package/dist/lib/modules/core/ulid.js +36 -0
  80. package/dist/lib/modules/core/version.js +6 -0
  81. package/dist/lib/modules/core/wire.js +255 -0
  82. package/dist/lib/modules/ipc/client.js +289 -0
  83. package/dist/lib/modules/ipc/framing.js +33 -0
  84. package/dist/lib/modules/ipc/index.js +6 -0
  85. package/dist/lib/modules/ipc/paths.js +102 -0
  86. package/dist/lib/modules/ipc/pipes.js +65 -0
  87. package/dist/lib/modules/ipc/rpc.js +8 -0
  88. package/dist/lib/modules/ipc/server.js +317 -0
  89. package/dist/lib/modules/ipc/testing.js +145 -0
  90. package/dist/lib/modules/ipc/trust.js +166 -0
  91. package/dist/lib/modules/ipc/windows-acl.js +275 -0
  92. package/dist/lib/modules/vendor/zod/index.js +7 -0
  93. package/dist/lib/modules/vendor/zod/v4/classic/checks.js +1 -0
  94. package/dist/lib/modules/vendor/zod/v4/classic/coerce.js +17 -0
  95. package/dist/lib/modules/vendor/zod/v4/classic/compat.js +31 -0
  96. package/dist/lib/modules/vendor/zod/v4/classic/deep-partial.js +13 -0
  97. package/dist/lib/modules/vendor/zod/v4/classic/errors.js +57 -0
  98. package/dist/lib/modules/vendor/zod/v4/classic/external.js +17 -0
  99. package/dist/lib/modules/vendor/zod/v4/classic/from-json-schema.js +881 -0
  100. package/dist/lib/modules/vendor/zod/v4/classic/in-out.js +36 -0
  101. package/dist/lib/modules/vendor/zod/v4/classic/iso.js +15 -0
  102. package/dist/lib/modules/vendor/zod/v4/classic/parse.js +16 -0
  103. package/dist/lib/modules/vendor/zod/v4/classic/schemas.js +1535 -0
  104. package/dist/lib/modules/vendor/zod/v4/core/api.js +1107 -0
  105. package/dist/lib/modules/vendor/zod/v4/core/checks.js +516 -0
  106. package/dist/lib/modules/vendor/zod/v4/core/compile.js +1866 -0
  107. package/dist/lib/modules/vendor/zod/v4/core/core.js +141 -0
  108. package/dist/lib/modules/vendor/zod/v4/core/doc.js +38 -0
  109. package/dist/lib/modules/vendor/zod/v4/core/errors.js +267 -0
  110. package/dist/lib/modules/vendor/zod/v4/core/index.js +19 -0
  111. package/dist/lib/modules/vendor/zod/v4/core/json-schema-generator.js +99 -0
  112. package/dist/lib/modules/vendor/zod/v4/core/json-schema-processors.js +843 -0
  113. package/dist/lib/modules/vendor/zod/v4/core/json-schema.js +1 -0
  114. package/dist/lib/modules/vendor/zod/v4/core/memoizer.js +311 -0
  115. package/dist/lib/modules/vendor/zod/v4/core/parse.js +175 -0
  116. package/dist/lib/modules/vendor/zod/v4/core/regexes.js +156 -0
  117. package/dist/lib/modules/vendor/zod/v4/core/registries.js +51 -0
  118. package/dist/lib/modules/vendor/zod/v4/core/schemas.js +2636 -0
  119. package/dist/lib/modules/vendor/zod/v4/core/to-json-schema.js +651 -0
  120. package/dist/lib/modules/vendor/zod/v4/core/util.js +873 -0
  121. package/dist/lib/modules/vendor/zod/v4/core/versions.js +5 -0
  122. package/dist/lib/modules/vendor/zod/v4/core/visit.js +169 -0
  123. package/dist/lib/modules/vendor/zod/v4/locales/ar.js +111 -0
  124. package/dist/lib/modules/vendor/zod/v4/locales/az.js +110 -0
  125. package/dist/lib/modules/vendor/zod/v4/locales/be.js +168 -0
  126. package/dist/lib/modules/vendor/zod/v4/locales/bg.js +125 -0
  127. package/dist/lib/modules/vendor/zod/v4/locales/bn.js +113 -0
  128. package/dist/lib/modules/vendor/zod/v4/locales/ca.js +112 -0
  129. package/dist/lib/modules/vendor/zod/v4/locales/ckb.js +133 -0
  130. package/dist/lib/modules/vendor/zod/v4/locales/cs.js +116 -0
  131. package/dist/lib/modules/vendor/zod/v4/locales/da.js +120 -0
  132. package/dist/lib/modules/vendor/zod/v4/locales/de.js +113 -0
  133. package/dist/lib/modules/vendor/zod/v4/locales/el.js +112 -0
  134. package/dist/lib/modules/vendor/zod/v4/locales/en.js +125 -0
  135. package/dist/lib/modules/vendor/zod/v4/locales/eo.js +114 -0
  136. package/dist/lib/modules/vendor/zod/v4/locales/es.js +136 -0
  137. package/dist/lib/modules/vendor/zod/v4/locales/fa.js +119 -0
  138. package/dist/lib/modules/vendor/zod/v4/locales/fi.js +117 -0
  139. package/dist/lib/modules/vendor/zod/v4/locales/fr-CA.js +112 -0
  140. package/dist/lib/modules/vendor/zod/v4/locales/fr.js +129 -0
  141. package/dist/lib/modules/vendor/zod/v4/locales/gu.js +113 -0
  142. package/dist/lib/modules/vendor/zod/v4/locales/he.js +221 -0
  143. package/dist/lib/modules/vendor/zod/v4/locales/hi.js +111 -0
  144. package/dist/lib/modules/vendor/zod/v4/locales/hr.js +126 -0
  145. package/dist/lib/modules/vendor/zod/v4/locales/hu.js +113 -0
  146. package/dist/lib/modules/vendor/zod/v4/locales/hy.js +158 -0
  147. package/dist/lib/modules/vendor/zod/v4/locales/id.js +111 -0
  148. package/dist/lib/modules/vendor/zod/v4/locales/index.js +63 -0
  149. package/dist/lib/modules/vendor/zod/v4/locales/is.js +114 -0
  150. package/dist/lib/modules/vendor/zod/v4/locales/it.js +113 -0
  151. package/dist/lib/modules/vendor/zod/v4/locales/ja.js +112 -0
  152. package/dist/lib/modules/vendor/zod/v4/locales/ka.js +117 -0
  153. package/dist/lib/modules/vendor/zod/v4/locales/kh.js +5 -0
  154. package/dist/lib/modules/vendor/zod/v4/locales/km.js +115 -0
  155. package/dist/lib/modules/vendor/zod/v4/locales/kn.js +116 -0
  156. package/dist/lib/modules/vendor/zod/v4/locales/ko.js +116 -0
  157. package/dist/lib/modules/vendor/zod/v4/locales/lt.js +207 -0
  158. package/dist/lib/modules/vendor/zod/v4/locales/mk.js +114 -0
  159. package/dist/lib/modules/vendor/zod/v4/locales/ms.js +112 -0
  160. package/dist/lib/modules/vendor/zod/v4/locales/ne.js +111 -0
  161. package/dist/lib/modules/vendor/zod/v4/locales/nl.js +115 -0
  162. package/dist/lib/modules/vendor/zod/v4/locales/nn.js +113 -0
  163. package/dist/lib/modules/vendor/zod/v4/locales/no.js +113 -0
  164. package/dist/lib/modules/vendor/zod/v4/locales/ota.js +114 -0
  165. package/dist/lib/modules/vendor/zod/v4/locales/pl.js +114 -0
  166. package/dist/lib/modules/vendor/zod/v4/locales/ps.js +119 -0
  167. package/dist/lib/modules/vendor/zod/v4/locales/pt-BR.js +143 -0
  168. package/dist/lib/modules/vendor/zod/v4/locales/pt.js +142 -0
  169. package/dist/lib/modules/vendor/zod/v4/locales/ro.js +122 -0
  170. package/dist/lib/modules/vendor/zod/v4/locales/ru.js +168 -0
  171. package/dist/lib/modules/vendor/zod/v4/locales/sk.js +116 -0
  172. package/dist/lib/modules/vendor/zod/v4/locales/sl.js +114 -0
  173. package/dist/lib/modules/vendor/zod/v4/locales/sv.js +115 -0
  174. package/dist/lib/modules/vendor/zod/v4/locales/ta.js +115 -0
  175. package/dist/lib/modules/vendor/zod/v4/locales/tg.js +117 -0
  176. package/dist/lib/modules/vendor/zod/v4/locales/th.js +115 -0
  177. package/dist/lib/modules/vendor/zod/v4/locales/tk.js +107 -0
  178. package/dist/lib/modules/vendor/zod/v4/locales/tr.js +110 -0
  179. package/dist/lib/modules/vendor/zod/v4/locales/ua.js +5 -0
  180. package/dist/lib/modules/vendor/zod/v4/locales/uk.js +113 -0
  181. package/dist/lib/modules/vendor/zod/v4/locales/ur.js +115 -0
  182. package/dist/lib/modules/vendor/zod/v4/locales/uz.js +113 -0
  183. package/dist/lib/modules/vendor/zod/v4/locales/vi.js +113 -0
  184. package/dist/lib/modules/vendor/zod/v4/locales/yo.js +112 -0
  185. package/dist/lib/modules/vendor/zod/v4/locales/zh-CN.js +114 -0
  186. package/dist/lib/modules/vendor/zod/v4/locales/zh-TW.js +112 -0
  187. package/dist/lib/testing-jelly-id.js +1219 -0
  188. package/dist/lib/zod.js +1 -0
  189. package/dist/spec/messaging-v1.md +3744 -0
  190. package/dist/third-party-notices.md +239 -0
  191. package/dist/third-party.json +88 -0
  192. package/dist/types/core/address.d.ts +16 -0
  193. package/dist/types/core/agent/operations.d.ts +129 -0
  194. package/dist/types/core/agent/present.d.ts +57 -0
  195. package/dist/types/core/agent/text.d.ts +62 -0
  196. package/dist/types/core/agent-tools.d.ts +966 -0
  197. package/dist/types/core/b64u.d.ts +6 -0
  198. package/dist/types/core/cards.d.ts +21 -0
  199. package/dist/types/core/check.d.ts +10 -0
  200. package/dist/types/core/consent.d.ts +132 -0
  201. package/dist/types/core/contact-card.d.ts +34 -0
  202. package/dist/types/core/conversation/context.d.ts +56 -0
  203. package/dist/types/core/conversation/event.d.ts +36 -0
  204. package/dist/types/core/conversation/ids.d.ts +18 -0
  205. package/dist/types/core/conversation/link.d.ts +16 -0
  206. package/dist/types/core/conversation/log.d.ts +142 -0
  207. package/dist/types/core/conversation/ratchet.d.ts +95 -0
  208. package/dist/types/core/conversation/seal.d.ts +47 -0
  209. package/dist/types/core/delegation.d.ts +71 -0
  210. package/dist/types/core/directory/device-log.d.ts +119 -0
  211. package/dist/types/core/directory/epoch.d.ts +53 -0
  212. package/dist/types/core/directory/handle.d.ts +11 -0
  213. package/dist/types/core/directory/jelly-device.d.ts +88 -0
  214. package/dist/types/core/directory/jelly-issuer.d.ts +8 -0
  215. package/dist/types/core/directory/jelly-token.d.ts +116 -0
  216. package/dist/types/core/directory/keys-statement.d.ts +22 -0
  217. package/dist/types/core/directory/link.d.ts +36 -0
  218. package/dist/types/core/directory/merkle-log.d.ts +40 -0
  219. package/dist/types/core/directory/person-view.d.ts +90 -0
  220. package/dist/types/core/directory/person.d.ts +8 -0
  221. package/dist/types/core/directory/realm.d.ts +23 -0
  222. package/dist/types/core/directory/smt.d.ts +52 -0
  223. package/dist/types/core/envelope.d.ts +68 -0
  224. package/dist/types/core/hash.d.ts +2 -0
  225. package/dist/types/core/head.d.ts +34 -0
  226. package/dist/types/core/identity.d.ts +35 -0
  227. package/dist/types/core/index.d.ts +45 -0
  228. package/dist/types/core/jcs.d.ts +12 -0
  229. package/dist/types/core/jelly-host-double.d.ts +21 -0
  230. package/dist/types/core/jelly-host.d.ts +86 -0
  231. package/dist/types/core/keys.d.ts +16 -0
  232. package/dist/types/core/peer-text.d.ts +87 -0
  233. package/dist/types/core/realm.d.ts +36 -0
  234. package/dist/types/core/roster.d.ts +146 -0
  235. package/dist/types/core/schema/a2a.d.ts +251 -0
  236. package/dist/types/core/schema/common.d.ts +328 -0
  237. package/dist/types/core/schema/consent.d.ts +140 -0
  238. package/dist/types/core/schema/conversation.d.ts +1657 -0
  239. package/dist/types/core/schema/delegation.d.ts +58 -0
  240. package/dist/types/core/schema/directory-frames.d.ts +1301 -0
  241. package/dist/types/core/schema/directory.d.ts +375 -0
  242. package/dist/types/core/schema/envelope.d.ts +756 -0
  243. package/dist/types/core/schema/errors.d.ts +142 -0
  244. package/dist/types/core/schema/index.d.ts +16 -0
  245. package/dist/types/core/schema/ipc/consent.d.ts +555 -0
  246. package/dist/types/core/schema/ipc/device.d.ts +735 -0
  247. package/dist/types/core/schema/ipc/jelly.d.ts +501 -0
  248. package/dist/types/core/schema/ipc/shapes.d.ts +414 -0
  249. package/dist/types/core/schema/ipc/surfaces.d.ts +680 -0
  250. package/dist/types/core/schema/ipc.d.ts +3146 -0
  251. package/dist/types/core/schema/jelly.d.ts +160 -0
  252. package/dist/types/core/schema/realm.d.ts +38 -0
  253. package/dist/types/core/schema/relay.d.ts +1134 -0
  254. package/dist/types/core/schema/roster.d.ts +347 -0
  255. package/dist/types/core/sign.d.ts +13 -0
  256. package/dist/types/core/task.d.ts +6 -0
  257. package/dist/types/core/testing.d.ts +19 -0
  258. package/dist/types/core/ulid.d.ts +5 -0
  259. package/dist/types/core/version.d.ts +6 -0
  260. package/dist/types/core/wire.d.ts +14 -0
  261. package/dist/types/ipc/client.d.ts +85 -0
  262. package/dist/types/ipc/framing.d.ts +12 -0
  263. package/dist/types/ipc/index.d.ts +6 -0
  264. package/dist/types/ipc/paths.d.ts +39 -0
  265. package/dist/types/ipc/pipes.d.ts +16 -0
  266. package/dist/types/ipc/rpc.d.ts +13 -0
  267. package/dist/types/ipc/server.d.ts +94 -0
  268. package/dist/types/ipc/testing.d.ts +89 -0
  269. package/dist/types/ipc/trust.d.ts +49 -0
  270. package/dist/types/ipc/windows-acl.d.ts +56 -0
  271. package/dist/types/testing/jelly-id.d.ts +518 -0
  272. package/dist/types/zod.d.ts +1 -0
  273. package/package.json +77 -4
@@ -0,0 +1,317 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { chmod, chown, rm, stat } from "node:fs/promises";
3
+ import net from "node:net";
4
+ import { dirname } from "node:path";
5
+ import { AGENT_METHODS, IPC_METHODS, IPC_PROTOCOL, IpcEvent, VERSION, LIMITS, MESSAGING_ERROR_RPC_CODE, MessagingError, parseWire, } from "../core/index.js";
6
+ import { encodeLine, LineDecoder, LineTooLongError } from "./framing.js";
7
+ import { IS_WINDOWS, MAX_SOCKET_PATH_BYTES } from "./paths.js";
8
+ import { newPipeName, readPointer, writePointer } from "./pipes.js";
9
+ import { RPC } from "./rpc.js";
10
+ import { checkPrivateDir, ensurePrivateDir, secureCreated } from "./trust.js";
11
+ const MAX_BUFFERED_BYTES = 8 * 1024 * 1024;
12
+ const AGENT_TIER = new Set(AGENT_METHODS);
13
+ /** Connection ids are process-wide: the daemon keys session bindings by them across both sockets. */
14
+ let nextConnectionId = 1;
15
+ /** Why the `agent` tier refuses a method it does not serve (spec §8.2, "Two sockets"). */
16
+ function agentTierRefusal(method) {
17
+ return `${method} is not served on the agent socket; people use it through the messaging CLI or the /messaging TUI`;
18
+ }
19
+ /**
20
+ * Starts one socket of the daemon API. Each connection first gets the hello (`IpcHello`), then
21
+ * answers to its requests. The socket's directory and its parent (`run/` and `$MESSAGING_HOME`)
22
+ * must be private: real directories owned by this user that no group or other user can write
23
+ * (`forbidden` otherwise); a missing one is created 0700. The caller owns their modes beyond that, so
24
+ * a deployment can let a group search them (spec §8.2). Refuses to start when another server answers
25
+ * on the socket; removes a stale socket file otherwise. The socket is 0600, or 0660 to `group`.
26
+ *
27
+ * On Windows (spec §8.2, "Windows") it listens on a fresh named pipe and writes `socketPath` as the pointer
28
+ * file that names it (`pipes.ts`); the directories are private by ACL, and a missing one is created private.
29
+ * `group` is refused there: Node cannot give a pipe to a group.
30
+ */
31
+ export async function createIpcServer(options) {
32
+ const path = options.socketPath;
33
+ if (options.tier !== "agent" && options.tier !== "admin")
34
+ throw new MessagingError("invalid_request", "an IPC server needs a tier: agent or admin");
35
+ if (options.group !== undefined && options.tier !== "agent" && options.authorize === undefined) {
36
+ throw new MessagingError("invalid_request", "only the agent socket may be shared with a group, unless every admin call needs a grant");
37
+ }
38
+ if (options.group !== undefined && IS_WINDOWS)
39
+ throw new MessagingError("invalid_request", "an agent socket shared with a group is not supported on Windows");
40
+ if (!IS_WINDOWS && Buffer.byteLength(path) > MAX_SOCKET_PATH_BYTES) {
41
+ throw new MessagingError("invalid_request", `socket path is longer than ${MAX_SOCKET_PATH_BYTES} bytes`);
42
+ }
43
+ const run = dirname(path);
44
+ const home = dirname(run);
45
+ const createdHome = mkdirSync(home, { recursive: true, mode: 0o700 }) !== undefined;
46
+ if (IS_WINDOWS && createdHome)
47
+ secureCreated(home, "the Messaging home directory");
48
+ checkPrivateDir(home, "the Messaging home directory", options.runParentOwners);
49
+ ensurePrivateDir(run, "the Messaging run directory");
50
+ await (IS_WINDOWS ? removeStalePointer(path) : removeStaleSocket(path));
51
+ // On Windows clients reach a pipe of a fresh name; `path` then becomes the pointer that names it.
52
+ const listenPath = IS_WINDOWS ? newPipeName(options.tier) : path;
53
+ const contexts = new Map();
54
+ // Every connection's first line (spec §8.2): which daemon this is, before any request.
55
+ const hello = {
56
+ jsonrpc: "2.0",
57
+ method: "hello",
58
+ params: { daemon: "messagingd", version: options.version ?? VERSION, protocol: IPC_PROTOCOL, pid: process.pid, ...(options.deployment ? { deployment: options.deployment } : {}) },
59
+ };
60
+ const server = net.createServer((socket) => {
61
+ const ctx = new Context(nextConnectionId++, options.tier, socket);
62
+ contexts.set(socket, ctx);
63
+ ctx.send(hello);
64
+ const decoder = new LineDecoder(LIMITS.ipcLineBytes);
65
+ socket.setEncoding("utf8");
66
+ socket.on("data", (chunk) => {
67
+ let lines;
68
+ try {
69
+ lines = decoder.push(chunk);
70
+ }
71
+ catch (error) {
72
+ if (error instanceof LineTooLongError)
73
+ ctx.send({ jsonrpc: "2.0", id: null, error: { code: RPC.invalidRequest, message: error.message } });
74
+ socket.destroy();
75
+ return;
76
+ }
77
+ for (const line of lines)
78
+ void dispatch(line, ctx);
79
+ });
80
+ socket.on("error", () => socket.destroy());
81
+ socket.on("close", () => {
82
+ contexts.delete(socket);
83
+ ctx.abort();
84
+ options.onDisconnect?.(ctx);
85
+ });
86
+ });
87
+ async function dispatch(line, ctx) {
88
+ let request;
89
+ try {
90
+ request = parseWire(line, { maxBytes: LIMITS.ipcLineBytes });
91
+ }
92
+ catch {
93
+ ctx.send({ jsonrpc: "2.0", id: null, error: { code: RPC.parseError, message: "parse error" } });
94
+ return;
95
+ }
96
+ if (!isRequest(request)) {
97
+ ctx.send({ jsonrpc: "2.0", id: null, error: { code: RPC.invalidRequest, message: "invalid request" } });
98
+ return;
99
+ }
100
+ const { id, method } = request;
101
+ if (id === undefined)
102
+ return; // client notifications are not part of the API
103
+ const spec = Object.hasOwn(IPC_METHODS, method) ? IPC_METHODS[method] : undefined;
104
+ const handler = options.handlers[method];
105
+ if (!spec || (!handler && method !== "events.subscribe")) {
106
+ ctx.send({ jsonrpc: "2.0", id, error: { code: RPC.methodNotFound, message: `method not found: ${method}` } });
107
+ return;
108
+ }
109
+ // The tier is checked before the params are read, so the agent socket tells nothing about the rest.
110
+ if (options.tier === "agent" && !AGENT_TIER.has(method)) {
111
+ ctx.send({ jsonrpc: "2.0", id, error: toRpcError(new MessagingError("forbidden", agentTierRefusal(method))) });
112
+ return;
113
+ }
114
+ // A grant is checked before the params are read too: a caller without one learns nothing about them.
115
+ let authority;
116
+ try {
117
+ authority = options.authorize?.({ method: method, params: request.params, grant: typeof request.grant === "string" ? request.grant : undefined, ctx });
118
+ }
119
+ catch (error) {
120
+ ctx.send({ jsonrpc: "2.0", id, error: toRpcError(error) });
121
+ if (!(error instanceof MessagingError))
122
+ options.onError?.(method, error);
123
+ return;
124
+ }
125
+ const parsed = spec.params.safeParse(request.params ?? {});
126
+ if (!parsed.success) {
127
+ ctx.send({ jsonrpc: "2.0", id, error: { code: RPC.invalidParams, message: describeIssues(parsed.error.issues) } });
128
+ return;
129
+ }
130
+ try {
131
+ const result = handler ? await handler(parsed.data, ctx, authority) : { ok: true };
132
+ // Subscriptions start only once the handler has accepted the keys.
133
+ if (method === "events.subscribe")
134
+ for (const key of parsed.data.keys)
135
+ ctx.subscriptions.add(key);
136
+ const checked = spec.result.safeParse(result);
137
+ if (!checked.success)
138
+ throw new Error(`handler for ${method} returned an invalid result: ${describeIssues(checked.error.issues)}`);
139
+ if (!ctx.send({ jsonrpc: "2.0", id, result: checked.data })) {
140
+ ctx.send({ jsonrpc: "2.0", id, error: toRpcError(new MessagingError("too_large", `the ${method} result exceeds the IPC line limit`)) });
141
+ }
142
+ }
143
+ catch (error) {
144
+ ctx.send({ jsonrpc: "2.0", id, error: toRpcError(error) });
145
+ if (!(error instanceof MessagingError))
146
+ options.onError?.(method, error);
147
+ }
148
+ }
149
+ await new Promise((resolve, reject) => {
150
+ server.once("error", reject);
151
+ // bind(2) creates the socket file with the umask's mode; keep it closed to everyone else until the
152
+ // chmod below, so a group that can search `run/` never reaches a socket meant for one user.
153
+ const umask = IS_WINDOWS ? undefined : process.umask(0o177);
154
+ try {
155
+ server.listen(listenPath, () => {
156
+ server.off("error", reject);
157
+ resolve();
158
+ });
159
+ }
160
+ finally {
161
+ if (umask !== undefined)
162
+ process.umask(umask);
163
+ }
164
+ });
165
+ try {
166
+ if (IS_WINDOWS)
167
+ writePointer(path, { pipe: listenPath, pid: process.pid });
168
+ else {
169
+ if (options.group !== undefined)
170
+ await chown(path, process.geteuid?.() ?? -1, options.group);
171
+ await chmod(path, options.group === undefined ? 0o600 : 0o660);
172
+ }
173
+ }
174
+ catch (error) {
175
+ await new Promise((resolve) => server.close(() => resolve()));
176
+ await rm(path, { force: true });
177
+ if (options.group === undefined)
178
+ throw error;
179
+ throw new MessagingError("forbidden", `cannot give the agent socket to group ${options.group}: ${error.code ?? "error"}`);
180
+ }
181
+ return {
182
+ socketPath: path,
183
+ publish(event) {
184
+ const parsed = IpcEvent.parse(event);
185
+ for (const ctx of contexts.values()) {
186
+ if (ctx.subscriptions.size === 0)
187
+ continue;
188
+ if ("key" in parsed && !ctx.subscriptions.has(parsed.key))
189
+ continue;
190
+ if (!ctx.notify(parsed))
191
+ options.onError?.("event", new MessagingError("too_large", `a ${parsed.type} event exceeds the IPC line limit`));
192
+ }
193
+ },
194
+ connections: () => contexts.size,
195
+ async close() {
196
+ for (const socket of contexts.keys())
197
+ socket.destroy();
198
+ await new Promise((resolve) => server.close(() => resolve()));
199
+ if (IS_WINDOWS) {
200
+ // Only this server's pointer: never one a later server wrote.
201
+ let ours = false;
202
+ try {
203
+ ours = readPointer(path).pipe === listenPath;
204
+ }
205
+ catch {
206
+ ours = false;
207
+ }
208
+ if (ours)
209
+ await rm(path, { force: true });
210
+ }
211
+ else
212
+ await rm(path, { force: true });
213
+ },
214
+ };
215
+ }
216
+ class Context {
217
+ id;
218
+ tier;
219
+ socket;
220
+ subscriptions = new Set();
221
+ controller = new AbortController();
222
+ constructor(id, tier, socket) {
223
+ this.id = id;
224
+ this.tier = tier;
225
+ this.socket = socket;
226
+ }
227
+ get signal() {
228
+ return this.controller.signal;
229
+ }
230
+ abort() {
231
+ this.controller.abort();
232
+ }
233
+ /** Writes one line. Returns false, writing nothing, when the line would exceed `LIMITS.ipcLineBytes`: no peer could read it. */
234
+ send(message) {
235
+ const line = encodeLine(message);
236
+ if (Buffer.byteLength(line, "utf8") - 1 > LIMITS.ipcLineBytes)
237
+ return false;
238
+ if (this.socket.destroyed)
239
+ return true;
240
+ if (this.socket.writableLength > MAX_BUFFERED_BYTES) {
241
+ this.socket.destroy(); // a client that stops reading cannot grow daemon memory
242
+ return true;
243
+ }
244
+ this.socket.write(line);
245
+ return true;
246
+ }
247
+ notify(event) {
248
+ return this.send({ jsonrpc: "2.0", method: "event", params: event });
249
+ }
250
+ }
251
+ function isRequest(value) {
252
+ if (typeof value !== "object" || value === null)
253
+ return false;
254
+ const v = value;
255
+ return (v.jsonrpc === "2.0" &&
256
+ typeof v.method === "string" &&
257
+ (v.id === undefined || typeof v.id === "string" || typeof v.id === "number"));
258
+ }
259
+ function toRpcError(error) {
260
+ if (error instanceof MessagingError) {
261
+ return { code: MESSAGING_ERROR_RPC_CODE, message: error.message.slice(0, 500), data: { code: error.code } };
262
+ }
263
+ return { code: RPC.internalError, message: "internal error" };
264
+ }
265
+ /** Issue paths and messages only; never the offending values. */
266
+ function describeIssues(issues) {
267
+ return `invalid params: ${issues
268
+ .slice(0, 5)
269
+ .map((i) => `${i.path.map(String).join(".") || "(root)"}: ${i.message}`)
270
+ .join("; ")}`.slice(0, 500);
271
+ }
272
+ /** Windows: refuses when the pipe an existing pointer names answers; removes the pointer otherwise. */
273
+ async function removeStalePointer(path) {
274
+ const exists = await stat(path).then((s) => s, () => undefined);
275
+ if (!exists)
276
+ return;
277
+ if (!exists.isFile())
278
+ throw new MessagingError("invalid_request", "pipe file path exists and is not a file");
279
+ let pipe;
280
+ try {
281
+ pipe = readPointer(path).pipe;
282
+ }
283
+ catch {
284
+ pipe = undefined;
285
+ }
286
+ if (pipe !== undefined && (await answers(pipe)))
287
+ throw new MessagingError("invalid_request", "another daemon is already listening on this socket");
288
+ await rm(path, { force: true });
289
+ }
290
+ function answers(path) {
291
+ return new Promise((resolve) => {
292
+ const probe = net.connect(path);
293
+ probe.once("connect", () => {
294
+ probe.destroy();
295
+ resolve(true);
296
+ });
297
+ probe.once("error", () => resolve(false));
298
+ });
299
+ }
300
+ async function removeStaleSocket(path) {
301
+ const exists = await stat(path).then((s) => s, () => undefined);
302
+ if (!exists)
303
+ return;
304
+ if (!exists.isSocket())
305
+ throw new MessagingError("invalid_request", "socket path exists and is not a socket");
306
+ const live = await new Promise((resolve) => {
307
+ const probe = net.connect(path);
308
+ probe.once("connect", () => {
309
+ probe.destroy();
310
+ resolve(true);
311
+ });
312
+ probe.once("error", () => resolve(false));
313
+ });
314
+ if (live)
315
+ throw new MessagingError("invalid_request", "another daemon is already listening on this socket");
316
+ await rm(path, { force: true });
317
+ }
@@ -0,0 +1,145 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { IPC_METHODS, MessagingError } from "../core/index.js";
5
+ import { MessagingClient } from "./client.js";
6
+ import { IS_WINDOWS, MAX_SOCKET_PATH_BYTES, socketPath as socketOf } from "./paths.js";
7
+ import { createIpcServer } from "./server.js";
8
+ import { newPipeName, readPointer, writePointer } from "./pipes.js";
9
+ import { securePrivate } from "./windows-acl.js";
10
+ /**
11
+ * `@messaging/ipc/testing`: helpers for tests only. Production code never imports this entry point.
12
+ *
13
+ * A fresh home whose `tier` socket path (`run/agent.sock` by default) is short enough for macOS
14
+ * (falls back to /tmp when the OS temp dir is too long). On Windows: a private (by ACL) home in the temp
15
+ * directory, and its `run\<tier>.pipe`.
16
+ */
17
+ export function tempSocketPath(prefix = "mg-", tier = "agent") {
18
+ if (IS_WINDOWS) {
19
+ // No length limit for a pipe pointer; the home is made private by ACL, as messagingd makes its own.
20
+ const dir = mkdtempSync(join(tmpdir(), prefix));
21
+ securePrivate(dir, "a test home");
22
+ return { dir, socketPath: socketOf(dir, tier) };
23
+ }
24
+ for (const base of [tmpdir(), "/tmp"]) {
25
+ const dir = mkdtempSync(join(base, prefix));
26
+ const socketPath = socketOf(dir, tier);
27
+ if (Buffer.byteLength(socketOf(dir, "admin")) <= MAX_SOCKET_PATH_BYTES && Buffer.byteLength(socketPath) <= MAX_SOCKET_PATH_BYTES)
28
+ return { dir, socketPath };
29
+ }
30
+ throw new Error("no temp directory gives a short enough socket path");
31
+ }
32
+ /**
33
+ * Makes a bare `net.Server` listen where a client of `socketPath` will find it: on the socket itself, or on Windows
34
+ * on a fresh pipe that a pointer file at `socketPath` names, with `pid` (default this process's) as the pid the
35
+ * client will require in the hello. For impostors and other raw servers in tests.
36
+ */
37
+ export async function listenAt(server, socketPath, { pid = process.pid } = {}) {
38
+ const tier = /admin\.(?:sock|pipe)$/.test(socketPath) ? "admin" : "agent";
39
+ const target = IS_WINDOWS ? newPipeName(tier) : socketPath;
40
+ await new Promise((resolve, reject) => {
41
+ server.once("error", reject);
42
+ server.listen(target, () => {
43
+ server.off("error", reject);
44
+ resolve();
45
+ });
46
+ });
47
+ if (IS_WINDOWS)
48
+ writePointer(socketPath, { pipe: target, pid });
49
+ }
50
+ /** What `net.connect` takes to reach the endpoint at `socketPath`: the socket itself, or on Windows the pipe its pointer names. */
51
+ export function rawEndpoint(socketPath) {
52
+ return IS_WINDOWS ? readPointer(socketPath).pipe : socketPath;
53
+ }
54
+ /** A deterministic ULID for test fixtures: `ulid(7)` is `01JC0000000000000000000007`. */
55
+ export function ulid(n) {
56
+ return `01JC${String(n).padStart(22, "0")}`;
57
+ }
58
+ /** Polls `predicate` every 10 ms until it holds; throws after `timeoutMs`. */
59
+ export async function until(predicate, timeoutMs = 3_000, what = "condition") {
60
+ const deadline = Date.now() + timeoutMs;
61
+ while (!predicate()) {
62
+ if (Date.now() > deadline)
63
+ throw new Error(`timed out waiting for ${what}`);
64
+ await new Promise((resolve) => setTimeout(resolve, 10));
65
+ }
66
+ }
67
+ export async function recordingDaemon(options = {}) {
68
+ const owned = options.at === undefined;
69
+ const dir = options.at?.dir ?? tempSocketPath(options.prefix).dir;
70
+ const socketPath = socketOf(dir, "agent");
71
+ const adminSocketPath = socketOf(dir, "admin");
72
+ const handlers = { ...options.handlers };
73
+ const calls = [];
74
+ const convSubscribers = new Set();
75
+ const dispatch = {};
76
+ for (const method of Object.keys(IPC_METHODS)) {
77
+ dispatch[method] = async (params, ctx) => {
78
+ calls.push({ method, params: structuredClone(params) });
79
+ const handler = handlers[method];
80
+ if (!handler)
81
+ throw new MessagingError("forbidden", `fake daemon: no handler for ${method}`);
82
+ const result = await handler(params, ctx);
83
+ if (method === "conv.subscribe") {
84
+ convSubscribers.add(ctx);
85
+ ctx.signal.addEventListener("abort", () => convSubscribers.delete(ctx));
86
+ }
87
+ return result;
88
+ };
89
+ }
90
+ let servers;
91
+ const start = async () => {
92
+ if (servers)
93
+ return;
94
+ const version = options.version === undefined ? {} : { version: options.version };
95
+ const agent = await createIpcServer({ socketPath, tier: "agent", handlers: dispatch, ...version });
96
+ const admin = await createIpcServer({ socketPath: adminSocketPath, tier: "admin", handlers: dispatch, ...version }).catch(async (error) => {
97
+ await agent.close();
98
+ throw error;
99
+ });
100
+ servers = [agent, admin];
101
+ };
102
+ const stop = async () => {
103
+ const closing = servers ?? [];
104
+ servers = undefined;
105
+ for (const server of closing)
106
+ await server.close();
107
+ };
108
+ await start();
109
+ const callsTo = (method) => calls.filter((call) => call.method === method).map((call) => call.params);
110
+ return {
111
+ dir,
112
+ socketPath,
113
+ adminSocketPath,
114
+ calls,
115
+ methods: () => calls.map((call) => call.method),
116
+ callsTo,
117
+ last: (method) => callsTo(method).at(-1),
118
+ handle: (method, handler) => {
119
+ ;
120
+ handlers[method] = handler;
121
+ },
122
+ publish: (event) => {
123
+ for (const server of servers ?? [])
124
+ server.publish(event);
125
+ },
126
+ conv: (event) => {
127
+ for (const ctx of convSubscribers)
128
+ ctx.notify(event);
129
+ },
130
+ connect: (tier = "agent") => MessagingClient.connect({ socketPath: tier === "agent" ? socketPath : adminSocketPath, connectTimeoutMs: 500 }),
131
+ connections: () => (servers ?? []).reduce((sum, server) => sum + server.connections(), 0),
132
+ start,
133
+ stop,
134
+ restart: async (downMs = 50) => {
135
+ await stop();
136
+ await new Promise((resolve) => setTimeout(resolve, downMs));
137
+ await start();
138
+ },
139
+ close: async () => {
140
+ await stop();
141
+ if (owned)
142
+ rmSync(dir, { recursive: true, force: true });
143
+ },
144
+ };
145
+ }
@@ -0,0 +1,166 @@
1
+ import { constants, lstatSync, mkdirSync, openSync } from "node:fs";
2
+ import { MessagingError } from "../core/index.js";
3
+ import { IS_WINDOWS } from "./paths.js";
4
+ import { aclProblem, currentUserSid, readDacl, daclProblem, resolveSid, securePrivate, SID_OWNER_RIGHTS } from "./windows-acl.js";
5
+ /**
6
+ * Directory trust for `$MESSAGING_HOME` and `run/` (spec §8.1, §8.2). A directory is private when it is a
7
+ * real directory (not a symlink), owned by a trusted user, and writable by nobody else (no group or
8
+ * other write bit). Whoever can write a directory on the socket's path can replace the socket and
9
+ * capture every client, so the server refuses to listen and clients refuse to connect unless both hold.
10
+ *
11
+ * The trusted owners are this process's effective user, plus, for a client, the daemon's own uid when
12
+ * one is configured (`MESSAGING_DAEMON_UID`): a deployment can run `messagingd` under its own user, with the
13
+ * engine in the group of `run/agent.sock` (spec §8.2, "Two sockets"). A group may read and search a
14
+ * directory, never write it.
15
+ *
16
+ * On Windows there is no mode and no cheap owner: a directory is private when it is a real directory whose
17
+ * ACL passes `daclProblem` with a pinned owner (`windows-acl.ts`); `owners` does not apply.
18
+ */
19
+ export function checkPrivateDir(path, what, owners = selfOnly()) {
20
+ let stats;
21
+ try {
22
+ stats = lstatSync(path);
23
+ }
24
+ catch (error) {
25
+ if (error.code === "ENOENT")
26
+ throw new MessagingError("daemon_unavailable", `${what} does not exist`);
27
+ throw new MessagingError("forbidden", `cannot inspect ${what}`);
28
+ }
29
+ const problem = IS_WINDOWS ? windowsProblem(stats, path, "directory") : privacyProblem(stats, owners);
30
+ if (problem !== undefined)
31
+ throw new MessagingError("forbidden", `${what} is not private: it ${problem}`);
32
+ }
33
+ /**
34
+ * Creates `path` (mode 0700) when it is missing, then checks it as `checkPrivateDir` does. On Windows a
35
+ * directory it created that is not yet private (it inherited its parent's ACL) is made private
36
+ * (`securePrivate`); one that already existed is only checked.
37
+ */
38
+ export function ensurePrivateDir(path, what) {
39
+ let created = false;
40
+ try {
41
+ mkdirSync(path, { mode: 0o700 });
42
+ created = true;
43
+ }
44
+ catch (error) {
45
+ if (error.code !== "EEXIST")
46
+ throw new MessagingError("forbidden", `cannot create ${what}`);
47
+ }
48
+ if (IS_WINDOWS && created)
49
+ secureCreated(path, what);
50
+ checkPrivateDir(path, what);
51
+ }
52
+ /** Windows: makes a directory this process just created private, unless it already is (it inherited a private ACL). */
53
+ export function secureCreated(path, what) {
54
+ if (aclProblem(path, { pinnedOwner: true }) !== undefined)
55
+ securePrivate(path, what);
56
+ }
57
+ /**
58
+ * Windows: the daemon's `chmod 0700` of a home directory (spec §8.1). A directory it just created is made
59
+ * private. An existing one is refused (`forbidden`) when it is a symlink, not a directory, or writable by anyone
60
+ * but this user, SYSTEM or Administrators; otherwise it is tightened to this user alone with its owner pinned
61
+ * (`securePrivate`), unless it already is.
62
+ */
63
+ export function adoptPrivateDir(path, what, created) {
64
+ if (created) {
65
+ secureCreated(path, what);
66
+ return;
67
+ }
68
+ let stats;
69
+ try {
70
+ stats = lstatSync(path);
71
+ }
72
+ catch {
73
+ throw new MessagingError("forbidden", `cannot inspect ${what}`);
74
+ }
75
+ if (stats.isSymbolicLink())
76
+ throw new MessagingError("forbidden", `${what} is not private: it is a symbolic link`);
77
+ if (!stats.isDirectory())
78
+ throw new MessagingError("forbidden", `${what} is not private: it is not a directory`);
79
+ const self = currentUserSid();
80
+ const dacl = readDacl(path);
81
+ const problem = daclProblem(dacl, self, { pinnedOwner: false });
82
+ if (problem !== undefined)
83
+ throw new MessagingError("forbidden", `${what} is not private: it ${problem}`);
84
+ const exact = daclProblem(dacl, self, { pinnedOwner: true }) === undefined && (dacl?.aces ?? []).every((ace) => [self, SID_OWNER_RIGHTS].includes(resolveSid(ace.sid, self)));
85
+ if (!exact)
86
+ securePrivate(path, what);
87
+ }
88
+ /** Why `stats` is not a private directory owned by one of `owners` (default: this user), or undefined when it is. */
89
+ export function privacyProblem(stats, owners = selfOnly()) {
90
+ if (stats.isSymbolicLink())
91
+ return "is a symbolic link";
92
+ if (!stats.isDirectory())
93
+ return "is not a directory";
94
+ if (owners.length > 0 && !owners.includes(stats.uid))
95
+ return "is owned by another user";
96
+ if ((stats.mode & 0o022) !== 0)
97
+ return "is writable by group or others";
98
+ return undefined;
99
+ }
100
+ /** Windows: why `path` (with its `lstat`) is not a private directory or file, or undefined when it is. */
101
+ function windowsProblem(stats, path, kind) {
102
+ if (stats.isSymbolicLink())
103
+ return "is a symbolic link";
104
+ if (kind === "directory" ? !stats.isDirectory() : !stats.isFile())
105
+ return `is not a ${kind}`;
106
+ return aclProblem(path, { pinnedOwner: true });
107
+ }
108
+ /**
109
+ * The socket must be a socket owned by a trusted user (no symlink to someone else's). On Windows `path` is the
110
+ * pipe pointer file (`pipes.ts`): a regular file, not a symlink, private by `daclProblem` with a pinned owner.
111
+ */
112
+ export function checkSocket(path, owners = selfOnly()) {
113
+ let stats;
114
+ try {
115
+ stats = lstatSync(path);
116
+ }
117
+ catch (error) {
118
+ if (error.code === "ENOENT")
119
+ throw new MessagingError("daemon_unavailable", "the Messaging daemon socket does not exist");
120
+ throw new MessagingError("forbidden", "cannot inspect the Messaging daemon socket");
121
+ }
122
+ if (IS_WINDOWS) {
123
+ const problem = windowsProblem(stats, path, "file");
124
+ if (problem !== undefined)
125
+ throw new MessagingError("forbidden", `the Messaging daemon's pipe file is not private: it ${problem}`);
126
+ return;
127
+ }
128
+ if (!stats.isSocket())
129
+ throw new MessagingError("forbidden", "the Messaging daemon socket path is not a socket");
130
+ if (owners.length > 0 && !owners.includes(stats.uid))
131
+ throw new MessagingError("forbidden", "the Messaging daemon socket is owned by another user");
132
+ }
133
+ /**
134
+ * The users a client trusts to own the daemon's directories and socket: itself, and `daemonUid` when
135
+ * given. Empty (no owner check) without `geteuid`.
136
+ */
137
+ export function trustedOwners(daemonUid) {
138
+ const self = selfOnly();
139
+ if (self.length === 0)
140
+ return [];
141
+ return daemonUid === undefined || daemonUid === self[0] ? self : [...self, daemonUid];
142
+ }
143
+ function selfOnly() {
144
+ const euid = process.geteuid?.();
145
+ return euid === undefined ? [] : [euid];
146
+ }
147
+ /**
148
+ * `openSync(path, flags | O_NOFOLLOW, mode)`: refuses a symbolic link at `path` with `ELOOP`. Windows has no
149
+ * `O_NOFOLLOW`, so there the path is checked with `lstat` first (a symlink or junction is `ELOOP`), then opened.
150
+ * In a private directory only its owner could swap the path in between.
151
+ */
152
+ export function openNoFollow(path, flags, mode) {
153
+ if (!IS_WINDOWS)
154
+ return openSync(path, flags | constants.O_NOFOLLOW, mode);
155
+ let link = false;
156
+ try {
157
+ link = lstatSync(path).isSymbolicLink();
158
+ }
159
+ catch (error) {
160
+ if (error.code !== "ENOENT")
161
+ throw error;
162
+ }
163
+ if (link)
164
+ throw Object.assign(new Error(`ELOOP: a symbolic link, open '${path}'`), { code: "ELOOP" });
165
+ return openSync(path, flags, mode);
166
+ }