@phigon/observer 8.1.0 → 8.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/README.md +244 -18
  2. package/dist/bin/guardian-runner-darwin-amd64 +0 -0
  3. package/dist/bin/guardian-runner-darwin-arm64 +0 -0
  4. package/dist/bin/guardian-runner-linux-amd64 +0 -0
  5. package/dist/bin/guardian-runner-linux-arm64 +0 -0
  6. package/dist/bin/guardian-runner-windows-amd64.exe +0 -0
  7. package/dist/bin/guardian-runner-windows-arm64.exe +0 -0
  8. package/dist/src/abortable-delay.d.ts +10 -0
  9. package/dist/src/abortable-delay.d.ts.map +1 -0
  10. package/dist/src/abortable-delay.js +21 -0
  11. package/dist/src/abortable-delay.js.map +1 -0
  12. package/dist/src/action-policy.d.ts.map +1 -1
  13. package/dist/src/action-policy.js +4 -2
  14. package/dist/src/action-policy.js.map +1 -1
  15. package/dist/src/activity/adapters/claude.d.ts.map +1 -1
  16. package/dist/src/activity/adapters/claude.js +28 -2
  17. package/dist/src/activity/adapters/claude.js.map +1 -1
  18. package/dist/src/activity/adapters/cline-task.d.ts +7 -0
  19. package/dist/src/activity/adapters/cline-task.d.ts.map +1 -0
  20. package/dist/src/activity/adapters/cline-task.js +197 -0
  21. package/dist/src/activity/adapters/cline-task.js.map +1 -0
  22. package/dist/src/activity/adapters/cline.d.ts +3 -0
  23. package/dist/src/activity/adapters/cline.d.ts.map +1 -0
  24. package/dist/src/activity/adapters/cline.js +102 -0
  25. package/dist/src/activity/adapters/cline.js.map +1 -0
  26. package/dist/src/activity/adapters/codex.d.ts.map +1 -1
  27. package/dist/src/activity/adapters/codex.js +76 -0
  28. package/dist/src/activity/adapters/codex.js.map +1 -1
  29. package/dist/src/activity/adapters/copilot-chat.d.ts +3 -0
  30. package/dist/src/activity/adapters/copilot-chat.d.ts.map +1 -0
  31. package/dist/src/activity/adapters/copilot-chat.js +176 -0
  32. package/dist/src/activity/adapters/copilot-chat.js.map +1 -0
  33. package/dist/src/activity/adapters/copilot.d.ts +2 -1
  34. package/dist/src/activity/adapters/copilot.d.ts.map +1 -1
  35. package/dist/src/activity/adapters/copilot.js +173 -2
  36. package/dist/src/activity/adapters/copilot.js.map +1 -1
  37. package/dist/src/activity/adapters/cursor.d.ts +3 -0
  38. package/dist/src/activity/adapters/cursor.d.ts.map +1 -0
  39. package/dist/src/activity/adapters/cursor.js +86 -0
  40. package/dist/src/activity/adapters/cursor.js.map +1 -0
  41. package/dist/src/activity/adapters/factory-droid.d.ts +3 -0
  42. package/dist/src/activity/adapters/factory-droid.d.ts.map +1 -0
  43. package/dist/src/activity/adapters/factory-droid.js +126 -0
  44. package/dist/src/activity/adapters/factory-droid.js.map +1 -0
  45. package/dist/src/activity/adapters/gemini.d.ts +3 -0
  46. package/dist/src/activity/adapters/gemini.d.ts.map +1 -0
  47. package/dist/src/activity/adapters/gemini.js +177 -0
  48. package/dist/src/activity/adapters/gemini.js.map +1 -0
  49. package/dist/src/activity/adapters/goose.d.ts +3 -0
  50. package/dist/src/activity/adapters/goose.d.ts.map +1 -0
  51. package/dist/src/activity/adapters/goose.js +116 -0
  52. package/dist/src/activity/adapters/goose.js.map +1 -0
  53. package/dist/src/activity/adapters/kiro.d.ts +3 -0
  54. package/dist/src/activity/adapters/kiro.d.ts.map +1 -0
  55. package/dist/src/activity/adapters/kiro.js +47 -0
  56. package/dist/src/activity/adapters/kiro.js.map +1 -0
  57. package/dist/src/activity/adapters/openai-compatible.d.ts.map +1 -1
  58. package/dist/src/activity/adapters/openai-compatible.js +9 -1
  59. package/dist/src/activity/adapters/openai-compatible.js.map +1 -1
  60. package/dist/src/activity/adapters/opencode.d.ts.map +1 -1
  61. package/dist/src/activity/adapters/opencode.js +108 -1
  62. package/dist/src/activity/adapters/opencode.js.map +1 -1
  63. package/dist/src/activity/adapters/zed.d.ts +3 -0
  64. package/dist/src/activity/adapters/zed.d.ts.map +1 -0
  65. package/dist/src/activity/adapters/zed.js +134 -0
  66. package/dist/src/activity/adapters/zed.js.map +1 -0
  67. package/dist/src/activity/collector.d.ts +43 -1
  68. package/dist/src/activity/collector.d.ts.map +1 -1
  69. package/dist/src/activity/collector.js +490 -77
  70. package/dist/src/activity/collector.js.map +1 -1
  71. package/dist/src/activity/goose-sqlite-source.d.ts +33 -0
  72. package/dist/src/activity/goose-sqlite-source.d.ts.map +1 -0
  73. package/dist/src/activity/goose-sqlite-source.js +274 -0
  74. package/dist/src/activity/goose-sqlite-source.js.map +1 -0
  75. package/dist/src/activity/index.d.ts +3 -0
  76. package/dist/src/activity/index.d.ts.map +1 -1
  77. package/dist/src/activity/index.js +3 -0
  78. package/dist/src/activity/index.js.map +1 -1
  79. package/dist/src/activity/ingest.d.ts +9 -0
  80. package/dist/src/activity/ingest.d.ts.map +1 -1
  81. package/dist/src/activity/ingest.js +94 -4
  82. package/dist/src/activity/ingest.js.map +1 -1
  83. package/dist/src/activity/json-document-source.d.ts +151 -0
  84. package/dist/src/activity/json-document-source.d.ts.map +1 -0
  85. package/dist/src/activity/json-document-source.js +586 -0
  86. package/dist/src/activity/json-document-source.js.map +1 -0
  87. package/dist/src/activity/native-capabilities.d.ts.map +1 -1
  88. package/dist/src/activity/native-capabilities.js +8 -12
  89. package/dist/src/activity/native-capabilities.js.map +1 -1
  90. package/dist/src/activity/normalize.d.ts.map +1 -1
  91. package/dist/src/activity/normalize.js +64 -6
  92. package/dist/src/activity/normalize.js.map +1 -1
  93. package/dist/src/activity/opencode-message-tables.d.ts +87 -0
  94. package/dist/src/activity/opencode-message-tables.d.ts.map +1 -0
  95. package/dist/src/activity/opencode-message-tables.js +546 -0
  96. package/dist/src/activity/opencode-message-tables.js.map +1 -0
  97. package/dist/src/activity/opencode-sqlite-source.d.ts +7 -0
  98. package/dist/src/activity/opencode-sqlite-source.d.ts.map +1 -1
  99. package/dist/src/activity/opencode-sqlite-source.js +154 -63
  100. package/dist/src/activity/opencode-sqlite-source.js.map +1 -1
  101. package/dist/src/activity/source-reader.d.ts +60 -4
  102. package/dist/src/activity/source-reader.d.ts.map +1 -1
  103. package/dist/src/activity/source-reader.js +593 -75
  104. package/dist/src/activity/source-reader.js.map +1 -1
  105. package/dist/src/activity/sqlite-connections.d.ts +77 -0
  106. package/dist/src/activity/sqlite-connections.d.ts.map +1 -0
  107. package/dist/src/activity/sqlite-connections.js +220 -0
  108. package/dist/src/activity/sqlite-connections.js.map +1 -0
  109. package/dist/src/activity/zed-sqlite-source.d.ts +65 -0
  110. package/dist/src/activity/zed-sqlite-source.d.ts.map +1 -0
  111. package/dist/src/activity/zed-sqlite-source.js +547 -0
  112. package/dist/src/activity/zed-sqlite-source.js.map +1 -0
  113. package/dist/src/agent-action-executor.d.ts +18 -1
  114. package/dist/src/agent-action-executor.d.ts.map +1 -1
  115. package/dist/src/agent-action-executor.js +113 -58
  116. package/dist/src/agent-action-executor.js.map +1 -1
  117. package/dist/src/agentic-guardian.d.ts.map +1 -1
  118. package/dist/src/agentic-guardian.js +6 -5
  119. package/dist/src/agentic-guardian.js.map +1 -1
  120. package/dist/src/atomic-replace.d.ts +7 -0
  121. package/dist/src/atomic-replace.d.ts.map +1 -1
  122. package/dist/src/atomic-replace.js +28 -3
  123. package/dist/src/atomic-replace.js.map +1 -1
  124. package/dist/src/bridge-journal.d.ts +32 -0
  125. package/dist/src/bridge-journal.d.ts.map +1 -0
  126. package/dist/src/bridge-journal.js +113 -0
  127. package/dist/src/bridge-journal.js.map +1 -0
  128. package/dist/src/cli.d.ts +7 -1
  129. package/dist/src/cli.d.ts.map +1 -1
  130. package/dist/src/cli.js +534 -50
  131. package/dist/src/cli.js.map +1 -1
  132. package/dist/src/collection-notice.d.ts +34 -0
  133. package/dist/src/collection-notice.d.ts.map +1 -0
  134. package/dist/src/collection-notice.js +73 -0
  135. package/dist/src/collection-notice.js.map +1 -0
  136. package/dist/src/config.d.ts +39 -0
  137. package/dist/src/config.d.ts.map +1 -1
  138. package/dist/src/config.js +187 -36
  139. package/dist/src/config.js.map +1 -1
  140. package/dist/src/connection-config.d.ts.map +1 -1
  141. package/dist/src/connection-config.js +5 -0
  142. package/dist/src/connection-config.js.map +1 -1
  143. package/dist/src/content-redaction.d.ts +17 -1
  144. package/dist/src/content-redaction.d.ts.map +1 -1
  145. package/dist/src/content-redaction.js +51 -3
  146. package/dist/src/content-redaction.js.map +1 -1
  147. package/dist/src/credential-store.d.ts +20 -1
  148. package/dist/src/credential-store.d.ts.map +1 -1
  149. package/dist/src/credential-store.js +29 -4
  150. package/dist/src/credential-store.js.map +1 -1
  151. package/dist/src/custom-agent-registry.d.ts.map +1 -1
  152. package/dist/src/custom-agent-registry.js +2 -1
  153. package/dist/src/custom-agent-registry.js.map +1 -1
  154. package/dist/src/discovery/adapters/generic.d.ts.map +1 -1
  155. package/dist/src/discovery/adapters/generic.js +160 -2
  156. package/dist/src/discovery/adapters/generic.js.map +1 -1
  157. package/dist/src/discovery/engine.d.ts +7 -1
  158. package/dist/src/discovery/engine.d.ts.map +1 -1
  159. package/dist/src/discovery/engine.js +21 -16
  160. package/dist/src/discovery/engine.js.map +1 -1
  161. package/dist/src/discovery/known-agent.d.ts +7 -0
  162. package/dist/src/discovery/known-agent.d.ts.map +1 -1
  163. package/dist/src/discovery/known-agent.js +9 -4
  164. package/dist/src/discovery/known-agent.js.map +1 -1
  165. package/dist/src/discovery/system-evidence.d.ts +40 -1
  166. package/dist/src/discovery/system-evidence.d.ts.map +1 -1
  167. package/dist/src/discovery/system-evidence.js +189 -16
  168. package/dist/src/discovery/system-evidence.js.map +1 -1
  169. package/dist/src/doctor-check.d.ts +51 -0
  170. package/dist/src/doctor-check.d.ts.map +1 -0
  171. package/dist/src/doctor-check.js +174 -0
  172. package/dist/src/doctor-check.js.map +1 -0
  173. package/dist/src/enroll.d.ts +86 -0
  174. package/dist/src/enroll.d.ts.map +1 -0
  175. package/dist/src/enroll.js +245 -0
  176. package/dist/src/enroll.js.map +1 -0
  177. package/dist/src/entry.d.ts +3 -0
  178. package/dist/src/entry.d.ts.map +1 -0
  179. package/dist/src/entry.js +14 -0
  180. package/dist/src/entry.js.map +1 -0
  181. package/dist/src/event-queue.d.ts +13 -1
  182. package/dist/src/event-queue.d.ts.map +1 -1
  183. package/dist/src/event-queue.js +137 -42
  184. package/dist/src/event-queue.js.map +1 -1
  185. package/dist/src/event-spool.d.ts +61 -3
  186. package/dist/src/event-spool.d.ts.map +1 -1
  187. package/dist/src/event-spool.js +341 -44
  188. package/dist/src/event-spool.js.map +1 -1
  189. package/dist/src/generated/phigon/v1/identity_pb.d.ts +7 -0
  190. package/dist/src/generated/phigon/v1/identity_pb.d.ts.map +1 -1
  191. package/dist/src/generated/phigon/v1/identity_pb.js +1 -1
  192. package/dist/src/generated/phigon/v1/identity_pb.js.map +1 -1
  193. package/dist/src/guardian-containment.d.ts +2 -6
  194. package/dist/src/guardian-containment.d.ts.map +1 -1
  195. package/dist/src/guardian-containment.js +12 -63
  196. package/dist/src/guardian-containment.js.map +1 -1
  197. package/dist/src/guardian-decision-journal.d.ts.map +1 -1
  198. package/dist/src/guardian-decision-journal.js +4 -19
  199. package/dist/src/guardian-decision-journal.js.map +1 -1
  200. package/dist/src/guardian-managed-exec.d.ts.map +1 -1
  201. package/dist/src/guardian-managed-exec.js +6 -18
  202. package/dist/src/guardian-managed-exec.js.map +1 -1
  203. package/dist/src/guardian-model-bridge.d.ts.map +1 -1
  204. package/dist/src/guardian-model-bridge.js +2 -1
  205. package/dist/src/guardian-model-bridge.js.map +1 -1
  206. package/dist/src/guardian-native.d.ts +0 -1
  207. package/dist/src/guardian-native.d.ts.map +1 -1
  208. package/dist/src/guardian-native.js +0 -6
  209. package/dist/src/guardian-native.js.map +1 -1
  210. package/dist/src/guardian-process-monitor.d.ts +4 -0
  211. package/dist/src/guardian-process-monitor.d.ts.map +1 -1
  212. package/dist/src/guardian-process-monitor.js +82 -26
  213. package/dist/src/guardian-process-monitor.js.map +1 -1
  214. package/dist/src/health.d.ts +19 -1
  215. package/dist/src/health.d.ts.map +1 -1
  216. package/dist/src/health.js +22 -1
  217. package/dist/src/health.js.map +1 -1
  218. package/dist/src/historical-identity-store.d.ts +8 -1
  219. package/dist/src/historical-identity-store.d.ts.map +1 -1
  220. package/dist/src/historical-identity-store.js +28 -10
  221. package/dist/src/historical-identity-store.js.map +1 -1
  222. package/dist/src/index.d.ts +2 -0
  223. package/dist/src/index.d.ts.map +1 -1
  224. package/dist/src/index.js +2 -0
  225. package/dist/src/index.js.map +1 -1
  226. package/dist/src/installation.d.ts +2 -0
  227. package/dist/src/installation.d.ts.map +1 -1
  228. package/dist/src/installation.js +6 -3
  229. package/dist/src/installation.js.map +1 -1
  230. package/dist/src/instance-lock.d.ts +45 -0
  231. package/dist/src/instance-lock.d.ts.map +1 -0
  232. package/dist/src/instance-lock.js +243 -0
  233. package/dist/src/instance-lock.js.map +1 -0
  234. package/dist/src/machine-command-executor.d.ts +22 -7
  235. package/dist/src/machine-command-executor.d.ts.map +1 -1
  236. package/dist/src/machine-command-executor.js +80 -19
  237. package/dist/src/machine-command-executor.js.map +1 -1
  238. package/dist/src/node-version.d.ts +8 -0
  239. package/dist/src/node-version.d.ts.map +1 -0
  240. package/dist/src/node-version.js +17 -0
  241. package/dist/src/node-version.js.map +1 -0
  242. package/dist/src/observer-log.d.ts +50 -0
  243. package/dist/src/observer-log.d.ts.map +1 -0
  244. package/dist/src/observer-log.js +141 -0
  245. package/dist/src/observer-log.js.map +1 -0
  246. package/dist/src/pair.d.ts +68 -1
  247. package/dist/src/pair.d.ts.map +1 -1
  248. package/dist/src/pair.js +204 -65
  249. package/dist/src/pair.js.map +1 -1
  250. package/dist/src/private-directory.d.ts +10 -0
  251. package/dist/src/private-directory.d.ts.map +1 -0
  252. package/dist/src/private-directory.js +66 -0
  253. package/dist/src/private-directory.js.map +1 -0
  254. package/dist/src/process-tree.d.ts +73 -0
  255. package/dist/src/process-tree.d.ts.map +1 -0
  256. package/dist/src/process-tree.js +256 -0
  257. package/dist/src/process-tree.js.map +1 -0
  258. package/dist/src/proxy.d.ts +68 -0
  259. package/dist/src/proxy.d.ts.map +1 -0
  260. package/dist/src/proxy.js +209 -0
  261. package/dist/src/proxy.js.map +1 -0
  262. package/dist/src/purge.d.ts +17 -0
  263. package/dist/src/purge.d.ts.map +1 -0
  264. package/dist/src/purge.js +84 -0
  265. package/dist/src/purge.js.map +1 -0
  266. package/dist/src/remove-machine.d.ts +5 -1
  267. package/dist/src/remove-machine.d.ts.map +1 -1
  268. package/dist/src/remove-machine.js +28 -14
  269. package/dist/src/remove-machine.js.map +1 -1
  270. package/dist/src/server-authority.d.ts +29 -0
  271. package/dist/src/server-authority.d.ts.map +1 -0
  272. package/dist/src/server-authority.js +106 -0
  273. package/dist/src/server-authority.js.map +1 -0
  274. package/dist/src/server-clock.d.ts +32 -0
  275. package/dist/src/server-clock.d.ts.map +1 -0
  276. package/dist/src/server-clock.js +72 -0
  277. package/dist/src/server-clock.js.map +1 -0
  278. package/dist/src/service-command.d.ts +20 -0
  279. package/dist/src/service-command.d.ts.map +1 -0
  280. package/dist/src/service-command.js +136 -0
  281. package/dist/src/service-command.js.map +1 -0
  282. package/dist/src/service.d.ts +98 -0
  283. package/dist/src/service.d.ts.map +1 -0
  284. package/dist/src/service.js +472 -0
  285. package/dist/src/service.js.map +1 -0
  286. package/dist/src/stream-client.d.ts +21 -1
  287. package/dist/src/stream-client.d.ts.map +1 -1
  288. package/dist/src/stream-client.js +137 -24
  289. package/dist/src/stream-client.js.map +1 -1
  290. package/dist/src/stream-state.d.ts +2 -0
  291. package/dist/src/stream-state.d.ts.map +1 -1
  292. package/dist/src/stream-state.js +5 -2
  293. package/dist/src/stream-state.js.map +1 -1
  294. package/dist/src/system-inspection.d.ts +78 -0
  295. package/dist/src/system-inspection.d.ts.map +1 -0
  296. package/dist/src/system-inspection.js +281 -0
  297. package/dist/src/system-inspection.js.map +1 -0
  298. package/dist/src/update.d.ts.map +1 -1
  299. package/dist/src/update.js +9 -3
  300. package/dist/src/update.js.map +1 -1
  301. package/dist/src/user-notices.d.ts +64 -0
  302. package/dist/src/user-notices.d.ts.map +1 -0
  303. package/dist/src/user-notices.js +141 -0
  304. package/dist/src/user-notices.js.map +1 -0
  305. package/package.json +7 -7
  306. package/dist/src/windows-process.d.ts +0 -7
  307. package/dist/src/windows-process.d.ts.map +0 -1
  308. package/dist/src/windows-process.js +0 -18
  309. package/dist/src/windows-process.js.map +0 -1
