@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.
Files changed (41) hide show
  1. package/README.md +110 -21
  2. package/dist/connection/connection.d.ts +36 -0
  3. package/dist/connection/connection.d.ts.map +1 -0
  4. package/dist/connection/credential.d.ts +34 -0
  5. package/dist/connection/credential.d.ts.map +1 -0
  6. package/dist/connection/types.d.ts +57 -0
  7. package/dist/connection/types.d.ts.map +1 -0
  8. package/dist/context.d.ts +11 -2
  9. package/dist/context.d.ts.map +1 -1
  10. package/dist/directives/apply.d.ts +10 -9
  11. package/dist/directives/apply.d.ts.map +1 -1
  12. package/dist/directives/guard.d.ts +2 -1
  13. package/dist/directives/guard.d.ts.map +1 -1
  14. package/dist/directives/read.d.ts +9 -1
  15. package/dist/directives/read.d.ts.map +1 -1
  16. package/dist/directives/validate.d.ts +7 -2
  17. package/dist/directives/validate.d.ts.map +1 -1
  18. package/dist/fresh.d.ts +37 -0
  19. package/dist/fresh.d.ts.map +1 -0
  20. package/dist/index.d.ts +12 -5
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +170 -23
  23. package/dist/index.js.map +14 -10
  24. package/dist/options.d.ts +17 -0
  25. package/dist/options.d.ts.map +1 -0
  26. package/dist/plugin.d.ts +8 -3
  27. package/dist/plugin.d.ts.map +1 -1
  28. package/dist/sdl.d.ts.map +1 -1
  29. package/dist/types.d.ts +6 -1
  30. package/dist/types.d.ts.map +1 -1
  31. package/docs/README.md +5 -3
  32. package/docs/guide/context.md +36 -3
  33. package/docs/guide/directives.md +64 -5
  34. package/docs/guide/errors.md +2 -1
  35. package/docs/guide/federation.md +2 -1
  36. package/docs/guide/step-up.md +186 -0
  37. package/docs/guide/subscriptions.md +251 -0
  38. package/docs/roadmap.md +29 -9
  39. package/docs/troubleshooting.md +218 -11
  40. package/graphql/janus.graphqls +8 -0
  41. 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` and `@permission`, and
6
- every error answered with the status it deserves — an outage as 503, never
7
- as 401, 403 or 404.
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 and `@envelop/core` 5.0.0
66
- as well, together, with a single copy of `graphql`, in the Floors job, on
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 — is a `TypeError` at start-up naming
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 `requireUser()` |
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, such as a
208
- WebSocket, needs its own wiring, which is on the [roadmap](docs/roadmap.md).
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
- Twenty-four plausible mistakes are refused by the compiler, each with a
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
- `plugin.ts` also holds this README's quick start, which must keep compiling.
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 { Authenticated, RequestLike } from '@nxgt/janus';
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 — answers a
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
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE9D,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;AAU9C,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;CACnC;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,WAAW,GAAG,SAAS,EAChC,OAAO,EAAE,cAAc,GACrB,KAAK,CAmCP;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"}
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 `@permission`
28
- * — on the field, on its type, or on an interface the type implements —
29
- * wrapped so its resolver runs only for a signed-in user, of one of the
30
- * types `type:` names when it names some, holding every permission asked.
31
- * Every directive that applies must hold.
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; for `@permission`, an object type or a
40
- * permission the model does not declare, a malformed `id:`, an argument the
41
- * field does not take, and a loader or a condition's `ctx` it needs and was
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;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAC1D,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAChC,aAAa,CAuBf"}
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, then each `@permission` in order. A
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;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAIpD,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,CAoBf;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"}
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,iFAAiF;AACjF,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,CAyBjB"}
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, or a `@permission` the model, the field or the wiring cannot answer
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,EAAE,MAAM,QAAQ,CAAC;AAGhE,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,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"}
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"}
@@ -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? })` — the
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? })` and `can(ctx, permission, object)` — the
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';