@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/docs/roadmap.md CHANGED
@@ -9,9 +9,6 @@ Nothing between releases.
9
9
 
10
10
  ## Next
11
11
 
12
- - **Subscriptions over graphql-ws** — `janusConnection`, which authenticates
13
- a WebSocket connection once, from its `connectionParams` or its upgrade
14
- request, and gives every operation on it the same `ctx.janus`.
15
12
  - **The renewed session cookie on GraphQL responses** — `authenticate`
16
13
  renews a sliding session in passing; the response would then carry its
17
14
  `Set-Cookie`, so a browser keeps the new expiry without an HTTP route of
@@ -19,7 +16,12 @@ Nothing between releases.
19
16
 
20
17
  ## Later
21
18
 
22
- Nothing yet.
19
+ - **Anonymous graphql-ws connections** — `janusConnection({ anonymous:
20
+ true })`, accepting a connection with no credential so its public fields
21
+ answer and its guarded ones deny, as over HTTP.
22
+ - **Re-authenticating a subscription on each event** — an opt-in for
23
+ streams that must end the moment their session is revoked, at the cost
24
+ of a sessions-store read per event.
23
25
 
24
26
  ## Not planned
25
27
 
@@ -41,6 +43,18 @@ Nothing yet.
41
43
 
42
44
  Newest first; from the first release on, the package's CHANGELOG holds every one.
43
45
 
46
+ - **Subscriptions over graphql-ws, `janusConnection()`, v0.3.0.** The same
47
+ `@authenticated`, `@fresh` and `@permission` on subscriptions served over
48
+ a WebSocket with graphql-ws: `onConnect` for its `useServer()`
49
+ authenticates the connection from `connectionParams.authorization` — else
50
+ from the upgrade request's headers and the browser's session cookie —
51
+ and refuses it `4403`, or rejects on an outage, closed `4500`. Each
52
+ operation's `ctx.janus` is built by `useJanus()` from that credential, in
53
+ Yoga's recommended setup, and authenticated again when it subscribes, so
54
+ a session revoked since the connection opened is refused at the next
55
+ subscribe. `context` for a server without Yoga, `upgrade` for Bun.
56
+ `graphql-ws` is an optional peer, `^6.0.0`, whose floor runs in CI.
57
+
44
58
  - **`@fresh(maxAge)` and `requireFresh()`, v0.2.0.** A field, a type or an
45
59
  interface only a session that proved who it is less than `maxAge` seconds
46
60
  ago may resolve — signed in, or confirmed since by `auth.stepUp.confirm`:
