@robosystems/core 0.8.4 → 0.8.6
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/auth-core/sso.js +23 -1
- package/components/console/ConsoleContent.js +33 -4
- package/package.json +1 -1
package/auth-core/sso.js
CHANGED
|
@@ -9,6 +9,28 @@ const debugLog = (message, error) => {
|
|
|
9
9
|
console.debug(`[SSO] ${message}`, error);
|
|
10
10
|
}
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Reduce a `returnUrl` to a same-origin path, or reject it.
|
|
14
|
+
*
|
|
15
|
+
* The value reaches us from a query parameter (or the sessionStorage mirror of
|
|
16
|
+
* one), so it is attacker-controlled on any crafted SSO link: an absolute URL
|
|
17
|
+
* would bounce the freshly authenticated user to another site, and a
|
|
18
|
+
* `javascript:` payload would execute as this origin. Cross-app returns don't
|
|
19
|
+
* need the escape hatch — `getSSORedirectUrl` hands the path to the *target*
|
|
20
|
+
* app, which resolves it against its own origin — so a bare absolute path is
|
|
21
|
+
* the only shape we ever legitimately produce.
|
|
22
|
+
*/
|
|
23
|
+
const sanitizeReturnUrl = (returnUrl) => {
|
|
24
|
+
if (!returnUrl)
|
|
25
|
+
return null;
|
|
26
|
+
// `//host` and `/\host` are both protocol-relative once a browser
|
|
27
|
+
// normalises them, so a leading slash alone isn't enough.
|
|
28
|
+
if (!returnUrl.startsWith('/'))
|
|
29
|
+
return null;
|
|
30
|
+
if (returnUrl.startsWith('//') || returnUrl.startsWith('/\\'))
|
|
31
|
+
return null;
|
|
32
|
+
return returnUrl;
|
|
33
|
+
};
|
|
12
34
|
export class SSOManager {
|
|
13
35
|
constructor(apiUrl) {
|
|
14
36
|
this.authClient = new RoboSystemsAuthClient(apiUrl);
|
|
@@ -84,7 +106,7 @@ export class SSOManager {
|
|
|
84
106
|
debugLog('sessionStorage read failed during SSO login', error);
|
|
85
107
|
}
|
|
86
108
|
const finalSessionId = sessionId; // Only use URL parameter for session ID
|
|
87
|
-
const finalReturnUrl = returnUrl || sessionStorageReturn;
|
|
109
|
+
const finalReturnUrl = sanitizeReturnUrl(returnUrl || sessionStorageReturn);
|
|
88
110
|
if (!finalSessionId) {
|
|
89
111
|
return null;
|
|
90
112
|
}
|
|
@@ -68,6 +68,10 @@ export function ConsoleContent({ config }) {
|
|
|
68
68
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
69
69
|
const terminalEndRef = useRef(null);
|
|
70
70
|
const terminalScrollRef = useRef(null);
|
|
71
|
+
// Prior operator exchanges, sent as `history` so a follow-up like "and for
|
|
72
|
+
// June?" resolves against the previous answer. Bounded to the last three
|
|
73
|
+
// exchanges — the backend seeds only its last five turns anyway.
|
|
74
|
+
const conversationRef = useRef([]);
|
|
71
75
|
const [currentQueryStartTime, setCurrentQueryStartTime] = useState(null);
|
|
72
76
|
const [apiVersion, setApiVersion] = useState(null);
|
|
73
77
|
const [operatorProgress, setOperatorProgress] = useState({ isRunning: false, message: '' });
|
|
@@ -190,6 +194,9 @@ export function ConsoleContent({ config }) {
|
|
|
190
194
|
streamingQuery.cancelQuery();
|
|
191
195
|
}
|
|
192
196
|
setTerminalMessages([]);
|
|
197
|
+
// A follow-up on the new graph must not resolve against the previous
|
|
198
|
+
// graph's answers.
|
|
199
|
+
conversationRef.current = [];
|
|
193
200
|
setCurrentQueryStartTime(null);
|
|
194
201
|
addSystemMessage(`═══════════════════════════════════════════════════════════════\n` +
|
|
195
202
|
`${config.welcome.contextLabel} context changed: ${previousGraphId.current} → ${graphId}\n` +
|
|
@@ -317,10 +324,11 @@ export function ConsoleContent({ config }) {
|
|
|
317
324
|
const startTime = Date.now();
|
|
318
325
|
try {
|
|
319
326
|
const { clients } = await import('@robosystems/client/clients');
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
327
|
+
const history = conversationRef.current;
|
|
328
|
+
const result = await clients.operator.executeQuery(graphId, Object.assign({ message: userQuery,
|
|
329
|
+
// `standard` gives the operator loop six tool turns; `quick` (three)
|
|
330
|
+
// was exhausted by schema + examples before the first query ran.
|
|
331
|
+
mode: 'standard' }, (history.length > 0 ? { history } : {})), {
|
|
324
332
|
mode: 'auto',
|
|
325
333
|
onProgress: (message, percentage) => {
|
|
326
334
|
setOperatorProgress({
|
|
@@ -331,6 +339,19 @@ export function ConsoleContent({ config }) {
|
|
|
331
339
|
},
|
|
332
340
|
});
|
|
333
341
|
setOperatorProgress({ isRunning: false, message: '' });
|
|
342
|
+
// The API reports operator failures (credit pre-flight, timeouts,
|
|
343
|
+
// unbilled calls) as HTTP 200 with `error_details` set and an
|
|
344
|
+
// explanatory `content`. Render those as errors, not as answers.
|
|
345
|
+
const errorDetails = result.error_details;
|
|
346
|
+
if (errorDetails) {
|
|
347
|
+
const detail = [
|
|
348
|
+
errorDetails.message,
|
|
349
|
+
errorDetails.error,
|
|
350
|
+
errorDetails.detail,
|
|
351
|
+
].find((v) => typeof v === 'string' && v.length > 0);
|
|
352
|
+
addErrorMessage(`Operator error: ${detail || result.content || 'The operator could not complete this request.'}`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
334
355
|
const duration = Date.now() - startTime;
|
|
335
356
|
const metadata = (result.metadata || {});
|
|
336
357
|
const creditsUsed = metadata.credits_consumed;
|
|
@@ -385,6 +406,13 @@ export function ConsoleContent({ config }) {
|
|
|
385
406
|
// plain footer, matching the direct-query result style.
|
|
386
407
|
if (narrative) {
|
|
387
408
|
addResultMessage(narrative, rows, cypher, { markdown: true, footer });
|
|
409
|
+
// Record the exchange for follow-ups. Only a narrative answer is
|
|
410
|
+
// worth carrying; the answer is clipped so history stays cheap.
|
|
411
|
+
conversationRef.current = [
|
|
412
|
+
...conversationRef.current,
|
|
413
|
+
{ role: 'user', content: userQuery },
|
|
414
|
+
{ role: 'assistant', content: narrative.slice(0, 2000) },
|
|
415
|
+
].slice(-6);
|
|
388
416
|
}
|
|
389
417
|
else {
|
|
390
418
|
addResultMessage(footer, rows, cypher);
|
|
@@ -611,6 +639,7 @@ export function ConsoleContent({ config }) {
|
|
|
611
639
|
return;
|
|
612
640
|
case '/clear':
|
|
613
641
|
setTerminalMessages([]);
|
|
642
|
+
conversationRef.current = [];
|
|
614
643
|
addSystemMessage('Console cleared.', true);
|
|
615
644
|
return;
|
|
616
645
|
case '/examples': {
|
package/package.json
CHANGED