@makerx/graphql-core 3.5.0 → 3.6.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 +3 -0
- package/package.json +2 -2
- package/subscriptions/server.d.cts +11 -1
- package/subscriptions/server.d.mts +11 -1
- package/subscriptions/server.d.ts +11 -1
- package/subscriptions/server.js +57 -26
- package/subscriptions/server.js.map +1 -1
- package/subscriptions/server.mjs +58 -27
- package/subscriptions/server.mjs.map +1 -1
package/README.md
CHANGED
|
@@ -379,6 +379,9 @@ This library includes a `subscriptions` module to provide simple setup using the
|
|
|
379
379
|
- Auth token validation as part of establishing (or rejecting) the connection (behaviour defined by `verifyToken`, `extractToken` and `requireAuth` args)
|
|
380
380
|
- GraphQL context creation
|
|
381
381
|
- Logging from the server `onConnect`, `onDisconnect`, `onOperation`, `onNext` and `onError` callbacks
|
|
382
|
+
- Refusal of query and mutation operations (see below)
|
|
383
|
+
|
|
384
|
+
By default the server runs only subscription operations. It refuses a query or mutation operation with a `BAD_REQUEST` error, and does not close the connection. Queries and mutations sent over the websocket do not go through the HTTP request pipeline, so they bypass its checks (Express middleware, Apollo Server plugins, rate limits and similar). Pass `allowQueriesAndMutations: true` to run them, but only when the websocket path applies the same checks. Before 3.6.0 the server ran all operation types.
|
|
382
385
|
|
|
383
386
|
Pass `includeDeprecatedElements: true` to add [deprecation usage](#collectdeprecatedelementusage) to the operation log entry as `deprecatedElements`. It is collected once, when the subscription is established, rather than per emitted payload — the usage is a property of the operation, not of each event.
|
|
384
387
|
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"**"
|
|
5
5
|
],
|
|
6
6
|
"name": "@makerx/graphql-core",
|
|
7
|
-
"version": "3.
|
|
7
|
+
"version": "3.6.0",
|
|
8
8
|
"private": false,
|
|
9
9
|
"description": "A set of core GraphQL utilities that MakerX uses to build GraphQL APIs",
|
|
10
10
|
"author": "MakerX",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"overrides": {
|
|
45
45
|
"esbuild": "^0.28.1",
|
|
46
|
-
"@graphql-tools/utils": "12.0
|
|
46
|
+
"@graphql-tools/utils": "12.1.0"
|
|
47
47
|
},
|
|
48
48
|
"main": "./index.js",
|
|
49
49
|
"module": "./index.mjs",
|
|
@@ -3,7 +3,7 @@ import { type GraphQLSchema } from 'graphql';
|
|
|
3
3
|
import type { Server } from 'http';
|
|
4
4
|
import type { GraphQLContext, JwtPayload } from '../context';
|
|
5
5
|
import type { CreateSubscriptionContext, ExtractSubscriptionToken } from './context';
|
|
6
|
-
export declare function useSubscriptionsServer<TLogger extends Logger = Logger>({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel, path, verifyToken, extractToken, requireAuth, jwtClaimsToLog, resolveSubscriptionOperationLogger, includeDeprecatedElements, connectionExpiry, keepAlive, }: {
|
|
6
|
+
export declare function useSubscriptionsServer<TLogger extends Logger = Logger>({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel, path, verifyToken, extractToken, requireAuth, jwtClaimsToLog, resolveSubscriptionOperationLogger, includeDeprecatedElements, connectionExpiry, keepAlive, allowQueriesAndMutations, }: {
|
|
7
7
|
schema: GraphQLSchema;
|
|
8
8
|
httpServer: Server;
|
|
9
9
|
createSubscriptionContext: CreateSubscriptionContext;
|
|
@@ -60,4 +60,14 @@ export declare function useSubscriptionsServer<TLogger extends Logger = Logger>(
|
|
|
60
60
|
* seconds.
|
|
61
61
|
*/
|
|
62
62
|
keepAlive?: number;
|
|
63
|
+
/**
|
|
64
|
+
* If true, the server also runs query and mutation operations. By default it runs only
|
|
65
|
+
* subscription operations, and refuses the others with a `BAD_REQUEST` error. The connection
|
|
66
|
+
* stays open.
|
|
67
|
+
*
|
|
68
|
+
* Queries and mutations sent over the websocket do not go through the HTTP request pipeline, so
|
|
69
|
+
* they bypass its checks (middleware, Apollo Server plugins, rate limits and similar). Set this
|
|
70
|
+
* only when the websocket path applies the same checks.
|
|
71
|
+
*/
|
|
72
|
+
allowQueriesAndMutations?: boolean;
|
|
63
73
|
}): import("graphql-ws").Disposable;
|
|
@@ -3,7 +3,7 @@ import { type GraphQLSchema } from 'graphql';
|
|
|
3
3
|
import type { Server } from 'http';
|
|
4
4
|
import type { GraphQLContext, JwtPayload } from '../context.mjs';
|
|
5
5
|
import type { CreateSubscriptionContext, ExtractSubscriptionToken } from './context.mjs';
|
|
6
|
-
export declare function useSubscriptionsServer<TLogger extends Logger = Logger>({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel, path, verifyToken, extractToken, requireAuth, jwtClaimsToLog, resolveSubscriptionOperationLogger, includeDeprecatedElements, connectionExpiry, keepAlive, }: {
|
|
6
|
+
export declare function useSubscriptionsServer<TLogger extends Logger = Logger>({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel, path, verifyToken, extractToken, requireAuth, jwtClaimsToLog, resolveSubscriptionOperationLogger, includeDeprecatedElements, connectionExpiry, keepAlive, allowQueriesAndMutations, }: {
|
|
7
7
|
schema: GraphQLSchema;
|
|
8
8
|
httpServer: Server;
|
|
9
9
|
createSubscriptionContext: CreateSubscriptionContext;
|
|
@@ -60,4 +60,14 @@ export declare function useSubscriptionsServer<TLogger extends Logger = Logger>(
|
|
|
60
60
|
* seconds.
|
|
61
61
|
*/
|
|
62
62
|
keepAlive?: number;
|
|
63
|
+
/**
|
|
64
|
+
* If true, the server also runs query and mutation operations. By default it runs only
|
|
65
|
+
* subscription operations, and refuses the others with a `BAD_REQUEST` error. The connection
|
|
66
|
+
* stays open.
|
|
67
|
+
*
|
|
68
|
+
* Queries and mutations sent over the websocket do not go through the HTTP request pipeline, so
|
|
69
|
+
* they bypass its checks (middleware, Apollo Server plugins, rate limits and similar). Set this
|
|
70
|
+
* only when the websocket path applies the same checks.
|
|
71
|
+
*/
|
|
72
|
+
allowQueriesAndMutations?: boolean;
|
|
63
73
|
}): import("graphql-ws").Disposable;
|
|
@@ -3,7 +3,7 @@ import { type GraphQLSchema } from 'graphql';
|
|
|
3
3
|
import type { Server } from 'http';
|
|
4
4
|
import type { GraphQLContext, JwtPayload } from '../context';
|
|
5
5
|
import type { CreateSubscriptionContext, ExtractSubscriptionToken } from './context';
|
|
6
|
-
export declare function useSubscriptionsServer<TLogger extends Logger = Logger>({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel, path, verifyToken, extractToken, requireAuth, jwtClaimsToLog, resolveSubscriptionOperationLogger, includeDeprecatedElements, connectionExpiry, keepAlive, }: {
|
|
6
|
+
export declare function useSubscriptionsServer<TLogger extends Logger = Logger>({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel, path, verifyToken, extractToken, requireAuth, jwtClaimsToLog, resolveSubscriptionOperationLogger, includeDeprecatedElements, connectionExpiry, keepAlive, allowQueriesAndMutations, }: {
|
|
7
7
|
schema: GraphQLSchema;
|
|
8
8
|
httpServer: Server;
|
|
9
9
|
createSubscriptionContext: CreateSubscriptionContext;
|
|
@@ -60,4 +60,14 @@ export declare function useSubscriptionsServer<TLogger extends Logger = Logger>(
|
|
|
60
60
|
* seconds.
|
|
61
61
|
*/
|
|
62
62
|
keepAlive?: number;
|
|
63
|
+
/**
|
|
64
|
+
* If true, the server also runs query and mutation operations. By default it runs only
|
|
65
|
+
* subscription operations, and refuses the others with a `BAD_REQUEST` error. The connection
|
|
66
|
+
* stays open.
|
|
67
|
+
*
|
|
68
|
+
* Queries and mutations sent over the websocket do not go through the HTTP request pipeline, so
|
|
69
|
+
* they bypass its checks (middleware, Apollo Server plugins, rate limits and similar). Set this
|
|
70
|
+
* only when the websocket path applies the same checks.
|
|
71
|
+
*/
|
|
72
|
+
allowQueriesAndMutations?: boolean;
|
|
63
73
|
}): import("graphql-ws").Disposable;
|
package/subscriptions/server.js
CHANGED
|
@@ -12,11 +12,25 @@ var utils = require('./utils.js');
|
|
|
12
12
|
const maxTimeoutMs = 2 ** 31 - 1;
|
|
13
13
|
// What onSubscribe returns for an operation it refuses, so onError can tell a refusal from a real error.
|
|
14
14
|
class SubscriptionRefusedError extends graphql.GraphQLError {
|
|
15
|
-
constructor() {
|
|
16
|
-
super(
|
|
15
|
+
constructor(message = 'Forbidden', code = 'FORBIDDEN') {
|
|
16
|
+
super(message, { extensions: { code } });
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
// The type of the operation the client asked to run, or undefined when graphql-ws must report the
|
|
20
|
+
// document as invalid.
|
|
21
|
+
const operationTypeOf = (query, operationName) => {
|
|
22
|
+
if (typeof query !== 'string')
|
|
23
|
+
return undefined;
|
|
24
|
+
let document;
|
|
25
|
+
try {
|
|
26
|
+
document = graphql.parse(query);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return graphql.getOperationAST(document, operationName)?.operation;
|
|
32
|
+
};
|
|
33
|
+
function useSubscriptionsServer({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel = 'info', path = '/graphql', verifyToken, extractToken = utils.defaultExtractSubscriptionToken, requireAuth, jwtClaimsToLog = ['oid', 'iss'], resolveSubscriptionOperationLogger, includeDeprecatedElements, connectionExpiry, keepAlive = 12000, allowQueriesAndMutations = false, }) {
|
|
20
34
|
if (requireAuth && !verifyToken)
|
|
21
35
|
throw new Error('verifyToken must be supplied when requireAuth is true');
|
|
22
36
|
const wsServer = new ws.WebSocketServer({
|
|
@@ -107,33 +121,50 @@ function useSubscriptionsServer({ schema, httpServer, createSubscriptionContext,
|
|
|
107
121
|
endConnectionAt(ctx.extra.socket, endsAt, claims);
|
|
108
122
|
return true;
|
|
109
123
|
},
|
|
110
|
-
onSubscribe: async (ctx) => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (endsAt !== undefined) {
|
|
115
|
-
// The token stands for the life connectionExpiry gave the connection. This covers an operation that
|
|
116
|
-
// arrives before the timer closes the connection.
|
|
117
|
-
if (Date.now() < endsAt)
|
|
124
|
+
onSubscribe: async (ctx, _id, payload) => {
|
|
125
|
+
// Returns the errors that refuse the operation, or undefined when the connection may run it.
|
|
126
|
+
const authorise = async () => {
|
|
127
|
+
if (!verifyToken)
|
|
118
128
|
return;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
logger.
|
|
129
|
+
const endsAt = connectionEnds.get(ctx.extra.socket);
|
|
130
|
+
if (endsAt !== undefined) {
|
|
131
|
+
// The token stands for the life connectionExpiry gave the connection. This covers an operation that
|
|
132
|
+
// arrives before the timer closes the connection.
|
|
133
|
+
if (Date.now() < endsAt)
|
|
134
|
+
return;
|
|
135
|
+
logger.warn('Subscription connection has expired', { claims: compat.pick(ctx.extra.claims, jwtClaimsToLog) });
|
|
136
|
+
return refuse(ctx.extra.socket);
|
|
137
|
+
}
|
|
138
|
+
const token = extractToken({ connectRequest: ctx.extra.request, connectionParams: ctx.connectionParams });
|
|
139
|
+
if (!token) {
|
|
140
|
+
if (requireAuth) {
|
|
141
|
+
logger.error('No auth token was supplied with the websocket connection');
|
|
142
|
+
return refuse(ctx.extra.socket);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
await verifyToken(utils.getHost(ctx.extra.request), token);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
logger.warn('Subscription connection auth token is no longer valid', { claims: compat.pick(ctx.extra.claims, jwtClaimsToLog), error });
|
|
126
151
|
return refuse(ctx.extra.socket);
|
|
127
152
|
}
|
|
153
|
+
};
|
|
154
|
+
const refused = await authorise();
|
|
155
|
+
if (refused)
|
|
156
|
+
return refused;
|
|
157
|
+
if (allowQueriesAndMutations)
|
|
128
158
|
return;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
159
|
+
const operationType = operationTypeOf(payload.query, payload.operationName);
|
|
160
|
+
if (operationType === undefined || operationType === 'subscription')
|
|
161
|
+
return;
|
|
162
|
+
logger.warn('Subscription connection refused a non-subscription operation', {
|
|
163
|
+
operationType,
|
|
164
|
+
operationName: payload.operationName,
|
|
165
|
+
claims: compat.pick(ctx.extra.claims, jwtClaimsToLog),
|
|
166
|
+
});
|
|
167
|
+
return [new SubscriptionRefusedError(`Only subscription operations are allowed, not ${operationType} operations`, 'BAD_REQUEST')];
|
|
137
168
|
},
|
|
138
169
|
onDisconnect({ extra: { claims } }) {
|
|
139
170
|
logger.info('Subscription connection disconnected', { claims: compat.pick(claims, jwtClaimsToLog) });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sources":["../../src/subscriptions/server.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { pick } from 'es-toolkit/compat'\nimport { GraphQLError, type GraphQLSchema } from 'graphql'\nimport { CloseCode } from 'graphql-ws'\nimport { useServer } from 'graphql-ws/use/ws'\nimport type { Server } from 'http'\nimport { WebSocketServer, type WebSocket } from 'ws'\nimport type { GraphQLContext, JwtPayload } from '../context'\nimport { logSubscriptionOperation } from '../logging'\nimport type { CreateSubscriptionContext, ExtractSubscriptionToken } from './context'\nimport { defaultExtractSubscriptionToken, getHost } from './utils'\n\n// The longest delay setTimeout accepts. A connection that ends later than this is timed in steps.\nconst maxTimeoutMs = 2 ** 31 - 1\n\n// What onSubscribe returns for an operation it refuses, so onError can tell a refusal from a real error.\nclass SubscriptionRefusedError extends GraphQLError {\n constructor() {\n super('Forbidden', { extensions: { code: 'FORBIDDEN' } })\n }\n}\n\nexport function useSubscriptionsServer<TLogger extends Logger = Logger>({\n schema,\n httpServer,\n createSubscriptionContext,\n logger,\n operationLogLevel = 'info',\n path = '/graphql',\n verifyToken,\n extractToken = defaultExtractSubscriptionToken,\n requireAuth,\n jwtClaimsToLog = ['oid', 'iss'],\n resolveSubscriptionOperationLogger,\n includeDeprecatedElements,\n connectionExpiry,\n keepAlive = 12_000,\n}: {\n schema: GraphQLSchema\n httpServer: Server\n createSubscriptionContext: CreateSubscriptionContext\n logger: TLogger\n operationLogLevel?: keyof TLogger\n path?: string\n verifyToken?: (host: string, token: string) => Promise<JwtPayload>\n /**\n * Returns the token to pass to `verifyToken`, e.g. from a JWT assertion header that a proxy adds\n * to the connect request. Defaults to the bearer token in the `authorization` or `Authorization`\n * connection parameter. Pass the same function to `createSubscriptionContextFactory`.\n */\n extractToken?: ExtractSubscriptionToken\n requireAuth?: boolean\n jwtClaimsToLog?: string[]\n resolveSubscriptionOperationLogger?: (context: GraphQLContext) => TLogger\n /**\n * If true, deprecated schema elements a subscription uses are collected and logged when it is\n * established. Not collected per emitted payload: the usage is a property of the operation.\n */\n includeDeprecatedElements?: boolean\n /**\n * Returns the time a connection ends, in milliseconds since the epoch, from the claims `verifyToken`\n * returned when it was established. `undefined` keeps the default behaviour below.\n *\n * When it returns a time:\n * - A time already past refuses the connection with `4403 Forbidden`, as an invalid token does.\n * - Otherwise at that time the connection is closed with `4403`, which ends every subscription\n * running on it.\n * - Until then the client may subscribe again on the connection, and the token is not verified\n * again: it was verified when the connection was established, and it cannot change for the life\n * of the connection.\n *\n * To end a connection when its token expires: `(claims) => (claims.exp === undefined ? undefined : claims.exp * 1000)`.\n * When the token is short-lived proof of a longer session, such as an assertion that a proxy adds to\n * the upgrade request, a later time keeps subscriptions from being cut off every few minutes:\n * `(claims) => (claims.iat === undefined ? undefined : (claims.iat + 3600) * 1000)`. A connection then\n * outlives its token, so a revoked session is noticed only when the connection ends.\n *\n * By default a connection has no set end. Its token is verified again each time the client\n * subscribes, and a token that no longer verifies closes the connection with `4403` then.\n * Subscriptions already running continue until that happens.\n *\n * graphql-ws clients retry `4403` by default, but a retry succeeds only with a fresh token: the\n * client's `connectionParams` must be a function that returns a current token, or the upgrade\n * request must be authenticated afresh (by a cookie and a proxy, for example). A client that sends\n * the same expired token fails again until its `retryAttempts` run out, and its subscriptions then\n * end with an error.\n */\n connectionExpiry?: (claims: JwtPayload) => number | undefined\n /**\n * Milliseconds between the server's websocket pings. A connection that has not answered a ping by\n * the time the next one is due is terminated. `0` turns the pings off. Defaults to graphql-ws's 12\n * seconds.\n */\n keepAlive?: number\n}) {\n if (requireAuth && !verifyToken) throw new Error('verifyToken must be supplied when requireAuth is true')\n\n const wsServer = new WebSocketServer({\n server: httpServer,\n path,\n })\n\n // When each connection ends, for the connections that connectionExpiry gave a time.\n const connectionEnds = new WeakMap<WebSocket, number>()\n\n const endConnectionAt = (socket: WebSocket, endsAt: number, claims: JwtPayload) => {\n // A client that went away while its token was being verified has already closed the socket.\n if (socket.readyState !== socket.OPEN) return\n connectionEnds.set(socket, endsAt)\n let timer: NodeJS.Timeout | undefined\n // The socket's own close event, not graphql-ws's onClose, which waits for every subscription on the connection to\n // return. A subscription waiting for its next event can hold that up indefinitely.\n socket.once('close', () => clearTimeout(timer))\n const schedule = () => {\n const remaining = endsAt - Date.now()\n timer = setTimeout(\n () => {\n if (remaining > maxTimeoutMs) return schedule()\n if (socket.readyState !== socket.OPEN) return\n logger.info('Subscription connection expired', { claims: pick(claims, jwtClaimsToLog) })\n socket.close(CloseCode.Forbidden, 'Forbidden')\n },\n Math.min(Math.max(remaining, 0), maxTimeoutMs),\n )\n timer.unref()\n }\n schedule()\n }\n\n // Closes the connection, and keeps graphql-ws from running the operation: it would run it after the\n // close if onSubscribe returned nothing.\n const refuse = (socket: WebSocket) => {\n socket.close(CloseCode.Forbidden, 'Forbidden')\n return [new SubscriptionRefusedError()]\n }\n\n return useServer(\n {\n schema,\n onError(_ctx, id, _payload, errors) {\n // A refusal is already logged, and is not a server error.\n if (errors.every((error) => error instanceof SubscriptionRefusedError)) return\n logger.error('GraphQL subscriptions server error', { id, errors })\n },\n onConnect: async (ctx) => {\n const connectionEstablished = 'Subscription connection established'\n if (!verifyToken) {\n logger.info(connectionEstablished)\n return true\n }\n\n const token = extractToken({ connectRequest: ctx.extra.request, connectionParams: ctx.connectionParams })\n if (!token) {\n if (requireAuth) {\n logger.error('No auth token was supplied with the websocket connection')\n return false\n }\n logger.info(connectionEstablished)\n return true\n }\n\n let claims: JwtPayload\n try {\n claims = await verifyToken(getHost(ctx.extra.request), token)\n } catch (error) {\n logger.error('Failed to verify subscription connection auth token', { error })\n return false\n }\n\n let endsAt: number | undefined\n try {\n endsAt = connectionExpiry?.(claims)\n } catch (error) {\n logger.error('Failed to compute subscription connection expiry', { claims: pick(claims, jwtClaimsToLog), error })\n return false\n }\n // Refused before the connection is acknowledged: an acknowledged connection resets a graphql-ws\n // client's retry count, so closing it at once would have the client reconnect without end.\n if (endsAt !== undefined && !(endsAt > Date.now())) {\n logger.warn('Subscription connection has already expired', { claims: pick(claims, jwtClaimsToLog) })\n return false\n }\n\n ctx.extra.claims = claims as unknown as undefined\n logger.info(connectionEstablished, {\n claims: pick(claims, jwtClaimsToLog),\n })\n if (endsAt !== undefined) endConnectionAt(ctx.extra.socket, endsAt, claims)\n return true\n },\n onSubscribe: async (ctx) => {\n if (!verifyToken) return\n const endsAt = connectionEnds.get(ctx.extra.socket)\n if (endsAt !== undefined) {\n // The token stands for the life connectionExpiry gave the connection. This covers an operation that\n // arrives before the timer closes the connection.\n if (Date.now() < endsAt) return\n logger.warn('Subscription connection has expired', { claims: pick(ctx.extra.claims, jwtClaimsToLog) })\n return refuse(ctx.extra.socket)\n }\n const token = extractToken({ connectRequest: ctx.extra.request, connectionParams: ctx.connectionParams })\n if (!token) {\n if (requireAuth) {\n logger.error('No auth token was supplied with the websocket connection')\n return refuse(ctx.extra.socket)\n }\n return\n }\n try {\n await verifyToken(getHost(ctx.extra.request), token)\n } catch (error) {\n logger.warn('Subscription connection auth token is no longer valid', { claims: pick(ctx.extra.claims, jwtClaimsToLog), error })\n return refuse(ctx.extra.socket)\n }\n },\n onDisconnect({ extra: { claims } }) {\n logger.info('Subscription connection disconnected', { claims: pick(claims, jwtClaimsToLog) })\n },\n context: async (ctx) => {\n return createSubscriptionContext({\n connectRequest: ctx.extra.request,\n connectionParams: ctx.connectionParams,\n claims: ctx.extra.claims as JwtPayload | undefined,\n })\n },\n onOperation(_ctx, id, _payload, args) {\n logSubscriptionOperation({\n id,\n args,\n logLevel: operationLogLevel,\n resolveLogger: resolveSubscriptionOperationLogger,\n includeDeprecatedElements,\n })\n },\n onNext(_ctx, id, _payload, args, { data, ...result }) {\n logSubscriptionOperation({ id, args, logLevel: operationLogLevel, result, resolveLogger: resolveSubscriptionOperationLogger })\n },\n },\n wsServer,\n keepAlive,\n )\n}\n"],"names":["GraphQLError","defaultExtractSubscriptionToken","WebSocketServer","pick","CloseCode","useServer","getHost","logSubscriptionOperation"],"mappings":";;;;;;;;;;AAYA;AACA,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC;AAEhC;AACA,MAAM,wBAAyB,SAAQA,oBAAY,CAAA;AACjD,IAAA,WAAA,GAAA;AACE,QAAA,KAAK,CAAC,WAAW,EAAE,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC;IAC3D;AACD;AAEK,SAAU,sBAAsB,CAAkC,EACtE,MAAM,EACN,UAAU,EACV,yBAAyB,EACzB,MAAM,EACN,iBAAiB,GAAG,MAAM,EAC1B,IAAI,GAAG,UAAU,EACjB,WAAW,EACX,YAAY,GAAGC,qCAA+B,EAC9C,WAAW,EACX,cAAc,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,EAC/B,kCAAkC,EAClC,yBAAyB,EACzB,gBAAgB,EAChB,SAAS,GAAG,KAAM,GA0DnB,EAAA;IACC,IAAI,WAAW,IAAI,CAAC,WAAW;AAAE,QAAA,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC;AAEzG,IAAA,MAAM,QAAQ,GAAG,IAAIC,kBAAe,CAAC;AACnC,QAAA,MAAM,EAAE,UAAU;QAClB,IAAI;AACL,KAAA,CAAC;;AAGF,IAAA,MAAM,cAAc,GAAG,IAAI,OAAO,EAAqB;IAEvD,MAAM,eAAe,GAAG,CAAC,MAAiB,EAAE,MAAc,EAAE,MAAkB,KAAI;;AAEhF,QAAA,IAAI,MAAM,CAAC,UAAU,KAAK,MAAM,CAAC,IAAI;YAAE;AACvC,QAAA,cAAc,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;AAClC,QAAA,IAAI,KAAiC;;;AAGrC,QAAA,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,YAAY,CAAC,KAAK,CAAC,CAAC;QAC/C,MAAM,QAAQ,GAAG,MAAK;YACpB,MAAM,SAAS,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE;AACrC,YAAA,KAAK,GAAG,UAAU,CAChB,MAAK;gBACH,IAAI,SAAS,GAAG,YAAY;oBAAE,OAAO,QAAQ,EAAE;AAC/C,gBAAA,IAAI,MAAM,CAAC,UAAU,KAAK,MAAM,CAAC,IAAI;oBAAE;AACvC,gBAAA,MAAM,CAAC,IAAI,CAAC,iCAAiC,EAAE,EAAE,MAAM,EAAEC,WAAI,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,CAAC;gBACxF,MAAM,CAAC,KAAK,CAACC,mBAAS,CAAC,SAAS,EAAE,WAAW,CAAC;AAChD,YAAA,CAAC,EACD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,YAAY,CAAC,CAC/C;YACD,KAAK,CAAC,KAAK,EAAE;AACf,QAAA,CAAC;AACD,QAAA,QAAQ,EAAE;AACZ,IAAA,CAAC;;;AAID,IAAA,MAAM,MAAM,GAAG,CAAC,MAAiB,KAAI;QACnC,MAAM,CAAC,KAAK,CAACA,mBAAS,CAAC,SAAS,EAAE,WAAW,CAAC;AAC9C,QAAA,OAAO,CAAC,IAAI,wBAAwB,EAAE,CAAC;AACzC,IAAA,CAAC;AAED,IAAA,OAAOC,cAAS,CACd;QACE,MAAM;AACN,QAAA,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAA;;AAEhC,YAAA,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,KAAK,KAAK,YAAY,wBAAwB,CAAC;gBAAE;YACxE,MAAM,CAAC,KAAK,CAAC,oCAAoC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC;QACpE,CAAC;AACD,QAAA,SAAS,EAAE,OAAO,GAAG,KAAI;YACvB,MAAM,qBAAqB,GAAG,qCAAqC;YACnE,IAAI,CAAC,WAAW,EAAE;AAChB,gBAAA,MAAM,CAAC,IAAI,CAAC,qBAAqB,CAAC;AAClC,gBAAA,OAAO,IAAI;YACb;YAEA,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,cAAc,EAAE,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,CAAC,gBAAgB,EAAE,CAAC;YACzG,IAAI,CAAC,KAAK,EAAE;gBACV,IAAI,WAAW,EAAE;AACf,oBAAA,MAAM,CAAC,KAAK,CAAC,0DAA0D,CAAC;AACxE,oBAAA,OAAO,KAAK;gBACd;AACA,gBAAA,MAAM,CAAC,IAAI,CAAC,qBAAqB,CAAC;AAClC,gBAAA,OAAO,IAAI;YACb;AAEA,YAAA,IAAI,MAAkB;AACtB,YAAA,IAAI;AACF,gBAAA,MAAM,GAAG,MAAM,WAAW,CAACC,aAAO,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC;YAC/D;YAAE,OAAO,KAAK,EAAE;gBACd,MAAM,CAAC,KAAK,CAAC,qDAAqD,EAAE,EAAE,KAAK,EAAE,CAAC;AAC9E,gBAAA,OAAO,KAAK;YACd;AAEA,YAAA,IAAI,MAA0B;AAC9B,YAAA,IAAI;AACF,gBAAA,MAAM,GAAG,gBAAgB,GAAG,MAAM,CAAC;YACrC;YAAE,OAAO,KAAK,EAAE;AACd,gBAAA,MAAM,CAAC,KAAK,CAAC,kDAAkD,EAAE,EAAE,MAAM,EAAEH,WAAI,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,KAAK,EAAE,CAAC;AACjH,gBAAA,OAAO,KAAK;YACd;;;AAGA,YAAA,IAAI,MAAM,KAAK,SAAS,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE;AAClD,gBAAA,MAAM,CAAC,IAAI,CAAC,6CAA6C,EAAE,EAAE,MAAM,EAAEA,WAAI,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,CAAC;AACpG,gBAAA,OAAO,KAAK;YACd;AAEA,YAAA,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,MAA8B;AACjD,YAAA,MAAM,CAAC,IAAI,CAAC,qBAAqB,EAAE;AACjC,gBAAA,MAAM,EAAEA,WAAI,CAAC,MAAM,EAAE,cAAc,CAAC;AACrC,aAAA,CAAC;YACF,IAAI,MAAM,KAAK,SAAS;gBAAE,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;AAC3E,YAAA,OAAO,IAAI;QACb,CAAC;AACD,QAAA,WAAW,EAAE,OAAO,GAAG,KAAI;AACzB,YAAA,IAAI,CAAC,WAAW;gBAAE;AAClB,YAAA,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC;AACnD,YAAA,IAAI,MAAM,KAAK,SAAS,EAAE;;;AAGxB,gBAAA,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM;oBAAE;gBACzB,MAAM,CAAC,IAAI,CAAC,qCAAqC,EAAE,EAAE,MAAM,EAAEA,WAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,CAAC;gBACtG,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC;YACjC;YACA,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,cAAc,EAAE,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,CAAC,gBAAgB,EAAE,CAAC;YACzG,IAAI,CAAC,KAAK,EAAE;gBACV,IAAI,WAAW,EAAE;AACf,oBAAA,MAAM,CAAC,KAAK,CAAC,0DAA0D,CAAC;oBACxE,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC;gBACjC;gBACA;YACF;AACA,YAAA,IAAI;AACF,gBAAA,MAAM,WAAW,CAACG,aAAO,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC;YACtD;YAAE,OAAO,KAAK,EAAE;gBACd,MAAM,CAAC,IAAI,CAAC,uDAAuD,EAAE,EAAE,MAAM,EAAEH,WAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,KAAK,EAAE,CAAC;gBAC/H,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC;YACjC;QACF,CAAC;AACD,QAAA,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAA;AAChC,YAAA,MAAM,CAAC,IAAI,CAAC,sCAAsC,EAAE,EAAE,MAAM,EAAEA,WAAI,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,CAAC;QAC/F,CAAC;AACD,QAAA,OAAO,EAAE,OAAO,GAAG,KAAI;AACrB,YAAA,OAAO,yBAAyB,CAAC;AAC/B,gBAAA,cAAc,EAAE,GAAG,CAAC,KAAK,CAAC,OAAO;gBACjC,gBAAgB,EAAE,GAAG,CAAC,gBAAgB;AACtC,gBAAA,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,MAAgC;AACnD,aAAA,CAAC;QACJ,CAAC;AACD,QAAA,WAAW,CAAC,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAA;AAClC,YAAAI,gCAAwB,CAAC;gBACvB,EAAE;gBACF,IAAI;AACJ,gBAAA,QAAQ,EAAE,iBAAiB;AAC3B,gBAAA,aAAa,EAAE,kCAAkC;gBACjD,yBAAyB;AAC1B,aAAA,CAAC;QACJ,CAAC;AACD,QAAA,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,EAAA;AAClD,YAAAA,gCAAwB,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,EAAE,aAAa,EAAE,kCAAkC,EAAE,CAAC;QAChI,CAAC;AACF,KAAA,EACD,QAAQ,EACR,SAAS,CACV;AACH;;;;"}
|
|
1
|
+
{"version":3,"file":"server.js","sources":["../../src/subscriptions/server.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { pick } from 'es-toolkit/compat'\nimport { getOperationAST, GraphQLError, parse, type DocumentNode, type GraphQLSchema } from 'graphql'\nimport { CloseCode } from 'graphql-ws'\nimport { useServer } from 'graphql-ws/use/ws'\nimport type { Server } from 'http'\nimport { WebSocketServer, type WebSocket } from 'ws'\nimport type { GraphQLContext, JwtPayload } from '../context'\nimport { logSubscriptionOperation } from '../logging'\nimport type { CreateSubscriptionContext, ExtractSubscriptionToken } from './context'\nimport { defaultExtractSubscriptionToken, getHost } from './utils'\n\n// The longest delay setTimeout accepts. A connection that ends later than this is timed in steps.\nconst maxTimeoutMs = 2 ** 31 - 1\n\n// What onSubscribe returns for an operation it refuses, so onError can tell a refusal from a real error.\nclass SubscriptionRefusedError extends GraphQLError {\n constructor(message = 'Forbidden', code = 'FORBIDDEN') {\n super(message, { extensions: { code } })\n }\n}\n\n// The type of the operation the client asked to run, or undefined when graphql-ws must report the\n// document as invalid.\nconst operationTypeOf = (query: unknown, operationName: string | null | undefined) => {\n if (typeof query !== 'string') return undefined\n let document: DocumentNode\n try {\n document = parse(query)\n } catch {\n return undefined\n }\n return getOperationAST(document, operationName)?.operation\n}\n\nexport function useSubscriptionsServer<TLogger extends Logger = Logger>({\n schema,\n httpServer,\n createSubscriptionContext,\n logger,\n operationLogLevel = 'info',\n path = '/graphql',\n verifyToken,\n extractToken = defaultExtractSubscriptionToken,\n requireAuth,\n jwtClaimsToLog = ['oid', 'iss'],\n resolveSubscriptionOperationLogger,\n includeDeprecatedElements,\n connectionExpiry,\n keepAlive = 12_000,\n allowQueriesAndMutations = false,\n}: {\n schema: GraphQLSchema\n httpServer: Server\n createSubscriptionContext: CreateSubscriptionContext\n logger: TLogger\n operationLogLevel?: keyof TLogger\n path?: string\n verifyToken?: (host: string, token: string) => Promise<JwtPayload>\n /**\n * Returns the token to pass to `verifyToken`, e.g. from a JWT assertion header that a proxy adds\n * to the connect request. Defaults to the bearer token in the `authorization` or `Authorization`\n * connection parameter. Pass the same function to `createSubscriptionContextFactory`.\n */\n extractToken?: ExtractSubscriptionToken\n requireAuth?: boolean\n jwtClaimsToLog?: string[]\n resolveSubscriptionOperationLogger?: (context: GraphQLContext) => TLogger\n /**\n * If true, deprecated schema elements a subscription uses are collected and logged when it is\n * established. Not collected per emitted payload: the usage is a property of the operation.\n */\n includeDeprecatedElements?: boolean\n /**\n * Returns the time a connection ends, in milliseconds since the epoch, from the claims `verifyToken`\n * returned when it was established. `undefined` keeps the default behaviour below.\n *\n * When it returns a time:\n * - A time already past refuses the connection with `4403 Forbidden`, as an invalid token does.\n * - Otherwise at that time the connection is closed with `4403`, which ends every subscription\n * running on it.\n * - Until then the client may subscribe again on the connection, and the token is not verified\n * again: it was verified when the connection was established, and it cannot change for the life\n * of the connection.\n *\n * To end a connection when its token expires: `(claims) => (claims.exp === undefined ? undefined : claims.exp * 1000)`.\n * When the token is short-lived proof of a longer session, such as an assertion that a proxy adds to\n * the upgrade request, a later time keeps subscriptions from being cut off every few minutes:\n * `(claims) => (claims.iat === undefined ? undefined : (claims.iat + 3600) * 1000)`. A connection then\n * outlives its token, so a revoked session is noticed only when the connection ends.\n *\n * By default a connection has no set end. Its token is verified again each time the client\n * subscribes, and a token that no longer verifies closes the connection with `4403` then.\n * Subscriptions already running continue until that happens.\n *\n * graphql-ws clients retry `4403` by default, but a retry succeeds only with a fresh token: the\n * client's `connectionParams` must be a function that returns a current token, or the upgrade\n * request must be authenticated afresh (by a cookie and a proxy, for example). A client that sends\n * the same expired token fails again until its `retryAttempts` run out, and its subscriptions then\n * end with an error.\n */\n connectionExpiry?: (claims: JwtPayload) => number | undefined\n /**\n * Milliseconds between the server's websocket pings. A connection that has not answered a ping by\n * the time the next one is due is terminated. `0` turns the pings off. Defaults to graphql-ws's 12\n * seconds.\n */\n keepAlive?: number\n /**\n * If true, the server also runs query and mutation operations. By default it runs only\n * subscription operations, and refuses the others with a `BAD_REQUEST` error. The connection\n * stays open.\n *\n * Queries and mutations sent over the websocket do not go through the HTTP request pipeline, so\n * they bypass its checks (middleware, Apollo Server plugins, rate limits and similar). Set this\n * only when the websocket path applies the same checks.\n */\n allowQueriesAndMutations?: boolean\n}) {\n if (requireAuth && !verifyToken) throw new Error('verifyToken must be supplied when requireAuth is true')\n\n const wsServer = new WebSocketServer({\n server: httpServer,\n path,\n })\n\n // When each connection ends, for the connections that connectionExpiry gave a time.\n const connectionEnds = new WeakMap<WebSocket, number>()\n\n const endConnectionAt = (socket: WebSocket, endsAt: number, claims: JwtPayload) => {\n // A client that went away while its token was being verified has already closed the socket.\n if (socket.readyState !== socket.OPEN) return\n connectionEnds.set(socket, endsAt)\n let timer: NodeJS.Timeout | undefined\n // The socket's own close event, not graphql-ws's onClose, which waits for every subscription on the connection to\n // return. A subscription waiting for its next event can hold that up indefinitely.\n socket.once('close', () => clearTimeout(timer))\n const schedule = () => {\n const remaining = endsAt - Date.now()\n timer = setTimeout(\n () => {\n if (remaining > maxTimeoutMs) return schedule()\n if (socket.readyState !== socket.OPEN) return\n logger.info('Subscription connection expired', { claims: pick(claims, jwtClaimsToLog) })\n socket.close(CloseCode.Forbidden, 'Forbidden')\n },\n Math.min(Math.max(remaining, 0), maxTimeoutMs),\n )\n timer.unref()\n }\n schedule()\n }\n\n // Closes the connection, and keeps graphql-ws from running the operation: it would run it after the\n // close if onSubscribe returned nothing.\n const refuse = (socket: WebSocket) => {\n socket.close(CloseCode.Forbidden, 'Forbidden')\n return [new SubscriptionRefusedError()]\n }\n\n return useServer(\n {\n schema,\n onError(_ctx, id, _payload, errors) {\n // A refusal is already logged, and is not a server error.\n if (errors.every((error) => error instanceof SubscriptionRefusedError)) return\n logger.error('GraphQL subscriptions server error', { id, errors })\n },\n onConnect: async (ctx) => {\n const connectionEstablished = 'Subscription connection established'\n if (!verifyToken) {\n logger.info(connectionEstablished)\n return true\n }\n\n const token = extractToken({ connectRequest: ctx.extra.request, connectionParams: ctx.connectionParams })\n if (!token) {\n if (requireAuth) {\n logger.error('No auth token was supplied with the websocket connection')\n return false\n }\n logger.info(connectionEstablished)\n return true\n }\n\n let claims: JwtPayload\n try {\n claims = await verifyToken(getHost(ctx.extra.request), token)\n } catch (error) {\n logger.error('Failed to verify subscription connection auth token', { error })\n return false\n }\n\n let endsAt: number | undefined\n try {\n endsAt = connectionExpiry?.(claims)\n } catch (error) {\n logger.error('Failed to compute subscription connection expiry', { claims: pick(claims, jwtClaimsToLog), error })\n return false\n }\n // Refused before the connection is acknowledged: an acknowledged connection resets a graphql-ws\n // client's retry count, so closing it at once would have the client reconnect without end.\n if (endsAt !== undefined && !(endsAt > Date.now())) {\n logger.warn('Subscription connection has already expired', { claims: pick(claims, jwtClaimsToLog) })\n return false\n }\n\n ctx.extra.claims = claims as unknown as undefined\n logger.info(connectionEstablished, {\n claims: pick(claims, jwtClaimsToLog),\n })\n if (endsAt !== undefined) endConnectionAt(ctx.extra.socket, endsAt, claims)\n return true\n },\n onSubscribe: async (ctx, _id, payload) => {\n // Returns the errors that refuse the operation, or undefined when the connection may run it.\n const authorise = async () => {\n if (!verifyToken) return\n const endsAt = connectionEnds.get(ctx.extra.socket)\n if (endsAt !== undefined) {\n // The token stands for the life connectionExpiry gave the connection. This covers an operation that\n // arrives before the timer closes the connection.\n if (Date.now() < endsAt) return\n logger.warn('Subscription connection has expired', { claims: pick(ctx.extra.claims, jwtClaimsToLog) })\n return refuse(ctx.extra.socket)\n }\n const token = extractToken({ connectRequest: ctx.extra.request, connectionParams: ctx.connectionParams })\n if (!token) {\n if (requireAuth) {\n logger.error('No auth token was supplied with the websocket connection')\n return refuse(ctx.extra.socket)\n }\n return\n }\n try {\n await verifyToken(getHost(ctx.extra.request), token)\n } catch (error) {\n logger.warn('Subscription connection auth token is no longer valid', { claims: pick(ctx.extra.claims, jwtClaimsToLog), error })\n return refuse(ctx.extra.socket)\n }\n }\n const refused = await authorise()\n if (refused) return refused\n if (allowQueriesAndMutations) return\n const operationType = operationTypeOf(payload.query, payload.operationName)\n if (operationType === undefined || operationType === 'subscription') return\n logger.warn('Subscription connection refused a non-subscription operation', {\n operationType,\n operationName: payload.operationName,\n claims: pick(ctx.extra.claims, jwtClaimsToLog),\n })\n return [new SubscriptionRefusedError(`Only subscription operations are allowed, not ${operationType} operations`, 'BAD_REQUEST')]\n },\n onDisconnect({ extra: { claims } }) {\n logger.info('Subscription connection disconnected', { claims: pick(claims, jwtClaimsToLog) })\n },\n context: async (ctx) => {\n return createSubscriptionContext({\n connectRequest: ctx.extra.request,\n connectionParams: ctx.connectionParams,\n claims: ctx.extra.claims as JwtPayload | undefined,\n })\n },\n onOperation(_ctx, id, _payload, args) {\n logSubscriptionOperation({\n id,\n args,\n logLevel: operationLogLevel,\n resolveLogger: resolveSubscriptionOperationLogger,\n includeDeprecatedElements,\n })\n },\n onNext(_ctx, id, _payload, args, { data, ...result }) {\n logSubscriptionOperation({ id, args, logLevel: operationLogLevel, result, resolveLogger: resolveSubscriptionOperationLogger })\n },\n },\n wsServer,\n keepAlive,\n )\n}\n"],"names":["GraphQLError","parse","getOperationAST","defaultExtractSubscriptionToken","WebSocketServer","pick","CloseCode","useServer","getHost","logSubscriptionOperation"],"mappings":"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"}
|
package/subscriptions/server.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pick } from 'es-toolkit/compat';
|
|
2
|
-
import { GraphQLError } from 'graphql';
|
|
2
|
+
import { parse, getOperationAST, GraphQLError } from 'graphql';
|
|
3
3
|
import { CloseCode } from 'graphql-ws';
|
|
4
4
|
import { useServer } from 'graphql-ws/use/ws';
|
|
5
5
|
import { WebSocketServer } from 'ws';
|
|
@@ -10,11 +10,25 @@ import { defaultExtractSubscriptionToken, getHost } from './utils.mjs';
|
|
|
10
10
|
const maxTimeoutMs = 2 ** 31 - 1;
|
|
11
11
|
// What onSubscribe returns for an operation it refuses, so onError can tell a refusal from a real error.
|
|
12
12
|
class SubscriptionRefusedError extends GraphQLError {
|
|
13
|
-
constructor() {
|
|
14
|
-
super(
|
|
13
|
+
constructor(message = 'Forbidden', code = 'FORBIDDEN') {
|
|
14
|
+
super(message, { extensions: { code } });
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
// The type of the operation the client asked to run, or undefined when graphql-ws must report the
|
|
18
|
+
// document as invalid.
|
|
19
|
+
const operationTypeOf = (query, operationName) => {
|
|
20
|
+
if (typeof query !== 'string')
|
|
21
|
+
return undefined;
|
|
22
|
+
let document;
|
|
23
|
+
try {
|
|
24
|
+
document = parse(query);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return getOperationAST(document, operationName)?.operation;
|
|
30
|
+
};
|
|
31
|
+
function useSubscriptionsServer({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel = 'info', path = '/graphql', verifyToken, extractToken = defaultExtractSubscriptionToken, requireAuth, jwtClaimsToLog = ['oid', 'iss'], resolveSubscriptionOperationLogger, includeDeprecatedElements, connectionExpiry, keepAlive = 12000, allowQueriesAndMutations = false, }) {
|
|
18
32
|
if (requireAuth && !verifyToken)
|
|
19
33
|
throw new Error('verifyToken must be supplied when requireAuth is true');
|
|
20
34
|
const wsServer = new WebSocketServer({
|
|
@@ -105,33 +119,50 @@ function useSubscriptionsServer({ schema, httpServer, createSubscriptionContext,
|
|
|
105
119
|
endConnectionAt(ctx.extra.socket, endsAt, claims);
|
|
106
120
|
return true;
|
|
107
121
|
},
|
|
108
|
-
onSubscribe: async (ctx) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (endsAt !== undefined) {
|
|
113
|
-
// The token stands for the life connectionExpiry gave the connection. This covers an operation that
|
|
114
|
-
// arrives before the timer closes the connection.
|
|
115
|
-
if (Date.now() < endsAt)
|
|
122
|
+
onSubscribe: async (ctx, _id, payload) => {
|
|
123
|
+
// Returns the errors that refuse the operation, or undefined when the connection may run it.
|
|
124
|
+
const authorise = async () => {
|
|
125
|
+
if (!verifyToken)
|
|
116
126
|
return;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
logger.
|
|
127
|
+
const endsAt = connectionEnds.get(ctx.extra.socket);
|
|
128
|
+
if (endsAt !== undefined) {
|
|
129
|
+
// The token stands for the life connectionExpiry gave the connection. This covers an operation that
|
|
130
|
+
// arrives before the timer closes the connection.
|
|
131
|
+
if (Date.now() < endsAt)
|
|
132
|
+
return;
|
|
133
|
+
logger.warn('Subscription connection has expired', { claims: pick(ctx.extra.claims, jwtClaimsToLog) });
|
|
134
|
+
return refuse(ctx.extra.socket);
|
|
135
|
+
}
|
|
136
|
+
const token = extractToken({ connectRequest: ctx.extra.request, connectionParams: ctx.connectionParams });
|
|
137
|
+
if (!token) {
|
|
138
|
+
if (requireAuth) {
|
|
139
|
+
logger.error('No auth token was supplied with the websocket connection');
|
|
140
|
+
return refuse(ctx.extra.socket);
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
await verifyToken(getHost(ctx.extra.request), token);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
logger.warn('Subscription connection auth token is no longer valid', { claims: pick(ctx.extra.claims, jwtClaimsToLog), error });
|
|
124
149
|
return refuse(ctx.extra.socket);
|
|
125
150
|
}
|
|
151
|
+
};
|
|
152
|
+
const refused = await authorise();
|
|
153
|
+
if (refused)
|
|
154
|
+
return refused;
|
|
155
|
+
if (allowQueriesAndMutations)
|
|
126
156
|
return;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
157
|
+
const operationType = operationTypeOf(payload.query, payload.operationName);
|
|
158
|
+
if (operationType === undefined || operationType === 'subscription')
|
|
159
|
+
return;
|
|
160
|
+
logger.warn('Subscription connection refused a non-subscription operation', {
|
|
161
|
+
operationType,
|
|
162
|
+
operationName: payload.operationName,
|
|
163
|
+
claims: pick(ctx.extra.claims, jwtClaimsToLog),
|
|
164
|
+
});
|
|
165
|
+
return [new SubscriptionRefusedError(`Only subscription operations are allowed, not ${operationType} operations`, 'BAD_REQUEST')];
|
|
135
166
|
},
|
|
136
167
|
onDisconnect({ extra: { claims } }) {
|
|
137
168
|
logger.info('Subscription connection disconnected', { claims: pick(claims, jwtClaimsToLog) });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","sources":["../../src/subscriptions/server.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { pick } from 'es-toolkit/compat'\nimport { GraphQLError, type GraphQLSchema } from 'graphql'\nimport { CloseCode } from 'graphql-ws'\nimport { useServer } from 'graphql-ws/use/ws'\nimport type { Server } from 'http'\nimport { WebSocketServer, type WebSocket } from 'ws'\nimport type { GraphQLContext, JwtPayload } from '../context'\nimport { logSubscriptionOperation } from '../logging'\nimport type { CreateSubscriptionContext, ExtractSubscriptionToken } from './context'\nimport { defaultExtractSubscriptionToken, getHost } from './utils'\n\n// The longest delay setTimeout accepts. A connection that ends later than this is timed in steps.\nconst maxTimeoutMs = 2 ** 31 - 1\n\n// What onSubscribe returns for an operation it refuses, so onError can tell a refusal from a real error.\nclass SubscriptionRefusedError extends GraphQLError {\n constructor() {\n super('Forbidden', { extensions: { code: 'FORBIDDEN' } })\n }\n}\n\nexport function useSubscriptionsServer<TLogger extends Logger = Logger>({\n schema,\n httpServer,\n createSubscriptionContext,\n logger,\n operationLogLevel = 'info',\n path = '/graphql',\n verifyToken,\n extractToken = defaultExtractSubscriptionToken,\n requireAuth,\n jwtClaimsToLog = ['oid', 'iss'],\n resolveSubscriptionOperationLogger,\n includeDeprecatedElements,\n connectionExpiry,\n keepAlive = 12_000,\n}: {\n schema: GraphQLSchema\n httpServer: Server\n createSubscriptionContext: CreateSubscriptionContext\n logger: TLogger\n operationLogLevel?: keyof TLogger\n path?: string\n verifyToken?: (host: string, token: string) => Promise<JwtPayload>\n /**\n * Returns the token to pass to `verifyToken`, e.g. from a JWT assertion header that a proxy adds\n * to the connect request. Defaults to the bearer token in the `authorization` or `Authorization`\n * connection parameter. Pass the same function to `createSubscriptionContextFactory`.\n */\n extractToken?: ExtractSubscriptionToken\n requireAuth?: boolean\n jwtClaimsToLog?: string[]\n resolveSubscriptionOperationLogger?: (context: GraphQLContext) => TLogger\n /**\n * If true, deprecated schema elements a subscription uses are collected and logged when it is\n * established. Not collected per emitted payload: the usage is a property of the operation.\n */\n includeDeprecatedElements?: boolean\n /**\n * Returns the time a connection ends, in milliseconds since the epoch, from the claims `verifyToken`\n * returned when it was established. `undefined` keeps the default behaviour below.\n *\n * When it returns a time:\n * - A time already past refuses the connection with `4403 Forbidden`, as an invalid token does.\n * - Otherwise at that time the connection is closed with `4403`, which ends every subscription\n * running on it.\n * - Until then the client may subscribe again on the connection, and the token is not verified\n * again: it was verified when the connection was established, and it cannot change for the life\n * of the connection.\n *\n * To end a connection when its token expires: `(claims) => (claims.exp === undefined ? undefined : claims.exp * 1000)`.\n * When the token is short-lived proof of a longer session, such as an assertion that a proxy adds to\n * the upgrade request, a later time keeps subscriptions from being cut off every few minutes:\n * `(claims) => (claims.iat === undefined ? undefined : (claims.iat + 3600) * 1000)`. A connection then\n * outlives its token, so a revoked session is noticed only when the connection ends.\n *\n * By default a connection has no set end. Its token is verified again each time the client\n * subscribes, and a token that no longer verifies closes the connection with `4403` then.\n * Subscriptions already running continue until that happens.\n *\n * graphql-ws clients retry `4403` by default, but a retry succeeds only with a fresh token: the\n * client's `connectionParams` must be a function that returns a current token, or the upgrade\n * request must be authenticated afresh (by a cookie and a proxy, for example). A client that sends\n * the same expired token fails again until its `retryAttempts` run out, and its subscriptions then\n * end with an error.\n */\n connectionExpiry?: (claims: JwtPayload) => number | undefined\n /**\n * Milliseconds between the server's websocket pings. A connection that has not answered a ping by\n * the time the next one is due is terminated. `0` turns the pings off. Defaults to graphql-ws's 12\n * seconds.\n */\n keepAlive?: number\n}) {\n if (requireAuth && !verifyToken) throw new Error('verifyToken must be supplied when requireAuth is true')\n\n const wsServer = new WebSocketServer({\n server: httpServer,\n path,\n })\n\n // When each connection ends, for the connections that connectionExpiry gave a time.\n const connectionEnds = new WeakMap<WebSocket, number>()\n\n const endConnectionAt = (socket: WebSocket, endsAt: number, claims: JwtPayload) => {\n // A client that went away while its token was being verified has already closed the socket.\n if (socket.readyState !== socket.OPEN) return\n connectionEnds.set(socket, endsAt)\n let timer: NodeJS.Timeout | undefined\n // The socket's own close event, not graphql-ws's onClose, which waits for every subscription on the connection to\n // return. A subscription waiting for its next event can hold that up indefinitely.\n socket.once('close', () => clearTimeout(timer))\n const schedule = () => {\n const remaining = endsAt - Date.now()\n timer = setTimeout(\n () => {\n if (remaining > maxTimeoutMs) return schedule()\n if (socket.readyState !== socket.OPEN) return\n logger.info('Subscription connection expired', { claims: pick(claims, jwtClaimsToLog) })\n socket.close(CloseCode.Forbidden, 'Forbidden')\n },\n Math.min(Math.max(remaining, 0), maxTimeoutMs),\n )\n timer.unref()\n }\n schedule()\n }\n\n // Closes the connection, and keeps graphql-ws from running the operation: it would run it after the\n // close if onSubscribe returned nothing.\n const refuse = (socket: WebSocket) => {\n socket.close(CloseCode.Forbidden, 'Forbidden')\n return [new SubscriptionRefusedError()]\n }\n\n return useServer(\n {\n schema,\n onError(_ctx, id, _payload, errors) {\n // A refusal is already logged, and is not a server error.\n if (errors.every((error) => error instanceof SubscriptionRefusedError)) return\n logger.error('GraphQL subscriptions server error', { id, errors })\n },\n onConnect: async (ctx) => {\n const connectionEstablished = 'Subscription connection established'\n if (!verifyToken) {\n logger.info(connectionEstablished)\n return true\n }\n\n const token = extractToken({ connectRequest: ctx.extra.request, connectionParams: ctx.connectionParams })\n if (!token) {\n if (requireAuth) {\n logger.error('No auth token was supplied with the websocket connection')\n return false\n }\n logger.info(connectionEstablished)\n return true\n }\n\n let claims: JwtPayload\n try {\n claims = await verifyToken(getHost(ctx.extra.request), token)\n } catch (error) {\n logger.error('Failed to verify subscription connection auth token', { error })\n return false\n }\n\n let endsAt: number | undefined\n try {\n endsAt = connectionExpiry?.(claims)\n } catch (error) {\n logger.error('Failed to compute subscription connection expiry', { claims: pick(claims, jwtClaimsToLog), error })\n return false\n }\n // Refused before the connection is acknowledged: an acknowledged connection resets a graphql-ws\n // client's retry count, so closing it at once would have the client reconnect without end.\n if (endsAt !== undefined && !(endsAt > Date.now())) {\n logger.warn('Subscription connection has already expired', { claims: pick(claims, jwtClaimsToLog) })\n return false\n }\n\n ctx.extra.claims = claims as unknown as undefined\n logger.info(connectionEstablished, {\n claims: pick(claims, jwtClaimsToLog),\n })\n if (endsAt !== undefined) endConnectionAt(ctx.extra.socket, endsAt, claims)\n return true\n },\n onSubscribe: async (ctx) => {\n if (!verifyToken) return\n const endsAt = connectionEnds.get(ctx.extra.socket)\n if (endsAt !== undefined) {\n // The token stands for the life connectionExpiry gave the connection. This covers an operation that\n // arrives before the timer closes the connection.\n if (Date.now() < endsAt) return\n logger.warn('Subscription connection has expired', { claims: pick(ctx.extra.claims, jwtClaimsToLog) })\n return refuse(ctx.extra.socket)\n }\n const token = extractToken({ connectRequest: ctx.extra.request, connectionParams: ctx.connectionParams })\n if (!token) {\n if (requireAuth) {\n logger.error('No auth token was supplied with the websocket connection')\n return refuse(ctx.extra.socket)\n }\n return\n }\n try {\n await verifyToken(getHost(ctx.extra.request), token)\n } catch (error) {\n logger.warn('Subscription connection auth token is no longer valid', { claims: pick(ctx.extra.claims, jwtClaimsToLog), error })\n return refuse(ctx.extra.socket)\n }\n },\n onDisconnect({ extra: { claims } }) {\n logger.info('Subscription connection disconnected', { claims: pick(claims, jwtClaimsToLog) })\n },\n context: async (ctx) => {\n return createSubscriptionContext({\n connectRequest: ctx.extra.request,\n connectionParams: ctx.connectionParams,\n claims: ctx.extra.claims as JwtPayload | undefined,\n })\n },\n onOperation(_ctx, id, _payload, args) {\n logSubscriptionOperation({\n id,\n args,\n logLevel: operationLogLevel,\n resolveLogger: resolveSubscriptionOperationLogger,\n includeDeprecatedElements,\n })\n },\n onNext(_ctx, id, _payload, args, { data, ...result }) {\n logSubscriptionOperation({ id, args, logLevel: operationLogLevel, result, resolveLogger: resolveSubscriptionOperationLogger })\n },\n },\n wsServer,\n keepAlive,\n )\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.mjs","sources":["../../src/subscriptions/server.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { pick } from 'es-toolkit/compat'\nimport { getOperationAST, GraphQLError, parse, type DocumentNode, type GraphQLSchema } from 'graphql'\nimport { CloseCode } from 'graphql-ws'\nimport { useServer } from 'graphql-ws/use/ws'\nimport type { Server } from 'http'\nimport { WebSocketServer, type WebSocket } from 'ws'\nimport type { GraphQLContext, JwtPayload } from '../context'\nimport { logSubscriptionOperation } from '../logging'\nimport type { CreateSubscriptionContext, ExtractSubscriptionToken } from './context'\nimport { defaultExtractSubscriptionToken, getHost } from './utils'\n\n// The longest delay setTimeout accepts. A connection that ends later than this is timed in steps.\nconst maxTimeoutMs = 2 ** 31 - 1\n\n// What onSubscribe returns for an operation it refuses, so onError can tell a refusal from a real error.\nclass SubscriptionRefusedError extends GraphQLError {\n constructor(message = 'Forbidden', code = 'FORBIDDEN') {\n super(message, { extensions: { code } })\n }\n}\n\n// The type of the operation the client asked to run, or undefined when graphql-ws must report the\n// document as invalid.\nconst operationTypeOf = (query: unknown, operationName: string | null | undefined) => {\n if (typeof query !== 'string') return undefined\n let document: DocumentNode\n try {\n document = parse(query)\n } catch {\n return undefined\n }\n return getOperationAST(document, operationName)?.operation\n}\n\nexport function useSubscriptionsServer<TLogger extends Logger = Logger>({\n schema,\n httpServer,\n createSubscriptionContext,\n logger,\n operationLogLevel = 'info',\n path = '/graphql',\n verifyToken,\n extractToken = defaultExtractSubscriptionToken,\n requireAuth,\n jwtClaimsToLog = ['oid', 'iss'],\n resolveSubscriptionOperationLogger,\n includeDeprecatedElements,\n connectionExpiry,\n keepAlive = 12_000,\n allowQueriesAndMutations = false,\n}: {\n schema: GraphQLSchema\n httpServer: Server\n createSubscriptionContext: CreateSubscriptionContext\n logger: TLogger\n operationLogLevel?: keyof TLogger\n path?: string\n verifyToken?: (host: string, token: string) => Promise<JwtPayload>\n /**\n * Returns the token to pass to `verifyToken`, e.g. from a JWT assertion header that a proxy adds\n * to the connect request. Defaults to the bearer token in the `authorization` or `Authorization`\n * connection parameter. Pass the same function to `createSubscriptionContextFactory`.\n */\n extractToken?: ExtractSubscriptionToken\n requireAuth?: boolean\n jwtClaimsToLog?: string[]\n resolveSubscriptionOperationLogger?: (context: GraphQLContext) => TLogger\n /**\n * If true, deprecated schema elements a subscription uses are collected and logged when it is\n * established. Not collected per emitted payload: the usage is a property of the operation.\n */\n includeDeprecatedElements?: boolean\n /**\n * Returns the time a connection ends, in milliseconds since the epoch, from the claims `verifyToken`\n * returned when it was established. `undefined` keeps the default behaviour below.\n *\n * When it returns a time:\n * - A time already past refuses the connection with `4403 Forbidden`, as an invalid token does.\n * - Otherwise at that time the connection is closed with `4403`, which ends every subscription\n * running on it.\n * - Until then the client may subscribe again on the connection, and the token is not verified\n * again: it was verified when the connection was established, and it cannot change for the life\n * of the connection.\n *\n * To end a connection when its token expires: `(claims) => (claims.exp === undefined ? undefined : claims.exp * 1000)`.\n * When the token is short-lived proof of a longer session, such as an assertion that a proxy adds to\n * the upgrade request, a later time keeps subscriptions from being cut off every few minutes:\n * `(claims) => (claims.iat === undefined ? undefined : (claims.iat + 3600) * 1000)`. A connection then\n * outlives its token, so a revoked session is noticed only when the connection ends.\n *\n * By default a connection has no set end. Its token is verified again each time the client\n * subscribes, and a token that no longer verifies closes the connection with `4403` then.\n * Subscriptions already running continue until that happens.\n *\n * graphql-ws clients retry `4403` by default, but a retry succeeds only with a fresh token: the\n * client's `connectionParams` must be a function that returns a current token, or the upgrade\n * request must be authenticated afresh (by a cookie and a proxy, for example). A client that sends\n * the same expired token fails again until its `retryAttempts` run out, and its subscriptions then\n * end with an error.\n */\n connectionExpiry?: (claims: JwtPayload) => number | undefined\n /**\n * Milliseconds between the server's websocket pings. A connection that has not answered a ping by\n * the time the next one is due is terminated. `0` turns the pings off. Defaults to graphql-ws's 12\n * seconds.\n */\n keepAlive?: number\n /**\n * If true, the server also runs query and mutation operations. By default it runs only\n * subscription operations, and refuses the others with a `BAD_REQUEST` error. The connection\n * stays open.\n *\n * Queries and mutations sent over the websocket do not go through the HTTP request pipeline, so\n * they bypass its checks (middleware, Apollo Server plugins, rate limits and similar). Set this\n * only when the websocket path applies the same checks.\n */\n allowQueriesAndMutations?: boolean\n}) {\n if (requireAuth && !verifyToken) throw new Error('verifyToken must be supplied when requireAuth is true')\n\n const wsServer = new WebSocketServer({\n server: httpServer,\n path,\n })\n\n // When each connection ends, for the connections that connectionExpiry gave a time.\n const connectionEnds = new WeakMap<WebSocket, number>()\n\n const endConnectionAt = (socket: WebSocket, endsAt: number, claims: JwtPayload) => {\n // A client that went away while its token was being verified has already closed the socket.\n if (socket.readyState !== socket.OPEN) return\n connectionEnds.set(socket, endsAt)\n let timer: NodeJS.Timeout | undefined\n // The socket's own close event, not graphql-ws's onClose, which waits for every subscription on the connection to\n // return. A subscription waiting for its next event can hold that up indefinitely.\n socket.once('close', () => clearTimeout(timer))\n const schedule = () => {\n const remaining = endsAt - Date.now()\n timer = setTimeout(\n () => {\n if (remaining > maxTimeoutMs) return schedule()\n if (socket.readyState !== socket.OPEN) return\n logger.info('Subscription connection expired', { claims: pick(claims, jwtClaimsToLog) })\n socket.close(CloseCode.Forbidden, 'Forbidden')\n },\n Math.min(Math.max(remaining, 0), maxTimeoutMs),\n )\n timer.unref()\n }\n schedule()\n }\n\n // Closes the connection, and keeps graphql-ws from running the operation: it would run it after the\n // close if onSubscribe returned nothing.\n const refuse = (socket: WebSocket) => {\n socket.close(CloseCode.Forbidden, 'Forbidden')\n return [new SubscriptionRefusedError()]\n }\n\n return useServer(\n {\n schema,\n onError(_ctx, id, _payload, errors) {\n // A refusal is already logged, and is not a server error.\n if (errors.every((error) => error instanceof SubscriptionRefusedError)) return\n logger.error('GraphQL subscriptions server error', { id, errors })\n },\n onConnect: async (ctx) => {\n const connectionEstablished = 'Subscription connection established'\n if (!verifyToken) {\n logger.info(connectionEstablished)\n return true\n }\n\n const token = extractToken({ connectRequest: ctx.extra.request, connectionParams: ctx.connectionParams })\n if (!token) {\n if (requireAuth) {\n logger.error('No auth token was supplied with the websocket connection')\n return false\n }\n logger.info(connectionEstablished)\n return true\n }\n\n let claims: JwtPayload\n try {\n claims = await verifyToken(getHost(ctx.extra.request), token)\n } catch (error) {\n logger.error('Failed to verify subscription connection auth token', { error })\n return false\n }\n\n let endsAt: number | undefined\n try {\n endsAt = connectionExpiry?.(claims)\n } catch (error) {\n logger.error('Failed to compute subscription connection expiry', { claims: pick(claims, jwtClaimsToLog), error })\n return false\n }\n // Refused before the connection is acknowledged: an acknowledged connection resets a graphql-ws\n // client's retry count, so closing it at once would have the client reconnect without end.\n if (endsAt !== undefined && !(endsAt > Date.now())) {\n logger.warn('Subscription connection has already expired', { claims: pick(claims, jwtClaimsToLog) })\n return false\n }\n\n ctx.extra.claims = claims as unknown as undefined\n logger.info(connectionEstablished, {\n claims: pick(claims, jwtClaimsToLog),\n })\n if (endsAt !== undefined) endConnectionAt(ctx.extra.socket, endsAt, claims)\n return true\n },\n onSubscribe: async (ctx, _id, payload) => {\n // Returns the errors that refuse the operation, or undefined when the connection may run it.\n const authorise = async () => {\n if (!verifyToken) return\n const endsAt = connectionEnds.get(ctx.extra.socket)\n if (endsAt !== undefined) {\n // The token stands for the life connectionExpiry gave the connection. This covers an operation that\n // arrives before the timer closes the connection.\n if (Date.now() < endsAt) return\n logger.warn('Subscription connection has expired', { claims: pick(ctx.extra.claims, jwtClaimsToLog) })\n return refuse(ctx.extra.socket)\n }\n const token = extractToken({ connectRequest: ctx.extra.request, connectionParams: ctx.connectionParams })\n if (!token) {\n if (requireAuth) {\n logger.error('No auth token was supplied with the websocket connection')\n return refuse(ctx.extra.socket)\n }\n return\n }\n try {\n await verifyToken(getHost(ctx.extra.request), token)\n } catch (error) {\n logger.warn('Subscription connection auth token is no longer valid', { claims: pick(ctx.extra.claims, jwtClaimsToLog), error })\n return refuse(ctx.extra.socket)\n }\n }\n const refused = await authorise()\n if (refused) return refused\n if (allowQueriesAndMutations) return\n const operationType = operationTypeOf(payload.query, payload.operationName)\n if (operationType === undefined || operationType === 'subscription') return\n logger.warn('Subscription connection refused a non-subscription operation', {\n operationType,\n operationName: payload.operationName,\n claims: pick(ctx.extra.claims, jwtClaimsToLog),\n })\n return [new SubscriptionRefusedError(`Only subscription operations are allowed, not ${operationType} operations`, 'BAD_REQUEST')]\n },\n onDisconnect({ extra: { claims } }) {\n logger.info('Subscription connection disconnected', { claims: pick(claims, jwtClaimsToLog) })\n },\n context: async (ctx) => {\n return createSubscriptionContext({\n connectRequest: ctx.extra.request,\n connectionParams: ctx.connectionParams,\n claims: ctx.extra.claims as JwtPayload | undefined,\n })\n },\n onOperation(_ctx, id, _payload, args) {\n logSubscriptionOperation({\n id,\n args,\n logLevel: operationLogLevel,\n resolveLogger: resolveSubscriptionOperationLogger,\n includeDeprecatedElements,\n })\n },\n onNext(_ctx, id, _payload, args, { data, ...result }) {\n logSubscriptionOperation({ id, args, logLevel: operationLogLevel, result, resolveLogger: resolveSubscriptionOperationLogger })\n },\n },\n wsServer,\n keepAlive,\n )\n}\n"],"names":[],"mappings":"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"}
|