@nxgt/janus-graphql 0.1.1 → 0.2.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 CHANGED
@@ -2,7 +2,8 @@
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
5
+ asks, fields and types guarded by `@authenticated`, `@fresh` and
6
+ `@permission`, and
6
7
  every error answered with the status it deserves — an outage as 503, never
7
8
  as 401, 403 or 404.
8
9
 
@@ -77,20 +78,22 @@ that reads files — `node_modules/@nxgt/janus-graphql/graphql/janus.graphqls`.
77
78
 
78
79
  | Export | What it is |
79
80
  | --- | --- |
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` |
81
+ | `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
82
  | `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
83
  | `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` |
84
+ | `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
85
  | `@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 |
86
+ | `@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
87
  | `@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
88
  | `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
89
  | `conditions: { [type]: (object, ctx) => ctx }` | The `ctx` `@permission` passes to `access.can` for a permission that reaches a `when()` |
88
90
  | `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
91
  | `requireUser(ctx, { type? })` | The signed-in user, narrowed to `type` — one or a non-empty list — or a denial: `UNAUTHENTICATED`, `FORBIDDEN` |
92
+ | `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
93
  | `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 |
91
94
  | `janusMaskError(fallback?)` | Yoga's `maskedErrors.maskError`: a `JanusError` a resolver let through answered with its code and status; anything else to `fallback` |
92
95
  | `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 |
96
+ | `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
97
  | `JanusContext<typeof auth, typeof access?, Type?>` | What `useJanus()` adds to the context, for your resolvers' `ctx` |
95
98
  | `JanusOptions`, `JanusOnContext`, `JanusDirectivesOptions`, `Loaders`, `Conditions`, `LoadedObject`, `RequireUserOptions`, `Auth`, `UserOfAuth`, `DenialCode`, `MaskError` | The types of the arguments and answers above |
96
99
 
@@ -108,6 +111,10 @@ type Record @permission(name: "view", type: "record") { # parent.id:
108
111
  title: String
109
112
  billing: Billing @permission(name: "manage", type: "record", onDeny: FORBIDDEN)
110
113
  }
114
+
115
+ type Mutation {
116
+ changeEmail(email: String!): User @fresh(maxAge: 600) # signed in or stepped up < 10 min ago
117
+ }
111
118
  ```
112
119
 
113
120
  ```ts
@@ -120,6 +127,12 @@ useJanus({
120
127
  ```
121
128
 
122
129
  Each runs before the resolver, which a refused request never reaches.
130
+ `@fresh(maxAge)` takes **seconds**, and answers an older session
131
+ `STEP_UP_REQUIRED`: the client then calls your two step-up mutations,
132
+ which run `auth.stepUp.request` and `confirm` — shown in
133
+ [the step-up guide](docs/guide/step-up.md) — and sends the request again.
134
+ Every `@authenticated` is checked first, then `@fresh`, then each
135
+ `@permission`, so a stale session asks no permission check.
123
136
  `@permission` implies a signed-in user, and asks `access.can` with the parent
124
137
  itself when the id is `parent.id`, with what `loaders[type]` answers for an
125
138
  id of a type with a `fromField`, and with `{ type, id }` otherwise. Every
@@ -129,8 +142,8 @@ a field's `FORBIDDEN` could tell the object exists. The same question asked
129
142
  twice in a request, by two fields or by a directive and `can()`, is one
130
143
  check. What no request could pass — an object type or a permission the
131
144
  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).
145
+ take, a missing loader or condition, a `maxAge` not above zero — is a
146
+ `TypeError` at start-up naming the field. Detail: [the directives guide](docs/guide/directives.md).
134
147
 
135
148
  ## Errors
136
149
 
@@ -143,8 +156,9 @@ every one carries a `code` and the HTTP status Yoga answers with:
143
156
 
144
157
  | Code | Status | When |
145
158
  | --- | --- | --- |
