@nxgt/janus-graphql 0.5.1 → 0.5.3
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 +7 -0
- package/docs/README.md +1 -1
- package/docs/guide/context.md +73 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -257,6 +257,13 @@ createYoga({
|
|
|
257
257
|
browser keeps the cookie with its old expiry until the next renewal over
|
|
258
258
|
HTTP, a `renewAfter` later: a browser whose only traffic is the WebSocket
|
|
259
259
|
should renew through an HTTP route of yours.
|
|
260
|
+
- **A sign-in mutation passes the device itself.** `@nxgt/janus-graphql`
|
|
261
|
+
signs nobody in, and ships no device helper: with `janus({ devices })`,
|
|
262
|
+
read the device cookie from `ctx.request` — `useCookies()`'s
|
|
263
|
+
`cookieStore`, or the `Cookie` header — call
|
|
264
|
+
`auth.signIn(input, { device: token ?? null })`, and set `deviceToken`
|
|
265
|
+
back as a long-lived `HttpOnly` cookie. See
|
|
266
|
+
[the context](docs/guide/context.md#a-sign-in-mutation-and-the-device-cookie).
|
|
260
267
|
- **An `auth` of your own needs `cookie` for the renewal.** A wrapper that
|
|
261
268
|
passes on only `authenticate` and `types` sends no renewed cookie; pass
|
|
262
269
|
`cookie: auth.cookie` along.
|
package/docs/README.md
CHANGED
|
@@ -8,7 +8,7 @@ give the detail.
|
|
|
8
8
|
| [Directives](guide/directives.md) | You are guarding fields, types or interfaces with `@authenticated`, `@fresh` or `@permission`: where the id is read, loaders and conditions, lists, the order, the denials, one check per question, and what is refused at start-up |
|
|
9
9
|
| [The step-up over GraphQL](guide/step-up.md) | A mutation must run only on a session that proved who it is recently: `@fresh`, `STEP_UP_REQUIRED`, `stepUp.request` and `confirm` as mutations, `requireFresh()`, subscriptions, and testing with a clock |
|
|
10
10
|
| [Subscriptions over graphql-ws](guide/subscriptions.md) | You serve subscriptions over a WebSocket: `janusConnection()`, the cookie or `connectionParams`, the close codes, a session revoked mid-stream, Bun, and a server without Yoga |
|
|
11
|
-
| [The context](guide/context.md) | You are reading the user in a resolver, narrowing it to a user type, checking a permission or freshness yourself with `requireUser()`, `requireFresh()` and `can()`,
|
|
11
|
+
| [The context](guide/context.md) | You are reading the user in a resolver, narrowing it to a user type, checking a permission or freshness yourself with `requireUser()`, `requireFresh()` and `can()`, wondering when the renewed session cookie is sent, or passing the device cookie from a sign-in mutation |
|
|
12
12
|
| [Errors](guide/errors.md) | You want to know what a client receives for a denial or an outage, what the status of the response is, how `janusMaskError()` fits Yoga's masking, and how `report` logs every outage |
|
|
13
13
|
| [Telemetry](guide/telemetry.md) | You want a span per authentication and per permission check, with `@nxgt/janus-telemetry` |
|
|
14
14
|
| [Federation](guide/federation.md) | The server is a subgraph: where the directives are checked, what the router must forward, and `@composeDirective` |
|
package/docs/guide/context.md
CHANGED
|
@@ -123,6 +123,79 @@ type StaffContext = YogaInitialContext & JanusContext<typeof auth, typeof access
|
|
|
123
123
|
Without `access`, write `JanusContext<typeof auth>`: `ctx.janus.access` is
|
|
124
124
|
then absent from the type, as it is from the context.
|
|
125
125
|
|
|
126
|
+
## A sign-in mutation and the device cookie
|
|
127
|
+
|
|
128
|
+
`@nxgt/janus-graphql` runs no sign-in: a mutation of yours calls
|
|
129
|
+
`auth.signIn`. With `janus({ devices })`, that call takes the **device
|
|
130
|
+
token** the browser holds, as `device`, and answers a `deviceToken` to keep
|
|
131
|
+
in a long-lived cookie of its own — so a sign-in from a device the user had
|
|
132
|
+
not signed in from is told apart
|
|
133
|
+
([`@nxgt/janus`'s devices guide](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/devices.md)).
|
|
134
|
+
No helper is shipped for it: read the cookie from `ctx.request`, and set it
|
|
135
|
+
back on the response.
|
|
136
|
+
|
|
137
|
+
With Yoga, [`useCookies()`](https://the-guild.dev/graphql/yoga-server/docs/features/cookies)
|
|
138
|
+
from `@whatwg-node/server-plugin-cookies` — a separate install, not a peer
|
|
139
|
+
of this package: `bun add @whatwg-node/server-plugin-cookies` — gives the
|
|
140
|
+
request a `cookieStore` to read and write through:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { janusMaskError, janusTypeDefs, useJanus } from '@nxgt/janus-graphql';
|
|
144
|
+
import { useCookies } from '@whatwg-node/server-plugin-cookies';
|
|
145
|
+
import { createSchema, createYoga } from 'graphql-yoga';
|
|
146
|
+
|
|
147
|
+
const DEVICE = 'janus-device';
|
|
148
|
+
const FOUR_HUNDRED_DAYS = 400 * 24 * 60 * 60 * 1000; // the longest a browser keeps a cookie
|
|
149
|
+
|
|
150
|
+
const resolvers = {
|
|
151
|
+
Mutation: {
|
|
152
|
+
signIn: async (_: unknown, args: { email: string; password: string }, ctx: Context) => {
|
|
153
|
+
const token = (await ctx.request.cookieStore?.get(DEVICE))?.value;
|
|
154
|
+
const result = await auth.signIn(args, { device: token ?? null });
|
|
155
|
+
if (result.status === 'secondFactor') return { challenge: result.challenge };
|
|
156
|
+
// …set the session cookie from result.token, as your sign-in already does
|
|
157
|
+
if (result.deviceToken !== null) {
|
|
158
|
+
await ctx.request.cookieStore?.set({
|
|
159
|
+
name: DEVICE,
|
|
160
|
+
value: result.deviceToken,
|
|
161
|
+
expires: Date.now() + FOUR_HUNDRED_DAYS,
|
|
162
|
+
httpOnly: true,
|
|
163
|
+
secure: true,
|
|
164
|
+
sameSite: 'lax',
|
|
165
|
+
path: '/',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return { userId: result.user.id, newDevice: result.newDevice };
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const yoga = createYoga({
|
|
174
|
+
schema: createSchema({ typeDefs: [janusTypeDefs, typeDefs], resolvers }),
|
|
175
|
+
plugins: [useCookies(), useJanus({ auth, access })],
|
|
176
|
+
maskedErrors: { maskError: janusMaskError() },
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Without the plugin, read the device token from the `Cookie` header —
|
|
181
|
+
`ctx.request.headers.get('cookie')` — and set the cookie with a plugin of
|
|
182
|
+
your own that appends a `Set-Cookie` in Yoga's `onResponse`.
|
|
183
|
+
|
|
184
|
+
- **`token ?? null`, never `token` alone.** A missing cookie is
|
|
185
|
+
`undefined`, and `undefined` means *not tracked*: that browser would never
|
|
186
|
+
be given a token, nor reported.
|
|
187
|
+
- **Give the device again to the second factor's mutation.** With an active
|
|
188
|
+
second factor, `signIn` answers a challenge that carries no device:
|
|
189
|
+
`auth.secondFactor.confirm(challenge, code, { device: token ?? null })`, and
|
|
190
|
+
the same for `recover`, `signInCode.confirm` and `magicLink.confirm`.
|
|
191
|
+
- **A cookie of its own, apart from the session's**, `HttpOnly` and
|
|
192
|
+
`Secure`, and left in place by a sign-out: the device stays known. It
|
|
193
|
+
holds one user's token — see
|
|
194
|
+
[devices](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/devices.md#keeping-the-token-the-cookie).
|
|
195
|
+
|
|
196
|
+
A bearer client has no cookie jar: answer it the `deviceToken` in the
|
|
197
|
+
mutation's result, and take it back as an argument of the next sign-in.
|
|
198
|
+
|
|
126
199
|
## Directives
|
|
127
200
|
|
|
128
201
|
`@authenticated`, `@fresh` and `@permission` guard a field, a type or an interface
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/janus-graphql",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
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",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@envelop/core": "^5.6.1",
|
|
57
57
|
"@graphql-tools/utils": "^12.0.1",
|
|
58
|
-
"@nxgt/janus": "^0.
|
|
58
|
+
"@nxgt/janus": "^0.17.0",
|
|
59
59
|
"@types/bun": "^1.4.2",
|
|
60
60
|
"@types/ws": "^8.18.1",
|
|
61
61
|
"graphql": "^17.0.2",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"@envelop/core": "^5.0.0",
|
|
69
69
|
"@graphql-tools/utils": ">=10.0.0 <13",
|
|
70
|
-
"@nxgt/janus": "^0.
|
|
70
|
+
"@nxgt/janus": "^0.17.0",
|
|
71
71
|
"graphql": "^16.9.0 || ^17.0.0",
|
|
72
72
|
"graphql-ws": "^6.0.0",
|
|
73
73
|
"typescript": "^6.0.3"
|