@@ -30,6 +30,14 @@ for what causes each.
30
30
  - [`TypeError: useJanus(): auth is not what janus() answered`](#typeerror-usejanus-auth-is-not-what-janus-answered)
31
31
  - [`TypeError: useJanus(): access is not what permissions() answered`](#typeerror-usejanus-access-is-not-what-permissions-answered)
32
32
  - [`TypeError: useJanus(): clock is not a Clock`](#typeerror-usejanus-clock-is-not-a-clock)
33
+ - [`TypeError: janusConnection(): auth is not what janus() answered`, `access …`, `clock …`](#typeerror-janusconnection-auth-is-not-what-janus-answered-access--clock-)
34
+ - [`TypeError: janusConnection(): upgrade is not a function`](#typeerror-janusconnection-upgrade-is-not-a-function)
35
+
36
+ **Over graphql-ws**
37
+ - [The socket closed `4403: Forbidden` right after connecting](#the-socket-closed-4403-forbidden-right-after-connecting)
38
+ - [The socket closed `4500`](#the-socket-closed-4500)
39
+ - [A stream keeps its events after its session was revoked](#a-stream-keeps-its-events-after-its-session-was-revoked)
40
+ - [`TypeError: janusConnection(): the connection's extra is not an object`](#typeerror-janusconnection-the-connections-extra-is-not-an-object)
33
41
 
34
42
  **In a response**
35
43
  - [`UNAUTHENTICATED` for a signed-in user](#unauthenticated-for-a-signed-in-user)
@@ -39,7 +47,7 @@ for what causes each.
39
47
  - [`SERVICE_UNAVAILABLE`, 503, on every guarded field](#service_unavailable-503-on-every-guarded-field)
40
48
  - [`Unexpected error.`, 500, where a `JanusError` was thrown](#unexpected-error-500-where-a-januserror-was-thrown)
41
49
  - [`TypeError: … ctx.janus is not set`](#typeerror--ctxjanus-is-not-set)
42
- - [`TypeError: useJanus(): the GraphQL context has no request to authenticate`](#typeerror-usejanus-the-graphql-context-has-no-request-to-authenticate)
50
+ - [`TypeError: …: the GraphQL context has no request to authenticate`](#typeerror--the-graphql-context-has-no-request-to-authenticate)
43
51
  - [`TypeError: can(): ctx.janus.access is not set`](#typeerror-can-ctxjanusaccess-is-not-set)
44
52
  - [`TypeError: requireUser(): type is an empty list, which no user could pass`](#typeerror-requireuser-type-is-an-empty-list-which-no-user-could-pass)
45
53
  - [`TypeError: requireFresh(): maxAge is a duration with its unit`](#typeerror-requirefresh-maxage-is-a-duration-with-its-unit)
@@ -278,6 +286,74 @@ answered — or leave `access` out when no field uses `@permission` or `can()`.
278
286
  `@nxgt/janus` in a spec — or leave `clock` out for the system's:
279
287
  `useJanus({ auth, clock })`.
280
288
 
289
+ ### `TypeError: janusConnection(): auth is not what janus() answered`, `access …`, `clock …`
290
+
291
+ **Why:** the same checks as `useJanus()`'s, named for `janusConnection()`:
292
+ `auth` is not what `janus()` answered, `access` not what `permissions()`
293
+ answered, `clock` not a `Clock`.
294
+
295
+ **Fix:** `janusConnection({ auth })`, with the `access` and `clock` given to
296
+ `useJanus()` when you pass them at all.
297
+
298
+ ### `TypeError: janusConnection(): upgrade is not a function`
299
+
300
+ **Why:** `upgrade` is how `onConnect` finds the upgrade request on a
301
+ transport whose `extra` does not carry it, and it was given something else.
302
+
303
+ **Fix:** a function of graphql-ws's context, or nothing — `ctx.extra.request`
304
+ is then read, as `graphql-ws/use/ws` sets it:
305
+
306
+ ```ts
307
+ janusConnection({ auth, upgrade: (ctx: { readonly extra: { readonly socket: Upgraded } }) => ctx.extra.socket.data.request });
308
+ ```
309
+
310
+ ## Over graphql-ws
311
+
312
+ ### The socket closed `4403: Forbidden` right after connecting
313
+
314
+ **Why:** `onConnect` found no credential that authenticates: none at all,
315
+ an unknown or lapsed token, a user gone or inactive, a user of another
316
+ type than `janusConnection({ type })`, or a `connectionParams.authorization`
317
+ that is not a string. A `Bearer` `connectionParams.authorization` is
318
+ read before the upgrade request, and **the first present wins**: a lapsed
319
+ token there refuses the connection even beside a live cookie. One of
320
+ another scheme (`Basic …`) does not count, and the cookie is read.
321
+
322
+ **Fix:** send `connectionParams: { authorization: 'Bearer <token>' }` —
323
+ the key lower-case — or connect from a browser holding the session cookie,
324
+ to the origin that set it. On Bun, pass `upgrade`, or the cookie is never
325
+ read. graphql-ws's client retries a `4403`; a `connectionParams` function
326
+ is asked again each time.
327
+
328
+ ### The socket closed `4500`
329
+
330
+ **Why:** the sessions store could not answer while `onConnect`
331
+ authenticated — `STORE_FAILED`. `graphql-ws/use/ws` closes the socket
332
+ `4500` and logs the error with `console.error`. It is an outage, **never a
333
+ refusal**: `4403` would tell the client its session is bad.
334
+
335
+ **Fix:** the store. graphql-ws's client does not retry a `4500` by default;
336
+ set `shouldRetry` to retry an outage.
337
+
338
+ ### A stream keeps its events after its session was revoked
339
+
340
+ **Why:** an operation is authenticated once, when it subscribes, and
341
+ each event's `@authenticated` reads that answer. The next operation on
342
+ the connection is refused `UNAUTHENTICATED`, and a reconnection `4403`.
343
+
344
+ **Fix:** close the session's sockets when you revoke it
345
+ ([the subscriptions guide](guide/subscriptions.md#a-session-revoked-while-connected)).
346
+ A permission revoked needs nothing: `@permission` asks again on every event.
347
+
348
+ ### `TypeError: janusConnection(): the connection's extra is not an object`
349
+
350
+ **Why:** `onConnect` was called with a context whose `extra` is not an
351
+ object — by hand, or by a transport of your own. It remembers the
352
+ connection by that object.
353
+
354
+ **Fix:** pass `onConnect` to graphql-ws's `useServer()` or `makeHandler()`,
355
+ which give it the context they made.
356
+
281
357
  ## In a response
282
358
 
283
359
  ### `UNAUTHENTICATED` for a signed-in user
@@ -353,14 +429,21 @@ transformed with `applyJanusDirectives()` and served by something else.
353
429
 
354
430
  **Fix:** `plugins: [useJanus({ auth })]`.
355
431
 
356
- ### `TypeError: useJanus(): the GraphQL context has no request to authenticate`
357
-
358
- **Why:** the context was built without `request` — a transport that is not
359
- HTTP, such as a WebSocket subscription.
360
-
361
- **Fix:** for now, authenticate subscriptions over HTTP (Yoga's server-sent
362
- events carry the request). A connection-level wiring for graphql-ws is on
363
- the [roadmap](roadmap.md).
432
+ ### `TypeError: …: the GraphQL context has no request to authenticate`
433
+
434
+ **Why:** the context was built without `request`, and for no connection
435
+ `janusConnection().onConnect` accepted: a transport that is not HTTP; a
436
+ graphql-ws server without `onConnect`; or Yoga's recipe with the context
437
+ built from something other than `{ ...ctx, … }`, which drops `ctx.extra`,
438
+ where `useJanus()` finds the connection. The message opens with
439
+ `useJanus():` under Yoga, and with `janusConnection().context:` without
440
+ it — the `context` of a server whose `onConnect` is not
441
+ `janusConnection()`'s.
442
+
443
+ **Fix:** over graphql-ws, pass `onConnect: janusConnection({ auth }).onConnect`
444
+ to `useServer()`, and keep the `...ctx` spread in `yoga.getEnveloped()`
445
+ ([the subscriptions guide](guide/subscriptions.md)). Without Yoga, pass
446
+ `context: connection.context` too.
364
447
 
365
448
  ### `TypeError: can(): ctx.janus.access is not set`
366
449
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/janus-graphql",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "GraphQL for @nxgt/janus: an envelop plugin with the signed-in user on the context, fields guarded by @authenticated, @fresh and @permission, and every error with the status it deserves",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -57,8 +57,11 @@
57
57
  "@graphql-tools/utils": "^12.0.1",
58
58
  "@nxgt/janus": "^0.12.0",
59
59
  "@types/bun": "^1.4.2",
60
+ "@types/ws": "^8.18.1",
60
61
  "graphql": "^17.0.2",
62
+ "graphql-ws": "^6.3.0",
61
63
  "graphql-yoga": "^5.24.1",
64
+ "ws": "^8.22.0",
62
65
  "zod": "^4.6.5"
63
66
  },
64
67
  "peerDependencies": {
@@ -66,6 +69,12 @@
66
69
  "@graphql-tools/utils": ">=10.0.0 <13",
67
70
  "@nxgt/janus": "^0.12.0",
68
71
  "graphql": "^16.9.0 || ^17.0.0",
72
+ "graphql-ws": "^6.0.0",
69
73
  "typescript": "^6.0.3"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "graphql-ws": {
77
+ "optional": true
78
+ }
70
79
  }
71
80
  }