@nxgt/janus-graphql 0.1.1 → 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 +110 -21
- 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 +11 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/directives/apply.d.ts +10 -9
- package/dist/directives/apply.d.ts.map +1 -1
- package/dist/directives/guard.d.ts +2 -1
- package/dist/directives/guard.d.ts.map +1 -1
- package/dist/directives/read.d.ts +9 -1
- package/dist/directives/read.d.ts.map +1 -1
- package/dist/directives/validate.d.ts +7 -2
- package/dist/directives/validate.d.ts.map +1 -1
- package/dist/fresh.d.ts +37 -0
- package/dist/fresh.d.ts.map +1 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +170 -23
- package/dist/index.js.map +14 -10
- package/dist/options.d.ts +17 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/plugin.d.ts +8 -3
- package/dist/plugin.d.ts.map +1 -1
- package/dist/sdl.d.ts.map +1 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +5 -3
- package/docs/guide/context.md +36 -3
- package/docs/guide/directives.md +64 -5
- package/docs/guide/errors.md +2 -1
- package/docs/guide/federation.md +2 -1
- package/docs/guide/step-up.md +186 -0
- package/docs/guide/subscriptions.md +251 -0
- package/docs/roadmap.md +29 -9
- package/docs/troubleshooting.md +218 -11
- package/graphql/janus.graphqls +8 -0
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
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
|
-
asks, fields and types guarded by `@authenticated
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
asks, fields and types guarded by `@authenticated`, `@fresh` and
|
|
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.
|
|
8
9
|
|
|
9
10
|
An [envelop](https://the-guild.dev/graphql/envelop) plugin, so it runs in
|
|
10
11
|
[GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) and any server built
|
|
@@ -51,7 +52,7 @@ bun add @nxgt/janus-graphql @nxgt/janus graphql @graphql-tools/utils @envelop/co
|
|
|
51
52
|
bun add -d typescript # 6
|
|
52
53
|
```
|
|
53
54
|
|
|
54
|
-
Every peer is required
|
|
55
|
+
Every peer is required but `graphql-ws`:
|
|
55
56
|
|
|
56
57
|
| Peer | Range | Why |
|
|
57
58
|
| --- | --- | --- |
|
|
@@ -60,11 +61,12 @@ Every peer is required:
|
|
|
60
61
|
| `@graphql-tools/utils` | `>=10.0.0 <13` | `mapSchema` and `getDirective`, which apply the directives |
|
|
61
62
|
| `@envelop/core` | `^5.0.0` | Types only — the `Plugin` `useJanus()` answers. Yoga already brings it |
|
|
62
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 |
|
|
63
65
|
|
|
64
66
|
The floors are tested, not claimed: the package's specs and its typecheck run
|
|
65
|
-
on `graphql` 16.9.0, `@graphql-tools/utils` 10.0.0
|
|
66
|
-
as well, together, with a single copy of `graphql`,
|
|
67
|
-
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.
|
|
68
70
|
|
|
69
71
|
Like `@nxgt/janus`, it expects `"moduleResolution": "bundler"`: the
|
|
70
72
|
declarations import without extensions, so `nodenext` is not supported.
|
|
@@ -77,22 +79,25 @@ that reads files — `node_modules/@nxgt/janus-graphql/graphql/janus.graphqls`.
|
|
|
77
79
|
|
|
78
80
|
| Export | What it is |
|
|
79
81
|
| --- | --- |
|
|
80
|
-
| `useJanus({ auth, access?, type?, loaders?, conditions? })` | The envelop plugin. Adds `ctx.janus` to every request's context, and applies the directives to every schema the server is given, once. `type` treats a user of any other type as anonymous. `loaders` and `conditions` are what `@permission` needs beside `access` |
|
|
82
|
+
| `useJanus({ auth, access?, type?, clock?, loaders?, conditions? })` | The envelop plugin. Adds `ctx.janus` to every request's context, and applies the directives to every schema the server is given, once. `type` treats a user of any other type as anonymous. `clock` is what `@fresh` and `requireFresh()` read — the one given to `janus()`, when it is not the system's. `loaders` and `conditions` are what `@permission` needs beside `access` |
|
|
81
83
|
| `ctx.janus.user()`, `ctx.janus.session()` | Who the request belongs to, and the session it presented — `null` for an anonymous request. `auth.authenticate(request)` runs the first time either is asked, once per request, and never when neither is |
|
|
82
84
|
| `ctx.janus.access` | The `permissions()` instance given to `useJanus()`. Absent from the type — and from the context — without one |
|
|
83
|
-
| `janusTypeDefs` | The SDL: `@authenticated`, `@permission` and the `JanusPermissionDenial` enum — prefixed, so it never collides with a type of your schema. The same text as `graphql/janus.graphqls` |
|
|
85
|
+
| `janusTypeDefs` | The SDL: `@authenticated`, `@fresh`, `@permission` and the `JanusPermissionDenial` enum — prefixed, so it never collides with a type of your schema. The same text as `graphql/janus.graphqls` |
|
|
84
86
|
| `@authenticated(type: [String!])` | On a field, a type or an interface. A signed-in user, of one of the `type`s when it names some. Anonymous: `UNAUTHENTICATED`. Another type: `FORBIDDEN`. Every one that applies — the field's, its type's, its interfaces' — must hold |
|
|
87
|
+
| `@fresh(maxAge: Int!)` | On a field, a type or an interface. A session that proved who it is less than `maxAge` **seconds** ago — signed in, or confirmed since by `auth.stepUp.confirm`. Anonymous: `UNAUTHENTICATED`. Older: `STEP_UP_REQUIRED`, 403. Checked after `@authenticated`, before `@permission`; the smallest `maxAge` that applies holds; on a subscription field, checked when it subscribes |
|
|
85
88
|
| `@permission(name, type, id, onDeny)` | On a field, a type or an interface. A user holding permission `name` on the object of `type` whose id `id` reads — `args.<path>` or `parent.<path>`; `args.id` on a field, `parent.id` on a type. A list requires it on every id. Repeated, every one must hold, in order. Denied: `NOT_FOUND`, or `FORBIDDEN` with `onDeny: FORBIDDEN` |
|
|
86
89
|
| `loaders: { [type]: (id, ctx) => object \| null }` | The object `@permission` checks for an id alone — from `args`, or a parent field other than `id` — required for a type with a `fromField`, whose fields `access.can` reads. `null` answers `NOT_FOUND` |
|
|
87
90
|
| `conditions: { [type]: (object, ctx) => ctx }` | The `ctx` `@permission` passes to `access.can` for a permission that reaches a `when()` |
|
|
88
91
|
| `applyJanusDirectives(schema, { auth, type?, access?, loaders?, conditions? })` | The schema transform alone — what `useJanus()` runs — to check a schema in a test or a build script. Throws a `TypeError` naming the field for a directive no request could pass. Its guards read `ctx.janus`, which only `useJanus()` builds |
|
|
89
92
|
| `requireUser(ctx, { type? })` | The signed-in user, narrowed to `type` — one or a non-empty list — or a denial: `UNAUTHENTICATED`, `FORBIDDEN` |
|
|
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` |
|
|
90
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 |
|
|
91
96
|
| `janusMaskError(fallback?)` | Yoga's `maskedErrors.maskError`: a `JanusError` a resolver let through answered with its code and status; anything else to `fallback` |
|
|
92
97
|
| `janusGraphQLError(error)` | A `JanusError` as the `GraphQLError` the client reads: its code, its status, and only what the client can act on |
|
|
93
|
-
| `denial(code, message?)` | A denial of your own: `UNAUTHENTICATED` 401, `FORBIDDEN` 403, `NOT_FOUND` 404 |
|
|
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 |
|
|
94
99
|
| `JanusContext<typeof auth, typeof access?, Type?>` | What `useJanus()` adds to the context, for your resolvers' `ctx` |
|
|
95
|
-
| `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 |
|
|
96
101
|
|
|
97
102
|
## Directives
|
|
98
103
|
|
|
@@ -108,6 +113,10 @@ type Record @permission(name: "view", type: "record") { # parent.id:
|
|
|
108
113
|
title: String
|
|
109
114
|
billing: Billing @permission(name: "manage", type: "record", onDeny: FORBIDDEN)
|
|
110
115
|
}
|
|
116
|
+
|
|
117
|
+
type Mutation {
|
|
118
|
+
changeEmail(email: String!): User @fresh(maxAge: 600) # signed in or stepped up < 10 min ago
|
|
119
|
+
}
|
|
111
120
|
```
|
|
112
121
|
|
|
113
122
|
```ts
|
|
@@ -120,6 +129,12 @@ useJanus({
|
|
|
120
129
|
```
|
|
121
130
|
|
|
122
131
|
Each runs before the resolver, which a refused request never reaches.
|
|
132
|
+
`@fresh(maxAge)` takes **seconds**, and answers an older session
|
|
133
|
+
`STEP_UP_REQUIRED`: the client then calls your two step-up mutations,
|
|
134
|
+
which run `auth.stepUp.request` and `confirm` — shown in
|
|
135
|
+
[the step-up guide](docs/guide/step-up.md) — and sends the request again.
|
|
136
|
+
Every `@authenticated` is checked first, then `@fresh`, then each
|
|
137
|
+
`@permission`, so a stale session asks no permission check.
|
|
123
138
|
`@permission` implies a signed-in user, and asks `access.can` with the parent
|
|
124
139
|
itself when the id is `parent.id`, with what `loaders[type]` answers for an
|
|
125
140
|
id of a type with a `fromField`, and with `{ type, id }` otherwise. Every
|
|
@@ -129,8 +144,48 @@ a field's `FORBIDDEN` could tell the object exists. The same question asked
|
|
|
129
144
|
twice in a request, by two fields or by a directive and `can()`, is one
|
|
130
145
|
check. What no request could pass — an object type or a permission the
|
|
131
146
|
model does not declare, a malformed `id:`, an argument the field does not
|
|
132
|
-
take, a missing loader or condition
|
|
133
|
-
the field. Detail: [the directives guide](docs/guide/directives.md).
|
|
147
|
+
take, a missing loader or condition, a `maxAge` not above zero — is a
|
|
148
|
+
`TypeError` at start-up naming the field. Detail: [the directives guide](docs/guide/directives.md).
|
|
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).
|
|
134
189
|
|
|
135
190
|
## Errors
|
|
136
191
|
|
|
@@ -143,8 +198,9 @@ every one carries a `code` and the HTTP status Yoga answers with:
|
|
|
143
198
|
|
|
144
199
|
| Code | Status | When |
|
|
145
200
|
| --- | --- | --- |
|
|
146
|
-
| `UNAUTHENTICATED` | 401 | An anonymous request reached a guarded field, or `
|
|
201
|
+
| `UNAUTHENTICATED` | 401 | An anonymous request reached a guarded field, `requireUser()` or `requireFresh()` |
|
|
147
202
|
| `FORBIDDEN` | 403 | A user of a type the directive or `requireUser()` does not name; a `@permission(onDeny: FORBIDDEN)` denied |
|
|
203
|
+
| `STEP_UP_REQUIRED` | 403 | A session older than `@fresh(maxAge)` or `requireFresh()` allows: ask for a step-up, then send the request again |
|
|
148
204
|
| `NOT_FOUND` | 404 | A `@permission` denied — its default — or a loader answered `null`; `denial('NOT_FOUND')` |
|
|
149
205
|
| `SERVICE_UNAVAILABLE` | 503 | A store could not answer — `STORE_FAILED`. **Never a denial** |
|
|
150
206
|
| any other `JanusErrorCode` | its [`statusOf`](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/errors.md) | A `JanusError` a resolver let through: `CREDENTIALS_INVALID` 401, `LOGIN_TAKEN` 409, … |
|
|
@@ -204,21 +260,44 @@ response with several errors: [the errors guide](docs/guide/errors.md).
|
|
|
204
260
|
on every one, and one denial denies the field; to answer only the items a
|
|
205
261
|
user may see, ask `access.list()` for their ids.
|
|
206
262
|
- **A context built without a request cannot authenticate.** `ctx.janus.user()`
|
|
207
|
-
rejects with a `TypeError` — a transport that is not HTTP,
|
|
208
|
-
|
|
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.
|
|
276
|
+
- **`@fresh` reads seconds; `requireFresh()` a duration.** `@fresh(maxAge:
|
|
277
|
+
10)` is ten seconds, not ten minutes — write `600`. `requireFresh(ctx,
|
|
278
|
+
'10m')` takes the unit, and refuses a bare number, which `@nxgt/janus`
|
|
279
|
+
would read as milliseconds.
|
|
280
|
+
- **`useJanus()` reads the system clock unless given `janus()`'s.** With
|
|
281
|
+
`janus({ clock: fixedClock(…) })` in a spec and no `useJanus({ clock })`,
|
|
282
|
+
every session looks as old as the fixed date, and every `@fresh` field
|
|
283
|
+
answers `STEP_UP_REQUIRED`. Pass the same clock to both.
|
|
284
|
+
- **A subscription field's `@fresh` is checked when it subscribes.** Its
|
|
285
|
+
events keep coming past `maxAge`; that `@fresh` is not asked again on
|
|
286
|
+
each one, as `@permission` is. A `@fresh` on the payload's type or its
|
|
287
|
+
fields is asked on every event, like any field.
|
|
209
288
|
- **A denial of a list field's item fails the whole list** where the item is
|
|
210
289
|
non-null (`[Doctor!]!`), as GraphQL's null propagation always does. Guard
|
|
211
290
|
the list field, or make the item nullable.
|
|
212
291
|
|
|
213
292
|
## Documentation
|
|
214
293
|
|
|
215
|
-
- [Guides](docs/README.md) — the directives, 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
|
|
216
295
|
- [Troubleshooting](docs/troubleshooting.md) — by the message or status you see
|
|
217
296
|
- [Roadmap](docs/roadmap.md) — what is next, and what is not planned
|
|
218
297
|
|
|
219
298
|
## Type safety, counted
|
|
220
299
|
|
|
221
|
-
|
|
300
|
+
Thirty-five plausible mistakes are refused by the compiler, each with a
|
|
222
301
|
`@ts-expect-error` case in `test/types/`:
|
|
223
302
|
|
|
224
303
|
- five in `context.ts`: reading the user or the session where either may be
|
|
@@ -239,9 +318,19 @@ Twenty-four plausible mistakes are refused by the compiler, each with a
|
|
|
239
318
|
does not declare, a loader answering an object without a field a
|
|
240
319
|
`fromField` reads, `conditions` for a type no `when()` is reached on, a
|
|
241
320
|
condition answering a `ctx` of the wrong shape, and `loaders` without
|
|
242
|
-
`access
|
|
243
|
-
|
|
244
|
-
|
|
321
|
+
`access`;
|
|
322
|
+
- five in `fresh.ts`: `requireFresh()` given a bare number, a duration
|
|
323
|
+
written as prose, no `maxAge` at all, or the request instead of the
|
|
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.
|
|
245
334
|
|
|
246
335
|
## Licence
|
|
247
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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* authentication, and the per-request check `can()` and the directives ask
|
|
4
4
|
* through.
|
|
5
5
|
*/
|
|
6
|
-
import type
|
|
6
|
+
import { type Authenticated, type Clock, type RequestLike } from '@nxgt/janus';
|
|
7
7
|
import type { JanusOnContext } from './types';
|
|
8
8
|
/** One check, as `access.can` answers it — loose: the typing is the caller's. */
|
|
9
9
|
export type Check = (subject: {
|
|
@@ -33,17 +33,26 @@ export interface ContextOptions {
|
|
|
33
33
|
readonly can: unknown;
|
|
34
34
|
} | undefined;
|
|
35
35
|
readonly type?: string | undefined;
|
|
36
|
+
readonly clock?: Clock | undefined;
|
|
37
|
+
/** The call the consumer wrote, named by the no-request `TypeError`. */
|
|
38
|
+
readonly caller?: string;
|
|
36
39
|
}
|
|
37
40
|
/**
|
|
38
41
|
* `ctx.janus` for one request. `authenticate` runs the first time `user()`
|
|
39
42
|
* or `session()` is asked, once, and its answer — or its failure — is the
|
|
40
43
|
* answer of both from then on.
|
|
41
44
|
*
|
|
42
|
-
* 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
|
|
43
47
|
* `TypeError` from `user()`, not `null`: anonymous is an answer, and nothing
|
|
44
48
|
* here asked.
|
|
45
49
|
*/
|
|
46
50
|
export declare function createJanusContext(request: RequestLike | undefined, options: ContextOptions): Built;
|
|
51
|
+
/**
|
|
52
|
+
* The clock `@fresh` and `requireFresh()` read the time from: the one given
|
|
53
|
+
* to `useJanus({ clock })`, or the system's.
|
|
54
|
+
*/
|
|
55
|
+
export declare function clockOf(janus: object): Clock;
|
|
47
56
|
/**
|
|
48
57
|
* The check `ctx.janus` answers through: the one its context was built
|
|
49
58
|
* with — `access.can`, memoized for the request (`memo.ts`) — or
|
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,KAAK,
|
|
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"}
|
|
@@ -24,11 +24,12 @@ export interface JanusDirectivesOptions<T extends string = string> {
|
|
|
24
24
|
readonly conditions?: Entries | undefined;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* The schema with every field guarded by `@authenticated` or
|
|
28
|
-
* — on the field, on its type, or on an interface the type
|
|
29
|
-
* wrapped so its resolver runs only for a signed-in user, of
|
|
30
|
-
* types `type:` names when it names some,
|
|
31
|
-
*
|
|
27
|
+
* The schema with every field guarded by `@authenticated`, `@fresh` or
|
|
28
|
+
* `@permission` — on the field, on its type, or on an interface the type
|
|
29
|
+
* implements — wrapped so its resolver runs only for a signed-in user, of
|
|
30
|
+
* one of the types `type:` names when it names some, on a session that
|
|
31
|
+
* proved who it is less than `maxAge` seconds ago, holding every permission
|
|
32
|
+
* asked. Every directive that applies must hold.
|
|
32
33
|
*
|
|
33
34
|
* `useJanus()` calls it on every schema it is given. Call it yourself to
|
|
34
35
|
* check a schema without a server: the guards it installs read `ctx.janus`,
|
|
@@ -36,10 +37,10 @@ export interface JanusDirectivesOptions<T extends string = string> {
|
|
|
36
37
|
*
|
|
37
38
|
* **Refuses when the schema is built**, with a `TypeError` naming the
|
|
38
39
|
* field: a user type `auth` does not know, a `type: []`, restrictions no
|
|
39
|
-
* user type meets together
|
|
40
|
-
* permission
|
|
41
|
-
*
|
|
42
|
-
* not given.
|
|
40
|
+
* user type meets together, a `@fresh` whose `maxAge` is not above zero;
|
|
41
|
+
* for `@permission`, an object type or a permission the model does not
|
|
42
|
+
* declare, a malformed `id:`, an argument the field does not take, and a
|
|
43
|
+
* loader or a condition's `ctx` it needs and was not given.
|
|
43
44
|
*/
|
|
44
45
|
export declare function applyJanusDirectives<const T extends string>(schema: GraphQLSchema, options: JanusDirectivesOptions<T>): GraphQLSchema;
|
|
45
46
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/directives/apply.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAMnE,KAAK,OAAO,GAAG;IACd,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC;CACrE,CAAC;AAEF,2CAA2C;AAC3C,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;IAChE,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAA;KAAE,CAAC;IAChD,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,+GAA+G;IAC/G,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;IAC1D,iCAAiC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,oCAAoC;IACpC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC1C;AAED
|
|
1
|
+
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/directives/apply.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAMnE,KAAK,OAAO,GAAG;IACd,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC;CACrE,CAAC;AAEF,2CAA2C;AAC3C,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;IAChE,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAA;KAAE,CAAC;IAChD,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,+GAA+G;IAC/G,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;IAC1D,iCAAiC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,oCAAoC;IACpC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAC1D,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAChC,aAAa,CA4Bf"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What runs before a guarded field's resolver, at request time: the user,
|
|
3
|
-
* then the requirement — its user types,
|
|
3
|
+
* then the requirement — its user types, the session's freshness, then each
|
|
4
|
+
* `@permission` in order. A
|
|
4
5
|
* denial is a `GraphQLError`; a failure to answer is `SERVICE_UNAVAILABLE`,
|
|
5
6
|
* never a denial.
|
|
6
7
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/directives/guard.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/directives/guard.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAKpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,qCAAqC;AACrC,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB;AAED,yDAAyD;AACzD,wBAAsB,OAAO,CAC5B,WAAW,EAAE,WAAW,EACxB,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,SAAS,GACrC,OAAO,CAAC,IAAI,CAAC,CAuBf;AAED,qDAAqD;AACrD,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAC9B,OAAO,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EACtC,WAAW,EAAE,WAAW,GACtB,oBAAoB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAU/B"}
|
|
@@ -12,6 +12,13 @@ export interface AuthenticatedUse {
|
|
|
12
12
|
/** Its `type:` argument, or `null` when it names none: any signed-in user. */
|
|
13
13
|
readonly types: readonly string[] | null;
|
|
14
14
|
}
|
|
15
|
+
/** One `@fresh`, and where it was written. */
|
|
16
|
+
export interface FreshUse {
|
|
17
|
+
/** `Type.field`, `Type`, `Interface.field` or `Interface`. */
|
|
18
|
+
readonly where: string;
|
|
19
|
+
/** Its `maxAge:` argument, in seconds, as written: checked by `validate.ts`. */
|
|
20
|
+
readonly maxAge: number;
|
|
21
|
+
}
|
|
15
22
|
/** One `@permission`, as written, and where. */
|
|
16
23
|
export interface PermissionUse {
|
|
17
24
|
/** `Type.field`, `Type`, `Interface.field` or `Interface`. */
|
|
@@ -27,9 +34,10 @@ export interface PermissionUse {
|
|
|
27
34
|
/** Everything written on one field's locations. */
|
|
28
35
|
export interface FieldDirectives {
|
|
29
36
|
readonly authenticated: readonly AuthenticatedUse[];
|
|
37
|
+
readonly fresh: readonly FreshUse[];
|
|
30
38
|
/** In the order they are checked: the type's and interfaces' first, then the field's. */
|
|
31
39
|
readonly permission: readonly PermissionUse[];
|
|
32
40
|
}
|
|
33
|
-
/** Every `@authenticated` and `@permission` that applies to one object field. */
|
|
41
|
+
/** Every `@authenticated`, `@fresh` and `@permission` that applies to one object field. */
|
|
34
42
|
export declare function readField(schema: GraphQLSchema, field: GraphQLFieldConfig<unknown, unknown>, fieldName: string, typeName: string): FieldDirectives;
|
|
35
43
|
//# sourceMappingURL=read.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/directives/read.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EACX,kBAAkB,EAGlB,aAAa,EACb,MAAM,SAAS,CAAC;AAEjB,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAChC,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;CACzC;AAED,gDAAgD;AAChD,MAAM,WAAW,aAAa;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAAC;CAC3C;AAED,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,aAAa,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACpD,yFAAyF;IACzF,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;CAC9C;AA0CD,
|
|
1
|
+
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/directives/read.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EACX,kBAAkB,EAGlB,aAAa,EACb,MAAM,SAAS,CAAC;AAEjB,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAChC,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;CACzC;AAED,8CAA8C;AAC9C,MAAM,WAAW,QAAQ;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,gDAAgD;AAChD,MAAM,WAAW,aAAa;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAAC;CAC3C;AAED,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,aAAa,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACpD,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC,yFAAyF;IACzF,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;CAC9C;AA0CD,2FAA2F;AAC3F,wBAAgB,SAAS,CACxB,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,kBAAkB,CAAC,OAAO,EAAE,OAAO,CAAC,EAC3C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACd,eAAe,CA6BjB"}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Decides, when the schema is built, what the directives on one field add up
|
|
3
3
|
* to — and refuses with a `TypeError` what no request could ever pass: a user
|
|
4
4
|
* type `auth` does not know, a `type: []`, restrictions that exclude each
|
|
5
|
-
* other,
|
|
6
|
-
* (`permission/validate.ts`).
|
|
5
|
+
* other, a `@fresh` whose `maxAge` is not above zero, or a `@permission` the
|
|
6
|
+
* model, the field or the wiring cannot answer (`permission/validate.ts`).
|
|
7
7
|
*/
|
|
8
8
|
import { type FieldShape, type PermissionCheck, type PermissionWiring } from './permission/validate';
|
|
9
9
|
import type { FieldDirectives } from './read';
|
|
@@ -18,6 +18,11 @@ export interface Requirement {
|
|
|
18
18
|
* narrows it: any signed-in user.
|
|
19
19
|
*/
|
|
20
20
|
readonly types: ReadonlySet<string> | null;
|
|
21
|
+
/**
|
|
22
|
+
* How long ago, in milliseconds, the session may have proved who it is at
|
|
23
|
+
* most — the smallest `@fresh` that applies; `null` when none does.
|
|
24
|
+
*/
|
|
25
|
+
readonly maxAgeMs: number | null;
|
|
21
26
|
/** The permissions asked after that, in order: every one must hold. */
|
|
22
27
|
readonly permissions: readonly PermissionCheck[];
|
|
23
28
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/directives/validate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACN,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,gBAAgB,EAErB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAoB,eAAe,
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/directives/validate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACN,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,gBAAgB,EAErB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAoB,eAAe,EAAY,MAAM,QAAQ,CAAC;AAG1E,sDAAsD;AACtD,MAAM,WAAW,WAAW;IAC3B,sCAAsC;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,uEAAuE;IACvE,QAAQ,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE,CAAC;CACjD;AAED,6FAA6F;AAC7F,MAAM,WAAW,KAAK;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC5B,KAAK,EAAE,UAAU,EACjB,UAAU,EAAE,eAAe,EAC3B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,gBAAgB,GACtB,WAAW,GAAG,IAAI,CAWpB"}
|
package/dist/fresh.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freshness: whether the request's session proved who it is recently enough
|
|
3
|
+
* — the check `@fresh` runs before a field, and `requireFresh()` in a
|
|
4
|
+
* resolver. Both are `@nxgt/janus`'s `assertFresh`, on the clock given to
|
|
5
|
+
* `useJanus({ clock })`.
|
|
6
|
+
*/
|
|
7
|
+
import { type Duration, type Session } from '@nxgt/janus';
|
|
8
|
+
/**
|
|
9
|
+
* The request's session, once it proved who it is less than `maxAgeMs` ago
|
|
10
|
+
* — or a denial: `UNAUTHENTICATED` 401 for an anonymous request,
|
|
11
|
+
* `STEP_UP_REQUIRED` 403 for an older session. A store that cannot answer is
|
|
12
|
+
* `SERVICE_UNAVAILABLE` 503, never either of them.
|
|
13
|
+
*/
|
|
14
|
+
export declare function freshSession(janus: {
|
|
15
|
+
session(): Promise<Session | null>;
|
|
16
|
+
}, maxAgeMs: number): Promise<Session>;
|
|
17
|
+
/**
|
|
18
|
+
* The request's session, once it proved who it is less than `maxAge` ago —
|
|
19
|
+
* signed in, or confirmed since by `auth.stepUp.confirm` — or a denial:
|
|
20
|
+
* `UNAUTHENTICATED` 401 for an anonymous request, `STEP_UP_REQUIRED` 403 for
|
|
21
|
+
* an older session, which tells the client to ask for a step-up.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* await requireFresh(ctx, '10m'); // before changing the e-mail
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* `maxAge` is a duration with its unit — `'10m'`, `'600s'` — never a bare
|
|
28
|
+
* number, which `@nxgt/janus` reads as milliseconds where `@fresh` reads
|
|
29
|
+
* seconds: a `TypeError`, as any other malformed duration.
|
|
30
|
+
*/
|
|
31
|
+
export declare function requireFresh(ctx: {
|
|
32
|
+
readonly janus: {
|
|
33
|
+
user(): Promise<unknown>;
|
|
34
|
+
session(): Promise<Session | null>;
|
|
35
|
+
};
|
|
36
|
+
}, maxAge: Exclude<Duration, number>): Promise<Session>;
|
|
37
|
+
//# sourceMappingURL=fresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fresh.d.ts","sourceRoot":"","sources":["../src/fresh.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAEN,KAAK,QAAQ,EAEb,KAAK,OAAO,EACZ,MAAM,aAAa,CAAC;AAIrB;;;;;GAKG;AACH,wBAAsB,YAAY,CACjC,KAAK,EAAE;IAAE,OAAO,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAA;CAAE,EAC7C,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC,CASlB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CACjC,GAAG,EAAE;IACJ,QAAQ,CAAC,KAAK,EAAE;QACf,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;QACzB,OAAO,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;KACnC,CAAC;CACF,EACD,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,OAAO,CAAC,CAQlB"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@nxgt/janus-graphql` — `@nxgt/janus` in a GraphQL server.
|
|
3
3
|
*
|
|
4
|
-
* - `useJanus({ auth, access?, type?, loaders?, conditions? })` —
|
|
5
|
-
* envelop plugin: `ctx.janus` on every request, and the directives —
|
|
6
|
-
* `@authenticated`, `@permission` — applied to the schema;
|
|
4
|
+
* - `useJanus({ auth, access?, type?, clock?, loaders?, conditions? })` —
|
|
5
|
+
* the envelop plugin: `ctx.janus` on every request, and the directives —
|
|
6
|
+
* `@authenticated`, `@fresh`, `@permission` — applied to the schema;
|
|
7
7
|
* - `janusTypeDefs` — the directives' SDL, also shipped as
|
|
8
8
|
* `graphql/janus.graphqls`;
|
|
9
9
|
* - `applyJanusDirectives(schema, { auth })` — the schema transform alone;
|
|
10
|
-
* - `requireUser(ctx, { type? })
|
|
11
|
-
* calls a resolver makes itself;
|
|
10
|
+
* - `requireUser(ctx, { type? })`, `requireFresh(ctx, maxAge)` and
|
|
11
|
+
* `can(ctx, permission, object)` — the calls a resolver makes itself;
|
|
12
12
|
* - `janusMaskError(fallback?)` — Yoga's `maskError`: a `JanusError` as its
|
|
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';
|
|
28
|
+
export { requireFresh } from './fresh';
|
|
22
29
|
export { can, type RequireUserOptions, requireUser } from './helpers';
|
|
23
30
|
export { useJanus } from './plugin';
|
|
24
31
|
export { janusTypeDefs } from './sdl';
|