@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/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
|
-
|
|
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`:
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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:
|
|
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:
|
|
357
|
-
|
|
358
|
-
**Why:** the context was built without `request
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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.
|
|
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
|
}
|