@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
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',
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEPLOY_CORPUS = void 0;
|
|
4
|
+
const SUBJECT = { userId: 'user-admin', clientId: 'web_app' };
|
|
5
|
+
exports.DEPLOY_CORPUS = [
|
|
6
|
+
{
|
|
7
|
+
id: 'deploy-unpublish',
|
|
8
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
9
|
+
description: 'removes a published app stream and answers the status',
|
|
10
|
+
request: {
|
|
11
|
+
subject: SUBJECT,
|
|
12
|
+
body: { streamServiceName: 'myApp', appLoc: '/Public/myApp' }
|
|
13
|
+
},
|
|
14
|
+
expect: { status: 200, body: { status: 'success' } }
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'deploy-unpublish-other-name',
|
|
18
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
19
|
+
description: 'removes the app by its folder when the name has been renamed',
|
|
20
|
+
request: {
|
|
21
|
+
subject: SUBJECT,
|
|
22
|
+
body: { streamServiceName: 'renamed-app', appLoc: '/Public/myApp' }
|
|
23
|
+
},
|
|
24
|
+
expect: { status: 200, body: { status: 'success' } }
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'deploy-unpublish-no-stream-name',
|
|
28
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
29
|
+
description: 'refuses a body that does not say which stream to remove',
|
|
30
|
+
request: { subject: SUBJECT, body: { appLoc: '/Public/myApp' } },
|
|
31
|
+
expect: { status: 400, error: '"streamServiceName" is required' }
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'deploy-unpublish-no-app-loc',
|
|
35
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
36
|
+
description: 'refuses a body that does not say which folder to remove',
|
|
37
|
+
request: { subject: SUBJECT, body: { streamServiceName: 'myApp' } },
|
|
38
|
+
expect: { status: 400, error: '"appLoc" is required' }
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'deploy-unpublish-unknown-folder',
|
|
42
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
43
|
+
description: 'answers 404 for a folder nothing is published at',
|
|
44
|
+
request: {
|
|
45
|
+
subject: SUBJECT,
|
|
46
|
+
body: { streamServiceName: 'myApp', appLoc: '/Public/not-an-app' }
|
|
47
|
+
},
|
|
48
|
+
expect: { status: 404, error: 'No such app stream entry.' }
|
|
49
|
+
}
|
|
50
|
+
];
|
|
@@ -4,8 +4,11 @@ exports.runCase = exports.servicesFor = void 0;
|
|
|
4
4
|
const dispatch_js_1 = require("../host/dispatch.js");
|
|
5
5
|
const service_js_1 = require("../drive/service.js");
|
|
6
6
|
const memory_store_js_1 = require("../drive/memory-store.js");
|
|
7
|
+
const registry_js_1 = require("../drive/registry.js");
|
|
7
8
|
const service_js_2 = require("../identity/service.js");
|
|
8
9
|
const records_js_1 = require("../identity/records.js");
|
|
10
|
+
const service_js_3 = require("../session/service.js");
|
|
11
|
+
const memory_host_js_1 = require("../session/memory-host.js");
|
|
9
12
|
const memory_store_js_2 = require("../identity/memory-store.js");
|
|
10
13
|
const fixture_js_1 = require("../identity/fixture.js");
|
|
11
14
|
const types_js_1 = require("../types.js");
|
|
@@ -26,7 +29,9 @@ const servicesFor = (testCase) => {
|
|
|
26
29
|
? new RefusingDriveStore(testCase.seed || {})
|
|
27
30
|
: new memory_store_js_1.MemoryDriveStore(testCase.seed || {})),
|
|
28
31
|
identity: (0, service_js_2.createIdentityService)(identity),
|
|
29
|
-
records: (0, records_js_1.createRecordService)(identity)
|
|
32
|
+
records: (0, records_js_1.createRecordService)(identity),
|
|
33
|
+
registry: new registry_js_1.MemoryAppStreamRegistry(),
|
|
34
|
+
session: (0, service_js_3.createSessionService)(identity, new memory_host_js_1.MemorySessionHost())
|
|
30
35
|
};
|
|
31
36
|
};
|
|
32
37
|
exports.servicesFor = servicesFor;
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
const notFound = (id) => `Session with ID '${id}' was not found.`;
|
|
7
|
+
const UNAUTHORIZED = 'Unauthorized';
|
|
8
|
+
exports.SESSION_CORPUS = [
|
|
9
|
+
// ---------------------------------------------------------------- summary
|
|
10
|
+
{
|
|
11
|
+
id: 'session-summary-own',
|
|
12
|
+
route: 'GET /SASjsApi/session/{sessionId}',
|
|
13
|
+
description: 'answers the owner the summary of their own session',
|
|
14
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-admin' } },
|
|
15
|
+
expect: {
|
|
16
|
+
status: 200,
|
|
17
|
+
body: {
|
|
18
|
+
sessionId: 'sess-admin',
|
|
19
|
+
state: 'completed',
|
|
20
|
+
pid: 4242,
|
|
21
|
+
creationTimeStamp: '2026-11-07T09:00:00.000Z',
|
|
22
|
+
deathTimeStamp: '2026-11-07T09:00:04.000Z',
|
|
23
|
+
expiresAfterMins: 60
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'session-summary-as-admin',
|
|
29
|
+
route: 'GET /SASjsApi/session/{sessionId}',
|
|
30
|
+
description: "answers an administrator another account's session",
|
|
31
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-restricted' } },
|
|
32
|
+
expect: {
|
|
33
|
+
status: 200,
|
|
34
|
+
body: {
|
|
35
|
+
sessionId: 'sess-restricted',
|
|
36
|
+
state: 'running',
|
|
37
|
+
pid: 4243,
|
|
38
|
+
creationTimeStamp: '2026-11-07T09:05:00.000Z'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'session-summary-unauthenticated',
|
|
44
|
+
route: 'GET /SASjsApi/session/{sessionId}',
|
|
45
|
+
description: 'refuses a caller with no token',
|
|
46
|
+
request: { params: { sessionId: 'sess-admin' } },
|
|
47
|
+
expect: { status: 401, error: UNAUTHORIZED }
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'session-summary-foreign',
|
|
51
|
+
route: 'GET /SASjsApi/session/{sessionId}',
|
|
52
|
+
description: "answers 404 for another account's session, not 403",
|
|
53
|
+
request: { subject: RESTRICTED, params: { sessionId: 'sess-admin' } },
|
|
54
|
+
expect: { status: 404, error: notFound('sess-admin') }
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'session-summary-unknown',
|
|
58
|
+
route: 'GET /SASjsApi/session/{sessionId}',
|
|
59
|
+
description: 'answers the same 404 for a session that does not exist',
|
|
60
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-nope' } },
|
|
61
|
+
expect: { status: 404, error: notFound('sess-nope') }
|
|
62
|
+
},
|
|
63
|
+
// -------------------------------------------------------------------- log
|
|
64
|
+
{
|
|
65
|
+
id: 'session-log-own',
|
|
66
|
+
route: 'GET /SASjsApi/session/{sessionId}/log',
|
|
67
|
+
description: 'answers the log this session wrote',
|
|
68
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-admin' } },
|
|
69
|
+
expect: {
|
|
70
|
+
status: 200,
|
|
71
|
+
contents: 'NOTE: session started\nNOTE: session completed\n'
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'session-log-nothing-written',
|
|
76
|
+
route: 'GET /SASjsApi/session/{sessionId}/log',
|
|
77
|
+
description: 'answers an empty body for a session that has written nothing',
|
|
78
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-silent' } },
|
|
79
|
+
expect: { status: 200, contents: '' }
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'session-log-unauthenticated',
|
|
83
|
+
route: 'GET /SASjsApi/session/{sessionId}/log',
|
|
84
|
+
description: 'refuses a caller with no token',
|
|
85
|
+
request: { params: { sessionId: 'sess-admin' } },
|
|
86
|
+
expect: { status: 401, error: UNAUTHORIZED }
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'session-log-foreign',
|
|
90
|
+
route: 'GET /SASjsApi/session/{sessionId}/log',
|
|
91
|
+
description: "will not read another account's log",
|
|
92
|
+
request: { subject: RESTRICTED, params: { sessionId: 'sess-admin' } },
|
|
93
|
+
expect: { status: 404, error: notFound('sess-admin') }
|
|
94
|
+
},
|
|
95
|
+
// ----------------------------------------------------------------- output
|
|
96
|
+
{
|
|
97
|
+
id: 'session-output-own',
|
|
98
|
+
route: 'GET /SASjsApi/session/{sessionId}/output',
|
|
99
|
+
description: 'answers the webout this session produced',
|
|
100
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-admin' } },
|
|
101
|
+
expect: { status: 200, contents: '<html><body>42</body></html>' }
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'session-output-unauthenticated',
|
|
105
|
+
route: 'GET /SASjsApi/session/{sessionId}/output',
|
|
106
|
+
description: 'refuses a caller with no token',
|
|
107
|
+
request: { params: { sessionId: 'sess-admin' } },
|
|
108
|
+
expect: { status: 401, error: UNAUTHORIZED }
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'session-output-foreign',
|
|
112
|
+
route: 'GET /SASjsApi/session/{sessionId}/output',
|
|
113
|
+
description: "will not read another account's webout",
|
|
114
|
+
request: { subject: RESTRICTED, params: { sessionId: 'sess-admin' } },
|
|
115
|
+
expect: { status: 404, error: notFound('sess-admin') }
|
|
116
|
+
},
|
|
117
|
+
// ------------------------------------------------------------------ state
|
|
118
|
+
{
|
|
119
|
+
id: 'session-state-own',
|
|
120
|
+
route: 'GET /SASjsApi/session/{sessionId}/state',
|
|
121
|
+
description: "answers the run time's state for this session",
|
|
122
|
+
request: { subject: RESTRICTED, params: { sessionId: 'sess-restricted' } },
|
|
123
|
+
expect: {
|
|
124
|
+
status: 200,
|
|
125
|
+
body: { state: 'running', pid: 4243, lineCount: 3 }
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'session-state-unauthenticated',
|
|
130
|
+
route: 'GET /SASjsApi/session/{sessionId}/state',
|
|
131
|
+
description: 'refuses a caller with no token',
|
|
132
|
+
request: { params: { sessionId: 'sess-restricted' } },
|
|
133
|
+
expect: { status: 401, error: UNAUTHORIZED }
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'session-state-foreign',
|
|
137
|
+
route: 'GET /SASjsApi/session/{sessionId}/state',
|
|
138
|
+
description: "will not report another account's session state",
|
|
139
|
+
request: { subject: RESTRICTED, params: { sessionId: 'sess-admin' } },
|
|
140
|
+
expect: { status: 404, error: notFound('sess-admin') }
|
|
141
|
+
}
|
|
142
|
+
];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MemoryAppStreamRegistry = exports.unpublishAppStream = void 0;
|
|
4
|
+
const types_js_1 = require("../types.js");
|
|
5
|
+
/**
|
|
6
|
+
* `DELETE /SASjsApi/drive/deploy`.
|
|
7
|
+
*
|
|
8
|
+
* The document requires both body values, so both are checked - a caller that
|
|
9
|
+
* names a stream but not the folder has not said what to remove. Only the folder
|
|
10
|
+
* reaches the registry, because that is what identifies the app.
|
|
11
|
+
*
|
|
12
|
+
* The checks are here rather than in the transport because this is the one
|
|
13
|
+
* implementation of the operation: a host calling it directly gets the same
|
|
14
|
+
* refusals in the same order, and with the same messages the document states.
|
|
15
|
+
*/
|
|
16
|
+
const unpublishAppStream = async (registry, body) => {
|
|
17
|
+
if (!body?.streamServiceName)
|
|
18
|
+
throw new types_js_1.ApiError(400, '"streamServiceName" is required');
|
|
19
|
+
if (!body.appLoc)
|
|
20
|
+
throw new types_js_1.ApiError(400, '"appLoc" is required');
|
|
21
|
+
if (!(await registry.unpublish(body.appLoc)))
|
|
22
|
+
throw new types_js_1.ApiError(404, 'No such app stream entry.');
|
|
23
|
+
return { status: 'success' };
|
|
24
|
+
};
|
|
25
|
+
exports.unpublishAppStream = unpublishAppStream;
|
|
26
|
+
/**
|
|
27
|
+
* The registry the corpus and the package's own tests read: one published app,
|
|
28
|
+
* identified by its folder alone.
|
|
29
|
+
*/
|
|
30
|
+
class MemoryAppStreamRegistry {
|
|
31
|
+
constructor(folders = ['/Public/myApp']) {
|
|
32
|
+
this.folders = new Set(folders);
|
|
33
|
+
}
|
|
34
|
+
async unpublish(appLoc) {
|
|
35
|
+
return this.folders.delete(appLoc);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.MemoryAppStreamRegistry = MemoryAppStreamRegistry;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HANDLED_ROUTES = exports.dispatch = void 0;
|
|
4
4
|
const index_js_1 = require("../routes/index.js");
|
|
5
|
+
const registry_js_1 = require("../drive/registry.js");
|
|
5
6
|
const types_js_1 = require("../types.js");
|
|
6
7
|
const deploy_js_1 = require("../drive/deploy.js");
|
|
7
8
|
/** The defaults sasjs/server's Client schema applies to an omitted lifetime. */
|
|
@@ -74,6 +75,12 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
74
75
|
const body = request.body || {};
|
|
75
76
|
const params = request.params || {};
|
|
76
77
|
/** The record service, or the 501 a host without one answers. */
|
|
78
|
+
/** The host's sessions, or the 501 that names the missing capability. */
|
|
79
|
+
const sessionRequired = () => {
|
|
80
|
+
if (!services.session)
|
|
81
|
+
throw new types_js_1.ApiError(501, `This host has no session service, so it cannot answer ${routeId}.`);
|
|
82
|
+
return services.session;
|
|
83
|
+
};
|
|
77
84
|
const required = () => {
|
|
78
85
|
if (!records)
|
|
79
86
|
throw new types_js_1.ApiError(501, `This host has no record service, so it cannot answer ${routeId}.`);
|
|
@@ -169,8 +176,40 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
169
176
|
await drive.deploy(String(body.appLoc ?? ''), body.fileTree);
|
|
170
177
|
return { status: 200, body: deploy_js_1.DEPLOY_SUCCESS };
|
|
171
178
|
}
|
|
179
|
+
case 'DELETE /SASjsApi/drive/deploy':
|
|
180
|
+
if (!services.registry)
|
|
181
|
+
throw new types_js_1.ApiError(501, `This host has no app stream registry, so it cannot answer ${routeId}.`);
|
|
182
|
+
return {
|
|
183
|
+
status: 200,
|
|
184
|
+
body: await (0, registry_js_1.unpublishAppStream)(services.registry, body)
|
|
185
|
+
};
|
|
172
186
|
case 'GET /SASjsApi/drive/filetree':
|
|
173
187
|
return { status: 200, body: await drive.tree(query._folderPath) };
|
|
188
|
+
// ------------------------------------------------------------- sessions
|
|
189
|
+
case 'GET /SASjsApi/session/{sessionId}': {
|
|
190
|
+
return {
|
|
191
|
+
status: 200,
|
|
192
|
+
body: await sessionRequired().summary(await caller(), String(params.sessionId ?? ''))
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
case 'GET /SASjsApi/session/{sessionId}/log': {
|
|
196
|
+
return {
|
|
197
|
+
status: 200,
|
|
198
|
+
contents: await sessionRequired().log(await caller(), String(params.sessionId ?? ''))
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
case 'GET /SASjsApi/session/{sessionId}/output': {
|
|
202
|
+
return {
|
|
203
|
+
status: 200,
|
|
204
|
+
contents: await sessionRequired().output(await caller(), String(params.sessionId ?? ''))
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
case 'GET /SASjsApi/session/{sessionId}/state': {
|
|
208
|
+
return {
|
|
209
|
+
status: 200,
|
|
210
|
+
body: await sessionRequired().state(await caller(), String(params.sessionId ?? ''))
|
|
211
|
+
};
|
|
212
|
+
}
|
|
174
213
|
// ------------------------------------------------------------- records
|
|
175
214
|
//
|
|
176
215
|
// The administration routes: users, groups, permission rules, clients and the
|
|
@@ -357,6 +396,11 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
357
396
|
exports.dispatch = dispatch;
|
|
358
397
|
/** Whether a route is one `dispatch` can answer today. */
|
|
359
398
|
exports.HANDLED_ROUTES = [
|
|
399
|
+
'DELETE /SASjsApi/drive/deploy',
|
|
400
|
+
'GET /SASjsApi/session/{sessionId}',
|
|
401
|
+
'GET /SASjsApi/session/{sessionId}/log',
|
|
402
|
+
'GET /SASjsApi/session/{sessionId}/output',
|
|
403
|
+
'GET /SASjsApi/session/{sessionId}/state',
|
|
360
404
|
'POST /SASjsApi/code/lint',
|
|
361
405
|
'POST /SASjsApi/code/format',
|
|
362
406
|
'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.DEPLOY_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,10 @@ 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 deploy_js_2 = require("./corpus/deploy.js");
|
|
83
|
+
Object.defineProperty(exports, "DEPLOY_CORPUS", { enumerable: true, get: function () { return deploy_js_2.DEPLOY_CORPUS; } });
|
|
84
|
+
var session_js_1 = require("./corpus/session.js");
|
|
85
|
+
Object.defineProperty(exports, "SESSION_CORPUS", { enumerable: true, get: function () { return session_js_1.SESSION_CORPUS; } });
|
|
82
86
|
var records_js_2 = require("./corpus/records.js");
|
|
83
87
|
Object.defineProperty(exports, "RECORD_CORPUS", { enumerable: true, get: function () { return records_js_2.RECORD_CORPUS; } });
|
|
84
88
|
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
|
},
|
|
@@ -1285,6 +1301,14 @@ exports.GENERATED_ROUTES = [
|
|
|
1285
1301
|
{
|
|
1286
1302
|
status: 200,
|
|
1287
1303
|
description: 'Ok'
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
status: 401,
|
|
1307
|
+
description: 'No valid access token or session was supplied'
|
|
1308
|
+
},
|
|
1309
|
+
{
|
|
1310
|
+
status: 404,
|
|
1311
|
+
description: 'Session with the given ID was not found.'
|
|
1288
1312
|
}
|
|
1289
1313
|
]
|
|
1290
1314
|
},
|
|
@@ -1307,6 +1331,14 @@ exports.GENERATED_ROUTES = [
|
|
|
1307
1331
|
{
|
|
1308
1332
|
status: 200,
|
|
1309
1333
|
description: 'Ok'
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
status: 401,
|
|
1337
|
+
description: 'No valid access token or session was supplied'
|
|
1338
|
+
},
|
|
1339
|
+
{
|
|
1340
|
+
status: 404,
|
|
1341
|
+
description: 'Session with the given ID was not found.'
|
|
1310
1342
|
}
|
|
1311
1343
|
]
|
|
1312
1344
|
},
|
|
@@ -1329,6 +1361,14 @@ exports.GENERATED_ROUTES = [
|
|
|
1329
1361
|
{
|
|
1330
1362
|
status: 200,
|
|
1331
1363
|
description: 'Ok'
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
status: 401,
|
|
1367
|
+
description: 'No valid access token or session was supplied'
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
status: 404,
|
|
1371
|
+
description: 'Session with the given ID was not found.'
|
|
1332
1372
|
}
|
|
1333
1373
|
]
|
|
1334
1374
|
},
|
|
@@ -1351,6 +1391,14 @@ exports.GENERATED_ROUTES = [
|
|
|
1351
1391
|
{
|
|
1352
1392
|
status: 200,
|
|
1353
1393
|
description: 'Ok'
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
status: 401,
|
|
1397
|
+
description: 'No valid access token or session was supplied'
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
status: 404,
|
|
1401
|
+
description: 'Session with the given ID was not found.'
|
|
1354
1402
|
}
|
|
1355
1403
|
]
|
|
1356
1404
|
},
|
|
@@ -27,12 +27,10 @@ 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.';
|
|
34
33
|
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.";
|
|
35
|
-
const UNPUBLISH = 'Unpublishing inverts the deploy route, so the two land together.';
|
|
36
34
|
const success = { status: 'success' };
|
|
37
35
|
const user = {
|
|
38
36
|
uid: '',
|
|
@@ -166,10 +164,7 @@ exports.OVERLAY = {
|
|
|
166
164
|
},
|
|
167
165
|
// `404` is a pair - the registry name and the appLoc it was published from -
|
|
168
166
|
// that matches no entry, and the drive's content is deliberately left alone.
|
|
169
|
-
'DELETE /SASjsApi/drive/deploy': {
|
|
170
|
-
implemented: false,
|
|
171
|
-
reason: UNPUBLISH
|
|
172
|
-
},
|
|
167
|
+
'DELETE /SASjsApi/drive/deploy': { implemented: true },
|
|
173
168
|
// Group - members and the groups themselves
|
|
174
169
|
'GET /SASjsApi/group': { implemented: true },
|
|
175
170
|
'GET /SASjsApi/group/{uid}': { implemented: true },
|
|
@@ -205,22 +200,13 @@ exports.OVERLAY = {
|
|
|
205
200
|
implemented: true,
|
|
206
201
|
responses: { 200: user }
|
|
207
202
|
},
|
|
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
|
-
},
|
|
203
|
+
// The port takes the owner as a uid and falls back to a username, which is
|
|
204
|
+
// what sasjs/server records today - a host hands its sessions through as they
|
|
205
|
+
// are, and the comparison tries the identifier first.
|
|
206
|
+
'GET /SASjsApi/session/{sessionId}': { implemented: true },
|
|
207
|
+
'GET /SASjsApi/session/{sessionId}/log': { implemented: true },
|
|
208
|
+
'GET /SASjsApi/session/{sessionId}/output': { implemented: true },
|
|
209
|
+
'GET /SASjsApi/session/{sessionId}/state': { implemented: true },
|
|
224
210
|
// STP - a stored program, on the runtime port
|
|
225
211
|
'GET /SASjsApi/stp/execute': { implemented: false, reason: RUNTIME },
|
|
226
212
|
'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;
|