146
- | `UNAUTHENTICATED` | 401 | An anonymous request reached a guarded field, or `requireUser()` |
159
+ | `UNAUTHENTICATED` | 401 | An anonymous request reached a guarded field, `requireUser()` or `requireFresh()` |
147
160
  | `FORBIDDEN` | 403 | A user of a type the directive or `requireUser()` does not name; a `@permission(onDeny: FORBIDDEN)` denied |
161
+ | `STEP_UP_REQUIRED` | 403 | A session older than `@fresh(maxAge)` or `requireFresh()` allows: ask for a step-up, then send the request again |
148
162
  | `NOT_FOUND` | 404 | A `@permission` denied — its default — or a loader answered `null`; `denial('NOT_FOUND')` |
149
163
  | `SERVICE_UNAVAILABLE` | 503 | A store could not answer — `STORE_FAILED`. **Never a denial** |
150
164
  | 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, … |
@@ -206,19 +220,31 @@ response with several errors: [the errors guide](docs/guide/errors.md).
206
220
  - **A context built without a request cannot authenticate.** `ctx.janus.user()`
207
221
  rejects with a `TypeError` — a transport that is not HTTP, such as a
208
222
  WebSocket, needs its own wiring, which is on the [roadmap](docs/roadmap.md).
223
+ - **`@fresh` reads seconds; `requireFresh()` a duration.** `@fresh(maxAge:
224
+ 10)` is ten seconds, not ten minutes — write `600`. `requireFresh(ctx,
225
+ '10m')` takes the unit, and refuses a bare number, which `@nxgt/janus`
226
+ would read as milliseconds.
227
+ - **`useJanus()` reads the system clock unless given `janus()`'s.** With
228
+ `janus({ clock: fixedClock(…) })` in a spec and no `useJanus({ clock })`,
229
+ every session looks as old as the fixed date, and every `@fresh` field
230
+ answers `STEP_UP_REQUIRED`. Pass the same clock to both.
231
+ - **A subscription field's `@fresh` is checked when it subscribes.** Its
232
+ events keep coming past `maxAge`; that `@fresh` is not asked again on
233
+ each one, as `@permission` is. A `@fresh` on the payload's type or its
234
+ fields is asked on every event, like any field.
209
235
  - **A denial of a list field's item fails the whole list** where the item is
210
236
  non-null (`[Doctor!]!`), as GraphQL's null propagation always does. Guard
211
237
  the list field, or make the item nullable.
212
238
 
213
239
  ## Documentation
214
240
 
215
- - [Guides](docs/README.md) — the directives, the context and the lazy user, errors as statuses, telemetry, and federation
241
+ - [Guides](docs/README.md) — the directives, the step-up over GraphQL, the context and the lazy user, errors as statuses, telemetry, and federation
216
242
  - [Troubleshooting](docs/troubleshooting.md) — by the message or status you see
217
243
  - [Roadmap](docs/roadmap.md) — what is next, and what is not planned
218
244
 
219
245
  ## Type safety, counted
220
246
 
221
- Twenty-four plausible mistakes are refused by the compiler, each with a
247
+ Twenty-nine plausible mistakes are refused by the compiler, each with a
222
248
  `@ts-expect-error` case in `test/types/`:
223
249
 
224
250
  - five in `context.ts`: reading the user or the session where either may be
@@ -239,7 +265,10 @@ Twenty-four plausible mistakes are refused by the compiler, each with a
239
265
  does not declare, a loader answering an object without a field a
240
266
  `fromField` reads, `conditions` for a type no `when()` is reached on, a
241
267
  condition answering a `ctx` of the wrong shape, and `loaders` without
242
- `access`.
268
+ `access`;
269
+ - five in `fresh.ts`: `requireFresh()` given a bare number, a duration
270
+ written as prose, no `maxAge` at all, or the request instead of the
271
+ context, and `useJanus()` given a timestamp as its `clock`.
243
272
 
244
273
  `plugin.ts` also holds this README's quick start, which must keep compiling.
245
274
 
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,6 +33,7 @@ export interface ContextOptions {
33
33
  readonly can: unknown;
34
34
  } | undefined;
35
35
  readonly type?: string | undefined;
