@nxgt/janus-graphql 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  [`@nxgt/janus`](https://www.npmjs.com/package/@nxgt/janus) in a GraphQL
4
4
  server: the signed-in user on the context, authenticated only when a field
5
5
  asks, fields and types guarded by `@authenticated`, `@fresh` and
6
- `@permission`, and
7
- every error answered with the status it deserves — an outage as 503, never
8
- as 401, 403 or 404.
6
+ `@permission`, the
7
+ same guards on subscriptions over graphql-ws, and every error answered with
8
+ the status it deserves — an outage as 503, never as 401, 403 or 404.
9
9
 
10
10
  An [envelop](https://the-guild.dev/graphql/envelop) plugin, so it runs in
11
11
  [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) and any server built
@@ -52,7 +52,7 @@ bun add @nxgt/janus-graphql @nxgt/janus graphql @graphql-tools/utils @envelop/co
52
52
  bun add -d typescript # 6
53
53
  ```
54
54
 
55
- Every peer is required:
55
+ Every peer is required but `graphql-ws`:
56
56
 
57
57
  | Peer | Range | Why |
58
58
  | --- | --- | --- |
@@ -61,11 +61,12 @@ Every peer is required:
61
61
  | `@graphql-tools/utils` | `>=10.0.0 <13` | `mapSchema` and `getDirective`, which apply the directives |
62
62
  | `@envelop/core` | `^5.0.0` | Types only — the `Plugin` `useJanus()` answers. Yoga already brings it |
63
63
  | `typescript` | `^6.0.3` | As for `@nxgt/janus` |
64
+ | `graphql-ws` | `^6.0.0`, **optional** | Only for subscriptions over a WebSocket, with `janusConnection()`. Nothing here imports it. For `graphql` 17, a `graphql-ws` whose peer range includes it — 6.3.0 does |
64
65
 
65
66
  The floors are tested, not claimed: the package's specs and its typecheck run
66
- on `graphql` 16.9.0, `@graphql-tools/utils` 10.0.0 and `@envelop/core` 5.0.0
67
- as well, together, with a single copy of `graphql`, in the Floors job, on
68
- every CI run.
67
+ on `graphql` 16.9.0, `@graphql-tools/utils` 10.0.0, `@envelop/core` 5.0.0
68
+ and `graphql-ws` 6.0.0 as well, together, with a single copy of `graphql`,
69
+ in the Floors job, on every CI run.
69
70
 
70
71
  Like `@nxgt/janus`, it expects `"moduleResolution": "bundler"`: the
71
72
  declarations import without extensions, so `nodenext` is not supported.
@@ -91,11 +92,12 @@ that reads files — `node_modules/@nxgt/janus-graphql/graphql/janus.graphqls`.
91
92
  | `requireUser(ctx, { type? })` | The signed-in user, narrowed to `type` — one or a non-empty list — or a denial: `UNAUTHENTICATED`, `FORBIDDEN` |
92
93
  | `requireFresh(ctx, maxAge)` | The request's session, once it proved who it is less than `maxAge` ago — a duration with its unit, `'10m'`, never a bare number — or a denial: `UNAUTHENTICATED`, `STEP_UP_REQUIRED` |
93
94
  | `can(ctx, permission, object, options?)` | `access.can` for the request's user, typed as `access.can` is. Anonymous answers `false`. Shares the request's checks with `@permission`: one question, one check per request |
95
+ | `janusConnection({ auth, access?, type?, clock?, upgrade? })` | Subscriptions over graphql-ws: `onConnect` for its `useServer()`, which accepts a connection whose `connectionParams.authorization` — else whose upgrade request's headers or cookie — authenticates, as a user of `type` when given, and refuses any other `4403`; an outage rejects, closed `4500`. Each operation's `ctx.janus` then authenticates from that credential, so the directives hold unchanged. `context` builds `{ janus }` for a server without Yoga; `upgrade` reads the upgrade request where the transport's `extra` does not carry it — Bun |
94
96
  | `janusMaskError(fallback?)` | Yoga's `maskedErrors.maskError`: a `JanusError` a resolver let through answered with its code and status; anything else to `fallback` |
95
97
  | `janusGraphQLError(error)` | A `JanusError` as the `GraphQLError` the client reads: its code, its status, and only what the client can act on |
96
98
  | `denial(code, message?)` | A denial of your own: `UNAUTHENTICATED` 401, `FORBIDDEN` 403, `NOT_FOUND` 404. For a stale session, call `requireFresh()`, whose `STEP_UP_REQUIRED` is `@nxgt/janus`'s |
97
99
  | `JanusContext<typeof auth, typeof access?, Type?>` | What `useJanus()` adds to the context, for your resolvers' `ctx` |
98
- | `JanusOptions`, `JanusOnContext`, `JanusDirectivesOptions`, `Loaders`, `Conditions`, `LoadedObject`, `RequireUserOptions`, `Auth`, `UserOfAuth`, `DenialCode`, `MaskError` | The types of the arguments and answers above |
100
+ | `JanusOptions`, `JanusOnContext`, `JanusConnectionOptions`, `JanusConnection`, `ConnectionContext`, `JanusDirectivesOptions`, `Loaders`, `Conditions`, `LoadedObject`, `RequireUserOptions`, `Auth`, `UserOfAuth`, `DenialCode`, `MaskError` | The types of the arguments and answers above |
99
101
 
100
102
  ## Directives
101
103
 
@@ -145,6 +147,46 @@ model does not declare, a malformed `id:`, an argument the field does not
145
147
  take, a missing loader or condition, a `maxAge` not above zero — is a
146
148
  `TypeError` at start-up naming the field. Detail: [the directives guide](docs/guide/directives.md).
147
149
 
150
+ ## Subscriptions over graphql-ws
151
+
152
+ Yoga's recommended graphql-ws setup, with `onConnect` added:
153
+
154
+ ```ts
155
+ import { janusConnection } from '@nxgt/janus-graphql';
156
+ import { useServer } from 'graphql-ws/use/ws';
157
+
158
+ useServer(
159
+ {
160
+ onConnect: janusConnection({ auth }).onConnect, // no valid session: closed 4403
161
+ execute: (args: any) => args.rootValue.execute(args),
162
+ subscribe: (args: any) => args.rootValue.subscribe(args),
163
+ onSubscribe: async (ctx, _id, params) => {
164
+ const { schema, execute, subscribe, contextFactory, parse, validate } =
165
+ yoga.getEnveloped({ ...ctx, req: ctx.extra.request, socket: ctx.extra.socket, params });
166
+ const args = {
167
+ schema,
168
+ operationName: params.operationName,
169
+ document: parse(params.query),
170
+ variableValues: params.variables,
171
+ contextValue: await contextFactory(), // useJanus() finds the connection by ctx.extra
172
+ rootValue: { execute, subscribe },
173
+ };
174
+ const errors = validate(args.schema, args.document);
175
+ return errors.length ? errors : args;
176
+ },
177
+ },
178
+ wsServer,
179
+ );
180
+ ```
181
+
182
+ A browser's session cookie travels on the upgrade request; any other client
183
+ sends `connectionParams: { authorization: 'Bearer <token>' }`, which is
184
+ read first. Each operation is authenticated again from that credential
185
+ when it subscribes: `@authenticated` and `@fresh` are checked then, and
186
+ `@permission` on every event. A session revoked mid-stream is refused at
187
+ the next subscribe, not at the next event. Detail, Bun, and a server
188
+ without Yoga: [the subscriptions guide](docs/guide/subscriptions.md).
189
+
148
190
  ## Errors
149
191
 
150
192
  This package defines **no error class**. A denial is a `GraphQLError`, and
@@ -218,8 +260,19 @@ response with several errors: [the errors guide](docs/guide/errors.md).
218
260
  on every one, and one denial denies the field; to answer only the items a
219
261
  user may see, ask `access.list()` for their ids.
220
262
  - **A context built without a request cannot authenticate.** `ctx.janus.user()`
221
- rejects with a `TypeError` — a transport that is not HTTP, such as a
222
- WebSocket, needs its own wiring, which is on the [roadmap](docs/roadmap.md).
263
+ rejects with a `TypeError` — a transport that is not HTTP, or a graphql-ws
264
+ operation whose connection no `janusConnection().onConnect` accepted, or
265
+ whose context was built without Yoga's `...ctx` spread, which carries
266
+ `ctx.extra`.
267
+ - **A session revoked while a socket is open keeps its running streams.**
268
+ An operation authenticates once, when it subscribes, and each event's
269
+ `@authenticated` reads that answer; the next subscribe, and a
270
+ reconnection, are refused. Close the
271
+ socket yourself to cut it at once. `@permission` sees a `revoke()` at the
272
+ next event.
273
+ - **Bun's graphql-ws `extra` holds no request.** Without
274
+ `janusConnection({ upgrade })`, a browser's cookie is never read there,
275
+ and only `connectionParams` authenticates.
223
276
  - **`@fresh` reads seconds; `requireFresh()` a duration.** `@fresh(maxAge:
224
277
  10)` is ten seconds, not ten minutes — write `600`. `requireFresh(ctx,
225
278
  '10m')` takes the unit, and refuses a bare number, which `@nxgt/janus`
@@ -238,13 +291,13 @@ response with several errors: [the errors guide](docs/guide/errors.md).
238
291
 
239
292
  ## Documentation
240
293
 
241
- - [Guides](docs/README.md) — the directives, the step-up over GraphQL, the context and the lazy user, errors as statuses, telemetry, and federation
294
+ - [Guides](docs/README.md) — the directives, the step-up over GraphQL, subscriptions over graphql-ws, the context and the lazy user, errors as statuses, telemetry, and federation
242
295
  - [Troubleshooting](docs/troubleshooting.md) — by the message or status you see
243
296
  - [Roadmap](docs/roadmap.md) — what is next, and what is not planned
244
297
 
245
298
  ## Type safety, counted
246
299
 
247
- Twenty-nine plausible mistakes are refused by the compiler, each with a
300
+ Thirty-five plausible mistakes are refused by the compiler, each with a
248
301
  `@ts-expect-error` case in `test/types/`:
249
302
 
250
303
  - five in `context.ts`: reading the user or the session where either may be
@@ -268,9 +321,16 @@ Twenty-nine plausible mistakes are refused by the compiler, each with a
268
321
  `access`;
269
322
  - five in `fresh.ts`: `requireFresh()` given a bare number, a duration
270
323
  written as prose, no `maxAge` at all, or the request instead of the
271
- context, and `useJanus()` given a timestamp as its `clock`.
272
-
273
- `plugin.ts` also holds this README's quick start, which must keep compiling.
324
+ context, and `useJanus()` given a timestamp as its `clock`;
325
+ - six in `connection.ts`: `janusConnection()` given a user type the
326
+ instance does not know, the `permissions()` instance as `auth`, or a
327
+ timestamp as its `clock`; `ctx.janus.access` from a connection given no
328
+ `access`, a field of another user type once narrowed, and an `upgrade`
329
+ answering a token rather than a request.
330
+
331
+ `plugin.ts` also holds this README's quick start, and `connection.ts` the
332
+ graphql-ws wiring — Yoga's recipe, without Yoga, and Bun's `upgrade` —
333
+ against graphql-ws's own types; both must keep compiling.
274
334
 
275
335
  ## Licence
276
336
 
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `janusConnection()`: authenticated subscriptions over graphql-ws.
3
+ */
4
+ import type { Auth, UserOfAuth } from '../types';
5
+ import type { JanusConnection, JanusConnectionOptions } from './types';
6
+ /**
7
+ * The pieces of graphql-ws's `useServer()` that authenticate a WebSocket
8
+ * connection, for Yoga's recommended setup:
9
+ *
10
+ * ```ts
11
+ * const connection = janusConnection({ auth });
12
+ * useServer({
13
+ * onConnect: connection.onConnect,
14
+ * execute: (args) => args.rootValue.execute(args),
15
+ * subscribe: (args) => args.rootValue.subscribe(args),
16
+ * onSubscribe: async (ctx, _id, params) => { … yoga.getEnveloped({ ...ctx, … }) … },
17
+ * }, wsServer);
18
+ * ```
19
+ *
20
+ * `onConnect` reads the connection's credential — `connectionParams`'
21
+ * `authorization`, else the upgrade request's headers and cookie — and
22
+ * refuses it unless it authenticates, as a user of `type` when it is given.
23
+ * Each operation on the connection then gets a `ctx.janus` of its own from
24
+ * `useJanus()`, authenticated again from that credential when a field asks:
25
+ * `@authenticated`, `@fresh` and `@permission` hold as over HTTP, and a
26
+ * session revoked since the connection opened is refused at the next
27
+ * subscribe.
28
+ *
29
+ * A server without Yoga passes `context` too, and applies the directives
30
+ * with `applyJanusDirectives()`.
31
+ */
32
+ export declare function janusConnection<A extends Auth<{
33
+ readonly type: string;
34
+ readonly id: string;
35
+ }>, P extends object | undefined = undefined, const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(options: JanusConnectionOptions<A, P, T>): JanusConnection<A, P, T>;
36
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/connection/connection.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAgB,UAAU,EAAE,MAAM,UAAU,CAAC;AAE/D,OAAO,KAAK,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,eAAe,CAC9B,CAAC,SAAS,IAAI,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,EAC9D,CAAC,SAAS,MAAM,GAAG,SAAS,GAAG,SAAS,EACxC,KAAK,CAAC,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAC5D,OAAO,EAAE,sBAAsB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAqCpE"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The credential a graphql-ws connection presents, and the connections
3
+ * `janusConnection().onConnect` accepted: what each operation on one is
4
+ * authenticated from.
5
+ */
6
+ import type { RequestLike } from '@nxgt/janus';
7
+ import type { ConnectionContext } from './types';
8
+ /** Where the upgrade request is, when the transport does not say `extra.request`. */
9
+ export type Upgrade = (ctx: ConnectionContext) => RequestLike | undefined;
10
+ /**
11
+ * The connection's credential, as `auth.authenticate` reads one:
12
+ *
13
+ * 1. `connectionParams.authorization`, when it is a `Bearer` token — a
14
+ * non-browser client's `'Bearer <token>'`;
15
+ * 2. otherwise the upgrade request itself, read as any request is —
16
+ * `Authorization`, then `X-Session-Token`, then the session cookie a
17
+ * browser sends with it.
18
+ *
19
+ * **The first present wins, not the first valid one**, as in `@nxgt/janus`:
20
+ * a lapsed token in `connectionParams` beside a live cookie is refused. An
21
+ * `authorization` of another scheme (`Basic`) is not a session credential,
22
+ * as in `@nxgt/janus`, and does not count. `null` when there is nothing to
23
+ * read, or an `authorization` that is not a string — refused, as a
24
+ * client's mistake.
25
+ */
26
+ export declare function credentialOf(ctx: ConnectionContext, upgrade: Upgrade | undefined): RequestLike | null;
27
+ /** Remembers `credential` as the one `ctx`'s connection presented. */
28
+ export declare function accept(ctx: ConnectionContext, credential: RequestLike): void;
29
+ /**
30
+ * The credential of the connection a context was built for, or `undefined`
31
+ * for one no `onConnect` accepted — and for every HTTP request.
32
+ */
33
+ export declare function acceptedOf(context: unknown): RequestLike | undefined;
34
+ //# sourceMappingURL=credential.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential.d.ts","sourceRoot":"","sources":["../../src/connection/credential.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,qFAAqF;AACrF,MAAM,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,iBAAiB,KAAK,WAAW,GAAG,SAAS,CAAC;AAK1E;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC3B,GAAG,EAAE,iBAAiB,EACtB,OAAO,EAAE,OAAO,GAAG,SAAS,GAC1B,WAAW,GAAG,IAAI,CAcpB;AAWD,sEAAsE;AACtE,wBAAgB,MAAM,CAAC,GAAG,EAAE,iBAAiB,EAAE,UAAU,EAAE,WAAW,QAQrE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAIpE"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The public types of `janusConnection()`: what it takes, and the pieces
3
+ * it answers for graphql-ws's `useServer()`. Typed by their shape, so the
4
+ * package imports nothing from graphql-ws. Nothing here runs.
5
+ */
6
+ import type { Clock, RequestLike } from '@nxgt/janus';
7
+ import type { JanusContext, UserOfAuth } from '../types';
8
+ /**
9
+ * The part of graphql-ws's `Context` read here: the `connectionParams` the
10
+ * client sent with `connection_init`, and the transport's `extra` —
11
+ * `{ socket, request }` for `graphql-ws/use/ws`.
12
+ */
13
+ export interface ConnectionContext {
14
+ readonly connectionParams?: Readonly<Record<string, unknown>> | undefined;
15
+ readonly extra: unknown;
16
+ }
17
+ /** What `janusConnection()` takes. */
18
+ export interface JanusConnectionOptions<A, P, T extends string> {
19
+ /** What `janus()` answered. */
20
+ readonly auth: A;
21
+ /**
22
+ * What `permissions()` answered: `ctx.janus.access` in the context
23
+ * `context()` builds. `useJanus()`'s own when Yoga builds it.
24
+ */
25
+ readonly access?: P;
26
+ /** Only a user of this type may connect; any other is refused. */
27
+ readonly type?: T;
28
+ /** The clock `@fresh` reads in the context `context()` builds. */
29
+ readonly clock?: Clock;
30
+ /**
31
+ * The upgrade request of a connection, for a transport whose `extra`
32
+ * does not carry it as `request` — Bun's is `{ socket }`:
33
+ * `upgrade: (ctx) => ctx.extra.socket.data.request`, with the request
34
+ * passed to `server.upgrade(req, { data: { request: req } })`.
35
+ *
36
+ * A method, so `ctx` may be annotated with the transport's own `extra`.
37
+ */
38
+ upgrade?(ctx: ConnectionContext): RequestLike | undefined;
39
+ }
40
+ /** What `janusConnection()` answers: the pieces of graphql-ws's `useServer()`. */
41
+ export interface JanusConnection<A, P = undefined, T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']> {
42
+ /**
43
+ * `useServer({ onConnect })`: accepts a connection whose credential
44
+ * authenticates, and answers `false` for any other — graphql-ws closes
45
+ * it `4403: Forbidden`. An outage rejects, never `false`: the transport
46
+ * closes it `4500`, and the client does not retry it as a refusal.
47
+ */
48
+ readonly onConnect: (ctx: ConnectionContext) => Promise<boolean>;
49
+ /**
50
+ * `useServer({ context })`, for a server without Yoga: `ctx.janus` for
51
+ * one operation, authenticated from the connection's credential. Yoga's
52
+ * recipe builds the context itself, and `useJanus()` finds the
53
+ * connection there: leave this out.
54
+ */
55
+ readonly context: (ctx: ConnectionContext) => JanusContext<A, P, T>;
56
+ }
57
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/connection/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEzD;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAC1E,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,MAAM;IAC7D,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACpB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClB,kEAAkE;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB;;;;;;;OAOG;IACH,OAAO,CAAC,CAAC,GAAG,EAAE,iBAAiB,GAAG,WAAW,GAAG,SAAS,CAAC;CAC1D;AAED,kFAAkF;AAClF,MAAM,WAAW,eAAe,CAC/B,CAAC,EACD,CAAC,GAAG,SAAS,EACb,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAEvD;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;CACpE"}
package/dist/context.d.ts CHANGED
@@ -34,13 +34,16 @@ export interface ContextOptions {
34
34
  } | undefined;
35
35
  readonly type?: string | undefined;
36
36
  readonly clock?: Clock | undefined;
37
+ /** The call the consumer wrote, named by the no-request `TypeError`. */
38
+ readonly caller?: string;
37
39
  }
38
40
  /**
39
41
  * `ctx.janus` for one request. `authenticate` runs the first time `user()`
40
42
  * or `session()` is asked, once, and its answer — or its failure — is the
41
43
  * answer of both from then on.
42
44
  *
43
- * A context with no request — a transport that is not HTTP — answers a
45
+ * A context with no request — a transport that is not HTTP, or a graphql-ws
46
+ * connection no `janusConnection().onConnect` accepted — answers a
44
47
  * `TypeError` from `user()`, not `null`: anonymous is an answer, and nothing
45
48
  * here asked.
46
49
  */
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACN,KAAK,aAAa,EAClB,KAAK,KAAK,EACV,KAAK,WAAW,EAEhB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,iFAAiF;AACjF,MAAM,MAAM,KAAK,GAAG,CACnB,OAAO,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAC9D,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,EACtD,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,KAChC,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB,qEAAqE;AACrE,KAAK,OAAO,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9D,KAAK,SAAS,GAAG;IAChB,YAAY,CACX,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAClC,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;CAC1C,CAAC;AAEF,KAAK,KAAK,GAAG,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAa9C,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;CACnC;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,WAAW,GAAG,SAAS,EAChC,OAAO,EAAE,cAAc,GACrB,KAAK,CAoCP;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAE5C;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,UAAQ,GAAG,KAAK,GAAG,IAAI,CAMlE;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAQ3D"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACN,KAAK,aAAa,EAClB,KAAK,KAAK,EACV,KAAK,WAAW,EAEhB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,iFAAiF;AACjF,MAAM,MAAM,KAAK,GAAG,CACnB,OAAO,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAC9D,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,EACtD,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,KAChC,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB,qEAAqE;AACrE,KAAK,OAAO,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9D,KAAK,SAAS,GAAG;IAChB,YAAY,CACX,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAClC,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;CAC1C,CAAC;AAEF,KAAK,KAAK,GAAG,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAa9C,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IACnC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,WAAW,GAAG,SAAS,EAChC,OAAO,EAAE,cAAc,GACrB,KAAK,CAoCP;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAE5C;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,UAAQ,GAAG,KAAK,GAAG,IAAI,CAMlE;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAQ3D"}
package/dist/index.d.ts CHANGED
@@ -13,10 +13,16 @@
13
13
  * code and status, `STORE_FAILED` as 503 and never as 401 or 403.
14
14
  * - `janusGraphQLError(error)` — a `JanusError` as the `GraphQLError` the
15
15
  * client reads, and `denial(code, message?)` — a denial of your own.
16
+ * - `janusConnection({ auth, access?, type?, clock?, upgrade? })` —
17
+ * subscriptions over graphql-ws: `onConnect` and `context` for its
18
+ * `useServer()`, a connection authenticated from its `connectionParams`
19
+ * or its upgrade request.
16
20
  *
17
21
  * It defines no error class: a denial is a `GraphQLError`, and what it lets
18
22
  * through is `@nxgt/janus`'s own.
19
23
  */
24
+ export { janusConnection } from './connection/connection';
25
+ export type { ConnectionContext, JanusConnection, JanusConnectionOptions, } from './connection/types';
20
26
  export { applyJanusDirectives, type JanusDirectivesOptions, } from './directives/apply';
21
27
  export { type DenialCode, denial, janusGraphQLError, janusMaskError, type MaskError, } from './errors';
22
28
  export { requireFresh } from './fresh';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACN,oBAAoB,EACpB,KAAK,sBAAsB,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACN,KAAK,UAAU,EACf,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,KAAK,SAAS,GACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,GAAG,EAAE,KAAK,kBAAkB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,YAAY,EACX,IAAI,EACJ,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,UAAU,GACV,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EACX,iBAAiB,EACjB,eAAe,EACf,sBAAsB,GACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACN,oBAAoB,EACpB,KAAK,sBAAsB,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACN,KAAK,UAAU,EACf,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,KAAK,SAAS,GACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,GAAG,EAAE,KAAK,kBAAkB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,YAAY,EACX,IAAI,EACJ,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,UAAU,GACV,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,3 @@
1
- // src/directives/apply.ts
2
- import { MapperKind, mapSchema } from "@graphql-tools/utils";
3
- import { defaultFieldResolver } from "graphql";
4
-
5
1
  // src/context.ts
6
2
  import {
7
3
  systemClock
@@ -39,13 +35,13 @@ function memoized(access) {
39
35
  var checks = new WeakMap;
40
36
  var clocks = new WeakMap;
41
37
  function createJanusContext(request, options) {
42
- const { auth, access, type, clock } = options;
38
+ const { auth, access, type, clock, caller = "useJanus()" } = options;
43
39
  let answer;
44
40
  let asked = false;
45
41
  const authenticated = () => {
46
42
  if (!asked) {
47
43
  asked = true;
48
- answer = request === undefined ? Promise.reject(new TypeError("useJanus(): the GraphQL context has no request to authenticate — build the context from an HTTP request")) : auth.authenticate(request, type === undefined ? undefined : { type });
44
+ answer = request === undefined ? Promise.reject(new TypeError(`${caller}: the GraphQL context has no request to authenticate — build the context from an HTTP request, or accept a graphql-ws connection with janusConnection().onConnect`)) : auth.authenticate(request, type === undefined ? undefined : { type });
49
45
  }
50
46
  return answer;
51
47
  };
@@ -83,6 +79,83 @@ function janusOf(ctx, caller) {
83
79
  return janus;
84
80
  }
85
81
 
82
+ // src/options.ts
83
+ function checkWiring(caller, { auth, access, clock }) {
84
+ if (typeof auth?.authenticate !== "function") {
85
+ throw new TypeError(`${caller}: auth is not what janus() answered — pass { auth }`);
86
+ }
87
+ if (access !== undefined && typeof access?.can !== "function") {
88
+ throw new TypeError(`${caller}: access is not what permissions() answered — pass { auth, access }`);
89
+ }
90
+ if (clock !== undefined && typeof clock?.now !== "function") {
91
+ throw new TypeError(`${caller}: clock is not a Clock — pass the clock given to janus(), or leave it out for the system clock`);
92
+ }
93
+ }
94
+
95
+ // src/connection/credential.ts
96
+ var BEARER = /^\s*bearer(?:\s|$)/i;
97
+ function credentialOf(ctx, upgrade) {
98
+ const params = ctx.connectionParams;
99
+ if (params !== undefined && Object.hasOwn(params, "authorization")) {
100
+ const { authorization } = params;
101
+ if (typeof authorization !== "string")
102
+ return null;
103
+ if (BEARER.test(authorization))
104
+ return { authorization };
105
+ }
106
+ const request = upgrade === undefined ? ctx.extra?.request : upgrade(ctx);
107
+ return request ?? null;
108
+ }
109
+ var accepted = new WeakMap;
110
+ function accept(ctx, credential) {
111
+ const { extra } = ctx;
112
+ if (typeof extra !== "object" || extra === null) {
113
+ throw new TypeError("janusConnection(): the connection's extra is not an object — pass the context graphql-ws gave onConnect");
114
+ }
115
+ accepted.set(extra, credential);
116
+ }
117
+ function acceptedOf(context) {
118
+ const extra = context?.extra;
119
+ if (typeof extra !== "object" || extra === null)
120
+ return;
121
+ return accepted.get(extra);
122
+ }
123
+
124
+ // src/connection/connection.ts
125
+ function janusConnection(options) {
126
+ const { auth, access, type, clock } = options;
127
+ checkWiring("janusConnection()", { auth, access, clock });
128
+ if (options.upgrade !== undefined && typeof options.upgrade !== "function") {
129
+ throw new TypeError("janusConnection(): upgrade is not a function — pass (ctx) => the upgrade request, or leave it out to read ctx.extra.request");
130
+ }
131
+ const upgrade = options.upgrade?.bind(options);
132
+ const wiring = {
133
+ auth,
134
+ access,
135
+ type,
136
+ clock,
137
+ caller: "janusConnection().context"
138
+ };
139
+ return {
140
+ onConnect: async (ctx) => {
141
+ const credential = credentialOf(ctx, upgrade);
142
+ if (credential === null)
143
+ return false;
144
+ const current = await auth.authenticate(credential, type === undefined ? undefined : { type });
145
+ if (current === null)
146
+ return false;
147
+ accept(ctx, credential);
148
+ return true;
149
+ },
150
+ context: (ctx) => ({
151
+ janus: createJanusContext(acceptedOf(ctx), wiring)
152
+ })
153
+ };
154
+ }
155
+ // src/directives/apply.ts
156
+ import { MapperKind, mapSchema } from "@graphql-tools/utils";
157
+ import { defaultFieldResolver } from "graphql";
158
+
86
159
  // src/errors.ts
87
160
  import { JanusError, statusOf } from "@nxgt/janus";
88
161
  import { GraphQLError } from "graphql";
@@ -628,20 +701,12 @@ async function can(ctx, permission, object, ...options) {
628
701
  // src/plugin.ts
629
702
  function useJanus(options) {
630
703
  const { auth, access, type, clock, loaders, conditions } = options;
631
- if (typeof auth?.authenticate !== "function") {
632
- throw new TypeError("useJanus(): auth is not what janus() answered — pass { auth }");
633
- }
634
- if (access !== undefined && typeof access?.can !== "function") {
635
- throw new TypeError("useJanus(): access is not what permissions() answered — pass { auth, access }");
636
- }
637
- if (clock !== undefined && typeof clock?.now !== "function") {
638
- throw new TypeError("useJanus(): clock is not a Clock — pass the clock given to janus(), or leave it out for the system clock");
639
- }
704
+ checkWiring("useJanus()", { auth, access, clock });
640
705
  const applied = new WeakSet;
641
706
  return {
642
707
  onContextBuilding: ({ context, extendContext }) => {
643
708
  const { request } = context;
644
- const janus = createJanusContext(request, {
709
+ const janus = createJanusContext(request ?? acceptedOf(context), {
645
710
  auth,
646
711
  access,
647
712
  type,
@@ -714,6 +779,7 @@ export {
714
779
  applyJanusDirectives,
715
780
  can,
716
781
  denial,
782
+ janusConnection,
717
783
  janusGraphQLError,
718
784
  janusMaskError,
719
785
  janusTypeDefs,
@@ -722,5 +788,5 @@ export {
722
788
  useJanus
723
789
  };
724
790
 
725
- //# debugId=50FC1B1149A482D964756E2164756E21
791
+ //# debugId=C6BF0BFA8BF13A9C64756E2164756E21
726
792
  //# sourceMappingURL=index.js.map