@sasjs/api 0.11.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 +189 -12
- package/build/cjs/corpus/deploy.js +50 -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 +9 -0
- package/build/cjs/index.js +3 -1
- package/build/cjs/routes/generated.js +32 -0
- package/build/cjs/routes/overlay.js +1 -5
- 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 +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 +9 -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 +1 -5
- package/package.json +1 -1
package/build/browser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* @sasjs/api 0.
|
|
1
|
+
/* @sasjs/api 0.12.0 - the SASjs Server API, for a service worker. https://github.com/sasjs/api */
|
|
2
2
|
"use strict";
|
|
3
3
|
var SASjsApi = (() => {
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -28,6 +28,7 @@ var SASjsApi = (() => {
|
|
|
28
28
|
CODE_CORPUS: () => CODE_CORPUS,
|
|
29
29
|
DEFAULT_GRANTABLE_ROUTES: () => DEFAULT_GRANTABLE_ROUTES,
|
|
30
30
|
DEFAULT_IDENTITY: () => DEFAULT_IDENTITY,
|
|
31
|
+
DEPLOY_CORPUS: () => DEPLOY_CORPUS,
|
|
31
32
|
DEPLOY_LIMITS: () => DEPLOY_LIMITS,
|
|
32
33
|
DEPLOY_PAYLOAD_BYTES: () => DEPLOY_PAYLOAD_BYTES,
|
|
33
34
|
DEPLOY_SUCCESS: () => DEPLOY_SUCCESS,
|
|
@@ -1847,6 +1848,14 @@ var SASjsApi = (() => {
|
|
|
1847
1848
|
{
|
|
1848
1849
|
status: 200,
|
|
1849
1850
|
description: "Ok"
|
|
1851
|
+
},
|
|
1852
|
+
{
|
|
1853
|
+
status: 401,
|
|
1854
|
+
description: "No valid access token or session was supplied"
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
status: 404,
|
|
1858
|
+
description: "Session with the given ID was not found."
|
|
1850
1859
|
}
|
|
1851
1860
|
]
|
|
1852
1861
|
},
|
|
@@ -1869,6 +1878,14 @@ var SASjsApi = (() => {
|
|
|
1869
1878
|
{
|
|
1870
1879
|
status: 200,
|
|
1871
1880
|
description: "Ok"
|
|
1881
|
+
},
|
|
1882
|
+
{
|
|
1883
|
+
status: 401,
|
|
1884
|
+
description: "No valid access token or session was supplied"
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
status: 404,
|
|
1888
|
+
description: "Session with the given ID was not found."
|
|
1872
1889
|
}
|
|
1873
1890
|
]
|
|
1874
1891
|
},
|
|
@@ -1891,6 +1908,14 @@ var SASjsApi = (() => {
|
|
|
1891
1908
|
{
|
|
1892
1909
|
status: 200,
|
|
1893
1910
|
description: "Ok"
|
|
1911
|
+
},
|
|
1912
|
+
{
|
|
1913
|
+
status: 401,
|
|
1914
|
+
description: "No valid access token or session was supplied"
|
|
1915
|
+
},
|
|
1916
|
+
{
|
|
1917
|
+
status: 404,
|
|
1918
|
+
description: "Session with the given ID was not found."
|
|
1894
1919
|
}
|
|
1895
1920
|
]
|
|
1896
1921
|
},
|
|
@@ -1913,6 +1938,14 @@ var SASjsApi = (() => {
|
|
|
1913
1938
|
{
|
|
1914
1939
|
status: 200,
|
|
1915
1940
|
description: "Ok"
|
|
1941
|
+
},
|
|
1942
|
+
{
|
|
1943
|
+
status: 401,
|
|
1944
|
+
description: "No valid access token or session was supplied"
|
|
1945
|
+
},
|
|
1946
|
+
{
|
|
1947
|
+
status: 404,
|
|
1948
|
+
description: "Session with the given ID was not found."
|
|
1916
1949
|
}
|
|
1917
1950
|
]
|
|
1918
1951
|
},
|
|
@@ -2482,7 +2515,6 @@ var SASjsApi = (() => {
|
|
|
2482
2515
|
var HOST_OWNED = "The host owns this report: it describes the host's own process and files, which a browser build has no equivalent of.";
|
|
2483
2516
|
var WEB_BUNDLE = "The host serves its own web bundle; there is no API decision here for this package to answer.";
|
|
2484
2517
|
var 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.";
|
|
2485
|
-
var UNPUBLISH = "Unpublishing inverts the deploy route, so the two land together.";
|
|
2486
2518
|
var success = { status: "success" };
|
|
2487
2519
|
var user = {
|
|
2488
2520
|
uid: "",
|
|
@@ -2616,10 +2648,7 @@ var SASjsApi = (() => {
|
|
|
2616
2648
|
},
|
|
2617
2649
|
// `404` is a pair - the registry name and the appLoc it was published from -
|
|
2618
2650
|
// that matches no entry, and the drive's content is deliberately left alone.
|
|
2619
|
-
"DELETE /SASjsApi/drive/deploy": {
|
|
2620
|
-
implemented: false,
|
|
2621
|
-
reason: UNPUBLISH
|
|
2622
|
-
},
|
|
2651
|
+
"DELETE /SASjsApi/drive/deploy": { implemented: true },
|
|
2623
2652
|
// Group - members and the groups themselves
|
|
2624
2653
|
"GET /SASjsApi/group": { implemented: true },
|
|
2625
2654
|
"GET /SASjsApi/group/{uid}": { implemented: true },
|
|
@@ -2777,6 +2806,24 @@ var SASjsApi = (() => {
|
|
|
2777
2806
|
return void 0;
|
|
2778
2807
|
};
|
|
2779
2808
|
|
|
2809
|
+
// src/drive/registry.ts
|
|
2810
|
+
var unpublishAppStream = async (registry, body) => {
|
|
2811
|
+
if (!body?.streamServiceName)
|
|
2812
|
+
throw new ApiError(400, '"streamServiceName" is required');
|
|
2813
|
+
if (!body.appLoc) throw new ApiError(400, '"appLoc" is required');
|
|
2814
|
+
if (!await registry.unpublish(body.appLoc))
|
|
2815
|
+
throw new ApiError(404, "No such app stream entry.");
|
|
2816
|
+
return { status: "success" };
|
|
2817
|
+
};
|
|
2818
|
+
var MemoryAppStreamRegistry = class {
|
|
2819
|
+
constructor(folders = ["/Public/myApp"]) {
|
|
2820
|
+
this.folders = new Set(folders);
|
|
2821
|
+
}
|
|
2822
|
+
async unpublish(appLoc) {
|
|
2823
|
+
return this.folders.delete(appLoc);
|
|
2824
|
+
}
|
|
2825
|
+
};
|
|
2826
|
+
|
|
2780
2827
|
// src/host/dispatch.ts
|
|
2781
2828
|
var SECONDS_IN_A_DAY = 86400;
|
|
2782
2829
|
var validate = (route, request) => {
|
|
@@ -2923,6 +2970,16 @@ var SASjsApi = (() => {
|
|
|
2923
2970
|
await drive.deploy(String(body.appLoc ?? ""), body.fileTree);
|
|
2924
2971
|
return { status: 200, body: DEPLOY_SUCCESS };
|
|
2925
2972
|
}
|
|
2973
|
+
case "DELETE /SASjsApi/drive/deploy":
|
|
2974
|
+
if (!services.registry)
|
|
2975
|
+
throw new ApiError(
|
|
2976
|
+
501,
|
|
2977
|
+
`This host has no app stream registry, so it cannot answer ${routeId}.`
|
|
2978
|
+
);
|
|
2979
|
+
return {
|
|
2980
|
+
status: 200,
|
|
2981
|
+
body: await unpublishAppStream(services.registry, body)
|
|
2982
|
+
};
|
|
2926
2983
|
case "GET /SASjsApi/drive/filetree":
|
|
2927
2984
|
return { status: 200, body: await drive.tree(query._folderPath) };
|
|
2928
2985
|
// ------------------------------------------------------------- sessions
|
|
@@ -3177,6 +3234,7 @@ var SASjsApi = (() => {
|
|
|
3177
3234
|
}
|
|
3178
3235
|
};
|
|
3179
3236
|
var HANDLED_ROUTES = [
|
|
3237
|
+
"DELETE /SASjsApi/drive/deploy",
|
|
3180
3238
|
"GET /SASjsApi/session/{sessionId}",
|
|
3181
3239
|
"GET /SASjsApi/session/{sessionId}/log",
|
|
3182
3240
|
"GET /SASjsApi/session/{sessionId}/output",
|
|
@@ -4898,10 +4956,62 @@ var SASjsApi = (() => {
|
|
|
4898
4956
|
}
|
|
4899
4957
|
];
|
|
4900
4958
|
|
|
4959
|
+
// src/corpus/deploy.ts
|
|
4960
|
+
var SUBJECT = { userId: "user-admin", clientId: "web_app" };
|
|
4961
|
+
var DEPLOY_CORPUS = [
|
|
4962
|
+
{
|
|
4963
|
+
id: "deploy-unpublish",
|
|
4964
|
+
route: "DELETE /SASjsApi/drive/deploy",
|
|
4965
|
+
description: "removes a published app stream and answers the status",
|
|
4966
|
+
request: {
|
|
4967
|
+
subject: SUBJECT,
|
|
4968
|
+
body: { streamServiceName: "myApp", appLoc: "/Public/myApp" }
|
|
4969
|
+
},
|
|
4970
|
+
expect: { status: 200, body: { status: "success" } }
|
|
4971
|
+
},
|
|
4972
|
+
{
|
|
4973
|
+
id: "deploy-unpublish-other-name",
|
|
4974
|
+
route: "DELETE /SASjsApi/drive/deploy",
|
|
4975
|
+
description: "removes the app by its folder when the name has been renamed",
|
|
4976
|
+
request: {
|
|
4977
|
+
subject: SUBJECT,
|
|
4978
|
+
body: { streamServiceName: "renamed-app", appLoc: "/Public/myApp" }
|
|
4979
|
+
},
|
|
4980
|
+
expect: { status: 200, body: { status: "success" } }
|
|
4981
|
+
},
|
|
4982
|
+
{
|
|
4983
|
+
id: "deploy-unpublish-no-stream-name",
|
|
4984
|
+
route: "DELETE /SASjsApi/drive/deploy",
|
|
4985
|
+
description: "refuses a body that does not say which stream to remove",
|
|
4986
|
+
request: { subject: SUBJECT, body: { appLoc: "/Public/myApp" } },
|
|
4987
|
+
expect: { status: 400, error: '"streamServiceName" is required' }
|
|
4988
|
+
},
|
|
4989
|
+
{
|
|
4990
|
+
id: "deploy-unpublish-no-app-loc",
|
|
4991
|
+
route: "DELETE /SASjsApi/drive/deploy",
|
|
4992
|
+
description: "refuses a body that does not say which folder to remove",
|
|
4993
|
+
request: { subject: SUBJECT, body: { streamServiceName: "myApp" } },
|
|
4994
|
+
expect: { status: 400, error: '"appLoc" is required' }
|
|
4995
|
+
},
|
|
4996
|
+
{
|
|
4997
|
+
id: "deploy-unpublish-unknown-folder",
|
|
4998
|
+
route: "DELETE /SASjsApi/drive/deploy",
|
|
4999
|
+
description: "answers 404 for a folder nothing is published at",
|
|
5000
|
+
request: {
|
|
5001
|
+
subject: SUBJECT,
|
|
5002
|
+
body: { streamServiceName: "myApp", appLoc: "/Public/not-an-app" }
|
|
5003
|
+
},
|
|
5004
|
+
expect: { status: 404, error: "No such app stream entry." }
|
|
5005
|
+
}
|
|
5006
|
+
];
|
|
5007
|
+
|
|
4901
5008
|
// src/corpus/session.ts
|
|
4902
5009
|
var ADMIN = { userId: "user-admin", clientId: "web_app" };
|
|
4903
5010
|
var RESTRICTED = { userId: "user-restricted", clientId: "web_app" };
|
|
5011
|
+
var notFound = (id) => `Session with ID '${id}' was not found.`;
|
|
5012
|
+
var UNAUTHORIZED = "Unauthorized";
|
|
4904
5013
|
var SESSION_CORPUS = [
|
|
5014
|
+
// ---------------------------------------------------------------- summary
|
|
4905
5015
|
{
|
|
4906
5016
|
id: "session-summary-own",
|
|
4907
5017
|
route: "GET /SASjsApi/session/{sessionId}",
|
|
@@ -4934,6 +5044,28 @@ var SASjsApi = (() => {
|
|
|
4934
5044
|
}
|
|
4935
5045
|
}
|
|
4936
5046
|
},
|
|
5047
|
+
{
|
|
5048
|
+
id: "session-summary-unauthenticated",
|
|
5049
|
+
route: "GET /SASjsApi/session/{sessionId}",
|
|
5050
|
+
description: "refuses a caller with no token",
|
|
5051
|
+
request: { params: { sessionId: "sess-admin" } },
|
|
5052
|
+
expect: { status: 401, error: UNAUTHORIZED }
|
|
5053
|
+
},
|
|
5054
|
+
{
|
|
5055
|
+
id: "session-summary-foreign",
|
|
5056
|
+
route: "GET /SASjsApi/session/{sessionId}",
|
|
5057
|
+
description: "answers 404 for another account's session, not 403",
|
|
5058
|
+
request: { subject: RESTRICTED, params: { sessionId: "sess-admin" } },
|
|
5059
|
+
expect: { status: 404, error: notFound("sess-admin") }
|
|
5060
|
+
},
|
|
5061
|
+
{
|
|
5062
|
+
id: "session-summary-unknown",
|
|
5063
|
+
route: "GET /SASjsApi/session/{sessionId}",
|
|
5064
|
+
description: "answers the same 404 for a session that does not exist",
|
|
5065
|
+
request: { subject: ADMIN, params: { sessionId: "sess-nope" } },
|
|
5066
|
+
expect: { status: 404, error: notFound("sess-nope") }
|
|
5067
|
+
},
|
|
5068
|
+
// -------------------------------------------------------------------- log
|
|
4937
5069
|
{
|
|
4938
5070
|
id: "session-log-own",
|
|
4939
5071
|
route: "GET /SASjsApi/session/{sessionId}/log",
|
|
@@ -4951,6 +5083,21 @@ var SASjsApi = (() => {
|
|
|
4951
5083
|
request: { subject: ADMIN, params: { sessionId: "sess-silent" } },
|
|
4952
5084
|
expect: { status: 200, contents: "" }
|
|
4953
5085
|
},
|
|
5086
|
+
{
|
|
5087
|
+
id: "session-log-unauthenticated",
|
|
5088
|
+
route: "GET /SASjsApi/session/{sessionId}/log",
|
|
5089
|
+
description: "refuses a caller with no token",
|
|
5090
|
+
request: { params: { sessionId: "sess-admin" } },
|
|
5091
|
+
expect: { status: 401, error: UNAUTHORIZED }
|
|
5092
|
+
},
|
|
5093
|
+
{
|
|
5094
|
+
id: "session-log-foreign",
|
|
5095
|
+
route: "GET /SASjsApi/session/{sessionId}/log",
|
|
5096
|
+
description: "will not read another account's log",
|
|
5097
|
+
request: { subject: RESTRICTED, params: { sessionId: "sess-admin" } },
|
|
5098
|
+
expect: { status: 404, error: notFound("sess-admin") }
|
|
5099
|
+
},
|
|
5100
|
+
// ----------------------------------------------------------------- output
|
|
4954
5101
|
{
|
|
4955
5102
|
id: "session-output-own",
|
|
4956
5103
|
route: "GET /SASjsApi/session/{sessionId}/output",
|
|
@@ -4958,6 +5105,21 @@ var SASjsApi = (() => {
|
|
|
4958
5105
|
request: { subject: ADMIN, params: { sessionId: "sess-admin" } },
|
|
4959
5106
|
expect: { status: 200, contents: "<html><body>42</body></html>" }
|
|
4960
5107
|
},
|
|
5108
|
+
{
|
|
5109
|
+
id: "session-output-unauthenticated",
|
|
5110
|
+
route: "GET /SASjsApi/session/{sessionId}/output",
|
|
5111
|
+
description: "refuses a caller with no token",
|
|
5112
|
+
request: { params: { sessionId: "sess-admin" } },
|
|
5113
|
+
expect: { status: 401, error: UNAUTHORIZED }
|
|
5114
|
+
},
|
|
5115
|
+
{
|
|
5116
|
+
id: "session-output-foreign",
|
|
5117
|
+
route: "GET /SASjsApi/session/{sessionId}/output",
|
|
5118
|
+
description: "will not read another account's webout",
|
|
5119
|
+
request: { subject: RESTRICTED, params: { sessionId: "sess-admin" } },
|
|
5120
|
+
expect: { status: 404, error: notFound("sess-admin") }
|
|
5121
|
+
},
|
|
5122
|
+
// ------------------------------------------------------------------ state
|
|
4961
5123
|
{
|
|
4962
5124
|
id: "session-state-own",
|
|
4963
5125
|
route: "GET /SASjsApi/session/{sessionId}/state",
|
|
@@ -4967,6 +5129,20 @@ var SASjsApi = (() => {
|
|
|
4967
5129
|
status: 200,
|
|
4968
5130
|
body: { state: "running", pid: 4243, lineCount: 3 }
|
|
4969
5131
|
}
|
|
5132
|
+
},
|
|
5133
|
+
{
|
|
5134
|
+
id: "session-state-unauthenticated",
|
|
5135
|
+
route: "GET /SASjsApi/session/{sessionId}/state",
|
|
5136
|
+
description: "refuses a caller with no token",
|
|
5137
|
+
request: { params: { sessionId: "sess-restricted" } },
|
|
5138
|
+
expect: { status: 401, error: UNAUTHORIZED }
|
|
5139
|
+
},
|
|
5140
|
+
{
|
|
5141
|
+
id: "session-state-foreign",
|
|
5142
|
+
route: "GET /SASjsApi/session/{sessionId}/state",
|
|
5143
|
+
description: "will not report another account's session state",
|
|
5144
|
+
request: { subject: RESTRICTED, params: { sessionId: "sess-admin" } },
|
|
5145
|
+
expect: { status: 404, error: notFound("sess-admin") }
|
|
4970
5146
|
}
|
|
4971
5147
|
];
|
|
4972
5148
|
|
|
@@ -6188,7 +6364,7 @@ var SASjsApi = (() => {
|
|
|
6188
6364
|
|
|
6189
6365
|
// src/session/service.ts
|
|
6190
6366
|
var createSessionService = (store, host) => {
|
|
6191
|
-
const
|
|
6367
|
+
const notFound2 = (id) => `Session with ID '${id}' was not found.`;
|
|
6192
6368
|
const visible = async (caller, id) => {
|
|
6193
6369
|
if (!caller) throw new ApiError(401, "Unauthorized");
|
|
6194
6370
|
const reader = await store.userById(
|
|
@@ -6196,10 +6372,10 @@ var SASjsApi = (() => {
|
|
|
6196
6372
|
);
|
|
6197
6373
|
if (!reader || !reader.isActive) throw new ApiError(401, "Unauthorized");
|
|
6198
6374
|
const session = await host.session(id);
|
|
6199
|
-
if (!session) throw new ApiError(404,
|
|
6375
|
+
if (!session) throw new ApiError(404, notFound2(id));
|
|
6200
6376
|
const owner = session.owner;
|
|
6201
6377
|
if (owner !== reader.uid && owner !== reader.username && !reader.isAdmin)
|
|
6202
|
-
throw new ApiError(404,
|
|
6378
|
+
throw new ApiError(404, notFound2(id));
|
|
6203
6379
|
return session;
|
|
6204
6380
|
};
|
|
6205
6381
|
return {
|
|
@@ -6210,19 +6386,19 @@ var SASjsApi = (() => {
|
|
|
6210
6386
|
async state(caller, id) {
|
|
6211
6387
|
await visible(caller, id);
|
|
6212
6388
|
const state = await host.state(id);
|
|
6213
|
-
if (!state) throw new ApiError(404,
|
|
6389
|
+
if (!state) throw new ApiError(404, notFound2(id));
|
|
6214
6390
|
return state;
|
|
6215
6391
|
},
|
|
6216
6392
|
async log(caller, id) {
|
|
6217
6393
|
await visible(caller, id);
|
|
6218
6394
|
const log = await host.log(id);
|
|
6219
|
-
if (log === void 0) throw new ApiError(404,
|
|
6395
|
+
if (log === void 0) throw new ApiError(404, notFound2(id));
|
|
6220
6396
|
return log;
|
|
6221
6397
|
},
|
|
6222
6398
|
async output(caller, id) {
|
|
6223
6399
|
await visible(caller, id);
|
|
6224
6400
|
const output = await host.output(id);
|
|
6225
|
-
if (output === void 0) throw new ApiError(404,
|
|
6401
|
+
if (output === void 0) throw new ApiError(404, notFound2(id));
|
|
6226
6402
|
return output;
|
|
6227
6403
|
}
|
|
6228
6404
|
};
|
|
@@ -6303,6 +6479,7 @@ var SASjsApi = (() => {
|
|
|
6303
6479
|
),
|
|
6304
6480
|
identity: createIdentityService(identity),
|
|
6305
6481
|
records: createRecordService(identity),
|
|
6482
|
+
registry: new MemoryAppStreamRegistry(),
|
|
6306
6483
|
session: createSessionService(identity, new MemorySessionHost())
|
|
6307
6484
|
};
|
|
6308
6485
|
};
|
|
@@ -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,6 +4,7 @@ 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");
|
|
9
10
|
const service_js_3 = require("../session/service.js");
|
|
@@ -29,6 +30,7 @@ const servicesFor = (testCase) => {
|
|
|
29
30
|
: new memory_store_js_1.MemoryDriveStore(testCase.seed || {})),
|
|
30
31
|
identity: (0, service_js_2.createIdentityService)(identity),
|
|
31
32
|
records: (0, records_js_1.createRecordService)(identity),
|
|
33
|
+
registry: new registry_js_1.MemoryAppStreamRegistry(),
|
|
32
34
|
session: (0, service_js_3.createSessionService)(identity, new memory_host_js_1.MemorySessionHost())
|
|
33
35
|
};
|
|
34
36
|
};
|
|
@@ -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. */
|
|
@@ -175,6 +176,13 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
175
176
|
await drive.deploy(String(body.appLoc ?? ''), body.fileTree);
|
|
176
177
|
return { status: 200, body: deploy_js_1.DEPLOY_SUCCESS };
|
|
177
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
|
+
};
|
|
178
186
|
case 'GET /SASjsApi/drive/filetree':
|
|
179
187
|
return { status: 200, body: await drive.tree(query._folderPath) };
|
|
180
188
|
// ------------------------------------------------------------- sessions
|
|
@@ -388,6 +396,7 @@ const dispatch = async (services, routeId, request = {}) => {
|
|
|
388
396
|
exports.dispatch = dispatch;
|
|
389
397
|
/** Whether a route is one `dispatch` can answer today. */
|
|
390
398
|
exports.HANDLED_ROUTES = [
|
|
399
|
+
'DELETE /SASjsApi/drive/deploy',
|
|
391
400
|
'GET /SASjsApi/session/{sessionId}',
|
|
392
401
|
'GET /SASjsApi/session/{sessionId}/log',
|
|
393
402
|
'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
|
},
|
|
@@ -31,7 +31,6 @@ const HOST_CONFIG = "The body is the host's own configuration - ports, run times
|
|
|
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
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
34
|
const success = { status: 'success' };
|
|
36
35
|
const user = {
|
|
37
36
|
uid: '',
|
|
@@ -165,10 +164,7 @@ exports.OVERLAY = {
|
|
|
165
164
|
},
|
|
166
165
|
// `404` is a pair - the registry name and the appLoc it was published from -
|
|
167
166
|
// 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
|
-
},
|
|
167
|
+
'DELETE /SASjsApi/drive/deploy': { implemented: true },
|
|
172
168
|
// Group - members and the groups themselves
|
|
173
169
|
'GET /SASjsApi/group': { implemented: true },
|
|
174
170
|
'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;AAI5C,eAAO,MAAM,aAAa,EAAE,UAAU,EA6CrC,CAAA"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const SUBJECT = { userId: 'user-admin', clientId: 'web_app' };
|
|
2
|
+
export const DEPLOY_CORPUS = [
|
|
3
|
+
{
|
|
4
|
+
id: 'deploy-unpublish',
|
|
5
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
6
|
+
description: 'removes a published app stream and answers the status',
|
|
7
|
+
request: {
|
|
8
|
+
subject: SUBJECT,
|
|
9
|
+
body: { streamServiceName: 'myApp', appLoc: '/Public/myApp' }
|
|
10
|
+
},
|
|
11
|
+
expect: { status: 200, body: { status: 'success' } }
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'deploy-unpublish-other-name',
|
|
15
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
16
|
+
description: 'removes the app by its folder when the name has been renamed',
|
|
17
|
+
request: {
|
|
18
|
+
subject: SUBJECT,
|
|
19
|
+
body: { streamServiceName: 'renamed-app', appLoc: '/Public/myApp' }
|
|
20
|
+
},
|
|
21
|
+
expect: { status: 200, body: { status: 'success' } }
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'deploy-unpublish-no-stream-name',
|
|
25
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
26
|
+
description: 'refuses a body that does not say which stream to remove',
|
|
27
|
+
request: { subject: SUBJECT, body: { appLoc: '/Public/myApp' } },
|
|
28
|
+
expect: { status: 400, error: '"streamServiceName" is required' }
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'deploy-unpublish-no-app-loc',
|
|
32
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
33
|
+
description: 'refuses a body that does not say which folder to remove',
|
|
34
|
+
request: { subject: SUBJECT, body: { streamServiceName: 'myApp' } },
|
|
35
|
+
expect: { status: 400, error: '"appLoc" is required' }
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'deploy-unpublish-unknown-folder',
|
|
39
|
+
route: 'DELETE /SASjsApi/drive/deploy',
|
|
40
|
+
description: 'answers 404 for a folder nothing is published at',
|
|
41
|
+
request: {
|
|
42
|
+
subject: SUBJECT,
|
|
43
|
+
body: { streamServiceName: 'myApp', appLoc: '/Public/not-an-app' }
|
|
44
|
+
},
|
|
45
|
+
expect: { status: 404, error: 'No such app stream entry.' }
|
|
46
|
+
}
|
|
47
|
+
];
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/drive/registry.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CAC5C;AAED,MAAM,WAAW,aAAa;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAC7B,UAAU,iBAAiB,EAC3B,MAAM,aAAa,GAAG,SAAS,KAC9B,OAAO,CAAC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAU/B,CAAA;AAED;;;GAGG;AACH,qBAAa,uBAAwB,YAAW,iBAAiB;IAC/D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAa;gBAEzB,OAAO,GAAE,MAAM,EAAsB;IAI3C,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAGlD"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ApiError } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* `DELETE /SASjsApi/drive/deploy`.
|
|
4
|
+
*
|
|
5
|
+
* The document requires both body values, so both are checked - a caller that
|
|
6
|
+
* names a stream but not the folder has not said what to remove. Only the folder
|
|
7
|
+
* reaches the registry, because that is what identifies the app.
|
|
8
|
+
*
|
|
9
|
+
* The checks are here rather than in the transport because this is the one
|
|
10
|
+
* implementation of the operation: a host calling it directly gets the same
|
|
11
|
+
* refusals in the same order, and with the same messages the document states.
|
|
12
|
+
*/
|
|
13
|
+
export const unpublishAppStream = async (registry, body) => {
|
|
14
|
+
if (!body?.streamServiceName)
|
|
15
|
+
throw new ApiError(400, '"streamServiceName" is required');
|
|
16
|
+
if (!body.appLoc)
|
|
17
|
+
throw new ApiError(400, '"appLoc" is required');
|
|
18
|
+
if (!(await registry.unpublish(body.appLoc)))
|
|
19
|
+
throw new ApiError(404, 'No such app stream entry.');
|
|
20
|
+
return { status: 'success' };
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The registry the corpus and the package's own tests read: one published app,
|
|
24
|
+
* identified by its folder alone.
|
|
25
|
+
*/
|
|
26
|
+
export class MemoryAppStreamRegistry {
|
|
27
|
+
constructor(folders = ['/Public/myApp']) {
|
|
28
|
+
this.folders = new Set(folders);
|
|
29
|
+
}
|
|
30
|
+
async unpublish(appLoc) {
|
|
31
|
+
return this.folders.delete(appLoc);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { DriveService } from '../drive/service.js';
|
|
|
2
2
|
import type { IdentityService } from '../identity/service.js';
|
|
3
3
|
import type { RecordService } from '../identity/records.js';
|
|
4
4
|
import type { SessionService } from '../session/service.js';
|
|
5
|
+
import type { AppStreamRegistry } from '../drive/registry.js';
|
|
5
6
|
import type { CodeService } from '../code/service.js';
|
|
6
7
|
import type { Subject } from '../identity/types.js';
|
|
7
8
|
export interface ApiRequest {
|
|
@@ -51,6 +52,11 @@ export interface Services {
|
|
|
51
52
|
* answers 501 for these four, naming the capability.
|
|
52
53
|
*/
|
|
53
54
|
session?: SessionService;
|
|
55
|
+
/**
|
|
56
|
+
* The app streams a host has published. Absent, `DELETE /drive/deploy`
|
|
57
|
+
* answers 501 - the package cannot know a host's stream registry.
|
|
58
|
+
*/
|
|
59
|
+
registry?: AppStreamRegistry;
|
|
54
60
|
}
|
|
55
61
|
export declare const dispatch: (services: Services, routeId: string, request?: ApiRequest) => Promise<ApiResponse>;
|
|
56
62
|
/** Whether a route is one `dispatch` can answer today. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/host/dispatch.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAOnD,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,oFAAoF;IACpF,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAA;IACtD;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,mBAAmB;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAC/B;AAED,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,KAAK,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,yFAAyF;IACzF,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/host/dispatch.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAE7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAOnD,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,oFAAoF;IACpF,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAA;IACtD;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,mBAAmB;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAC/B;AAED,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,KAAK,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,yFAAyF;IACzF,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,cAAc,CAAA;IACxB;;;OAGG;IACH,QAAQ,CAAC,EAAE,iBAAiB,CAAA;CAC7B;AA2CD,eAAO,MAAM,QAAQ,GACnB,UAAU,QAAQ,EAClB,SAAS,MAAM,EACf,UAAS,UAAe,KACvB,OAAO,CAAC,WAAW,CAycrB,CAAA;AAED,0DAA0D;AAC1D,eAAO,MAAM,cAAc,EAAE,MAAM,EAiDlC,CAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { routeById } from '../routes/index.js';
|
|
2
|
+
import { unpublishAppStream } from '../drive/registry.js';
|
|
2
3
|
import { ApiError } from '../types.js';
|
|
3
4
|
import { DEPLOY_SUCCESS } from '../drive/deploy.js';
|
|
4
5
|
/** The defaults sasjs/server's Client schema applies to an omitted lifetime. */
|
|
@@ -172,6 +173,13 @@ export const dispatch = async (services, routeId, request = {}) => {
|
|
|
172
173
|
await drive.deploy(String(body.appLoc ?? ''), body.fileTree);
|
|
173
174
|
return { status: 200, body: DEPLOY_SUCCESS };
|
|
174
175
|
}
|
|
176
|
+
case 'DELETE /SASjsApi/drive/deploy':
|
|
177
|
+
if (!services.registry)
|
|
178
|
+
throw new ApiError(501, `This host has no app stream registry, so it cannot answer ${routeId}.`);
|
|
179
|
+
return {
|
|
180
|
+
status: 200,
|
|
181
|
+
body: await unpublishAppStream(services.registry, body)
|
|
182
|
+
};
|
|
175
183
|
case 'GET /SASjsApi/drive/filetree':
|
|
176
184
|
return { status: 200, body: await drive.tree(query._folderPath) };
|
|
177
185
|
// ------------------------------------------------------------- sessions
|
|
@@ -384,6 +392,7 @@ export const dispatch = async (services, routeId, request = {}) => {
|
|
|
384
392
|
};
|
|
385
393
|
/** Whether a route is one `dispatch` can answer today. */
|
|
386
394
|
export const HANDLED_ROUTES = [
|
|
395
|
+
'DELETE /SASjsApi/drive/deploy',
|
|
387
396
|
'GET /SASjsApi/session/{sessionId}',
|
|
388
397
|
'GET /SASjsApi/session/{sessionId}/log',
|
|
389
398
|
'GET /SASjsApi/session/{sessionId}/output',
|
package/build/esm/index.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export type { IdentityFixture, IdentityFixtureClient, IdentityFixtureCode, Ident
|
|
|
38
38
|
export type { IdentityClient, IdentityClientRecord, IdentityGroupRecord, IdentityPermissionRecord, IdentityStore, IdentityUser, IdentityUserRecord, NewGroup, NewPermission, NewUser, Subject, TokenPair, UserChange } from './identity/types.js';
|
|
39
39
|
export { DRIVE_CORPUS } from './corpus/drive.js';
|
|
40
40
|
export { IDENTITY_CORPUS } from './corpus/identity.js';
|
|
41
|
+
export { DEPLOY_CORPUS } from './corpus/deploy.js';
|
|
41
42
|
export { SESSION_CORPUS } from './corpus/session.js';
|
|
42
43
|
export { RECORD_CORPUS, ADMIN_SIGNED_IN, PROVIDER_MANAGED } from './corpus/records.js';
|
|
43
44
|
export type { CorpusCase } from './corpus/drive.js';
|
package/build/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,MAAM,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EAClB,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAG7D,cAAc,2BAA2B,CAAA;AACzC,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,YAAY,EACb,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,aAAa,EACb,OAAO,EACP,OAAO,EACP,SAAS,EACT,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EACL,aAAa,EACb,eAAe,EACf,gBAAgB,EACjB,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,MAAM,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EAClB,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAG7D,cAAc,2BAA2B,CAAA;AACzC,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,YAAY,EACb,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,aAAa,EACb,OAAO,EACP,OAAO,EACP,SAAS,EACT,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EACL,aAAa,EACb,eAAe,EACf,gBAAgB,EACjB,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA"}
|
package/build/esm/index.js
CHANGED
|
@@ -30,6 +30,7 @@ export { MemoryIdentityStore } from './identity/memory-store.js';
|
|
|
30
30
|
export { DEFAULT_IDENTITY, IDENTITY_FORMAT, parseIdentity } from './identity/fixture.js';
|
|
31
31
|
export { DRIVE_CORPUS } from './corpus/drive.js';
|
|
32
32
|
export { IDENTITY_CORPUS } from './corpus/identity.js';
|
|
33
|
+
export { DEPLOY_CORPUS } from './corpus/deploy.js';
|
|
33
34
|
export { SESSION_CORPUS } from './corpus/session.js';
|
|
34
35
|
export { RECORD_CORPUS, ADMIN_SIGNED_IN, PROVIDER_MANAGED } from './corpus/records.js';
|
|
35
36
|
export { runCase, servicesFor } from './corpus/runner.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generated.d.ts","sourceRoot":"","sources":["../../../src/routes/generated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD,eAAO,MAAM,gBAAgB,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"generated.d.ts","sourceRoot":"","sources":["../../../src/routes/generated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD,eAAO,MAAM,gBAAgB,EAAE,cAAc,EAw6D5C,CAAA"}
|
|
@@ -1298,6 +1298,14 @@ export const GENERATED_ROUTES = [
|
|
|
1298
1298
|
{
|
|
1299
1299
|
status: 200,
|
|
1300
1300
|
description: 'Ok'
|
|
1301
|
+
},
|
|
1302
|
+
{
|
|
1303
|
+
status: 401,
|
|
1304
|
+
description: 'No valid access token or session was supplied'
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
status: 404,
|
|
1308
|
+
description: 'Session with the given ID was not found.'
|
|
1301
1309
|
}
|
|
1302
1310
|
]
|
|
1303
1311
|
},
|
|
@@ -1320,6 +1328,14 @@ export const GENERATED_ROUTES = [
|
|
|
1320
1328
|
{
|
|
1321
1329
|
status: 200,
|
|
1322
1330
|
description: 'Ok'
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
status: 401,
|
|
1334
|
+
description: 'No valid access token or session was supplied'
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
status: 404,
|
|
1338
|
+
description: 'Session with the given ID was not found.'
|
|
1323
1339
|
}
|
|
1324
1340
|
]
|
|
1325
1341
|
},
|
|
@@ -1342,6 +1358,14 @@ export const GENERATED_ROUTES = [
|
|
|
1342
1358
|
{
|
|
1343
1359
|
status: 200,
|
|
1344
1360
|
description: 'Ok'
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
status: 401,
|
|
1364
|
+
description: 'No valid access token or session was supplied'
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
status: 404,
|
|
1368
|
+
description: 'Session with the given ID was not found.'
|
|
1345
1369
|
}
|
|
1346
1370
|
]
|
|
1347
1371
|
},
|
|
@@ -1364,6 +1388,14 @@ export const GENERATED_ROUTES = [
|
|
|
1364
1388
|
{
|
|
1365
1389
|
status: 200,
|
|
1366
1390
|
description: 'Ok'
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
status: 401,
|
|
1394
|
+
description: 'No valid access token or session was supplied'
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
status: 404,
|
|
1398
|
+
description: 'Session with the given ID was not found.'
|
|
1367
1399
|
}
|
|
1368
1400
|
]
|
|
1369
1401
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,yDAAyD;AACzD,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CAAC;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;
|
|
1
|
+
{"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,yDAAyD;AACzD,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CAAC;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;AAqCD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CA6MhD,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,UAAU,EAA8C,CAAA"}
|
|
@@ -28,7 +28,6 @@ const HOST_CONFIG = "The body is the host's own configuration - ports, run times
|
|
|
28
28
|
const HOST_OWNED = "The host owns this report: it describes the host's own process and files, which a browser build has no equivalent of.";
|
|
29
29
|
const WEB_BUNDLE = 'The host serves its own web bundle; there is no API decision here for this package to answer.';
|
|
30
30
|
const ARCHIVE = "Expanding the uploaded archive is the host's transport work - multer and a zip library on the server, `DecompressionStream` in a worker - and the host hands this package the tree it produces, which is the same tree `POST /SASjsApi/drive/deploy` takes.";
|
|
31
|
-
const UNPUBLISH = 'Unpublishing inverts the deploy route, so the two land together.';
|
|
32
31
|
const success = { status: 'success' };
|
|
33
32
|
const user = {
|
|
34
33
|
uid: '',
|
|
@@ -162,10 +161,7 @@ export const OVERLAY = {
|
|
|
162
161
|
},
|
|
163
162
|
// `404` is a pair - the registry name and the appLoc it was published from -
|
|
164
163
|
// that matches no entry, and the drive's content is deliberately left alone.
|
|
165
|
-
'DELETE /SASjsApi/drive/deploy': {
|
|
166
|
-
implemented: false,
|
|
167
|
-
reason: UNPUBLISH
|
|
168
|
-
},
|
|
164
|
+
'DELETE /SASjsApi/drive/deploy': { implemented: true },
|
|
169
165
|
// Group - members and the groups themselves
|
|
170
166
|
'GET /SASjsApi/group': { implemented: true },
|
|
171
167
|
'GET /SASjsApi/group/{uid}': { implemented: true },
|
package/package.json
CHANGED