@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.
- package/CHANGELOG.md +561 -1
- package/README.md +162 -909
- package/bin/bat/oar.bat +46 -46
- package/package.json +14 -14
- package/server/README.md +369 -13
- package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
- package/server/claude-worker/claude-session-process.mjs +121 -9
- package/server/claude-worker/claude-session-worker.mjs +9 -1
- package/server/claude-worker/claude-turn-publisher.mjs +3 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
- package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +79 -12
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
- package/server/db-schema.mjs +95 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +4 -3
- package/server/migrations/0009-subagent-timeline.mjs +135 -0
- package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
- package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
- package/server/migrations/0013-previews.mjs +130 -0
- package/server/migrations/0014-conversation-groups.mjs +89 -0
- package/server/migrations/0015-conversation-group-position.mjs +76 -0
- package/server/migrations/0016-share-access-log.mjs +110 -0
- package/server/public/app/about-ui.js +73 -0
- package/server/public/app/about-view.mjs +234 -0
- package/server/public/app/action-confirmations.js +31 -14
- package/server/public/app/agent-exchange-fold-view.mjs +187 -0
- package/server/public/app/api-client.js +312 -16
- package/server/public/app/archive-confirm.mjs +28 -0
- package/server/public/app/ask-user-view.js +17 -12
- package/server/public/app/attachments-view.js +516 -6
- package/server/public/app/background-tasks-view.mjs +18 -26
- package/server/public/app/bootstrap.js +336 -59
- package/server/public/app/chat-actions-menu-state.mjs +126 -0
- package/server/public/app/claude-cloud-conversation-ui.js +10 -22
- package/server/public/app/composer-control-state.mjs +16 -0
- package/server/public/app/composer-keyboard.mjs +54 -0
- package/server/public/app/context-menu.mjs +90 -6
- package/server/public/app/conversation-group-drag.mjs +191 -0
- package/server/public/app/conversation-group-order.mjs +265 -0
- package/server/public/app/conversation-group-state.mjs +100 -0
- package/server/public/app/conversation-list-drag.mjs +449 -0
- package/server/public/app/conversation-turn-state.mjs +81 -0
- package/server/public/app/conversation-view.js +436 -117
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/explorer-edit-state.mjs +68 -0
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +13 -4
- package/server/public/app/journal-view.js +930 -45
- package/server/public/app/no-conversation-state.mjs +58 -0
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/provider-readiness.mjs +120 -0
- package/server/public/app/relay-board-view.js +19 -3
- package/server/public/app/relay-event-subscriptions-ui.js +184 -0
- package/server/public/app/relay-event-view.mjs +234 -0
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +93 -9
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/share-format.mjs +49 -0
- package/server/public/app/share-preview-marks.mjs +167 -0
- package/server/public/app/share-state.js +109 -0
- package/server/public/app/shared-conversations-settings.js +480 -0
- package/server/public/app/socket-handlers.js +137 -40
- package/server/public/app/store.js +115 -32
- package/server/public/app/subagent-row.mjs +138 -0
- package/server/public/app/subagent-timeline-view.mjs +186 -0
- package/server/public/app/subagent-viewer-watch.mjs +50 -0
- package/server/public/app/subagent-viewer.mjs +558 -0
- package/server/public/app/task-row-format.mjs +29 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +464 -0
- package/server/public/app/transcript-jump-bottom.mjs +210 -0
- package/server/public/app/transcript-reload-freshness.mjs +25 -0
- package/server/public/app/transcript-viewport-anchor.mjs +140 -0
- package/server/public/app/update-ui.js +1 -1
- package/server/public/app/viewer-history.mjs +5 -3
- package/server/public/index.html +774 -12
- package/server/relay-tools.md +2 -1
- package/server/relay.mjs +2 -1
- package/server/repositories/conversation-group-repository.mjs +444 -0
- package/server/repositories/message-repository.mjs +30 -3
- package/server/repositories/question-repository.mjs +9 -3
- package/server/repositories/remote-relay-repository.mjs +6 -2
- package/server/repositories/session-repository.mjs +12 -37
- package/server/repositories/share-repository.mjs +322 -0
- package/server/repositories/share-visibility-statements.mjs +10 -0
- package/server/repositories/subagent-timeline-statements.mjs +170 -0
- package/server/routes/about-routes.mjs +15 -0
- package/server/routes/ask-user-routes.mjs +3 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +698 -114
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/relay-event-routes.mjs +157 -0
- package/server/routes/remote-relay-routes.mjs +15 -0
- package/server/routes/sessions-routes.mjs +843 -193
- package/server/server-runtime.mjs +559 -160
- package/server/services/about-service.mjs +461 -0
- package/server/services/background-task-store.mjs +128 -0
- package/server/services/background-work-view.mjs +228 -0
- package/server/services/cache-rebuild-service.mjs +30 -0
- package/server/services/delete-archive-service.mjs +4 -1
- package/server/services/detached-worker-watch-service.mjs +150 -0
- package/server/services/explorer-entry-name.mjs +26 -0
- package/server/services/explorer-mutations.mjs +122 -0
- package/server/services/hidden-message-list.mjs +21 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/message-pin-list.mjs +1 -1
- package/server/services/preview-health-probe.mjs +8 -1
- package/server/services/preview-proxy-server.mjs +49 -27
- package/server/services/preview-registry-service.mjs +676 -26
- package/server/services/preview-static-handler.mjs +40 -7
- package/server/services/preview-store.mjs +274 -0
- package/server/services/push-dispatch-service.mjs +18 -0
- package/server/services/rate-limit-service.mjs +12 -1
- package/server/services/relay-auth.mjs +183 -0
- package/server/services/relay-cli-launcher-service.mjs +3 -1
- package/server/services/relay-event-link-service.mjs +289 -0
- package/server/services/relay-event-service.mjs +1952 -0
- package/server/services/relay-shutdown-gate.mjs +31 -0
- package/server/services/remote-relay-caller-context.mjs +52 -7
- package/server/services/remote-relay-dispatcher.mjs +551 -7
- package/server/services/remote-relay-registry-service.mjs +11 -1
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +695 -0
- package/server/services/session-history-refresh-service.mjs +7 -0
- package/server/services/session-worker-launch-service.mjs +190 -6
- package/server/services/session-worker-process-service.mjs +10 -6
- package/server/services/share-access-log-service.mjs +123 -0
- package/server/services/share-path-redaction.mjs +424 -0
- package/server/services/stale-attempt-log-limiter.mjs +96 -0
- package/server/services/stale-recovery-clock-guard.mjs +57 -0
- package/server/services/stale-subagent-run-sweep.mjs +109 -0
- package/server/services/status-event-service.mjs +7 -3
- package/server/services/subagent-record-retention.mjs +164 -0
- package/server/services/subagent-timeline-service.mjs +328 -0
- package/server/services/subagent-viewer-rooms.mjs +119 -0
- package/server/services/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/upgrade-socket-guard.mjs +16 -0
- package/server/services/windows-autostart-service.mjs +6 -1
- package/server/services/windows-boot-autostart-service.mjs +207 -23
- package/shared/preview-tool-core.mjs +15 -5
- package/shared/relay-events.mjs +371 -0
- package/shared/remote-relay-contract.mjs +160 -6
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/subagent-record-retention.mjs +31 -0
- package/shared/subagent-timeline.mjs +79 -0
- package/shared/windows-system-tools.mjs +106 -0
- package/shared/worker-runtime/background-task-digest.mjs +30 -0
- package/shared/worker-runtime/heartbeat.mjs +24 -0
- package/shared/worker-runtime/kept-posts.mjs +83 -0
- 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 ||
|
|
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
|
+
}
|