@sasjs/api 0.10.0 → 0.11.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 +375 -103
- package/build/cjs/corpus/code.js +44 -7
- package/build/cjs/corpus/runner.js +4 -1
- package/build/cjs/corpus/session.js +73 -0
- package/build/cjs/host/dispatch.js +35 -0
- package/build/cjs/index.js +3 -1
- package/build/cjs/routes/generated.js +16 -0
- package/build/cjs/routes/overlay.js +7 -17
- 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/runner.d.ts.map +1 -1
- package/build/esm/corpus/runner.js +4 -1
- package/build/esm/corpus/session.d.ts +16 -0
- package/build/esm/corpus/session.d.ts.map +1 -0
- package/build/esm/corpus/session.js +70 -0
- package/build/esm/host/dispatch.d.ts +6 -0
- package/build/esm/host/dispatch.d.ts.map +1 -1
- package/build/esm/host/dispatch.js +35 -0
- package/build/esm/index.d.ts +1 -0
- package/build/esm/index.d.ts.map +1 -1
- package/build/esm/index.js +1 -0
- package/build/esm/routes/generated.d.ts.map +1 -1
- package/build/esm/routes/generated.js +16 -0
- package/build/esm/routes/overlay.d.ts.map +1 -1
- package/build/esm/routes/overlay.js +7 -17
- 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
package/build/cjs/corpus/code.js
CHANGED
|
@@ -38,13 +38,50 @@ exports.CODE_CORPUS = [
|
|
|
38
38
|
]
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
{
|
|
42
|
+
id: 'code-lint-unauthenticated',
|
|
43
|
+
route: 'POST /SASjsApi/code/lint',
|
|
44
|
+
description: 'refuses a caller with no token',
|
|
45
|
+
request: { body: { code: 'data _null_; run;' } },
|
|
46
|
+
expect: { status: 401, error: 'Unauthorized' }
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'code-lint-non-string',
|
|
50
|
+
route: 'POST /SASjsApi/code/lint',
|
|
51
|
+
description: 'refuses a code value that is not a string, rather than coercing it',
|
|
52
|
+
request: {
|
|
53
|
+
subject: { userId: 'user-admin', clientId: 'web_app' },
|
|
54
|
+
body: { code: 123 }
|
|
55
|
+
},
|
|
56
|
+
expect: { status: 400, error: '"code" must be a string' }
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'code-lint-empty',
|
|
60
|
+
route: 'POST /SASjsApi/code/lint',
|
|
61
|
+
description: 'refuses an empty buffer, which is not the same as a missing one',
|
|
62
|
+
request: {
|
|
63
|
+
subject: { userId: 'user-admin', clientId: 'web_app' },
|
|
64
|
+
body: { code: '' }
|
|
65
|
+
},
|
|
66
|
+
expect: { status: 400, error: '"code" is not allowed to be empty' }
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'code-format-unauthenticated',
|
|
70
|
+
route: 'POST /SASjsApi/code/format',
|
|
71
|
+
description: 'refuses a caller with no token',
|
|
72
|
+
request: { body: { code: 'data _null_; run;' } },
|
|
73
|
+
expect: { status: 401, error: 'Unauthorized' }
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'code-format-non-string',
|
|
77
|
+
route: 'POST /SASjsApi/code/format',
|
|
78
|
+
description: 'refuses a code value that is not a string',
|
|
79
|
+
request: {
|
|
80
|
+
subject: { userId: 'user-admin', clientId: 'web_app' },
|
|
81
|
+
body: { code: 123 }
|
|
82
|
+
},
|
|
83
|
+
expect: { status: 400, error: '"code" must be a string' }
|
|
84
|
+
},
|
|
48
85
|
{
|
|
49
86
|
id: 'code-format-adds-the-header',
|
|
50
87
|
route: 'POST /SASjsApi/code/format',
|
|
@@ -6,6 +6,8 @@ const service_js_1 = require("../drive/service.js");
|
|
|
6
6
|
const memory_store_js_1 = require("../drive/memory-store.js");
|
|
7
7
|
const service_js_2 = require("../identity/service.js");
|
|
8
8
|
const records_js_1 = require("../identity/records.js");
|
|
9
|
+
const service_js_3 = require("../session/service.js");
|
|
10
|
+
const memory_host_js_1 = require("../session/memory-host.js");
|
|
9
11
|
const memory_store_js_2 = require("../identity/memory-store.js");
|
|
10
12
|
const fixture_js_1 = require("../identity/fixture.js");
|
|
11
13
|
const types_js_1 = require("../types.js");
|
|
@@ -26,7 +28,8 @@ const servicesFor = (testCase) => {
|
|
|
26
28
|
? new RefusingDriveStore(testCase.seed || {})
|
|
27
29
|
: new memory_store_js_1.MemoryDriveStore(testCase.seed || {})),
|
|
28
30
|
identity: (0, service_js_2.createIdentityService)(identity),
|
|
29
|
-
records: (0, records_js_1.createRecordService)(identity)
|
|
31
|
+
records: (0, records_js_1.createRecordService)(identity),
|
|
32
|
+
session: (0, service_js_3.createSessionService)(identity, new memory_host_js_1.MemorySessionHost())
|
|
30
33
|
};
|
|
31
34
|
};
|
|
32
35
|
exports.servicesFor = servicesFor;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SESSION_CORPUS = void 0;
|
|
4
|
+
const ADMIN = { userId: 'user-admin', clientId: 'web_app' };
|
|
5
|
+
const RESTRICTED = { userId: 'user-restricted', clientId: 'web_app' };
|
|
6
|
+
exports.SESSION_CORPUS = [
|
|
7
|
+
{
|
|
8
|
+
id: 'session-summary-own',
|
|
9
|
+
route: 'GET /SASjsApi/session/{sessionId}',
|
|
10
|
+
description: 'answers the owner the summary of their own session',
|
|
11
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-admin' } },
|
|
12
|
+
expect: {
|
|
13
|
+
status: 200,
|
|
14
|
+
body: {
|
|
15
|
+
sessionId: 'sess-admin',
|
|
16
|
+
state: 'completed',
|
|
17
|
+
pid: 4242,
|
|
18
|
+
creationTimeStamp: '2026-11-07T09:00:00.000Z',
|
|
19
|
+
deathTimeStamp: '2026-11-07T09:00:04.000Z',
|
|
20
|
+
expiresAfterMins: 60
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'session-summary-as-admin',
|
|
26
|
+
route: 'GET /SASjsApi/session/{sessionId}',
|
|
27
|
+
description: "answers an administrator another account's session",
|
|
28
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-restricted' } },
|
|
29
|
+
expect: {
|
|
30
|
+
status: 200,
|
|
31
|
+
body: {
|
|
32
|
+
sessionId: 'sess-restricted',
|
|
33
|
+
state: 'running',
|
|
34
|
+
pid: 4243,
|
|
35
|
+
creationTimeStamp: '2026-11-07T09:05:00.000Z'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'session-log-own',
|
|
41
|
+
route: 'GET /SASjsApi/session/{sessionId}/log',
|
|
42
|
+
description: 'answers the log this session wrote',
|
|
43
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-admin' } },
|
|
44
|
+
expect: {
|
|
45
|
+
status: 200,
|
|
46
|
+
contents: 'NOTE: session started\nNOTE: session completed\n'
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'session-log-nothing-written',
|
|
51
|
+
route: 'GET /SASjsApi/session/{sessionId}/log',
|
|
52
|
+
description: 'answers an empty body for a session that has written nothing',
|
|
53
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-silent' } },
|
|
54
|
+
expect: { status: 200, contents: '' }
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'session-output-own',
|
|
58
|
+
route: 'GET /SASjsApi/session/{sessionId}/output',
|
|
59
|
+
description: 'answers the webout this session produced',
|
|
60
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-admin' } },
|
|
61
|
+
expect: { status: 200, contents: '<html><body>42</body></html>' }
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'session-state-own',
|
|
65
|
+
route: 'GET /SASjsApi/session/{sessionId}/state',
|
|
66
|
+
description: "answers the run time's state for this session",
|
|
67
|
+
request: { subject: RESTRICTED, params: { sessionId: 'sess-restricted' } },
|
|
68
|
+
expect: {
|
|
69
|
+
status: 200,
|
|
70
|
+
body: { state: 'running', pid: 4243, lineCount: 3 }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
];
|
|
@@ -74,6 +74,12 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
74
74
|
const body = request.body || {};
|
|
75
75
|
const params = request.params || {};
|
|
76
76
|
/** The record service, or the 501 a host without one answers. */
|
|
77
|
+
/** The host's sessions, or the 501 that names the missing capability. */
|
|
78
|
+
const sessionRequired = () => {
|
|
79
|
+
if (!services.session)
|
|
80
|
+
throw new types_js_1.ApiError(501, `This host has no session service, so it cannot answer ${routeId}.`);
|
|
81
|
+
return services.session;
|
|
82
|
+
};
|
|
77
83
|
const required = () => {
|
|
78
84
|
if (!records)
|
|
79
85
|
throw new types_js_1.ApiError(501, `This host has no record service, so it cannot answer ${routeId}.`);
|
|
@@ -171,6 +177,31 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
171
177
|
}
|
|
172
178
|
case 'GET /SASjsApi/drive/filetree':
|
|
173
179
|
return { status: 200, body: await drive.tree(query._folderPath) };
|
|
180
|
+
// ------------------------------------------------------------- sessions
|
|
181
|
+
case 'GET /SASjsApi/session/{sessionId}': {
|
|
182
|
+
return {
|
|
183
|
+
status: 200,
|
|
184
|
+
body: await sessionRequired().summary(await caller(), String(params.sessionId ?? ''))
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
case 'GET /SASjsApi/session/{sessionId}/log': {
|
|
188
|
+
return {
|
|
189
|
+
status: 200,
|
|
190
|
+
contents: await sessionRequired().log(await caller(), String(params.sessionId ?? ''))
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
case 'GET /SASjsApi/session/{sessionId}/output': {
|
|
194
|
+
return {
|
|
195
|
+
status: 200,
|
|
196
|
+
contents: await sessionRequired().output(await caller(), String(params.sessionId ?? ''))
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
case 'GET /SASjsApi/session/{sessionId}/state': {
|
|
200
|
+
return {
|
|
201
|
+
status: 200,
|
|
202
|
+
body: await sessionRequired().state(await caller(), String(params.sessionId ?? ''))
|
|
203
|
+
};
|
|
204
|
+
}
|
|
174
205
|
// ------------------------------------------------------------- records
|
|
175
206
|
//
|
|
176
207
|
// The administration routes: users, groups, permission rules, clients and the
|
|
@@ -357,6 +388,10 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
357
388
|
exports.dispatch = dispatch;
|
|
358
389
|
/** Whether a route is one `dispatch` can answer today. */
|
|
359
390
|
exports.HANDLED_ROUTES = [
|
|
391
|
+
'GET /SASjsApi/session/{sessionId}',
|
|
392
|
+
'GET /SASjsApi/session/{sessionId}/log',
|
|
393
|
+
'GET /SASjsApi/session/{sessionId}/output',
|
|
394
|
+
'GET /SASjsApi/session/{sessionId}/state',
|
|
360
395
|
'POST /SASjsApi/code/lint',
|
|
361
396
|
'POST /SASjsApi/code/format',
|
|
362
397
|
'GET /SASjsApi/info/authorizedRoutes',
|
package/build/cjs/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.servicesFor = exports.runCase = exports.PROVIDER_MANAGED = exports.ADMIN_SIGNED_IN = exports.RECORD_CORPUS = exports.IDENTITY_CORPUS = exports.DRIVE_CORPUS = exports.parseIdentity = exports.IDENTITY_FORMAT = exports.DEFAULT_IDENTITY = exports.MemoryIdentityStore = exports.CODE_CORPUS = exports.sanitiseDisplayName = exports.createRecordService = exports.createIdentityService = exports.HANDLED_ROUTES = exports.dispatch = exports.TAG_CAPABILITIES = exports.OVERLAY = exports.findRoute = exports.routeById = exports.ROUTES = exports.MemoryDriveStore = exports.measureDeployTree = exports.isDeployTree = exports.deployTree = exports.decodeBase64 = exports.checkDeployTree = exports.DEPLOY_SUCCESS = exports.DEPLOY_PAYLOAD_BYTES = exports.DEPLOY_LIMITS = exports.createDriveService = exports.isRoot = exports.basename = exports.dirname = exports.hasBlockedExtension = exports.normaliseDrivePath = void 0;
|
|
17
|
+
exports.servicesFor = exports.runCase = exports.PROVIDER_MANAGED = exports.ADMIN_SIGNED_IN = exports.RECORD_CORPUS = exports.SESSION_CORPUS = exports.IDENTITY_CORPUS = exports.DRIVE_CORPUS = exports.parseIdentity = exports.IDENTITY_FORMAT = exports.DEFAULT_IDENTITY = exports.MemoryIdentityStore = exports.CODE_CORPUS = exports.sanitiseDisplayName = exports.createRecordService = exports.createIdentityService = exports.HANDLED_ROUTES = exports.dispatch = exports.TAG_CAPABILITIES = exports.OVERLAY = exports.findRoute = exports.routeById = exports.ROUTES = exports.MemoryDriveStore = exports.measureDeployTree = exports.isDeployTree = exports.deployTree = exports.decodeBase64 = exports.checkDeployTree = exports.DEPLOY_SUCCESS = exports.DEPLOY_PAYLOAD_BYTES = exports.DEPLOY_LIMITS = exports.createDriveService = exports.isRoot = exports.basename = exports.dirname = exports.hasBlockedExtension = exports.normaliseDrivePath = void 0;
|
|
18
18
|
/**
|
|
19
19
|
* `@sasjs/api` - the SASjs Server API described once.
|
|
20
20
|
*
|
|
@@ -79,6 +79,8 @@ var drive_js_1 = require("./corpus/drive.js");
|
|
|
79
79
|
Object.defineProperty(exports, "DRIVE_CORPUS", { enumerable: true, get: function () { return drive_js_1.DRIVE_CORPUS; } });
|
|
80
80
|
var identity_js_1 = require("./corpus/identity.js");
|
|
81
81
|
Object.defineProperty(exports, "IDENTITY_CORPUS", { enumerable: true, get: function () { return identity_js_1.IDENTITY_CORPUS; } });
|
|
82
|
+
var session_js_1 = require("./corpus/session.js");
|
|
83
|
+
Object.defineProperty(exports, "SESSION_CORPUS", { enumerable: true, get: function () { return session_js_1.SESSION_CORPUS; } });
|
|
82
84
|
var records_js_2 = require("./corpus/records.js");
|
|
83
85
|
Object.defineProperty(exports, "RECORD_CORPUS", { enumerable: true, get: function () { return records_js_2.RECORD_CORPUS; } });
|
|
84
86
|
Object.defineProperty(exports, "ADMIN_SIGNED_IN", { enumerable: true, get: function () { return records_js_2.ADMIN_SIGNED_IN; } });
|
|
@@ -347,6 +347,14 @@ exports.GENERATED_ROUTES = [
|
|
|
347
347
|
{
|
|
348
348
|
status: 200,
|
|
349
349
|
description: 'Ok'
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
status: 400,
|
|
353
|
+
description: '"code" is required / "code" must be a string / "code" is not allowed to be empty'
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
status: 401,
|
|
357
|
+
description: 'Unauthorized'
|
|
350
358
|
}
|
|
351
359
|
]
|
|
352
360
|
},
|
|
@@ -375,6 +383,14 @@ exports.GENERATED_ROUTES = [
|
|
|
375
383
|
{
|
|
376
384
|
status: 200,
|
|
377
385
|
description: 'Ok'
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
status: 400,
|
|
389
|
+
description: '"code" is required / "code" must be a string / "code" is not allowed to be empty'
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
status: 401,
|
|
393
|
+
description: 'Unauthorized'
|
|
378
394
|
}
|
|
379
395
|
]
|
|
380
396
|
},
|
|
@@ -27,7 +27,6 @@ exports.TAG_CAPABILITIES = {
|
|
|
27
27
|
*/
|
|
28
28
|
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.';
|
|
29
29
|
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.';
|
|
30
|
-
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.";
|
|
31
30
|
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.";
|
|
32
31
|
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.";
|
|
33
32
|
const WEB_BUNDLE = 'The host serves its own web bundle; there is no API decision here for this package to answer.';
|
|
@@ -205,22 +204,13 @@ exports.OVERLAY = {
|
|
|
205
204
|
implemented: true,
|
|
206
205
|
responses: { 200: user }
|
|
207
206
|
},
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
},
|
|
212
|
-
'GET /SASjsApi/session/{sessionId}/log': {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
},
|
|
216
|
-
'GET /SASjsApi/session/{sessionId}/output': {
|
|
217
|
-
implemented: false,
|
|
218
|
-
reason: SESSION_RECORD
|
|
219
|
-
},
|
|
220
|
-
'GET /SASjsApi/session/{sessionId}/state': {
|
|
221
|
-
implemented: false,
|
|
222
|
-
reason: SESSION_RECORD
|
|
223
|
-
},
|
|
207
|
+
// The port takes the owner as a uid and falls back to a username, which is
|
|
208
|
+
// what sasjs/server records today - a host hands its sessions through as they
|
|
209
|
+
// are, and the comparison tries the identifier first.
|
|
210
|
+
'GET /SASjsApi/session/{sessionId}': { implemented: true },
|
|
211
|
+
'GET /SASjsApi/session/{sessionId}/log': { implemented: true },
|
|
212
|
+
'GET /SASjsApi/session/{sessionId}/output': { implemented: true },
|
|
213
|
+
'GET /SASjsApi/session/{sessionId}/state': { implemented: true },
|
|
224
214
|
// STP - a stored program, on the runtime port
|
|
225
215
|
'GET /SASjsApi/stp/execute': { implemented: false, reason: RUNTIME },
|
|
226
216
|
'POST /SASjsApi/stp/execute': { implemented: false, reason: RUNTIME },
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MemorySessionHost = exports.DEFAULT_SESSIONS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The sessions the corpus and the package's own tests read.
|
|
6
|
+
*
|
|
7
|
+
* One session per account, so the interesting case is expressible: an
|
|
8
|
+
* administrator may read a session that is not theirs, and the other way round
|
|
9
|
+
* answers the 404 that hides it. `sess-silent` exists with nothing written yet,
|
|
10
|
+
* because an empty log is a 200 and not a 404.
|
|
11
|
+
*/
|
|
12
|
+
exports.DEFAULT_SESSIONS = {
|
|
13
|
+
'sess-admin': {
|
|
14
|
+
owner: 'user-admin',
|
|
15
|
+
summary: {
|
|
16
|
+
sessionId: 'sess-admin',
|
|
17
|
+
state: 'completed',
|
|
18
|
+
pid: 4242,
|
|
19
|
+
creationTimeStamp: '2026-11-07T09:00:00.000Z',
|
|
20
|
+
deathTimeStamp: '2026-11-07T09:00:04.000Z',
|
|
21
|
+
expiresAfterMins: 60
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
'sess-restricted': {
|
|
25
|
+
owner: 'user-restricted',
|
|
26
|
+
summary: {
|
|
27
|
+
sessionId: 'sess-restricted',
|
|
28
|
+
state: 'running',
|
|
29
|
+
pid: 4243,
|
|
30
|
+
creationTimeStamp: '2026-11-07T09:05:00.000Z'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
'sess-silent': {
|
|
34
|
+
owner: 'user-admin',
|
|
35
|
+
summary: {
|
|
36
|
+
sessionId: 'sess-silent',
|
|
37
|
+
state: 'running',
|
|
38
|
+
pid: 4244,
|
|
39
|
+
creationTimeStamp: '2026-11-07T09:10:00.000Z'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const STATES = {
|
|
44
|
+
'sess-admin': { state: 'completed', pid: 4242, lineCount: 12 },
|
|
45
|
+
'sess-restricted': { state: 'running', pid: 4243, lineCount: 3 }
|
|
46
|
+
};
|
|
47
|
+
const LOGS = {
|
|
48
|
+
'sess-admin': 'NOTE: session started\nNOTE: session completed\n',
|
|
49
|
+
'sess-restricted': 'NOTE: session started\n',
|
|
50
|
+
'sess-silent': ''
|
|
51
|
+
};
|
|
52
|
+
const OUTPUTS = {
|
|
53
|
+
'sess-admin': '<html><body>42</body></html>',
|
|
54
|
+
'sess-restricted': '',
|
|
55
|
+
'sess-silent': ''
|
|
56
|
+
};
|
|
57
|
+
class MemorySessionHost {
|
|
58
|
+
constructor(sessions = exports.DEFAULT_SESSIONS) {
|
|
59
|
+
this.sessions = sessions;
|
|
60
|
+
}
|
|
61
|
+
async session(id) {
|
|
62
|
+
return this.sessions[id];
|
|
63
|
+
}
|
|
64
|
+
async state(id) {
|
|
65
|
+
return STATES[id];
|
|
66
|
+
}
|
|
67
|
+
async log(id) {
|
|
68
|
+
return LOGS[id];
|
|
69
|
+
}
|
|
70
|
+
async output(id) {
|
|
71
|
+
return OUTPUTS[id];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.MemorySessionHost = MemorySessionHost;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSessionService = void 0;
|
|
4
|
+
const types_js_1 = require("../types.js");
|
|
5
|
+
/**
|
|
6
|
+
* The four session routes, over the host's own session store.
|
|
7
|
+
*
|
|
8
|
+
* The caller passes a `Subject` and nothing else: which account that is, whether
|
|
9
|
+
* it is an administrator, and whether the session is theirs are all resolved
|
|
10
|
+
* here, against the same store the records service reads. A host never states a
|
|
11
|
+
* role.
|
|
12
|
+
*
|
|
13
|
+
* `404` covers both "no such session" and "not yours", on purpose: a session id
|
|
14
|
+
* is handed to the caller that triggered the program and is built from a
|
|
15
|
+
* timestamp plus a small random number, so it is guessable within a window, and
|
|
16
|
+
* confirming the id exists would say it was right.
|
|
17
|
+
*/
|
|
18
|
+
const createSessionService = (store, host) => {
|
|
19
|
+
/** The server's wording, so adopting it here does not become a later change. */
|
|
20
|
+
const notFound = (id) => `Session with ID '${id}' was not found.`;
|
|
21
|
+
/** The caller, and the session if they may see it - or the 404 that hides it. */
|
|
22
|
+
const visible = async (caller, id) => {
|
|
23
|
+
if (!caller)
|
|
24
|
+
throw new types_js_1.ApiError(401, 'Unauthorized');
|
|
25
|
+
const reader = await store.userById(caller.userId);
|
|
26
|
+
if (!reader || !reader.isActive)
|
|
27
|
+
throw new types_js_1.ApiError(401, 'Unauthorized');
|
|
28
|
+
const session = await host.session(id);
|
|
29
|
+
if (!session)
|
|
30
|
+
throw new types_js_1.ApiError(404, notFound(id));
|
|
31
|
+
// the uid is the key to compare; a name is the fallback a host may need
|
|
32
|
+
const owner = session.owner;
|
|
33
|
+
if (owner !== reader.uid && owner !== reader.username && !reader.isAdmin)
|
|
34
|
+
throw new types_js_1.ApiError(404, notFound(id));
|
|
35
|
+
return session;
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
async summary(caller, id) {
|
|
39
|
+
const session = await visible(caller, id);
|
|
40
|
+
return session.summary;
|
|
41
|
+
},
|
|
42
|
+
async state(caller, id) {
|
|
43
|
+
await visible(caller, id);
|
|
44
|
+
const state = await host.state(id);
|
|
45
|
+
if (!state)
|
|
46
|
+
throw new types_js_1.ApiError(404, notFound(id));
|
|
47
|
+
return state;
|
|
48
|
+
},
|
|
49
|
+
async log(caller, id) {
|
|
50
|
+
await visible(caller, id);
|
|
51
|
+
const log = await host.log(id);
|
|
52
|
+
if (log === undefined)
|
|
53
|
+
throw new types_js_1.ApiError(404, notFound(id));
|
|
54
|
+
return log;
|
|
55
|
+
},
|
|
56
|
+
async output(caller, id) {
|
|
57
|
+
await visible(caller, id);
|
|
58
|
+
const output = await host.output(id);
|
|
59
|
+
if (output === undefined)
|
|
60
|
+
throw new types_js_1.ApiError(404, notFound(id));
|
|
61
|
+
return output;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
exports.createSessionService = createSessionService;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"code.d.ts","sourceRoot":"","sources":["../../../src/corpus/code.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"code.d.ts","sourceRoot":"","sources":["../../../src/corpus/code.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW,EAAE,UAAU,EAoFnC,CAAA"}
|
package/build/esm/corpus/code.js
CHANGED
|
@@ -35,13 +35,50 @@ export const CODE_CORPUS = [
|
|
|
35
35
|
]
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
{
|
|
39
|
+
id: 'code-lint-unauthenticated',
|
|
40
|
+
route: 'POST /SASjsApi/code/lint',
|
|
41
|
+
description: 'refuses a caller with no token',
|
|
42
|
+
request: { body: { code: 'data _null_; run;' } },
|
|
43
|
+
expect: { status: 401, error: 'Unauthorized' }
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'code-lint-non-string',
|
|
47
|
+
route: 'POST /SASjsApi/code/lint',
|
|
48
|
+
description: 'refuses a code value that is not a string, rather than coercing it',
|
|
49
|
+
request: {
|
|
50
|
+
subject: { userId: 'user-admin', clientId: 'web_app' },
|
|
51
|
+
body: { code: 123 }
|
|
52
|
+
},
|
|
53
|
+
expect: { status: 400, error: '"code" must be a string' }
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'code-lint-empty',
|
|
57
|
+
route: 'POST /SASjsApi/code/lint',
|
|
58
|
+
description: 'refuses an empty buffer, which is not the same as a missing one',
|
|
59
|
+
request: {
|
|
60
|
+
subject: { userId: 'user-admin', clientId: 'web_app' },
|
|
61
|
+
body: { code: '' }
|
|
62
|
+
},
|
|
63
|
+
expect: { status: 400, error: '"code" is not allowed to be empty' }
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'code-format-unauthenticated',
|
|
67
|
+
route: 'POST /SASjsApi/code/format',
|
|
68
|
+
description: 'refuses a caller with no token',
|
|
69
|
+
request: { body: { code: 'data _null_; run;' } },
|
|
70
|
+
expect: { status: 401, error: 'Unauthorized' }
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'code-format-non-string',
|
|
74
|
+
route: 'POST /SASjsApi/code/format',
|
|
75
|
+
description: 'refuses a code value that is not a string',
|
|
76
|
+
request: {
|
|
77
|
+
subject: { userId: 'user-admin', clientId: 'web_app' },
|
|
78
|
+
body: { code: 123 }
|
|
79
|
+
},
|
|
80
|
+
expect: { status: 400, error: '"code" must be a string' }
|
|
81
|
+
},
|
|
45
82
|
{
|
|
46
83
|
id: 'code-format-adds-the-header',
|
|
47
84
|
route: 'POST /SASjsApi/code/format',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/corpus/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/corpus/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAWnD,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;IAC9B,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,UAAU,UAAU,KAAG,QAmBlD,CAAA;AAkBD;;;;GAIG;AACH,eAAO,MAAM,OAAO,GAClB,UAAU,UAAU,EACpB,WAAU,QAAgC,KACzC,OAAO,CAAC,UAAU,CAepB,CAAA"}
|
|
@@ -3,6 +3,8 @@ import { createDriveService } from '../drive/service.js';
|
|
|
3
3
|
import { MemoryDriveStore } from '../drive/memory-store.js';
|
|
4
4
|
import { createIdentityService } from '../identity/service.js';
|
|
5
5
|
import { createRecordService } from '../identity/records.js';
|
|
6
|
+
import { createSessionService } from '../session/service.js';
|
|
7
|
+
import { MemorySessionHost } from '../session/memory-host.js';
|
|
6
8
|
import { MemoryIdentityStore } from '../identity/memory-store.js';
|
|
7
9
|
import { DEFAULT_IDENTITY } from '../identity/fixture.js';
|
|
8
10
|
import { isApiError } from '../types.js';
|
|
@@ -23,7 +25,8 @@ export const servicesFor = (testCase) => {
|
|
|
23
25
|
? new RefusingDriveStore(testCase.seed || {})
|
|
24
26
|
: new MemoryDriveStore(testCase.seed || {})),
|
|
25
27
|
identity: createIdentityService(identity),
|
|
26
|
-
records: createRecordService(identity)
|
|
28
|
+
records: createRecordService(identity),
|
|
29
|
+
session: createSessionService(identity, new MemorySessionHost())
|
|
27
30
|
};
|
|
28
31
|
};
|
|
29
32
|
/**
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session routes' conformance corpus.
|
|
3
|
+
*
|
|
4
|
+
* Only the `200` answers are here, and that is a statement about the document
|
|
5
|
+
* rather than about the routes: sasjs/server documents these four with a `200`
|
|
6
|
+
* and nothing else, so `spec/corpus-parity.spec.ts` refuses a case that pins a
|
|
7
|
+
* refusal. Their `401` and the `404` that covers both "no such session" and
|
|
8
|
+
* "not yours" are pinned in `spec/session.spec.ts` instead, which the parity
|
|
9
|
+
* gate does not govern.
|
|
10
|
+
*
|
|
11
|
+
* The routes are in `documentRefusals.baseline.json` upstream; when they carry
|
|
12
|
+
* their `@Response(401)` and `@Response(404)`, those cases move here.
|
|
13
|
+
*/
|
|
14
|
+
import type { CorpusCase } from './drive.js';
|
|
15
|
+
export declare const SESSION_CORPUS: CorpusCase[];
|
|
16
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/corpus/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAK5C,eAAO,MAAM,cAAc,EAAE,UAAU,EAmEtC,CAAA"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const ADMIN = { userId: 'user-admin', clientId: 'web_app' };
|
|
2
|
+
const RESTRICTED = { userId: 'user-restricted', clientId: 'web_app' };
|
|
3
|
+
export const SESSION_CORPUS = [
|
|
4
|
+
{
|
|
5
|
+
id: 'session-summary-own',
|
|
6
|
+
route: 'GET /SASjsApi/session/{sessionId}',
|
|
7
|
+
description: 'answers the owner the summary of their own session',
|
|
8
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-admin' } },
|
|
9
|
+
expect: {
|
|
10
|
+
status: 200,
|
|
11
|
+
body: {
|
|
12
|
+
sessionId: 'sess-admin',
|
|
13
|
+
state: 'completed',
|
|
14
|
+
pid: 4242,
|
|
15
|
+
creationTimeStamp: '2026-11-07T09:00:00.000Z',
|
|
16
|
+
deathTimeStamp: '2026-11-07T09:00:04.000Z',
|
|
17
|
+
expiresAfterMins: 60
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'session-summary-as-admin',
|
|
23
|
+
route: 'GET /SASjsApi/session/{sessionId}',
|
|
24
|
+
description: "answers an administrator another account's session",
|
|
25
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-restricted' } },
|
|
26
|
+
expect: {
|
|
27
|
+
status: 200,
|
|
28
|
+
body: {
|
|
29
|
+
sessionId: 'sess-restricted',
|
|
30
|
+
state: 'running',
|
|
31
|
+
pid: 4243,
|
|
32
|
+
creationTimeStamp: '2026-11-07T09:05:00.000Z'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'session-log-own',
|
|
38
|
+
route: 'GET /SASjsApi/session/{sessionId}/log',
|
|
39
|
+
description: 'answers the log this session wrote',
|
|
40
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-admin' } },
|
|
41
|
+
expect: {
|
|
42
|
+
status: 200,
|
|
43
|
+
contents: 'NOTE: session started\nNOTE: session completed\n'
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'session-log-nothing-written',
|
|
48
|
+
route: 'GET /SASjsApi/session/{sessionId}/log',
|
|
49
|
+
description: 'answers an empty body for a session that has written nothing',
|
|
50
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-silent' } },
|
|
51
|
+
expect: { status: 200, contents: '' }
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'session-output-own',
|
|
55
|
+
route: 'GET /SASjsApi/session/{sessionId}/output',
|
|
56
|
+
description: 'answers the webout this session produced',
|
|
57
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-admin' } },
|
|
58
|
+
expect: { status: 200, contents: '<html><body>42</body></html>' }
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'session-state-own',
|
|
62
|
+
route: 'GET /SASjsApi/session/{sessionId}/state',
|
|
63
|
+
description: "answers the run time's state for this session",
|
|
64
|
+
request: { subject: RESTRICTED, params: { sessionId: 'sess-restricted' } },
|
|
65
|
+
expect: {
|
|
66
|
+
status: 200,
|
|
67
|
+
body: { state: 'running', pid: 4243, lineCount: 3 }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DriveService } from '../drive/service.js';
|
|
2
2
|
import type { IdentityService } from '../identity/service.js';
|
|
3
3
|
import type { RecordService } from '../identity/records.js';
|
|
4
|
+
import type { SessionService } from '../session/service.js';
|
|
4
5
|
import type { CodeService } from '../code/service.js';
|
|
5
6
|
import type { Subject } from '../identity/types.js';
|
|
6
7
|
export interface ApiRequest {
|
|
@@ -45,6 +46,11 @@ export interface Services {
|
|
|
45
46
|
identity?: IdentityService;
|
|
46
47
|
/** Users, groups, permission rules and clients. Absent, the record routes answer 501. */
|
|
47
48
|
records?: RecordService;
|
|
49
|
+
/**
|
|
50
|
+
* A triggered session's record, log, output and state. A host without one
|
|
51
|
+
* answers 501 for these four, naming the capability.
|
|
52
|
+
*/
|
|
53
|
+
session?: SessionService;
|
|
48
54
|
}
|
|
49
55
|
export declare const dispatch: (services: Services, routeId: string, request?: ApiRequest) => Promise<ApiResponse>;
|
|
50
56
|
/** Whether a route is one `dispatch` can answer today. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/host/dispatch.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAOnD,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,oFAAoF;IACpF,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAA;IACtD;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,mBAAmB;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAC/B;AAED,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,KAAK,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,yFAAyF;IACzF,OAAO,CAAC,EAAE,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/host/dispatch.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAOnD,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,oFAAoF;IACpF,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAA;IACtD;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,mBAAmB;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAC/B;AAED,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,KAAK,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,yFAAyF;IACzF,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,cAAc,CAAA;CACzB;AA2CD,eAAO,MAAM,QAAQ,GACnB,UAAU,QAAQ,EAClB,SAAS,MAAM,EACf,UAAS,UAAe,KACvB,OAAO,CAAC,WAAW,CA8brB,CAAA;AAED,0DAA0D;AAC1D,eAAO,MAAM,cAAc,EAAE,MAAM,EAgDlC,CAAA"}
|