@oar-sh/oar 0.9.3 → 0.9.4
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/.github/extensions/web-relay/extension.mjs +6 -1
- package/.github/extensions/web-relay/skills/tool-activity.mjs +7 -0
- package/CHANGELOG.md +67 -0
- package/README.md +62 -13
- package/package.json +5 -2
- package/server/README.md +43 -3
- package/server/claude-worker/claude-remote-relay-tool.mjs +99 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +26 -4
- package/server/claude-worker/claude-session-process.mjs +92 -25
- package/server/claude-worker/sdk-message-normalizer.mjs +7 -0
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +59 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +12 -2
- package/server/copilot-worker/copilot-sdk-session-process.mjs +83 -9
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +65 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +45 -2
- package/server/cursor-worker/sdk-message-normalizer.mjs +25 -0
- package/server/db-schema.mjs +8 -0
- package/server/grok-worker/acp-client.mjs +92 -6
- package/server/grok-worker/grok-turn-runner.mjs +59 -8
- package/server/grok-worker/sdk-message-normalizer.mjs +13 -0
- package/server/mcp/oar-mcp-server.mjs +473 -0
- package/server/migrations/0005-remote-relays.mjs +90 -0
- package/server/public/app/action-confirmations.js +141 -13
- package/server/public/app/api-client.js +103 -0
- package/server/public/app/bootstrap.js +21 -6
- package/server/public/app/conversation-deep-link.mjs +42 -0
- package/server/public/app/conversation-view.js +98 -7
- package/server/public/app/host-suspend-ui.js +272 -0
- package/server/public/app/journal-view.js +4 -1
- package/server/public/app/mention-autocomplete.mjs +298 -0
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/pwa-install.js +8 -3
- package/server/public/app/relay-prompt-sanitizer.mjs +48 -5
- package/server/public/app/remote-relay-origin-view.mjs +76 -0
- package/server/public/app/remote-relay-shared.mjs +85 -0
- package/server/public/app/remote-relays-settings.js +618 -0
- package/server/public/app/remote-relays-store.mjs +126 -0
- package/server/public/app/settings-modal.js +4 -2
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +18 -1
- package/server/public/app/thinking-follow.js +260 -0
- package/server/public/app/tool-activity-emoji.mjs +4 -0
- package/server/public/index.html +257 -8
- package/server/relay-tools.md +5 -1
- package/server/repositories/message-repository.mjs +7 -0
- package/server/repositories/question-repository.mjs +3 -0
- package/server/repositories/remote-relay-repository.mjs +75 -0
- package/server/repositories/session-repository.mjs +6 -1
- package/server/routes/ask-user-routes.mjs +42 -2
- package/server/routes/messages-routes.mjs +90 -16
- package/server/routes/remote-relay-routes.mjs +133 -0
- package/server/routes/sessions-routes.mjs +80 -29
- package/server/server-runtime.mjs +238 -0
- package/server/services/github-ci-activity-service.mjs +199 -0
- package/server/services/host-suspend-command.mjs +38 -0
- package/server/services/host-suspend-service.mjs +254 -0
- package/server/services/push-dispatch-service.mjs +37 -4
- package/server/services/remote-relay-caller-context.mjs +341 -0
- package/server/services/remote-relay-client.mjs +332 -0
- package/server/services/remote-relay-dispatcher.mjs +1500 -0
- package/server/services/remote-relay-inbound.mjs +187 -0
- package/server/services/remote-relay-pairing.mjs +189 -0
- package/server/services/remote-relay-registry-service.mjs +581 -0
- package/server/services/session-worker-launch-service.mjs +131 -12
- package/shared/remote-relay-contract.mjs +539 -0
- package/shared/remote-relay-mentions.mjs +129 -0
- package/shared/remote-relay-tool-core.mjs +314 -0
- package/shared/worker-runtime/api-client.mjs +112 -22
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
REMOTE_RELAY_ERROR_CODES,
|
|
5
|
+
REMOTE_RELAY_HEADERS,
|
|
6
|
+
REMOTE_RELAY_LIMITS,
|
|
7
|
+
checkRemoteRelayUrlPolicy,
|
|
8
|
+
} from '../../shared/remote-relay-contract.mjs';
|
|
9
|
+
|
|
10
|
+
// The outbound HTTP client a relay uses to call the OTHER relays it is paired
|
|
11
|
+
// with (plan §5.3). Deliberately separate from the worker `api` helper: that
|
|
12
|
+
// one adds X-Relay-* worker-identity headers, which a remote would read as
|
|
13
|
+
// queue ownership. This client sends exactly five headers — the bearer token,
|
|
14
|
+
// Accept, Content-Type (with a body), and the two x-oar-remote-* headers that
|
|
15
|
+
// carry the calling relay and the hop count — and nothing else.
|
|
16
|
+
//
|
|
17
|
+
// The token must never leave for a host the user did not configure, so
|
|
18
|
+
// redirects are refused rather than followed, and the address policy
|
|
19
|
+
// (https, or http only on loopback/private ranges) is re-checked per call.
|
|
20
|
+
|
|
21
|
+
const IDENTITY_PATH = '/api/relay/identity';
|
|
22
|
+
const LEGACY_STATUS_PATH = '/api/status';
|
|
23
|
+
const LEGACY_NAME_PATH = '/api/settings/pwa-app-name';
|
|
24
|
+
const REMOTE_ERROR_TEXT_MAX = 500;
|
|
25
|
+
|
|
26
|
+
export class RemoteRelayError extends Error {
|
|
27
|
+
/**
|
|
28
|
+
* `code` is one of REMOTE_RELAY_ERROR_CODES (or REMOTE_RELAY_HTTP_<status>).
|
|
29
|
+
* `status` is the remote's HTTP status when it answered, `detail` a short
|
|
30
|
+
* technical reason (a socket error code, the remote's own error code),
|
|
31
|
+
* `remoteError` the remote's `error` text and `remoteBody` its parsed JSON.
|
|
32
|
+
*/
|
|
33
|
+
constructor(code, message, { status = null, detail = null, remoteError = null, remoteBody = null } = {}) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = 'RemoteRelayError';
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.status = status;
|
|
38
|
+
this.detail = detail;
|
|
39
|
+
this.remoteError = remoteError;
|
|
40
|
+
this.remoteBody = remoteBody;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toText(value) {
|
|
45
|
+
return String(value ?? '').trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hostOf(url) {
|
|
49
|
+
try {
|
|
50
|
+
return new URL(String(url)).hostname.toLowerCase();
|
|
51
|
+
} catch {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function relayLabel(relay) {
|
|
57
|
+
const name = toText(relay?.name) || hostOf(relay?.url);
|
|
58
|
+
return name ? `Relay "${name}"` : 'The remote relay';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isPlainObject(value) {
|
|
62
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function tooLargeError(relay, maxBytes) {
|
|
66
|
+
return new RemoteRelayError(
|
|
67
|
+
REMOTE_RELAY_ERROR_CODES.unsupported,
|
|
68
|
+
`${relayLabel(relay)} sent an answer larger than ${maxBytes} bytes`,
|
|
69
|
+
{ detail: 'response too large' },
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Reads the body as text, stopping (and cancelling the stream) past maxBytes. */
|
|
74
|
+
async function readCappedText(response, relay, maxBytes) {
|
|
75
|
+
const declared = Number(response.headers?.get?.('content-length'));
|
|
76
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
77
|
+
try { await response.body?.cancel?.(); } catch {}
|
|
78
|
+
throw tooLargeError(relay, maxBytes);
|
|
79
|
+
}
|
|
80
|
+
const body = response.body;
|
|
81
|
+
if (!body || typeof body.getReader !== 'function') {
|
|
82
|
+
const text = typeof response.text === 'function' ? String(await response.text()) : '';
|
|
83
|
+
if (Buffer.byteLength(text, 'utf8') > maxBytes) throw tooLargeError(relay, maxBytes);
|
|
84
|
+
return text;
|
|
85
|
+
}
|
|
86
|
+
const reader = body.getReader();
|
|
87
|
+
const chunks = [];
|
|
88
|
+
let total = 0;
|
|
89
|
+
for (;;) {
|
|
90
|
+
const { done, value } = await reader.read();
|
|
91
|
+
if (done) break;
|
|
92
|
+
const chunk = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
93
|
+
total += chunk.length;
|
|
94
|
+
if (total > maxBytes) {
|
|
95
|
+
try { await reader.cancel(); } catch {}
|
|
96
|
+
throw tooLargeError(relay, maxBytes);
|
|
97
|
+
}
|
|
98
|
+
chunks.push(chunk);
|
|
99
|
+
}
|
|
100
|
+
return Buffer.concat(chunks, total).toString('utf8');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseJson(text) {
|
|
104
|
+
if (!text.trim()) return { ok: true, value: null };
|
|
105
|
+
try {
|
|
106
|
+
return { ok: true, value: JSON.parse(text) };
|
|
107
|
+
} catch {
|
|
108
|
+
return { ok: false, value: null };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function networkError(relay, error, timeoutMs) {
|
|
113
|
+
const timedOut = error?.name === 'TimeoutError' || error?.name === 'AbortError';
|
|
114
|
+
const detail = timedOut
|
|
115
|
+
? `timed out after ${Math.round(timeoutMs / 100) / 10} s`
|
|
116
|
+
: toText(error?.cause?.code || error?.code || error?.cause?.message || error?.message) || 'network error';
|
|
117
|
+
return new RemoteRelayError(
|
|
118
|
+
REMOTE_RELAY_ERROR_CODES.offline,
|
|
119
|
+
`${relayLabel(relay)} is not reachable (${detail})`,
|
|
120
|
+
{ detail },
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function httpError(relay, status, body) {
|
|
125
|
+
const remoteError = isPlainObject(body) && toText(body.error) ? toText(body.error).slice(0, REMOTE_ERROR_TEXT_MAX) : null;
|
|
126
|
+
const remoteCode = isPlainObject(body) && toText(body.code) ? toText(body.code).slice(0, 100) : null;
|
|
127
|
+
const extras = { status, detail: remoteCode, remoteError, remoteBody: isPlainObject(body) ? body : null };
|
|
128
|
+
const label = relayLabel(relay);
|
|
129
|
+
if (status === 401) {
|
|
130
|
+
return new RemoteRelayError(REMOTE_RELAY_ERROR_CODES.unauthorized, `${label} rejected the token (401)`, extras);
|
|
131
|
+
}
|
|
132
|
+
if (status === 403 && remoteCode === REMOTE_RELAY_ERROR_CODES.inboundDisabled) {
|
|
133
|
+
return new RemoteRelayError(
|
|
134
|
+
REMOTE_RELAY_ERROR_CODES.inboundDisabled,
|
|
135
|
+
`${label} does not accept prompts from other relays' agents (its inbound switch is off)`,
|
|
136
|
+
extras,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (status === 404) {
|
|
140
|
+
return new RemoteRelayError(REMOTE_RELAY_ERROR_CODES.notFound, `${label}: not found${remoteError ? ` (${remoteError})` : ''}`, extras);
|
|
141
|
+
}
|
|
142
|
+
return new RemoteRelayError(
|
|
143
|
+
`${REMOTE_RELAY_ERROR_CODES.httpPrefix}${status}`,
|
|
144
|
+
`${label} answered HTTP ${status}${remoteError ? `: ${remoteError}` : ''}`,
|
|
145
|
+
extras,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function capText(value, max) {
|
|
150
|
+
const text = toText(value);
|
|
151
|
+
return text ? text.slice(0, max) : '';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeIdentity(identity, host) {
|
|
155
|
+
const remoteRelays = isPlainObject(identity.remoteRelays) ? identity.remoteRelays : {};
|
|
156
|
+
const protocol = Number(remoteRelays.protocol);
|
|
157
|
+
const publicUrl = capText(identity.publicUrl, 300);
|
|
158
|
+
return {
|
|
159
|
+
relayId: capText(identity.relayId, 100) || null,
|
|
160
|
+
name: capText(identity.name, 60) || host,
|
|
161
|
+
version: capText(identity.version, 40) || null,
|
|
162
|
+
platform: capText(identity.platform, 20) || null,
|
|
163
|
+
publicUrl: publicUrl && /^https?:\/\//i.test(publicUrl) ? publicUrl : null,
|
|
164
|
+
// A relay that serves the identity route speaks at least protocol 1.
|
|
165
|
+
protocol: Number.isInteger(protocol) && protocol >= 0 ? protocol : 1,
|
|
166
|
+
inbound: remoteRelays.inbound !== false,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function createRemoteRelayClient({
|
|
171
|
+
fetchImpl = globalThis.fetch,
|
|
172
|
+
getOwnRelayId = () => '',
|
|
173
|
+
getOwnToken = () => '',
|
|
174
|
+
timeoutMs: defaultTimeoutMs = REMOTE_RELAY_LIMITS.requestTimeoutMs,
|
|
175
|
+
maxBytes = REMOTE_RELAY_LIMITS.responseMaxBytes,
|
|
176
|
+
} = {}) {
|
|
177
|
+
function tokenFor(relay) {
|
|
178
|
+
return relay?.tokenMode === 'custom'
|
|
179
|
+
? toText(relay?.token)
|
|
180
|
+
: toText(getOwnToken?.());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The request URL: the stored base (with its path prefix) plus `path` and `query`. */
|
|
184
|
+
function buildUrl(relay, path, query) {
|
|
185
|
+
const base = toText(relay?.url).replace(/\/+$/, '');
|
|
186
|
+
const route = String(path || '');
|
|
187
|
+
if (!route.startsWith('/') || route.startsWith('//')) {
|
|
188
|
+
throw new RemoteRelayError(REMOTE_RELAY_ERROR_CODES.invalidInput, `Invalid remote path: ${route.slice(0, 80)}`);
|
|
189
|
+
}
|
|
190
|
+
let baseUrl;
|
|
191
|
+
let target;
|
|
192
|
+
try {
|
|
193
|
+
baseUrl = new URL(base);
|
|
194
|
+
target = new URL(`${base}${route}`);
|
|
195
|
+
} catch {
|
|
196
|
+
throw new RemoteRelayError(REMOTE_RELAY_ERROR_CODES.unsupported, `${relayLabel(relay)} has no valid address`);
|
|
197
|
+
}
|
|
198
|
+
// `..` segments must not climb out of the relay's path prefix.
|
|
199
|
+
const prefix = `${baseUrl.pathname.replace(/\/+$/, '')}/`;
|
|
200
|
+
if (target.origin !== baseUrl.origin || !target.pathname.startsWith(prefix)) {
|
|
201
|
+
throw new RemoteRelayError(REMOTE_RELAY_ERROR_CODES.invalidInput, `Invalid remote path: ${route.slice(0, 80)}`);
|
|
202
|
+
}
|
|
203
|
+
if (isPlainObject(query)) {
|
|
204
|
+
for (const [key, value] of Object.entries(query)) {
|
|
205
|
+
if (value === undefined || value === null) continue;
|
|
206
|
+
target.searchParams.set(key, String(value));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return target;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* One call to a remote relay. `relay` is a registry entry ({ url, tokenMode,
|
|
214
|
+
* token?, name? }). Resolves to the parsed JSON body (null when empty);
|
|
215
|
+
* rejects with a RemoteRelayError.
|
|
216
|
+
*/
|
|
217
|
+
async function request(relay, method, path, { body, query, hops = 1, timeoutMs } = {}) {
|
|
218
|
+
const target = buildUrl(relay, path, query);
|
|
219
|
+
const policy = checkRemoteRelayUrlPolicy(target);
|
|
220
|
+
if (!policy.ok) {
|
|
221
|
+
throw new RemoteRelayError(REMOTE_RELAY_ERROR_CODES.unsupported, `${relayLabel(relay)}: ${policy.error}`);
|
|
222
|
+
}
|
|
223
|
+
const token = tokenFor(relay);
|
|
224
|
+
if (!token) {
|
|
225
|
+
throw new RemoteRelayError(REMOTE_RELAY_ERROR_CODES.unauthorized, `${relayLabel(relay)} has no token configured`);
|
|
226
|
+
}
|
|
227
|
+
if (typeof fetchImpl !== 'function') {
|
|
228
|
+
throw new RemoteRelayError(REMOTE_RELAY_ERROR_CODES.offline, 'No HTTP client is available', { detail: 'fetch unavailable' });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const hopCount = Number.isInteger(Number(hops)) && Number(hops) >= 0 ? Number(hops) : 1;
|
|
232
|
+
const headers = {
|
|
233
|
+
Authorization: `Bearer ${token}`,
|
|
234
|
+
Accept: 'application/json',
|
|
235
|
+
};
|
|
236
|
+
const ownRelayId = toText(getOwnRelayId?.());
|
|
237
|
+
if (ownRelayId) headers[REMOTE_RELAY_HEADERS.origin] = ownRelayId;
|
|
238
|
+
headers[REMOTE_RELAY_HEADERS.hops] = String(hopCount);
|
|
239
|
+
const hasBody = body !== undefined;
|
|
240
|
+
if (hasBody) headers['Content-Type'] = 'application/json';
|
|
241
|
+
|
|
242
|
+
const effectiveTimeoutMs = Number.isFinite(Number(timeoutMs)) && Number(timeoutMs) > 0
|
|
243
|
+
? Number(timeoutMs)
|
|
244
|
+
: defaultTimeoutMs;
|
|
245
|
+
|
|
246
|
+
let response;
|
|
247
|
+
try {
|
|
248
|
+
response = await fetchImpl(target.href, {
|
|
249
|
+
method: String(method || 'GET').toUpperCase(),
|
|
250
|
+
headers,
|
|
251
|
+
...(hasBody ? { body: JSON.stringify(body) } : {}),
|
|
252
|
+
// A redirect would carry the bearer token to wherever it points.
|
|
253
|
+
redirect: 'manual',
|
|
254
|
+
signal: AbortSignal.timeout(effectiveTimeoutMs),
|
|
255
|
+
});
|
|
256
|
+
} catch (error) {
|
|
257
|
+
throw networkError(relay, error, effectiveTimeoutMs);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const status = Number(response?.status) || 0;
|
|
261
|
+
if (response?.type === 'opaqueredirect' || (status >= 300 && status < 400)) {
|
|
262
|
+
try { await response.body?.cancel?.(); } catch {}
|
|
263
|
+
throw new RemoteRelayError(
|
|
264
|
+
`${REMOTE_RELAY_ERROR_CODES.httpPrefix}${status || '3xx'}`,
|
|
265
|
+
`${relayLabel(relay)} answered with a redirect; redirects are refused so the token cannot follow them (check the address, or a login page in front of the relay)`,
|
|
266
|
+
{ status: status || null, detail: 'redirect refused' },
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let text;
|
|
271
|
+
try {
|
|
272
|
+
text = await readCappedText(response, relay, maxBytes);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error instanceof RemoteRelayError) throw error;
|
|
275
|
+
throw networkError(relay, error, effectiveTimeoutMs);
|
|
276
|
+
}
|
|
277
|
+
const parsed = parseJson(text);
|
|
278
|
+
|
|
279
|
+
if (status < 200 || status >= 300) throw httpError(relay, status, parsed.value);
|
|
280
|
+
if (!parsed.ok) {
|
|
281
|
+
throw new RemoteRelayError(
|
|
282
|
+
REMOTE_RELAY_ERROR_CODES.unsupported,
|
|
283
|
+
`${relayLabel(relay)} did not answer with JSON (is this an OAR relay address?)`,
|
|
284
|
+
{ status, detail: 'not json' },
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return parsed.value;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Identity of the relay at `baseUrl`, used when adding one and by the health
|
|
292
|
+
* check. A relay that predates remote relays (the identity route answers
|
|
293
|
+
* 404) is described from /api/status and its PWA app name: protocol 0.
|
|
294
|
+
*/
|
|
295
|
+
async function probe(baseUrl, token, { timeoutMs } = {}) {
|
|
296
|
+
const url = toText(baseUrl).replace(/\/+$/, '');
|
|
297
|
+
const host = hostOf(url);
|
|
298
|
+
const relay = token ? { url, tokenMode: 'custom', token } : { url, tokenMode: 'own' };
|
|
299
|
+
const unexpected = () => new RemoteRelayError(
|
|
300
|
+
REMOTE_RELAY_ERROR_CODES.unsupported,
|
|
301
|
+
`${relayLabel(relay)} did not answer like an OAR relay`,
|
|
302
|
+
{ detail: 'unexpected identity' },
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const identity = await request(relay, 'GET', IDENTITY_PATH, { timeoutMs });
|
|
307
|
+
if (!isPlainObject(identity)) throw unexpected();
|
|
308
|
+
return normalizeIdentity(identity, host);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (!(error instanceof RemoteRelayError) || error.code !== REMOTE_RELAY_ERROR_CODES.notFound) throw error;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const status = await request(relay, 'GET', LEGACY_STATUS_PATH, { timeoutMs });
|
|
314
|
+
if (!isPlainObject(status)) throw unexpected();
|
|
315
|
+
let appName = '';
|
|
316
|
+
try {
|
|
317
|
+
const pwa = await request(relay, 'GET', LEGACY_NAME_PATH, { timeoutMs });
|
|
318
|
+
appName = capText(pwa?.appName, 60);
|
|
319
|
+
} catch {}
|
|
320
|
+
return {
|
|
321
|
+
relayId: null,
|
|
322
|
+
name: appName || host,
|
|
323
|
+
version: capText(status.version, 40) || null,
|
|
324
|
+
platform: capText(status.platform, 20) || null,
|
|
325
|
+
publicUrl: null,
|
|
326
|
+
protocol: 0,
|
|
327
|
+
inbound: true,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return { tokenFor, request, probe };
|
|
332
|
+
}
|