@oceanum/oceanumlab 4.7.0 → 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.
- package/README.md +102 -11
- package/lib/DatameshUI.d.ts +1 -6
- package/lib/DatameshUI.js +1 -9
- package/lib/DatameshWidget.d.ts +18 -2
- package/lib/DatameshWidget.js +70 -11
- package/lib/StoredNotebooks.d.ts +16 -0
- package/lib/StoredNotebooks.js +84 -0
- package/lib/Tabs.d.ts +26 -0
- package/lib/Tabs.js +45 -0
- package/lib/aiBackend.d.ts +1 -1
- package/lib/aiBackend.js +1 -1
- package/lib/auth/accountWidget.d.ts +35 -0
- package/lib/auth/accountWidget.js +95 -0
- package/lib/auth/claims.d.ts +3 -0
- package/lib/auth/claims.js +25 -0
- package/lib/auth/plugin.d.ts +9 -0
- package/lib/auth/plugin.js +126 -0
- package/lib/auth/serverAuth.d.ts +79 -0
- package/lib/auth/serverAuth.js +253 -0
- package/lib/auth/tokens.d.ts +95 -0
- package/lib/auth/tokens.js +9 -0
- package/lib/chatRouter.d.ts +13 -1
- package/lib/chatRouter.js +9 -2
- package/lib/constants.d.ts +1 -1
- package/lib/constants.js +1 -1
- package/lib/datameshUiUrl.d.ts +2 -2
- package/lib/datameshUiUrl.js +6 -6
- package/lib/index.d.ts +8 -1
- package/lib/index.js +56 -17
- package/lib/specStore.d.ts +37 -0
- package/lib/specStore.js +57 -0
- package/package.json +9 -1
- package/schema/datamesh-connect.json +3 -12
- package/src/DatameshUI.tsx +1 -10
- package/src/DatameshWidget.tsx +124 -19
- package/src/StoredNotebooks.tsx +166 -0
- package/src/Tabs.tsx +102 -0
- package/src/__tests__/DatameshWidget.spec.tsx +33 -20
- package/src/__tests__/OceanumPanel.spec.tsx +101 -0
- package/src/__tests__/Tabs.spec.tsx +168 -0
- package/src/__tests__/aiBackend.spec.ts +5 -6
- package/src/__tests__/chatRouter.spec.ts +9 -15
- package/src/__tests__/datameshUiUrl.spec.ts +4 -3
- package/src/__tests__/pluginArity.spec.ts +24 -0
- package/src/__tests__/serverAuth.spec.ts +277 -0
- package/src/__tests__/settingsSchema.spec.ts +73 -0
- package/src/__tests__/specStore.spec.ts +126 -0
- package/src/aiBackend.ts +1 -1
- package/src/auth/accountWidget.ts +113 -0
- package/src/auth/claims.ts +30 -0
- package/src/auth/plugin.ts +155 -0
- package/src/auth/serverAuth.ts +341 -0
- package/src/auth/tokens.ts +104 -0
- package/src/chatRouter.ts +8 -4
- package/src/constants.ts +1 -1
- package/src/datameshUiUrl.ts +6 -6
- package/src/index.ts +85 -36
- package/src/specStore.ts +82 -0
- package/style/index.css +158 -0
|
@@ -0,0 +1,341 @@
|
|
|
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 { IDisposable } from '@lumino/disposable';
|
|
16
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
17
|
+
|
|
18
|
+
import { userFromClaims } from './claims';
|
|
19
|
+
import {
|
|
20
|
+
IOceanumAuth,
|
|
21
|
+
IOceanumEnvironment,
|
|
22
|
+
IOceanumServiceUrls,
|
|
23
|
+
IOceanumUser
|
|
24
|
+
} from './tokens';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The page option the server extension publishes when it runs the device sign-in. Must
|
|
28
|
+
* match SERVER_AUTH_OPTION in oceanumlab/__init__.py.
|
|
29
|
+
*/
|
|
30
|
+
export const SERVER_AUTH_OPTION = 'oceanumServerAuth';
|
|
31
|
+
|
|
32
|
+
/** A sign-in waiting on the user: the code to confirm, and where to confirm it. */
|
|
33
|
+
export interface IPendingSignIn {
|
|
34
|
+
readonly userCode: string;
|
|
35
|
+
readonly verificationUri: string;
|
|
36
|
+
/** The verification page with the code already filled in. */
|
|
37
|
+
readonly verificationUriComplete: string;
|
|
38
|
+
/** Seconds until the code lapses. */
|
|
39
|
+
readonly expiresIn: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface IServerStatus {
|
|
43
|
+
state: 'signedOut' | 'pending' | 'signedIn';
|
|
44
|
+
claims: Record<string, unknown> | null;
|
|
45
|
+
pending: IPendingSignIn | null;
|
|
46
|
+
error: string | null;
|
|
47
|
+
accessToken?: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One exchange with the server extension's `/oceanum/auth/<action>` handler. */
|
|
51
|
+
export type AuthRequest = (
|
|
52
|
+
action: 'status' | 'token' | 'start' | 'cancel' | 'signout',
|
|
53
|
+
method: 'GET' | 'POST'
|
|
54
|
+
) => Promise<unknown>;
|
|
55
|
+
|
|
56
|
+
function httpUrl(value: unknown): string | null {
|
|
57
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(value);
|
|
62
|
+
return url.protocol === 'https:' || url.protocol === 'http:'
|
|
63
|
+
? url.href.replace(/\/+$/, '')
|
|
64
|
+
: null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The Oceanum deployment the server signs in to, from the page option, or `null` when the
|
|
72
|
+
* server offers no device sign-in (or the option is malformed, which is treated the same).
|
|
73
|
+
*/
|
|
74
|
+
export function readServerAuthOption(
|
|
75
|
+
option: string | undefined,
|
|
76
|
+
hostname: string
|
|
77
|
+
): IOceanumEnvironment | null {
|
|
78
|
+
if (!option) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
let raw: unknown;
|
|
82
|
+
try {
|
|
83
|
+
raw = JSON.parse(option);
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const config = raw as Record<string, unknown>;
|
|
91
|
+
const urlsIn = (config.urls ?? {}) as Record<string, unknown>;
|
|
92
|
+
const datamesh = httpUrl(urlsIn.datamesh);
|
|
93
|
+
const specs = httpUrl(urlsIn.specs);
|
|
94
|
+
const manage = httpUrl(urlsIn.manage);
|
|
95
|
+
if (
|
|
96
|
+
typeof config.auth0Domain !== 'string' ||
|
|
97
|
+
typeof config.clientId !== 'string' ||
|
|
98
|
+
!datamesh ||
|
|
99
|
+
!specs ||
|
|
100
|
+
!manage
|
|
101
|
+
) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const urls: IOceanumServiceUrls = {
|
|
105
|
+
datamesh,
|
|
106
|
+
specs,
|
|
107
|
+
manage,
|
|
108
|
+
...(httpUrl(urlsIn.datameshUi)
|
|
109
|
+
? { datameshUi: httpUrl(urlsIn.datameshUi) as string }
|
|
110
|
+
: {}),
|
|
111
|
+
...(httpUrl(urlsIn.ai) ? { ai: httpUrl(urlsIn.ai) as string } : {})
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
// The device grant is indifferent to where the page is served from, so this is simply
|
|
115
|
+
// wherever that is, rather than a list to match against.
|
|
116
|
+
hosts: [hostname.toLowerCase()],
|
|
117
|
+
auth0Domain: config.auth0Domain,
|
|
118
|
+
clientId: config.clientId,
|
|
119
|
+
oceanumDomain:
|
|
120
|
+
typeof config.oceanumDomain === 'string' ? config.oceanumDomain : '',
|
|
121
|
+
signInRedirect: false,
|
|
122
|
+
urls
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface IServerAuthOptions {
|
|
127
|
+
/** The deployment the server signs in to, or `null` when it offers no sign-in. */
|
|
128
|
+
environment: IOceanumEnvironment | null;
|
|
129
|
+
request: AuthRequest;
|
|
130
|
+
/** Called when a session ends other than by `signOut()`. */
|
|
131
|
+
onSessionEnded?: (message: string) => void;
|
|
132
|
+
/** How often to ask the server whether a pending sign-in has completed. */
|
|
133
|
+
pendingPollMs?: number;
|
|
134
|
+
/** How often to pick up a refreshed access token while signed in. */
|
|
135
|
+
tokenPollMs?: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class ServerAuth implements IOceanumAuth, IDisposable {
|
|
139
|
+
constructor(options: IServerAuthOptions) {
|
|
140
|
+
this.environment = options.environment;
|
|
141
|
+
this._request = options.request;
|
|
142
|
+
this._onSessionEnded = options.onSessionEnded;
|
|
143
|
+
this._pendingPollMs = options.pendingPollMs ?? 2000;
|
|
144
|
+
this._tokenPollMs = options.tokenPollMs ?? 60000;
|
|
145
|
+
if (!this.environment) {
|
|
146
|
+
this._ready.resolve();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// A server that signed in before this page loaded (a reload, a second tab) is already
|
|
150
|
+
// signed in; `ready` waits for that answer so nothing downstream sees a false sign-out.
|
|
151
|
+
void this._sync('token').finally(() => this._ready.resolve());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
readonly environment: IOceanumEnvironment | null;
|
|
155
|
+
|
|
156
|
+
get urls(): IOceanumServiceUrls | null {
|
|
157
|
+
return this.environment?.urls ?? null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get ready(): Promise<void> {
|
|
161
|
+
return this._ready.promise;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
get user(): IOceanumUser | null {
|
|
165
|
+
return this._user;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get userChanged(): ISignal<IOceanumAuth, IOceanumUser | null> {
|
|
169
|
+
return this._userChanged;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get tokenChanged(): ISignal<IOceanumAuth, string | null> {
|
|
173
|
+
return this._tokenChanged;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The sign-in waiting on the user, if any. */
|
|
177
|
+
get pending(): IPendingSignIn | null {
|
|
178
|
+
return this._pending;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Why the last sign-in attempt ended, if it failed. */
|
|
182
|
+
get error(): string | null {
|
|
183
|
+
return this._error;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Emits when `pending` or `error` changes. */
|
|
187
|
+
get stateChanged(): ISignal<ServerAuth, void> {
|
|
188
|
+
return this._stateChanged;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async signIn(): Promise<void> {
|
|
192
|
+
if (!this.environment || this._user) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
await this._sync('start', 'POST');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Abandon a sign-in that is waiting on the user. */
|
|
199
|
+
async cancelSignIn(): Promise<void> {
|
|
200
|
+
if (this._pending) {
|
|
201
|
+
await this._sync('cancel', 'POST');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async signOut(): Promise<void> {
|
|
206
|
+
this._signingOut = true;
|
|
207
|
+
try {
|
|
208
|
+
await this._sync('signout', 'POST');
|
|
209
|
+
} finally {
|
|
210
|
+
this._signingOut = false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async getAccessToken(): Promise<string | null> {
|
|
215
|
+
if (!this.environment || !this._user) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
await this._sync('token');
|
|
219
|
+
return this._token;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Re-read the server's state, e.g. when the tab becomes visible again. */
|
|
223
|
+
async refresh(): Promise<void> {
|
|
224
|
+
if (this.environment) {
|
|
225
|
+
await this._sync(this._user ? 'token' : 'status');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
get isDisposed(): boolean {
|
|
230
|
+
return this._disposed;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
dispose(): void {
|
|
234
|
+
if (this._disposed) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this._disposed = true;
|
|
238
|
+
this._clearTimer();
|
|
239
|
+
Signal.clearData(this);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async _sync(
|
|
243
|
+
action: 'status' | 'token' | 'start' | 'cancel' | 'signout',
|
|
244
|
+
method: 'GET' | 'POST' = 'GET'
|
|
245
|
+
): Promise<void> {
|
|
246
|
+
let status: IServerStatus;
|
|
247
|
+
try {
|
|
248
|
+
status = (await this._request(action, method)) as IServerStatus;
|
|
249
|
+
} catch (reason) {
|
|
250
|
+
// The Jupyter server being briefly unreachable is not a sign-out: keep what we have
|
|
251
|
+
// and let the next poll find out.
|
|
252
|
+
console.warn(`Oceanum.io sign-in: ${action} failed`, reason);
|
|
253
|
+
this._schedule();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (this._disposed || !status || typeof status.state !== 'string') {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
this._apply(status);
|
|
260
|
+
if (status.state === 'signedIn' && action !== 'token') {
|
|
261
|
+
// A sign-in that has just completed: fetch the token it produced.
|
|
262
|
+
await this._sync('token');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private _apply(status: IServerStatus): void {
|
|
267
|
+
const user =
|
|
268
|
+
status.state === 'signedIn' ? userFromClaims(status.claims) : null;
|
|
269
|
+
const pending = status.state === 'pending' ? status.pending : null;
|
|
270
|
+
const error = status.error ?? null;
|
|
271
|
+
|
|
272
|
+
if (
|
|
273
|
+
pending?.userCode !== this._pending?.userCode ||
|
|
274
|
+
error !== this._error
|
|
275
|
+
) {
|
|
276
|
+
this._pending = pending;
|
|
277
|
+
this._error = error;
|
|
278
|
+
this._stateChanged.emit();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const wasSignedIn = this._user !== null;
|
|
282
|
+
if ((user?.sub ?? null) !== (this._user?.sub ?? null)) {
|
|
283
|
+
this._user = user;
|
|
284
|
+
this._userChanged.emit(user);
|
|
285
|
+
if (wasSignedIn && !user && !this._signingOut) {
|
|
286
|
+
this._onSessionEnded?.(
|
|
287
|
+
error ?? 'Your Oceanum.io session has ended. Sign in again.'
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const token = user ? (status.accessToken ?? this._token) : null;
|
|
293
|
+
if (token !== this._token) {
|
|
294
|
+
this._token = token;
|
|
295
|
+
this._tokenChanged.emit(token);
|
|
296
|
+
}
|
|
297
|
+
this._schedule();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Poll quickly while a sign-in is pending, slowly while signed in, not at all otherwise. */
|
|
301
|
+
private _schedule(): void {
|
|
302
|
+
this._clearTimer();
|
|
303
|
+
if (this._disposed) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (this._pending) {
|
|
307
|
+
this._timer = setTimeout(
|
|
308
|
+
() => void this._sync('status'),
|
|
309
|
+
this._pendingPollMs
|
|
310
|
+
);
|
|
311
|
+
} else if (this._user) {
|
|
312
|
+
this._timer = setTimeout(
|
|
313
|
+
() => void this._sync('token'),
|
|
314
|
+
this._tokenPollMs
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private _clearTimer(): void {
|
|
320
|
+
if (this._timer !== null) {
|
|
321
|
+
clearTimeout(this._timer);
|
|
322
|
+
this._timer = null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private _request: AuthRequest;
|
|
327
|
+
private _onSessionEnded: ((message: string) => void) | undefined;
|
|
328
|
+
private _pendingPollMs: number;
|
|
329
|
+
private _tokenPollMs: number;
|
|
330
|
+
private _ready = new PromiseDelegate<void>();
|
|
331
|
+
private _user: IOceanumUser | null = null;
|
|
332
|
+
private _token: string | null = null;
|
|
333
|
+
private _pending: IPendingSignIn | null = null;
|
|
334
|
+
private _error: string | null = null;
|
|
335
|
+
private _signingOut = false;
|
|
336
|
+
private _disposed = false;
|
|
337
|
+
private _timer: ReturnType<typeof setTimeout> | null = null;
|
|
338
|
+
private _userChanged = new Signal<IOceanumAuth, IOceanumUser | null>(this);
|
|
339
|
+
private _tokenChanged = new Signal<IOceanumAuth, string | null>(this);
|
|
340
|
+
private _stateChanged = new Signal<ServerAuth, void>(this);
|
|
341
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Token } from '@lumino/coreutils';
|
|
2
|
+
import { ISignal } from '@lumino/signaling';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One Oceanum deployment (production or development).
|
|
6
|
+
*/
|
|
7
|
+
export interface IOceanumEnvironment {
|
|
8
|
+
/** Page hostnames served by this environment, e.g. `notebook.oceanum.io`. */
|
|
9
|
+
readonly hosts: readonly string[];
|
|
10
|
+
/** Auth0 tenant domain, e.g. `auth.oceanum.io`. */
|
|
11
|
+
readonly auth0Domain: string;
|
|
12
|
+
/** Auth0 SPA client id. */
|
|
13
|
+
readonly clientId: string;
|
|
14
|
+
/**
|
|
15
|
+
* The Oceanum apps domain, e.g. `oceanum.io`. The Oceanum nav builds its app, account,
|
|
16
|
+
* docs and sign-out URLs from it (`https://platform.oceanum.io/account`, ...).
|
|
17
|
+
*/
|
|
18
|
+
readonly oceanumDomain: string;
|
|
19
|
+
/** Service base URLs for this environment. */
|
|
20
|
+
readonly urls: IOceanumServiceUrls;
|
|
21
|
+
/**
|
|
22
|
+
* Whether to check for an existing Oceanum.io session with a full-page redirect when the
|
|
23
|
+
* silent iframe check finds none. Only for a tenant on another site than the notebook
|
|
24
|
+
* (browsers that block third-party cookies hide the session from the iframe), and only where
|
|
25
|
+
* the site origin is an allowed callback URL.
|
|
26
|
+
*/
|
|
27
|
+
readonly signInRedirect: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Service base URLs for an environment, without trailing slashes. */
|
|
31
|
+
export interface IOceanumServiceUrls {
|
|
32
|
+
/** Datamesh gateway, e.g. `https://datamesh.oceanum.io`. */
|
|
33
|
+
readonly datamesh: string;
|
|
34
|
+
/** Spec store, e.g. `https://specs.oceanum.io`. */
|
|
35
|
+
readonly specs: string;
|
|
36
|
+
/** User management service (Auth0 user metadata), e.g. `https://manage.oceanum.io`. */
|
|
37
|
+
readonly manage: string;
|
|
38
|
+
/**
|
|
39
|
+
* Datamesh UI, e.g. `https://ui.datamesh.oceanum.io`, shown by oceanumlab's Datamesh panel.
|
|
40
|
+
* Optional: without it, Oceanum production applies.
|
|
41
|
+
*/
|
|
42
|
+
readonly datameshUi?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Oceanum AI backend, e.g. `https://ai.oceanum.io`, called by oceanumlab's AI chat.
|
|
45
|
+
* Optional: without it, Oceanum production applies.
|
|
46
|
+
*/
|
|
47
|
+
readonly ai?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ColorScheme = 'light' | 'dark' | 'auto';
|
|
51
|
+
|
|
52
|
+
/** The signed-in user, from the Auth0 ID token claims. */
|
|
53
|
+
export interface IOceanumUser {
|
|
54
|
+
readonly sub: string;
|
|
55
|
+
readonly email: string;
|
|
56
|
+
readonly name: string | null;
|
|
57
|
+
/** `https://oceanum.io/active_org`. */
|
|
58
|
+
readonly activeOrg: string | null;
|
|
59
|
+
/** `https://oceanum.io/color_scheme`. */
|
|
60
|
+
readonly colorScheme: ColorScheme | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Oceanum.io sign-in state, shared with other Oceanum Notebook extensions.
|
|
65
|
+
*/
|
|
66
|
+
export interface IOceanumAuth {
|
|
67
|
+
/** The environment for this page, or `null` if sign-in is not configured for this host. */
|
|
68
|
+
readonly environment: IOceanumEnvironment | null;
|
|
69
|
+
/** Service URLs for `environment`, or `null` when it is `null`. */
|
|
70
|
+
readonly urls: IOceanumServiceUrls | null;
|
|
71
|
+
/** Resolves once any login redirect has been handled and the session restored. */
|
|
72
|
+
readonly ready: Promise<void>;
|
|
73
|
+
/** The signed-in user, or `null`. */
|
|
74
|
+
readonly user: IOceanumUser | null;
|
|
75
|
+
/** Emits the new user (or `null`) on sign-in and sign-out. */
|
|
76
|
+
readonly userChanged: ISignal<IOceanumAuth, IOceanumUser | null>;
|
|
77
|
+
/** Emits the new access token (or `null`) whenever it changes, including refreshes. */
|
|
78
|
+
readonly tokenChanged: ISignal<IOceanumAuth, string | null>;
|
|
79
|
+
/**
|
|
80
|
+
* Start sign-in without leaving the page, so its kernels survive. Resolves once sign-in has
|
|
81
|
+
* started, not when it completes; watch `userChanged` for the result. Does nothing while a
|
|
82
|
+
* user is signed in.
|
|
83
|
+
*/
|
|
84
|
+
signIn(): Promise<void>;
|
|
85
|
+
/** Sign out of Oceanum.io and clear the session. */
|
|
86
|
+
signOut(): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* A current access token, refreshed if it is near expiry, or `null` when signed out.
|
|
89
|
+
* Never cache the result: Auth0 access tokens are short-lived.
|
|
90
|
+
*/
|
|
91
|
+
getAccessToken(): Promise<string | null>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The sign-in contract. oceanumlab owns it and exports it from its package entry, because a
|
|
96
|
+
* Lumino token is matched by object identity: every extension that provides or consumes it
|
|
97
|
+
* has to import this one object, which JupyterLab arranges by sharing `@oceanum/oceanumlab`
|
|
98
|
+
* as a singleton module. On an ordinary JupyterLab oceanumlab's own device sign-in provides
|
|
99
|
+
* it; on notebook.oceanum.io the notebook's Oceanum widget extension does.
|
|
100
|
+
*/
|
|
101
|
+
export const IOceanumAuth = new Token<IOceanumAuth>(
|
|
102
|
+
'@oceanum/oceanumlab:IOceanumAuth',
|
|
103
|
+
'Oceanum.io sign-in state and Datamesh access tokens.'
|
|
104
|
+
);
|
package/src/chatRouter.ts
CHANGED
|
@@ -92,7 +92,13 @@ export class ChatRouter {
|
|
|
92
92
|
constructor(
|
|
93
93
|
private _settings: ISettingRegistry.ISettings,
|
|
94
94
|
private _commands: AuthCommands,
|
|
95
|
-
private _notebook: () => Promise<INotebookSnapshot | null
|
|
95
|
+
private _notebook: () => Promise<INotebookSnapshot | null>,
|
|
96
|
+
/**
|
|
97
|
+
* The Oceanum AI backend's address: the signed-in deployment's, or empty for the
|
|
98
|
+
* production default. Deployment configuration, not a setting -- a user-editable
|
|
99
|
+
* address is somewhere to send the user's credential.
|
|
100
|
+
*/
|
|
101
|
+
private _backend: () => string = () => ''
|
|
96
102
|
) {}
|
|
97
103
|
|
|
98
104
|
async route(
|
|
@@ -200,9 +206,7 @@ export class ChatRouter {
|
|
|
200
206
|
: 'Datamesh token not configured. Set your token in Settings → Oceanum.io.'
|
|
201
207
|
);
|
|
202
208
|
}
|
|
203
|
-
const backend = aiBackendUrl(
|
|
204
|
-
this._settings.get('aiBackendUrl').composite as string
|
|
205
|
-
);
|
|
209
|
+
const backend = aiBackendUrl(this._backend());
|
|
206
210
|
|
|
207
211
|
let response: Response;
|
|
208
212
|
try {
|
package/src/constants.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* These values are compiled into the extension.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
/** Default URL of the Oceanum AI backend service
|
|
6
|
+
/** Default URL of the Oceanum AI backend service, where the deployment names none. */
|
|
7
7
|
export const OCEANUM_AI_BACKEND_URL = 'https://ai.oceanum.io';
|
|
8
8
|
|
|
9
9
|
/** URL of the Datamesh UI embedded in the Datamesh panel */
|
package/src/datameshUiUrl.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Datamesh UI panel's address, and which messages may come from it.
|
|
3
3
|
*
|
|
4
|
-
* The address
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
10
|
import { validHttpUrl } from './httpUrl';
|
|
11
11
|
|
|
12
|
-
/** The Datamesh UI address to use for a
|
|
12
|
+
/** The Datamesh UI address to use for a configured value. */
|
|
13
13
|
export function validDatameshUiUrl(url: string | null | undefined): URL {
|
|
14
14
|
return validHttpUrl(url, DATAMESH_UI_SERVICE);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* The panel's iframe `src`: the Datamesh UI in embed mode, which hides the
|
|
19
|
-
* Oceanum.io navigator. Any query the
|
|
19
|
+
* Oceanum.io navigator. Any query the address already has is kept.
|
|
20
20
|
*/
|
|
21
21
|
export function datameshUiSrc(url: string | null | undefined): string {
|
|
22
22
|
const src = validDatameshUiUrl(url);
|