@sasjs/api 0.11.0 → 0.13.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 +284 -17
- package/build/cjs/corpus/deploy.js +139 -0
- package/build/cjs/corpus/runner.js +2 -0
- package/build/cjs/corpus/session.js +69 -0
- package/build/cjs/drive/registry.js +38 -0
- package/build/cjs/host/dispatch.js +15 -0
- package/build/cjs/index.js +3 -1
- package/build/cjs/routes/generated.js +32 -0
- package/build/cjs/routes/overlay.js +2 -10
- 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 +136 -0
- package/build/esm/corpus/runner.d.ts.map +1 -1
- package/build/esm/corpus/runner.js +2 -0
- package/build/esm/corpus/session.d.ts +9 -8
- package/build/esm/corpus/session.d.ts.map +1 -1
- package/build/esm/corpus/session.js +69 -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 +6 -0
- package/build/esm/host/dispatch.d.ts.map +1 -1
- package/build/esm/host/dispatch.js +15 -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 +32 -0
- package/build/esm/routes/overlay.d.ts.map +1 -1
- package/build/esm/routes/overlay.js +2 -10
- package/package.json +1 -1
|
@@ -3,7 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SESSION_CORPUS = void 0;
|
|
4
4
|
const ADMIN = { userId: 'user-admin', clientId: 'web_app' };
|
|
5
5
|
const RESTRICTED = { userId: 'user-restricted', clientId: 'web_app' };
|
|
6
|
+
const notFound = (id) => `Session with ID '${id}' was not found.`;
|
|
7
|
+
const UNAUTHORIZED = 'Unauthorized';
|
|
6
8
|
exports.SESSION_CORPUS = [
|
|
9
|
+
// ---------------------------------------------------------------- summary
|
|
7
10
|
{
|
|
8
11
|
id: 'session-summary-own',
|
|
9
12
|
route: 'GET /SASjsApi/session/{sessionId}',
|
|
@@ -36,6 +39,28 @@ exports.SESSION_CORPUS = [
|
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
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
|
|
39
64
|
{
|
|
40
65
|
id: 'session-log-own',
|
|
41
66
|
route: 'GET /SASjsApi/session/{sessionId}/log',
|
|
@@ -53,6 +78,21 @@ exports.SESSION_CORPUS = [
|
|
|
53
78
|
request: { subject: ADMIN, params: { sessionId: 'sess-silent' } },
|
|
54
79
|
expect: { status: 200, contents: '' }
|
|
55
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
|
|
56
96
|
{
|
|
57
97
|
id: 'session-output-own',
|
|
58
98
|
route: 'GET /SASjsApi/session/{sessionId}/output',
|
|
@@ -60,6 +100,21 @@ exports.SESSION_CORPUS = [
|
|
|
60
100
|
request: { subject: ADMIN, params: { sessionId: 'sess-admin' } },
|
|
61
101
|
expect: { status: 200, contents: '<html><body>42</body></html>' }
|
|
62
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
|
|
63
118
|
{
|
|
64
119
|
id: 'session-state-own',
|
|
65
120
|
route: 'GET /SASjsApi/session/{sessionId}/state',
|
|
@@ -69,5 +124,19 @@ exports.SESSION_CORPUS = [
|
|
|
69
124
|
status: 200,
|
|
70
125
|
body: { state: 'running', pid: 4243, lineCount: 3 }
|
|
71
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') }
|
|
72
141
|
}
|
|
73
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. */
|
|
@@ -169,12 +170,24 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
169
170
|
status: 200,
|
|
170
171
|
body: await drive.rename(String(request.body?.oldPath || ''), String(request.body?.newPath || ''))
|
|
171
172
|
};
|
|
173
|
+
// The archive is the host's transport - multer and a zip library on a
|
|
174
|
+
// server, `DecompressionStream` in a worker - and it hands this the same tree
|
|
175
|
+
// `POST /deploy` takes. The deploy cap is measured on that tree, after
|
|
176
|
+
// expansion, which is why the two share this path rather than reading bytes.
|
|
177
|
+
case 'POST /SASjsApi/drive/deploy/upload':
|
|
172
178
|
case 'POST /SASjsApi/drive/deploy': {
|
|
173
179
|
// `streamWebFolder` is the host's: publishing an app stream is not a drive
|
|
174
180
|
// operation, and a host that has no app stream ignores it.
|
|
175
181
|
await drive.deploy(String(body.appLoc ?? ''), body.fileTree);
|
|
176
182
|
return { status: 200, body: deploy_js_1.DEPLOY_SUCCESS };
|
|
177
183
|
}
|
|
184
|
+
case 'DELETE /SASjsApi/drive/deploy':
|
|
185
|
+
if (!services.registry)
|
|
186
|
+
throw new types_js_1.ApiError(501, `This host has no app stream registry, so it cannot answer ${routeId}.`);
|
|
187
|
+
return {
|
|
188
|
+
status: 200,
|
|
189
|
+
body: await (0, registry_js_1.unpublishAppStream)(services.registry, body)
|
|
190
|
+
};
|
|
178
191
|
case 'GET /SASjsApi/drive/filetree':
|
|
179
192
|
return { status: 200, body: await drive.tree(query._folderPath) };
|
|
180
193
|
// ------------------------------------------------------------- sessions
|
|
@@ -388,6 +401,8 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
388
401
|
exports.dispatch = dispatch;
|
|
389
402
|
/** Whether a route is one `dispatch` can answer today. */
|
|
390
403
|
exports.HANDLED_ROUTES = [
|
|
404
|
+
'POST /SASjsApi/drive/deploy/upload',
|
|
405
|
+
'DELETE /SASjsApi/drive/deploy',
|
|
391
406
|
'GET /SASjsApi/session/{sessionId}',
|
|
392
407
|
'GET /SASjsApi/session/{sessionId}/log',
|
|
393
408
|
'GET /SASjsApi/session/{sessionId}/output',
|
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.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;
|
|
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,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 deploy_js_2 = require("./corpus/deploy.js");
|
|
83
|
+
Object.defineProperty(exports, "DEPLOY_CORPUS", { enumerable: true, get: function () { return deploy_js_2.DEPLOY_CORPUS; } });
|
|
82
84
|
var session_js_1 = require("./corpus/session.js");
|
|
83
85
|
Object.defineProperty(exports, "SESSION_CORPUS", { enumerable: true, get: function () { return session_js_1.SESSION_CORPUS; } });
|
|
84
86
|
var records_js_2 = require("./corpus/records.js");
|
|
@@ -1301,6 +1301,14 @@ exports.GENERATED_ROUTES = [
|
|
|
1301
1301
|
{
|
|
1302
1302
|
status: 200,
|
|
1303
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.'
|
|
1304
1312
|
}
|
|
1305
1313
|
]
|
|
1306
1314
|
},
|
|
@@ -1323,6 +1331,14 @@ exports.GENERATED_ROUTES = [
|
|
|
1323
1331
|
{
|
|
1324
1332
|
status: 200,
|
|
1325
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.'
|
|
1326
1342
|
}
|
|
1327
1343
|
]
|
|
1328
1344
|
},
|
|
@@ -1345,6 +1361,14 @@ exports.GENERATED_ROUTES = [
|
|
|
1345
1361
|
{
|
|
1346
1362
|
status: 200,
|
|
1347
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.'
|
|
1348
1372
|
}
|
|
1349
1373
|
]
|
|
1350
1374
|
},
|
|
@@ -1367,6 +1391,14 @@ exports.GENERATED_ROUTES = [
|
|
|
1367
1391
|
{
|
|
1368
1392
|
status: 200,
|
|
1369
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.'
|
|
1370
1402
|
}
|
|
1371
1403
|
]
|
|
1372
1404
|
},
|
|
@@ -30,8 +30,6 @@ const RUNTIME = 'Starting a program needs a runtime port, which this package doe
|
|
|
30
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.";
|
|
31
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.";
|
|
32
32
|
const WEB_BUNDLE = 'The host serves its own web bundle; there is no API decision here for this package to answer.';
|
|
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.";
|
|
34
|
-
const UNPUBLISH = 'Unpublishing inverts the deploy route, so the two land together.';
|
|
35
33
|
const success = { status: 'success' };
|
|
36
34
|
const user = {
|
|
37
35
|
uid: '',
|
|
@@ -159,16 +157,10 @@ exports.OVERLAY = {
|
|
|
159
157
|
500: { status: 'failure', message: 'Deployment failed!' }
|
|
160
158
|
}
|
|
161
159
|
},
|
|
162
|
-
'POST /SASjsApi/drive/deploy/upload': {
|
|
163
|
-
implemented: false,
|
|
164
|
-
reason: ARCHIVE
|
|
165
|
-
},
|
|
160
|
+
'POST /SASjsApi/drive/deploy/upload': { implemented: true },
|
|
166
161
|
// `404` is a pair - the registry name and the appLoc it was published from -
|
|
167
162
|
// that matches no entry, and the drive's content is deliberately left alone.
|
|
168
|
-
'DELETE /SASjsApi/drive/deploy': {
|
|
169
|
-
implemented: false,
|
|
170
|
-
reason: UNPUBLISH
|
|
171
|
-
},
|
|
163
|
+
'DELETE /SASjsApi/drive/deploy': { implemented: true },
|
|
172
164
|
// Group - members and the groups themselves
|
|
173
165
|
'GET /SASjsApi/group': { implemented: true },
|
|
174
166
|
'GET /SASjsApi/group/{uid}': { implemented: true },
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deploy routes' conformance corpus.
|
|
3
|
+
*
|
|
4
|
+
* `DELETE /SASjsApi/drive/deploy` documents three answers - `200`, `400` and
|
|
5
|
+
* `404` - and each has a case here, which is what
|
|
6
|
+
* `spec/corpus-parity.spec.ts` enforces in both directions.
|
|
7
|
+
*
|
|
8
|
+
* **The folder is the identity.** An app is one thing to deploy and one thing to
|
|
9
|
+
* remove; its name is a label on it. The document requires both body values, so a
|
|
10
|
+
* caller must supply both, but only the folder decides what is deleted - and the
|
|
11
|
+
* case below with a stale name and the right folder pins exactly that, because a
|
|
12
|
+
* name-keyed lookup would answer 404 there and leave the app in place.
|
|
13
|
+
*/
|
|
14
|
+
import type { CorpusCase } from './drive.js';
|
|
15
|
+
export declare const DEPLOY_CORPUS: CorpusCase[];
|
|
16
|
+
//# sourceMappingURL=deploy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/corpus/deploy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAW5C,eAAO,MAAM,aAAa,EAAE,UAAU,EAmIrC,CAAA"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const SUBJECT = { userId: 'user-admin', clientId: 'web_app' };
|
|
2
|
+
/** A tree of `count` file members, for the deploy cap. */
|
|
3
|
+
const manyMembers = (count) => Array.from({ length: count }, (_, at) => ({
|
|
4
|
+
name: `file-${at}.txt`,
|
|
5
|
+
type: 'file',
|
|
6
|
+
code: ''
|
|
7
|
+
}));
|
|
8
|
+
export const DEPLOY_CORPUS = [
|
|
9
|
+
{
|
|
10
|
+
id: 'deploy-unpublish',
|
|
11
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
12
|
+
description: 'removes a published app stream and answers the status',
|
|
13
|
+
request: {
|
|
14
|
+
subject: SUBJECT,
|
|
15
|
+
body: { streamServiceName: 'myApp', appLoc: '/Public/myApp' }
|
|
16
|
+
},
|
|
17
|
+
expect: { status: 200, body: { status: 'success' } }
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'deploy-unpublish-other-name',
|
|
21
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
22
|
+
description: 'removes the app by its folder when the name has been renamed',
|
|
23
|
+
request: {
|
|
24
|
+
subject: SUBJECT,
|
|
25
|
+
body: { streamServiceName: 'renamed-app', appLoc: '/Public/myApp' }
|
|
26
|
+
},
|
|
27
|
+
expect: { status: 200, body: { status: 'success' } }
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'deploy-unpublish-no-stream-name',
|
|
31
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
32
|
+
description: 'refuses a body that does not say which stream to remove',
|
|
33
|
+
request: { subject: SUBJECT, body: { appLoc: '/Public/myApp' } },
|
|
34
|
+
expect: { status: 400, error: '"streamServiceName" is required' }
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'deploy-unpublish-no-app-loc',
|
|
38
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
39
|
+
description: 'refuses a body that does not say which folder to remove',
|
|
40
|
+
request: { subject: SUBJECT, body: { streamServiceName: 'myApp' } },
|
|
41
|
+
expect: { status: 400, error: '"appLoc" is required' }
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'deploy-unpublish-unknown-folder',
|
|
45
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
46
|
+
description: 'answers 404 for a folder nothing is published at',
|
|
47
|
+
request: {
|
|
48
|
+
subject: SUBJECT,
|
|
49
|
+
body: { streamServiceName: 'myApp', appLoc: '/Public/not-an-app' }
|
|
50
|
+
},
|
|
51
|
+
expect: { status: 404, error: 'No such app stream entry.' }
|
|
52
|
+
},
|
|
53
|
+
// ------------------------------------------- the uploaded-archive route
|
|
54
|
+
// The archive is expanded by the host, so what arrives here is the same tree
|
|
55
|
+
// `POST /deploy` takes - which is why every answer below matches its twin.
|
|
56
|
+
{
|
|
57
|
+
id: 'upload-tree',
|
|
58
|
+
route: 'POST /SASjsApi/drive/deploy/upload',
|
|
59
|
+
description: 'writes the tree an uploaded archive expanded to',
|
|
60
|
+
request: {
|
|
61
|
+
subject: SUBJECT,
|
|
62
|
+
body: {
|
|
63
|
+
appLoc: '/Public/apps/uploaded',
|
|
64
|
+
fileTree: {
|
|
65
|
+
members: [
|
|
66
|
+
{ name: 'index.html', type: 'file', code: 'PGgxPkhlbGxvPC9oMT4=' }
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
expect: {
|
|
72
|
+
status: 200,
|
|
73
|
+
body: {
|
|
74
|
+
status: 'success',
|
|
75
|
+
message: 'Files deployed successfully to @sasjs/server.'
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'upload-refused-by-shape',
|
|
81
|
+
route: 'POST /SASjsApi/drive/deploy/upload',
|
|
82
|
+
description: 'refuses an expansion that is not a tree',
|
|
83
|
+
request: {
|
|
84
|
+
subject: SUBJECT,
|
|
85
|
+
body: { appLoc: '/Public/apps/bad', fileTree: { members: 1 } }
|
|
86
|
+
},
|
|
87
|
+
expect: {
|
|
88
|
+
status: 400,
|
|
89
|
+
body: {
|
|
90
|
+
status: 'failure',
|
|
91
|
+
message: 'Provided not supported data format.'
|
|
92
|
+
},
|
|
93
|
+
error: 'Provided not supported data format.'
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'upload-refused-by-entry-count',
|
|
98
|
+
route: 'POST /SASjsApi/drive/deploy/upload',
|
|
99
|
+
description: 'applies the deploy cap to the expanded tree, not the bytes',
|
|
100
|
+
request: {
|
|
101
|
+
subject: SUBJECT,
|
|
102
|
+
body: {
|
|
103
|
+
appLoc: '/Public/apps/many',
|
|
104
|
+
fileTree: { members: manyMembers(10001) }
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
expect: {
|
|
108
|
+
status: 413,
|
|
109
|
+
body: {
|
|
110
|
+
status: 'failure',
|
|
111
|
+
message: 'The file tree exceeds the deploy limit: 10001 entries (maximum 10000).'
|
|
112
|
+
},
|
|
113
|
+
error: 'The file tree exceeds the deploy limit: 10001 entries (maximum 10000).'
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'upload-refused-by-store',
|
|
118
|
+
route: 'POST /SASjsApi/drive/deploy/upload',
|
|
119
|
+
description: 'answers the routes 500 when the drive refuses the write',
|
|
120
|
+
unwritable: true,
|
|
121
|
+
request: {
|
|
122
|
+
subject: SUBJECT,
|
|
123
|
+
body: {
|
|
124
|
+
appLoc: '/Public/apps/demo',
|
|
125
|
+
fileTree: {
|
|
126
|
+
members: [{ name: 'run', type: 'service', code: '%put hi;' }]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
expect: {
|
|
131
|
+
status: 500,
|
|
132
|
+
body: { status: 'failure', message: 'Deployment failed!' },
|
|
133
|
+
error: 'Deployment failed!'
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
];
|
|
@@ -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;AAYnD,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,QAoBlD,CAAA;AAkBD;;;;GAIG;AACH,eAAO,MAAM,OAAO,GAClB,UAAU,UAAU,EACpB,WAAU,QAAgC,KACzC,OAAO,CAAC,UAAU,CAepB,CAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dispatch } from '../host/dispatch.js';
|
|
2
2
|
import { createDriveService } from '../drive/service.js';
|
|
3
3
|
import { MemoryDriveStore } from '../drive/memory-store.js';
|
|
4
|
+
import { MemoryAppStreamRegistry } from '../drive/registry.js';
|
|
4
5
|
import { createIdentityService } from '../identity/service.js';
|
|
5
6
|
import { createRecordService } from '../identity/records.js';
|
|
6
7
|
import { createSessionService } from '../session/service.js';
|
|
@@ -26,6 +27,7 @@ export const servicesFor = (testCase) => {
|
|
|
26
27
|
: new MemoryDriveStore(testCase.seed || {})),
|
|
27
28
|
identity: createIdentityService(identity),
|
|
28
29
|
records: createRecordService(identity),
|
|
30
|
+
registry: new MemoryAppStreamRegistry(),
|
|
29
31
|
session: createSessionService(identity, new MemorySessionHost())
|
|
30
32
|
};
|
|
31
33
|
};
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The session routes' conformance corpus.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* gate does not govern.
|
|
4
|
+
* Every status the document states for these four routes has a case, in both
|
|
5
|
+
* directions - that is what `spec/corpus-parity.spec.ts` enforces. The refusals
|
|
6
|
+
* arrived here only after sasjs/server#574 documented them: until then the
|
|
7
|
+
* package answered a 401 and a 404 that the document did not state, and a case
|
|
8
|
+
* pinning one would have been called drift.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* `404` covers an unknown session and someone else's alike, on purpose - the id
|
|
11
|
+
* is guessable within a window, so confirming that it exists would say the id
|
|
12
|
+
* was right. The cases below keep the two apart so a change that starts
|
|
13
|
+
* answering `403` for a foreign session is visible.
|
|
13
14
|
*/
|
|
14
15
|
import type { CorpusCase } from './drive.js';
|
|
15
16
|
export declare const SESSION_CORPUS: CorpusCase[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/corpus/session.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/corpus/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAQ5C,eAAO,MAAM,cAAc,EAAE,UAAU,EAyItC,CAAA"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const ADMIN = { userId: 'user-admin', clientId: 'web_app' };
|
|
2
2
|
const RESTRICTED = { userId: 'user-restricted', clientId: 'web_app' };
|
|
3
|
+
const notFound = (id) => `Session with ID '${id}' was not found.`;
|
|
4
|
+
const UNAUTHORIZED = 'Unauthorized';
|
|
3
5
|
export const SESSION_CORPUS = [
|
|
6
|
+
// ---------------------------------------------------------------- summary
|
|
4
7
|
{
|
|
5
8
|
id: 'session-summary-own',
|
|
6
9
|
route: 'GET /SASjsApi/session/{sessionId}',
|
|
@@ -33,6 +36,28 @@ export const SESSION_CORPUS = [
|
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
},
|
|
39
|
+
{
|
|
40
|
+
id: 'session-summary-unauthenticated',
|
|
41
|
+
route: 'GET /SASjsApi/session/{sessionId}',
|
|
42
|
+
description: 'refuses a caller with no token',
|
|
43
|
+
request: { params: { sessionId: 'sess-admin' } },
|
|
44
|
+
expect: { status: 401, error: UNAUTHORIZED }
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'session-summary-foreign',
|
|
48
|
+
route: 'GET /SASjsApi/session/{sessionId}',
|
|
49
|
+
description: "answers 404 for another account's session, not 403",
|
|
50
|
+
request: { subject: RESTRICTED, params: { sessionId: 'sess-admin' } },
|
|
51
|
+
expect: { status: 404, error: notFound('sess-admin') }
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'session-summary-unknown',
|
|
55
|
+
route: 'GET /SASjsApi/session/{sessionId}',
|
|
56
|
+
description: 'answers the same 404 for a session that does not exist',
|
|
57
|
+
request: { subject: ADMIN, params: { sessionId: 'sess-nope' } },
|
|
58
|
+
expect: { status: 404, error: notFound('sess-nope') }
|
|
59
|
+
},
|
|
60
|
+
// -------------------------------------------------------------------- log
|
|
36
61
|
{
|
|
37
62
|
id: 'session-log-own',
|
|
38
63
|
route: 'GET /SASjsApi/session/{sessionId}/log',
|
|
@@ -50,6 +75,21 @@ export const SESSION_CORPUS = [
|
|
|
50
75
|
request: { subject: ADMIN, params: { sessionId: 'sess-silent' } },
|
|
51
76
|
expect: { status: 200, contents: '' }
|
|
52
77
|
},
|
|
78
|
+
{
|
|
79
|
+
id: 'session-log-unauthenticated',
|
|
80
|
+
route: 'GET /SASjsApi/session/{sessionId}/log',
|
|
81
|
+
description: 'refuses a caller with no token',
|
|
82
|
+
request: { params: { sessionId: 'sess-admin' } },
|
|
83
|
+
expect: { status: 401, error: UNAUTHORIZED }
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'session-log-foreign',
|
|
87
|
+
route: 'GET /SASjsApi/session/{sessionId}/log',
|
|
88
|
+
description: "will not read another account's log",
|
|
89
|
+
request: { subject: RESTRICTED, params: { sessionId: 'sess-admin' } },
|
|
90
|
+
expect: { status: 404, error: notFound('sess-admin') }
|
|
91
|
+
},
|
|
92
|
+
// ----------------------------------------------------------------- output
|
|
53
93
|
{
|
|
54
94
|
id: 'session-output-own',
|
|
55
95
|
route: 'GET /SASjsApi/session/{sessionId}/output',
|
|
@@ -57,6 +97,21 @@ export const SESSION_CORPUS = [
|
|
|
57
97
|
request: { subject: ADMIN, params: { sessionId: 'sess-admin' } },
|
|
58
98
|
expect: { status: 200, contents: '<html><body>42</body></html>' }
|
|
59
99
|
},
|
|
100
|
+
{
|
|
101
|
+
id: 'session-output-unauthenticated',
|
|
102
|
+
route: 'GET /SASjsApi/session/{sessionId}/output',
|
|
103
|
+
description: 'refuses a caller with no token',
|
|
104
|
+
request: { params: { sessionId: 'sess-admin' } },
|
|
105
|
+
expect: { status: 401, error: UNAUTHORIZED }
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'session-output-foreign',
|
|
109
|
+
route: 'GET /SASjsApi/session/{sessionId}/output',
|
|
110
|
+
description: "will not read another account's webout",
|
|
111
|
+
request: { subject: RESTRICTED, params: { sessionId: 'sess-admin' } },
|
|
112
|
+
expect: { status: 404, error: notFound('sess-admin') }
|
|
113
|
+
},
|
|
114
|
+
// ------------------------------------------------------------------ state
|
|
60
115
|
{
|
|
61
116
|
id: 'session-state-own',
|
|
62
117
|
route: 'GET /SASjsApi/session/{sessionId}/state',
|
|
@@ -66,5 +121,19 @@ export const SESSION_CORPUS = [
|
|
|
66
121
|
status: 200,
|
|
67
122
|
body: { state: 'running', pid: 4243, lineCount: 3 }
|
|
68
123
|
}
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'session-state-unauthenticated',
|
|
127
|
+
route: 'GET /SASjsApi/session/{sessionId}/state',
|
|
128
|
+
description: 'refuses a caller with no token',
|
|
129
|
+
request: { params: { sessionId: 'sess-restricted' } },
|
|
130
|
+
expect: { status: 401, error: UNAUTHORIZED }
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: 'session-state-foreign',
|
|
134
|
+
route: 'GET /SASjsApi/session/{sessionId}/state',
|
|
135
|
+
description: "will not report another account's session state",
|
|
136
|
+
request: { subject: RESTRICTED, params: { sessionId: 'sess-admin' } },
|
|
137
|
+
expect: { status: 404, error: notFound('sess-admin') }
|
|
69
138
|
}
|
|
70
139
|
];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app streams a host has published, as the API needs to unpublish one.
|
|
3
|
+
*
|
|
4
|
+
* A published stream is the host's own record - a directory convention, a
|
|
5
|
+
* registry table, whatever it uses to know that `/Public/myApp` is served at
|
|
6
|
+
* `/Public/myApp/index.html`. The package cannot invent it, so the route takes
|
|
7
|
+
* this port and answers 501 without it, the same way the session routes do.
|
|
8
|
+
*/
|
|
9
|
+
export interface AppStreamRegistry {
|
|
10
|
+
/**
|
|
11
|
+
* Removes the app published at `appLoc`.
|
|
12
|
+
*
|
|
13
|
+
* **The folder is the identity.** An app is one thing to deploy and one thing
|
|
14
|
+
* to delete, and what it is called is a label on it: two apps can share a
|
|
15
|
+
* folder only as a special case, and they are still one thing to load or
|
|
16
|
+
* remove. So the lookup is by folder, and a name that no longer matches the
|
|
17
|
+
* one recorded does not turn a correct deletion into a `404`.
|
|
18
|
+
*
|
|
19
|
+
* `false` when nothing is published at that folder, which the route reports as
|
|
20
|
+
* a 404 - a delete that silently succeeded on a folder nobody deployed would
|
|
21
|
+
* hide a caller's typo.
|
|
22
|
+
*/
|
|
23
|
+
unpublish(appLoc: string): Promise<boolean>;
|
|
24
|
+
}
|
|
25
|
+
export interface UnpublishBody {
|
|
26
|
+
streamServiceName?: string;
|
|
27
|
+
appLoc?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* `DELETE /SASjsApi/drive/deploy`.
|
|
31
|
+
*
|
|
32
|
+
* The document requires both body values, so both are checked - a caller that
|
|
33
|
+
* names a stream but not the folder has not said what to remove. Only the folder
|
|
34
|
+
* reaches the registry, because that is what identifies the app.
|
|
35
|
+
*
|
|
36
|
+
* The checks are here rather than in the transport because this is the one
|
|
37
|
+
* implementation of the operation: a host calling it directly gets the same
|
|
38
|
+
* refusals in the same order, and with the same messages the document states.
|
|
39
|
+
*/
|
|
40
|
+
export declare const unpublishAppStream: (registry: AppStreamRegistry, body: UnpublishBody | undefined) => Promise<{
|
|
41
|
+
status: "success";
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* The registry the corpus and the package's own tests read: one published app,
|
|
45
|
+
* identified by its folder alone.
|
|
46
|
+
*/
|
|
47
|
+
export declare class MemoryAppStreamRegistry implements AppStreamRegistry {
|
|
48
|
+
private readonly folders;
|
|
49
|
+
constructor(folders?: string[]);
|
|
50
|
+
unpublish(appLoc: string): Promise<boolean>;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=registry.d.ts.map
|