@oceanum/oceanumlab 4.7.1 → 4.8.0
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/lib/DatameshWidget.d.ts +11 -0
- package/lib/DatameshWidget.js +35 -5
- package/lib/StoredNotebooks.d.ts +27 -5
- package/lib/StoredNotebooks.js +141 -24
- package/lib/auth/serverAuth.js +1 -0
- package/lib/auth/startSignIn.d.ts +11 -0
- package/lib/auth/startSignIn.js +16 -0
- package/lib/auth/tokens.d.ts +13 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +43 -7
- package/lib/share/client.d.ts +86 -0
- package/lib/share/client.js +206 -0
- package/lib/share/events.d.ts +7 -0
- package/lib/share/events.js +9 -0
- package/lib/share/notebook.d.ts +216 -0
- package/lib/share/notebook.js +355 -0
- package/lib/share/oceanumOnly.d.ts +72 -0
- package/lib/share/oceanumOnly.js +127 -0
- package/lib/share/plugin.d.ts +23 -0
- package/lib/share/plugin.js +981 -0
- package/lib/share/upload.d.ts +6 -0
- package/lib/share/upload.js +27 -0
- package/lib/share/widgets.d.ts +25 -0
- package/lib/share/widgets.js +154 -0
- package/package.json +5 -1
- package/schema/datamesh-connect.json +6 -0
- package/src/DatameshWidget.tsx +79 -2
- package/src/StoredNotebooks.tsx +274 -24
- package/src/__tests__/OceanumPanel.spec.tsx +159 -5
- package/src/__tests__/StoredNotebooks.spec.tsx +528 -0
- package/src/__tests__/oceanumOnly.spec.ts +317 -0
- package/src/__tests__/settingsSchema.spec.ts +11 -1
- package/src/__tests__/shareClient.spec.ts +328 -0
- package/src/__tests__/shareNotebook.spec.ts +624 -0
- package/src/__tests__/sharePlugin.spec.ts +664 -0
- package/src/auth/serverAuth.ts +1 -0
- package/src/auth/startSignIn.ts +23 -0
- package/src/auth/tokens.ts +13 -0
- package/src/index.ts +43 -7
- package/src/share/client.ts +291 -0
- package/src/share/events.ts +10 -0
- package/src/share/notebook.ts +525 -0
- package/src/share/oceanumOnly.ts +177 -0
- package/src/share/plugin.ts +1220 -0
- package/src/share/upload.ts +27 -0
- package/src/share/widgets.ts +217 -0
- package/style/index.css +212 -18
- package/lib/specStore.d.ts +0 -37
- package/lib/specStore.js +0 -57
- package/src/__tests__/specStore.spec.ts +0 -126
- package/src/specStore.ts +0 -82
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { isSpecId } from './notebook';
|
|
2
|
+
const MESSAGES = {
|
|
3
|
+
'signed-out': 'Sign in to Oceanum.io to continue.',
|
|
4
|
+
expired: 'Your Oceanum session has expired, sign in again.',
|
|
5
|
+
forbidden: 'You have no access to this notebook. It may not exist, or was created under another organisation.',
|
|
6
|
+
'not-found': 'This notebook no longer exists on Oceanum.io.',
|
|
7
|
+
'too-large': 'The notebook is too large for Oceanum.io (32 MiB limit).',
|
|
8
|
+
network: 'Could not reach Oceanum.io. Check your connection; very large notebooks can also fail this way.',
|
|
9
|
+
invalid: 'Oceanum.io returned an unexpected response.',
|
|
10
|
+
unsupported: 'This Oceanum.io service does not offer that operation yet.'
|
|
11
|
+
};
|
|
12
|
+
/** A spec store failure with a message that is safe to show to the user. */
|
|
13
|
+
export class SpecStoreError extends Error {
|
|
14
|
+
constructor(kind, status = null) {
|
|
15
|
+
super(kind === 'server'
|
|
16
|
+
? `Oceanum.io returned an error (HTTP ${status}).`
|
|
17
|
+
: MESSAGES[kind]);
|
|
18
|
+
this.kind = kind;
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.name = 'SpecStoreError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Map a failed HTTP status to an error. */
|
|
24
|
+
export function errorForStatus(status) {
|
|
25
|
+
switch (status) {
|
|
26
|
+
// The spec store's authentication middleware answers a bad or expired token with 400.
|
|
27
|
+
case 400:
|
|
28
|
+
return new SpecStoreError('expired', status);
|
|
29
|
+
// Casbin answers 403 both for no access and for a record that does not exist.
|
|
30
|
+
case 403:
|
|
31
|
+
return new SpecStoreError('forbidden', status);
|
|
32
|
+
case 404:
|
|
33
|
+
return new SpecStoreError('not-found', status);
|
|
34
|
+
case 405:
|
|
35
|
+
// The route is there but not this method: a store from before it had PATCH.
|
|
36
|
+
return new SpecStoreError('unsupported', status);
|
|
37
|
+
case 413:
|
|
38
|
+
return new SpecStoreError('too-large', status);
|
|
39
|
+
default:
|
|
40
|
+
return new SpecStoreError('server', status);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function isSummary(value) {
|
|
44
|
+
if (typeof value !== 'object' || value === null) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const v = value;
|
|
48
|
+
return (isSpecId(v.id) &&
|
|
49
|
+
typeof v.name === 'string' &&
|
|
50
|
+
typeof v.modified === 'string');
|
|
51
|
+
}
|
|
52
|
+
function isRecord(value) {
|
|
53
|
+
return isSummary(value) && 'spec' in value;
|
|
54
|
+
}
|
|
55
|
+
/** Client for one spec type of the Oceanum spec store, `notebook` by default. */
|
|
56
|
+
export class SpecStoreClient {
|
|
57
|
+
constructor(options) {
|
|
58
|
+
var _a, _b;
|
|
59
|
+
const specType = (_a = options.specType) !== null && _a !== void 0 ? _a : 'notebook';
|
|
60
|
+
this._base = `${options.specsUrl.replace(/\/+$/, '')}/specs/${specType}`;
|
|
61
|
+
this._getAccessToken = options.getAccessToken;
|
|
62
|
+
this._fetch = (_b = options.fetch) !== null && _b !== void 0 ? _b : ((input, init) => fetch(input, init));
|
|
63
|
+
}
|
|
64
|
+
/** Every notebook the user may read, including public ones. */
|
|
65
|
+
async list() {
|
|
66
|
+
const data = await this._request('GET', this._base, { auth: 'required' });
|
|
67
|
+
if (!Array.isArray(data) || !data.every(isSummary)) {
|
|
68
|
+
throw new SpecStoreError('invalid');
|
|
69
|
+
}
|
|
70
|
+
return data;
|
|
71
|
+
}
|
|
72
|
+
/** Read a record, anonymously when signed out. */
|
|
73
|
+
async get(id) {
|
|
74
|
+
return this._record(await this._request('GET', this._url(id), { auth: 'optional' }));
|
|
75
|
+
}
|
|
76
|
+
/** Create a record. The server mints the id: use the returned record's. */
|
|
77
|
+
async create(body) {
|
|
78
|
+
return this._record(await this._request('POST', this._base, {
|
|
79
|
+
auth: 'required',
|
|
80
|
+
body: pick(body)
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
/** Replace a record's name, description and notebook. */
|
|
84
|
+
async update(id, body) {
|
|
85
|
+
return this._record(await this._request('PUT', this._url(id), {
|
|
86
|
+
auth: 'required',
|
|
87
|
+
body: pick(body)
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Change some of a record's fields and leave the rest as they are. Needs the same
|
|
92
|
+
* write access as `update`, which the store enforces a patch as.
|
|
93
|
+
*/
|
|
94
|
+
async patch(id, changes) {
|
|
95
|
+
if (Object.keys(changes).length === 0) {
|
|
96
|
+
// The store answers an empty patch with 400; there is nothing to send anyway.
|
|
97
|
+
throw new SpecStoreError('invalid');
|
|
98
|
+
}
|
|
99
|
+
return this._record(await this._request('PATCH', this._url(id), {
|
|
100
|
+
auth: 'required',
|
|
101
|
+
body: changes
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Give a record a new name, without sending the notebook back with it.
|
|
106
|
+
*
|
|
107
|
+
* Falls back to reading the record and putting it back where the store has no PATCH
|
|
108
|
+
* (production had none when this was written). That fallback races another writer's
|
|
109
|
+
* change to the notebook, which is the whole reason to prefer a patch.
|
|
110
|
+
*/
|
|
111
|
+
async rename(id, name) {
|
|
112
|
+
try {
|
|
113
|
+
return await this.patch(id, { name });
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (!(error instanceof SpecStoreError) || error.kind !== 'unsupported') {
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
const record = await this.get(id);
|
|
120
|
+
return this.update(id, {
|
|
121
|
+
name,
|
|
122
|
+
description: record.description,
|
|
123
|
+
spec: record.spec
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Delete a record (requires admin access to it). */
|
|
128
|
+
async remove(id) {
|
|
129
|
+
await this._request('DELETE', this._url(id), { auth: 'required' });
|
|
130
|
+
}
|
|
131
|
+
/** Grant a permission on a record (requires admin access to it). */
|
|
132
|
+
async addPermission(id, grant) {
|
|
133
|
+
await this._request('POST', `${this._url(id)}/permissions`, {
|
|
134
|
+
auth: 'required',
|
|
135
|
+
body: {
|
|
136
|
+
type: grant.type,
|
|
137
|
+
entity: grant.entity,
|
|
138
|
+
permission: grant.permission
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Remove a permission (requires admin access). The server matches the exact grant and
|
|
144
|
+
* answers 404 when there is no such permission.
|
|
145
|
+
*/
|
|
146
|
+
async removePermission(id, grant) {
|
|
147
|
+
await this._request('DELETE', `${this._url(id)}/permissions`, {
|
|
148
|
+
auth: 'required',
|
|
149
|
+
body: {
|
|
150
|
+
type: grant.type,
|
|
151
|
+
entity: grant.entity,
|
|
152
|
+
permission: grant.permission
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
_url(id) {
|
|
157
|
+
if (!isSpecId(id)) {
|
|
158
|
+
throw new SpecStoreError('invalid');
|
|
159
|
+
}
|
|
160
|
+
return `${this._base}/${id}`;
|
|
161
|
+
}
|
|
162
|
+
_record(data) {
|
|
163
|
+
if (!isRecord(data)) {
|
|
164
|
+
throw new SpecStoreError('invalid');
|
|
165
|
+
}
|
|
166
|
+
return data;
|
|
167
|
+
}
|
|
168
|
+
async _request(method, url, options) {
|
|
169
|
+
const token = await this._getAccessToken();
|
|
170
|
+
if (token === null && options.auth === 'required') {
|
|
171
|
+
throw new SpecStoreError('signed-out');
|
|
172
|
+
}
|
|
173
|
+
const headers = {};
|
|
174
|
+
if (token !== null) {
|
|
175
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
176
|
+
}
|
|
177
|
+
const init = { method, headers };
|
|
178
|
+
if (options.body !== undefined) {
|
|
179
|
+
headers['Content-Type'] = 'application/json';
|
|
180
|
+
init.body = JSON.stringify(options.body);
|
|
181
|
+
}
|
|
182
|
+
let response;
|
|
183
|
+
try {
|
|
184
|
+
response = await this._fetch(url, init);
|
|
185
|
+
}
|
|
186
|
+
catch (_a) {
|
|
187
|
+
throw new SpecStoreError('network');
|
|
188
|
+
}
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw errorForStatus(response.status);
|
|
191
|
+
}
|
|
192
|
+
if (response.status === 204) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
return await response.json();
|
|
197
|
+
}
|
|
198
|
+
catch (_b) {
|
|
199
|
+
throw new SpecStoreError('invalid');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Only the fields the server accepts; in particular never send an `id`. */
|
|
204
|
+
function pick(body) {
|
|
205
|
+
return { name: body.name, description: body.description, spec: body.spec };
|
|
206
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Signal } from '@lumino/signaling';
|
|
2
|
+
/**
|
|
3
|
+
* Emits after this extension creates, renames or deletes a notebook record, so the
|
|
4
|
+
* Notebooks tab can list again. A plain module-level signal: the tab and the commands
|
|
5
|
+
* live in different plugins, and the change is the only thing they need to share.
|
|
6
|
+
*/
|
|
7
|
+
export declare const notebooksChanged: Signal<object, void>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Signal } from '@lumino/signaling';
|
|
2
|
+
/**
|
|
3
|
+
* Emits after this extension creates, renames or deletes a notebook record, so the
|
|
4
|
+
* Notebooks tab can list again. A plain module-level signal: the tab and the commands
|
|
5
|
+
* live in different plugins, and the change is the only thing they need to share.
|
|
6
|
+
*/
|
|
7
|
+
// The sender is a throwaway object, not null: Lumino keys its receivers by sender in a
|
|
8
|
+
// WeakMap, which refuses null.
|
|
9
|
+
export const notebooksChanged = new Signal({});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notebook records in the Oceanum spec store: the link a local notebook keeps to its
|
|
3
|
+
* record, the body that is stored, and the names, paths and addresses around them.
|
|
4
|
+
*
|
|
5
|
+
* Ported from oceanum-notebook's share-oceanum package, which notebook.oceanum.io will
|
|
6
|
+
* drop in favour of this one.
|
|
7
|
+
*/
|
|
8
|
+
import type { INotebookContent } from '@jupyterlab/nbformat';
|
|
9
|
+
/** Notebook metadata key linking a local notebook to its spec store record. */
|
|
10
|
+
export declare const METADATA_KEY = "oceanum";
|
|
11
|
+
/** Folder that notebooks opened from Oceanum.io are written to. */
|
|
12
|
+
export declare const OCEANUM_DIR = "Oceanum";
|
|
13
|
+
/** Query parameter carrying a shared notebook id. */
|
|
14
|
+
export declare const QUERY_PARAM = "oceanum-notebook";
|
|
15
|
+
/**
|
|
16
|
+
* Largest request body we send. Cloud Run rejects requests over 32 MiB; keep a margin
|
|
17
|
+
* for headers and the server's own encoding.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MAX_BODY_BYTES: number;
|
|
20
|
+
/** The `oceanum` notebook metadata value. */
|
|
21
|
+
export interface IOceanumLink {
|
|
22
|
+
spec_id: string;
|
|
23
|
+
/** Spec store the id belongs to, so a development id is never PUT to production. */
|
|
24
|
+
specs_url: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
}
|
|
27
|
+
/** A spec store request body for `POST`/`PUT /specs/notebook`. */
|
|
28
|
+
export interface ISpecBody {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string | null;
|
|
31
|
+
spec: INotebookContent;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The fields a `PATCH /specs/notebook/{id}` may change. A field left out keeps its
|
|
35
|
+
* stored value, so a rename need not send the notebook back.
|
|
36
|
+
*
|
|
37
|
+
* The store refuses anything else outright rather than ignoring it, and refuses a patch
|
|
38
|
+
* that carries no field at all, so never send an empty one. `spec` is replaced whole:
|
|
39
|
+
* there is no way to change one key of a stored notebook.
|
|
40
|
+
*/
|
|
41
|
+
export interface ISpecPatch {
|
|
42
|
+
name?: string;
|
|
43
|
+
description?: string | null;
|
|
44
|
+
spec?: INotebookContent;
|
|
45
|
+
}
|
|
46
|
+
/** A spec store list entry (the list omits `spec`). */
|
|
47
|
+
export interface ISpecSummary {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
description: string | null;
|
|
51
|
+
/**
|
|
52
|
+
* When the record was created, or last patched. A `PUT` does not touch it, so for a
|
|
53
|
+
* record nothing has patched this is still a creation time.
|
|
54
|
+
*/
|
|
55
|
+
modified: string;
|
|
56
|
+
/** Null unless the caller created the record: the store hides other creators. */
|
|
57
|
+
creator: string | null;
|
|
58
|
+
}
|
|
59
|
+
/** A full spec store record. */
|
|
60
|
+
export interface ISpecRecord extends ISpecSummary {
|
|
61
|
+
spec: unknown;
|
|
62
|
+
}
|
|
63
|
+
export type BuildResult = {
|
|
64
|
+
ok: true;
|
|
65
|
+
body: ISpecBody;
|
|
66
|
+
bytes: number;
|
|
67
|
+
strippedOutputs: boolean;
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
bytes: number;
|
|
71
|
+
};
|
|
72
|
+
/** Whether `value` is a UUID, and so safe to put in a spec store URL. */
|
|
73
|
+
export declare function isSpecId(value: unknown): value is string;
|
|
74
|
+
/**
|
|
75
|
+
* Read the link from an `oceanum` metadata value, or `null` if it is missing, malformed,
|
|
76
|
+
* or belongs to a different spec store.
|
|
77
|
+
*/
|
|
78
|
+
export declare function readLink(value: unknown, specsUrl: string): IOceanumLink | null;
|
|
79
|
+
/** The notebook name stored on Oceanum.io for a local path. */
|
|
80
|
+
export declare function nameFromPath(path: string): string;
|
|
81
|
+
/**
|
|
82
|
+
* The notebook with every cell's `trusted` metadata removed.
|
|
83
|
+
*
|
|
84
|
+
* JupyterLab trusts a cell whose metadata says `trusted: true` and then renders its HTML
|
|
85
|
+
* and Markdown unsanitised, running any script in them. A notebook from the spec store
|
|
86
|
+
* (anyone with write access can edit a record) must arrive untrusted; its outputs
|
|
87
|
+
* become trusted again when the reader re-runs the cells.
|
|
88
|
+
*/
|
|
89
|
+
export declare function withoutTrust(content: INotebookContent): INotebookContent;
|
|
90
|
+
/**
|
|
91
|
+
* Build the spec store body for a notebook. The `oceanum` link is not stored in the
|
|
92
|
+
* record. If the body is over `maxBytes`, code-cell outputs and execution counts are
|
|
93
|
+
* dropped; if it is still too large, the result is not ok.
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildSpecBody(content: INotebookContent, path: string, specsUrl: string, maxBytes?: number): BuildResult;
|
|
96
|
+
/**
|
|
97
|
+
* A notebook uploaded from the user's computer, untrusted and linked to nothing: a file
|
|
98
|
+
* someone else downloaded from a record must not save over that record, and its outputs
|
|
99
|
+
* must not run scripts on the origin that holds the Oceanum.io session. Throws if the
|
|
100
|
+
* text is not an nbformat 4 notebook; the message never quotes the file.
|
|
101
|
+
*/
|
|
102
|
+
export declare function notebookFromUpload(text: string): INotebookContent;
|
|
103
|
+
/**
|
|
104
|
+
* The notebook stored in a record, with its `oceanum` link set. Throws if the record
|
|
105
|
+
* does not hold an nbformat 4 notebook.
|
|
106
|
+
*/
|
|
107
|
+
export declare function notebookFromRecord(record: ISpecRecord, specsUrl: string): INotebookContent;
|
|
108
|
+
/**
|
|
109
|
+
* A copy of the notebook stored in a record, linked to nothing: untrusted, and without
|
|
110
|
+
* any `oceanum` link, so saving it creates a record of the user's own instead of
|
|
111
|
+
* writing to this one. Throws if the record does not hold an nbformat 4 notebook.
|
|
112
|
+
*/
|
|
113
|
+
export declare function unlinkedNotebookFromRecord(record: ISpecRecord): INotebookContent;
|
|
114
|
+
/** Make a record name safe to use as a single path segment. */
|
|
115
|
+
export declare function sanitizeName(name: string): string;
|
|
116
|
+
/**
|
|
117
|
+
* A path `Oceanum/<name>.ipynb` that does not collide with `existing` (file names in the
|
|
118
|
+
* `Oceanum` folder), appending ` (1)`, ` (2)`, ... as needed.
|
|
119
|
+
*/
|
|
120
|
+
export declare function uniqueNotebookPath(name: string, existing: Iterable<string>): string;
|
|
121
|
+
/**
|
|
122
|
+
* The names in `existing` that `uniqueNotebookPath` could have given a record called
|
|
123
|
+
* `name`, in the order it would have tried them: `<base>.ipynb`, then `<base> (1).ipynb`,
|
|
124
|
+
* `<base> (2).ipynb` and so on.
|
|
125
|
+
*
|
|
126
|
+
* Opening a record looks through these for the copy a previous session left, so the same
|
|
127
|
+
* record keeps one local file. Without that the second copy is named `<base> (1)`, and
|
|
128
|
+
* since a record takes its name from the file on every save, opening a notebook twice
|
|
129
|
+
* would quietly rename it on Oceanum.
|
|
130
|
+
*/
|
|
131
|
+
export declare function notebookNamesFor(name: string, existing: Iterable<string>): string[];
|
|
132
|
+
/**
|
|
133
|
+
* Split a listing into the caller's own notebooks and the rest, newest first.
|
|
134
|
+
*
|
|
135
|
+
* The store only sends `creator` on records the caller created, so anything else it
|
|
136
|
+
* returns is shared with them — directly, or because it is public. Addresses are
|
|
137
|
+
* compared case-insensitively: the store matches entities as case-sensitive globs, so
|
|
138
|
+
* the two sides can differ in case.
|
|
139
|
+
*/
|
|
140
|
+
export declare function partitionSummaries(items: readonly ISpecSummary[], email: string | null): {
|
|
141
|
+
mine: ISpecSummary[];
|
|
142
|
+
shared: ISpecSummary[];
|
|
143
|
+
};
|
|
144
|
+
/** The `kind` of a Notebook Demo record's spec. */
|
|
145
|
+
export declare const NOTEBOOK_DEMO_KIND = "notebook-demo";
|
|
146
|
+
/** One example in a Notebook Demo record: a notebook record and how to list it. */
|
|
147
|
+
export interface INotebookDemoItem {
|
|
148
|
+
id: string;
|
|
149
|
+
title: string;
|
|
150
|
+
summary?: string;
|
|
151
|
+
}
|
|
152
|
+
/** A titled group of examples. */
|
|
153
|
+
export interface INotebookDemoSection {
|
|
154
|
+
title: string;
|
|
155
|
+
items: INotebookDemoItem[];
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The sections of a Notebook Demo record's spec, keeping only what can be listed: items
|
|
159
|
+
* with a record id and a title, in sections with a title and at least one such item.
|
|
160
|
+
* A spec of another kind or version gives no sections, with a warning.
|
|
161
|
+
*/
|
|
162
|
+
export declare function parseNotebookDemo(spec: unknown): INotebookDemoSection[];
|
|
163
|
+
/** Parse a spec store timestamp, which is UTC but usually has no zone designator. */
|
|
164
|
+
export declare function parseTimestamp(value: string): Date | null;
|
|
165
|
+
/**
|
|
166
|
+
* Whether `email` can be granted access. Glob characters are refused: the server
|
|
167
|
+
* matches user entities as globs.
|
|
168
|
+
*/
|
|
169
|
+
export declare function isShareableEmail(email: string): boolean;
|
|
170
|
+
/** Addresses typed into the share dialog, split into the ones we can grant and the rest. */
|
|
171
|
+
export interface IParsedShareEmails {
|
|
172
|
+
/** Valid, lower-cased and de-duplicated, in the order first written. */
|
|
173
|
+
emails: string[];
|
|
174
|
+
/** Everything that is not a shareable address, as written, for reporting back. */
|
|
175
|
+
invalid: string[];
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Split what the user typed into individual addresses. Commas, semicolons and any
|
|
179
|
+
* whitespace all separate, so one-per-line and a comma-separated list both work.
|
|
180
|
+
*
|
|
181
|
+
* Bare addresses only: a mail client's "To:" line carries display names, and
|
|
182
|
+
* `"Alice B" <alice@example.com>` splits into tokens that are not addresses. Those
|
|
183
|
+
* come back in `invalid` so the user is told, rather than being silently dropped.
|
|
184
|
+
*
|
|
185
|
+
* Addresses are lower-cased because the spec store matches the user entity as a
|
|
186
|
+
* case-sensitive glob against the Auth0 email claim, which Auth0 stores lower case.
|
|
187
|
+
*/
|
|
188
|
+
export declare function parseShareEmails(raw: string): IParsedShareEmails;
|
|
189
|
+
/**
|
|
190
|
+
* How to report share failures.
|
|
191
|
+
*
|
|
192
|
+
* `original` means hand the underlying error to the reporter untouched — it holds the
|
|
193
|
+
* only explanation the user gets, and the reporter reads its type to decide whether to
|
|
194
|
+
* offer sign-in. `partial` is only for the case the original error cannot describe on
|
|
195
|
+
* its own: some addresses worked and some did not, so the message has to name them.
|
|
196
|
+
*/
|
|
197
|
+
export type ShareReport = {
|
|
198
|
+
kind: 'original';
|
|
199
|
+
} | {
|
|
200
|
+
kind: 'partial';
|
|
201
|
+
named: string[];
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Decide how to report a set of failed grants.
|
|
205
|
+
*
|
|
206
|
+
* `entities` are the failed grants' entities, in order; the public grant's entity is
|
|
207
|
+
* the empty string, which names nobody.
|
|
208
|
+
*/
|
|
209
|
+
export declare function reportForShareFailures(grantCount: number, entities: readonly string[], needsSignIn: boolean): ShareReport;
|
|
210
|
+
/**
|
|
211
|
+
* The link that opens a shared notebook in this deployment.
|
|
212
|
+
*
|
|
213
|
+
* JupyterLite is static files, so its page is `lab/index.html`; a JupyterLab server
|
|
214
|
+
* serves the application at `lab` and has no `index.html` to route.
|
|
215
|
+
*/
|
|
216
|
+
export declare function shareLink(baseUrl: string, pageHref: string, id: string): string;
|