@enkaku/server 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/lib/access-control.d.ts +13 -0
- package/lib/access-control.d.ts.map +1 -0
- package/lib/access-control.js +47 -0
- package/lib/rejections.js +3 -3
- package/lib/server.d.ts +15 -2
- package/lib/server.d.ts.map +1 -1
- package/lib/server.js +60 -12
- package/package.json +7 -6
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SignedToken } from '@enkaku/jwt';
|
|
2
|
+
import type { AnyClientPayloadOf, AnyDefinitions } from '@enkaku/protocol';
|
|
3
|
+
export type CommandAccessRecord = Record<string, boolean | Array<string>>;
|
|
4
|
+
export type CommandAccessPayload = {
|
|
5
|
+
iss?: string;
|
|
6
|
+
sub?: string;
|
|
7
|
+
aud?: string;
|
|
8
|
+
cmd?: string;
|
|
9
|
+
exp?: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function checkCommandAccess(record: CommandAccessRecord, token: SignedToken<CommandAccessPayload>, atTime?: number): Promise<void>;
|
|
12
|
+
export declare function checkClientToken<Definition extends AnyDefinitions>(serverID: string, record: CommandAccessRecord, token: SignedToken<AnyClientPayloadOf<Definition>>, atTime?: number): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=access-control.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-control.d.ts","sourceRoot":"","sources":["../src/access-control.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAE1E,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAA;AAEzE,MAAM,MAAM,oBAAoB,GAAG;IACjC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAA;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,WAAW,CAAC,oBAAoB,CAAC,EACxC,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAsB,gBAAgB,CAAC,UAAU,SAAS,cAAc,EACtE,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,WAAW,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC,EAClD,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAgBf"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { assertNonExpired, checkCapability, hasPartsMatch } from '@enkaku/capability';
|
|
2
|
+
export async function checkCommandAccess(record, token, atTime) {
|
|
3
|
+
const payload = token.payload;
|
|
4
|
+
if (payload.cmd == null) {
|
|
5
|
+
throw new Error('No command to check');
|
|
6
|
+
}
|
|
7
|
+
const subject = payload.sub ?? payload.iss;
|
|
8
|
+
for (const [command, access] of Object.entries(record)){
|
|
9
|
+
if (hasPartsMatch(payload.cmd, command)) {
|
|
10
|
+
if (access === true) {
|
|
11
|
+
// Command can be publicly accessed
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (access === false) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (!access.includes(subject)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
await checkCapability({
|
|
22
|
+
act: payload.cmd,
|
|
23
|
+
res: subject
|
|
24
|
+
}, payload, atTime);
|
|
25
|
+
return;
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
throw new Error('Access denied');
|
|
30
|
+
}
|
|
31
|
+
export async function checkClientToken(serverID, record, token, atTime) {
|
|
32
|
+
const payload = token.payload;
|
|
33
|
+
if (payload.iss === serverID) {
|
|
34
|
+
// If issuer uses the server's signer, only check audience and expiration if provided
|
|
35
|
+
if (payload.aud != null && payload.aud !== serverID) {
|
|
36
|
+
throw new Error('Invalid audience');
|
|
37
|
+
}
|
|
38
|
+
if (payload.exp != null) {
|
|
39
|
+
assertNonExpired(payload, atTime);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (payload.aud !== serverID) {
|
|
44
|
+
throw new Error('Invalid audience');
|
|
45
|
+
}
|
|
46
|
+
await checkCommandAccess(record, token, atTime);
|
|
47
|
+
}
|
package/lib/rejections.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export var RejectionReason
|
|
2
|
-
(function(RejectionReason) {
|
|
1
|
+
export var RejectionReason = /*#__PURE__*/ function(RejectionReason) {
|
|
3
2
|
RejectionReason[RejectionReason["ABORT"] = 1] = "ABORT";
|
|
4
3
|
RejectionReason[RejectionReason["ERROR"] = 2] = "ERROR";
|
|
5
4
|
RejectionReason[RejectionReason["TIMEOUT"] = 3] = "TIMEOUT";
|
|
6
|
-
|
|
5
|
+
return RejectionReason;
|
|
6
|
+
}({});
|
|
7
7
|
export class Rejection {
|
|
8
8
|
#reason;
|
|
9
9
|
#info;
|
package/lib/server.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AnyDefinitions, ServerTransportOf } from '@enkaku/protocol';
|
|
2
2
|
import { type Disposer } from '@enkaku/util';
|
|
3
|
+
import { type CommandAccessRecord } from './access-control.js';
|
|
3
4
|
import { type RejectionType } from './rejections.js';
|
|
4
5
|
import type { CommandHandlers, HandlerController } from './types.js';
|
|
5
6
|
export type HandleMessagesParams<Definitions extends AnyDefinitions> = {
|
|
@@ -8,12 +9,24 @@ export type HandleMessagesParams<Definitions extends AnyDefinitions> = {
|
|
|
8
9
|
reject: (rejection: RejectionType) => void;
|
|
9
10
|
signal: AbortSignal;
|
|
10
11
|
transport: ServerTransportOf<Definitions>;
|
|
11
|
-
}
|
|
12
|
+
} & ({
|
|
13
|
+
insecure: true;
|
|
14
|
+
} | {
|
|
15
|
+
insecure: false;
|
|
16
|
+
serverID: string;
|
|
17
|
+
access: CommandAccessRecord;
|
|
18
|
+
});
|
|
12
19
|
export type ServeParams<Definitions extends AnyDefinitions> = {
|
|
13
20
|
handlers: CommandHandlers<Definitions>;
|
|
14
21
|
signal?: AbortSignal;
|
|
15
22
|
transport: ServerTransportOf<Definitions>;
|
|
16
|
-
}
|
|
23
|
+
} & ({
|
|
24
|
+
insecure: true;
|
|
25
|
+
} | {
|
|
26
|
+
insecure?: false;
|
|
27
|
+
id: string;
|
|
28
|
+
access?: CommandAccessRecord;
|
|
29
|
+
});
|
|
17
30
|
export type Server = Disposer & {
|
|
18
31
|
rejections: ReadableStream<RejectionType>;
|
|
19
32
|
};
|
package/lib/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,cAAc,EAEd,iBAAiB,EAClB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,KAAK,QAAQ,EAAkB,MAAM,cAAc,CAAA;AAE5D,OAAO,EAAE,KAAK,mBAAmB,EAAoB,MAAM,qBAAqB,CAAA;AAKhF,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACpE,OAAO,KAAK,EAEV,eAAe,EAEf,iBAAiB,EAClB,MAAM,YAAY,CAAA;AAQnB,MAAM,MAAM,oBAAoB,CAAC,WAAW,SAAS,cAAc,IAAI;IACrE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;IAC9C,QAAQ,EAAE,eAAe,CAAC,WAAW,CAAC,CAAA;IACtC,MAAM,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,IAAI,CAAA;IAC1C,MAAM,EAAE,WAAW,CAAA;IACnB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,CAAA;CAC1C,GAAG,CAAC;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAAA;AAwH7F,MAAM,MAAM,WAAW,CAAC,WAAW,SAAS,cAAc,IAAI;IAC5D,QAAQ,EAAE,eAAe,CAAC,WAAW,CAAC,CAAA;IACtC,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,CAAA;CAC1C,GAAG,CAAC;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAAA;AAEzF,MAAM,MAAM,MAAM,GAAG,QAAQ,GAAG;IAC9B,UAAU,EAAE,cAAc,CAAC,aAAa,CAAC,CAAA;CAC1C,CAAA;AAED,wBAAgB,KAAK,CAAC,WAAW,SAAS,cAAc,EACtD,MAAM,EAAE,WAAW,CAAC,WAAW,CAAC,GAC/B,MAAM,CAiCR"}
|
package/lib/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createUnsignedToken } from '@enkaku/jwt';
|
|
1
|
+
import { createUnsignedToken, isSignedToken } from '@enkaku/jwt';
|
|
2
2
|
import { createPipe } from '@enkaku/stream';
|
|
3
3
|
import { createDisposer } from '@enkaku/util';
|
|
4
|
+
import { checkClientToken } from './access-control.js';
|
|
4
5
|
import { handleChannel } from './handlers/channel.js';
|
|
5
6
|
import { handleEvent } from './handlers/event.js';
|
|
6
7
|
import { handleRequest } from './handlers/request.js';
|
|
@@ -23,17 +24,45 @@ async function handleMessages(params) {
|
|
|
23
24
|
// Wait until all running handlers are done
|
|
24
25
|
await Promise.all(Object.values(running));
|
|
25
26
|
}, signal);
|
|
26
|
-
function
|
|
27
|
+
function processHandler(message, handle) {
|
|
28
|
+
const returned = handle();
|
|
27
29
|
if (returned instanceof ErrorRejection) {
|
|
28
30
|
reject(returned);
|
|
29
31
|
} else {
|
|
30
|
-
const id = payload.typ === 'event' ? Math.random().toString(36).slice(2) : payload.rid;
|
|
32
|
+
const id = message.payload.typ === 'event' ? Math.random().toString(36).slice(2) : message.payload.rid;
|
|
31
33
|
running[id] = returned;
|
|
32
34
|
returned.then(()=>{
|
|
33
35
|
delete running[id];
|
|
34
36
|
});
|
|
35
37
|
}
|
|
36
38
|
}
|
|
39
|
+
const process = params.insecure ? processHandler : async (message, handle)=>{
|
|
40
|
+
try {
|
|
41
|
+
if (!params.insecure) {
|
|
42
|
+
if (!isSignedToken(message)) {
|
|
43
|
+
throw new Error('Message is not signed');
|
|
44
|
+
}
|
|
45
|
+
await checkClientToken(params.serverID, params.access, message);
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const errorMessage = err.message ?? 'Access denied';
|
|
49
|
+
if (message.payload.typ === 'event') {
|
|
50
|
+
reject(new ErrorRejection(errorMessage, {
|
|
51
|
+
cause: err,
|
|
52
|
+
info: message
|
|
53
|
+
}));
|
|
54
|
+
} else {
|
|
55
|
+
context.send({
|
|
56
|
+
typ: 'error',
|
|
57
|
+
rid: message.payload.rid,
|
|
58
|
+
code: 'EKK1000',
|
|
59
|
+
msg: errorMessage
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
processHandler(message, handle);
|
|
65
|
+
};
|
|
37
66
|
async function handleNext() {
|
|
38
67
|
const next = await transport.read();
|
|
39
68
|
if (next.done) {
|
|
@@ -46,14 +75,23 @@ async function handleMessages(params) {
|
|
|
46
75
|
controllers[msg.payload.rid]?.abort();
|
|
47
76
|
break;
|
|
48
77
|
case 'channel':
|
|
49
|
-
|
|
50
|
-
|
|
78
|
+
{
|
|
79
|
+
const message = msg;
|
|
80
|
+
process(message, ()=>handleChannel(context, message));
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
51
83
|
case 'event':
|
|
52
|
-
|
|
53
|
-
|
|
84
|
+
{
|
|
85
|
+
const message = msg;
|
|
86
|
+
process(message, ()=>handleEvent(context, message));
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
54
89
|
case 'request':
|
|
55
|
-
|
|
56
|
-
|
|
90
|
+
{
|
|
91
|
+
const message = msg;
|
|
92
|
+
process(message, ()=>handleRequest(context, message));
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
57
95
|
case 'send':
|
|
58
96
|
{
|
|
59
97
|
const controller = controllers[msg.payload.rid];
|
|
@@ -61,8 +99,11 @@ async function handleMessages(params) {
|
|
|
61
99
|
break;
|
|
62
100
|
}
|
|
63
101
|
case 'stream':
|
|
64
|
-
|
|
65
|
-
|
|
102
|
+
{
|
|
103
|
+
const message = msg;
|
|
104
|
+
process(message, ()=>handleStream(context, message));
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
66
107
|
}
|
|
67
108
|
handleNext();
|
|
68
109
|
}
|
|
@@ -81,8 +122,15 @@ export function serve(params) {
|
|
|
81
122
|
controllers,
|
|
82
123
|
handlers: params.handlers,
|
|
83
124
|
reject,
|
|
125
|
+
signal: abortController.signal,
|
|
84
126
|
transport: params.transport,
|
|
85
|
-
|
|
127
|
+
...params.insecure ? {
|
|
128
|
+
insecure: true
|
|
129
|
+
} : {
|
|
130
|
+
insecure: false,
|
|
131
|
+
serverID: params.id,
|
|
132
|
+
access: params.access ?? {}
|
|
133
|
+
}
|
|
86
134
|
});
|
|
87
135
|
const disposer = createDisposer(async ()=>{
|
|
88
136
|
// Signal messages handler to stop execution and run cleanup logic
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enkaku/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"type": "module",
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
],
|
|
15
15
|
"sideEffects": false,
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@enkaku/
|
|
18
|
-
"@enkaku/util": "^0.
|
|
19
|
-
"@enkaku/stream": "^0.
|
|
17
|
+
"@enkaku/capability": "^0.3.0",
|
|
18
|
+
"@enkaku/util": "^0.3.0",
|
|
19
|
+
"@enkaku/stream": "^0.3.0",
|
|
20
|
+
"@enkaku/jwt": "^0.3.0"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
|
-
"@enkaku/protocol": "^0.
|
|
23
|
-
"@enkaku/transport": "^0.
|
|
23
|
+
"@enkaku/protocol": "^0.3.0",
|
|
24
|
+
"@enkaku/transport": "^0.3.0"
|
|
24
25
|
},
|
|
25
26
|
"scripts": {
|
|
26
27
|
"build:clean": "del lib",
|