@oceanum/oceanumlab 4.6.2 → 4.7.1

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 (62) hide show
  1. package/README.md +102 -11
  2. package/lib/DatameshUI.d.ts +1 -6
  3. package/lib/DatameshUI.js +1 -9
  4. package/lib/DatameshWidget.d.ts +24 -2
  5. package/lib/DatameshWidget.js +171 -45
  6. package/lib/StoredNotebooks.d.ts +16 -0
  7. package/lib/StoredNotebooks.js +84 -0
  8. package/lib/Tabs.d.ts +26 -0
  9. package/lib/Tabs.js +45 -0
  10. package/lib/aiBackend.d.ts +66 -0
  11. package/lib/aiBackend.js +101 -0
  12. package/lib/auth/accountWidget.d.ts +35 -0
  13. package/lib/auth/accountWidget.js +95 -0
  14. package/lib/auth/claims.d.ts +3 -0
  15. package/lib/auth/claims.js +25 -0
  16. package/lib/auth/plugin.d.ts +9 -0
  17. package/lib/auth/plugin.js +126 -0
  18. package/lib/auth/serverAuth.d.ts +79 -0
  19. package/lib/auth/serverAuth.js +253 -0
  20. package/lib/auth/tokens.d.ts +95 -0
  21. package/lib/auth/tokens.js +9 -0
  22. package/lib/chatRouter.d.ts +17 -1
  23. package/lib/chatRouter.js +34 -9
  24. package/lib/constants.d.ts +1 -1
  25. package/lib/constants.js +1 -1
  26. package/lib/datameshUiUrl.d.ts +2 -2
  27. package/lib/datameshUiUrl.js +8 -16
  28. package/lib/httpUrl.d.ts +9 -0
  29. package/lib/httpUrl.js +20 -0
  30. package/lib/index.d.ts +8 -1
  31. package/lib/index.js +67 -15
  32. package/lib/specStore.d.ts +37 -0
  33. package/lib/specStore.js +57 -0
  34. package/package.json +9 -1
  35. package/schema/datamesh-connect.json +3 -6
  36. package/src/DatameshUI.tsx +1 -10
  37. package/src/DatameshWidget.tsx +294 -59
  38. package/src/StoredNotebooks.tsx +166 -0
  39. package/src/Tabs.tsx +102 -0
  40. package/src/__tests__/DatameshWidget.spec.tsx +393 -0
  41. package/src/__tests__/OceanumPanel.spec.tsx +101 -0
  42. package/src/__tests__/Tabs.spec.tsx +168 -0
  43. package/src/__tests__/aiBackend.spec.ts +271 -0
  44. package/src/__tests__/chatRouter.spec.ts +253 -17
  45. package/src/__tests__/datameshUiUrl.spec.ts +4 -3
  46. package/src/__tests__/pluginArity.spec.ts +24 -0
  47. package/src/__tests__/serverAuth.spec.ts +277 -0
  48. package/src/__tests__/settingsSchema.spec.ts +73 -0
  49. package/src/__tests__/specStore.spec.ts +126 -0
  50. package/src/aiBackend.ts +150 -0
  51. package/src/auth/accountWidget.ts +113 -0
  52. package/src/auth/claims.ts +30 -0
  53. package/src/auth/plugin.ts +155 -0
  54. package/src/auth/serverAuth.ts +341 -0
  55. package/src/auth/tokens.ts +104 -0
  56. package/src/chatRouter.ts +41 -9
  57. package/src/constants.ts +1 -1
  58. package/src/datameshUiUrl.ts +8 -15
  59. package/src/httpUrl.ts +23 -0
  60. package/src/index.ts +99 -33
  61. package/src/specStore.ts +82 -0
  62. package/style/index.css +158 -0
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Oceanum.io sign-in on an ordinary JupyterLab, where the Jupyter server runs the OAuth
3
+ * device grant (oceanumlab/auth.py) and this only reflects its state.
4
+ *
5
+ * A browser sign-in cannot work here. Popup, redirect and the silent iframe check all send
6
+ * the page's own origin to Auth0 as the callback, Auth0 only accepts addresses registered in
7
+ * advance, and it cannot wildcard a port or an arbitrary host -- so everywhere but Oceanum's
8
+ * own sites the answer is "Callback URL mismatch". The device grant has no callback: the
9
+ * user opens a link and confirms a code, and the server collects the tokens.
10
+ *
11
+ * The refresh token and the device code never reach this page. It is handed short-lived
12
+ * access tokens only.
13
+ */
14
+ import { PromiseDelegate } from '@lumino/coreutils';
15
+ import { Signal } from '@lumino/signaling';
16
+ import { userFromClaims } from './claims';
17
+ /**
18
+ * The page option the server extension publishes when it runs the device sign-in. Must
19
+ * match SERVER_AUTH_OPTION in oceanumlab/__init__.py.
20
+ */
21
+ export const SERVER_AUTH_OPTION = 'oceanumServerAuth';
22
+ function httpUrl(value) {
23
+ if (typeof value !== 'string' || value.trim() === '') {
24
+ return null;
25
+ }
26
+ try {
27
+ const url = new URL(value);
28
+ return url.protocol === 'https:' || url.protocol === 'http:'
29
+ ? url.href.replace(/\/+$/, '')
30
+ : null;
31
+ }
32
+ catch (_a) {
33
+ return null;
34
+ }
35
+ }
36
+ /**
37
+ * The Oceanum deployment the server signs in to, from the page option, or `null` when the
38
+ * server offers no device sign-in (or the option is malformed, which is treated the same).
39
+ */
40
+ export function readServerAuthOption(option, hostname) {
41
+ var _a;
42
+ if (!option) {
43
+ return null;
44
+ }
45
+ let raw;
46
+ try {
47
+ raw = JSON.parse(option);
48
+ }
49
+ catch (_b) {
50
+ return null;
51
+ }
52
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
53
+ return null;
54
+ }
55
+ const config = raw;
56
+ const urlsIn = ((_a = config.urls) !== null && _a !== void 0 ? _a : {});
57
+ const datamesh = httpUrl(urlsIn.datamesh);
58
+ const specs = httpUrl(urlsIn.specs);
59
+ const manage = httpUrl(urlsIn.manage);
60
+ if (typeof config.auth0Domain !== 'string' ||
61
+ typeof config.clientId !== 'string' ||
62
+ !datamesh ||
63
+ !specs ||
64
+ !manage) {
65
+ return null;
66
+ }
67
+ const urls = {
68
+ datamesh,
69
+ specs,
70
+ manage,
71
+ ...(httpUrl(urlsIn.datameshUi)
72
+ ? { datameshUi: httpUrl(urlsIn.datameshUi) }
73
+ : {}),
74
+ ...(httpUrl(urlsIn.ai) ? { ai: httpUrl(urlsIn.ai) } : {})
75
+ };
76
+ return {
77
+ // The device grant is indifferent to where the page is served from, so this is simply
78
+ // wherever that is, rather than a list to match against.
79
+ hosts: [hostname.toLowerCase()],
80
+ auth0Domain: config.auth0Domain,
81
+ clientId: config.clientId,
82
+ oceanumDomain: typeof config.oceanumDomain === 'string' ? config.oceanumDomain : '',
83
+ signInRedirect: false,
84
+ urls
85
+ };
86
+ }
87
+ export class ServerAuth {
88
+ constructor(options) {
89
+ var _a, _b;
90
+ this._ready = new PromiseDelegate();
91
+ this._user = null;
92
+ this._token = null;
93
+ this._pending = null;
94
+ this._error = null;
95
+ this._signingOut = false;
96
+ this._disposed = false;
97
+ this._timer = null;
98
+ this._userChanged = new Signal(this);
99
+ this._tokenChanged = new Signal(this);
100
+ this._stateChanged = new Signal(this);
101
+ this.environment = options.environment;
102
+ this._request = options.request;
103
+ this._onSessionEnded = options.onSessionEnded;
104
+ this._pendingPollMs = (_a = options.pendingPollMs) !== null && _a !== void 0 ? _a : 2000;
105
+ this._tokenPollMs = (_b = options.tokenPollMs) !== null && _b !== void 0 ? _b : 60000;
106
+ if (!this.environment) {
107
+ this._ready.resolve();
108
+ return;
109
+ }
110
+ // A server that signed in before this page loaded (a reload, a second tab) is already
111
+ // signed in; `ready` waits for that answer so nothing downstream sees a false sign-out.
112
+ void this._sync('token').finally(() => this._ready.resolve());
113
+ }
114
+ get urls() {
115
+ var _a, _b;
116
+ return (_b = (_a = this.environment) === null || _a === void 0 ? void 0 : _a.urls) !== null && _b !== void 0 ? _b : null;
117
+ }
118
+ get ready() {
119
+ return this._ready.promise;
120
+ }
121
+ get user() {
122
+ return this._user;
123
+ }
124
+ get userChanged() {
125
+ return this._userChanged;
126
+ }
127
+ get tokenChanged() {
128
+ return this._tokenChanged;
129
+ }
130
+ /** The sign-in waiting on the user, if any. */
131
+ get pending() {
132
+ return this._pending;
133
+ }
134
+ /** Why the last sign-in attempt ended, if it failed. */
135
+ get error() {
136
+ return this._error;
137
+ }
138
+ /** Emits when `pending` or `error` changes. */
139
+ get stateChanged() {
140
+ return this._stateChanged;
141
+ }
142
+ async signIn() {
143
+ if (!this.environment || this._user) {
144
+ return;
145
+ }
146
+ await this._sync('start', 'POST');
147
+ }
148
+ /** Abandon a sign-in that is waiting on the user. */
149
+ async cancelSignIn() {
150
+ if (this._pending) {
151
+ await this._sync('cancel', 'POST');
152
+ }
153
+ }
154
+ async signOut() {
155
+ this._signingOut = true;
156
+ try {
157
+ await this._sync('signout', 'POST');
158
+ }
159
+ finally {
160
+ this._signingOut = false;
161
+ }
162
+ }
163
+ async getAccessToken() {
164
+ if (!this.environment || !this._user) {
165
+ return null;
166
+ }
167
+ await this._sync('token');
168
+ return this._token;
169
+ }
170
+ /** Re-read the server's state, e.g. when the tab becomes visible again. */
171
+ async refresh() {
172
+ if (this.environment) {
173
+ await this._sync(this._user ? 'token' : 'status');
174
+ }
175
+ }
176
+ get isDisposed() {
177
+ return this._disposed;
178
+ }
179
+ dispose() {
180
+ if (this._disposed) {
181
+ return;
182
+ }
183
+ this._disposed = true;
184
+ this._clearTimer();
185
+ Signal.clearData(this);
186
+ }
187
+ async _sync(action, method = 'GET') {
188
+ let status;
189
+ try {
190
+ status = (await this._request(action, method));
191
+ }
192
+ catch (reason) {
193
+ // The Jupyter server being briefly unreachable is not a sign-out: keep what we have
194
+ // and let the next poll find out.
195
+ console.warn(`Oceanum.io sign-in: ${action} failed`, reason);
196
+ this._schedule();
197
+ return;
198
+ }
199
+ if (this._disposed || !status || typeof status.state !== 'string') {
200
+ return;
201
+ }
202
+ this._apply(status);
203
+ if (status.state === 'signedIn' && action !== 'token') {
204
+ // A sign-in that has just completed: fetch the token it produced.
205
+ await this._sync('token');
206
+ }
207
+ }
208
+ _apply(status) {
209
+ var _a, _b, _c, _d, _e, _f, _g;
210
+ const user = status.state === 'signedIn' ? userFromClaims(status.claims) : null;
211
+ const pending = status.state === 'pending' ? status.pending : null;
212
+ const error = (_a = status.error) !== null && _a !== void 0 ? _a : null;
213
+ if ((pending === null || pending === void 0 ? void 0 : pending.userCode) !== ((_b = this._pending) === null || _b === void 0 ? void 0 : _b.userCode) ||
214
+ error !== this._error) {
215
+ this._pending = pending;
216
+ this._error = error;
217
+ this._stateChanged.emit();
218
+ }
219
+ const wasSignedIn = this._user !== null;
220
+ if (((_c = user === null || user === void 0 ? void 0 : user.sub) !== null && _c !== void 0 ? _c : null) !== ((_e = (_d = this._user) === null || _d === void 0 ? void 0 : _d.sub) !== null && _e !== void 0 ? _e : null)) {
221
+ this._user = user;
222
+ this._userChanged.emit(user);
223
+ if (wasSignedIn && !user && !this._signingOut) {
224
+ (_f = this._onSessionEnded) === null || _f === void 0 ? void 0 : _f.call(this, error !== null && error !== void 0 ? error : 'Your Oceanum.io session has ended. Sign in again.');
225
+ }
226
+ }
227
+ const token = user ? ((_g = status.accessToken) !== null && _g !== void 0 ? _g : this._token) : null;
228
+ if (token !== this._token) {
229
+ this._token = token;
230
+ this._tokenChanged.emit(token);
231
+ }
232
+ this._schedule();
233
+ }
234
+ /** Poll quickly while a sign-in is pending, slowly while signed in, not at all otherwise. */
235
+ _schedule() {
236
+ this._clearTimer();
237
+ if (this._disposed) {
238
+ return;
239
+ }
240
+ if (this._pending) {
241
+ this._timer = setTimeout(() => void this._sync('status'), this._pendingPollMs);
242
+ }
243
+ else if (this._user) {
244
+ this._timer = setTimeout(() => void this._sync('token'), this._tokenPollMs);
245
+ }
246
+ }
247
+ _clearTimer() {
248
+ if (this._timer !== null) {
249
+ clearTimeout(this._timer);
250
+ this._timer = null;
251
+ }
252
+ }
253
+ }
@@ -0,0 +1,95 @@
1
+ import { Token } from '@lumino/coreutils';
2
+ import { ISignal } from '@lumino/signaling';
3
+ /**
4
+ * One Oceanum deployment (production or development).
5
+ */
6
+ export interface IOceanumEnvironment {
7
+ /** Page hostnames served by this environment, e.g. `notebook.oceanum.io`. */
8
+ readonly hosts: readonly string[];
9
+ /** Auth0 tenant domain, e.g. `auth.oceanum.io`. */
10
+ readonly auth0Domain: string;
11
+ /** Auth0 SPA client id. */
12
+ readonly clientId: string;
13
+ /**
14
+ * The Oceanum apps domain, e.g. `oceanum.io`. The Oceanum nav builds its app, account,
15
+ * docs and sign-out URLs from it (`https://platform.oceanum.io/account`, ...).
16
+ */
17
+ readonly oceanumDomain: string;
18
+ /** Service base URLs for this environment. */
19
+ readonly urls: IOceanumServiceUrls;
20
+ /**
21
+ * Whether to check for an existing Oceanum.io session with a full-page redirect when the
22
+ * silent iframe check finds none. Only for a tenant on another site than the notebook
23
+ * (browsers that block third-party cookies hide the session from the iframe), and only where
24
+ * the site origin is an allowed callback URL.
25
+ */
26
+ readonly signInRedirect: boolean;
27
+ }
28
+ /** Service base URLs for an environment, without trailing slashes. */
29
+ export interface IOceanumServiceUrls {
30
+ /** Datamesh gateway, e.g. `https://datamesh.oceanum.io`. */
31
+ readonly datamesh: string;
32
+ /** Spec store, e.g. `https://specs.oceanum.io`. */
33
+ readonly specs: string;
34
+ /** User management service (Auth0 user metadata), e.g. `https://manage.oceanum.io`. */
35
+ readonly manage: string;
36
+ /**
37
+ * Datamesh UI, e.g. `https://ui.datamesh.oceanum.io`, shown by oceanumlab's Datamesh panel.
38
+ * Optional: without it, Oceanum production applies.
39
+ */
40
+ readonly datameshUi?: string;
41
+ /**
42
+ * Oceanum AI backend, e.g. `https://ai.oceanum.io`, called by oceanumlab's AI chat.
43
+ * Optional: without it, Oceanum production applies.
44
+ */
45
+ readonly ai?: string;
46
+ }
47
+ export type ColorScheme = 'light' | 'dark' | 'auto';
48
+ /** The signed-in user, from the Auth0 ID token claims. */
49
+ export interface IOceanumUser {
50
+ readonly sub: string;
51
+ readonly email: string;
52
+ readonly name: string | null;
53
+ /** `https://oceanum.io/active_org`. */
54
+ readonly activeOrg: string | null;
55
+ /** `https://oceanum.io/color_scheme`. */
56
+ readonly colorScheme: ColorScheme | null;
57
+ }
58
+ /**
59
+ * Oceanum.io sign-in state, shared with other Oceanum Notebook extensions.
60
+ */
61
+ export interface IOceanumAuth {
62
+ /** The environment for this page, or `null` if sign-in is not configured for this host. */
63
+ readonly environment: IOceanumEnvironment | null;
64
+ /** Service URLs for `environment`, or `null` when it is `null`. */
65
+ readonly urls: IOceanumServiceUrls | null;
66
+ /** Resolves once any login redirect has been handled and the session restored. */
67
+ readonly ready: Promise<void>;
68
+ /** The signed-in user, or `null`. */
69
+ readonly user: IOceanumUser | null;
70
+ /** Emits the new user (or `null`) on sign-in and sign-out. */
71
+ readonly userChanged: ISignal<IOceanumAuth, IOceanumUser | null>;
72
+ /** Emits the new access token (or `null`) whenever it changes, including refreshes. */
73
+ readonly tokenChanged: ISignal<IOceanumAuth, string | null>;
74
+ /**
75
+ * Start sign-in without leaving the page, so its kernels survive. Resolves once sign-in has
76
+ * started, not when it completes; watch `userChanged` for the result. Does nothing while a
77
+ * user is signed in.
78
+ */
79
+ signIn(): Promise<void>;
80
+ /** Sign out of Oceanum.io and clear the session. */
81
+ signOut(): Promise<void>;
82
+ /**
83
+ * A current access token, refreshed if it is near expiry, or `null` when signed out.
84
+ * Never cache the result: Auth0 access tokens are short-lived.
85
+ */
86
+ getAccessToken(): Promise<string | null>;
87
+ }
88
+ /**
89
+ * The sign-in contract. oceanumlab owns it and exports it from its package entry, because a
90
+ * Lumino token is matched by object identity: every extension that provides or consumes it
91
+ * has to import this one object, which JupyterLab arranges by sharing `@oceanum/oceanumlab`
92
+ * as a singleton module. On an ordinary JupyterLab oceanumlab's own device sign-in provides
93
+ * it; on notebook.oceanum.io the notebook's Oceanum widget extension does.
94
+ */
95
+ export declare const IOceanumAuth: Token<IOceanumAuth>;
@@ -0,0 +1,9 @@
1
+ import { Token } from '@lumino/coreutils';
2
+ /**
3
+ * The sign-in contract. oceanumlab owns it and exports it from its package entry, because a
4
+ * Lumino token is matched by object identity: every extension that provides or consumes it
5
+ * has to import this one object, which JupyterLab arranges by sharing `@oceanum/oceanumlab`
6
+ * as a singleton module. On an ordinary JupyterLab oceanumlab's own device sign-in provides
7
+ * it; on notebook.oceanum.io the notebook's Oceanum widget extension does.
8
+ */
9
+ export const IOceanumAuth = new Token('@oceanum/oceanumlab:IOceanumAuth', 'Oceanum.io sign-in state and Datamesh access tokens.');
@@ -1,4 +1,5 @@
1
1
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
2
+ import { type AuthCommands } from './aiBackend';
2
3
  import type { Progress } from './progress';
