@sasjs/api 0.10.0 → 0.12.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/build/browser.js +557 -108
- package/build/cjs/corpus/code.js +44 -7
- package/build/cjs/corpus/deploy.js +50 -0
- package/build/cjs/corpus/runner.js +6 -1
- package/build/cjs/corpus/session.js +142 -0
- package/build/cjs/drive/registry.js +38 -0
- package/build/cjs/host/dispatch.js +44 -0
- package/build/cjs/index.js +5 -1
- package/build/cjs/routes/generated.js +48 -0
- package/build/cjs/routes/overlay.js +8 -22
- package/build/cjs/session/memory-host.js +74 -0
- package/build/cjs/session/service.js +65 -0
- package/build/esm/corpus/code.d.ts.map +1 -1
- package/build/esm/corpus/code.js +44 -7
- package/build/esm/corpus/deploy.d.ts +16 -0
- package/build/esm/corpus/deploy.d.ts.map +1 -0
- package/build/esm/corpus/deploy.js +47 -0
- package/build/esm/corpus/runner.d.ts.map +1 -1
- package/build/esm/corpus/runner.js +6 -1
- package/build/esm/corpus/session.d.ts +17 -0
- package/build/esm/corpus/session.d.ts.map +1 -0
- package/build/esm/corpus/session.js +139 -0
- package/build/esm/drive/registry.d.ts +52 -0
- package/build/esm/drive/registry.d.ts.map +1 -0
- package/build/esm/drive/registry.js +33 -0
- package/build/esm/host/dispatch.d.ts +12 -0
- package/build/esm/host/dispatch.d.ts.map +1 -1
- package/build/esm/host/dispatch.js +44 -0
- package/build/esm/index.d.ts +2 -0
- package/build/esm/index.d.ts.map +1 -1
- package/build/esm/index.js +2 -0
- package/build/esm/routes/generated.d.ts.map +1 -1
- package/build/esm/routes/generated.js +48 -0
- package/build/esm/routes/overlay.d.ts.map +1 -1
- package/build/esm/routes/overlay.js +8 -22
- package/build/esm/session/memory-host.d.ts +19 -0
- package/build/esm/session/memory-host.d.ts.map +1 -0
- package/build/esm/session/memory-host.js +70 -0
- package/build/esm/session/service.d.ts +62 -0
- package/build/esm/session/service.d.ts.map +1 -0
- package/build/esm/session/service.js +61 -0
- package/package.json +1 -1
|
@@ -344,6 +344,14 @@ export const GENERATED_ROUTES = [
|
|
|
344
344
|
{
|
|
345
345
|
status: 200,
|
|
346
346
|
description: 'Ok'
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
status: 400,
|
|
350
|
+
description: '"code" is required / "code" must be a string / "code" is not allowed to be empty'
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
status: 401,
|
|
354
|
+
description: 'Unauthorized'
|
|
347
355
|
}
|
|
348
356
|
]
|
|
349
357
|
},
|
|
@@ -372,6 +380,14 @@ export const GENERATED_ROUTES = [
|
|
|
372
380
|
{
|
|
373
381
|
status: 200,
|
|
374
382
|
description: 'Ok'
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
status: 400,
|
|
386
|
+
description: '"code" is required / "code" must be a string / "code" is not allowed to be empty'
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
status: 401,
|
|
390
|
+
description: 'Unauthorized'
|
|
375
391
|
}
|
|
376
392
|
]
|
|
377
393
|
},
|
|
@@ -1282,6 +1298,14 @@ export const GENERATED_ROUTES = [
|
|
|
1282
1298
|
{
|
|
1283
1299
|
status: 200,
|
|
1284
1300
|
description: 'Ok'
|
|
1301
|
+
},
|
|
1302
|
+
{
|
|
1303
|
+
status: 401,
|
|
1304
|
+
description: 'No valid access token or session was supplied'
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
status: 404,
|
|
1308
|
+
description: 'Session with the given ID was not found.'
|
|
1285
1309
|
}
|
|
1286
1310
|
]
|
|
1287
1311
|
},
|
|
@@ -1304,6 +1328,14 @@ export const GENERATED_ROUTES = [
|
|
|
1304
1328
|
{
|
|
1305
1329
|
status: 200,
|
|
1306
1330
|
description: 'Ok'
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
status: 401,
|
|
1334
|
+
description: 'No valid access token or session was supplied'
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
status: 404,
|
|
1338
|
+
description: 'Session with the given ID was not found.'
|
|
1307
1339
|
}
|
|
1308
1340
|
]
|
|
1309
1341
|
},
|
|
@@ -1326,6 +1358,14 @@ export const GENERATED_ROUTES = [
|
|
|
1326
1358
|
{
|
|
1327
1359
|
status: 200,
|
|
1328
1360
|
description: 'Ok'
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
status: 401,
|
|
1364
|
+
description: 'No valid access token or session was supplied'
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
status: 404,
|
|
1368
|
+
description: 'Session with the given ID was not found.'
|
|
1329
1369
|
}
|
|
1330
1370
|
]
|
|
1331
1371
|
},
|
|
@@ -1348,6 +1388,14 @@ export const GENERATED_ROUTES = [
|
|
|
1348
1388
|
{
|
|
1349
1389
|
status: 200,
|
|
1350
1390
|
description: 'Ok'
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
status: 401,
|
|
1394
|
+
description: 'No valid access token or session was supplied'
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
status: 404,
|
|
1398
|
+
description: 'Session with the given ID was not found.'
|
|
1351
1399
|
}
|
|
1352
1400
|
]
|
|
1353
1401
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,yDAAyD;AACzD,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CAAC;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;
|
|
1
|
+
{"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,yDAAyD;AACzD,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CAAC;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;AAqCD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CA6MhD,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,UAAU,EAA8C,CAAA"}
|
|
@@ -24,12 +24,10 @@ export const TAG_CAPABILITIES = {
|
|
|
24
24
|
*/
|
|
25
25
|
const DIRECTORY = 'Synchronising with an external auth provider needs a directory port, which this package does not have: a host decides which directory it holds accounts in, and answers this itself.';
|
|
26
26
|
const RUNTIME = 'Starting a program needs a runtime port, which this package does not have: a host decides which languages it holds, and answers this itself until then.';
|
|
27
|
-
const SESSION_RECORD = "A triggered session's record, log, output and state belong to the runtime that ran the program, and this package has no runtime port.";
|
|
28
27
|
const HOST_CONFIG = "The body is the host's own configuration - ports, run times and providers - which a mock states for itself and this package cannot know.";
|
|
29
28
|
const HOST_OWNED = "The host owns this report: it describes the host's own process and files, which a browser build has no equivalent of.";
|
|
30
29
|
const WEB_BUNDLE = 'The host serves its own web bundle; there is no API decision here for this package to answer.';
|
|
31
30
|
const ARCHIVE = "Expanding the uploaded archive is the host's transport work - multer and a zip library on the server, `DecompressionStream` in a worker - and the host hands this package the tree it produces, which is the same tree `POST /SASjsApi/drive/deploy` takes.";
|
|
32
|
-
const UNPUBLISH = 'Unpublishing inverts the deploy route, so the two land together.';
|
|
33
31
|
const success = { status: 'success' };
|
|
34
32
|
const user = {
|
|
35
33
|
uid: '',
|
|
@@ -163,10 +161,7 @@ export const OVERLAY = {
|
|
|
163
161
|
},
|
|
164
162
|
// `404` is a pair - the registry name and the appLoc it was published from -
|
|
165
163
|
// that matches no entry, and the drive's content is deliberately left alone.
|
|
166
|
-
'DELETE /SASjsApi/drive/deploy': {
|
|
167
|
-
implemented: false,
|
|
168
|
-
reason: UNPUBLISH
|
|
169
|
-
},
|
|
164
|
+
'DELETE /SASjsApi/drive/deploy': { implemented: true },
|
|
170
165
|
// Group - members and the groups themselves
|
|
171
166
|
'GET /SASjsApi/group': { implemented: true },
|
|
172
167
|
'GET /SASjsApi/group/{uid}': { implemented: true },
|
|
@@ -202,22 +197,13 @@ export const OVERLAY = {
|
|
|
202
197
|
implemented: true,
|
|
203
198
|
responses: { 200: user }
|
|
204
199
|
},
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
},
|
|
209
|
-
'GET /SASjsApi/session/{sessionId}/log': {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
},
|
|
213
|
-
'GET /SASjsApi/session/{sessionId}/output': {
|
|
214
|
-
implemented: false,
|
|
215
|
-
reason: SESSION_RECORD
|
|
216
|
-
},
|
|
217
|
-
'GET /SASjsApi/session/{sessionId}/state': {
|
|
218
|
-
implemented: false,
|
|
219
|
-
reason: SESSION_RECORD
|
|
220
|
-
},
|
|
200
|
+
// The port takes the owner as a uid and falls back to a username, which is
|
|
201
|
+
// what sasjs/server records today - a host hands its sessions through as they
|
|
202
|
+
// are, and the comparison tries the identifier first.
|
|
203
|
+
'GET /SASjsApi/session/{sessionId}': { implemented: true },
|
|
204
|
+
'GET /SASjsApi/session/{sessionId}/log': { implemented: true },
|
|
205
|
+
'GET /SASjsApi/session/{sessionId}/output': { implemented: true },
|
|
206
|
+
'GET /SASjsApi/session/{sessionId}/state': { implemented: true },
|
|
221
207
|
// STP - a stored program, on the runtime port
|
|
222
208
|
'GET /SASjsApi/stp/execute': { implemented: false, reason: RUNTIME },
|
|
223
209
|
'POST /SASjsApi/stp/execute': { implemented: false, reason: RUNTIME },
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HostSession, SessionHost, SessionStateResponse } from './service.js';
|
|
2
|
+
/**
|
|
3
|
+
* The sessions the corpus and the package's own tests read.
|
|
4
|
+
*
|
|
5
|
+
* One session per account, so the interesting case is expressible: an
|
|
6
|
+
* administrator may read a session that is not theirs, and the other way round
|
|
7
|
+
* answers the 404 that hides it. `sess-silent` exists with nothing written yet,
|
|
8
|
+
* because an empty log is a 200 and not a 404.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_SESSIONS: Record<string, HostSession>;
|
|
11
|
+
export declare class MemorySessionHost implements SessionHost {
|
|
12
|
+
private readonly sessions;
|
|
13
|
+
constructor(sessions?: Record<string, HostSession>);
|
|
14
|
+
session(id: string): Promise<HostSession>;
|
|
15
|
+
state(id: string): Promise<SessionStateResponse | undefined>;
|
|
16
|
+
log(id: string): Promise<string | undefined>;
|
|
17
|
+
output(id: string): Promise<string | undefined>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=memory-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-host.d.ts","sourceRoot":"","sources":["../../../src/session/memory-host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,oBAAoB,EACrB,MAAM,cAAc,CAAA;AAErB;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CA8BxD,CAAA;AAmBD,qBAAa,iBAAkB,YAAW,WAAW;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,8BAAmB;IAElD,OAAO,CAAC,EAAE,EAAE,MAAM;IAIlB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAI5D,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAI5C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;CAGtD"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sessions the corpus and the package's own tests read.
|
|
3
|
+
*
|
|
4
|
+
* One session per account, so the interesting case is expressible: an
|
|
5
|
+
* administrator may read a session that is not theirs, and the other way round
|
|
6
|
+
* answers the 404 that hides it. `sess-silent` exists with nothing written yet,
|
|
7
|
+
* because an empty log is a 200 and not a 404.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_SESSIONS = {
|
|
10
|
+
'sess-admin': {
|
|
11
|
+
owner: 'user-admin',
|
|
12
|
+
summary: {
|
|
13
|
+
sessionId: 'sess-admin',
|
|
14
|
+
state: 'completed',
|
|
15
|
+
pid: 4242,
|
|
16
|
+
creationTimeStamp: '2026-11-07T09:00:00.000Z',
|
|
17
|
+
deathTimeStamp: '2026-11-07T09:00:04.000Z',
|
|
18
|
+
expiresAfterMins: 60
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
'sess-restricted': {
|
|
22
|
+
owner: 'user-restricted',
|
|
23
|
+
summary: {
|
|
24
|
+
sessionId: 'sess-restricted',
|
|
25
|
+
state: 'running',
|
|
26
|
+
pid: 4243,
|
|
27
|
+
creationTimeStamp: '2026-11-07T09:05:00.000Z'
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
'sess-silent': {
|
|
31
|
+
owner: 'user-admin',
|
|
32
|
+
summary: {
|
|
33
|
+
sessionId: 'sess-silent',
|
|
34
|
+
state: 'running',
|
|
35
|
+
pid: 4244,
|
|
36
|
+
creationTimeStamp: '2026-11-07T09:10:00.000Z'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const STATES = {
|
|
41
|
+
'sess-admin': { state: 'completed', pid: 4242, lineCount: 12 },
|
|
42
|
+
'sess-restricted': { state: 'running', pid: 4243, lineCount: 3 }
|
|
43
|
+
};
|
|
44
|
+
const LOGS = {
|
|
45
|
+
'sess-admin': 'NOTE: session started\nNOTE: session completed\n',
|
|
46
|
+
'sess-restricted': 'NOTE: session started\n',
|
|
47
|
+
'sess-silent': ''
|
|
48
|
+
};
|
|
49
|
+
const OUTPUTS = {
|
|
50
|
+
'sess-admin': '<html><body>42</body></html>',
|
|
51
|
+
'sess-restricted': '',
|
|
52
|
+
'sess-silent': ''
|
|
53
|
+
};
|
|
54
|
+
export class MemorySessionHost {
|
|
55
|
+
constructor(sessions = DEFAULT_SESSIONS) {
|
|
56
|
+
this.sessions = sessions;
|
|
57
|
+
}
|
|
58
|
+
async session(id) {
|
|
59
|
+
return this.sessions[id];
|
|
60
|
+
}
|
|
61
|
+
async state(id) {
|
|
62
|
+
return STATES[id];
|
|
63
|
+
}
|
|
64
|
+
async log(id) {
|
|
65
|
+
return LOGS[id];
|
|
66
|
+
}
|
|
67
|
+
async output(id) {
|
|
68
|
+
return OUTPUTS[id];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Subject } from '../identity/types.js';
|
|
2
|
+
import type { IdentityStore } from '../identity/types.js';
|
|
3
|
+
/** A triggered session as the API reports one. */
|
|
4
|
+
export interface SessionSummary {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
state: string;
|
|
7
|
+
pid?: number;
|
|
8
|
+
creationTimeStamp?: string;
|
|
9
|
+
deathTimeStamp?: string;
|
|
10
|
+
failureReason?: string;
|
|
11
|
+
expiresAfterMins?: number;
|
|
12
|
+
}
|
|
13
|
+
/** What `/state` answers: whatever the host's run time reports for a session. */
|
|
14
|
+
export type SessionStateResponse = Record<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* A session as the host holds it.
|
|
17
|
+
*
|
|
18
|
+
* `owner` should be a **uid**: a username is mutable client data - an account
|
|
19
|
+
* can be renamed, and two accounts can swap names - so a name comparison can
|
|
20
|
+
* hand a session to whoever holds the name later. A username is accepted too,
|
|
21
|
+
* because sasjs/server's own session records carry the owner's username, and a
|
|
22
|
+
* host that hands those straight through the port must not 404 the owner of
|
|
23
|
+
* every session. The uid is tried first; the name is the fallback.
|
|
24
|
+
*/
|
|
25
|
+
export interface HostSession {
|
|
26
|
+
owner: string;
|
|
27
|
+
summary: SessionSummary;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The host's sessions: a run time's registry, and the files that session wrote.
|
|
31
|
+
*
|
|
32
|
+
* Every capability answers `undefined` for a session the host does not hold,
|
|
33
|
+
* which the service turns into a 404. `log` and `output` answer an empty string
|
|
34
|
+
* for one that exists but has written nothing yet, because that is not an error.
|
|
35
|
+
*/
|
|
36
|
+
export interface SessionHost {
|
|
37
|
+
session(id: string): Promise<HostSession | undefined>;
|
|
38
|
+
state(id: string): Promise<SessionStateResponse | undefined>;
|
|
39
|
+
log(id: string): Promise<string | undefined>;
|
|
40
|
+
output(id: string): Promise<string | undefined>;
|
|
41
|
+
}
|
|
42
|
+
export interface SessionService {
|
|
43
|
+
summary(caller: Subject | undefined, id: string): Promise<SessionSummary>;
|
|
44
|
+
state(caller: Subject | undefined, id: string): Promise<SessionStateResponse>;
|
|
45
|
+
log(caller: Subject | undefined, id: string): Promise<string>;
|
|
46
|
+
output(caller: Subject | undefined, id: string): Promise<string>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The four session routes, over the host's own session store.
|
|
50
|
+
*
|
|
51
|
+
* The caller passes a `Subject` and nothing else: which account that is, whether
|
|
52
|
+
* it is an administrator, and whether the session is theirs are all resolved
|
|
53
|
+
* here, against the same store the records service reads. A host never states a
|
|
54
|
+
* role.
|
|
55
|
+
*
|
|
56
|
+
* `404` covers both "no such session" and "not yours", on purpose: a session id
|
|
57
|
+
* is handed to the caller that triggered the program and is built from a
|
|
58
|
+
* timestamp plus a small random number, so it is guessable within a window, and
|
|
59
|
+
* confirming the id exists would say it was right.
|
|
60
|
+
*/
|
|
61
|
+
export declare const createSessionService: (store: IdentityStore, host: SessionHost) => SessionService;
|
|
62
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/session/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,sBAAsB,CAAA;AAE7E,kDAAkD;AAClD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,iFAAiF;AACjF,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAE1D;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,cAAc,CAAA;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAAA;IACrD,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAAA;IAC5D,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAC5C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CAChD;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IACzE,KAAK,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAA;IAC7E,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC7D,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACjE;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAC/B,OAAO,aAAa,EACpB,MAAM,WAAW,KAChB,cAsDF,CAAA"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ApiError } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The four session routes, over the host's own session store.
|
|
4
|
+
*
|
|
5
|
+
* The caller passes a `Subject` and nothing else: which account that is, whether
|
|
6
|
+
* it is an administrator, and whether the session is theirs are all resolved
|
|
7
|
+
* here, against the same store the records service reads. A host never states a
|
|
8
|
+
* role.
|
|
9
|
+
*
|
|
10
|
+
* `404` covers both "no such session" and "not yours", on purpose: a session id
|
|
11
|
+
* is handed to the caller that triggered the program and is built from a
|
|
12
|
+
* timestamp plus a small random number, so it is guessable within a window, and
|
|
13
|
+
* confirming the id exists would say it was right.
|
|
14
|
+
*/
|
|
15
|
+
export const createSessionService = (store, host) => {
|
|
16
|
+
/** The server's wording, so adopting it here does not become a later change. */
|
|
17
|
+
const notFound = (id) => `Session with ID '${id}' was not found.`;
|
|
18
|
+
/** The caller, and the session if they may see it - or the 404 that hides it. */
|
|
19
|
+
const visible = async (caller, id) => {
|
|
20
|
+
if (!caller)
|
|
21
|
+
throw new ApiError(401, 'Unauthorized');
|
|
22
|
+
const reader = await store.userById(caller.userId);
|
|
23
|
+
if (!reader || !reader.isActive)
|
|
24
|
+
throw new ApiError(401, 'Unauthorized');
|
|
25
|
+
const session = await host.session(id);
|
|
26
|
+
if (!session)
|
|
27
|
+
throw new ApiError(404, notFound(id));
|
|
28
|
+
// the uid is the key to compare; a name is the fallback a host may need
|
|
29
|
+
const owner = session.owner;
|
|
30
|
+
if (owner !== reader.uid && owner !== reader.username && !reader.isAdmin)
|
|
31
|
+
throw new ApiError(404, notFound(id));
|
|
32
|
+
return session;
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
async summary(caller, id) {
|
|
36
|
+
const session = await visible(caller, id);
|
|
37
|
+
return session.summary;
|
|
38
|
+
},
|
|
39
|
+
async state(caller, id) {
|
|
40
|
+
await visible(caller, id);
|
|
41
|
+
const state = await host.state(id);
|
|
42
|
+
if (!state)
|
|
43
|
+
throw new ApiError(404, notFound(id));
|
|
44
|
+
return state;
|
|
45
|
+
},
|
|
46
|
+
async log(caller, id) {
|
|
47
|
+
await visible(caller, id);
|
|
48
|
+
const log = await host.log(id);
|
|
49
|
+
if (log === undefined)
|
|
50
|
+
throw new ApiError(404, notFound(id));
|
|
51
|
+
return log;
|
|
52
|
+
},
|
|
53
|
+
async output(caller, id) {
|
|
54
|
+
await visible(caller, id);
|
|
55
|
+
const output = await host.output(id);
|
|
56
|
+
if (output === undefined)
|
|
57
|
+
throw new ApiError(404, notFound(id));
|
|
58
|
+
return output;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
};
|
package/package.json
CHANGED