@oar-sh/oar 0.9.9 → 0.9.11

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 (154) hide show
  1. package/CHANGELOG.md +561 -1
  2. package/README.md +162 -909
  3. package/bin/bat/oar.bat +46 -46
  4. package/package.json +14 -14
  5. package/server/README.md +369 -13
  6. package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
  7. package/server/claude-worker/claude-session-process.mjs +121 -9
  8. package/server/claude-worker/claude-session-worker.mjs +9 -1
  9. package/server/claude-worker/claude-turn-publisher.mjs +3 -0
  10. package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
  11. package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
  12. package/server/copilot-worker/copilot-sdk-session-process.mjs +79 -12
  13. package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
  14. package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
  15. package/server/db-schema.mjs +95 -2
  16. package/server/grok-worker/acp-client.mjs +2 -1
  17. package/server/grok-worker/acp-host-services.mjs +4 -3
  18. package/server/migrations/0009-subagent-timeline.mjs +135 -0
  19. package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
  20. package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
  21. package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
  22. package/server/migrations/0013-previews.mjs +130 -0
  23. package/server/migrations/0014-conversation-groups.mjs +89 -0
  24. package/server/migrations/0015-conversation-group-position.mjs +76 -0
  25. package/server/migrations/0016-share-access-log.mjs +110 -0
  26. package/server/public/app/about-ui.js +73 -0
  27. package/server/public/app/about-view.mjs +234 -0
  28. package/server/public/app/action-confirmations.js +31 -14
  29. package/server/public/app/agent-exchange-fold-view.mjs +187 -0
  30. package/server/public/app/api-client.js +312 -16
  31. package/server/public/app/archive-confirm.mjs +28 -0
  32. package/server/public/app/ask-user-view.js +17 -12
  33. package/server/public/app/attachments-view.js +516 -6
  34. package/server/public/app/background-tasks-view.mjs +18 -26
  35. package/server/public/app/bootstrap.js +336 -59
  36. package/server/public/app/chat-actions-menu-state.mjs +126 -0
  37. package/server/public/app/claude-cloud-conversation-ui.js +10 -22
  38. package/server/public/app/composer-control-state.mjs +16 -0
  39. package/server/public/app/composer-keyboard.mjs +54 -0
  40. package/server/public/app/context-menu.mjs +90 -6
  41. package/server/public/app/conversation-group-drag.mjs +191 -0
  42. package/server/public/app/conversation-group-order.mjs +265 -0
  43. package/server/public/app/conversation-group-state.mjs +100 -0
  44. package/server/public/app/conversation-list-drag.mjs +449 -0
  45. package/server/public/app/conversation-turn-state.mjs +81 -0
  46. package/server/public/app/conversation-view.js +436 -117
  47. package/server/public/app/cwd-picker.js +1 -1
  48. package/server/public/app/explorer-edit-state.mjs +68 -0
  49. package/server/public/app/hidden-messages-view.mjs +312 -0
  50. package/server/public/app/host-suspend-ui.js +13 -4
  51. package/server/public/app/journal-view.js +930 -45
  52. package/server/public/app/no-conversation-state.mjs +58 -0
  53. package/server/public/app/preview-cards.mjs +117 -31
  54. package/server/public/app/preview-command.mjs +10 -1
  55. package/server/public/app/provider-readiness.mjs +120 -0
  56. package/server/public/app/relay-board-view.js +19 -3
  57. package/server/public/app/relay-event-subscriptions-ui.js +184 -0
  58. package/server/public/app/relay-event-view.mjs +234 -0
  59. package/server/public/app/relay-restart-error.mjs +16 -0
  60. package/server/public/app/settings-modal.js +93 -9
  61. package/server/public/app/settings-tabs.js +1 -1
  62. package/server/public/app/share-format.mjs +49 -0
  63. package/server/public/app/share-preview-marks.mjs +167 -0
  64. package/server/public/app/share-state.js +109 -0
  65. package/server/public/app/shared-conversations-settings.js +480 -0
  66. package/server/public/app/socket-handlers.js +137 -40
  67. package/server/public/app/store.js +115 -32
  68. package/server/public/app/subagent-row.mjs +138 -0
  69. package/server/public/app/subagent-timeline-view.mjs +186 -0
  70. package/server/public/app/subagent-viewer-watch.mjs +50 -0
  71. package/server/public/app/subagent-viewer.mjs +558 -0
  72. package/server/public/app/task-row-format.mjs +29 -0
  73. package/server/public/app/transcript-jump-bottom-view.mjs +464 -0
  74. package/server/public/app/transcript-jump-bottom.mjs +210 -0
  75. package/server/public/app/transcript-reload-freshness.mjs +25 -0
  76. package/server/public/app/transcript-viewport-anchor.mjs +140 -0
  77. package/server/public/app/update-ui.js +1 -1
  78. package/server/public/app/viewer-history.mjs +5 -3
  79. package/server/public/index.html +774 -12
  80. package/server/relay-tools.md +2 -1
  81. package/server/relay.mjs +2 -1
  82. package/server/repositories/conversation-group-repository.mjs +444 -0
  83. package/server/repositories/message-repository.mjs +30 -3
  84. package/server/repositories/question-repository.mjs +9 -3
  85. package/server/repositories/remote-relay-repository.mjs +6 -2
  86. package/server/repositories/session-repository.mjs +12 -37
  87. package/server/repositories/share-repository.mjs +322 -0
  88. package/server/repositories/share-visibility-statements.mjs +10 -0
  89. package/server/repositories/subagent-timeline-statements.mjs +170 -0
  90. package/server/routes/about-routes.mjs +15 -0
  91. package/server/routes/ask-user-routes.mjs +3 -0
  92. package/server/routes/cache-routes.mjs +4 -0
  93. package/server/routes/conversation-group-routes.mjs +171 -0
  94. package/server/routes/messages-routes.mjs +698 -114
  95. package/server/routes/preview-routes.mjs +9 -1
  96. package/server/routes/relay-event-routes.mjs +157 -0
  97. package/server/routes/remote-relay-routes.mjs +15 -0
  98. package/server/routes/sessions-routes.mjs +843 -193
  99. package/server/server-runtime.mjs +559 -160
  100. package/server/services/about-service.mjs +461 -0
  101. package/server/services/background-task-store.mjs +128 -0
  102. package/server/services/background-work-view.mjs +228 -0
  103. package/server/services/cache-rebuild-service.mjs +30 -0
  104. package/server/services/delete-archive-service.mjs +4 -1
  105. package/server/services/detached-worker-watch-service.mjs +150 -0
  106. package/server/services/explorer-entry-name.mjs +26 -0
  107. package/server/services/explorer-mutations.mjs +122 -0
  108. package/server/services/hidden-message-list.mjs +21 -0
  109. package/server/services/host-suspend-command.mjs +2 -1
  110. package/server/services/message-pin-list.mjs +1 -1
  111. package/server/services/preview-health-probe.mjs +8 -1
  112. package/server/services/preview-proxy-server.mjs +49 -27
  113. package/server/services/preview-registry-service.mjs +676 -26
  114. package/server/services/preview-static-handler.mjs +40 -7
  115. package/server/services/preview-store.mjs +274 -0
  116. package/server/services/push-dispatch-service.mjs +18 -0
  117. package/server/services/rate-limit-service.mjs +12 -1
  118. package/server/services/relay-auth.mjs +183 -0
  119. package/server/services/relay-cli-launcher-service.mjs +3 -1
  120. package/server/services/relay-event-link-service.mjs +289 -0
  121. package/server/services/relay-event-service.mjs +1952 -0
  122. package/server/services/relay-shutdown-gate.mjs +31 -0
  123. package/server/services/remote-relay-caller-context.mjs +52 -7
  124. package/server/services/remote-relay-dispatcher.mjs +551 -7
  125. package/server/services/remote-relay-registry-service.mjs +11 -1
  126. package/server/services/request-boundary.mjs +183 -0
  127. package/server/services/session-activity-tracker.mjs +695 -0
  128. package/server/services/session-history-refresh-service.mjs +7 -0
  129. package/server/services/session-worker-launch-service.mjs +190 -6
  130. package/server/services/session-worker-process-service.mjs +10 -6
  131. package/server/services/share-access-log-service.mjs +123 -0
  132. package/server/services/share-path-redaction.mjs +424 -0
  133. package/server/services/stale-attempt-log-limiter.mjs +96 -0
  134. package/server/services/stale-recovery-clock-guard.mjs +57 -0
  135. package/server/services/stale-subagent-run-sweep.mjs +109 -0
  136. package/server/services/status-event-service.mjs +7 -3
  137. package/server/services/subagent-record-retention.mjs +164 -0
  138. package/server/services/subagent-timeline-service.mjs +328 -0
  139. package/server/services/subagent-viewer-rooms.mjs +119 -0
  140. package/server/services/tunnel-worker-path-guard.mjs +24 -41
  141. package/server/services/upgrade-socket-guard.mjs +16 -0
  142. package/server/services/windows-autostart-service.mjs +6 -1
  143. package/server/services/windows-boot-autostart-service.mjs +207 -23
  144. package/shared/preview-tool-core.mjs +15 -5
  145. package/shared/relay-events.mjs +371 -0
  146. package/shared/remote-relay-contract.mjs +160 -6
  147. package/shared/share-path-privacy.mjs +30 -0
  148. package/shared/subagent-record-retention.mjs +31 -0
  149. package/shared/subagent-timeline.mjs +79 -0
  150. package/shared/windows-system-tools.mjs +106 -0
  151. package/shared/worker-runtime/background-task-digest.mjs +30 -0
  152. package/shared/worker-runtime/heartbeat.mjs +24 -0
  153. package/shared/worker-runtime/kept-posts.mjs +83 -0
  154. package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
