@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 +75 -15
- package/dist/connection/connection.d.ts +36 -0
- package/dist/connection/connection.d.ts.map +1 -0
- package/dist/connection/credential.d.ts +34 -0
- package/dist/connection/credential.d.ts.map +1 -0
- package/dist/connection/types.d.ts +57 -0
- package/dist/connection/types.d.ts.map +1 -0
- package/dist/context.d.ts +4 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +83 -17
- package/dist/index.js.map +9 -6
- package/dist/options.d.ts +17 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.d.ts.map +1 -1
- package/docs/README.md +2 -1
- package/docs/guide/context.md +4 -1
- package/docs/guide/step-up.md +7 -3
- package/docs/guide/subscriptions.md +251 -0
- package/docs/roadmap.md +18 -4
- package/docs/troubleshooting.md +92 -9
- package/package.json +10 -1
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`,
|
|
7
|
-
|
|
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
|
|
67
|
-
as well, together, with a single copy of `graphql`,
|
|
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,
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
*/
|
package/dist/context.d.ts.map
CHANGED
|
@@ -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;
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
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(
|
|
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
|
-
|
|
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=
|
|
791
|
+
//# debugId=C6BF0BFA8BF13A9C64756E2164756E21
|
|
726
792
|
//# sourceMappingURL=index.js.map
|