@oceanum/oceanumlab 4.7.0 → 4.7.2

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 (81) 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 +18 -2
  5. package/lib/DatameshWidget.js +80 -12
  6. package/lib/StoredNotebooks.d.ts +20 -0
  7. package/lib/StoredNotebooks.js +112 -0
  8. package/lib/Tabs.d.ts +26 -0
  9. package/lib/Tabs.js +45 -0
  10. package/lib/aiBackend.d.ts +1 -1
  11. package/lib/aiBackend.js +1 -1
  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 +254 -0
  20. package/lib/auth/startSignIn.d.ts +11 -0
  21. package/lib/auth/startSignIn.js +16 -0
  22. package/lib/auth/tokens.d.ts +102 -0
  23. package/lib/auth/tokens.js +9 -0
  24. package/lib/chatRouter.d.ts +13 -1
  25. package/lib/chatRouter.js +9 -2
  26. package/lib/constants.d.ts +1 -1
  27. package/lib/constants.js +1 -1
  28. package/lib/datameshUiUrl.d.ts +2 -2
  29. package/lib/datameshUiUrl.js +6 -6
  30. package/lib/index.d.ts +8 -1
  31. package/lib/index.js +58 -17
  32. package/lib/share/client.d.ts +84 -0
  33. package/lib/share/client.js +205 -0
  34. package/lib/share/events.d.ts +7 -0
  35. package/lib/share/events.js +9 -0
  36. package/lib/share/notebook.d.ts +184 -0
  37. package/lib/share/notebook.js +277 -0
  38. package/lib/share/oceanumOnly.d.ts +49 -0
  39. package/lib/share/oceanumOnly.js +90 -0
  40. package/lib/share/plugin.d.ts +21 -0
  41. package/lib/share/plugin.js +883 -0
  42. package/lib/share/widgets.d.ts +25 -0
  43. package/lib/share/widgets.js +154 -0
  44. package/package.json +13 -1
  45. package/schema/datamesh-connect.json +3 -12
  46. package/src/DatameshUI.tsx +1 -10
  47. package/src/DatameshWidget.tsx +143 -19
  48. package/src/StoredNotebooks.tsx +206 -0
  49. package/src/Tabs.tsx +102 -0
  50. package/src/__tests__/DatameshWidget.spec.tsx +33 -20
  51. package/src/__tests__/OceanumPanel.spec.tsx +157 -0
  52. package/src/__tests__/StoredNotebooks.spec.tsx +157 -0
  53. package/src/__tests__/Tabs.spec.tsx +168 -0
  54. package/src/__tests__/aiBackend.spec.ts +5 -6
  55. package/src/__tests__/chatRouter.spec.ts +9 -15
  56. package/src/__tests__/datameshUiUrl.spec.ts +4 -3
  57. package/src/__tests__/oceanumOnly.spec.ts +193 -0
  58. package/src/__tests__/pluginArity.spec.ts +24 -0
  59. package/src/__tests__/serverAuth.spec.ts +277 -0
  60. package/src/__tests__/settingsSchema.spec.ts +73 -0
  61. package/src/__tests__/shareClient.spec.ts +314 -0
  62. package/src/__tests__/shareNotebook.spec.ts +451 -0
  63. package/src/__tests__/sharePlugin.spec.ts +426 -0
  64. package/src/aiBackend.ts +1 -1
  65. package/src/auth/accountWidget.ts +113 -0
  66. package/src/auth/claims.ts +30 -0
  67. package/src/auth/plugin.ts +155 -0
  68. package/src/auth/serverAuth.ts +342 -0
  69. package/src/auth/startSignIn.ts +23 -0
  70. package/src/auth/tokens.ts +111 -0
  71. package/src/chatRouter.ts +8 -4
  72. package/src/constants.ts +1 -1
  73. package/src/datameshUiUrl.ts +6 -6
  74. package/src/index.ts +87 -36
  75. package/src/share/client.ts +288 -0
  76. package/src/share/events.ts +10 -0
  77. package/src/share/notebook.ts +416 -0
  78. package/src/share/oceanumOnly.ts +126 -0
  79. package/src/share/plugin.ts +1096 -0
  80. package/src/share/widgets.ts +217 -0
  81. package/style/index.css +266 -13