@@ -146,6 +146,10 @@ export function createRemoteRelayRegistry({
146
146
  setIntervalImpl = setInterval,
147
147
  clearIntervalImpl = clearInterval,
148
148
  randomUUID = nodeRandomUUID,
149
+ // Called with the removed entry (address and token still in it), after it
150
+ // is gone from the list: the event link tells that relay its
151
+ // subscriptions ended.
152
+ onRemove = () => {},
149
153
  } = {}) {
150
154
  let cachedInstanceId = '';
151
155
  let healthTimer = null;
@@ -446,14 +450,20 @@ export function createRemoteRelayRegistry({
446
450
  function remove(id) {
447
451
  const key = toText(id);
448
452
  const entries = readList();
453
+ const removed = entries.find((entry) => entry.id === key) || null;
449
454
  const remaining = entries.filter((entry) => entry.id !== key);
450
- if (!key || remaining.length === entries.length) return invalid('Unknown remote relay', 404);
455
+ if (!key || !removed) return invalid('Unknown remote relay', 404);
451
456
  writeList(remaining);
452
457
  try {
453
458
  repository?.forgetRelay?.(key);
454
459
  } catch (error) {
455
460
  logger?.warn?.(`[remote-relays] forgetting unlocks failed: ${error?.message || error}`);
456
461
  }
462
+ try {
463
+ onRemove(removed);
464
+ } catch (error) {
465
+ logger?.warn?.(`[remote-relays] removal hook failed: ${error?.message || error}`);
466
+ }
457
467
  emitChange();
458
468
  return { ok: true };
459
469
  }
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+
3
+ // Where a request came from, as far as the relay can tell.
4
+ //
5
+ // "Truly local" is the boundary for the few things only a process on the
6
+ // relay's own machine may do (run an image operation, open a session-worker
7
+ // socket): the TCP peer is loopback AND nothing in front of the relay touched
8
+ // the request. A same-host reverse proxy (Caddy, nginx), a hand-run
9
+ // cloudflared and the built-in tunnel all connect from 127.0.0.1 too, so the
10
+ // peer alone proves nothing; what tells them apart is what they add: an edge
11
+ // marker (`cf-ray`, or a configured one) or a forwarding header. Local workers
12
+ // and agent tools call the relay over 127.0.0.1 and send none of these.
13
+ //
14
+ // A forwarder that adds no header at all (an SSH reverse tunnel, a bare TCP
15
+ // proxy) cannot be told apart from a local process; the access token is then
16
+ // the only boundary, as it is for every other route.
17
+
18
+ // Cloudflare's edge stamps `cf-ray` on every proxied request and a remote
19
+ // client cannot strip it.
20
+ export const DEFAULT_TUNNEL_MARKER_HEADERS = Object.freeze(['cf-ray']);
21
+
22
+ // Headers a proxy adds to say who it forwards for. Read here only as a trace
23
+ // that a proxy was involved, never as an address.
24
+ export const FORWARDING_HEADERS = Object.freeze(['x-forwarded-for', 'forwarded', 'x-real-ip']);
25
+
26
+ function normalizeHeaderName(value) {
27
+ return String(value || '').trim().toLowerCase();
28
+ }
29
+
30
+ function headerNamesFrom(source) {
31
+ if (Array.isArray(source)) return source;
32
+ if (source === undefined || source === null) return [];
33
+ return String(source).split(',');
34
+ }
35
+
36
+ /**
37
+ * The marker headers to look for: `cf-ray` always, plus every name from every
38
+ * source given (an array or a comma-separated string). Sources are merged,
39
+ * none replaces another, so `tunnelMarkerHeaders: []` in the config does not
40
+ * switch off COPILOT_TUNNEL_MARKER_HEADERS.
41
+ */
42
+ export function resolveTunnelMarkerHeaders(...sources) {
43
+ const names = [...DEFAULT_TUNNEL_MARKER_HEADERS];
44
+ for (const source of sources) {
45
+ for (const entry of headerNamesFrom(source)) {
46
+ const name = normalizeHeaderName(entry);
47
+ if (name && !names.includes(name)) names.push(name);
48
+ }
49
+ }
50
+ return names;
51
+ }
52
+
53
+ /** 127.0.0.0/8 and ::1, plain or IPv4-mapped. */
54
+ export function isLoopbackAddress(value) {
55
+ let text = String(value || '').trim().toLowerCase();
56
+ if (!text) return false;
57
+ if (text === '::1' || text === '0:0:0:0:0:0:0:1') return true;
58
+ if (text.startsWith('::ffff:')) text = text.slice(7);
59
+ if (text === '7f00:1') return true;
60
+ return /^127(?:\.\d{1,3}){3}$/.test(text);
61
+ }
62
+
63
+ function headerPresent(headers, name) {
64
+ const value = headers?.[name];
65
+ if (Array.isArray(value)) return value.some((entry) => String(entry || '').trim());
66
+ return typeof value === 'string' ? !!value.trim() : !!value;
67
+ }
68
+
69
+ function peerAddressOf(req) {
70
+ return req?.socket?.remoteAddress || req?.connection?.remoteAddress || '';
71
+ }
72
+
73
+ /**
74
+ * `markerHeaders`: names as resolveTunnelMarkerHeaders returns them (the
75
+ * default `cf-ray` is added if missing).
76
+ */
77
+ export function createLocalRequestCheck({ markerHeaders = DEFAULT_TUNNEL_MARKER_HEADERS } = {}) {
78
+ const markers = resolveTunnelMarkerHeaders(markerHeaders);
79
+
80
+ function matchedMarkerHeader(headers) {
81
+ for (const name of markers) {
82
+ if (headerPresent(headers, name)) return name;
83
+ }
84
+ return null;
85
+ }
86
+
87
+ function matchedForwardingHeader(headers) {
88
+ for (const name of FORWARDING_HEADERS) {
89
+ if (headerPresent(headers, name)) return name;
90
+ }
91
+ return null;
92
+ }
93
+
94
+ /**
95
+ * Why a request is not truly local: `{ reason: 'peer' | 'marker' |
96
+ * 'forwarded', header }`, or null when it is.
97
+ */
98
+ function explain(req) {
99
+ if (!isLoopbackAddress(peerAddressOf(req))) return { reason: 'peer', header: null };
100
+ const marker = matchedMarkerHeader(req?.headers);
101
+ if (marker) return { reason: 'marker', header: marker };
102
+ const forwarded = matchedForwardingHeader(req?.headers);
103
+ if (forwarded) return { reason: 'forwarded', header: forwarded };
104
+ return null;
105
+ }
106
+
107
+ function isTrulyLocal(req) {
108
+ return explain(req) === null;
109
+ }
110
+
111
+ return {
112
+ markerHeaders: markers,
113
+ matchedMarkerHeader,
114
+ explain,
115
+ isTrulyLocal,
116
+ };
117
+ }
118
+
119
+ function trustsPeer(req) {
120
+ const trust = typeof req?.app?.get === 'function' ? req.app.get('trust proxy fn') : null;
121
+ return typeof trust === 'function' && trust(peerAddressOf(req), 0) === true;
122
+ }
123
+
124
+ function firstListEntry(value) {
125
+ return String(Array.isArray(value) ? value[0] : (value || '')).split(',')[0].trim();
126
+ }
127
+
128
+ /**
129
+ * The host (with port) the client addressed: X-Forwarded-Host when the
130
+ * immediate peer is a trusted proxy (Express `trust proxy`), else Host. A
131
+ * direct client cannot forge it. Express's own `req.hostname` drops the port,
132
+ * which a LAN address needs.
133
+ */
134
+ export function requestHostOf(req) {
135
+ if (trustsPeer(req)) {
136
+ const forwardedHost = firstListEntry(req?.headers?.['x-forwarded-host']);
137
+ if (forwardedHost) return forwardedHost;
138
+ }
139
+ return firstListEntry(req?.headers?.host);
140
+ }
141
+
142
+ /** An Express-style view of a raw request (a socket.io handshake), so `ip`, `protocol` and the trust checks follow `trust proxy`. */
143
+ export function expressRequestView(app, rawReq) {
144
+ const view = Object.create(app.request);
145
+ view.headers = rawReq?.headers || {};
146
+ view.socket = rawReq?.socket || rawReq?.connection || null;
147
+ view.connection = view.socket;
148
+ view.method = rawReq?.method;
149
+ view.url = rawReq?.url;
150
+ return view;
151
+ }
152
+
153
+ function normalizedHost(protocol, host) {
154
+ try {
155
+ return new URL(`${protocol}//${host}`).host.toLowerCase();
156
+ } catch {
157
+ return '';
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Same-origin check for a browser handshake. No Origin header (a non-browser
163
+ * client) passes; otherwise the Origin's host must be the host the request
164
+ * was addressed to (see requestHostOf), or be listed in `allowedOrigins`.
165
+ */
166
+ export function isSameOriginRequest(req, { allowedOrigins = [] } = {}) {
167
+ const origin = firstListEntry(req?.headers?.origin);
168
+ if (!origin) return true;
169
+ let parsed;
170
+ try {
171
+ parsed = new URL(origin);
172
+ } catch {
173
+ return false;
174
+ }
175
+ if (parsed.origin === 'null') return false;
176
+ const allowed = (Array.isArray(allowedOrigins) ? allowedOrigins : headerNamesFrom(allowedOrigins))
177
+ .map((entry) => String(entry || '').trim().replace(/\/+$/, '').toLowerCase())
178
+ .filter(Boolean);
179
+ if (allowed.includes(parsed.origin.toLowerCase())) return true;
180
+ const host = requestHostOf(req);
181
+ if (!host) return false;
182
+ return normalizedHost(parsed.protocol, host) === parsed.host.toLowerCase();
183
+ }