@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 CHANGED
@@ -1,4 +1,4 @@
1
- /* @sasjs/api 0.11.0 - the SASjs Server API, for a service worker. https://github.com/sasjs/api */
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 notFound = (id) => `Session with ID '${id}' was not found.`;
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, notFound(id));
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, notFound(id));
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, notFound(id));
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, notFound(id));
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, notFound(id));
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',
@@ -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;AAWnD,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;IAC9B,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,UAAU,UAAU,KAAG,QAmBlD,CAAA;AAkBD;;;;GAIG;AACH,eAAO,MAAM,OAAO,GAClB,UAAU,UAAU,EACpB,WAAU,QAAgC,KACzC,OAAO,CAAC,UAAU,CAepB,CAAA"}
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
- * Only the `200` answers are here, and that is a statement about the document
5
- * rather than about the routes: sasjs/server documents these four with a `200`
6
- * and nothing else, so `spec/corpus-parity.spec.ts` refuses a case that pins a
7
- * refusal. Their `401` and the `404` that covers both "no such session" and
8
- * "not yours" are pinned in `spec/session.spec.ts` instead, which the parity
9
- * gate does not govern.
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
- * The routes are in `documentRefusals.baseline.json` upstream; when they carry
12
- * their `@Response(401)` and `@Response(404)`, those cases move here.
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;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAK5C,eAAO,MAAM,cAAc,EAAE,UAAU,EAmEtC,CAAA"}
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;CACzB;AA2CD,eAAO,MAAM,QAAQ,GACnB,UAAU,QAAQ,EAClB,SAAS,MAAM,EACf,UAAS,UAAe,KACvB,OAAO,CAAC,WAAW,CA8brB,CAAA;AAED,0DAA0D;AAC1D,eAAO,MAAM,cAAc,EAAE,MAAM,EAgDlC,CAAA"}
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',
@@ -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';
@@ -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"}
@@ -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,EAw4D5C,CAAA"}
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;AAwCD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAgNhD,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,UAAU,EAA8C,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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sasjs/api",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "The SASjs Server API, described once - a generated route table, the services behind it, and the conformance corpus both hosts run",
5
5
  "license": "MIT",
6
6
  "author": "SASjs",