@sasjs/api 0.9.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 +457 -102
- package/build/cjs/code/index.js +12 -0
- package/build/cjs/code/service.js +57 -0
- package/build/cjs/corpus/code.js +99 -0
- package/build/cjs/corpus/runner.js +4 -1
- package/build/cjs/corpus/session.js +73 -0
- package/build/cjs/host/dispatch.js +65 -3
- package/build/cjs/index.js +5 -1
- package/build/cjs/routes/generated.js +16 -0
- package/build/cjs/routes/overlay.js +15 -20
- package/build/cjs/session/memory-host.js +74 -0
- package/build/cjs/session/service.js +65 -0
- package/build/esm/code/index.d.ts +10 -0
- package/build/esm/code/index.d.ts.map +1 -0
- package/build/esm/code/index.js +8 -0
- package/build/esm/code/service.d.ts +40 -0
- package/build/esm/code/service.d.ts.map +1 -0
- package/build/esm/code/service.js +53 -0
- package/build/esm/corpus/code.d.ts +17 -0
- package/build/esm/corpus/code.d.ts.map +1 -0
- package/build/esm/corpus/code.js +96 -0
- package/build/esm/corpus/drive.d.ts +9 -0
- package/build/esm/corpus/drive.d.ts.map +1 -1
- 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 +9 -0
- package/build/esm/host/dispatch.d.ts.map +1 -1
- package/build/esm/host/dispatch.js +65 -3
- 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 +16 -0
- package/build/esm/routes/overlay.d.ts.map +1 -1
- package/build/esm/routes/overlay.js +15 -20
- 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 +9 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createCodeService = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The code service, on its own entry point.
|
|
6
|
+
*
|
|
7
|
+
* `@sasjs/lint` reads the filesystem (its config resolution walks up for a
|
|
8
|
+
* `.sasjslint`), so it cannot be bundled for a service worker: the package root
|
|
9
|
+
* stays free of it and a host that wants these routes imports `@sasjs/api/code`.
|
|
10
|
+
*/
|
|
11
|
+
var service_js_1 = require("./service.js");
|
|
12
|
+
Object.defineProperty(exports, "createCodeService", { enumerable: true, get: function () { return service_js_1.createCodeService; } });
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createCodeService = void 0;
|
|
4
|
+
const lint_1 = require("@sasjs/lint");
|
|
5
|
+
const types_js_1 = require("../types.js");
|
|
6
|
+
/**
|
|
7
|
+
* `code`, refused the way the server's schema refuses it.
|
|
8
|
+
*
|
|
9
|
+
* Coercing with `String(...)` defeated the refusal: `{"code": 123}` was linted as
|
|
10
|
+
* the text "123" and `{"code": ""}` answered "required" where the server says it
|
|
11
|
+
* is empty. The three messages are Joi's, because they are the ones a client
|
|
12
|
+
* written against the document already handles.
|
|
13
|
+
*/
|
|
14
|
+
const codeOf = (body) => {
|
|
15
|
+
const value = body.code;
|
|
16
|
+
if (value === undefined)
|
|
17
|
+
throw new types_js_1.ApiError(400, '"code" is required');
|
|
18
|
+
if (typeof value !== 'string')
|
|
19
|
+
throw new types_js_1.ApiError(400, '"code" must be a string');
|
|
20
|
+
if (value === '')
|
|
21
|
+
throw new types_js_1.ApiError(400, '"code" is not allowed to be empty');
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The two editor routes, over `@sasjs/lint`.
|
|
26
|
+
*
|
|
27
|
+
* The linter is a dependency because it is the same everywhere, and it is
|
|
28
|
+
* deliberately NOT reachable from the package root: `@sasjs/lint` resolves its
|
|
29
|
+
* configuration by walking the filesystem, so it cannot be bundled into the
|
|
30
|
+
* service worker the browser entry produces. A host that wants these routes
|
|
31
|
+
* imports `@sasjs/api/code`, which is Node-only, exactly like the linter.
|
|
32
|
+
*
|
|
33
|
+
* The *rules* are the host's: which `.sasjslint` applies to a path is a fact
|
|
34
|
+
* about a drive, so `lintConfig` is asked for it.
|
|
35
|
+
* Neither route touches the drive or runs anything; they answer about the text
|
|
36
|
+
* in the request, which is what lets an editor check unsaved changes.
|
|
37
|
+
*/
|
|
38
|
+
const createCodeService = (options = {}) => {
|
|
39
|
+
const requireCaller = (caller) => {
|
|
40
|
+
// both routes are permissioned at the gate, so a caller that reached the
|
|
41
|
+
// service without one is a host that skipped it rather than a public route
|
|
42
|
+
if (!caller)
|
|
43
|
+
throw new types_js_1.ApiError(401, 'Unauthorized');
|
|
44
|
+
};
|
|
45
|
+
const configFor = (filePath) => options.lintConfig ? options.lintConfig(filePath) : undefined;
|
|
46
|
+
return {
|
|
47
|
+
async lint(caller, body) {
|
|
48
|
+
requireCaller(caller);
|
|
49
|
+
return (0, lint_1.lintText)(codeOf(body), configFor(body.filePath));
|
|
50
|
+
},
|
|
51
|
+
async format(caller, body) {
|
|
52
|
+
requireCaller(caller);
|
|
53
|
+
return (0, lint_1.formatText)(codeOf(body), configFor(body.filePath));
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
exports.createCodeService = createCodeService;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CODE_CORPUS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The editor's corpus: the two routes that answer about a buffer rather than a
|
|
6
|
+
* file.
|
|
7
|
+
*
|
|
8
|
+
* Both expectations are the linter's own output for the same input, measured
|
|
9
|
+
* against `@sasjs/lint` with no configuration - the defaults an untitled buffer
|
|
10
|
+
* lints against. A host that supplies a `.sasjslint` changes the rules for its
|
|
11
|
+
* own drive, which is why the case names no `filePath`.
|
|
12
|
+
*
|
|
13
|
+
* lintText('data _null_;\n x = 1;\nrun;\n')
|
|
14
|
+
* -> [{ message: 'File missing Doxygen header', lineNumber: 1, ... }]
|
|
15
|
+
* formatText(...)
|
|
16
|
+
* -> the header block, then the program with its spacing normalised
|
|
17
|
+
*/
|
|
18
|
+
exports.CODE_CORPUS = [
|
|
19
|
+
{
|
|
20
|
+
id: 'code-lint-missing-header',
|
|
21
|
+
route: 'POST /SASjsApi/code/lint',
|
|
22
|
+
description: 'answers the diagnostics for a buffer, without touching a file',
|
|
23
|
+
request: {
|
|
24
|
+
// the routes are permissioned; the runner resolves the fixture's admin
|
|
25
|
+
subject: { userId: 'user-admin', clientId: 'web_app' },
|
|
26
|
+
body: { code: 'data _null_;\n x = 1;\nrun;\n' }
|
|
27
|
+
},
|
|
28
|
+
expect: {
|
|
29
|
+
status: 200,
|
|
30
|
+
body: [
|
|
31
|
+
{
|
|
32
|
+
message: 'File missing Doxygen header',
|
|
33
|
+
lineNumber: 1,
|
|
34
|
+
startColumnNumber: 1,
|
|
35
|
+
endColumnNumber: 1,
|
|
36
|
+
severity: 1
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
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
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'code-format-adds-the-header',
|
|
87
|
+
route: 'POST /SASjsApi/code/format',
|
|
88
|
+
description: 'answers the formatted buffer, header block included',
|
|
89
|
+
request: {
|
|
90
|
+
// the routes are permissioned; the runner resolves the fixture's admin
|
|
91
|
+
subject: { userId: 'user-admin', clientId: 'web_app' },
|
|
92
|
+
body: { code: 'data _null_;\n x = 1;\nrun;\n' }
|
|
93
|
+
},
|
|
94
|
+
expect: {
|
|
95
|
+
status: 200,
|
|
96
|
+
body: '/**\n @file\n @brief <Your brief here>\n\n <h4> SAS Macros </h4>\n\n**/\ndata _null_;\n x = 1;\nrun;\n'
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
];
|
|
@@ -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
|
+
];
|
|
@@ -24,8 +24,13 @@ const validate = (route, request) => {
|
|
|
24
24
|
const given = param.in === 'query'
|
|
25
25
|
? request.query?.[param.name]
|
|
26
26
|
: request.body?.[param.name];
|
|
27
|
-
|
|
27
|
+
// missing and empty are two refusals, the way the schema has them: a
|
|
28
|
+
// client that sent the field an empty value has a different bug from one
|
|
29
|
+
// that omitted it
|
|
30
|
+
if (given === undefined)
|
|
28
31
|
throw new types_js_1.ApiError(400, `"${param.name}" is required`);
|
|
32
|
+
if (given === '')
|
|
33
|
+
throw new types_js_1.ApiError(400, `"${param.name}" is not allowed to be empty`);
|
|
29
34
|
}
|
|
30
35
|
};
|
|
31
36
|
/** The drive path a write names: the query parameter wins over the form field. */
|
|
@@ -44,7 +49,7 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
44
49
|
const route = (0, index_js_1.routeById)(routeId);
|
|
45
50
|
if (!route)
|
|
46
51
|
throw new types_js_1.ApiError(404, `No such operation: ${routeId}`);
|
|
47
|
-
const { drive, identity, records } = services;
|
|
52
|
+
const { drive, identity, records, code } = services;
|
|
48
53
|
// The server authenticates before its payload schema runs, so an anonymous
|
|
49
54
|
// request with an incomplete body is a 401 there rather than a 400. The order
|
|
50
55
|
// is only observable on the route groups that gate on a caller: the sign-in
|
|
@@ -69,6 +74,12 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
69
74
|
const body = request.body || {};
|
|
70
75
|
const params = request.params || {};
|
|
71
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
|
+
};
|
|
72
83
|
const required = () => {
|
|
73
84
|
if (!records)
|
|
74
85
|
throw new types_js_1.ApiError(501, `This host has no record service, so it cannot answer ${routeId}.`);
|
|
@@ -92,7 +103,7 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
92
103
|
status: 200,
|
|
93
104
|
body: await identity.token({
|
|
94
105
|
clientId: String(body.clientId ?? ''),
|
|
95
|
-
code:
|
|
106
|
+
code: body.code
|
|
96
107
|
})
|
|
97
108
|
};
|
|
98
109
|
case 'POST /SASjsApi/auth/refresh':
|
|
@@ -166,6 +177,31 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
166
177
|
}
|
|
167
178
|
case 'GET /SASjsApi/drive/filetree':
|
|
168
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
|
+
}
|
|
169
205
|
// ------------------------------------------------------------- records
|
|
170
206
|
//
|
|
171
207
|
// The administration routes: users, groups, permission rules, clients and the
|
|
@@ -219,6 +255,26 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
219
255
|
status: 200,
|
|
220
256
|
body: await required().deleteUser(await caller(), { username: String(params.username ?? '') }, { password: body.password })
|
|
221
257
|
};
|
|
258
|
+
case 'POST /SASjsApi/code/lint':
|
|
259
|
+
if (!code)
|
|
260
|
+
throw new types_js_1.ApiError(501, `This host has no code service, so it cannot answer ${routeId}.`);
|
|
261
|
+
return {
|
|
262
|
+
status: 200,
|
|
263
|
+
body: await code.lint(await caller(), {
|
|
264
|
+
code: body.code,
|
|
265
|
+
filePath: body.filePath
|
|
266
|
+
})
|
|
267
|
+
};
|
|
268
|
+
case 'POST /SASjsApi/code/format':
|
|
269
|
+
if (!code)
|
|
270
|
+
throw new types_js_1.ApiError(501, `This host has no code service, so it cannot answer ${routeId}.`);
|
|
271
|
+
return {
|
|
272
|
+
status: 200,
|
|
273
|
+
body: await code.format(await caller(), {
|
|
274
|
+
code: body.code,
|
|
275
|
+
filePath: body.filePath
|
|
276
|
+
})
|
|
277
|
+
};
|
|
222
278
|
case 'GET /SASjsApi/info/authorizedRoutes':
|
|
223
279
|
return {
|
|
224
280
|
status: 200,
|
|
@@ -332,6 +388,12 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
332
388
|
exports.dispatch = dispatch;
|
|
333
389
|
/** Whether a route is one `dispatch` can answer today. */
|
|
334
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',
|
|
395
|
+
'POST /SASjsApi/code/lint',
|
|
396
|
+
'POST /SASjsApi/code/format',
|
|
335
397
|
'GET /SASjsApi/info/authorizedRoutes',
|
|
336
398
|
'GET /SASjsApi/info/myAuthorizedRoutes',
|
|
337
399
|
'GET /SASjsApi/drive/file',
|
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.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
|
*
|
|
@@ -67,6 +67,8 @@ Object.defineProperty(exports, "createIdentityService", { enumerable: true, get:
|
|
|
67
67
|
var records_js_1 = require("./identity/records.js");
|
|
68
68
|
Object.defineProperty(exports, "createRecordService", { enumerable: true, get: function () { return records_js_1.createRecordService; } });
|
|
69
69
|
Object.defineProperty(exports, "sanitiseDisplayName", { enumerable: true, get: function () { return records_js_1.sanitiseDisplayName; } });
|
|
70
|
+
var code_js_1 = require("./corpus/code.js");
|
|
71
|
+
Object.defineProperty(exports, "CODE_CORPUS", { enumerable: true, get: function () { return code_js_1.CODE_CORPUS; } });
|
|
70
72
|
var memory_store_js_2 = require("./identity/memory-store.js");
|
|
71
73
|
Object.defineProperty(exports, "MemoryIdentityStore", { enumerable: true, get: function () { return memory_store_js_2.MemoryIdentityStore; } });
|
|
72
74
|
var fixture_js_1 = require("./identity/fixture.js");
|
|
@@ -77,6 +79,8 @@ var drive_js_1 = require("./corpus/drive.js");
|
|
|
77
79
|
Object.defineProperty(exports, "DRIVE_CORPUS", { enumerable: true, get: function () { return drive_js_1.DRIVE_CORPUS; } });
|
|
78
80
|
var identity_js_1 = require("./corpus/identity.js");
|
|
79
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; } });
|
|
80
84
|
var records_js_2 = require("./corpus/records.js");
|
|
81
85
|
Object.defineProperty(exports, "RECORD_CORPUS", { enumerable: true, get: function () { return records_js_2.RECORD_CORPUS; } });
|
|
82
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
|
},
|
|
@@ -26,9 +26,7 @@ exports.TAG_CAPABILITIES = {
|
|
|
26
26
|
* thing thirty times.
|
|
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
|
-
const EDITOR = 'Linting and formatting come from @sasjs/lint, which this package does not depend on. They land with the code routes so an editor has one wiring.';
|
|
30
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.';
|
|
31
|
-
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.";
|
|
32
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.";
|
|
33
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.";
|
|
34
32
|
const WEB_BUNDLE = 'The host serves its own web bundle; there is no API decision here for this package to answer.';
|
|
@@ -96,8 +94,14 @@ exports.OVERLAY = {
|
|
|
96
94
|
// Code - the editor's three calls, and the runtime one
|
|
97
95
|
'POST /SASjsApi/code/execute': { implemented: false, reason: RUNTIME },
|
|
98
96
|
'POST /SASjsApi/code/trigger': { implemented: false, reason: RUNTIME },
|
|
99
|
-
'POST /SASjsApi/code/lint': {
|
|
100
|
-
|
|
97
|
+
'POST /SASjsApi/code/lint': {
|
|
98
|
+
implemented: true,
|
|
99
|
+
capabilities: ['identity']
|
|
100
|
+
},
|
|
101
|
+
'POST /SASjsApi/code/format': {
|
|
102
|
+
implemented: true,
|
|
103
|
+
capabilities: ['identity']
|
|
104
|
+
},
|
|
101
105
|
// Drive - the file and folder rules, and the three deploy routes
|
|
102
106
|
// The body is the file itself, so there is nothing to annotate
|
|
103
107
|
'GET /SASjsApi/drive/file': { implemented: true },
|
|
@@ -200,22 +204,13 @@ exports.OVERLAY = {
|
|
|
200
204
|
implemented: true,
|
|
201
205
|
responses: { 200: user }
|
|
202
206
|
},
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
},
|
|
207
|
-
'GET /SASjsApi/session/{sessionId}/log': {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
},
|
|
211
|
-
'GET /SASjsApi/session/{sessionId}/output': {
|
|
212
|
-
implemented: false,
|
|
213
|
-
reason: SESSION_RECORD
|
|
214
|
-
},
|
|
215
|
-
'GET /SASjsApi/session/{sessionId}/state': {
|
|
216
|
-
implemented: false,
|
|
217
|
-
reason: SESSION_RECORD
|
|
218
|
-
},
|
|
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 },
|
|
219
214
|
// STP - a stored program, on the runtime port
|
|
220
215
|
'GET /SASjsApi/stp/execute': { implemented: false, reason: RUNTIME },
|
|
221
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;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The code service, on its own entry point.
|
|
3
|
+
*
|
|
4
|
+
* `@sasjs/lint` reads the filesystem (its config resolution walks up for a
|
|
5
|
+
* `.sasjslint`), so it cannot be bundled for a service worker: the package root
|
|
6
|
+
* stays free of it and a host that wants these routes imports `@sasjs/api/code`.
|
|
7
|
+
*/
|
|
8
|
+
export { createCodeService } from './service.js';
|
|
9
|
+
export type { CodeRequest, CodeService, CodeServiceOptions } from './service.js';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/code/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The code service, on its own entry point.
|
|
3
|
+
*
|
|
4
|
+
* `@sasjs/lint` reads the filesystem (its config resolution walks up for a
|
|
5
|
+
* `.sasjslint`), so it cannot be bundled for a service worker: the package root
|
|
6
|
+
* stays free of it and a host that wants these routes imports `@sasjs/api/code`.
|
|
7
|
+
*/
|
|
8
|
+
export { createCodeService } from './service.js';
|