3
4
  import type { ObservedRun } from './notebookRun';
4
5
  import type { INotebookSnapshot } from './notebookContext';
@@ -36,14 +37,29 @@ export declare class ChatRouterError extends Error {
36
37
  }
37
38
  export declare class ChatRouter {
38
39
  private _settings;
40
+ private _commands;
39
41
  private _notebook;
40
42
  /**
43
+ * The Oceanum AI backend's address: the signed-in deployment's, or empty for the
44
+ * production default. Deployment configuration, not a setting -- a user-editable
45
+ * address is somewhere to send the user's credential.
46
+ */
47
+ private _backend;
48
+ /**
49
+ * @param _commands Where the host's Oceanum.io sign-in is asked for, when
50
+ * there is no pasted Datamesh token.
41
51
  * @param _notebook What the conversation's notebook holds -- the one its
42
52
  * answers are placed in -- or null if there is nothing to send. Asked on
43
53
  * every request, so it follows the conversation rather than whichever tab
44
54
  * happens to be active.
45
55
  */
46
- constructor(_settings: ISettingRegistry.ISettings, _notebook: () => Promise<INotebookSnapshot | null>);
56
+ constructor(_settings: ISettingRegistry.ISettings, _commands: AuthCommands, _notebook: () => Promise<INotebookSnapshot | null>,
57
+ /**
58
+ * The Oceanum AI backend's address: the signed-in deployment's, or empty for the
59
+ * production default. Deployment configuration, not a setting -- a user-editable
60
+ * address is somewhere to send the user's credential.
61
+ */
62
+ _backend?: () => string);
47
63
  route(prompt: string, chatHistory?: ChatMessage[], signal?: AbortSignal, onProgress?: (progress: Progress) => void): Promise<RouteResult>;
48
64
  /**
49
65
  * The notebook half of the execute loop: the code ran in the user's own
package/lib/chatRouter.js CHANGED
@@ -1,4 +1,4 @@
1
- import { OCEANUM_AI_BACKEND_URL } from './constants';
1
+ import { aiBackendUrl, canSignIn, resolveAiCredential, signInToken } from './aiBackend';
2
2
  import { readSse } from './sse';
3
3
  import { formatNotebookCells } from './notebookContext';
4
4
  export class ChatRouterError extends Error {
@@ -31,14 +31,24 @@ function asOceanumResponse(data) {
31
31
  }
32
32
  export class ChatRouter {
33
33
  /**
34
+ * @param _commands Where the host's Oceanum.io sign-in is asked for, when
35
+ * there is no pasted Datamesh token.
34
36
  * @param _notebook What the conversation's notebook holds -- the one its
35
37
  * answers are placed in -- or null if there is nothing to send. Asked on
36
38
  * every request, so it follows the conversation rather than whichever tab
37
39
  * happens to be active.
38
40
  */
39
- constructor(_settings, _notebook) {
41
+ constructor(_settings, _commands, _notebook,
42
+ /**
43
+ * The Oceanum AI backend's address: the signed-in deployment's, or empty for the
44
+ * production default. Deployment configuration, not a setting -- a user-editable
45
+ * address is somewhere to send the user's credential.
46
+ */
47
+ _backend = () => '') {
40
48
  this._settings = _settings;
49
+ this._commands = _commands;
41
50
  this._notebook = _notebook;
51
+ this._backend = _backend;
42
52
  }
43
53
  async route(prompt, chatHistory = [], signal, onProgress) {
44
54
  const { payload, isCodeCell, context } = await this._gather(prompt, chatHistory);
@@ -99,17 +109,22 @@ export class ChatRouter {
99
109
  }
100
110
  async _send(path, payload, signal, onProgress) {
101
111
  var _a;
102
- const token = this._settings.get('datameshToken').composite;
103
- if (!token) {
104
- throw new ChatRouterError('Datamesh token not configured. Set your token in Settings → Oceanum.io.');
112
+ // Resolved per request, not once: the sign-in token refreshes, and the
113
+ // user can sign in, sign out or paste a token between two requests.
114
+ const credential = await resolveAiCredential(this._settings.get('datameshToken').composite, signInToken(this._commands));
115
+ if (!credential) {
116
+ throw new ChatRouterError(canSignIn(this._commands)
117
+ ? 'Not signed in. Sign in to Oceanum.io to use Oceanum AI, or set a Datamesh token in Settings → Oceanum.io.'
118
+ : 'Datamesh token not configured. Set your token in Settings → Oceanum.io.');
105
119
  }
120
+ const backend = aiBackendUrl(this._backend());
106
121
  let response;
107
122
  try {
108
- response = await fetch(`${OCEANUM_AI_BACKEND_URL}${path}`, {
123
+ response = await fetch(`${backend}${path}`, {
109
124
  method: 'POST',
110
125
  headers: {
111
126
  'Content-Type': 'application/json',
112
- 'X-Datamesh-Token': token,
127
+ ...credential.headers,
113
128
  // Ask for the streamed form only when someone is listening. The
114
129
  // server requires the media type to be NAMED -- a wildcard never
115
130
  // selects it -- so omitting this is what keeps the plain JSON path
@@ -126,10 +141,20 @@ export class ChatRouter {
126
141
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
127
142
  throw err;
128
143
  }
129
- throw new ChatRouterError(`Could not reach Oceanum AI backend at ${OCEANUM_AI_BACKEND_URL}. Is it running?`);
144
+ throw new ChatRouterError(`Could not reach Oceanum AI backend at ${backend}. Is it running?`);
130
145
  }
131
146
  if (response.status === 401) {
132
- throw new ChatRouterError('Invalid or expired Datamesh token.', 401);
147
+ let message = 'Invalid or expired Datamesh token.';
148
+ if (credential.source === 'sign-in') {
149
+ message =
150
+ 'Your Oceanum.io sign-in was not accepted. Sign in to Oceanum.io again.';
151
+ }
152
+ else if (canSignIn(this._commands)) {
153
+ // A pasted token wins over the sign-in, so say how to fall back to it.
154
+ message +=
155
+ ' Update it, or clear it to use your Oceanum.io sign-in, in Settings → Oceanum.io.';
156
+ }
157
+ throw new ChatRouterError(message, 401);
133
158
  }
134
159
  if (!response.ok) {
135
160
  let detail = '';
@@ -2,7 +2,7 @@
2
2
  * Build-time constants for Oceanum AI integration.
3
3
  * These values are compiled into the extension.
4
4
  */
5
- /** URL of the Oceanum AI backend service */
5
+ /** Default URL of the Oceanum AI backend service, where the deployment names none. */
6
6
  export declare const OCEANUM_AI_BACKEND_URL = "https://ai.oceanum.io";
7
7
  /** URL of the Datamesh UI embedded in the Datamesh panel */
8
8
  export declare const DATAMESH_UI_SERVICE = "https://ui.datamesh.oceanum.io";
package/lib/constants.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Build-time constants for Oceanum AI integration.
3
3
  * These values are compiled into the extension.
4
4
  */
5
- /** URL of the Oceanum AI backend service */
5
+ /** Default URL of the Oceanum AI backend service, where the deployment names none. */
6
6
  export const OCEANUM_AI_BACKEND_URL = 'https://ai.oceanum.io';
7
7
  /** URL of the Datamesh UI embedded in the Datamesh panel */
8
8
  export const DATAMESH_UI_SERVICE = 'https://ui.datamesh.oceanum.io';
@@ -1,8 +1,8 @@
1
- /** The Datamesh UI address to use for a `datameshUiUrl` setting value. */
1
+ /** The Datamesh UI address to use for a configured value. */
2
2
  export declare function validDatameshUiUrl(url: string | null | undefined): URL;
3
3
  /**
4
4
  * The panel's iframe `src`: the Datamesh UI in embed mode, which hides the
5
- * Oceanum.io navigator. Any query the setting already has is kept.
5
+ * Oceanum.io navigator. Any query the address already has is kept.
6
6
  */
7
7
  export declare function datameshUiSrc(url: string | null | undefined): string;
8
8
  /**
@@ -1,28 +1,20 @@
1
1
  /**
2
2
  * The Datamesh UI panel's address, and which messages may come from it.
3
3
  *
4
- * The address is a setting (`datameshUiUrl`), so a deployment can point the
5
- * panel at another Datamesh UI -- a dev one, say. A setting is typed in by
6
- * hand, so anything that is not an http(s) URL falls back to the default
7
- * rather than ending up in an iframe's `src`.
4
+ * The address comes from the deployment's sign-in environment, so a development
5
+ * deployment can point the panel at its own Datamesh UI. It is still written by a
6
+ * person in a config file, so anything that is not an http(s) URL falls back to the
7
+ * default rather than ending up in an iframe's `src`.
8
8
  */
9
9
  import { DATAMESH_UI_SERVICE } from './constants';
10
- /** The Datamesh UI address to use for a `datameshUiUrl` setting value. */
10
+ import { validHttpUrl } from './httpUrl';
11
+ /** The Datamesh UI address to use for a configured value. */
11
12
  export function validDatameshUiUrl(url) {
12
- try {
13
- const parsed = new URL((url !== null && url !== void 0 ? url : '').trim());
14
- if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {
15
- return parsed;
16
- }
17
- }
18
- catch (_a) {
19
- // Empty or not a URL: the default below.
20
- }
21
- return new URL(DATAMESH_UI_SERVICE);
13
+ return validHttpUrl(url, DATAMESH_UI_SERVICE);
22
14
  }
23
15
  /**
24
16
  * The panel's iframe `src`: the Datamesh UI in embed mode, which hides the
25
- * Oceanum.io navigator. Any query the setting already has is kept.
17
+ * Oceanum.io navigator. Any query the address already has is kept.
26
18
  */
27
19
  export function datameshUiSrc(url) {
28
20
  const src = validDatameshUiUrl(url);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * An address typed into a setting, checked before anything uses it.
3
+ *
4
+ * A setting is typed in by hand, so anything that is not an http(s) URL falls
5
+ * back to the default rather than ending up in an iframe's `src` or a
6
+ * request's address.
7
+ */
8
+ /** `value` as an http(s) URL, or `fallback` when it is empty or not one. */
9
+ export declare function validHttpUrl(value: string | null | undefined, fallback: string): URL;
package/lib/httpUrl.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * An address typed into a setting, checked before anything uses it.
3
+ *
4
+ * A setting is typed in by hand, so anything that is not an http(s) URL falls
5
+ * back to the default rather than ending up in an iframe's `src` or a
6
+ * request's address.
7
+ */
8
+ /** `value` as an http(s) URL, or `fallback` when it is empty or not one. */
9
+ export function validHttpUrl(value, fallback) {
10
+ try {
11
+ const parsed = new URL((value !== null && value !== void 0 ? value : '').trim());
12
+ if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {
13
+ return parsed;
14
+ }
15
+ }
16
+ catch (_a) {
17
+ // Empty or not a URL: the default below.
18
+ }
19
+ return new URL(fallback);
20
+ }
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
2
  import { LabIcon } from '@jupyterlab/ui-components';
3
+ import { IOceanumAuth } from './auth/tokens';
3
4
  import '../style/index.css';
4
5
  export declare const oceanumIcon: LabIcon;
5
6
  /**
@@ -12,5 +13,11 @@ export declare const datamesh_connect_extension: JupyterFrontEndPlugin<void>;
12
13
  * `oceanum-ai:submit-prompt` command.
13
14
  */
14
15
  export declare const oceanum_ai_extension: JupyterFrontEndPlugin<void>;
15
- declare const _default: JupyterFrontEndPlugin<void>[];
16
+ /**
17
+ * The sign-in contract, exported so another extension can provide or consume it: a Lumino
18
+ * token is matched by object identity, so everyone has to import this one object, and
19
+ * JupyterLab shares `@oceanum/oceanumlab` between extensions as a singleton module.
20
+ */
21
+ export * from './auth/tokens';
22
+ declare const _default: (JupyterFrontEndPlugin<IOceanumAuth> | JupyterFrontEndPlugin<void>)[];
16
23
  export default _default;