36
+ readonly clock?: Clock | undefined;
36
37
  }
37
38
  /**
38
39
  * `ctx.janus` for one request. `authenticate` runs the first time `user()`
@@ -44,6 +45,11 @@ export interface ContextOptions {
44
45
  * here asked.
45
46
  */
46
47
  export declare function createJanusContext(request: RequestLike | undefined, options: ContextOptions): Built;
48
+ /**
49
+ * The clock `@fresh` and `requireFresh()` read the time from: the one given
50
+ * to `useJanus({ clock })`, or the system's.
51
+ */
52
+ export declare function clockOf(janus: object): Clock;
47
53
  /**
48
54
  * The check `ctx.janus` answers through: the one its context was built
49
55
  * 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;CACnC;AAED;;;;;;;;GAQG;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,14 +1,14 @@
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
@@ -19,6 +19,7 @@
19
19
  */
20
20
  export { applyJanusDirectives, type JanusDirectivesOptions, } from './directives/apply';
21
21
  export { type DenialCode, denial, janusGraphQLError, janusMaskError, type MaskError, } from './errors';
22
+ export { requireFresh } from './fresh';
22
23
  export { can, type RequireUserOptions, requireUser } from './helpers';
23
24
  export { useJanus } from './plugin';
24
25
  export { janusTypeDefs } from './sdl';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACN,oBAAoB,EACpB,KAAK,sBAAsB,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACN,KAAK,UAAU,EACf,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,KAAK,SAAS,GACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,GAAG,EAAE,KAAK,kBAAkB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,YAAY,EACX,IAAI,EACJ,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,UAAU,GACV,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACN,oBAAoB,EACpB,KAAK,sBAAsB,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACN,KAAK,UAAU,EACf,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,KAAK,SAAS,GACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,GAAG,EAAE,KAAK,kBAAkB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,YAAY,EACX,IAAI,EACJ,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,UAAU,GACV,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
package/dist/index.js CHANGED
@@ -2,6 +2,11 @@
2
2
  import { MapperKind, mapSchema } from "@graphql-tools/utils";
3
3
  import { defaultFieldResolver } from "graphql";
4
4
 
5
+ // src/context.ts
6
+ import {
7
+ systemClock
8
+ } from "@nxgt/janus";
9
+
5
10
  // src/memo.ts
