@sasjs/api 0.4.0 → 0.6.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/README.md +12 -13
- package/build/browser.js +515 -54
- package/build/cjs/corpus/drive.js +119 -0
- package/build/cjs/corpus/runner.js +18 -2
- package/build/cjs/drive/deploy.js +270 -0
- package/build/cjs/drive/service.js +5 -0
- package/build/cjs/host/dispatch.js +15 -2
- package/build/cjs/index.js +10 -1
- package/build/cjs/routes/generated.js +8 -0
- package/build/cjs/routes/index.js +8 -2
- package/build/cjs/routes/overlay.js +181 -40
- package/build/cjs/types.js +11 -1
- package/build/esm/corpus/drive.d.ts +6 -0
- package/build/esm/corpus/drive.d.ts.map +1 -1
- package/build/esm/corpus/drive.js +119 -0
- package/build/esm/corpus/runner.d.ts.map +1 -1
- package/build/esm/corpus/runner.js +18 -2
- package/build/esm/drive/deploy.d.ts +143 -0
- package/build/esm/drive/deploy.d.ts.map +1 -0
- package/build/esm/drive/deploy.js +262 -0
- package/build/esm/drive/service.d.ts +8 -0
- package/build/esm/drive/service.d.ts.map +1 -1
- package/build/esm/drive/service.js +5 -0
- package/build/esm/host/dispatch.d.ts.map +1 -1
- package/build/esm/host/dispatch.js +15 -2
- package/build/esm/index.d.ts +2 -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 +8 -0
- package/build/esm/routes/index.d.ts.map +1 -1
- package/build/esm/routes/index.js +8 -2
- package/build/esm/routes/overlay.d.ts +26 -3
- package/build/esm/routes/overlay.d.ts.map +1 -1
- package/build/esm/routes/overlay.js +181 -40
- package/build/esm/types.d.ts +26 -1
- package/build/esm/types.d.ts.map +1 -1
- package/build/esm/types.js +11 -1
- package/package.json +1 -1
package/build/browser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* @sasjs/api 0.
|
|
1
|
+
/* @sasjs/api 0.6.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;
|
|
@@ -24,6 +24,9 @@ var SASjsApi = (() => {
|
|
|
24
24
|
__export(index_exports, {
|
|
25
25
|
ApiError: () => ApiError,
|
|
26
26
|
DEFAULT_IDENTITY: () => DEFAULT_IDENTITY,
|
|
27
|
+
DEPLOY_LIMITS: () => DEPLOY_LIMITS,
|
|
28
|
+
DEPLOY_PAYLOAD_BYTES: () => DEPLOY_PAYLOAD_BYTES,
|
|
29
|
+
DEPLOY_SUCCESS: () => DEPLOY_SUCCESS,
|
|
27
30
|
DRIVE_CORPUS: () => DRIVE_CORPUS,
|
|
28
31
|
HANDLED_ROUTES: () => HANDLED_ROUTES,
|
|
29
32
|
IDENTITY_CORPUS: () => IDENTITY_CORPUS,
|
|
@@ -34,14 +37,19 @@ var SASjsApi = (() => {
|
|
|
34
37
|
ROUTES: () => ROUTES,
|
|
35
38
|
TAG_CAPABILITIES: () => TAG_CAPABILITIES,
|
|
36
39
|
basename: () => basename,
|
|
40
|
+
checkDeployTree: () => checkDeployTree,
|
|
37
41
|
createDriveService: () => createDriveService,
|
|
38
42
|
createIdentityService: () => createIdentityService,
|
|
43
|
+
decodeBase64: () => decodeBase64,
|
|
44
|
+
deployTree: () => deployTree,
|
|
39
45
|
dirname: () => dirname,
|
|
40
46
|
dispatch: () => dispatch,
|
|
41
47
|
findRoute: () => findRoute,
|
|
42
48
|
hasBlockedExtension: () => hasBlockedExtension,
|
|
43
49
|
isApiError: () => isApiError,
|
|
50
|
+
isDeployTree: () => isDeployTree,
|
|
44
51
|
isRoot: () => isRoot,
|
|
52
|
+
measureDeployTree: () => measureDeployTree,
|
|
45
53
|
normaliseDrivePath: () => normaliseDrivePath,
|
|
46
54
|
parseIdentity: () => parseIdentity,
|
|
47
55
|
routeById: () => routeById,
|
|
@@ -51,9 +59,10 @@ var SASjsApi = (() => {
|
|
|
51
59
|
|
|
52
60
|
// src/types.ts
|
|
53
61
|
var ApiError = class extends Error {
|
|
54
|
-
constructor(status, message) {
|
|
62
|
+
constructor(status, message, body) {
|
|
55
63
|
super(message);
|
|
56
64
|
this.status = status;
|
|
65
|
+
this.body = body;
|
|
57
66
|
/** Told apart by marker rather than `instanceof` - see `isApiError`. */
|
|
58
67
|
this.apiError = true;
|
|
59
68
|
this.name = "ApiError";
|
|
@@ -91,6 +100,169 @@ var SASjsApi = (() => {
|
|
|
91
100
|
return { path };
|
|
92
101
|
};
|
|
93
102
|
|
|
103
|
+
// src/drive/deploy.ts
|
|
104
|
+
var DEPLOY_LIMITS = {
|
|
105
|
+
entries: 1e4,
|
|
106
|
+
totalBytes: 100 * 1024 * 1024,
|
|
107
|
+
fileBytes: 20 * 1024 * 1024
|
|
108
|
+
};
|
|
109
|
+
var DEPLOY_PAYLOAD_BYTES = 140 * 1024 * 1024;
|
|
110
|
+
var DEPLOY_SUCCESS = {
|
|
111
|
+
status: "success",
|
|
112
|
+
message: "Files deployed successfully to @sasjs/server."
|
|
113
|
+
};
|
|
114
|
+
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
115
|
+
var base64Body = (value) => value.replace(/[^A-Za-z0-9+/]/g, "");
|
|
116
|
+
var base64Length = (value) => Math.floor(base64Body(value).length * 3 / 4);
|
|
117
|
+
var decodeBase64 = (value) => {
|
|
118
|
+
const body = base64Body(value);
|
|
119
|
+
const bytes2 = new Uint8Array(Math.floor(body.length * 3 / 4));
|
|
120
|
+
let buffer = 0;
|
|
121
|
+
let bits = 0;
|
|
122
|
+
let at = 0;
|
|
123
|
+
for (const char of body) {
|
|
124
|
+
buffer = buffer << 6 | BASE64_ALPHABET.indexOf(char);
|
|
125
|
+
bits += 6;
|
|
126
|
+
if (bits >= 8) {
|
|
127
|
+
bits -= 8;
|
|
128
|
+
bytes2[at++] = buffer >> bits & 255;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return bytes2.subarray(0, at);
|
|
132
|
+
};
|
|
133
|
+
var utf8Length = (value) => {
|
|
134
|
+
let bytes2 = 0;
|
|
135
|
+
for (let at = 0; at < value.length; at++) {
|
|
136
|
+
const code = value.charCodeAt(at);
|
|
137
|
+
if (code < 128) bytes2 += 1;
|
|
138
|
+
else if (code < 2048) bytes2 += 2;
|
|
139
|
+
else if (code >= 55296 && code <= 56319 && at + 1 < value.length) {
|
|
140
|
+
bytes2 += 4;
|
|
141
|
+
at++;
|
|
142
|
+
} else bytes2 += 3;
|
|
143
|
+
}
|
|
144
|
+
return bytes2;
|
|
145
|
+
};
|
|
146
|
+
var memberName = (member) => member.type === "service" ? `${member.name}.sas` : member.name;
|
|
147
|
+
var memberBytes = (member) => member.type === "file" ? base64Length(member.code) : utf8Length(member.code);
|
|
148
|
+
var describeSize = (bytes2) => bytes2 < 1024 * 1024 ? `${bytes2} bytes` : `${Math.round(bytes2 / (1024 * 1024) * 10) / 10} MB`;
|
|
149
|
+
var emptyWalk = () => ({
|
|
150
|
+
entries: 0,
|
|
151
|
+
totalBytes: 0,
|
|
152
|
+
largest: { path: "", bytes: 0 }
|
|
153
|
+
});
|
|
154
|
+
var isSafeSegment = (name) => name !== "" && name !== "." && name !== ".." && !/[/\\]/.test(name) && // a Windows drive or a NUL byte is never a segment either
|
|
155
|
+
!/[\0]/.test(name);
|
|
156
|
+
var walk = (members, parentPath, acc) => {
|
|
157
|
+
for (const member of members) {
|
|
158
|
+
const name = memberName(member);
|
|
159
|
+
const memberPath = parentPath ? `${parentPath}/${name}` : name;
|
|
160
|
+
if (!isSafeSegment(name) && !acc.nameRefusal)
|
|
161
|
+
acc.nameRefusal = {
|
|
162
|
+
code: 400,
|
|
163
|
+
status: "failure",
|
|
164
|
+
message: `Invalid member name: '${name}'. Member names must be single path segments (no '/', '\\', '..').`
|
|
165
|
+
};
|
|
166
|
+
acc.entries++;
|
|
167
|
+
if (member.type === "folder") {
|
|
168
|
+
walk(member.members, memberPath, acc);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const bytes2 = memberBytes(member);
|
|
172
|
+
acc.totalBytes += bytes2;
|
|
173
|
+
if (bytes2 > acc.largest.bytes) acc.largest = { path: memberPath, bytes: bytes2 };
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
var measureDeployTree = (members) => {
|
|
177
|
+
const acc = emptyWalk();
|
|
178
|
+
walk(members, "", acc);
|
|
179
|
+
return {
|
|
180
|
+
entries: acc.entries,
|
|
181
|
+
totalBytes: acc.totalBytes,
|
|
182
|
+
largest: acc.largest
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
var checkDeployTree = (members, limits = DEPLOY_LIMITS) => {
|
|
186
|
+
const measured = emptyWalk();
|
|
187
|
+
walk(members, "", measured);
|
|
188
|
+
if (measured.nameRefusal) return measured.nameRefusal;
|
|
189
|
+
if (measured.entries > limits.entries)
|
|
190
|
+
return {
|
|
191
|
+
code: 413,
|
|
192
|
+
status: "failure",
|
|
193
|
+
message: `The file tree exceeds the deploy limit: ${measured.entries} entries (maximum ${limits.entries}).`
|
|
194
|
+
};
|
|
195
|
+
if (measured.totalBytes > limits.totalBytes)
|
|
196
|
+
return {
|
|
197
|
+
code: 413,
|
|
198
|
+
status: "failure",
|
|
199
|
+
message: `The file tree exceeds the deploy limit: ${describeSize(measured.totalBytes)} (maximum ${describeSize(limits.totalBytes)}).`
|
|
200
|
+
};
|
|
201
|
+
if (measured.largest.bytes > limits.fileBytes)
|
|
202
|
+
return {
|
|
203
|
+
code: 413,
|
|
204
|
+
status: "failure",
|
|
205
|
+
message: `A file in the tree exceeds the deploy limit: ${measured.largest.path} is ${describeSize(measured.largest.bytes)} (maximum ${describeSize(limits.fileBytes)} per file).`
|
|
206
|
+
};
|
|
207
|
+
return null;
|
|
208
|
+
};
|
|
209
|
+
var isDeployTree = (value) => {
|
|
210
|
+
if (!value || typeof value !== "object") return false;
|
|
211
|
+
const members = value.members;
|
|
212
|
+
return Array.isArray(members) && members.every(isDeployMember);
|
|
213
|
+
};
|
|
214
|
+
var isDeployMember = (value) => {
|
|
215
|
+
if (!value || typeof value !== "object") return false;
|
|
216
|
+
const member = value;
|
|
217
|
+
if (typeof member.name !== "string") return false;
|
|
218
|
+
if (member.type === "folder")
|
|
219
|
+
return Array.isArray(member.members) && member.members.every(isDeployMember);
|
|
220
|
+
if (member.type === "service" || member.type === "file")
|
|
221
|
+
return typeof member.code === "string";
|
|
222
|
+
return false;
|
|
223
|
+
};
|
|
224
|
+
var deployTree = async (store, appLoc, tree, limits = DEPLOY_LIMITS) => {
|
|
225
|
+
if (!isDeployTree(tree))
|
|
226
|
+
throw new ApiError(400, "Provided not supported data format.", {
|
|
227
|
+
status: "failure",
|
|
228
|
+
message: "Provided not supported data format."
|
|
229
|
+
});
|
|
230
|
+
const root = normaliseDrivePath(appLoc);
|
|
231
|
+
if (root === null)
|
|
232
|
+
throw new ApiError(400, "appLoc cannot be outside drive.", {
|
|
233
|
+
status: "failure",
|
|
234
|
+
message: "appLoc cannot be outside drive."
|
|
235
|
+
});
|
|
236
|
+
const refusal = checkDeployTree(tree.members, limits);
|
|
237
|
+
if (refusal)
|
|
238
|
+
throw new ApiError(refusal.code, refusal.message, {
|
|
239
|
+
status: refusal.status,
|
|
240
|
+
message: refusal.message
|
|
241
|
+
});
|
|
242
|
+
try {
|
|
243
|
+
await writeMembers(store, tree.members, root);
|
|
244
|
+
} catch {
|
|
245
|
+
throw new ApiError(500, "Deployment failed!", {
|
|
246
|
+
status: "failure",
|
|
247
|
+
message: "Deployment failed!"
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
var writeMembers = async (store, members, parentPath) => {
|
|
252
|
+
for (const member of members) {
|
|
253
|
+
const path = `${parentPath}/${memberName(member)}`;
|
|
254
|
+
if (member.type === "folder") {
|
|
255
|
+
await store.mkdirp(path);
|
|
256
|
+
await writeMembers(store, member.members, path);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
await store.write(
|
|
260
|
+
path,
|
|
261
|
+
member.type === "file" ? decodeBase64(member.code) : member.code
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
94
266
|
// src/drive/service.ts
|
|
95
267
|
var filePath = (input, outside) => {
|
|
96
268
|
const checked = checkFilePath(input);
|
|
@@ -226,6 +398,10 @@ var SASjsApi = (() => {
|
|
|
226
398
|
const stat = await store.stat(path);
|
|
227
399
|
if (!stat.directory) throw new ApiError(400, "Not a Folder.");
|
|
228
400
|
return { status: "success", tree: await treeOf(path) };
|
|
401
|
+
},
|
|
402
|
+
async deploy(appLoc, tree) {
|
|
403
|
+
await deployTree(store, appLoc, tree);
|
|
404
|
+
return { status: "success" };
|
|
229
405
|
}
|
|
230
406
|
};
|
|
231
407
|
};
|
|
@@ -816,6 +992,10 @@ var SASjsApi = (() => {
|
|
|
816
992
|
status: 400,
|
|
817
993
|
description: "Invalid Format"
|
|
818
994
|
},
|
|
995
|
+
{
|
|
996
|
+
status: 413,
|
|
997
|
+
description: "File tree exceeds the deploy limit"
|
|
998
|
+
},
|
|
819
999
|
{
|
|
820
1000
|
status: 500,
|
|
821
1001
|
description: "Execution Error"
|
|
@@ -846,6 +1026,10 @@ var SASjsApi = (() => {
|
|
|
846
1026
|
status: 400,
|
|
847
1027
|
description: "Invalid Format"
|
|
848
1028
|
},
|
|
1029
|
+
{
|
|
1030
|
+
status: 413,
|
|
1031
|
+
description: "File tree exceeds the deploy limit"
|
|
1032
|
+
},
|
|
849
1033
|
{
|
|
850
1034
|
status: 500,
|
|
851
1035
|
description: "Execution Error"
|
|
@@ -2070,6 +2254,16 @@ var SASjsApi = (() => {
|
|
|
2070
2254
|
Client: ["identity"],
|
|
2071
2255
|
Web: ["identity"]
|
|
2072
2256
|
};
|
|
2257
|
+
var IDENTITY_RECORDS = "The identity service answers the sign-in flow. User, group, permission rule, client and provider records need read and write methods on the same store, which this package does not have yet.";
|
|
2258
|
+
var EDITOR = "Linting and formatting come from @sasjs/lint, which this package does not depend on. They land with the code routes so an editor has one wiring.";
|
|
2259
|
+
var RUNTIME = "Starting a program needs a runtime port, which this package does not have: a host decides which languages it holds, and answers this itself until then.";
|
|
2260
|
+
var SESSION_RECORD = "A triggered session's record, log, output and state belong to the runtime that ran the program, and this package has no runtime port.";
|
|
2261
|
+
var HOST_CONFIG = "The body is the host's own configuration - ports, run times and providers - which a mock states for itself and this package cannot know.";
|
|
2262
|
+
var PERMISSIONS = "The route inventory is the host's permission model, which this package does not hold.";
|
|
2263
|
+
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.";
|
|
2264
|
+
var WEB_BUNDLE = "The host serves its own web bundle; there is no API decision here for this package to answer.";
|
|
2265
|
+
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.";
|
|
2266
|
+
var UNPUBLISH = "Unpublishing inverts the deploy route, so the two land together.";
|
|
2073
2267
|
var success = { status: "success" };
|
|
2074
2268
|
var user = {
|
|
2075
2269
|
uid: "",
|
|
@@ -2079,11 +2273,61 @@ var SASjsApi = (() => {
|
|
|
2079
2273
|
needsToUpdatePassword: false
|
|
2080
2274
|
};
|
|
2081
2275
|
var OVERLAY = {
|
|
2082
|
-
//
|
|
2083
|
-
|
|
2084
|
-
"
|
|
2085
|
-
|
|
2086
|
-
|
|
2276
|
+
// Web - the sign-in pages the identity service answers, and the bundle itself
|
|
2277
|
+
"GET /": { implemented: false, reason: WEB_BUNDLE },
|
|
2278
|
+
"POST /SASLogon/login": {
|
|
2279
|
+
implemented: true,
|
|
2280
|
+
// The lockout and the provider failure are answered before the package is
|
|
2281
|
+
// consulted (routes/web/web.ts in sasjs/server).
|
|
2282
|
+
hostResponses: [429, 500],
|
|
2283
|
+
responses: { 200: { loggedIn: true, user } }
|
|
2284
|
+
},
|
|
2285
|
+
"POST /SASLogon/authorize": {
|
|
2286
|
+
implemented: true,
|
|
2287
|
+
responses: { 200: { code: "" } }
|
|
2288
|
+
},
|
|
2289
|
+
"GET /SASLogon/logout": {
|
|
2290
|
+
implemented: true,
|
|
2291
|
+
hostResponses: [500],
|
|
2292
|
+
responses: { 200: "OK!" }
|
|
2293
|
+
},
|
|
2294
|
+
// Admin - the host's own process and files
|
|
2295
|
+
"GET /SASjsApi/admin/diagnostics": { implemented: false, reason: HOST_OWNED },
|
|
2296
|
+
"GET /SASjsApi/admin/logs": { implemented: false, reason: HOST_OWNED },
|
|
2297
|
+
"GET /SASjsApi/admin/logs/{fileName}": {
|
|
2298
|
+
implemented: false,
|
|
2299
|
+
reason: HOST_OWNED
|
|
2300
|
+
},
|
|
2301
|
+
// Auth - the token flow, plus a password change the identity store cannot write
|
|
2302
|
+
"POST /SASjsApi/auth/token": {
|
|
2303
|
+
implemented: true,
|
|
2304
|
+
responses: { 200: { accessToken: "", refreshToken: "" } }
|
|
2305
|
+
},
|
|
2306
|
+
"POST /SASjsApi/auth/refresh": {
|
|
2307
|
+
implemented: true,
|
|
2308
|
+
responses: { 200: { accessToken: "", refreshToken: "" } }
|
|
2309
|
+
},
|
|
2310
|
+
"POST /SASjsApi/auth/logout": { implemented: true },
|
|
2311
|
+
"PATCH /SASjsApi/auth/updatePassword": {
|
|
2312
|
+
implemented: false,
|
|
2313
|
+
reason: IDENTITY_RECORDS
|
|
2314
|
+
},
|
|
2315
|
+
// Auth Config - the providers a host was started with, and an LDAP import
|
|
2316
|
+
"GET /SASjsApi/authConfig": { implemented: false, reason: HOST_CONFIG },
|
|
2317
|
+
"POST /SASjsApi/authConfig/synchroniseWithLDAP": {
|
|
2318
|
+
implemented: false,
|
|
2319
|
+
reason: IDENTITY_RECORDS
|
|
2320
|
+
},
|
|
2321
|
+
// Client - the OAuth clients the identity store already holds for sign-in
|
|
2322
|
+
"GET /SASjsApi/client": { implemented: false, reason: IDENTITY_RECORDS },
|
|
2323
|
+
"POST /SASjsApi/client": { implemented: false, reason: IDENTITY_RECORDS },
|
|
2324
|
+
// Code - the editor's three calls, and the runtime one
|
|
2325
|
+
"POST /SASjsApi/code/execute": { implemented: false, reason: RUNTIME },
|
|
2326
|
+
"POST /SASjsApi/code/trigger": { implemented: false, reason: RUNTIME },
|
|
2327
|
+
"POST /SASjsApi/code/lint": { implemented: false, reason: EDITOR },
|
|
2328
|
+
"POST /SASjsApi/code/format": { implemented: false, reason: EDITOR },
|
|
2329
|
+
// Drive - the file and folder rules, and the three deploy routes
|
|
2330
|
+
// The body is the file itself, so there is nothing to annotate
|
|
2087
2331
|
"GET /SASjsApi/drive/file": { implemented: true },
|
|
2088
2332
|
"POST /SASjsApi/drive/file": {
|
|
2089
2333
|
implemented: true,
|
|
@@ -2121,60 +2365,137 @@ var SASjsApi = (() => {
|
|
|
2121
2365
|
implemented: true,
|
|
2122
2366
|
responses: { 200: { status: "success", tree: {} } }
|
|
2123
2367
|
},
|
|
2124
|
-
// The
|
|
2125
|
-
//
|
|
2126
|
-
//
|
|
2368
|
+
// The deploy routes answer an ENVELOPE, unlike the file routes above: their
|
|
2369
|
+
// `400` is `{status:'failure', message}` rather than the message as text, and
|
|
2370
|
+
// `413` is the cap refusing the tree before anything is written.
|
|
2371
|
+
// `"appLoc" is required` and `"fileTree" is required` are the other `400` this
|
|
2372
|
+
// route sends - a missing body property, worded by the table's own
|
|
2373
|
+
// validation - so the body annotated here is the substantial one.
|
|
2127
2374
|
"POST /SASjsApi/drive/deploy": {
|
|
2128
|
-
implemented: false
|
|
2129
|
-
},
|
|
2130
|
-
"POST /SASjsApi/drive/deploy/upload": {
|
|
2131
|
-
implemented: false
|
|
2132
|
-
},
|
|
2133
|
-
// Unpublishing is the counterpart of the two above and lands with them: the
|
|
2134
|
-
// document in sasjs/server states it (sasjs/server#543), so a host may answer
|
|
2135
|
-
// it in the meantime, but this package cannot until the publish side it would
|
|
2136
|
-
// invert is implemented here. The statuses are the document's; `404` is a pair
|
|
2137
|
-
// - the registry name and the appLoc it was published from - that matches no
|
|
2138
|
-
// entry, and the drive's content is deliberately left alone.
|
|
2139
|
-
"DELETE /SASjsApi/drive/deploy": {
|
|
2140
|
-
implemented: false
|
|
2141
|
-
},
|
|
2142
|
-
// The lockout and the provider failure are answered before the package is
|
|
2143
|
-
// consulted (routes/web/web.ts in sasjs/server).
|
|
2144
|
-
"POST /SASLogon/login": {
|
|
2145
|
-
implemented: true,
|
|
2146
|
-
hostResponses: [429, 500],
|
|
2147
|
-
responses: { 200: { loggedIn: true, user } }
|
|
2148
|
-
},
|
|
2149
|
-
// The sign-in flow. The statuses are the document's; these entries state the
|
|
2150
|
-
// body a client keys on, and that this package answers the operation.
|
|
2151
|
-
"POST /SASjsApi/auth/token": {
|
|
2152
|
-
implemented: true,
|
|
2153
|
-
responses: { 200: { accessToken: "", refreshToken: "" } }
|
|
2154
|
-
},
|
|
2155
|
-
"POST /SASjsApi/auth/refresh": {
|
|
2156
|
-
implemented: true,
|
|
2157
|
-
responses: { 200: { accessToken: "", refreshToken: "" } }
|
|
2158
|
-
},
|
|
2159
|
-
"POST /SASjsApi/auth/logout": { implemented: true },
|
|
2160
|
-
"POST /SASLogon/authorize": {
|
|
2161
2375
|
implemented: true,
|
|
2162
|
-
responses: {
|
|
2376
|
+
responses: {
|
|
2377
|
+
200: DEPLOY_SUCCESS,
|
|
2378
|
+
400: {
|
|
2379
|
+
status: "failure",
|
|
2380
|
+
message: "Provided not supported data format."
|
|
2381
|
+
},
|
|
2382
|
+
413: "The file tree exceeds the deploy limit.",
|
|
2383
|
+
500: { status: "failure", message: "Deployment failed!" }
|
|
2384
|
+
}
|
|
2163
2385
|
},
|
|
2164
|
-
"
|
|
2165
|
-
implemented:
|
|
2166
|
-
|
|
2167
|
-
responses: { 200: "OK!" }
|
|
2386
|
+
"POST /SASjsApi/drive/deploy/upload": {
|
|
2387
|
+
implemented: false,
|
|
2388
|
+
reason: ARCHIVE
|
|
2168
2389
|
},
|
|
2390
|
+
// `404` is a pair - the registry name and the appLoc it was published from -
|
|
2391
|
+
// that matches no entry, and the drive's content is deliberately left alone.
|
|
2392
|
+
"DELETE /SASjsApi/drive/deploy": {
|
|
2393
|
+
implemented: false,
|
|
2394
|
+
reason: UNPUBLISH
|
|
2395
|
+
},
|
|
2396
|
+
// Group - members and the groups themselves
|
|
2397
|
+
"GET /SASjsApi/group": { implemented: false, reason: IDENTITY_RECORDS },
|
|
2398
|
+
"GET /SASjsApi/group/{uid}": { implemented: false, reason: IDENTITY_RECORDS },
|
|
2399
|
+
"GET /SASjsApi/group/by/groupname/{name}": {
|
|
2400
|
+
implemented: false,
|
|
2401
|
+
reason: IDENTITY_RECORDS
|
|
2402
|
+
},
|
|
2403
|
+
"POST /SASjsApi/group": { implemented: false, reason: IDENTITY_RECORDS },
|
|
2404
|
+
"POST /SASjsApi/group/{groupUid}/{userUid}": {
|
|
2405
|
+
implemented: false,
|
|
2406
|
+
reason: IDENTITY_RECORDS
|
|
2407
|
+
},
|
|
2408
|
+
"DELETE /SASjsApi/group/{groupUid}/{userUid}": {
|
|
2409
|
+
implemented: false,
|
|
2410
|
+
reason: IDENTITY_RECORDS
|
|
2411
|
+
},
|
|
2412
|
+
"DELETE /SASjsApi/group/{uid}": {
|
|
2413
|
+
implemented: false,
|
|
2414
|
+
reason: IDENTITY_RECORDS
|
|
2415
|
+
},
|
|
2416
|
+
// Info - what the host is, and what this caller may use
|
|
2417
|
+
"GET /SASjsApi/info": { implemented: false, reason: HOST_CONFIG },
|
|
2418
|
+
"GET /SASjsApi/info/authorizedRoutes": {
|
|
2419
|
+
implemented: false,
|
|
2420
|
+
// the route inventory reports what the CALLER may use, so it needs an
|
|
2421
|
+
// identity even though the rest of the Info tag needs nothing
|
|
2422
|
+
capabilities: ["identity"],
|
|
2423
|
+
reason: PERMISSIONS
|
|
2424
|
+
},
|
|
2425
|
+
"GET /SASjsApi/info/myAuthorizedRoutes": {
|
|
2426
|
+
implemented: false,
|
|
2427
|
+
capabilities: ["identity"],
|
|
2428
|
+
reason: PERMISSIONS
|
|
2429
|
+
},
|
|
2430
|
+
// Permission - the rules the inventory above is derived from
|
|
2431
|
+
"GET /SASjsApi/permission": { implemented: false, reason: IDENTITY_RECORDS },
|
|
2432
|
+
"POST /SASjsApi/permission": { implemented: false, reason: IDENTITY_RECORDS },
|
|
2433
|
+
"PATCH /SASjsApi/permission/{uid}": {
|
|
2434
|
+
implemented: false,
|
|
2435
|
+
reason: IDENTITY_RECORDS
|
|
2436
|
+
},
|
|
2437
|
+
"DELETE /SASjsApi/permission/{uid}": {
|
|
2438
|
+
implemented: false,
|
|
2439
|
+
reason: IDENTITY_RECORDS
|
|
2440
|
+
},
|
|
2441
|
+
// Session - the caller's own session, then the record behind a triggered one
|
|
2169
2442
|
"GET /SASjsApi/session": {
|
|
2170
2443
|
implemented: true,
|
|
2171
2444
|
responses: { 200: user }
|
|
2445
|
+
},
|
|
2446
|
+
"GET /SASjsApi/session/{sessionId}": {
|
|
2447
|
+
implemented: false,
|
|
2448
|
+
reason: SESSION_RECORD
|
|
2449
|
+
},
|
|
2450
|
+
"GET /SASjsApi/session/{sessionId}/log": {
|
|
2451
|
+
implemented: false,
|
|
2452
|
+
reason: SESSION_RECORD
|
|
2453
|
+
},
|
|
2454
|
+
"GET /SASjsApi/session/{sessionId}/output": {
|
|
2455
|
+
implemented: false,
|
|
2456
|
+
reason: SESSION_RECORD
|
|
2457
|
+
},
|
|
2458
|
+
"GET /SASjsApi/session/{sessionId}/state": {
|
|
2459
|
+
implemented: false,
|
|
2460
|
+
reason: SESSION_RECORD
|
|
2461
|
+
},
|
|
2462
|
+
// STP - a stored program, on the runtime port
|
|
2463
|
+
"GET /SASjsApi/stp/execute": { implemented: false, reason: RUNTIME },
|
|
2464
|
+
"POST /SASjsApi/stp/execute": { implemented: false, reason: RUNTIME },
|
|
2465
|
+
"POST /SASjsApi/stp/trigger": { implemented: false, reason: RUNTIME },
|
|
2466
|
+
// User - the accounts the identity store already holds for sign-in
|
|
2467
|
+
"GET /SASjsApi/user": { implemented: false, reason: IDENTITY_RECORDS },
|
|
2468
|
+
"GET /SASjsApi/user/{uid}": { implemented: false, reason: IDENTITY_RECORDS },
|
|
2469
|
+
"GET /SASjsApi/user/by/username/{username}": {
|
|
2470
|
+
implemented: false,
|
|
2471
|
+
reason: IDENTITY_RECORDS
|
|
2472
|
+
},
|
|
2473
|
+
"POST /SASjsApi/user": { implemented: false, reason: IDENTITY_RECORDS },
|
|
2474
|
+
"PATCH /SASjsApi/user/{uid}": {
|
|
2475
|
+
implemented: false,
|
|
2476
|
+
reason: IDENTITY_RECORDS
|
|
2477
|
+
},
|
|
2478
|
+
"PATCH /SASjsApi/user/by/username/{username}": {
|
|
2479
|
+
implemented: false,
|
|
2480
|
+
reason: IDENTITY_RECORDS
|
|
2481
|
+
},
|
|
2482
|
+
"DELETE /SASjsApi/user/{uid}": {
|
|
2483
|
+
implemented: false,
|
|
2484
|
+
reason: IDENTITY_RECORDS
|
|
2485
|
+
},
|
|
2486
|
+
"DELETE /SASjsApi/user/by/username/{username}": {
|
|
2487
|
+
implemented: false,
|
|
2488
|
+
reason: IDENTITY_RECORDS
|
|
2172
2489
|
}
|
|
2173
2490
|
};
|
|
2174
2491
|
|
|
2175
2492
|
// src/routes/index.ts
|
|
2176
2493
|
var merge = (generated) => {
|
|
2177
|
-
const overlay = OVERLAY[generated.id]
|
|
2494
|
+
const overlay = OVERLAY[generated.id];
|
|
2495
|
+
if (!overlay)
|
|
2496
|
+
throw new Error(
|
|
2497
|
+
`src/routes/overlay.ts has no entry for ${generated.id}. Every operation states whether this package implements it and, when it does not, the reason it does not: add \`{ implemented: false, reason: '...' }\` until a handler exists.`
|
|
2498
|
+
);
|
|
2178
2499
|
const documented = new Map(
|
|
2179
2500
|
generated.responses.map((r) => [r.status, r.description])
|
|
2180
2501
|
);
|
|
@@ -2203,7 +2524,8 @@ var SASjsApi = (() => {
|
|
|
2203
2524
|
responses,
|
|
2204
2525
|
hostResponses: overlay.hostResponses || [],
|
|
2205
2526
|
capabilities: overlay.capabilities || TAG_CAPABILITIES[generated.tag] || [],
|
|
2206
|
-
implemented: overlay.implemented
|
|
2527
|
+
implemented: overlay.implemented,
|
|
2528
|
+
...overlay.implemented ? {} : { reason: overlay.reason }
|
|
2207
2529
|
};
|
|
2208
2530
|
};
|
|
2209
2531
|
var ROUTES = GENERATED_ROUTES.map(merge);
|
|
@@ -2365,10 +2687,19 @@ var SASjsApi = (() => {
|
|
|
2365
2687
|
String(request.body?.newPath || "")
|
|
2366
2688
|
)
|
|
2367
2689
|
};
|
|
2690
|
+
case "POST /SASjsApi/drive/deploy": {
|
|
2691
|
+
await drive.deploy(String(body.appLoc ?? ""), body.fileTree);
|
|
2692
|
+
return { status: 200, body: DEPLOY_SUCCESS };
|
|
2693
|
+
}
|
|
2368
2694
|
case "GET /SASjsApi/drive/filetree":
|
|
2369
2695
|
return { status: 200, body: await drive.tree(query._folderPath) };
|
|
2370
|
-
default:
|
|
2371
|
-
|
|
2696
|
+
default: {
|
|
2697
|
+
const reason = routeById(routeId)?.reason;
|
|
2698
|
+
throw new ApiError(
|
|
2699
|
+
501,
|
|
2700
|
+
`This package has no handler for ${routeId}.${reason ? ` ${reason}` : ""}`
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2372
2703
|
}
|
|
2373
2704
|
};
|
|
2374
2705
|
var HANDLED_ROUTES = [
|
|
@@ -2381,6 +2712,7 @@ var SASjsApi = (() => {
|
|
|
2381
2712
|
"DELETE /SASjsApi/drive/folder",
|
|
2382
2713
|
"POST /SASjsApi/drive/rename",
|
|
2383
2714
|
"GET /SASjsApi/drive/filetree",
|
|
2715
|
+
"POST /SASjsApi/drive/deploy",
|
|
2384
2716
|
"POST /SASjsApi/auth/token",
|
|
2385
2717
|
"POST /SASjsApi/auth/refresh",
|
|
2386
2718
|
"POST /SASjsApi/auth/logout",
|
|
@@ -2747,6 +3079,11 @@ var SASjsApi = (() => {
|
|
|
2747
3079
|
// src/corpus/drive.ts
|
|
2748
3080
|
var success2 = { status: "success" };
|
|
2749
3081
|
var bytes = (...values) => Uint8Array.from(values);
|
|
3082
|
+
var manyMembers = (count) => Array.from({ length: count }, (_, at) => ({
|
|
3083
|
+
name: `f${at}.txt`,
|
|
3084
|
+
type: "file",
|
|
3085
|
+
code: ""
|
|
3086
|
+
}));
|
|
2750
3087
|
var DRIVE_CORPUS = [
|
|
2751
3088
|
{
|
|
2752
3089
|
id: "create-file",
|
|
@@ -3036,6 +3373,119 @@ var SASjsApi = (() => {
|
|
|
3036
3373
|
}
|
|
3037
3374
|
}
|
|
3038
3375
|
},
|
|
3376
|
+
{
|
|
3377
|
+
id: "deploy-tree",
|
|
3378
|
+
route: "POST /SASjsApi/drive/deploy",
|
|
3379
|
+
description: "writes a whole tree under the appLoc it names",
|
|
3380
|
+
request: {
|
|
3381
|
+
body: {
|
|
3382
|
+
appLoc: "/Public/apps/demo",
|
|
3383
|
+
fileTree: {
|
|
3384
|
+
members: [
|
|
3385
|
+
{
|
|
3386
|
+
name: "services",
|
|
3387
|
+
type: "folder",
|
|
3388
|
+
members: [{ name: "run", type: "service", code: "%put hi;" }]
|
|
3389
|
+
},
|
|
3390
|
+
{ name: "index.html", type: "file", code: "PGgxPkhlbGxvPC9oMT4=" }
|
|
3391
|
+
]
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
},
|
|
3395
|
+
expect: {
|
|
3396
|
+
status: 200,
|
|
3397
|
+
body: {
|
|
3398
|
+
status: "success",
|
|
3399
|
+
message: "Files deployed successfully to @sasjs/server."
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
},
|
|
3403
|
+
{
|
|
3404
|
+
id: "deploy-refused-by-entry-count",
|
|
3405
|
+
route: "POST /SASjsApi/drive/deploy",
|
|
3406
|
+
description: "refuses a tree over the entry cap, naming the figure and the cap",
|
|
3407
|
+
request: {
|
|
3408
|
+
body: {
|
|
3409
|
+
appLoc: "/Public/apps/many",
|
|
3410
|
+
fileTree: { members: manyMembers(10001) }
|
|
3411
|
+
}
|
|
3412
|
+
},
|
|
3413
|
+
expect: {
|
|
3414
|
+
status: 413,
|
|
3415
|
+
body: {
|
|
3416
|
+
status: "failure",
|
|
3417
|
+
message: "The file tree exceeds the deploy limit: 10001 entries (maximum 10000)."
|
|
3418
|
+
},
|
|
3419
|
+
error: "The file tree exceeds the deploy limit: 10001 entries (maximum 10000)."
|
|
3420
|
+
}
|
|
3421
|
+
},
|
|
3422
|
+
{
|
|
3423
|
+
id: "deploy-refused-by-name",
|
|
3424
|
+
route: "POST /SASjsApi/drive/deploy",
|
|
3425
|
+
description: "refuses a member name that would escape the deployment root",
|
|
3426
|
+
request: {
|
|
3427
|
+
body: {
|
|
3428
|
+
appLoc: "/Public/apps/hostile",
|
|
3429
|
+
fileTree: {
|
|
3430
|
+
members: [{ name: "../escape", type: "service", code: "%put hi;" }]
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
},
|
|
3434
|
+
expect: {
|
|
3435
|
+
status: 400,
|
|
3436
|
+
body: {
|
|
3437
|
+
status: "failure",
|
|
3438
|
+
message: "Invalid member name: '../escape.sas'. Member names must be single path segments (no '/', '\\', '..')."
|
|
3439
|
+
},
|
|
3440
|
+
error: "Invalid member name: '../escape.sas'. Member names must be single path segments (no '/', '\\', '..')."
|
|
3441
|
+
}
|
|
3442
|
+
},
|
|
3443
|
+
{
|
|
3444
|
+
id: "deploy-refused-by-shape",
|
|
3445
|
+
route: "POST /SASjsApi/drive/deploy",
|
|
3446
|
+
description: "refuses a fileTree that is not a tree, in the routes own envelope",
|
|
3447
|
+
request: { body: { appLoc: "/Public/apps/bad", fileTree: { members: 1 } } },
|
|
3448
|
+
expect: {
|
|
3449
|
+
status: 400,
|
|
3450
|
+
body: {
|
|
3451
|
+
status: "failure",
|
|
3452
|
+
message: "Provided not supported data format."
|
|
3453
|
+
},
|
|
3454
|
+
error: "Provided not supported data format."
|
|
3455
|
+
}
|
|
3456
|
+
},
|
|
3457
|
+
{
|
|
3458
|
+
id: "deploy-refused-by-store",
|
|
3459
|
+
route: "POST /SASjsApi/drive/deploy",
|
|
3460
|
+
description: "answers the routes 500 when the drive refuses the write",
|
|
3461
|
+
unwritable: true,
|
|
3462
|
+
request: {
|
|
3463
|
+
body: {
|
|
3464
|
+
appLoc: "/Public/apps/demo",
|
|
3465
|
+
fileTree: {
|
|
3466
|
+
members: [{ name: "run", type: "service", code: "%put hi;" }]
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
},
|
|
3470
|
+
expect: {
|
|
3471
|
+
status: 500,
|
|
3472
|
+
body: { status: "failure", message: "Deployment failed!" },
|
|
3473
|
+
error: "Deployment failed!"
|
|
3474
|
+
}
|
|
3475
|
+
},
|
|
3476
|
+
{
|
|
3477
|
+
id: "deploy-refused-outside-drive",
|
|
3478
|
+
route: "POST /SASjsApi/drive/deploy",
|
|
3479
|
+
description: "refuses an appLoc that resolves outside the drive",
|
|
3480
|
+
request: {
|
|
3481
|
+
body: { appLoc: "/../outside", fileTree: { members: [] } }
|
|
3482
|
+
},
|
|
3483
|
+
expect: {
|
|
3484
|
+
status: 400,
|
|
3485
|
+
body: { status: "failure", message: "appLoc cannot be outside drive." },
|
|
3486
|
+
error: "appLoc cannot be outside drive."
|
|
3487
|
+
}
|
|
3488
|
+
},
|
|
3039
3489
|
{
|
|
3040
3490
|
id: "bytes-round-trip",
|
|
3041
3491
|
route: "GET /SASjsApi/drive/file",
|
|
@@ -3313,11 +3763,21 @@ var SASjsApi = (() => {
|
|
|
3313
3763
|
|
|
3314
3764
|
// src/corpus/runner.ts
|
|
3315
3765
|
var servicesFor = (testCase) => ({
|
|
3316
|
-
drive: createDriveService(
|
|
3766
|
+
drive: createDriveService(
|
|
3767
|
+
testCase.unwritable ? new RefusingDriveStore(testCase.seed || {}) : new MemoryDriveStore(testCase.seed || {})
|
|
3768
|
+
),
|
|
3317
3769
|
identity: createIdentityService(
|
|
3318
3770
|
new MemoryIdentityStore(testCase.identity || DEFAULT_IDENTITY)
|
|
3319
3771
|
)
|
|
3320
3772
|
});
|
|
3773
|
+
var RefusingDriveStore = class extends MemoryDriveStore {
|
|
3774
|
+
async write() {
|
|
3775
|
+
throw new Error("the store refused the write");
|
|
3776
|
+
}
|
|
3777
|
+
async mkdirp() {
|
|
3778
|
+
throw new Error("the store refused the write");
|
|
3779
|
+
}
|
|
3780
|
+
};
|
|
3321
3781
|
var runCase = async (testCase, services = servicesFor(testCase)) => {
|
|
3322
3782
|
try {
|
|
3323
3783
|
const response = await dispatch(services, testCase.route, testCase.request);
|
|
@@ -3327,7 +3787,8 @@ var SASjsApi = (() => {
|
|
|
3327
3787
|
contents: response.contents
|
|
3328
3788
|
};
|
|
3329
3789
|
} catch (error) {
|
|
3330
|
-
if (isApiError(error))
|
|
3790
|
+
if (isApiError(error))
|
|
3791
|
+
return { status: error.status, body: error.body, error: error.message };
|
|
3331
3792
|
throw error;
|
|
3332
3793
|
}
|
|
3333
3794
|
};
|