@sasjs/api 0.8.0 → 0.10.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 +239 -18
- package/build/cjs/code/index.js +12 -0
- package/build/cjs/code/service.js +57 -0
- package/build/cjs/corpus/code.js +62 -0
- package/build/cjs/corpus/records.js +64 -0
- package/build/cjs/host/dispatch.js +42 -3
- package/build/cjs/identity/records.js +38 -1
- package/build/cjs/identity/routeAccess.js +77 -0
- package/build/cjs/index.js +5 -1
- package/build/cjs/routes/overlay.js +12 -12
- package/build/esm/code/index.d.ts +10 -0
- package/build/esm/code/index.d.ts.map +1 -0
- package/build/esm/code/index.js +8 -0
- package/build/esm/code/service.d.ts +40 -0
- package/build/esm/code/service.d.ts.map +1 -0
- package/build/esm/code/service.js +53 -0
- package/build/esm/corpus/code.d.ts +17 -0
- package/build/esm/corpus/code.d.ts.map +1 -0
- package/build/esm/corpus/code.js +59 -0
- package/build/esm/corpus/drive.d.ts +9 -0
- package/build/esm/corpus/drive.d.ts.map +1 -1
- package/build/esm/corpus/records.d.ts.map +1 -1
- package/build/esm/corpus/records.js +64 -0
- package/build/esm/host/dispatch.d.ts +3 -0
- package/build/esm/host/dispatch.d.ts.map +1 -1
- package/build/esm/host/dispatch.js +42 -3
- package/build/esm/identity/records.d.ts +20 -1
- package/build/esm/identity/records.d.ts.map +1 -1
- package/build/esm/identity/records.js +38 -1
- package/build/esm/identity/routeAccess.d.ts +57 -0
- package/build/esm/identity/routeAccess.d.ts.map +1 -0
- package/build/esm/identity/routeAccess.js +72 -0
- package/build/esm/index.d.ts +3 -1
- package/build/esm/index.d.ts.map +1 -1
- package/build/esm/index.js +3 -0
- package/build/esm/routes/overlay.d.ts.map +1 -1
- package/build/esm/routes/overlay.js +12 -12
- package/package.json +9 -1
|
@@ -44,6 +44,10 @@ export interface PermissionResponse {
|
|
|
44
44
|
/** The uid of the group the rule is assigned to, when it is a group's. */
|
|
45
45
|
group?: string;
|
|
46
46
|
}
|
|
47
|
+
/** What `authorizedRoutes` and `myAuthorizedRoutes` answer. */
|
|
48
|
+
export interface AuthorizedRoutesResponse {
|
|
49
|
+
paths: string[];
|
|
50
|
+
}
|
|
47
51
|
export interface RecordService {
|
|
48
52
|
listUsers(caller: Subject | undefined): Promise<UserResponse[]>;
|
|
49
53
|
getUser(caller: Subject | undefined, by: {
|
|
@@ -88,6 +92,13 @@ export interface RecordService {
|
|
|
88
92
|
deleteGroup(caller: Subject | undefined, uid: string): Promise<void>;
|
|
89
93
|
addUserToGroup(caller: Subject | undefined, groupUid: string, userUid: string): Promise<GroupDetailsResponse>;
|
|
90
94
|
removeUserFromGroup(caller: Subject | undefined, groupUid: string, userUid: string): Promise<GroupDetailsResponse>;
|
|
95
|
+
/**
|
|
96
|
+
* The routes that accept a rule. Any authenticated caller may read it: it is
|
|
97
|
+
* the inventory, not a statement about what this caller holds.
|
|
98
|
+
*/
|
|
99
|
+
listAuthorizedRoutes(caller: Subject | undefined): Promise<AuthorizedRoutesResponse>;
|
|
100
|
+
/** The routes this caller may use, from the rules that bear on them. */
|
|
101
|
+
myAuthorizedRoutes(caller: Subject | undefined): Promise<AuthorizedRoutesResponse>;
|
|
91
102
|
listPermissions(caller: Subject | undefined): Promise<PermissionResponse[]>;
|
|
92
103
|
createPermission(caller: Subject | undefined, body: {
|
|
93
104
|
path: string;
|
|
@@ -109,5 +120,13 @@ export interface RecordService {
|
|
|
109
120
|
}): Promise<void>;
|
|
110
121
|
}
|
|
111
122
|
export declare const sanitiseDisplayName: (value: unknown) => string;
|
|
112
|
-
export
|
|
123
|
+
export interface RecordServiceOptions {
|
|
124
|
+
/**
|
|
125
|
+
* The routes that accept a permission rule. Defaults to the ones this package
|
|
126
|
+
* knows; a host that streams applications adds `/AppStream/<app>` for each,
|
|
127
|
+
* because that set is the host's configuration and not a fact about the API.
|
|
128
|
+
*/
|
|
129
|
+
grantableRoutes?: string[];
|
|
130
|
+
}
|
|
131
|
+
export declare const createRecordService: (store: IdentityStore, options?: RecordServiceOptions) => RecordService;
|
|
113
132
|
//# sourceMappingURL=records.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../../../src/identity/records.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../../../src/identity/records.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* the server sends.
|
|
15
15
|
*/
|
|
16
16
|
import { ApiError } from '../types.js';
|
|
17
|
+
import { ALWAYS_VISIBLE_ROUTES, DEFAULT_GRANTABLE_ROUTES, isRouteGranted, topLevelRouteFor } from './routeAccess.js';
|
|
17
18
|
/** The name the server gives the group every account is added to on creation. */
|
|
18
19
|
const ALL_USERS_GROUP = 'all-users';
|
|
19
20
|
/** The group whose membership the API refuses to change. */
|
|
@@ -79,7 +80,8 @@ const asGroup = (group) => ({
|
|
|
79
80
|
name: group.name,
|
|
80
81
|
description: group.description
|
|
81
82
|
});
|
|
82
|
-
export const createRecordService = (store) => {
|
|
83
|
+
export const createRecordService = (store, options = {}) => {
|
|
84
|
+
const grantableRoutes = options.grantableRoutes || DEFAULT_GRANTABLE_ROUTES;
|
|
83
85
|
const groupDetails = async (group) => {
|
|
84
86
|
const members = [];
|
|
85
87
|
for (const uid of group.users) {
|
|
@@ -281,6 +283,41 @@ export const createRecordService = (store) => {
|
|
|
281
283
|
throw new ApiError(400, 'Unable to update group.');
|
|
282
284
|
return groupDetails(updated);
|
|
283
285
|
},
|
|
286
|
+
async listAuthorizedRoutes(caller) {
|
|
287
|
+
// an identity is required because the route is about a caller's access,
|
|
288
|
+
// even though what it returns is the whole inventory
|
|
289
|
+
await requireCaller(store, caller);
|
|
290
|
+
return { paths: [...grantableRoutes] };
|
|
291
|
+
},
|
|
292
|
+
async myAuthorizedRoutes(caller) {
|
|
293
|
+
const reader = await requireCaller(store, caller);
|
|
294
|
+
// an administrator holds every route, and the always-visible ones are
|
|
295
|
+
// visible whatever the rules say
|
|
296
|
+
if (reader.isAdmin)
|
|
297
|
+
return { paths: [...grantableRoutes] };
|
|
298
|
+
const rules = await store.permissions();
|
|
299
|
+
const mine = {
|
|
300
|
+
user: rules
|
|
301
|
+
.filter((rule) => rule.user === reader.uid)
|
|
302
|
+
.map((rule) => ({ path: rule.path, setting: rule.setting })),
|
|
303
|
+
group: rules
|
|
304
|
+
.filter((rule) => rule.group !== undefined && reader.groups.includes(rule.group))
|
|
305
|
+
.map((rule) => ({ path: rule.path, setting: rule.setting }))
|
|
306
|
+
};
|
|
307
|
+
// a grant on the reserved public group admits any caller, and it is
|
|
308
|
+
// checked before the rules so no user or group rule can take it away
|
|
309
|
+
const publicGroup = (await store.groups()).find((group) => group.name === 'public');
|
|
310
|
+
const publicGrants = publicGroup
|
|
311
|
+
? rules
|
|
312
|
+
.filter((rule) => rule.group === publicGroup.uid && rule.setting === 'Grant')
|
|
313
|
+
.map((rule) => rule.path)
|
|
314
|
+
: [];
|
|
315
|
+
return {
|
|
316
|
+
paths: grantableRoutes.filter((path) => ALWAYS_VISIBLE_ROUTES.includes(path) ||
|
|
317
|
+
publicGrants.includes(path) ||
|
|
318
|
+
isRouteGranted(mine, path, topLevelRouteFor(path)))
|
|
319
|
+
};
|
|
320
|
+
},
|
|
284
321
|
async listPermissions(caller) {
|
|
285
322
|
const reader = await requireCaller(store, caller);
|
|
286
323
|
const rules = await store.permissions();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which routes a caller may use, from the permission rules alone.
|
|
3
|
+
*
|
|
4
|
+
* This is the answer `GET /SASjsApi/info/myAuthorizedRoutes` gives, and it is the
|
|
5
|
+
* same decision the gate makes when the route is actually requested - a
|
|
6
|
+
* web interface that offers a route the gate then refuses is the bug this exists
|
|
7
|
+
* to prevent, so the two must not be two implementations. A host that gates its
|
|
8
|
+
* own middleware should call `isRouteGranted` rather than re-derive it.
|
|
9
|
+
*/
|
|
10
|
+
/** The two top-level routes, which a rule can address to cover everything below. */
|
|
11
|
+
export declare const TOP_LEVEL_ROUTES: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Routes any authenticated caller may use whatever the rules say. `/AppStream` is
|
|
14
|
+
* the navigation page whose job is to list the apps the caller may open, so
|
|
15
|
+
* gating it on a grant that lists them would hide the page that leads to them.
|
|
16
|
+
*/
|
|
17
|
+
export declare const ALWAYS_VISIBLE_ROUTES: string[];
|
|
18
|
+
/**
|
|
19
|
+
* The routes that accept a rule, before a host adds its own. A host with
|
|
20
|
+
* streamed applications extends this with `/AppStream/<app>` for each one, which
|
|
21
|
+
* is why the inventory is passed in rather than derived from a fixed list here.
|
|
22
|
+
*/
|
|
23
|
+
export declare const STATIC_AUTHORIZED_ROUTES: string[];
|
|
24
|
+
/** The route inventory when the host states no additions of its own. */
|
|
25
|
+
export declare const DEFAULT_GRANTABLE_ROUTES: string[];
|
|
26
|
+
export interface RouteRule {
|
|
27
|
+
path: string;
|
|
28
|
+
setting: string;
|
|
29
|
+
}
|
|
30
|
+
/** The rules that can bear on one caller. */
|
|
31
|
+
export interface RouteRules {
|
|
32
|
+
user: RouteRule[];
|
|
33
|
+
group: RouteRule[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The top-level route that governs a request: `/SASjsApi/code/execute` is
|
|
37
|
+
* governed by `/SASjsApi`, and a rule on the top-level route applies to
|
|
38
|
+
* everything beneath it.
|
|
39
|
+
*/
|
|
40
|
+
export declare const topLevelRouteFor: (routePath: string) => string;
|
|
41
|
+
/**
|
|
42
|
+
* Whether these rules grant access to `path`.
|
|
43
|
+
*
|
|
44
|
+
* The order is the documented one and the only implementation of it:
|
|
45
|
+
*
|
|
46
|
+
* 1. a rule addressed to the user on the path itself decides either way;
|
|
47
|
+
* 2. otherwise a rule addressed to the user on the top-level route decides;
|
|
48
|
+
* 3. otherwise a group grant on the path allows, and a group deny on the path
|
|
49
|
+
* blocks the top-level check - but a grant from another group still allows;
|
|
50
|
+
* 4. otherwise a group grant on the top-level route allows;
|
|
51
|
+
* 5. otherwise denied.
|
|
52
|
+
*
|
|
53
|
+
* A grant beats a deny at the same level, which is why step 3 looks at every
|
|
54
|
+
* group rule rather than stopping at the first deny.
|
|
55
|
+
*/
|
|
56
|
+
export declare const isRouteGranted: (rules: RouteRules, path: string, topLevelRoute?: string) => boolean;
|
|
57
|
+
//# sourceMappingURL=routeAccess.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routeAccess.d.ts","sourceRoot":"","sources":["../../../src/identity/routeAccess.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,UAA8B,CAAA;AAE3D;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,UAAiB,CAAA;AAEnD;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,UASpC,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,UAGpC,CAAA;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,6CAA6C;AAC7C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,EAAE,CAAA;IACjB,KAAK,EAAE,SAAS,EAAE,CAAA;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,WAAW,MAAM,KAAG,MAGrC,CAAA;AAQhB;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,UAAU,EACjB,MAAM,MAAM,EACZ,gBAAe,MAA+B,KAC7C,OAcF,CAAA"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which routes a caller may use, from the permission rules alone.
|
|
3
|
+
*
|
|
4
|
+
* This is the answer `GET /SASjsApi/info/myAuthorizedRoutes` gives, and it is the
|
|
5
|
+
* same decision the gate makes when the route is actually requested - a
|
|
6
|
+
* web interface that offers a route the gate then refuses is the bug this exists
|
|
7
|
+
* to prevent, so the two must not be two implementations. A host that gates its
|
|
8
|
+
* own middleware should call `isRouteGranted` rather than re-derive it.
|
|
9
|
+
*/
|
|
10
|
+
/** The two top-level routes, which a rule can address to cover everything below. */
|
|
11
|
+
export const TOP_LEVEL_ROUTES = ['/AppStream', '/SASjsApi'];
|
|
12
|
+
/**
|
|
13
|
+
* Routes any authenticated caller may use whatever the rules say. `/AppStream` is
|
|
14
|
+
* the navigation page whose job is to list the apps the caller may open, so
|
|
15
|
+
* gating it on a grant that lists them would hide the page that leads to them.
|
|
16
|
+
*/
|
|
17
|
+
export const ALWAYS_VISIBLE_ROUTES = ['/AppStream'];
|
|
18
|
+
/**
|
|
19
|
+
* The routes that accept a rule, before a host adds its own. A host with
|
|
20
|
+
* streamed applications extends this with `/AppStream/<app>` for each one, which
|
|
21
|
+
* is why the inventory is passed in rather than derived from a fixed list here.
|
|
22
|
+
*/
|
|
23
|
+
export const STATIC_AUTHORIZED_ROUTES = [
|
|
24
|
+
'/SASjsApi/code/execute',
|
|
25
|
+
'/SASjsApi/stp/execute',
|
|
26
|
+
'/SASjsApi/drive/deploy',
|
|
27
|
+
'/SASjsApi/drive/deploy/upload',
|
|
28
|
+
'/SASjsApi/drive/file',
|
|
29
|
+
'/SASjsApi/drive/folder',
|
|
30
|
+
'/SASjsApi/drive/fileTree',
|
|
31
|
+
'/SASjsApi/drive/rename'
|
|
32
|
+
];
|
|
33
|
+
/** The route inventory when the host states no additions of its own. */
|
|
34
|
+
export const DEFAULT_GRANTABLE_ROUTES = [
|
|
35
|
+
...TOP_LEVEL_ROUTES,
|
|
36
|
+
...STATIC_AUTHORIZED_ROUTES
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* The top-level route that governs a request: `/SASjsApi/code/execute` is
|
|
40
|
+
* governed by `/SASjsApi`, and a rule on the top-level route applies to
|
|
41
|
+
* everything beneath it.
|
|
42
|
+
*/
|
|
43
|
+
export const topLevelRouteFor = (routePath) => TOP_LEVEL_ROUTES.find((route) => routePath.toLowerCase().startsWith(route.toLowerCase())) ?? routePath;
|
|
44
|
+
const settingOn = (rules, path) => rules.find((rule) => rule.path === path)?.setting;
|
|
45
|
+
const GRANT = 'Grant';
|
|
46
|
+
const DENY = 'Deny';
|
|
47
|
+
/**
|
|
48
|
+
* Whether these rules grant access to `path`.
|
|
49
|
+
*
|
|
50
|
+
* The order is the documented one and the only implementation of it:
|
|
51
|
+
*
|
|
52
|
+
* 1. a rule addressed to the user on the path itself decides either way;
|
|
53
|
+
* 2. otherwise a rule addressed to the user on the top-level route decides;
|
|
54
|
+
* 3. otherwise a group grant on the path allows, and a group deny on the path
|
|
55
|
+
* blocks the top-level check - but a grant from another group still allows;
|
|
56
|
+
* 4. otherwise a group grant on the top-level route allows;
|
|
57
|
+
* 5. otherwise denied.
|
|
58
|
+
*
|
|
59
|
+
* A grant beats a deny at the same level, which is why step 3 looks at every
|
|
60
|
+
* group rule rather than stopping at the first deny.
|
|
61
|
+
*/
|
|
62
|
+
export const isRouteGranted = (rules, path, topLevelRoute = topLevelRouteFor(path)) => {
|
|
63
|
+
const userSetting = settingOn(rules.user, path) ?? settingOn(rules.user, topLevelRoute);
|
|
64
|
+
if (userSetting)
|
|
65
|
+
return userSetting === GRANT;
|
|
66
|
+
const onPath = rules.group.filter((rule) => rule.path === path);
|
|
67
|
+
if (onPath.some((rule) => rule.setting === GRANT))
|
|
68
|
+
return true;
|
|
69
|
+
if (onPath.some((rule) => rule.setting === DENY))
|
|
70
|
+
return false;
|
|
71
|
+
return rules.group.some((rule) => rule.path === topLevelRoute && rule.setting === GRANT);
|
|
72
|
+
};
|
package/build/esm/index.d.ts
CHANGED
|
@@ -25,10 +25,12 @@ export type { RouteMatch } from './routes/index.js';
|
|
|
25
25
|
export { OVERLAY, TAG_CAPABILITIES } from './routes/overlay.js';
|
|
26
26
|
export type { RouteOverlay } from './routes/overlay.js';
|
|
27
27
|
export { dispatch, HANDLED_ROUTES } from './host/dispatch.js';
|
|
28
|
+
export * from './identity/routeAccess.js';
|
|
28
29
|
export type { ApiRequest, ApiResponse, Services } from './host/dispatch.js';
|
|
29
30
|
export { createIdentityService } from './identity/service.js';
|
|
30
31
|
export { createRecordService, sanitiseDisplayName } from './identity/records.js';
|
|
31
|
-
export
|
|
32
|
+
export { CODE_CORPUS } from './corpus/code.js';
|
|
33
|
+
export type { AuthorizedRoutesResponse, GroupDetailsResponse, GroupResponse, PermissionResponse, RecordServiceOptions, RecordService, UserDetailsResponse, UserResponse } from './identity/records.js';
|
|
32
34
|
export type { IdentityService, LoginResult, SessionResult } from './identity/service.js';
|
|
33
35
|
export { MemoryIdentityStore } from './identity/memory-store.js';
|
|
34
36
|
export { DEFAULT_IDENTITY, IDENTITY_FORMAT, parseIdentity } from './identity/fixture.js';
|
package/build/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,MAAM,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EAClB,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;
|
|
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,EACL,aAAa,EACb,eAAe,EACf,gBAAgB,EACjB,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA"}
|
package/build/esm/index.js
CHANGED
|
@@ -21,8 +21,11 @@ export { MemoryDriveStore } from './drive/memory-store.js';
|
|
|
21
21
|
export { ROUTES, routeById, findRoute } from './routes/index.js';
|
|
22
22
|
export { OVERLAY, TAG_CAPABILITIES } from './routes/overlay.js';
|
|
23
23
|
export { dispatch, HANDLED_ROUTES } from './host/dispatch.js';
|
|
24
|
+
// a host gating its own middleware reads the same decision the inventory reports
|
|
25
|
+
export * from './identity/routeAccess.js';
|
|
24
26
|
export { createIdentityService } from './identity/service.js';
|
|
25
27
|
export { createRecordService, sanitiseDisplayName } from './identity/records.js';
|
|
28
|
+
export { CODE_CORPUS } from './corpus/code.js';
|
|
26
29
|
export { MemoryIdentityStore } from './identity/memory-store.js';
|
|
27
30
|
export { DEFAULT_IDENTITY, IDENTITY_FORMAT, parseIdentity } from './identity/fixture.js';
|
|
28
31
|
export { DRIVE_CORPUS } from './corpus/drive.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,yDAAyD;AACzD,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CAAC;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;
|
|
1
|
+
{"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,yDAAyD;AACzD,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CAAC;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;AA2CD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAyNhD,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,UAAU,EAA8C,CAAA"}
|
|
@@ -23,11 +23,9 @@ export const TAG_CAPABILITIES = {
|
|
|
23
23
|
* thing thirty times.
|
|
24
24
|
*/
|
|
25
25
|
const DIRECTORY = 'Synchronising with an external auth provider needs a directory port, which this package does not have: a host decides which directory it holds accounts in, and answers this itself.';
|
|
26
|
-
const 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.';
|
|
27
26
|
const 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.';
|
|
28
27
|
const 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.";
|
|
29
28
|
const HOST_CONFIG = "The body is the host's own configuration - ports, run times and providers - which a mock states for itself and this package cannot know.";
|
|
30
|
-
const PERMISSIONS = "The route inventory is the host's permission model, which this package does not hold.";
|
|
31
29
|
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
30
|
const WEB_BUNDLE = 'The host serves its own web bundle; there is no API decision here for this package to answer.';
|
|
33
31
|
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.";
|
|
@@ -94,8 +92,14 @@ export const OVERLAY = {
|
|
|
94
92
|
// Code - the editor's three calls, and the runtime one
|
|
95
93
|
'POST /SASjsApi/code/execute': { implemented: false, reason: RUNTIME },
|
|
96
94
|
'POST /SASjsApi/code/trigger': { implemented: false, reason: RUNTIME },
|
|
97
|
-
'POST /SASjsApi/code/lint': {
|
|
98
|
-
|
|
95
|
+
'POST /SASjsApi/code/lint': {
|
|
96
|
+
implemented: true,
|
|
97
|
+
capabilities: ['identity']
|
|
98
|
+
},
|
|
99
|
+
'POST /SASjsApi/code/format': {
|
|
100
|
+
implemented: true,
|
|
101
|
+
capabilities: ['identity']
|
|
102
|
+
},
|
|
99
103
|
// Drive - the file and folder rules, and the three deploy routes
|
|
100
104
|
// The body is the file itself, so there is nothing to annotate
|
|
101
105
|
'GET /SASjsApi/drive/file': { implemented: true },
|
|
@@ -174,16 +178,12 @@ export const OVERLAY = {
|
|
|
174
178
|
// Info - what the host is, and what this caller may use
|
|
175
179
|
'GET /SASjsApi/info': { implemented: false, reason: HOST_CONFIG },
|
|
176
180
|
'GET /SASjsApi/info/authorizedRoutes': {
|
|
177
|
-
implemented:
|
|
178
|
-
|
|
179
|
-
// identity even though the rest of the Info tag needs nothing
|
|
180
|
-
capabilities: ['identity'],
|
|
181
|
-
reason: PERMISSIONS
|
|
181
|
+
implemented: true,
|
|
182
|
+
capabilities: ['identity']
|
|
182
183
|
},
|
|
183
184
|
'GET /SASjsApi/info/myAuthorizedRoutes': {
|
|
184
|
-
implemented:
|
|
185
|
-
capabilities: ['identity']
|
|
186
|
-
reason: PERMISSIONS
|
|
185
|
+
implemented: true,
|
|
186
|
+
capabilities: ['identity']
|
|
187
187
|
},
|
|
188
188
|
// Permission - the rules the inventory above is derived from
|
|
189
189
|
'GET /SASjsApi/permission': {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sasjs/api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
"types": "./build/esm/index.d.ts",
|
|
18
18
|
"import": "./build/esm/index.js",
|
|
19
19
|
"require": "./build/cjs/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./code": {
|
|
22
|
+
"types": "./build/esm/code/index.d.ts",
|
|
23
|
+
"import": "./build/esm/code/index.js",
|
|
24
|
+
"require": "./build/cjs/code/index.js"
|
|
20
25
|
}
|
|
21
26
|
},
|
|
22
27
|
"files": [
|
|
@@ -60,5 +65,8 @@
|
|
|
60
65
|
"deepmerge": "$deepmerge",
|
|
61
66
|
"glob": "14.0.3",
|
|
62
67
|
"minimatch": "11.0.4"
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"@sasjs/lint": "5.0.0"
|
|
63
71
|
}
|
|
64
72
|
}
|