@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 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()`, or wondering when the renewed session cookie is sent |
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` |
@@ -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.1",
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.15.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.15.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"