6
11
  var NOTATION = /[@#]/;
7
12
  function keyOf(subject, permission, object, options) {
@@ -32,8 +37,9 @@ function memoized(access) {
32
37
 
33
38
  // src/context.ts
34
39
  var checks = new WeakMap;
40
+ var clocks = new WeakMap;
35
41
  function createJanusContext(request, options) {
36
- const { auth, access, type } = options;
42
+ const { auth, access, type, clock } = options;
37
43
  let answer;
38
44
  let asked = false;
39
45
  const authenticated = () => {
@@ -55,8 +61,13 @@ function createJanusContext(request, options) {
55
61
  fresh: (...args) => can.apply(access, args)
56
62
  });
57
63
  }
64
+ if (clock !== undefined)
65
+ clocks.set(janus, clock);
58
66
  return janus;
59
67
  }
68
+ function clockOf(janus) {
69
+ return clocks.get(janus) ?? systemClock;
70
+ }
60
71
  function checkOf(janus, fresh = false) {
61
72
  const built = checks.get(janus);
62
73
  if (built !== undefined)
@@ -165,6 +176,30 @@ function masked(error, message) {
165
176
  });
166
177
  }
167
178
 
179
+ // src/fresh.ts
180
+ import {
181
+ assertFresh,
182
+ parseDuration
183
+ } from "@nxgt/janus";
184
+ async function freshSession(janus, maxAgeMs) {
185
+ const session = await janus.session().then(undefined, rethrown);
186
+ if (session === null)
187
+ throw denial("UNAUTHENTICATED");
188
+ try {
189
+ assertFresh(session, maxAgeMs, clockOf(janus));
190
+ } catch (error) {
191
+ rethrown(error);
192
+ }
193
+ return session;
194
+ }
195
+ async function requireFresh(ctx, maxAge) {
196
+ if (typeof maxAge !== "string") {
197
+ throw new TypeError("requireFresh(): maxAge is a duration with its unit, such as '10m' — a bare number would be milliseconds, where @fresh reads seconds");
198
+ }
199
+ const maxAgeMs = parseDuration(maxAge, "requireFresh(): maxAge");
200
+ return freshSession(janusOf(ctx, "requireFresh()"), maxAgeMs);
201
+ }
202
+
168
203
  // src/directives/permission/path.ts
169
204
  var NAME = /^[_A-Za-z][_0-9A-Za-z]*$/;
170
205
  function parsePath(text) {
@@ -278,6 +313,9 @@ async function enforce(requirement, { parent, args, ctx, fresh }) {
278
313
  if (requirement.types !== null && !requirement.types.has(user.type)) {
279
314
  throw denial("FORBIDDEN");
280
315
  }
316
+ if (requirement.maxAgeMs !== null) {
317
+ await freshSession(janus, requirement.maxAgeMs);
318
+ }
281
319
  const [first] = requirement.permissions;
282
320
  if (first === undefined)
283
321
  return;
@@ -326,12 +364,16 @@ function locations(schema, field, fieldName, typeName) {
326
364
  }
327
365
  function readField(schema, field, fieldName, typeName) {
328
366
  const authenticated = [];
367
+ const fresh = [];
329
368
  const permission = [];
330
369
  for (const { where, onType, node } of locations(schema, field, fieldName, typeName)) {
331
370
  for (const args of getDirective(schema, node, "authenticated") ?? []) {
332
371
  const types = args.type;
333
372
  authenticated.push({ where, types: types ?? null });
334
373
  }
374
+ for (const args of getDirective(schema, node, "fresh") ?? []) {
375
+ fresh.push({ where, maxAge: args.maxAge });
376
+ }
335
377
  for (const args of getDirective(schema, node, "permission") ?? []) {
336
378
  permission.push({
337
379
  where,
@@ -343,7 +385,7 @@ function readField(schema, field, fieldName, typeName) {
343
385
  });
344
386
  }
345
387
  }
346
- return { authenticated, permission };
388
+ return { authenticated, fresh, permission };
347
389
  }
348
390
 
349
391
  // src/directives/words.ts
@@ -473,15 +515,15 @@ function conditionOf(model, use, wiring, refuse) {
473
515
 
474
516
  // src/directives/validate.ts
475
517
  function requirementOf(field, directives, known, wiring) {
476
- const { authenticated, permission } = directives;
477
- if (authenticated.length === 0 && permission.length === 0)
518
+ const { authenticated, fresh, permission } = directives;
519
+ const [name, first] = firstOf(directives);
520
+ if (first === undefined)
478
521
  return null;
479
- const [first] = authenticated.length > 0 ? authenticated : permission;
480
- const name = authenticated.length > 0 ? "@authenticated" : "@permission";
481
522
  return {
482
523
  where: field.name,
483
- label: `${name} on ${on(first?.where ?? field.name, field.name)}`,
524
+ label: `${name} on ${on(first.where, field.name)}`,
484
525
  types: typesOf(field.name, authenticated, known),
526
+ maxAgeMs: maxAgeOf(field.name, fresh),
485
527
  permissions: permission.map((use) => permissionCheckOf(use, field, wiring))
486
528
  };
487
529
  }
@@ -505,6 +547,27 @@ function typesOf(field, uses, known) {
505
547
  }
506
548
  return types;
507
549
  }
550
+ function firstOf({
551
+ authenticated,
552
+ fresh,
553
+ permission
554
+ }) {
555
+ if (authenticated.length > 0)
556
+ return ["@authenticated", authenticated[0]];
557
+ if (fresh.length > 0)
558
+ return ["@fresh", fresh[0]];
559
+ return ["@permission", permission[0]];
560
+ }
561
+ function maxAgeOf(field, uses) {
562
+ let seconds = null;
563
+ for (const { where, maxAge } of uses) {
564
+ if (!Number.isSafeInteger(maxAge) || maxAge <= 0) {
565
+ throw new TypeError(`${PREFIX}: @fresh on ${on(where, field)} asks maxAge: ${maxAge} — write a number of seconds above zero, such as maxAge: 600 for ten minutes`);
566
+ }
567
+ seconds = seconds === null ? maxAge : Math.min(seconds, maxAge);
568
+ }
569
+ return seconds === null ? null : seconds * 1000;
570
+ }
508
571
 
509
572
  // src/directives/apply.ts
510
573
  function applyJanusDirectives(schema, options) {
@@ -517,10 +580,15 @@ function applyJanusDirectives(schema, options) {
517
580
  }, readField(schema, field, fieldName, typeName), known, options);
518
581
  if (requirement === null)
519
582
  return field;
583
+ const resolve = field.resolve ?? defaultFieldResolver;
584
+ if (field.subscribe === undefined) {
585
+ return { ...field, resolve: guarded(resolve, requirement) };
586
+ }
587
+ const onEvent = { ...requirement, maxAgeMs: null };
520
588
  return {
521
589
  ...field,
522
- resolve: guarded(field.resolve ?? defaultFieldResolver, requirement),
523
- ...field.subscribe === undefined ? {} : { subscribe: guarded(field.subscribe, requirement) }
590
+ subscribe: guarded(field.subscribe, requirement),
591
+ resolve: guarded(resolve, onEvent)
524
592
  };
525
593
  }
526
594
  });
@@ -559,13 +627,16 @@ async function can(ctx, permission, object, ...options) {
559
627
  }
560
628
  // src/plugin.ts
561
629
  function useJanus(options) {
562
- const { auth, access, type, loaders, conditions } = options;
630
+ const { auth, access, type, clock, loaders, conditions } = options;
563
631
  if (typeof auth?.authenticate !== "function") {
564
632
  throw new TypeError("useJanus(): auth is not what janus() answered — pass { auth }");
565
633
  }
566
634
  if (access !== undefined && typeof access?.can !== "function") {
567
635
  throw new TypeError("useJanus(): access is not what permissions() answered — pass { auth, access }");
568
636
  }
637
+ if (clock !== undefined && typeof clock?.now !== "function") {
638
+ throw new TypeError("useJanus(): clock is not a Clock — pass the clock given to janus(), or leave it out for the system clock");
639
+ }
569
640
  const applied = new WeakSet;
570
641
  return {
571
642
  onContextBuilding: ({ context, extendContext }) => {
@@ -573,7 +644,8 @@ function useJanus(options) {
573
644
  const janus = createJanusContext(request, {
574
645
  auth,
575
646
  access,
576
- type
647
+ type,
648
+ clock
577
649
  });
578
650
  extendContext({ janus });
579
651
  },
@@ -602,6 +674,14 @@ directive @authenticated(
602
674
  type: [String!]
603
675
  ) on OBJECT | INTERFACE | FIELD_DEFINITION
604
676
 
677
+ """
678
+ Only a session that proved who it is less than \`maxAge\` seconds ago reaches
679
+ this field, or every field of this type or interface: signed in, or confirmed
680
+ since by a step-up. Anonymous is answered UNAUTHENTICATED, an older session
681
+ STEP_UP_REQUIRED. Where several apply, the smallest \`maxAge\` holds.
682
+ """
683
+ directive @fresh(maxAge: Int!) on OBJECT | INTERFACE | FIELD_DEFINITION
684
+
605
685
  """
606
686
  What a denied \`@permission\` answers.
607
687
  """
@@ -637,9 +717,10 @@ export {
637
717
  janusGraphQLError,
638
718
  janusMaskError,
639
719
  janusTypeDefs,
720
+ requireFresh,
640
721
  requireUser,
641
722
  useJanus
642
723
  };
643
724
 
644
- //# debugId=D0DEC0D35AFB92A464756E2164756E21
725
+ //# debugId=50FC1B1149A482D964756E2164756E21
645
726
  //# sourceMappingURL=index.js.map