@enkaku/server 0.13.3 → 0.14.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 +3 -2
- package/lib/access-control.d.ts.map +1 -1
- package/lib/access-control.js +12 -7
- package/lib/handlers/channel.d.ts.map +1 -1
- package/lib/handlers/channel.js +12 -0
- package/lib/handlers/stream.d.ts.map +1 -1
- package/lib/handlers/stream.js +7 -0
- package/lib/server.d.ts +12 -7
- package/lib/server.d.ts.map +1 -1
- package/lib/server.js +229 -31
- package/package.json +11 -10
package/lib/access-control.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DelegationChainOptions } from '@enkaku/capability';
|
|
1
2
|
import type { SignedToken } from '@enkaku/token';
|
|
2
3
|
export type EncryptionPolicy = 'required' | 'optional' | 'none';
|
|
3
4
|
export type ProcedureAccessConfig = {
|
|
@@ -14,6 +15,6 @@ export type ProcedureAccessPayload = {
|
|
|
14
15
|
prc?: string;
|
|
15
16
|
exp?: number;
|
|
16
17
|
};
|
|
17
|
-
export declare function checkProcedureAccess(serverID: string, record: ProcedureAccessRecord, token: SignedToken<ProcedureAccessPayload>,
|
|
18
|
-
export declare function checkClientToken(serverID: string, record: ProcedureAccessRecord, token: SignedToken,
|
|
18
|
+
export declare function checkProcedureAccess(serverID: string, record: ProcedureAccessRecord, token: SignedToken<ProcedureAccessPayload>, options?: DelegationChainOptions): Promise<void>;
|
|
19
|
+
export declare function checkClientToken(serverID: string, record: ProcedureAccessRecord, token: SignedToken, options?: DelegationChainOptions): Promise<void>;
|
|
19
20
|
//# sourceMappingURL=access-control.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-control.d.ts","sourceRoot":"","sources":["../src/access-control.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"access-control.d.ts","sourceRoot":"","sources":["../src/access-control.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,sBAAsB,EAE5B,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEhD,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,UAAU,GAAG,MAAM,CAAA;AAE/D,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAA;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,qBAAqB,CAAA;AAElF,MAAM,MAAM,qBAAqB,GAAG,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;AAgBxE,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,qBAAqB,GAAG,SAAS,EACzC,YAAY,EAAE,gBAAgB,GAC7B,gBAAgB,CAYlB;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,GAAG,EAAE,MAAM,CAAA;IACX,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,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,WAAW,CAAC,sBAAsB,CAAC,EAC1C,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,IAAI,CAAC,CA2Cf;AAED,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,WAAW,EAClB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAiCf"}
|
package/lib/access-control.js
CHANGED
|
@@ -24,7 +24,7 @@ export function resolveEncryptionPolicy(procedure, record, globalPolicy) {
|
|
|
24
24
|
}
|
|
25
25
|
return globalPolicy;
|
|
26
26
|
}
|
|
27
|
-
export async function checkProcedureAccess(serverID, record, token,
|
|
27
|
+
export async function checkProcedureAccess(serverID, record, token, options) {
|
|
28
28
|
const payload = token.payload;
|
|
29
29
|
if (payload.prc == null) {
|
|
30
30
|
throw new Error('No procedure to check');
|
|
@@ -51,14 +51,19 @@ export async function checkProcedureAccess(serverID, record, token, atTime) {
|
|
|
51
51
|
await checkCapability({
|
|
52
52
|
act: payload.prc,
|
|
53
53
|
res: serverID
|
|
54
|
-
}, payload,
|
|
54
|
+
}, payload, options);
|
|
55
55
|
return;
|
|
56
|
-
} catch
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const message = err instanceof Error ? err.message : '';
|
|
58
|
+
if (!message.startsWith('Invalid capability') && !message.startsWith('Invalid payload') && !message.startsWith('Invalid token')) {
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
throw new Error('Access denied');
|
|
60
65
|
}
|
|
61
|
-
export async function checkClientToken(serverID, record, token,
|
|
66
|
+
export async function checkClientToken(serverID, record, token, options) {
|
|
62
67
|
const payload = token.payload;
|
|
63
68
|
const procedure = payload.prc;
|
|
64
69
|
if (procedure == null) {
|
|
@@ -70,7 +75,7 @@ export async function checkClientToken(serverID, record, token, atTime) {
|
|
|
70
75
|
throw new Error('Invalid audience');
|
|
71
76
|
}
|
|
72
77
|
if (payload.exp != null) {
|
|
73
|
-
assertNonExpired(payload, atTime);
|
|
78
|
+
assertNonExpired(payload, options?.atTime);
|
|
74
79
|
}
|
|
75
80
|
return;
|
|
76
81
|
}
|
|
@@ -79,11 +84,11 @@ export async function checkClientToken(serverID, record, token, atTime) {
|
|
|
79
84
|
await checkCapability({
|
|
80
85
|
act: procedure,
|
|
81
86
|
res: serverID
|
|
82
|
-
}, payload,
|
|
87
|
+
}, payload, options);
|
|
83
88
|
return;
|
|
84
89
|
}
|
|
85
90
|
if (payload.aud !== serverID) {
|
|
86
91
|
throw new Error('Invalid audience');
|
|
87
92
|
}
|
|
88
|
-
await checkProcedureAccess(serverID, record, token,
|
|
93
|
+
await checkProcedureAccess(serverID, record, token, options);
|
|
89
94
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/handlers/channel.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/handlers/channel.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EACnB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,EAGV,cAAc,EAGf,MAAM,aAAa,CAAA;AAGpB,MAAM,MAAM,gBAAgB,CAC1B,QAAQ,SAAS,kBAAkB,EACnC,SAAS,SAAS,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,QAAQ,GAAG,MAAM,IACjE,aAAa,CAAC,gBAAgB,CAAC,SAAS,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AAEnE,wBAAgB,aAAa,CAC3B,QAAQ,SAAS,kBAAkB,EACnC,SAAS,SAAS,MAAM,QAAQ,GAAG,MAAM,EAEzC,GAAG,EAAE,cAAc,CAAC,QAAQ,CAAC,EAC7B,GAAG,EAAE,gBAAgB,CAAC,QAAQ,EAAE,SAAS,CAAC,GACzC,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CA6FvB"}
|
package/lib/handlers/channel.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AttributeKeys, getActiveSpan } from '@enkaku/otel';
|
|
1
2
|
import { createPipe, tap, writeTo } from '@enkaku/stream';
|
|
2
3
|
import { executeHandler } from '../utils.js';
|
|
3
4
|
export function handleChannel(ctx, msg) {
|
|
@@ -18,6 +19,7 @@ export function handleChannel(ctx, msg) {
|
|
|
18
19
|
param
|
|
19
20
|
});
|
|
20
21
|
}
|
|
22
|
+
const activeSpan = getActiveSpan();
|
|
21
23
|
const sendStream = createPipe();
|
|
22
24
|
const issuer = msg.payload.iss;
|
|
23
25
|
const controller = Object.assign(new AbortController(), {
|
|
@@ -33,6 +35,11 @@ export function handleChannel(ctx, msg) {
|
|
|
33
35
|
if (controller.signal.aborted) {
|
|
34
36
|
return;
|
|
35
37
|
}
|
|
38
|
+
if (activeSpan != null) {
|
|
39
|
+
activeSpan.addEvent('channel.message.sent', {
|
|
40
|
+
[AttributeKeys.MESSAGE_DIRECTION]: 'send'
|
|
41
|
+
});
|
|
42
|
+
}
|
|
36
43
|
ctx.logger.trace('send value to channel {procedure} with ID {rid}: {val}', {
|
|
37
44
|
procedure: msg.payload.prc,
|
|
38
45
|
rid: msg.payload.rid,
|
|
@@ -45,6 +52,11 @@ export function handleChannel(ctx, msg) {
|
|
|
45
52
|
});
|
|
46
53
|
}));
|
|
47
54
|
const readable = sendStream.readable.pipeThrough(tap((value)=>{
|
|
55
|
+
if (activeSpan != null) {
|
|
56
|
+
activeSpan.addEvent('channel.message.received', {
|
|
57
|
+
[AttributeKeys.MESSAGE_DIRECTION]: 'receive'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
48
60
|
ctx.logger.trace('received value from channel {procedure} with ID {rid}: {value}', {
|
|
49
61
|
procedure: msg.payload.prc,
|
|
50
62
|
rid: msg.payload.rid,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/handlers/stream.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/handlers/stream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,aAAa,EACb,kBAAkB,EAClB,eAAe,EAChB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,EAAE,cAAc,EAA8B,MAAM,aAAa,CAAA;AAG7E,MAAM,MAAM,eAAe,CACzB,QAAQ,SAAS,kBAAkB,EACnC,SAAS,SAAS,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,QAAQ,GAAG,MAAM,IACjE,aAAa,CAAC,eAAe,CAAC,SAAS,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AAElE,wBAAgB,YAAY,CAC1B,QAAQ,SAAS,kBAAkB,EACnC,SAAS,SAAS,MAAM,QAAQ,GAAG,MAAM,EACzC,GAAG,EAAE,cAAc,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,eAAe,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAqEjG"}
|
package/lib/handlers/stream.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AttributeKeys, getActiveSpan } from '@enkaku/otel';
|
|
1
2
|
import { createPipe, writeTo } from '@enkaku/stream';
|
|
2
3
|
import { executeHandler } from '../utils.js';
|
|
3
4
|
export function handleStream(ctx, msg) {
|
|
@@ -18,6 +19,7 @@ export function handleStream(ctx, msg) {
|
|
|
18
19
|
param
|
|
19
20
|
});
|
|
20
21
|
}
|
|
22
|
+
const activeSpan = getActiveSpan();
|
|
21
23
|
const controller = new AbortController();
|
|
22
24
|
ctx.controllers[msg.payload.rid] = controller;
|
|
23
25
|
const receiveStream = createPipe();
|
|
@@ -25,6 +27,11 @@ export function handleStream(ctx, msg) {
|
|
|
25
27
|
if (controller.signal.aborted) {
|
|
26
28
|
return;
|
|
27
29
|
}
|
|
30
|
+
if (activeSpan != null) {
|
|
31
|
+
activeSpan.addEvent('stream.message.sent', {
|
|
32
|
+
[AttributeKeys.MESSAGE_DIRECTION]: 'send'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
28
35
|
ctx.logger.trace('send value to stream {procedure} with ID {rid}: {val}', {
|
|
29
36
|
procedure: msg.payload.prc,
|
|
30
37
|
rid: msg.payload.rid,
|
package/lib/server.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Disposer } from '@enkaku/async';
|
|
2
|
+
import type { VerifyTokenHook } from '@enkaku/capability';
|
|
2
3
|
import { type Logger } from '@enkaku/log';
|
|
4
|
+
import { type Tracer } from '@enkaku/otel';
|
|
3
5
|
import { type AnyClientMessageOf, type ProtocolDefinition, type ServerTransportOf } from '@enkaku/protocol';
|
|
4
6
|
import { type Validator } from '@enkaku/schema';
|
|
5
7
|
import { type Identity } from '@enkaku/token';
|
|
@@ -7,15 +9,16 @@ import { type EncryptionPolicy, type ProcedureAccessRecord } from './access-cont
|
|
|
7
9
|
import { type ResourceLimiter, type ResourceLimits } from './limits.js';
|
|
8
10
|
import type { ProcedureHandlers, ServerEmitter } from './types.js';
|
|
9
11
|
export type AccessControlParams = ({
|
|
10
|
-
|
|
12
|
+
requireAuth: false;
|
|
11
13
|
serverID?: string;
|
|
12
|
-
access
|
|
14
|
+
access: ProcedureAccessRecord;
|
|
13
15
|
} | {
|
|
14
|
-
|
|
16
|
+
requireAuth: true;
|
|
15
17
|
serverID: string;
|
|
16
18
|
access: ProcedureAccessRecord;
|
|
17
19
|
}) & {
|
|
18
20
|
encryptionPolicy?: EncryptionPolicy;
|
|
21
|
+
verifyToken?: VerifyTokenHook;
|
|
19
22
|
};
|
|
20
23
|
export type HandleMessagesParams<Protocol extends ProtocolDefinition> = AccessControlParams & {
|
|
21
24
|
events: ServerEmitter;
|
|
@@ -23,11 +26,12 @@ export type HandleMessagesParams<Protocol extends ProtocolDefinition> = AccessCo
|
|
|
23
26
|
limiter: ResourceLimiter;
|
|
24
27
|
logger: Logger;
|
|
25
28
|
signal: AbortSignal;
|
|
29
|
+
tracer: Tracer;
|
|
26
30
|
transport: ServerTransportOf<Protocol>;
|
|
27
31
|
validator?: Validator<AnyClientMessageOf<Protocol>>;
|
|
28
32
|
};
|
|
29
33
|
export type ServerParams<Protocol extends ProtocolDefinition> = {
|
|
30
|
-
|
|
34
|
+
accessControl?: false | true | ProcedureAccessRecord;
|
|
31
35
|
encryptionPolicy?: EncryptionPolicy;
|
|
32
36
|
getRandomID?: () => string;
|
|
33
37
|
handlers: ProcedureHandlers<Protocol>;
|
|
@@ -35,14 +39,15 @@ export type ServerParams<Protocol extends ProtocolDefinition> = {
|
|
|
35
39
|
limits?: Partial<ResourceLimits>;
|
|
36
40
|
logger?: Logger;
|
|
37
41
|
protocol?: Protocol;
|
|
38
|
-
|
|
42
|
+
tracer?: Tracer;
|
|
39
43
|
signal?: AbortSignal;
|
|
40
44
|
transports?: Array<ServerTransportOf<Protocol>>;
|
|
45
|
+
verifyToken?: VerifyTokenHook;
|
|
41
46
|
};
|
|
42
47
|
export type HandleOptions = {
|
|
43
|
-
|
|
48
|
+
accessControl?: false | true | ProcedureAccessRecord;
|
|
44
49
|
logger?: Logger;
|
|
45
|
-
|
|
50
|
+
verifyToken?: VerifyTokenHook;
|
|
46
51
|
};
|
|
47
52
|
export declare class Server<Protocol extends ProtocolDefinition> extends Disposer {
|
|
48
53
|
#private;
|
package/lib/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,QAAQ,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/lib/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DisposeInterruption, Disposer } from '@enkaku/async';
|
|
2
2
|
import { EventEmitter } from '@enkaku/event';
|
|
3
3
|
import { getEnkakuLogger } from '@enkaku/log';
|
|
4
|
+
import { AttributeKeys, createTracer, extractTraceContext, SpanNames, SpanStatusCode, setSpanOnContext, TraceFlags, withActiveContext } from '@enkaku/otel';
|
|
4
5
|
import { createClientMessageSchema } from '@enkaku/protocol';
|
|
5
6
|
import { createValidator, ValidationError } from '@enkaku/schema';
|
|
6
7
|
import { createUnsignedToken, isSignedToken } from '@enkaku/token';
|
|
@@ -11,6 +12,7 @@ import { handleEvent } from './handlers/event.js';
|
|
|
11
12
|
import { handleRequest } from './handlers/request.js';
|
|
12
13
|
import { handleStream } from './handlers/stream.js';
|
|
13
14
|
import { createResourceLimiter } from './limits.js';
|
|
15
|
+
const defaultTracer = createTracer('server');
|
|
14
16
|
function defaultRandomID() {
|
|
15
17
|
return globalThis.crypto.randomUUID();
|
|
16
18
|
}
|
|
@@ -70,6 +72,20 @@ async function handleMessages(params) {
|
|
|
70
72
|
const processMessage = validator ? (message)=>{
|
|
71
73
|
const result = validator(message);
|
|
72
74
|
if (result instanceof ValidationError) {
|
|
75
|
+
const validationSpan = params.tracer.startSpan(SpanNames.SERVER_HANDLE, {
|
|
76
|
+
attributes: {
|
|
77
|
+
[AttributeKeys.RPC_SYSTEM]: 'enkaku'
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
validationSpan.addEvent('enkaku.validation', {
|
|
81
|
+
[AttributeKeys.VALIDATION_SUCCESS]: false,
|
|
82
|
+
[AttributeKeys.VALIDATION_ERROR]: result.message
|
|
83
|
+
});
|
|
84
|
+
validationSpan.setStatus({
|
|
85
|
+
code: SpanStatusCode.ERROR,
|
|
86
|
+
message: 'Validation failed'
|
|
87
|
+
});
|
|
88
|
+
validationSpan.end();
|
|
73
89
|
logger.debug('received invalid message', {
|
|
74
90
|
error: result
|
|
75
91
|
});
|
|
@@ -167,26 +183,174 @@ async function handleMessages(params) {
|
|
|
167
183
|
context.send(error.toPayload(message.payload.rid));
|
|
168
184
|
}
|
|
169
185
|
}
|
|
170
|
-
|
|
186
|
+
function getParentContext(message) {
|
|
187
|
+
const header = message.header;
|
|
188
|
+
return extractTraceContext(header);
|
|
189
|
+
}
|
|
190
|
+
function createHandleSpan(message) {
|
|
191
|
+
const parentCtx = getParentContext(message);
|
|
192
|
+
const procedure = message.payload.prc;
|
|
193
|
+
const rid = 'rid' in message.payload ? message.payload.rid : undefined;
|
|
194
|
+
// Build span links from client trace context
|
|
195
|
+
const header = message.header;
|
|
196
|
+
const links = [];
|
|
197
|
+
if (typeof header.tid === 'string' && typeof header.sid === 'string') {
|
|
198
|
+
links.push({
|
|
199
|
+
context: {
|
|
200
|
+
traceId: header.tid,
|
|
201
|
+
spanId: header.sid,
|
|
202
|
+
traceFlags: TraceFlags.SAMPLED,
|
|
203
|
+
isRemote: true
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return params.tracer.startSpan(SpanNames.SERVER_HANDLE, {
|
|
208
|
+
attributes: {
|
|
209
|
+
[AttributeKeys.RPC_SYSTEM]: 'enkaku',
|
|
210
|
+
...procedure != null ? {
|
|
211
|
+
[AttributeKeys.RPC_PROCEDURE]: procedure
|
|
212
|
+
} : {},
|
|
213
|
+
...rid != null ? {
|
|
214
|
+
[AttributeKeys.RPC_REQUEST_ID]: rid
|
|
215
|
+
} : {}
|
|
216
|
+
},
|
|
217
|
+
links
|
|
218
|
+
}, parentCtx);
|
|
219
|
+
}
|
|
220
|
+
function wrapHandle(span, handle) {
|
|
221
|
+
return ()=>{
|
|
222
|
+
const spanCtx = setSpanOnContext(undefined, span);
|
|
223
|
+
const result = withActiveContext(spanCtx, ()=>{
|
|
224
|
+
const handlerSpan = params.tracer.startSpan(SpanNames.SERVER_HANDLER);
|
|
225
|
+
const handlerResult = handle();
|
|
226
|
+
if (handlerResult instanceof Error) {
|
|
227
|
+
if ('code' in handlerResult) {
|
|
228
|
+
handlerSpan.setAttribute(AttributeKeys.ERROR_CODE, handlerResult.code);
|
|
229
|
+
}
|
|
230
|
+
handlerSpan.setAttribute(AttributeKeys.ERROR_MESSAGE, handlerResult.message);
|
|
231
|
+
handlerSpan.setStatus({
|
|
232
|
+
code: SpanStatusCode.ERROR,
|
|
233
|
+
message: handlerResult.message
|
|
234
|
+
});
|
|
235
|
+
handlerSpan.recordException(handlerResult);
|
|
236
|
+
handlerSpan.end();
|
|
237
|
+
return handlerResult;
|
|
238
|
+
}
|
|
239
|
+
handlerResult.then(()=>{
|
|
240
|
+
handlerSpan.setStatus({
|
|
241
|
+
code: SpanStatusCode.OK
|
|
242
|
+
});
|
|
243
|
+
handlerSpan.end();
|
|
244
|
+
}).catch((err)=>{
|
|
245
|
+
if ('code' in err) {
|
|
246
|
+
handlerSpan.setAttribute(AttributeKeys.ERROR_CODE, err.code);
|
|
247
|
+
}
|
|
248
|
+
handlerSpan.setAttribute(AttributeKeys.ERROR_MESSAGE, err.message);
|
|
249
|
+
handlerSpan.setStatus({
|
|
250
|
+
code: SpanStatusCode.ERROR,
|
|
251
|
+
message: err.message
|
|
252
|
+
});
|
|
253
|
+
handlerSpan.recordException(err);
|
|
254
|
+
handlerSpan.end();
|
|
255
|
+
});
|
|
256
|
+
return handlerResult;
|
|
257
|
+
});
|
|
258
|
+
if (result instanceof Error) {
|
|
259
|
+
if ('code' in result) {
|
|
260
|
+
span.setAttribute(AttributeKeys.ERROR_CODE, result.code);
|
|
261
|
+
}
|
|
262
|
+
span.setAttribute(AttributeKeys.ERROR_MESSAGE, result.message);
|
|
263
|
+
span.setStatus({
|
|
264
|
+
code: SpanStatusCode.ERROR,
|
|
265
|
+
message: result.message
|
|
266
|
+
});
|
|
267
|
+
span.recordException(result);
|
|
268
|
+
span.end();
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
result.then(()=>{
|
|
272
|
+
span.setStatus({
|
|
273
|
+
code: SpanStatusCode.OK
|
|
274
|
+
});
|
|
275
|
+
span.end();
|
|
276
|
+
}).catch((err)=>{
|
|
277
|
+
if ('code' in err) {
|
|
278
|
+
span.setAttribute(AttributeKeys.ERROR_CODE, err.code);
|
|
279
|
+
}
|
|
280
|
+
span.setAttribute(AttributeKeys.ERROR_MESSAGE, err.message);
|
|
281
|
+
span.setStatus({
|
|
282
|
+
code: SpanStatusCode.ERROR,
|
|
283
|
+
message: err.message
|
|
284
|
+
});
|
|
285
|
+
span.recordException(err);
|
|
286
|
+
span.end();
|
|
287
|
+
});
|
|
288
|
+
return result;
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const process = !params.requireAuth ? (message, handle)=>{
|
|
292
|
+
const span = createHandleSpan(message);
|
|
293
|
+
if (validator != null) {
|
|
294
|
+
span.addEvent('enkaku.validation', {
|
|
295
|
+
[AttributeKeys.VALIDATION_SUCCESS]: true
|
|
296
|
+
});
|
|
297
|
+
}
|
|
171
298
|
if (!checkMessageEncryption(message)) {
|
|
299
|
+
span.setAttribute(AttributeKeys.AUTH_REASON, 'encryption_required');
|
|
300
|
+
span.setAttribute(AttributeKeys.ERROR_CODE, 'EK_ENCRYPTION');
|
|
301
|
+
span.setAttribute(AttributeKeys.ERROR_MESSAGE, 'Encryption required');
|
|
302
|
+
span.setStatus({
|
|
303
|
+
code: SpanStatusCode.ERROR,
|
|
304
|
+
message: 'Encryption required'
|
|
305
|
+
});
|
|
306
|
+
span.end();
|
|
172
307
|
handleEncryptionViolation(message);
|
|
173
308
|
return;
|
|
174
309
|
}
|
|
175
|
-
processHandler(message, handle);
|
|
310
|
+
processHandler(message, wrapHandle(span, handle));
|
|
176
311
|
} : async (message, handle)=>{
|
|
312
|
+
const span = createHandleSpan(message);
|
|
313
|
+
if (validator != null) {
|
|
314
|
+
span.addEvent('enkaku.validation', {
|
|
315
|
+
[AttributeKeys.VALIDATION_SUCCESS]: true
|
|
316
|
+
});
|
|
317
|
+
}
|
|
177
318
|
try {
|
|
178
|
-
if (!
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
await checkClientToken(params.serverID, params.access, message);
|
|
319
|
+
if (!isSignedToken(message)) {
|
|
320
|
+
span.setAttribute(AttributeKeys.AUTH_REASON, 'unsigned_message');
|
|
321
|
+
span.setAttribute(AttributeKeys.AUTH_ALLOWED, false);
|
|
322
|
+
throw new Error('Message is not signed');
|
|
183
323
|
}
|
|
324
|
+
await checkClientToken(params.serverID, params.access, message, params.verifyToken != null ? {
|
|
325
|
+
verifyToken: params.verifyToken
|
|
326
|
+
} : undefined);
|
|
327
|
+
const did = message.payload.iss;
|
|
328
|
+
if (did != null) {
|
|
329
|
+
span.setAttribute(AttributeKeys.AUTH_DID, did);
|
|
330
|
+
}
|
|
331
|
+
span.setAttribute(AttributeKeys.AUTH_ALLOWED, true);
|
|
184
332
|
} catch (cause) {
|
|
333
|
+
const did = isSignedToken(message) ? message.payload.iss : undefined;
|
|
334
|
+
if (did != null) {
|
|
335
|
+
span.setAttribute(AttributeKeys.AUTH_DID, did);
|
|
336
|
+
}
|
|
337
|
+
span.setAttribute(AttributeKeys.AUTH_ALLOWED, false);
|
|
338
|
+
if (!cause.message?.includes('unsigned')) {
|
|
339
|
+
span.setAttribute(AttributeKeys.AUTH_REASON, cause.message);
|
|
340
|
+
}
|
|
185
341
|
const error = new HandlerError({
|
|
186
342
|
cause,
|
|
187
343
|
code: 'EK02',
|
|
188
344
|
message: cause.message ?? 'Access denied'
|
|
189
345
|
});
|
|
346
|
+
span.setAttribute(AttributeKeys.ERROR_CODE, error.code);
|
|
347
|
+
span.setAttribute(AttributeKeys.ERROR_MESSAGE, error.message);
|
|
348
|
+
span.setStatus({
|
|
349
|
+
code: SpanStatusCode.ERROR,
|
|
350
|
+
message: error.message
|
|
351
|
+
});
|
|
352
|
+
span.recordException(error);
|
|
353
|
+
span.end();
|
|
190
354
|
if (message.payload.typ === 'event') {
|
|
191
355
|
events.emit('eventAuthError', {
|
|
192
356
|
error,
|
|
@@ -202,10 +366,18 @@ async function handleMessages(params) {
|
|
|
202
366
|
return;
|
|
203
367
|
}
|
|
204
368
|
if (!checkMessageEncryption(message)) {
|
|
369
|
+
span.setAttribute(AttributeKeys.AUTH_REASON, 'encryption_required');
|
|
370
|
+
span.setAttribute(AttributeKeys.ERROR_CODE, 'EK_ENCRYPTION');
|
|
371
|
+
span.setAttribute(AttributeKeys.ERROR_MESSAGE, 'Encryption required');
|
|
372
|
+
span.setStatus({
|
|
373
|
+
code: SpanStatusCode.ERROR,
|
|
374
|
+
message: 'Encryption required'
|
|
375
|
+
});
|
|
376
|
+
span.end();
|
|
205
377
|
handleEncryptionViolation(message);
|
|
206
378
|
return;
|
|
207
379
|
}
|
|
208
|
-
processHandler(message, handle);
|
|
380
|
+
processHandler(message, wrapHandle(span, handle));
|
|
209
381
|
};
|
|
210
382
|
async function handleNext() {
|
|
211
383
|
const next = await transport.read();
|
|
@@ -262,8 +434,8 @@ async function handleMessages(params) {
|
|
|
262
434
|
});
|
|
263
435
|
break;
|
|
264
436
|
}
|
|
265
|
-
// In
|
|
266
|
-
if (
|
|
437
|
+
// In authenticated mode, validate send messages against the channel owner
|
|
438
|
+
if (params.requireAuth) {
|
|
267
439
|
if (!isSignedToken(msg)) {
|
|
268
440
|
const error = new HandlerError({
|
|
269
441
|
code: 'EK02',
|
|
@@ -307,6 +479,7 @@ export class Server extends Disposer {
|
|
|
307
479
|
#handling = [];
|
|
308
480
|
#limiter;
|
|
309
481
|
#logger;
|
|
482
|
+
#tracer;
|
|
310
483
|
#validator;
|
|
311
484
|
constructor(params){
|
|
312
485
|
super({
|
|
@@ -348,22 +521,37 @@ export class Server extends Disposer {
|
|
|
348
521
|
this.#logger = params.logger ?? getEnkakuLogger('server', {
|
|
349
522
|
serverID: serverID ?? this.#getRandomID()
|
|
350
523
|
});
|
|
524
|
+
this.#tracer = params.tracer ?? defaultTracer;
|
|
525
|
+
const accessControl = params.accessControl;
|
|
351
526
|
if (serverID == null) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
access: params.access,
|
|
356
|
-
encryptionPolicy: params.encryptionPolicy
|
|
357
|
-
};
|
|
358
|
-
} else {
|
|
359
|
-
throw new Error('Invalid server parameters: either the server "identity" must be provided or the "public" parameter must be set to true');
|
|
527
|
+
// No identity: accessControl must be explicitly false
|
|
528
|
+
if (accessControl !== false) {
|
|
529
|
+
throw new Error('Invalid server parameters: either "identity" must be provided or "accessControl" must be set to false');
|
|
360
530
|
}
|
|
531
|
+
this.#accessControl = {
|
|
532
|
+
requireAuth: false,
|
|
533
|
+
access: {},
|
|
534
|
+
encryptionPolicy: params.encryptionPolicy,
|
|
535
|
+
verifyToken: params.verifyToken
|
|
536
|
+
};
|
|
537
|
+
} else if (accessControl === false) {
|
|
538
|
+
// Has identity but public access
|
|
539
|
+
this.#accessControl = {
|
|
540
|
+
requireAuth: false,
|
|
541
|
+
serverID,
|
|
542
|
+
access: {},
|
|
543
|
+
encryptionPolicy: params.encryptionPolicy,
|
|
544
|
+
verifyToken: params.verifyToken
|
|
545
|
+
};
|
|
361
546
|
} else {
|
|
547
|
+
// Has identity with access control (true = server-only, record = granular)
|
|
548
|
+
const access = accessControl === true || accessControl == null ? {} : accessControl;
|
|
362
549
|
this.#accessControl = {
|
|
363
|
-
|
|
550
|
+
requireAuth: true,
|
|
364
551
|
serverID,
|
|
365
|
-
access
|
|
366
|
-
encryptionPolicy: params.encryptionPolicy
|
|
552
|
+
access,
|
|
553
|
+
encryptionPolicy: params.encryptionPolicy,
|
|
554
|
+
verifyToken: params.verifyToken
|
|
367
555
|
};
|
|
368
556
|
}
|
|
369
557
|
this.#limiter = createResourceLimiter(params.limits);
|
|
@@ -380,29 +568,38 @@ export class Server extends Disposer {
|
|
|
380
568
|
return this.#events;
|
|
381
569
|
}
|
|
382
570
|
handle(transport, options = {}) {
|
|
383
|
-
const
|
|
384
|
-
const access = options.access ?? this.#accessControl.access ?? {};
|
|
571
|
+
const accessControlOverride = options.accessControl;
|
|
385
572
|
const logger = options.logger ?? this.#logger.getChild('handler').with({
|
|
386
573
|
transportID: this.#getRandomID()
|
|
387
574
|
});
|
|
388
575
|
const encryptionPolicy = this.#accessControl.encryptionPolicy;
|
|
389
576
|
let accessControl;
|
|
390
|
-
if (
|
|
577
|
+
if (accessControlOverride === false) {
|
|
391
578
|
accessControl = {
|
|
392
|
-
|
|
393
|
-
access,
|
|
394
|
-
encryptionPolicy
|
|
579
|
+
requireAuth: false,
|
|
580
|
+
access: this.#accessControl.access ?? {},
|
|
581
|
+
encryptionPolicy,
|
|
582
|
+
verifyToken: options.verifyToken ?? this.#accessControl.verifyToken
|
|
395
583
|
};
|
|
396
|
-
} else {
|
|
584
|
+
} else if (accessControlOverride != null) {
|
|
585
|
+
// Override with true or ProcedureAccessRecord
|
|
397
586
|
const serverID = this.#accessControl.serverID;
|
|
398
587
|
if (serverID == null) {
|
|
399
|
-
return Promise.reject(new Error('Server
|
|
588
|
+
return Promise.reject(new Error('Server identity is required to enable access control on transport'));
|
|
400
589
|
}
|
|
590
|
+
const access = accessControlOverride === true ? {} : accessControlOverride;
|
|
401
591
|
accessControl = {
|
|
402
|
-
|
|
592
|
+
requireAuth: true,
|
|
403
593
|
serverID,
|
|
404
594
|
access,
|
|
405
|
-
encryptionPolicy
|
|
595
|
+
encryptionPolicy,
|
|
596
|
+
verifyToken: options.verifyToken ?? this.#accessControl.verifyToken
|
|
597
|
+
};
|
|
598
|
+
} else {
|
|
599
|
+
// Use server-level defaults
|
|
600
|
+
accessControl = {
|
|
601
|
+
...this.#accessControl,
|
|
602
|
+
verifyToken: options.verifyToken ?? this.#accessControl.verifyToken
|
|
406
603
|
};
|
|
407
604
|
}
|
|
408
605
|
const done = handleMessages({
|
|
@@ -411,6 +608,7 @@ export class Server extends Disposer {
|
|
|
411
608
|
limiter: this.#limiter,
|
|
412
609
|
logger,
|
|
413
610
|
signal: this.#abortController.signal,
|
|
611
|
+
tracer: this.#tracer,
|
|
414
612
|
transport,
|
|
415
613
|
validator: this.#validator,
|
|
416
614
|
...accessControl
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enkaku/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://enkaku.dev",
|
|
6
6
|
"description": "Server logic for Enkaku RPC",
|
|
@@ -25,17 +25,18 @@
|
|
|
25
25
|
],
|
|
26
26
|
"sideEffects": false,
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@enkaku/async": "^0.
|
|
29
|
-
"@enkaku/
|
|
30
|
-
"@enkaku/
|
|
31
|
-
"@enkaku/log": "^0.
|
|
32
|
-
"@enkaku/
|
|
33
|
-
"@enkaku/
|
|
34
|
-
"@enkaku/
|
|
35
|
-
"@enkaku/schema": "^0.
|
|
28
|
+
"@enkaku/async": "^0.14.0",
|
|
29
|
+
"@enkaku/capability": "^0.14.0",
|
|
30
|
+
"@enkaku/otel": "^0.14.0",
|
|
31
|
+
"@enkaku/log": "^0.14.0",
|
|
32
|
+
"@enkaku/protocol": "^0.14.0",
|
|
33
|
+
"@enkaku/stream": "^0.14.0",
|
|
34
|
+
"@enkaku/event": "^0.14.0",
|
|
35
|
+
"@enkaku/schema": "^0.14.0",
|
|
36
|
+
"@enkaku/token": "^0.14.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
|
-
"@enkaku/transport": "^0.
|
|
39
|
+
"@enkaku/transport": "^0.14.0"
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
42
|
"build:clean": "del lib",
|