@@ -0,0 +1,95 @@
1
+ import { Menu, Widget } from '@lumino/widgets';
2
+ export const CommandIDs = {
3
+ signIn: 'oceanum-auth:sign-in',
4
+ signOut: 'oceanum-auth:sign-out',
5
+ account: 'oceanum-auth:account',
6
+ /**
7
+ * For other parts of oceanumlab and other extensions, such as the AI chat: resolves to
8
+ * the current access token, or `null` when signed out. Never rejects.
9
+ */
10
+ accessToken: 'oceanum-auth:access-token'
11
+ };
12
+ /**
13
+ * The top-bar account control: "Sign in" when signed out, the user's email with a sign-out
14
+ * menu when signed in.
15
+ */
16
+ export class AccountWidget extends Widget {
17
+ constructor(auth, commands) {
18
+ super({ node: document.createElement('button') });
19
+ this._onClick = () => {
20
+ if (!this._auth.user) {
21
+ void this._commands.execute(CommandIDs.signIn);
22
+ return;
23
+ }
24
+ const rect = this.node.getBoundingClientRect();
25
+ this._menu.open(rect.left, rect.bottom);
26
+ };
27
+ this._auth = auth;
28
+ this._commands = commands;
29
+ this._menu = new Menu({ commands });
30
+ this._menu.addItem({ command: CommandIDs.account });
31
+ this._menu.addItem({ type: 'separator' });
32
+ this._menu.addItem({ command: CommandIDs.signOut });
33
+ // The application shell refuses widgets without an id.
34
+ this.id = 'oceanum-account';
35
+ this.addClass('oc-Account');
36
+ this.node.setAttribute('type', 'button');
37
+ this.node.addEventListener('click', this._onClick);
38
+ auth.userChanged.connect(this._render, this);
39
+ auth.stateChanged.connect(this._render, this);
40
+ void auth.ready.then(() => this._render());
41
+ this._render();
42
+ }
43
+ dispose() {
44
+ if (this.isDisposed) {
45
+ return;
46
+ }
47
+ this.node.removeEventListener('click', this._onClick);
48
+ this._menu.dispose();
49
+ super.dispose();
50
+ }
51
+ _render() {
52
+ const user = this._auth.user;
53
+ const waiting = !user && this._auth.pending !== null;
54
+ this.node.textContent = user
55
+ ? user.email
56
+ : waiting
57
+ ? 'Signing in to Oceanum.io…'
58
+ : 'Sign in to Oceanum.io';
59
+ this.node.title = user
60
+ ? `Signed in to Oceanum.io as ${user.email}`
61
+ : 'Sign in to Oceanum.io';
62
+ this.toggleClass('oc-mod-signedIn', user !== null);
63
+ }
64
+ }
65
+ /**
66
+ * What the sign-in dialog shows: the code to confirm, and a link to the page that confirms
67
+ * it. A real link rather than `window.open`, which a popup blocker stops once there has
68
+ * been an `await` between the click and the call -- and there always has, because the code
69
+ * comes from the server.
70
+ */
71
+ export class SignInBody extends Widget {
72
+ constructor(pending) {
73
+ super();
74
+ this.addClass('oc-SignIn');
75
+ const intro = document.createElement('p');
76
+ intro.textContent =
77
+ 'Open the Oceanum.io sign-in page, and check that it shows this code:';
78
+ const code = document.createElement('div');
79
+ code.className = 'oc-SignIn-code';
80
+ code.textContent = pending.userCode;
81
+ const link = document.createElement('a');
82
+ link.className = 'oc-SignIn-link jp-mod-styled jp-mod-accept';
83
+ link.href = pending.verificationUriComplete;
84
+ link.target = '_blank';
85
+ link.rel = 'noopener noreferrer';
86
+ link.textContent = 'Open the sign-in page';
87
+ const note = document.createElement('p');
88
+ note.className = 'oc-SignIn-note';
89
+ const minutes = Math.max(1, Math.round(pending.expiresIn / 60));
90
+ note.textContent =
91
+ `This closes by itself once you have signed in. The code lasts ${minutes} ` +
92
+ `minute${minutes === 1 ? '' : 's'}. Only confirm a code you asked for yourself.`;
93
+ this.node.append(intro, code, link, note);
94
+ }
95
+ }
@@ -0,0 +1,3 @@
1
+ import { IOceanumUser } from './tokens';
2
+ /** Build the user from Auth0 ID token claims; `null` if the claims lack an identity. */
3
+ export declare function userFromClaims(claims: Record<string, unknown> | null | undefined): IOceanumUser | null;
@@ -0,0 +1,25 @@
1
+ const CLAIM = 'https://oceanum.io/';
2
+ const COLOR_SCHEMES = ['light', 'dark', 'auto'];
3
+ /** Build the user from Auth0 ID token claims; `null` if the claims lack an identity. */
4
+ export function userFromClaims(claims) {
5
+ var _a;
6
+ if (!claims) {
7
+ return null;
8
+ }
9
+ const str = (value) => typeof value === 'string' && value.length > 0 ? value : null;
10
+ const sub = str(claims.sub);
11
+ const email = (_a = str(claims.email)) !== null && _a !== void 0 ? _a : str(claims[`${CLAIM}email`]);
12
+ if (!sub || !email) {
13
+ return null;
14
+ }
15
+ const scheme = claims[`${CLAIM}color_scheme`];
16
+ return {
17
+ sub,
18
+ email,
19
+ name: str(claims.name),
20
+ activeOrg: str(claims[`${CLAIM}active_org`]),
21
+ colorScheme: COLOR_SCHEMES.includes(scheme)
22
+ ? scheme
23
+ : null
24
+ };
25
+ }
@@ -0,0 +1,9 @@
1
+ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ import { IOceanumAuth } from './tokens';
3
+ /**
4
+ * The id other deployments disable. notebook.oceanum.io provides IOceanumAuth from its own
5
+ * Oceanum widget extension, and Lumino lets a second provider silently replace the first
6
+ * rather than refusing it, so that site lists this id under `disabledExtensions`.
7
+ */
8
+ export declare const DEVICE_AUTH_PLUGIN_ID = "@oceanum/oceanumlab:device-auth";
9
+ export declare const deviceAuthPlugin: JupyterFrontEndPlugin<IOceanumAuth>;
@@ -0,0 +1,126 @@
1
+ import { Dialog, ICommandPalette, Notification } from '@jupyterlab/apputils';
2
+ import { PageConfig, URLExt } from '@jupyterlab/coreutils';
3
+ import { ServerConnection } from '@jupyterlab/services';
4
+ import { AccountWidget, CommandIDs, SignInBody } from './accountWidget';
5
+ import { readServerAuthOption, SERVER_AUTH_OPTION, ServerAuth } from './serverAuth';
6
+ import { IOceanumAuth } from './tokens';
7
+ /**
8
+ * The id other deployments disable. notebook.oceanum.io provides IOceanumAuth from its own
9
+ * Oceanum widget extension, and Lumino lets a second provider silently replace the first
10
+ * rather than refusing it, so that site lists this id under `disabledExtensions`.
11
+ */
12
+ export const DEVICE_AUTH_PLUGIN_ID = '@oceanum/oceanumlab:device-auth';
13
+ function serverRequest(settings) {
14
+ return async (action, method) => {
15
+ const url = URLExt.join(settings.baseUrl, 'oceanum', 'auth', action);
16
+ const response = await ServerConnection.makeRequest(url, { method }, settings);
17
+ if (!response.ok) {
18
+ throw new ServerConnection.ResponseError(response);
19
+ }
20
+ return response.json();
21
+ };
22
+ }
23
+ /** Show the code and the link until the sign-in completes, fails, or is cancelled. */
24
+ async function showSignIn(auth) {
25
+ const pending = auth.pending;
26
+ if (!pending) {
27
+ return;
28
+ }
29
+ const dialog = new Dialog({
30
+ title: 'Sign in to Oceanum.io',
31
+ body: new SignInBody(pending),
32
+ buttons: [Dialog.cancelButton()]
33
+ });
34
+ const closeWhenSettled = () => {
35
+ if (auth.pending === null) {
36
+ dialog.reject();
37
+ }
38
+ };
39
+ auth.stateChanged.connect(closeWhenSettled);
40
+ auth.userChanged.connect(closeWhenSettled);
41
+ try {
42
+ await dialog.launch();
43
+ }
44
+ finally {
45
+ auth.stateChanged.disconnect(closeWhenSettled);
46
+ auth.userChanged.disconnect(closeWhenSettled);
47
+ }
48
+ if (auth.pending) {
49
+ // Still waiting, so it was the user who closed it.
50
+ await auth.cancelSignIn();
51
+ }
52
+ else if (!auth.user && auth.error) {
53
+ Notification.warning(`Oceanum.io sign-in did not complete: ${auth.error}`, {
54
+ autoClose: false
55
+ });
56
+ }
57
+ }
58
+ export const deviceAuthPlugin = {
59
+ id: DEVICE_AUTH_PLUGIN_ID,
60
+ description: 'Oceanum.io sign-in by device code, run by the Jupyter server extension.',
61
+ autoStart: true,
62
+ provides: IOceanumAuth,
63
+ optional: [ICommandPalette],
64
+ activate: (app, palette) => {
65
+ const environment = readServerAuthOption(PageConfig.getOption(SERVER_AUTH_OPTION), window.location.hostname);
66
+ const auth = new ServerAuth({
67
+ environment,
68
+ request: serverRequest(app.serviceManager.serverSettings),
69
+ onSessionEnded: message => Notification.warning(message, { autoClose: false })
70
+ });
71
+ if (!environment) {
72
+ // The server offers no sign-in (`c.OceanumLab.sign_in = "off"`), or there is no
73
+ // oceanumlab server extension at all. Consumers see an auth with no environment and
74
+ // say sign-in is not configured; nothing is added to the top bar.
75
+ return auth;
76
+ }
77
+ document.addEventListener('visibilitychange', () => {
78
+ if (document.visibilityState === 'visible') {
79
+ void auth.refresh();
80
+ }
81
+ });
82
+ const { commands } = app;
83
+ let dialogOpen = false;
84
+ commands.addCommand(CommandIDs.signIn, {
85
+ label: 'Sign in to Oceanum.io',
86
+ isVisible: () => !auth.user,
87
+ execute: async () => {
88
+ await auth.signIn();
89
+ if (auth.pending && !dialogOpen) {
90
+ dialogOpen = true;
91
+ try {
92
+ await showSignIn(auth);
93
+ }
94
+ finally {
95
+ dialogOpen = false;
96
+ }
97
+ }
98
+ else if (!auth.user && !auth.pending && auth.error) {
99
+ Notification.warning(`Oceanum.io sign-in could not start: ${auth.error}`, { autoClose: false });
100
+ }
101
+ }
102
+ });
103
+ commands.addCommand(CommandIDs.signOut, {
104
+ label: 'Sign out of Oceanum.io',
105
+ isVisible: () => auth.user !== null,
106
+ execute: () => auth.signOut()
107
+ });
108
+ commands.addCommand(CommandIDs.account, {
109
+ label: () => auth.user ? `Signed in as ${auth.user.email}` : 'Not signed in',
110
+ isEnabled: () => false,
111
+ execute: () => undefined
112
+ });
113
+ commands.addCommand(CommandIDs.accessToken, {
114
+ label: 'Oceanum.io access token',
115
+ isVisible: () => false,
116
+ execute: () => auth.getAccessToken().catch(() => null)
117
+ });
118
+ auth.userChanged.connect(() => commands.notifyCommandChanged());
119
+ if (palette) {
120
+ palette.addItem({ command: CommandIDs.signIn, category: 'Oceanum' });
121
+ palette.addItem({ command: CommandIDs.signOut, category: 'Oceanum' });
122
+ }
123
+ app.shell.add(new AccountWidget(auth, commands), 'top', { rank: 1000 });
124
+ return auth;
125
+ }
126
+ };
@@ -0,0 +1,79 @@
1
+ import { IDisposable } from '@lumino/disposable';
2
+ import { ISignal } from '@lumino/signaling';
3
+ import { IOceanumAuth, IOceanumEnvironment, IOceanumServiceUrls, IOceanumUser } from './tokens';
4
+ /**
5
+ * The page option the server extension publishes when it runs the device sign-in. Must
6
+ * match SERVER_AUTH_OPTION in oceanumlab/__init__.py.
7
+ */
8
+ export declare const SERVER_AUTH_OPTION = "oceanumServerAuth";
9
+ /** A sign-in waiting on the user: the code to confirm, and where to confirm it. */
10
+ export interface IPendingSignIn {
11
+ readonly userCode: string;
12
+ readonly verificationUri: string;
13
+ /** The verification page with the code already filled in. */
14
+ readonly verificationUriComplete: string;
15
+ /** Seconds until the code lapses. */
16
+ readonly expiresIn: number;
17
+ }
18
+ /** One exchange with the server extension's `/oceanum/auth/<action>` handler. */
19
+ export type AuthRequest = (action: 'status' | 'token' | 'start' | 'cancel' | 'signout', method: 'GET' | 'POST') => Promise<unknown>;
20
+ /**
21
+ * The Oceanum deployment the server signs in to, from the page option, or `null` when the
22
+ * server offers no device sign-in (or the option is malformed, which is treated the same).
23
+ */
24
+ export declare function readServerAuthOption(option: string | undefined, hostname: string): IOceanumEnvironment | null;
25
+ export interface IServerAuthOptions {
26
+ /** The deployment the server signs in to, or `null` when it offers no sign-in. */
27
+ environment: IOceanumEnvironment | null;
28
+ request: AuthRequest;
29
+ /** Called when a session ends other than by `signOut()`. */
30
+ onSessionEnded?: (message: string) => void;
31
+ /** How often to ask the server whether a pending sign-in has completed. */
32
+ pendingPollMs?: number;
33
+ /** How often to pick up a refreshed access token while signed in. */
34
+ tokenPollMs?: number;
35
+ }
36
+ export declare class ServerAuth implements IOceanumAuth, IDisposable {
37
+ constructor(options: IServerAuthOptions);
38
+ readonly environment: IOceanumEnvironment | null;
39
+ get urls(): IOceanumServiceUrls | null;
40
+ get ready(): Promise<void>;
41
+ get user(): IOceanumUser | null;
42
+ get userChanged(): ISignal<IOceanumAuth, IOceanumUser | null>;
43
+ get tokenChanged(): ISignal<IOceanumAuth, string | null>;
44
+ /** The sign-in waiting on the user, if any. */
45
+ get pending(): IPendingSignIn | null;
46
+ /** Why the last sign-in attempt ended, if it failed. */
47
+ get error(): string | null;
48
+ /** Emits when `pending` or `error` changes. */
49
+ get stateChanged(): ISignal<ServerAuth, void>;
50
+ signIn(): Promise<void>;
51
+ /** Abandon a sign-in that is waiting on the user. */
52
+ cancelSignIn(): Promise<void>;
53
+ signOut(): Promise<void>;
54
+ getAccessToken(): Promise<string | null>;
55
+ /** Re-read the server's state, e.g. when the tab becomes visible again. */
56
+ refresh(): Promise<void>;
57
+ get isDisposed(): boolean;
58
+ dispose(): void;
59
+ private _sync;
60
+ private _apply;
61
+ /** Poll quickly while a sign-in is pending, slowly while signed in, not at all otherwise. */
62
+ private _schedule;
63
+ private _clearTimer;
64
+ private _request;
65
+ private _onSessionEnded;
66
+ private _pendingPollMs;
67
+ private _tokenPollMs;
68
+ private _ready;
69
+ private _user;
70
+ private _token;
71
+ private _pending;
72
+ private _error;
73
+ private _signingOut;
74
+ private _disposed;
75
+ private _timer;
76
+ private _userChanged;
77
+ private _tokenChanged;
78
+ private _stateChanged;
79
+ }
@@ -0,0 +1,254 @@
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
+ fileManagement: config.fileManagement === 'oceanum' ? 'oceanum' : 'local',
85
+ urls
86
+ };
87
+ }
88
+ export class ServerAuth {
89
+ constructor(options) {
90
+ var _a, _b;
91
+ this._ready = new PromiseDelegate();
92
+ this._user = null;
93
+ this._token = null;
94
+ this._pending = null;
95
+ this._error = null;
96
+ this._signingOut = false;
97
+ this._disposed = false;
98
+ this._timer = null;
99
+ this._userChanged = new Signal(this);
100
+ this._tokenChanged = new Signal(this);
101
+ this._stateChanged = new Signal(this);
102
+ this.environment = options.environment;
103
+ this._request = options.request;
104
+ this._onSessionEnded = options.onSessionEnded;
105
+ this._pendingPollMs = (_a = options.pendingPollMs) !== null && _a !== void 0 ? _a : 2000;
106
+ this._tokenPollMs = (_b = options.tokenPollMs) !== null && _b !== void 0 ? _b : 60000;
107
+ if (!this.environment) {
108
+ this._ready.resolve();
109
+ return;
110
+ }
111
+ // A server that signed in before this page loaded (a reload, a second tab) is already
112
+ // signed in; `ready` waits for that answer so nothing downstream sees a false sign-out.
113
+ void this._sync('token').finally(() => this._ready.resolve());
114
+ }
115
+ get urls() {
116
+ var _a, _b;
117
+ return (_b = (_a = this.environment) === null || _a === void 0 ? void 0 : _a.urls) !== null && _b !== void 0 ? _b : null;
118
+ }
119
+ get ready() {
120
+ return this._ready.promise;
121
+ }
122
+ get user() {
123
+ return this._user;
124
+ }
125
+ get userChanged() {
126
+ return this._userChanged;
127
+ }
128
+ get tokenChanged() {
129
+ return this._tokenChanged;
130
+ }
131
+ /** The sign-in waiting on the user, if any. */
132
+ get pending() {
133
+ return this._pending;
134
+ }
135
+ /** Why the last sign-in attempt ended, if it failed. */
136
+ get error() {
137
+ return this._error;
138
+ }
139
+ /** Emits when `pending` or `error` changes. */
140
+ get stateChanged() {
141
+ return this._stateChanged;
142
+ }
143
+ async signIn() {
144
+ if (!this.environment || this._user) {
145
+ return;
146
+ }
147
+ await this._sync('start', 'POST');
148
+ }
149
+ /** Abandon a sign-in that is waiting on the user. */
150
+ async cancelSignIn() {
151
+ if (this._pending) {
152
+ await this._sync('cancel', 'POST');
153
+ }
154
+ }
155
+ async signOut() {
156
+ this._signingOut = true;
157
+ try {
158
+ await this._sync('signout', 'POST');
159
+ }
160
+ finally {
161
+ this._signingOut = false;
162
+ }
163
+ }
164
+ async getAccessToken() {
165
+ if (!this.environment || !this._user) {
166
+ return null;
167
+ }
168
+ await this._sync('token');
169
+ return this._token;
170
+ }
171
+ /** Re-read the server's state, e.g. when the tab becomes visible again. */
172
+ async refresh() {
173
+ if (this.environment) {
174
+ await this._sync(this._user ? 'token' : 'status');
175
+ }
176
+ }
177
+ get isDisposed() {
178
+ return this._disposed;
179
+ }
180
+ dispose() {
181
+ if (this._disposed) {
182
+ return;
183
+ }
184
+ this._disposed = true;
185
+ this._clearTimer();
186
+ Signal.clearData(this);
187
+ }
188
+ async _sync(action, method = 'GET') {
189
+ let status;
190
+ try {
191
+ status = (await this._request(action, method));
192
+ }
193
+ catch (reason) {
194
+ // The Jupyter server being briefly unreachable is not a sign-out: keep what we have
195
+ // and let the next poll find out.
196
+ console.warn(`Oceanum.io sign-in: ${action} failed`, reason);
197
+ this._schedule();
198
+ return;
199
+ }
200
+ if (this._disposed || !status || typeof status.state !== 'string') {
201
+ return;
202
+ }
203
+ this._apply(status);
204
+ if (status.state === 'signedIn' && action !== 'token') {
205
+ // A sign-in that has just completed: fetch the token it produced.
206
+ await this._sync('token');
207
+ }
208
+ }
209
+ _apply(status) {
210
+ var _a, _b, _c, _d, _e, _f, _g;
211
+ const user = status.state === 'signedIn' ? userFromClaims(status.claims) : null;
212
+ const pending = status.state === 'pending' ? status.pending : null;
213
+ const error = (_a = status.error) !== null && _a !== void 0 ? _a : null;
214
+ if ((pending === null || pending === void 0 ? void 0 : pending.userCode) !== ((_b = this._pending) === null || _b === void 0 ? void 0 : _b.userCode) ||
215
+ error !== this._error) {
216
+ this._pending = pending;
217
+ this._error = error;
218
+ this._stateChanged.emit();
219
+ }
220
+ const wasSignedIn = this._user !== null;
221
+ 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)) {
222
+ this._user = user;
223
+ this._userChanged.emit(user);
224
+ if (wasSignedIn && !user && !this._signingOut) {
225
+ (_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.');
226
+ }
227
+ }
228
+ const token = user ? ((_g = status.accessToken) !== null && _g !== void 0 ? _g : this._token) : null;
229
+ if (token !== this._token) {
230
+ this._token = token;
231
+ this._tokenChanged.emit(token);
232
+ }
233
+ this._schedule();
234
+ }
235
+ /** Poll quickly while a sign-in is pending, slowly while signed in, not at all otherwise. */
236
+ _schedule() {
237
+ this._clearTimer();
238
+ if (this._disposed) {
239
+ return;
240
+ }
241
+ if (this._pending) {
242
+ this._timer = setTimeout(() => void this._sync('status'), this._pendingPollMs);
243
+ }
244
+ else if (this._user) {
245
+ this._timer = setTimeout(() => void this._sync('token'), this._tokenPollMs);
246
+ }
247
+ }
248
+ _clearTimer() {
249
+ if (this._timer !== null) {
250
+ clearTimeout(this._timer);
251
+ this._timer = null;
252
+ }
253
+ }
254
+ }
@@ -0,0 +1,11 @@
1
+ import type { CommandRegistry } from '@lumino/commands';
2
+ import type { IOceanumAuth } from './tokens';
3
+ /**
4
+ * Start signing in the way this host shows it.
5
+ *
6
+ * On a JupyterLab server the device sign-in command both starts the flow and shows the
7
+ * code dialog; `auth.signIn()` alone starts the flow and shows nothing, so the user is
8
+ * left with a "Signing in…" top bar and no code. notebook.oceanum.io disables that
9
+ * command and provides an auth whose `signIn()` opens the Oceanum sign-in itself.
10
+ */
11
+ export declare function startSignIn(commands: CommandRegistry, auth: IOceanumAuth): Promise<void>;
@@ -0,0 +1,16 @@
1
+ import { CommandIDs } from './accountWidget';
2
+ /**
3
+ * Start signing in the way this host shows it.
4
+ *
5
+ * On a JupyterLab server the device sign-in command both starts the flow and shows the
6
+ * code dialog; `auth.signIn()` alone starts the flow and shows nothing, so the user is
7
+ * left with a "Signing in…" top bar and no code. notebook.oceanum.io disables that
8
+ * command and provides an auth whose `signIn()` opens the Oceanum sign-in itself.
9
+ */
10
+ export async function startSignIn(commands, auth) {
11
+ if (commands.hasCommand(CommandIDs.signIn)) {
12
+ await commands.execute(CommandIDs.signIn);
13
+ return;
14
+ }
15
+ await auth.signIn();
16
+ }