@sasjs/api 0.2.1 → 0.3.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 +28 -8
- package/build/browser.js +788 -4
- package/build/cjs/corpus/identity.js +268 -0
- package/build/cjs/corpus/runner.js +11 -2
- package/build/cjs/host/dispatch.js +57 -2
- package/build/cjs/identity/fixture.js +118 -0
- package/build/cjs/identity/memory-store.js +179 -0
- package/build/cjs/identity/service.js +142 -0
- package/build/cjs/identity/types.js +11 -0
- package/build/cjs/index.js +11 -1
- package/build/cjs/routes/generated.js +72 -0
- package/build/cjs/routes/index.js +6 -0
- package/build/cjs/routes/overlay.js +38 -0
- package/build/esm/corpus/drive.d.ts +14 -0
- package/build/esm/corpus/drive.d.ts.map +1 -1
- package/build/esm/corpus/identity.d.ts +17 -0
- package/build/esm/corpus/identity.d.ts.map +1 -0
- package/build/esm/corpus/identity.js +265 -0
- package/build/esm/corpus/runner.d.ts +6 -1
- package/build/esm/corpus/runner.d.ts.map +1 -1
- package/build/esm/corpus/runner.js +11 -2
- package/build/esm/host/dispatch.d.ts +16 -0
- package/build/esm/host/dispatch.d.ts.map +1 -1
- package/build/esm/host/dispatch.js +57 -2
- package/build/esm/identity/fixture.d.ts +84 -0
- package/build/esm/identity/fixture.d.ts.map +1 -0
- package/build/esm/identity/fixture.js +114 -0
- package/build/esm/identity/memory-store.d.ts +51 -0
- package/build/esm/identity/memory-store.d.ts.map +1 -0
- package/build/esm/identity/memory-store.js +175 -0
- package/build/esm/identity/service.d.ts +48 -0
- package/build/esm/identity/service.d.ts.map +1 -0
- package/build/esm/identity/service.js +137 -0
- package/build/esm/identity/types.d.ts +79 -0
- package/build/esm/identity/types.d.ts.map +1 -0
- package/build/esm/identity/types.js +10 -0
- package/build/esm/index.d.ts +7 -0
- package/build/esm/index.d.ts.map +1 -1
- package/build/esm/index.js +4 -0
- package/build/esm/routes/generated.d.ts.map +1 -1
- package/build/esm/routes/generated.js +72 -0
- package/build/esm/routes/index.d.ts.map +1 -1
- package/build/esm/routes/index.js +6 -0
- package/build/esm/routes/overlay.d.ts +9 -0
- package/build/esm/routes/overlay.d.ts.map +1 -1
- package/build/esm/routes/overlay.js +38 -0
- package/build/esm/types.d.ts +7 -0
- package/build/esm/types.d.ts.map +1 -1
- package/package.json +1 -1
package/build/browser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* @sasjs/api 0.
|
|
1
|
+
/* @sasjs/api 0.3.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;
|
|
@@ -23,14 +23,19 @@ var SASjsApi = (() => {
|
|
|
23
23
|
var index_exports = {};
|
|
24
24
|
__export(index_exports, {
|
|
25
25
|
ApiError: () => ApiError,
|
|
26
|
+
DEFAULT_IDENTITY: () => DEFAULT_IDENTITY,
|
|
26
27
|
DRIVE_CORPUS: () => DRIVE_CORPUS,
|
|
27
28
|
HANDLED_ROUTES: () => HANDLED_ROUTES,
|
|
29
|
+
IDENTITY_CORPUS: () => IDENTITY_CORPUS,
|
|
30
|
+
IDENTITY_FORMAT: () => IDENTITY_FORMAT,
|
|
28
31
|
MemoryDriveStore: () => MemoryDriveStore,
|
|
32
|
+
MemoryIdentityStore: () => MemoryIdentityStore,
|
|
29
33
|
OVERLAY: () => OVERLAY,
|
|
30
34
|
ROUTES: () => ROUTES,
|
|
31
35
|
TAG_CAPABILITIES: () => TAG_CAPABILITIES,
|
|
32
36
|
basename: () => basename,
|
|
33
37
|
createDriveService: () => createDriveService,
|
|
38
|
+
createIdentityService: () => createIdentityService,
|
|
34
39
|
dirname: () => dirname,
|
|
35
40
|
dispatch: () => dispatch,
|
|
36
41
|
findRoute: () => findRoute,
|
|
@@ -38,6 +43,7 @@ var SASjsApi = (() => {
|
|
|
38
43
|
isApiError: () => isApiError,
|
|
39
44
|
isRoot: () => isRoot,
|
|
40
45
|
normaliseDrivePath: () => normaliseDrivePath,
|
|
46
|
+
parseIdentity: () => parseIdentity,
|
|
41
47
|
routeById: () => routeById,
|
|
42
48
|
runCase: () => runCase,
|
|
43
49
|
servicesFor: () => servicesFor
|
|
@@ -425,6 +431,10 @@ var SASjsApi = (() => {
|
|
|
425
431
|
{
|
|
426
432
|
status: 204,
|
|
427
433
|
description: "No content"
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
status: 401,
|
|
437
|
+
description: "No valid access token was supplied"
|
|
428
438
|
}
|
|
429
439
|
]
|
|
430
440
|
},
|
|
@@ -440,6 +450,10 @@ var SASjsApi = (() => {
|
|
|
440
450
|
{
|
|
441
451
|
status: 200,
|
|
442
452
|
description: "Ok"
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
status: 401,
|
|
456
|
+
description: "The refresh token is missing, invalid or expired, or the client id is unknown"
|
|
443
457
|
}
|
|
444
458
|
]
|
|
445
459
|
},
|
|
@@ -468,6 +482,14 @@ var SASjsApi = (() => {
|
|
|
468
482
|
{
|
|
469
483
|
status: 200,
|
|
470
484
|
description: "Ok"
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
status: 400,
|
|
488
|
+
description: "The request body is missing `clientId` or `code`"
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
status: 401,
|
|
492
|
+
description: "The authorization code is invalid, expired, already used, or was issued for another client, or the client id is unknown"
|
|
471
493
|
}
|
|
472
494
|
]
|
|
473
495
|
},
|
|
@@ -496,6 +518,26 @@ var SASjsApi = (() => {
|
|
|
496
518
|
{
|
|
497
519
|
status: 204,
|
|
498
520
|
description: "No content"
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
status: 400,
|
|
524
|
+
description: "The request body is missing `currentPassword` or `newPassword`"
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
status: 401,
|
|
528
|
+
description: "No valid access token was supplied"
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
status: 403,
|
|
532
|
+
description: "The current password is incorrect"
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
status: 404,
|
|
536
|
+
description: "The authenticated user no longer exists"
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
status: 405,
|
|
540
|
+
description: "The account authenticates through an external provider, which holds its password"
|
|
499
541
|
}
|
|
500
542
|
]
|
|
501
543
|
},
|
|
@@ -1409,6 +1451,10 @@ var SASjsApi = (() => {
|
|
|
1409
1451
|
{
|
|
1410
1452
|
status: 200,
|
|
1411
1453
|
description: "Ok"
|
|
1454
|
+
},
|
|
1455
|
+
{
|
|
1456
|
+
status: 401,
|
|
1457
|
+
description: "No valid access token or session was supplied"
|
|
1412
1458
|
}
|
|
1413
1459
|
]
|
|
1414
1460
|
},
|
|
@@ -1892,6 +1938,18 @@ var SASjsApi = (() => {
|
|
|
1892
1938
|
{
|
|
1893
1939
|
status: 200,
|
|
1894
1940
|
description: "Ok"
|
|
1941
|
+
},
|
|
1942
|
+
{
|
|
1943
|
+
status: 400,
|
|
1944
|
+
description: "The request body is missing `clientId`, or the CSRF token is missing or invalid"
|
|
1945
|
+
},
|
|
1946
|
+
{
|
|
1947
|
+
status: 401,
|
|
1948
|
+
description: "No signed-in session"
|
|
1949
|
+
},
|
|
1950
|
+
{
|
|
1951
|
+
status: 403,
|
|
1952
|
+
description: "The client id is unknown"
|
|
1895
1953
|
}
|
|
1896
1954
|
]
|
|
1897
1955
|
},
|
|
@@ -1920,6 +1978,22 @@ var SASjsApi = (() => {
|
|
|
1920
1978
|
{
|
|
1921
1979
|
status: 200,
|
|
1922
1980
|
description: "Ok"
|
|
1981
|
+
},
|
|
1982
|
+
{
|
|
1983
|
+
status: 400,
|
|
1984
|
+
description: "The request body is missing `username` or `password`"
|
|
1985
|
+
},
|
|
1986
|
+
{
|
|
1987
|
+
status: 401,
|
|
1988
|
+
description: "The credentials are not valid, or password sign-in is disabled for local accounts"
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
status: 429,
|
|
1992
|
+
description: "Too many failed attempts for this account"
|
|
1993
|
+
},
|
|
1994
|
+
{
|
|
1995
|
+
status: 500,
|
|
1996
|
+
description: "An unexpected error occurred while signing in"
|
|
1923
1997
|
}
|
|
1924
1998
|
]
|
|
1925
1999
|
},
|
|
@@ -1935,6 +2009,10 @@ var SASjsApi = (() => {
|
|
|
1935
2009
|
{
|
|
1936
2010
|
status: 200,
|
|
1937
2011
|
description: "Ok"
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
status: 500,
|
|
2015
|
+
description: "The session store failed to replace the session"
|
|
1938
2016
|
}
|
|
1939
2017
|
]
|
|
1940
2018
|
}
|
|
@@ -1957,6 +2035,13 @@ var SASjsApi = (() => {
|
|
|
1957
2035
|
Web: ["identity"]
|
|
1958
2036
|
};
|
|
1959
2037
|
var success = { status: "success" };
|
|
2038
|
+
var user = {
|
|
2039
|
+
uid: "",
|
|
2040
|
+
username: "",
|
|
2041
|
+
displayName: "",
|
|
2042
|
+
isAdmin: false,
|
|
2043
|
+
needsToUpdatePassword: false
|
|
2044
|
+
};
|
|
1960
2045
|
var OVERLAY = {
|
|
1961
2046
|
// the route inventory reports what the CALLER may use, so it needs an identity
|
|
1962
2047
|
// even though the rest of the Info tag needs nothing
|
|
@@ -2008,6 +2093,37 @@ var SASjsApi = (() => {
|
|
|
2008
2093
|
},
|
|
2009
2094
|
"POST /SASjsApi/drive/deploy/upload": {
|
|
2010
2095
|
implemented: false
|
|
2096
|
+
},
|
|
2097
|
+
// The lockout and the provider failure are answered before the package is
|
|
2098
|
+
// consulted (routes/web/web.ts in sasjs/server).
|
|
2099
|
+
"POST /SASLogon/login": {
|
|
2100
|
+
implemented: true,
|
|
2101
|
+
hostResponses: [429, 500],
|
|
2102
|
+
responses: { 200: { loggedIn: true, user } }
|
|
2103
|
+
},
|
|
2104
|
+
// The sign-in flow. The statuses are the document's; these entries state the
|
|
2105
|
+
// body a client keys on, and that this package answers the operation.
|
|
2106
|
+
"POST /SASjsApi/auth/token": {
|
|
2107
|
+
implemented: true,
|
|
2108
|
+
responses: { 200: { accessToken: "", refreshToken: "" } }
|
|
2109
|
+
},
|
|
2110
|
+
"POST /SASjsApi/auth/refresh": {
|
|
2111
|
+
implemented: true,
|
|
2112
|
+
responses: { 200: { accessToken: "", refreshToken: "" } }
|
|
2113
|
+
},
|
|
2114
|
+
"POST /SASjsApi/auth/logout": { implemented: true },
|
|
2115
|
+
"POST /SASLogon/authorize": {
|
|
2116
|
+
implemented: true,
|
|
2117
|
+
responses: { 200: { code: "" } }
|
|
2118
|
+
},
|
|
2119
|
+
"GET /SASLogon/logout": {
|
|
2120
|
+
implemented: true,
|
|
2121
|
+
hostResponses: [500],
|
|
2122
|
+
responses: { 200: "OK!" }
|
|
2123
|
+
},
|
|
2124
|
+
"GET /SASjsApi/session": {
|
|
2125
|
+
implemented: true,
|
|
2126
|
+
responses: { 200: user }
|
|
2011
2127
|
}
|
|
2012
2128
|
};
|
|
2013
2129
|
|
|
@@ -2032,9 +2148,15 @@ var SASjsApi = (() => {
|
|
|
2032
2148
|
...body === void 0 ? {} : { body }
|
|
2033
2149
|
};
|
|
2034
2150
|
});
|
|
2151
|
+
for (const status of overlay.hostResponses || [])
|
|
2152
|
+
if (!documented.has(status))
|
|
2153
|
+
throw new Error(
|
|
2154
|
+
`src/routes/overlay.ts marks status ${status} of ${generated.id} as host-only, and the OpenAPI document does not state it. Correct the document in sasjs/server and re-vendor spec/swagger.yaml, or drop the annotation.`
|
|
2155
|
+
);
|
|
2035
2156
|
return {
|
|
2036
2157
|
...generated,
|
|
2037
2158
|
responses,
|
|
2159
|
+
hostResponses: overlay.hostResponses || [],
|
|
2038
2160
|
capabilities: overlay.capabilities || TAG_CAPABILITIES[generated.tag] || [],
|
|
2039
2161
|
implemented: overlay.implemented === true
|
|
2040
2162
|
};
|
|
@@ -2110,8 +2232,57 @@ var SASjsApi = (() => {
|
|
|
2110
2232
|
const route = routeById(routeId);
|
|
2111
2233
|
if (!route) throw new ApiError(404, `No such operation: ${routeId}`);
|
|
2112
2234
|
validate(route, request);
|
|
2113
|
-
const { drive } = services;
|
|
2235
|
+
const { drive, identity } = services;
|
|
2114
2236
|
const query = request.query || {};
|
|
2237
|
+
const body = request.body || {};
|
|
2238
|
+
const caller = async () => {
|
|
2239
|
+
if (request.subject) return request.subject;
|
|
2240
|
+
return identity?.authenticate(request.token);
|
|
2241
|
+
};
|
|
2242
|
+
if (routeId.startsWith("POST /SASjsApi/auth/") || routeId.startsWith("POST /SASLogon/") || routeId === "GET /SASLogon/logout" || routeId === "GET /SASjsApi/session") {
|
|
2243
|
+
if (!identity)
|
|
2244
|
+
throw new ApiError(
|
|
2245
|
+
501,
|
|
2246
|
+
`This host has no identity service, so it cannot answer ${routeId}.`
|
|
2247
|
+
);
|
|
2248
|
+
switch (routeId) {
|
|
2249
|
+
case "POST /SASjsApi/auth/token":
|
|
2250
|
+
return {
|
|
2251
|
+
status: 200,
|
|
2252
|
+
body: await identity.token({
|
|
2253
|
+
clientId: String(body.clientId ?? ""),
|
|
2254
|
+
code: String(body.code ?? "")
|
|
2255
|
+
})
|
|
2256
|
+
};
|
|
2257
|
+
case "POST /SASjsApi/auth/refresh":
|
|
2258
|
+
return { status: 200, body: await identity.refresh(request.token) };
|
|
2259
|
+
case "POST /SASjsApi/auth/logout":
|
|
2260
|
+
await identity.logout(await caller());
|
|
2261
|
+
return { status: 204 };
|
|
2262
|
+
case "POST /SASLogon/login":
|
|
2263
|
+
return {
|
|
2264
|
+
status: 200,
|
|
2265
|
+
body: await identity.login({
|
|
2266
|
+
username: String(body.username ?? ""),
|
|
2267
|
+
password: String(body.password ?? "")
|
|
2268
|
+
})
|
|
2269
|
+
};
|
|
2270
|
+
case "POST /SASLogon/authorize":
|
|
2271
|
+
return {
|
|
2272
|
+
status: 200,
|
|
2273
|
+
body: {
|
|
2274
|
+
code: await identity.authorize(
|
|
2275
|
+
await caller(),
|
|
2276
|
+
String(body.clientId ?? "")
|
|
2277
|
+
)
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
case "GET /SASLogon/logout":
|
|
2281
|
+
return { status: 200, contents: await identity.logoutSession() };
|
|
2282
|
+
case "GET /SASjsApi/session":
|
|
2283
|
+
return { status: 200, body: await identity.session(await caller()) };
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2115
2286
|
switch (routeId) {
|
|
2116
2287
|
case "GET /SASjsApi/drive/file": {
|
|
2117
2288
|
const file = await drive.read(String(query._filePath || ""));
|
|
@@ -2164,9 +2335,354 @@ var SASjsApi = (() => {
|
|
|
2164
2335
|
"POST /SASjsApi/drive/folder",
|
|
2165
2336
|
"DELETE /SASjsApi/drive/folder",
|
|
2166
2337
|
"POST /SASjsApi/drive/rename",
|
|
2167
|
-
"GET /SASjsApi/drive/filetree"
|
|
2338
|
+
"GET /SASjsApi/drive/filetree",
|
|
2339
|
+
"POST /SASjsApi/auth/token",
|
|
2340
|
+
"POST /SASjsApi/auth/refresh",
|
|
2341
|
+
"POST /SASjsApi/auth/logout",
|
|
2342
|
+
"POST /SASLogon/login",
|
|
2343
|
+
"POST /SASLogon/authorize",
|
|
2344
|
+
"GET /SASLogon/logout",
|
|
2345
|
+
"GET /SASjsApi/session"
|
|
2168
2346
|
];
|
|
2169
2347
|
|
|
2348
|
+
// src/identity/service.ts
|
|
2349
|
+
var unauthorized = () => new ApiError(401, "Unauthorized");
|
|
2350
|
+
var liveSubject = async (store, subject) => {
|
|
2351
|
+
const user3 = await store.userById(subject.userId);
|
|
2352
|
+
if (!user3 || !user3.isActive) throw unauthorized();
|
|
2353
|
+
return subject;
|
|
2354
|
+
};
|
|
2355
|
+
var createIdentityService = (store) => ({
|
|
2356
|
+
async authenticate(token) {
|
|
2357
|
+
if (token === void 0) return void 0;
|
|
2358
|
+
const subject = await store.readAccess(token);
|
|
2359
|
+
if (!subject) return void 0;
|
|
2360
|
+
const user3 = await store.userById(subject.userId);
|
|
2361
|
+
return user3 && user3.isActive ? subject : void 0;
|
|
2362
|
+
},
|
|
2363
|
+
async login({ username, password }) {
|
|
2364
|
+
const user3 = await store.userByName(username);
|
|
2365
|
+
if (!user3) throw new ApiError(401, "Username is not found.");
|
|
2366
|
+
if (!await store.passwordMatches(user3, password))
|
|
2367
|
+
throw new ApiError(401, "Invalid Password.");
|
|
2368
|
+
if (!user3.isActive) throw new ApiError(401, "This account is not active.");
|
|
2369
|
+
return {
|
|
2370
|
+
loggedIn: true,
|
|
2371
|
+
user: {
|
|
2372
|
+
uid: user3.uid,
|
|
2373
|
+
username: user3.username,
|
|
2374
|
+
displayName: user3.displayName,
|
|
2375
|
+
isAdmin: user3.isAdmin,
|
|
2376
|
+
needsToUpdatePassword: user3.needsToUpdatePassword
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
},
|
|
2380
|
+
async authorize(subject, clientId) {
|
|
2381
|
+
if (!subject) throw unauthorized();
|
|
2382
|
+
if (!await store.client(clientId))
|
|
2383
|
+
throw new ApiError(403, "Invalid clientId.");
|
|
2384
|
+
return store.issueCode({ userId: subject.userId, clientId });
|
|
2385
|
+
},
|
|
2386
|
+
async token({ clientId, code }) {
|
|
2387
|
+
const subject = await store.readCode(code);
|
|
2388
|
+
if (!subject || subject.clientId !== clientId)
|
|
2389
|
+
throw new ApiError(401, "Invalid Auth Code");
|
|
2390
|
+
const client = await store.client(clientId);
|
|
2391
|
+
if (!client) throw new ApiError(401, "Invalid clientId.");
|
|
2392
|
+
const named = await liveSubject(store, subject);
|
|
2393
|
+
if (!await store.claimCode(code))
|
|
2394
|
+
throw new ApiError(401, "Invalid Auth Code");
|
|
2395
|
+
const existing = await store.pairFor(named);
|
|
2396
|
+
if (existing) return existing;
|
|
2397
|
+
const pair = await store.issue(named, client);
|
|
2398
|
+
await store.savePair(named, pair);
|
|
2399
|
+
return pair;
|
|
2400
|
+
},
|
|
2401
|
+
async refresh(token) {
|
|
2402
|
+
const subject = token === void 0 ? void 0 : await store.readRefresh(token);
|
|
2403
|
+
if (!subject) throw unauthorized();
|
|
2404
|
+
const client = await store.client(subject.clientId);
|
|
2405
|
+
if (!client) throw new ApiError(401, "Invalid clientId.");
|
|
2406
|
+
const named = await liveSubject(store, subject);
|
|
2407
|
+
const pair = await store.issue(named, client);
|
|
2408
|
+
await store.savePair(named, pair);
|
|
2409
|
+
return pair;
|
|
2410
|
+
},
|
|
2411
|
+
async logout(subject) {
|
|
2412
|
+
if (!subject) throw unauthorized();
|
|
2413
|
+
await store.deletePair(subject);
|
|
2414
|
+
},
|
|
2415
|
+
async logoutSession() {
|
|
2416
|
+
return "OK!";
|
|
2417
|
+
},
|
|
2418
|
+
async session(subject) {
|
|
2419
|
+
if (!subject) throw unauthorized();
|
|
2420
|
+
const user3 = await store.userById(subject.userId);
|
|
2421
|
+
if (!user3) throw unauthorized();
|
|
2422
|
+
return asSession(user3);
|
|
2423
|
+
}
|
|
2424
|
+
});
|
|
2425
|
+
var asSession = (user3) => ({
|
|
2426
|
+
uid: user3.uid,
|
|
2427
|
+
username: user3.username,
|
|
2428
|
+
displayName: user3.displayName,
|
|
2429
|
+
isAdmin: user3.isAdmin,
|
|
2430
|
+
needsToUpdatePassword: user3.needsToUpdatePassword
|
|
2431
|
+
});
|
|
2432
|
+
|
|
2433
|
+
// src/identity/fixture.ts
|
|
2434
|
+
var IDENTITY_FORMAT = 1;
|
|
2435
|
+
var SECONDS_IN_A_DAY = 86400;
|
|
2436
|
+
var DEFAULT_IDENTITY = {
|
|
2437
|
+
format: IDENTITY_FORMAT,
|
|
2438
|
+
users: [
|
|
2439
|
+
{
|
|
2440
|
+
uid: "user-admin",
|
|
2441
|
+
username: "admin",
|
|
2442
|
+
displayName: "Demo Admin",
|
|
2443
|
+
// long enough to pass the API's own password rule (six characters), so a
|
|
2444
|
+
// demo can sign in against a real server as well as a mock
|
|
2445
|
+
password: "admin123",
|
|
2446
|
+
isAdmin: true,
|
|
2447
|
+
isActive: true,
|
|
2448
|
+
needsToUpdatePassword: false
|
|
2449
|
+
},
|
|
2450
|
+
{
|
|
2451
|
+
uid: "user-restricted",
|
|
2452
|
+
username: "user2",
|
|
2453
|
+
displayName: "Demo User",
|
|
2454
|
+
password: "user123",
|
|
2455
|
+
isAdmin: false,
|
|
2456
|
+
isActive: true,
|
|
2457
|
+
needsToUpdatePassword: false
|
|
2458
|
+
}
|
|
2459
|
+
],
|
|
2460
|
+
groups: [
|
|
2461
|
+
{
|
|
2462
|
+
uid: "group-data-controller",
|
|
2463
|
+
name: "DCGroup",
|
|
2464
|
+
description: "Data Controller users",
|
|
2465
|
+
isActive: true,
|
|
2466
|
+
users: ["user2"]
|
|
2467
|
+
},
|
|
2468
|
+
{
|
|
2469
|
+
uid: "group-admins",
|
|
2470
|
+
name: "Admins",
|
|
2471
|
+
description: "Everything the API offers",
|
|
2472
|
+
isActive: true,
|
|
2473
|
+
users: ["admin"]
|
|
2474
|
+
}
|
|
2475
|
+
],
|
|
2476
|
+
permissions: [
|
|
2477
|
+
{
|
|
2478
|
+
uid: "rule-routes",
|
|
2479
|
+
path: "/SASjsApi/info",
|
|
2480
|
+
type: "Route",
|
|
2481
|
+
setting: "Grant",
|
|
2482
|
+
group: "group-data-controller"
|
|
2483
|
+
},
|
|
2484
|
+
{
|
|
2485
|
+
uid: "rule-execute",
|
|
2486
|
+
path: "/SASjsApi/stp/execute",
|
|
2487
|
+
type: "Route",
|
|
2488
|
+
setting: "Grant",
|
|
2489
|
+
group: "group-data-controller"
|
|
2490
|
+
},
|
|
2491
|
+
{
|
|
2492
|
+
uid: "rule-code",
|
|
2493
|
+
path: "/SASjsApi/code/execute",
|
|
2494
|
+
type: "Route",
|
|
2495
|
+
setting: "Deny",
|
|
2496
|
+
user: "user-restricted"
|
|
2497
|
+
}
|
|
2498
|
+
],
|
|
2499
|
+
clients: [
|
|
2500
|
+
{
|
|
2501
|
+
clientId: "web_app",
|
|
2502
|
+
clientSecret: "web_app",
|
|
2503
|
+
accessTokenExpiration: SECONDS_IN_A_DAY,
|
|
2504
|
+
refreshTokenExpiration: SECONDS_IN_A_DAY * 30
|
|
2505
|
+
},
|
|
2506
|
+
{
|
|
2507
|
+
clientId: "sasjs",
|
|
2508
|
+
clientSecret: "sasjs",
|
|
2509
|
+
accessTokenExpiration: 3600,
|
|
2510
|
+
refreshTokenExpiration: SECONDS_IN_A_DAY
|
|
2511
|
+
}
|
|
2512
|
+
]
|
|
2513
|
+
};
|
|
2514
|
+
var parseIdentity = (raw) => {
|
|
2515
|
+
const fixture = raw;
|
|
2516
|
+
if (!fixture || typeof fixture !== "object")
|
|
2517
|
+
throw new Error("The identity fixture is not an object.");
|
|
2518
|
+
if (fixture.format !== IDENTITY_FORMAT)
|
|
2519
|
+
throw new Error(
|
|
2520
|
+
`The identity fixture states format ${String(fixture.format)}, and this build reads format ${IDENTITY_FORMAT}. Rebuild the site, or replace mock-identity.json with one this version wrote.`
|
|
2521
|
+
);
|
|
2522
|
+
for (const key of ["users", "groups", "permissions", "clients"])
|
|
2523
|
+
if (!Array.isArray(fixture[key]))
|
|
2524
|
+
throw new Error(`The identity fixture has no \`${key}\` array.`);
|
|
2525
|
+
for (const token of fixture.tokens || [])
|
|
2526
|
+
if (!token.accessToken || !token.refreshToken)
|
|
2527
|
+
throw new Error(
|
|
2528
|
+
`The identity fixture declares a half pair for ${token.username}/${token.clientId}: a seeded token needs both an accessToken and a refreshToken, because a token only authenticates while the subject's pair holds it.`
|
|
2529
|
+
);
|
|
2530
|
+
if (!fixture.users.some((user3) => user3.isAdmin))
|
|
2531
|
+
throw new Error(
|
|
2532
|
+
"The identity fixture declares no administrator: a demo with nobody able to administer it cannot exercise the admin routes."
|
|
2533
|
+
);
|
|
2534
|
+
return fixture;
|
|
2535
|
+
};
|
|
2536
|
+
|
|
2537
|
+
// src/identity/memory-store.ts
|
|
2538
|
+
var keyOf = (subject) => `${subject.userId}|${subject.clientId}`;
|
|
2539
|
+
var randomHex = (bytes2) => {
|
|
2540
|
+
const source = globalThis.crypto;
|
|
2541
|
+
if (!source)
|
|
2542
|
+
throw new ApiError(
|
|
2543
|
+
500,
|
|
2544
|
+
"This host has no Web Crypto (`globalThis.crypto`), so it cannot mint an unguessable identity string."
|
|
2545
|
+
);
|
|
2546
|
+
const buffer = new Uint8Array(bytes2);
|
|
2547
|
+
source.getRandomValues(buffer);
|
|
2548
|
+
return Array.from(buffer, (byte) => byte.toString(16).padStart(2, "0")).join(
|
|
2549
|
+
""
|
|
2550
|
+
);
|
|
2551
|
+
};
|
|
2552
|
+
var CODE_LIFETIME_SECONDS = 30;
|
|
2553
|
+
var live = (issued, now) => issued && (issued.expiresAt === void 0 || issued.expiresAt > now) ? issued.subject : void 0;
|
|
2554
|
+
var MemoryIdentityStore = class {
|
|
2555
|
+
constructor(fixture = DEFAULT_IDENTITY) {
|
|
2556
|
+
this.users = [];
|
|
2557
|
+
this.passwords = /* @__PURE__ */ new Map();
|
|
2558
|
+
this.clients = /* @__PURE__ */ new Map();
|
|
2559
|
+
this.pairs = /* @__PURE__ */ new Map();
|
|
2560
|
+
this.access = /* @__PURE__ */ new Map();
|
|
2561
|
+
this.refresh = /* @__PURE__ */ new Map();
|
|
2562
|
+
this.codes = /* @__PURE__ */ new Map();
|
|
2563
|
+
this.claimed = /* @__PURE__ */ new Set();
|
|
2564
|
+
if (fixture.format !== IDENTITY_FORMAT)
|
|
2565
|
+
throw new ApiError(
|
|
2566
|
+
500,
|
|
2567
|
+
`The identity fixture states format ${String(fixture.format)}; this store reads ${IDENTITY_FORMAT}.`
|
|
2568
|
+
);
|
|
2569
|
+
for (const user3 of fixture.users) {
|
|
2570
|
+
this.users.push({
|
|
2571
|
+
uid: user3.uid,
|
|
2572
|
+
username: user3.username,
|
|
2573
|
+
displayName: user3.displayName,
|
|
2574
|
+
isAdmin: user3.isAdmin,
|
|
2575
|
+
isActive: user3.isActive,
|
|
2576
|
+
needsToUpdatePassword: user3.needsToUpdatePassword
|
|
2577
|
+
});
|
|
2578
|
+
this.passwords.set(user3.uid, user3.password);
|
|
2579
|
+
}
|
|
2580
|
+
for (const client of fixture.clients)
|
|
2581
|
+
this.clients.set(client.clientId, {
|
|
2582
|
+
clientId: client.clientId,
|
|
2583
|
+
accessTokenExpiration: client.accessTokenExpiration,
|
|
2584
|
+
refreshTokenExpiration: client.refreshTokenExpiration
|
|
2585
|
+
});
|
|
2586
|
+
for (const token of fixture.tokens || []) {
|
|
2587
|
+
const user3 = this.users.find(
|
|
2588
|
+
(candidate) => candidate.username === token.username
|
|
2589
|
+
);
|
|
2590
|
+
if (!user3) continue;
|
|
2591
|
+
const subject = { userId: user3.uid, clientId: token.clientId };
|
|
2592
|
+
if (token.accessToken) this.access.set(token.accessToken, { subject });
|
|
2593
|
+
if (token.refreshToken) this.refresh.set(token.refreshToken, { subject });
|
|
2594
|
+
if (token.accessToken && token.refreshToken)
|
|
2595
|
+
this.pairs.set(keyOf(subject), {
|
|
2596
|
+
accessToken: token.accessToken,
|
|
2597
|
+
refreshToken: token.refreshToken
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
for (const code of fixture.codes || []) {
|
|
2601
|
+
const user3 = this.users.find(
|
|
2602
|
+
(candidate) => candidate.username === code.username
|
|
2603
|
+
);
|
|
2604
|
+
if (!user3) continue;
|
|
2605
|
+
this.codes.set(code.code, {
|
|
2606
|
+
subject: { userId: user3.uid, clientId: code.clientId }
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
async userByName(username) {
|
|
2611
|
+
return this.users.find((user3) => user3.username === username);
|
|
2612
|
+
}
|
|
2613
|
+
async userById(uid) {
|
|
2614
|
+
return this.users.find((user3) => user3.uid === uid);
|
|
2615
|
+
}
|
|
2616
|
+
/** A fixture holds the password in clear, so the check is an equality. */
|
|
2617
|
+
async passwordMatches(user3, password) {
|
|
2618
|
+
return this.passwords.get(user3.uid) === password;
|
|
2619
|
+
}
|
|
2620
|
+
async client(clientId) {
|
|
2621
|
+
return this.clients.get(clientId);
|
|
2622
|
+
}
|
|
2623
|
+
async issue(subject, client) {
|
|
2624
|
+
const issuedAt = Date.now();
|
|
2625
|
+
const pair = {
|
|
2626
|
+
accessToken: `access-${randomHex(16)}`,
|
|
2627
|
+
refreshToken: `refresh-${randomHex(16)}`
|
|
2628
|
+
};
|
|
2629
|
+
this.access.set(pair.accessToken, {
|
|
2630
|
+
subject,
|
|
2631
|
+
expiresAt: issuedAt + client.accessTokenExpiration * 1e3
|
|
2632
|
+
});
|
|
2633
|
+
this.refresh.set(pair.refreshToken, {
|
|
2634
|
+
subject,
|
|
2635
|
+
expiresAt: issuedAt + client.refreshTokenExpiration * 1e3
|
|
2636
|
+
});
|
|
2637
|
+
return pair;
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* A token names its subject only while that subject's CURRENT pair holds it.
|
|
2641
|
+
* The server verifies a token against the pair saved for the user and client,
|
|
2642
|
+
* so a logged-out or rotated token stops working there too; without the same
|
|
2643
|
+
* check here, the browser host would honour a token the server had revoked.
|
|
2644
|
+
*/
|
|
2645
|
+
async readAccess(token) {
|
|
2646
|
+
return this.current(token, this.access, "accessToken");
|
|
2647
|
+
}
|
|
2648
|
+
async readRefresh(token) {
|
|
2649
|
+
return this.current(token, this.refresh, "refreshToken");
|
|
2650
|
+
}
|
|
2651
|
+
current(token, issued, kind) {
|
|
2652
|
+
const entry = issued.get(token);
|
|
2653
|
+
if (!entry) return void 0;
|
|
2654
|
+
const subject = live(entry, Date.now());
|
|
2655
|
+
if (!subject) return void 0;
|
|
2656
|
+
const pair = this.pairs.get(keyOf(subject));
|
|
2657
|
+
return pair && pair[kind] === token ? subject : void 0;
|
|
2658
|
+
}
|
|
2659
|
+
async pairFor(subject) {
|
|
2660
|
+
return this.pairs.get(keyOf(subject));
|
|
2661
|
+
}
|
|
2662
|
+
async savePair(subject, pair) {
|
|
2663
|
+
this.pairs.set(keyOf(subject), pair);
|
|
2664
|
+
}
|
|
2665
|
+
async deletePair(subject) {
|
|
2666
|
+
this.pairs.delete(keyOf(subject));
|
|
2667
|
+
}
|
|
2668
|
+
async issueCode(subject) {
|
|
2669
|
+
const code = `code-${randomHex(16)}`;
|
|
2670
|
+
this.codes.set(code, {
|
|
2671
|
+
subject,
|
|
2672
|
+
expiresAt: Date.now() + CODE_LIFETIME_SECONDS * 1e3
|
|
2673
|
+
});
|
|
2674
|
+
return code;
|
|
2675
|
+
}
|
|
2676
|
+
async readCode(code) {
|
|
2677
|
+
return live(this.codes.get(code), Date.now());
|
|
2678
|
+
}
|
|
2679
|
+
async claimCode(code) {
|
|
2680
|
+
if (this.claimed.has(code) || !this.codes.has(code)) return false;
|
|
2681
|
+
this.claimed.add(code);
|
|
2682
|
+
return true;
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
|
|
2170
2686
|
// src/corpus/drive.ts
|
|
2171
2687
|
var success2 = { status: "success" };
|
|
2172
2688
|
var bytes = (...values) => Uint8Array.from(values);
|
|
@@ -2469,9 +2985,277 @@ var SASjsApi = (() => {
|
|
|
2469
2985
|
}
|
|
2470
2986
|
];
|
|
2471
2987
|
|
|
2988
|
+
// src/corpus/identity.ts
|
|
2989
|
+
var SIGNED_IN = {
|
|
2990
|
+
...DEFAULT_IDENTITY,
|
|
2991
|
+
tokens: [
|
|
2992
|
+
{
|
|
2993
|
+
username: "user2",
|
|
2994
|
+
clientId: "web_app",
|
|
2995
|
+
accessToken: "seed-access",
|
|
2996
|
+
refreshToken: "seed-refresh"
|
|
2997
|
+
},
|
|
2998
|
+
{
|
|
2999
|
+
username: "admin",
|
|
3000
|
+
clientId: "removed_client",
|
|
3001
|
+
accessToken: "seed-access-orphan",
|
|
3002
|
+
refreshToken: "seed-refresh-orphan"
|
|
3003
|
+
}
|
|
3004
|
+
],
|
|
3005
|
+
codes: [
|
|
3006
|
+
{ code: "seed-code", username: "user2", clientId: "web_app" },
|
|
3007
|
+
{ code: "seed-orphan-code", username: "user2", clientId: "removed_client" },
|
|
3008
|
+
// the same shape with a pair already issued: the exchange must still refuse,
|
|
3009
|
+
// rather than handing back tokens that belong to a client that is gone
|
|
3010
|
+
{
|
|
3011
|
+
code: "seed-orphan-pair-code",
|
|
3012
|
+
username: "admin",
|
|
3013
|
+
clientId: "removed_client"
|
|
3014
|
+
}
|
|
3015
|
+
]
|
|
3016
|
+
};
|
|
3017
|
+
var DEACTIVATED = {
|
|
3018
|
+
...SIGNED_IN,
|
|
3019
|
+
users: SIGNED_IN.users.map(
|
|
3020
|
+
(user3) => user3.username === "user2" ? { ...user3, isActive: false } : user3
|
|
3021
|
+
)
|
|
3022
|
+
};
|
|
3023
|
+
var user2 = {
|
|
3024
|
+
username: "user2",
|
|
3025
|
+
displayName: "Demo User",
|
|
3026
|
+
isAdmin: false,
|
|
3027
|
+
needsToUpdatePassword: false
|
|
3028
|
+
};
|
|
3029
|
+
var IDENTITY_CORPUS = [
|
|
3030
|
+
{
|
|
3031
|
+
id: "login",
|
|
3032
|
+
route: "POST /SASLogon/login",
|
|
3033
|
+
description: "signs a seeded user in",
|
|
3034
|
+
request: { body: { username: "admin", password: "admin123" } },
|
|
3035
|
+
expect: {
|
|
3036
|
+
status: 200,
|
|
3037
|
+
partial: {
|
|
3038
|
+
loggedIn: true,
|
|
3039
|
+
user: {
|
|
3040
|
+
username: "admin",
|
|
3041
|
+
displayName: "Demo Admin",
|
|
3042
|
+
isAdmin: true,
|
|
3043
|
+
needsToUpdatePassword: false
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
},
|
|
3048
|
+
{
|
|
3049
|
+
id: "login-missing-username",
|
|
3050
|
+
route: "POST /SASLogon/login",
|
|
3051
|
+
description: "refuses a sign-in with no username as 400, naming the field",
|
|
3052
|
+
request: { body: { password: "admin123" } },
|
|
3053
|
+
expect: { status: 400, error: '"username" is required' }
|
|
3054
|
+
},
|
|
3055
|
+
{
|
|
3056
|
+
id: "login-deactivated",
|
|
3057
|
+
route: "POST /SASLogon/login",
|
|
3058
|
+
description: "refuses a deactivated account, naming it",
|
|
3059
|
+
identity: DEACTIVATED,
|
|
3060
|
+
request: { body: { username: "user2", password: "user123" } },
|
|
3061
|
+
expect: { status: 401, error: "This account is not active." }
|
|
3062
|
+
},
|
|
3063
|
+
{
|
|
3064
|
+
id: "login-wrong-password",
|
|
3065
|
+
route: "POST /SASLogon/login",
|
|
3066
|
+
description: "refuses a wrong password as 401, naming it",
|
|
3067
|
+
request: { body: { username: "admin", password: "wrong123" } },
|
|
3068
|
+
expect: { status: 401, error: "Invalid Password." }
|
|
3069
|
+
},
|
|
3070
|
+
{
|
|
3071
|
+
id: "login-unknown-user",
|
|
3072
|
+
route: "POST /SASLogon/login",
|
|
3073
|
+
description: "refuses an unknown username as 401, naming it",
|
|
3074
|
+
request: { body: { username: "nobody", password: "admin123" } },
|
|
3075
|
+
expect: { status: 401, error: "Username is not found." }
|
|
3076
|
+
},
|
|
3077
|
+
{
|
|
3078
|
+
id: "authorize",
|
|
3079
|
+
route: "POST /SASLogon/authorize",
|
|
3080
|
+
description: "issues a code to a signed-in caller",
|
|
3081
|
+
identity: SIGNED_IN,
|
|
3082
|
+
request: {
|
|
3083
|
+
token: "seed-access",
|
|
3084
|
+
body: { clientId: "web_app" }
|
|
3085
|
+
},
|
|
3086
|
+
expect: { status: 200, keys: ["code"] }
|
|
3087
|
+
},
|
|
3088
|
+
{
|
|
3089
|
+
id: "authorize-missing-client",
|
|
3090
|
+
route: "POST /SASLogon/authorize",
|
|
3091
|
+
description: "refuses an authorize with no clientId as 400, naming the field",
|
|
3092
|
+
identity: SIGNED_IN,
|
|
3093
|
+
request: { token: "seed-access", body: {} },
|
|
3094
|
+
expect: { status: 400, error: '"clientId" is required' }
|
|
3095
|
+
},
|
|
3096
|
+
{
|
|
3097
|
+
id: "authorize-without-session",
|
|
3098
|
+
route: "POST /SASLogon/authorize",
|
|
3099
|
+
description: "refuses with 401 when no caller is signed in",
|
|
3100
|
+
request: { body: { clientId: "web_app" } },
|
|
3101
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3102
|
+
},
|
|
3103
|
+
{
|
|
3104
|
+
id: "authorize-unknown-client",
|
|
3105
|
+
route: "POST /SASLogon/authorize",
|
|
3106
|
+
description: "refuses an unknown client with 403",
|
|
3107
|
+
identity: SIGNED_IN,
|
|
3108
|
+
request: { token: "seed-access", body: { clientId: "nope" } },
|
|
3109
|
+
expect: { status: 403, error: "Invalid clientId." }
|
|
3110
|
+
},
|
|
3111
|
+
{
|
|
3112
|
+
id: "exchange-code",
|
|
3113
|
+
route: "POST /SASjsApi/auth/token",
|
|
3114
|
+
description: "exchanges a code for a token pair",
|
|
3115
|
+
identity: SIGNED_IN,
|
|
3116
|
+
request: { body: { clientId: "web_app", code: "seed-code" } },
|
|
3117
|
+
expect: { status: 200, keys: ["accessToken", "refreshToken"] }
|
|
3118
|
+
},
|
|
3119
|
+
{
|
|
3120
|
+
id: "exchange-missing-client",
|
|
3121
|
+
route: "POST /SASjsApi/auth/token",
|
|
3122
|
+
description: "refuses an exchange with no clientId as 400, naming the field",
|
|
3123
|
+
identity: SIGNED_IN,
|
|
3124
|
+
request: { body: { code: "seed-code" } },
|
|
3125
|
+
expect: { status: 400, error: '"clientId" is required' }
|
|
3126
|
+
},
|
|
3127
|
+
{
|
|
3128
|
+
id: "exchange-code-unknown",
|
|
3129
|
+
route: "POST /SASjsApi/auth/token",
|
|
3130
|
+
description: "refuses a code the host never issued",
|
|
3131
|
+
request: { body: { clientId: "web_app", code: "not-a-code" } },
|
|
3132
|
+
expect: { status: 401, error: "Invalid Auth Code" }
|
|
3133
|
+
},
|
|
3134
|
+
{
|
|
3135
|
+
id: "exchange-code-other-client",
|
|
3136
|
+
route: "POST /SASjsApi/auth/token",
|
|
3137
|
+
description: "refuses a code presented for another client, as a bad code",
|
|
3138
|
+
identity: SIGNED_IN,
|
|
3139
|
+
request: { body: { clientId: "sasjs", code: "seed-code" } },
|
|
3140
|
+
expect: { status: 401, error: "Invalid Auth Code" }
|
|
3141
|
+
},
|
|
3142
|
+
{
|
|
3143
|
+
id: "exchange-code-client-gone",
|
|
3144
|
+
route: "POST /SASjsApi/auth/token",
|
|
3145
|
+
description: "refuses a valid code whose client no longer exists",
|
|
3146
|
+
identity: SIGNED_IN,
|
|
3147
|
+
request: { body: { clientId: "removed_client", code: "seed-orphan-code" } },
|
|
3148
|
+
expect: { status: 401, error: "Invalid clientId." }
|
|
3149
|
+
},
|
|
3150
|
+
{
|
|
3151
|
+
id: "exchange-code-client-gone-with-pair",
|
|
3152
|
+
route: "POST /SASjsApi/auth/token",
|
|
3153
|
+
description: "refuses an exchange whose client is gone even when a pair was issued before it was deleted",
|
|
3154
|
+
identity: SIGNED_IN,
|
|
3155
|
+
request: {
|
|
3156
|
+
body: { clientId: "removed_client", code: "seed-orphan-pair-code" }
|
|
3157
|
+
},
|
|
3158
|
+
expect: { status: 401, error: "Invalid clientId." }
|
|
3159
|
+
},
|
|
3160
|
+
{
|
|
3161
|
+
id: "exchange-code-deactivated",
|
|
3162
|
+
route: "POST /SASjsApi/auth/token",
|
|
3163
|
+
description: "refuses an exchange for an account that has been deactivated",
|
|
3164
|
+
identity: DEACTIVATED,
|
|
3165
|
+
request: { body: { clientId: "web_app", code: "seed-code" } },
|
|
3166
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3167
|
+
},
|
|
3168
|
+
{
|
|
3169
|
+
id: "refresh",
|
|
3170
|
+
route: "POST /SASjsApi/auth/refresh",
|
|
3171
|
+
description: "mints a new pair from a refresh token",
|
|
3172
|
+
identity: SIGNED_IN,
|
|
3173
|
+
request: { token: "seed-refresh" },
|
|
3174
|
+
expect: { status: 200, keys: ["accessToken", "refreshToken"] }
|
|
3175
|
+
},
|
|
3176
|
+
{
|
|
3177
|
+
id: "refresh-invalid",
|
|
3178
|
+
route: "POST /SASjsApi/auth/refresh",
|
|
3179
|
+
description: "refuses a refresh token it did not issue",
|
|
3180
|
+
request: { token: "not-a-token" },
|
|
3181
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3182
|
+
},
|
|
3183
|
+
{
|
|
3184
|
+
id: "refresh-without-token",
|
|
3185
|
+
route: "POST /SASjsApi/auth/refresh",
|
|
3186
|
+
description: "refuses a refresh with no token at all",
|
|
3187
|
+
request: {},
|
|
3188
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3189
|
+
},
|
|
3190
|
+
{
|
|
3191
|
+
id: "refresh-client-gone",
|
|
3192
|
+
route: "POST /SASjsApi/auth/refresh",
|
|
3193
|
+
description: "refuses a refresh whose client no longer exists",
|
|
3194
|
+
identity: SIGNED_IN,
|
|
3195
|
+
request: { token: "seed-refresh-orphan" },
|
|
3196
|
+
expect: { status: 401, error: "Invalid clientId." }
|
|
3197
|
+
},
|
|
3198
|
+
{
|
|
3199
|
+
id: "refresh-deactivated",
|
|
3200
|
+
route: "POST /SASjsApi/auth/refresh",
|
|
3201
|
+
description: "refuses a refresh for an account that has been deactivated, as the server does on every request",
|
|
3202
|
+
identity: DEACTIVATED,
|
|
3203
|
+
request: { token: "seed-refresh" },
|
|
3204
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3205
|
+
},
|
|
3206
|
+
{
|
|
3207
|
+
id: "logout",
|
|
3208
|
+
route: "POST /SASjsApi/auth/logout",
|
|
3209
|
+
description: "revokes the caller pair and answers 204",
|
|
3210
|
+
identity: SIGNED_IN,
|
|
3211
|
+
request: { token: "seed-access" },
|
|
3212
|
+
expect: { status: 204 }
|
|
3213
|
+
},
|
|
3214
|
+
{
|
|
3215
|
+
id: "logout-without-token",
|
|
3216
|
+
route: "POST /SASjsApi/auth/logout",
|
|
3217
|
+
description: "refuses a logout with no valid token",
|
|
3218
|
+
request: {},
|
|
3219
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3220
|
+
},
|
|
3221
|
+
{
|
|
3222
|
+
id: "session",
|
|
3223
|
+
route: "GET /SASjsApi/session",
|
|
3224
|
+
description: "reports the caller the token names",
|
|
3225
|
+
identity: SIGNED_IN,
|
|
3226
|
+
request: { token: "seed-access" },
|
|
3227
|
+
expect: { status: 200, partial: user2 }
|
|
3228
|
+
},
|
|
3229
|
+
{
|
|
3230
|
+
id: "session-deactivated",
|
|
3231
|
+
route: "GET /SASjsApi/session",
|
|
3232
|
+
description: "stops resolving a token whose account has been deactivated",
|
|
3233
|
+
identity: DEACTIVATED,
|
|
3234
|
+
request: { token: "seed-access" },
|
|
3235
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3236
|
+
},
|
|
3237
|
+
{
|
|
3238
|
+
id: "session-without-token",
|
|
3239
|
+
route: "GET /SASjsApi/session",
|
|
3240
|
+
description: "refuses the session read with no caller",
|
|
3241
|
+
request: {},
|
|
3242
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3243
|
+
},
|
|
3244
|
+
{
|
|
3245
|
+
id: "sign-out",
|
|
3246
|
+
route: "GET /SASLogon/logout",
|
|
3247
|
+
description: "answers the body the browser sign-out returns",
|
|
3248
|
+
request: {},
|
|
3249
|
+
expect: { status: 200, contents: "OK!" }
|
|
3250
|
+
}
|
|
3251
|
+
];
|
|
3252
|
+
|
|
2472
3253
|
// src/corpus/runner.ts
|
|
2473
3254
|
var servicesFor = (testCase) => ({
|
|
2474
|
-
drive: createDriveService(new MemoryDriveStore(testCase.seed || {}))
|
|
3255
|
+
drive: createDriveService(new MemoryDriveStore(testCase.seed || {})),
|
|
3256
|
+
identity: createIdentityService(
|
|
3257
|
+
new MemoryIdentityStore(testCase.identity || DEFAULT_IDENTITY)
|
|
3258
|
+
)
|
|
2475
3259
|
});
|
|
2476
3260
|
var runCase = async (testCase, services = servicesFor(testCase)) => {
|
|
2477
3261
|
try {
|