@enkaku/server 0.2.1 → 0.3.1

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.
@@ -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(serverID: string, 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,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,WAAW,CAAC,oBAAoB,CAAC,EACxC,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAiCf;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,CA4Bf"}
@@ -0,0 +1,63 @@
1
+ import { assertNonExpired, checkCapability, hasPartsMatch } from '@enkaku/capability';
2
+ export async function checkCommandAccess(serverID, record, token, atTime) {
3
+ const payload = token.payload;
4
+ if (payload.cmd == null) {
5
+ throw new Error('No command to check');
6
+ }
7
+ for (const [command, access] of Object.entries(record)){
8
+ if (hasPartsMatch(payload.cmd, command)) {
9
+ if (access === true) {
10
+ // Command can be publicly accessed
11
+ return;
12
+ }
13
+ if (access === false) {
14
+ continue;
15
+ }
16
+ if (access.includes(payload.iss)) {
17
+ // Issuer is allowed directly
18
+ return;
19
+ }
20
+ if (payload.sub == null || !access.includes(payload.sub)) {
21
+ continue;
22
+ }
23
+ try {
24
+ // Check delegation from subject
25
+ await checkCapability({
26
+ act: payload.cmd,
27
+ res: serverID
28
+ }, payload, atTime);
29
+ return;
30
+ } catch {}
31
+ }
32
+ }
33
+ throw new Error('Access denied');
34
+ }
35
+ export async function checkClientToken(serverID, record, token, atTime) {
36
+ const payload = token.payload;
37
+ const command = payload.cmd;
38
+ if (command == null) {
39
+ throw new Error('No command to check');
40
+ }
41
+ if (payload.iss === serverID) {
42
+ // If issuer uses the server's signer, only check audience and expiration if provided
43
+ if (payload.aud != null && payload.aud !== serverID) {
44
+ throw new Error('Invalid audience');
45
+ }
46
+ if (payload.exp != null) {
47
+ assertNonExpired(payload, atTime);
48
+ }
49
+ return;
50
+ }
51
+ if (payload.sub === serverID) {
52
+ // If subject is the server, check capability directly
53
+ await checkCapability({
54
+ act: command,
55
+ res: serverID
56
+ }, payload, atTime);
57
+ return;
58
+ }
59
+ if (payload.aud !== serverID) {
60
+ throw new Error('Invalid audience');
61
+ }
62
+ await checkCommandAccess(serverID, record, token, atTime);
63
+ }
package/lib/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export type { CommandAccessRecord } from './access-control.js';
1
2
  export type { RejectionType } from './rejections.js';
2
3
  export { type ServeParams, type Server, serve } from './server.js';
3
4
  export type { ChannelHandler, ChannelHandlerContext, CommandHandlers, EventHandler, EventHandlerContext, HandlerReturn, RequestHandler, RequestHandlerContext, StreamHandler, StreamHandlerContext, } from './types.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,MAAM,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAClE,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,eAAe,EACf,YAAY,EACZ,mBAAmB,EACnB,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,aAAa,EACb,oBAAoB,GACrB,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,MAAM,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAClE,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,eAAe,EACf,YAAY,EACZ,mBAAmB,EACnB,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,aAAa,EACb,oBAAoB,GACrB,MAAM,YAAY,CAAA"}
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
- })(RejectionReason || (RejectionReason = {}));
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
  };
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAsB,cAAc,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAE7F,OAAO,EAAE,KAAK,QAAQ,EAAkB,MAAM,cAAc,CAAA;AAM5D,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACpE,OAAO,KAAK,EAEV,eAAe,EAEf,iBAAiB,EAClB,MAAM,YAAY,CAAA;AAEnB,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,CAAA;AAkFD,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,CAAA;AAED,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,CA8BR"}
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 process(payload, returned) {
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
- process(msg.payload, handleChannel(context, msg));
50
- break;
78
+ {
79
+ const message = msg;
80
+ process(message, ()=>handleChannel(context, message));
81
+ break;
82
+ }
51
83
  case 'event':
52
- process(msg.payload, handleEvent(context, msg));
53
- break;
84
+ {
85
+ const message = msg;
86
+ process(message, ()=>handleEvent(context, message));
87
+ break;
88
+ }
54
89
  case 'request':
55
- process(msg.payload, handleRequest(context, msg));
56
- break;
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
- process(msg.payload, handleStream(context, msg));
65
- break;
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
- signal: abortController.signal
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.2.1",
3
+ "version": "0.3.1",
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/jwt": "^0.2.3",
18
- "@enkaku/util": "^0.2.0",
19
- "@enkaku/stream": "^0.2.1"
17
+ "@enkaku/capability": "^0.3.0",
18
+ "@enkaku/jwt": "^0.3.0",
19
+ "@enkaku/stream": "^0.3.0",
20
+ "@enkaku/util": "^0.3.0"
20
21
  },
21
22
  "devDependencies": {
22
- "@enkaku/protocol": "^0.2.1",
23
- "@enkaku/transport": "^0.2.0"
23
+ "@enkaku/protocol": "^0.3.1",
24
+ "@enkaku/transport": "^0.3.0"
24
25
  },
25
26
  "scripts": {
26
27
  "build:clean": "del lib",