@@ -0,0 +1,50 @@
1
+ import type { ObserverLogLevel } from "./config.js";
2
+ /** The log folder in the data directory, and the file the observer writes. */
3
+ export declare const logDirectoryName = "logs";
4
+ export declare const logFileName = "observer.log";
5
+ /** About 5 MB per file and three files: the current one and two older ones. */
6
+ export declare const defaultLogFileBytes: number;
7
+ export declare const defaultLogFiles = 3;
8
+ export declare function logFilePath(dataDirectory: string): string;
9
+ /**
10
+ * `PHIGON_LOG_LEVEL`, read before the rest of the configuration so that a
11
+ * configuration error is logged too. An invalid value logs at info; the
12
+ * configuration check then reports it.
13
+ */
14
+ export declare function logLevelFrom(value: string | undefined): ObserverLogLevel;
15
+ export type RotatingLogFileOptions = Readonly<{
16
+ maxBytes?: number;
17
+ files?: number;
18
+ now?: () => Date;
19
+ /** Told once when the file cannot be written; logging then stops. */
20
+ onFailure?: (message: string) => void;
21
+ }>;
22
+ /**
23
+ * A log file in the data directory that a background service keeps, since
24
+ * a scheduled task or LaunchAgent throws its output away. It is written
25
+ * synchronously, one short line at a time, and rotated by size, so it never
26
+ * holds anything in memory and never grows past `files` × `maxBytes`. It
27
+ * only ever receives messages that are already safe to print.
28
+ */
29
+ export declare class RotatingLogFile {
30
+ #private;
31
+ constructor(dataDirectory: string, options?: RotatingLogFileOptions);
32
+ get path(): string;
33
+ write(level: ObserverLogLevel, message: string): void;
34
+ }
35
+ export type LogOutput = Readonly<{
36
+ stdout: (value: string) => void;
37
+ stderr: (value: string) => void;
38
+ }>;
39
+ /** `LogOutput` that also takes errors and debug details. */
40
+ export type LevelledOutput = LogOutput & Readonly<{
41
+ error: (value: string) => void;
42
+ debug: (value: string) => void;
43
+ }>;
44
+ /**
45
+ * Applies `PHIGON_LOG_LEVEL` to what a command prints and copies each
46
+ * message it keeps to the log file: status lines are info, problems on
47
+ * stderr are warnings, and the message a command fails with is an error.
48
+ */
49
+ export declare function levelledOutput(output: LogOutput, level: ObserverLogLevel, log?: RotatingLogFile): LevelledOutput;
50
+ //# sourceMappingURL=observer-log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observer-log.d.ts","sourceRoot":"","sources":["../../src/observer-log.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB,SAAS,CAAC;AACvC,eAAO,MAAM,WAAW,iBAAiB,CAAC;AAC1C,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,QAAkB,CAAC;AACnD,eAAO,MAAM,eAAe,IAAI,CAAC;AAUjC,wBAAgB,WAAW,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,CAExE;AAED,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,qEAAqE;IACrE,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,qBAAa,eAAe;;gBAUd,aAAa,EAAE,MAAM,EAAE,OAAO,GAAE,sBAA2B;IASvE,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,KAAK,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;CAsDtD;AASD,MAAM,MAAM,SAAS,GAAG,QAAQ,CAAC;IAC/B,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC,CAAC,CAAC;AAEH,4DAA4D;AAC5D,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,QAAQ,CAAC;IAChD,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC,CAAC,CAAC;AAEH;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,gBAAgB,EACvB,GAAG,CAAC,EAAE,eAAe,GACpB,cAAc,CAchB"}
@@ -0,0 +1,141 @@
1
+ import { appendFileSync, existsSync, mkdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { atomicReplaceSync } from "./atomic-replace.js";
4
+ import { makeDirectoryPrivateSync } from "./private-directory.js";
5
+ /** The log folder in the data directory, and the file the observer writes. */
6
+ export const logDirectoryName = "logs";
7
+ export const logFileName = "observer.log";
8
+ /** About 5 MB per file and three files: the current one and two older ones. */
9
+ export const defaultLogFileBytes = 5 * 1024 * 1024;
10
+ export const defaultLogFiles = 3;
11
+ const maxLineBytes = 4 * 1024;
12
+ const severity = Object.freeze({
13
+ debug: 10,
14
+ info: 20,
15
+ warn: 30,
16
+ error: 40,
17
+ });
18
+ export function logFilePath(dataDirectory) {
19
+ return join(dataDirectory, logDirectoryName, logFileName);
20
+ }
21
+ /**
22
+ * `PHIGON_LOG_LEVEL`, read before the rest of the configuration so that a
23
+ * configuration error is logged too. An invalid value logs at info; the
24
+ * configuration check then reports it.
25
+ */
26
+ export function logLevelFrom(value) {
27
+ return value === "debug" || value === "warn" || value === "error" ? value : "info";
28
+ }
29
+ /**
30
+ * A log file in the data directory that a background service keeps, since
31
+ * a scheduled task or LaunchAgent throws its output away. It is written
32
+ * synchronously, one short line at a time, and rotated by size, so it never
33
+ * holds anything in memory and never grows past `files` × `maxBytes`. It
34
+ * only ever receives messages that are already safe to print.
35
+ */
36
+ export class RotatingLogFile {
37
+ #dataDirectory;
38
+ #path;
39
+ #maxBytes;
40
+ #files;
41
+ #now;
42
+ #onFailure;
43
+ #size;
44
+ #failed = false;
45
+ constructor(dataDirectory, options = {}) {
46
+ this.#dataDirectory = dataDirectory;
47
+ this.#path = logFilePath(dataDirectory);
48
+ this.#maxBytes = options.maxBytes ?? defaultLogFileBytes;
49
+ this.#files = Math.max(1, options.files ?? defaultLogFiles);
50
+ this.#now = options.now ?? (() => new Date());
51
+ this.#onFailure = options.onFailure;
52
+ }
53
+ get path() {
54
+ return this.#path;
55
+ }
56
+ write(level, message) {
57
+ if (this.#failed)
58
+ return;
59
+ const stamp = this.#now().toISOString();
60
+ const label = level.toUpperCase().padEnd(5);
61
+ const text = message
62
+ .split(/\r?\n/u)
63
+ .map(clean)
64
+ .filter((line) => line.length > 0)
65
+ .map((line) => `${stamp} ${label} ${line}\n`)
66
+ .join("");
67
+ if (text.length === 0)
68
+ return;
69
+ try {
70
+ if (this.#size === undefined)
71
+ this.#size = this.#open();
72
+ const bytes = Buffer.byteLength(text, "utf8");
73
+ if (this.#size > 0 && this.#size + bytes > this.#maxBytes) {
74
+ this.#rotate();
75
+ this.#size = 0;
76
+ }
77
+ appendFileSync(this.#path, text, { encoding: "utf8", mode: 0o600 });
78
+ this.#size += bytes;
79
+ }
80
+ catch {
81
+ // The observer keeps running without its log; say so once.
82
+ this.#failed = true;
83
+ this.#onFailure?.(`The observer log could not be written to ${this.#path}; messages are no longer logged there.\n`);
84
+ }
85
+ }
86
+ #open() {
87
+ const directory = join(this.#dataDirectory, logDirectoryName);
88
+ if (!existsSync(this.#dataDirectory)) {
89
+ mkdirSync(this.#dataDirectory, { recursive: true, mode: 0o700 });
90
+ }
91
+ makeDirectoryPrivateSync(this.#dataDirectory);
92
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
93
+ try {
94
+ return statSync(this.#path).size;
95
+ }
96
+ catch (error) {
97
+ if (error.code === "ENOENT")
98
+ return 0;
99
+ throw error;
100
+ }
101
+ }
102
+ #rotate() {
103
+ for (let index = this.#files - 1; index >= 1; index--) {
104
+ const from = index === 1 ? this.#path : `${this.#path}.${index - 1}`;
105
+ if (existsSync(from))
106
+ atomicReplaceSync(from, `${this.#path}.${index}`);
107
+ }
108
+ // With a single file there is nothing to keep: start it again.
109
+ if (this.#files === 1 && existsSync(this.#path)) {
110
+ appendFileSync(this.#path, "", { flag: "w" });
111
+ }
112
+ }
113
+ }
114
+ /** Drops control characters, such as terminal colour codes, and caps the length. */
115
+ function clean(line) {
116
+ const printable = line.replace(/[\u0000-\u0008\u000b-\u001f\u007f\u009b]/gu, "").trimEnd();
117
+ if (Buffer.byteLength(printable, "utf8") <= maxLineBytes)
118
+ return printable;
119
+ return `${Buffer.from(printable, "utf8").subarray(0, maxLineBytes).toString("utf8").replace(/�$/u, "")}…`;
120
+ }
121
+ /**
122
+ * Applies `PHIGON_LOG_LEVEL` to what a command prints and copies each
123
+ * message it keeps to the log file: status lines are info, problems on
124
+ * stderr are warnings, and the message a command fails with is an error.
125
+ */
126
+ export function levelledOutput(output, level, log) {
127
+ const emit = (messageLevel, write) => (value) => {
128
+ if (severity[messageLevel] < severity[level])
129
+ return;
130
+ // The log first: a background service's console may already be gone.
131
+ log?.write(messageLevel, value);
132
+ write(value);
133
+ };
134
+ return Object.freeze({
135
+ stdout: emit("info", output.stdout),
136
+ stderr: emit("warn", output.stderr),
137
+ error: emit("error", output.stderr),
138
+ debug: emit("debug", output.stdout),
139
+ });
140
+ }
141
+ //# sourceMappingURL=observer-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observer-log.js","sourceRoot":"","sources":["../../src/observer-log.ts"],"names":[],"mappings":"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"}
@@ -1,14 +1,43 @@
1
1
  import type { ObserverConfig } from "./config.js";
2
- import { type CredentialStore } from "./credential-store.js";
2
+ import { type CredentialStore, type StoredMachineCredential } from "./credential-store.js";
3
3
  export declare const OBSERVER_CONSENT_POLICY_VERSION = "observer-policy-v1";
4
4
  export type ConsentProfile = "metadata" | "full";
5
+ export type PairingMachine = Readonly<{
6
+ id: string;
7
+ tenant_id: string;
8
+ display_name: string;
9
+ state: string;
10
+ }>;
11
+ /** What the pairing exchange and an enrollment both answer with. */
12
+ export type ConnectedMachine = Readonly<{
13
+ machine: PairingMachine;
14
+ credential: string;
15
+ }>;
5
16
  export type PairResult = Readonly<{
6
17
  machineId: string;
7
18
  displayName: string;
8
19
  state: string;
9
20
  controlPlaneOrigin: string;
10
21
  credentialStored: true;
22
+ /**
23
+ * The earlier machine this computer was paired as, when the workspace no
24
+ * longer accepted its key and the computer was paired as a new machine.
25
+ */
26
+ replacedMachineId?: string;
27
+ }>;
28
+ /**
29
+ * One exchange with the control plane. A failure other than a stored key
30
+ * the server refused is `credentialRejected: false`, and its error is
31
+ * thrown as is.
32
+ */
33
+ export type ExchangeOutcome<T extends ConnectedMachine> = Readonly<{
34
+ paired: T;
35
+ }> | Readonly<{
36
+ error: Error;
37
+ credentialRejected: boolean;
11
38
  }>;
39
+ /** The consent evidence a connection stores with the machine credential. */
40
+ export type ConsentEvidence = Pick<StoredMachineCredential, "consentProfile" | "consentPolicyVersion" | "consentedAt" | "consentSource" | "enrollmentCodeId" | "enrollmentCodeLabel">;
12
41
  export type PairOptions = Readonly<{
13
42
  config: ObserverConfig;
14
43
  code: string;
@@ -18,4 +47,42 @@ export type PairOptions = Readonly<{
18
47
  fetcher?: typeof fetch;
19
48
  }>;
20
49
  export declare function pairMachine(options: PairOptions): Promise<PairResult>;
50
+ export type ConnectOptions<T extends ConnectedMachine> = Readonly<{
51
+ config: ObserverConfig;
52
+ endpoint: URL;
53
+ fetcher: typeof fetch;
54
+ credentialStore?: CredentialStore;
55
+ /** Exchanges a code for this installation, with the stored key if any. */
56
+ exchange: (installationID: string, existingCredential?: string) => Promise<ExchangeOutcome<T>>;
57
+ /** The consent evidence to store with the credential `paired` returned. */
58
+ evidence: (paired: T, at: string) => ConsentEvidence;
59
+ }>;
60
+ /**
61
+ * Connects this computer with a code, keeping its machine when the server
62
+ * still accepts its stored key: pairing again, or enrolling, renews consent
63
+ * and keeps the same machine, so its queued activity stays deliverable.
64
+ * When the server no longer accepts the key, the computer connects as a
65
+ * new machine and its old queue moves aside.
66
+ */
67
+ export declare function connectMachine<T extends ConnectedMachine>(options: ConnectOptions<T>): Promise<Readonly<{
68
+ paired: T;
69
+ result: PairResult;
70
+ }>>;
71
+ /**
72
+ * Whether a failed exchange refused the stored key. The server answers a
73
+ * key it no longer accepts with HTTP 400 and "machine credential is
74
+ * invalid".
75
+ */
76
+ export declare function credentialRejected(status: number, failure: Readonly<{
77
+ code?: string;
78
+ message?: string;
79
+ }>): boolean;
80
+ /** A response with a machine and a machine credential. */
81
+ export declare function isConnectedMachine(value: unknown): value is ConnectedMachine;
82
+ /** The `{ error: { code, message } }` body of a failed control-plane call. */
83
+ export declare function apiError(response: Response): Promise<Readonly<{
84
+ code?: string;
85
+ message?: string;
86
+ }>>;
87
+ export declare function normalizedOperatingSystem(): "macos" | "linux" | "windows";
21
88
  //# sourceMappingURL=pair.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pair.d.ts","sourceRoot":"","sources":["../../src/pair.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAG/B,eAAO,MAAM,+BAA+B,uBAAuB,CAAC;AACpE,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,MAAM,CAAC;AAcjD,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,IAAI,CAAC;CACxB,CAAC,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;IACjC,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,cAAc,CAAC;IAC/B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB,CAAC,CAAC;AAEH,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CA8F3E"}
1
+ {"version":3,"file":"pair.d.ts","sourceRoot":"","sources":["../../src/pair.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC7B,MAAM,uBAAuB,CAAC;AAK/B,eAAO,MAAM,+BAA+B,uBAAuB,CAAC;AACpE,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,MAAM,CAAC;AAEjD,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,oEAAoE;AACpE,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,IAAI,CAAC;IACvB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,gBAAgB,IAClD,QAAQ,CAAC;IAAE,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC,GACvB,QAAQ,CAAC;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,kBAAkB,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AAE5D,4EAA4E;AAC5E,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,uBAAuB,EACrB,gBAAgB,GAChB,sBAAsB,GACtB,aAAa,GACb,eAAe,GACf,kBAAkB,GAClB,qBAAqB,CACxB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;IACjC,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,cAAc,CAAC;IAC/B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB,CAAC,CAAC;AAEH,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CA6C3E;AAED,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,gBAAgB,IAAI,QAAQ,CAAC;IAChE,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,GAAG,CAAC;IACd,OAAO,EAAE,OAAO,KAAK,CAAC;IACtB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,0EAA0E;IAC1E,QAAQ,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,kBAAkB,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/F,2EAA2E;IAC3E,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,KAAK,eAAe,CAAC;CACtD,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,CAAC,SAAS,gBAAgB,EAC7D,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GACzB,OAAO,CAAC,QAAQ,CAAC;IAAE,MAAM,EAAE,CAAC,CAAC;IAAC,MAAM,EAAE,UAAU,CAAA;CAAE,CAAC,CAAC,CAkFtD;AA6DD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,QAAQ,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GACrD,OAAO,CAIT;AA4CD,0DAA0D;AAC1D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAiB5E;AAED,8EAA8E;AAC9E,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,QAAQ,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAwBxD;AAED,wBAAgB,yBAAyB,IAAI,OAAO,GAAG,OAAO,GAAG,SAAS,CAKzE"}
package/dist/src/pair.js CHANGED
@@ -1,25 +1,141 @@
1
1
  import { arch, hostname, platform } from "node:os";
2
- import { FileCredentialStore, } from "./credential-store.js";
3
- import { loadOrCreateInstallationID } from "./installation.js";
2
+ import { FileCredentialStore, credentialFileName, } from "./credential-store.js";
3
+ import { retirePairingState } from "./event-spool.js";
4
+ import { installationFileName, loadOrCreateInstallationID } from "./installation.js";
5
+ import { InstanceLock } from "./instance-lock.js";
4
6
  export const OBSERVER_CONSENT_POLICY_VERSION = "observer-policy-v1";
5
7
  export async function pairMachine(options) {
6
8
  const code = options.code.trim();
7
9
  if (code.length === 0) {
8
10
  throw new Error("Pairing code is required.");
9
11
  }
10
- const installationID = await loadOrCreateInstallationID(options.config.dataDirectory);
11
12
  if (options.config.observabilityProfile === "full" &&
12
13
  options.consentProfile !== "full") {
13
14
  throw new Error("The configured full observability profile requires full-content consent.");
14
15
  }
15
16
  const endpoint = new URL("/v1/pairing/exchange", options.config.controlPlaneUrl);
16
17
  const fetcher = options.fetcher ?? fetch;
18
+ const connected = await connectMachine({
19
+ config: options.config,
20
+ endpoint,
21
+ fetcher,
22
+ ...(options.credentialStore === undefined ? {} : { credentialStore: options.credentialStore }),
23
+ exchange: (installationID, existingCredential) => exchangePairing(fetcher, endpoint, {
24
+ code,
25
+ installation_id: installationID,
26
+ display_name: options.displayName?.trim() || hostname(),
27
+ hostname: hostname(),
28
+ operating_system: normalizedOperatingSystem(),
29
+ architecture: arch(),
30
+ consent: {
31
+ granted: true,
32
+ profile: options.consentProfile,
33
+ policy_version: OBSERVER_CONSENT_POLICY_VERSION,
34
+ },
35
+ ...(existingCredential === undefined ? {} : { existing_credential: existingCredential }),
36
+ }),
37
+ evidence: (_paired, at) => ({
38
+ consentProfile: options.consentProfile,
39
+ consentPolicyVersion: OBSERVER_CONSENT_POLICY_VERSION,
40
+ consentedAt: at,
41
+ consentSource: "owner",
42
+ }),
43
+ });
44
+ return connected.result;
45
+ }
46
+ /**
47
+ * Connects this computer with a code, keeping its machine when the server
48
+ * still accepts its stored key: pairing again, or enrolling, renews consent
49
+ * and keeps the same machine, so its queued activity stays deliverable.
50
+ * When the server no longer accepts the key, the computer connects as a
51
+ * new machine and its old queue moves aside.
52
+ */
53
+ export async function connectMachine(options) {
54
+ const dataDirectory = options.config.dataDirectory;
55
+ const installationID = await loadOrCreateInstallationID(dataDirectory);
17
56
  const credentialStore = options.credentialStore ??
18
- new FileCredentialStore(options.config.dataDirectory);
57
+ new FileCredentialStore(dataDirectory);
19
58
  const previous = await credentialStore.load();
20
- if (previous !== null && previous.controlPlaneOrigin !== endpoint.origin) {
59
+ if (previous !== null && previous.controlPlaneOrigin !== options.endpoint.origin) {
21
60
  throw new Error("The stored machine belongs to a different control plane. Use a separate data directory to pair another workspace.");
22
61
  }
62
+ let paired;
63
+ if (previous !== null) {
64
+ const renewal = await options.exchange(installationID, previous.credential);
65
+ if ("paired" in renewal) {
66
+ paired = renewal.paired;
67
+ }
68
+ else if (!renewal.credentialRejected) {
69
+ throw renewal.error;
70
+ }
71
+ else if (await credentialStillAccepted(options.fetcher, options.config.controlPlaneUrl, previous.credential)) {
72
+ // The exchange also refuses a key that is valid in another workspace.
73
+ throw new Error("This computer is paired with another workspace on this server. Use a separate data directory to pair another workspace.");
74
+ }
75
+ }
76
+ let lock;
77
+ try {
78
+ if (paired === undefined || !sameMachine(previous, paired)) {
79
+ // A new machine. Its queue, stream cursor and credential start empty;
80
+ // the observer must not be using the old ones while they move.
81
+ lock = await InstanceLock.acquire(dataDirectory);
82
+ // The old credential goes with its queue: it is the only key that can
83
+ // decrypt it. Once it is gone, a retry after a lost response pairs
84
+ // without it, and the server answers with the same new machine.
85
+ //
86
+ // A key the server no longer accepts also retires its installation ID:
87
+ // a server can still hold that ID for the deleted machine, and the
88
+ // built-in SQLite server refuses to pair it again. A new ID pairs
89
+ // cleanly everywhere.
90
+ const replacing = paired === undefined && previous !== null;
91
+ retirePairingState(dataDirectory, previous?.machineId, [
92
+ credentialFileName,
93
+ ...(replacing ? [installationFileName] : []),
94
+ ]);
95
+ if (paired === undefined) {
96
+ const freshInstallationID = replacing
97
+ ? await loadOrCreateInstallationID(dataDirectory)
98
+ : installationID;
99
+ const fresh = await options.exchange(freshInstallationID);
100
+ if (!("paired" in fresh))
101
+ throw fresh.error;
102
+ paired = fresh.paired;
103
+ }
104
+ }
105
+ const pairedAt = new Date().toISOString();
106
+ await credentialStore.save({
107
+ machineId: paired.machine.id,
108
+ tenantId: paired.machine.tenant_id,
109
+ credential: paired.credential,
110
+ controlPlaneOrigin: options.config.controlPlaneUrl.origin,
111
+ grpcOrigin: options.config.grpcUrl.origin,
112
+ createdAt: pairedAt,
113
+ ...options.evidence(paired, pairedAt),
114
+ });
115
+ }
116
+ finally {
117
+ await lock?.release();
118
+ }
119
+ const replaced = previous !== null && previous.machineId !== paired.machine.id;
120
+ return Object.freeze({
121
+ paired,
122
+ result: Object.freeze({
123
+ machineId: paired.machine.id,
124
+ displayName: paired.machine.display_name,
125
+ state: paired.machine.state,
126
+ controlPlaneOrigin: options.config.controlPlaneUrl.origin,
127
+ credentialStored: true,
128
+ ...(replaced ? { replacedMachineId: previous.machineId } : {}),
129
+ }),
130
+ });
131
+ }
132
+ function sameMachine(previous, paired) {
133
+ return previous !== null &&
134
+ previous.machineId === paired.machine.id &&
135
+ previous.tenantId === paired.machine.tenant_id &&
136
+ previous.credential === paired.credential;
137
+ }
138
+ async function exchangePairing(fetcher, endpoint, body) {
23
139
  let response;
24
140
  try {
25
141
  response = await fetcher(endpoint, {
@@ -28,20 +144,7 @@ export async function pairMachine(options) {
28
144
  "Content-Type": "application/json",
29
145
  Accept: "application/json",
30
146
  },
31
- body: JSON.stringify({
32
- code,
33
- ...(previous === null ? {} : { existing_credential: previous.credential }),
34
- installation_id: installationID,
35
- display_name: options.displayName?.trim() || hostname(),
36
- hostname: hostname(),
37
- operating_system: normalizedOperatingSystem(),
38
- architecture: arch(),
39
- consent: {
40
- granted: true,
41
- profile: options.consentProfile,
42
- policy_version: OBSERVER_CONSENT_POLICY_VERSION,
43
- },
44
- }),
147
+ body: JSON.stringify(body),
45
148
  redirect: "error",
46
149
  signal: AbortSignal.timeout(10_000),
47
150
  });
@@ -53,7 +156,11 @@ export async function pairMachine(options) {
53
156
  throw new Error(`Could not reach ${endpoint.origin}. Check your internet connection and try again.`);
54
157
  }
55
158
  if (!response.ok) {
56
- throw new Error(await pairingFailureMessage(response));
159
+ const failure = await apiError(response);
160
+ return {
161
+ error: new Error(failure.message ?? `Pairing failed with HTTP ${response.status}.`),
162
+ credentialRejected: credentialRejected(response.status, failure),
163
+ };
57
164
  }
58
165
  let value;
59
166
  try {
@@ -64,66 +171,98 @@ export async function pairMachine(options) {
64
171
  // print fragments. The server supports retries for this installation.
65
172
  throw new Error("The pairing response was incomplete or invalid. Retry with the same code before it expires; keep the existing data directory.");
66
173
  }
67
- const paired = parsePairingResponse(value);
68
- await credentialStore.save({
69
- machineId: paired.machine.id,
70
- tenantId: paired.machine.tenant_id,
71
- credential: paired.credential,
72
- controlPlaneOrigin: options.config.controlPlaneUrl.origin,
73
- grpcOrigin: options.config.grpcUrl.origin,
74
- createdAt: new Date().toISOString(),
75
- consentProfile: options.consentProfile,
76
- consentPolicyVersion: OBSERVER_CONSENT_POLICY_VERSION,
77
- consentedAt: new Date().toISOString(),
78
- });
79
- return Object.freeze({
80
- machineId: paired.machine.id,
81
- displayName: paired.machine.display_name,
82
- state: paired.machine.state,
83
- controlPlaneOrigin: options.config.controlPlaneUrl.origin,
84
- credentialStored: true,
85
- });
86
- }
87
- function parsePairingResponse(value) {
88
- if (typeof value !== "object" ||
89
- value === null ||
90
- !("credential" in value) ||
91
- typeof value.credential !== "string" ||
92
- !value.credential.startsWith("phg_m_") ||
93
- !("machine" in value) ||
94
- typeof value.machine !== "object" ||
95
- value.machine === null ||
96
- !("id" in value.machine) ||
97
- typeof value.machine.id !== "string" ||
98
- !("tenant_id" in value.machine) ||
99
- typeof value.machine.tenant_id !== "string" ||
100
- !("display_name" in value.machine) ||
101
- typeof value.machine.display_name !== "string" ||
102
- !("state" in value.machine) ||
103
- typeof value.machine.state !== "string") {
174
+ if (!isConnectedMachine(value)) {
104
175
  throw new Error("Control plane returned an invalid pairing response.");
105
176
  }
106
- return value;
177
+ return { paired: value };
178
+ }
179
+ /**
180
+ * Whether a failed exchange refused the stored key. The server answers a
181
+ * key it no longer accepts with HTTP 400 and "machine credential is
182
+ * invalid".
183
+ */
184
+ export function credentialRejected(status, failure) {
185
+ return failure.code === "invalid_credential" ||
186
+ (status === 400 &&
187
+ /^machine credential is (?:invalid|revoked)\b/iu.test(failure.message ?? ""));
188
+ }
189
+ /**
190
+ * Whether a server still accepts a stored key. Only a key the server
191
+ * itself calls invalid or revoked, because an administrator removed the
192
+ * computer or revoked the key, may be set aside for a new pairing.
193
+ */
194
+ async function credentialStillAccepted(fetcher, controlPlaneUrl, credential) {
195
+ const endpoint = new URL("/v1/machine/heartbeat", controlPlaneUrl);
196
+ let response;
197
+ try {
198
+ response = await fetcher(endpoint, {
199
+ method: "POST",
200
+ headers: {
201
+ Authorization: `Bearer ${credential}`,
202
+ Accept: "application/json",
203
+ },
204
+ redirect: "error",
205
+ signal: AbortSignal.timeout(10_000),
206
+ });
207
+ }
208
+ catch {
209
+ throw new Error(`Could not reach ${endpoint.origin} to check this computer's earlier pairing. Check your internet connection and try again.`);
210
+ }
211
+ if (response.ok) {
212
+ await response.body?.cancel();
213
+ return true;
214
+ }
215
+ if (response.status === 401 &&
216
+ (await apiError(response)).code === "invalid_credential") {
217
+ return false;
218
+ }
219
+ throw new Error(`Could not check this computer's earlier pairing (HTTP ${response.status}). Try again.`);
220
+ }
221
+ /** A response with a machine and a machine credential. */
222
+ export function isConnectedMachine(value) {
223
+ return typeof value === "object" &&
224
+ value !== null &&
225
+ "credential" in value &&
226
+ typeof value.credential === "string" &&
227
+ value.credential.startsWith("phg_m_") &&
228
+ "machine" in value &&
229
+ typeof value.machine === "object" &&
230
+ value.machine !== null &&
231
+ "id" in value.machine &&
232
+ typeof value.machine.id === "string" &&
233
+ "tenant_id" in value.machine &&
234
+ typeof value.machine.tenant_id === "string" &&
235
+ "display_name" in value.machine &&
236
+ typeof value.machine.display_name === "string" &&
237
+ "state" in value.machine &&
238
+ typeof value.machine.state === "string";
107
239
  }
108
- async function pairingFailureMessage(response) {
240
+ /** The `{ error: { code, message } }` body of a failed control-plane call. */
241
+ export async function apiError(response) {
109
242
  try {
110
243
  const value = await response.json();
111
244
  if (typeof value === "object" &&
112
245
  value !== null &&
113
246
  "error" in value &&
114
247
  typeof value.error === "object" &&
115
- value.error !== null &&
116
- "message" in value.error &&
117
- typeof value.error.message === "string") {
118
- return value.error.message;
248
+ value.error !== null) {
249
+ const error = value.error;
250
+ return {
251
+ ...("code" in error && typeof error.code === "string"
252
+ ? { code: error.code }
253
+ : {}),
254
+ ...("message" in error && typeof error.message === "string"
255
+ ? { message: error.message }
256
+ : {}),
257
+ };
119
258
  }
120
259
  }
121
260
  catch {
122
261
  // The status code still provides an actionable, non-sensitive failure.
123
262
  }
124
- return `Pairing failed with HTTP ${response.status}.`;
263
+ return {};
125
264
  }
126
- function normalizedOperatingSystem() {
265
+ export function normalizedOperatingSystem() {
127
266
  const value = platform();
128
267
  if (value === "darwin")
129
268
  return "macos";