@nxgt/janus-graphql 0.1.0 → 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 +39 -10
- package/dist/context.d.ts +7 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/directives/apply.d.ts +10 -9
- package/dist/directives/apply.d.ts.map +1 -1
- package/dist/directives/guard.d.ts +2 -1
- package/dist/directives/guard.d.ts.map +1 -1
- package/dist/directives/read.d.ts +9 -1
- package/dist/directives/read.d.ts.map +1 -1
- package/dist/directives/validate.d.ts +7 -2
- package/dist/directives/validate.d.ts.map +1 -1
- package/dist/fresh.d.ts +37 -0
- package/dist/fresh.d.ts.map +1 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +93 -12
- package/dist/index.js.map +11 -10
- package/dist/plugin.d.ts +4 -3
- package/dist/plugin.d.ts.map +1 -1
- package/dist/sdl.d.ts.map +1 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +4 -3
- package/docs/guide/context.md +32 -2
- package/docs/guide/directives.md +64 -5
- package/docs/guide/errors.md +2 -1
- package/docs/guide/federation.md +2 -1
- package/docs/guide/step-up.md +182 -0
- package/docs/roadmap.md +11 -3
- package/docs/troubleshooting.md +128 -4
- package/graphql/janus.graphqls +8 -0
- package/package.json +4 -4
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
|
|
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
|
|
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 `
|
|
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-
|
|
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
|
|
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
|
package/dist/context.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACN,KAAK,aAAa,EAClB,KAAK,KAAK,EACV,KAAK,WAAW,EAEhB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,iFAAiF;AACjF,MAAM,MAAM,KAAK,GAAG,CACnB,OAAO,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAC9D,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,EACtD,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,KAChC,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB,qEAAqE;AACrE,KAAK,OAAO,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9D,KAAK,SAAS,GAAG;IAChB,YAAY,CACX,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAClC,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;CAC1C,CAAC;AAEF,KAAK,KAAK,GAAG,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAa9C,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;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
|
|
28
|
-
* — on the field, on its type, or on an interface the type
|
|
29
|
-
* wrapped so its resolver runs only for a signed-in user, of
|
|
30
|
-
* types `type:` names when it names some,
|
|
31
|
-
*
|
|
27
|
+
* The schema with every field guarded by `@authenticated`, `@fresh` or
|
|
28
|
+
* `@permission` — on the field, on its type, or on an interface the type
|
|
29
|
+
* implements — wrapped so its resolver runs only for a signed-in user, of
|
|
30
|
+
* one of the types `type:` names when it names some, on a session that
|
|
31
|
+
* proved who it is less than `maxAge` seconds ago, holding every permission
|
|
32
|
+
* asked. Every directive that applies must hold.
|
|
32
33
|
*
|
|
33
34
|
* `useJanus()` calls it on every schema it is given. Call it yourself to
|
|
34
35
|
* check a schema without a server: the guards it installs read `ctx.janus`,
|
|
@@ -36,10 +37,10 @@ export interface JanusDirectivesOptions<T extends string = string> {
|
|
|
36
37
|
*
|
|
37
38
|
* **Refuses when the schema is built**, with a `TypeError` naming the
|
|
38
39
|
* field: a user type `auth` does not know, a `type: []`, restrictions no
|
|
39
|
-
* user type meets together
|
|
40
|
-
* permission
|
|
41
|
-
*
|
|
42
|
-
* not given.
|
|
40
|
+
* user type meets together, a `@fresh` whose `maxAge` is not above zero;
|
|
41
|
+
* for `@permission`, an object type or a permission the model does not
|
|
42
|
+
* declare, a malformed `id:`, an argument the field does not take, and a
|
|
43
|
+
* loader or a condition's `ctx` it needs and was not given.
|
|
43
44
|
*/
|
|
44
45
|
export declare function applyJanusDirectives<const T extends string>(schema: GraphQLSchema, options: JanusDirectivesOptions<T>): GraphQLSchema;
|
|
45
46
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/directives/apply.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAMnE,KAAK,OAAO,GAAG;IACd,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC;CACrE,CAAC;AAEF,2CAA2C;AAC3C,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;IAChE,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAA;KAAE,CAAC;IAChD,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,+GAA+G;IAC/G,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;IAC1D,iCAAiC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,oCAAoC;IACpC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC1C;AAED
|
|
1
|
+
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/directives/apply.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAMnE,KAAK,OAAO,GAAG;IACd,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC;CACrE,CAAC;AAEF,2CAA2C;AAC3C,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;IAChE,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAA;KAAE,CAAC;IAChD,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,+GAA+G;IAC/G,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;IAC1D,iCAAiC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,oCAAoC;IACpC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAC1D,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAChC,aAAa,CA4Bf"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What runs before a guarded field's resolver, at request time: the user,
|
|
3
|
-
* then the requirement — its user types,
|
|
3
|
+
* then the requirement — its user types, the session's freshness, then each
|
|
4
|
+
* `@permission` in order. A
|
|
4
5
|
* denial is a `GraphQLError`; a failure to answer is `SERVICE_UNAVAILABLE`,
|
|
5
6
|
* never a denial.
|
|
6
7
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/directives/guard.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/directives/guard.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAKpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,qCAAqC;AACrC,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB;AAED,yDAAyD;AACzD,wBAAsB,OAAO,CAC5B,WAAW,EAAE,WAAW,EACxB,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,SAAS,GACrC,OAAO,CAAC,IAAI,CAAC,CAuBf;AAED,qDAAqD;AACrD,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAC9B,OAAO,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EACtC,WAAW,EAAE,WAAW,GACtB,oBAAoB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAU/B"}
|
|
@@ -12,6 +12,13 @@ export interface AuthenticatedUse {
|
|
|
12
12
|
/** Its `type:` argument, or `null` when it names none: any signed-in user. */
|
|
13
13
|
readonly types: readonly string[] | null;
|
|
14
14
|
}
|
|
15
|
+
/** One `@fresh`, and where it was written. */
|
|
16
|
+
export interface FreshUse {
|
|
17
|
+
/** `Type.field`, `Type`, `Interface.field` or `Interface`. */
|
|
18
|
+
readonly where: string;
|
|
19
|
+
/** Its `maxAge:` argument, in seconds, as written: checked by `validate.ts`. */
|
|
20
|
+
readonly maxAge: number;
|
|
21
|
+
}
|
|
15
22
|
/** One `@permission`, as written, and where. */
|
|
16
23
|
export interface PermissionUse {
|
|
17
24
|
/** `Type.field`, `Type`, `Interface.field` or `Interface`. */
|
|
@@ -27,9 +34,10 @@ export interface PermissionUse {
|
|
|
27
34
|
/** Everything written on one field's locations. */
|
|
28
35
|
export interface FieldDirectives {
|
|
29
36
|
readonly authenticated: readonly AuthenticatedUse[];
|
|
37
|
+
readonly fresh: readonly FreshUse[];
|
|
30
38
|
/** In the order they are checked: the type's and interfaces' first, then the field's. */
|
|
31
39
|
readonly permission: readonly PermissionUse[];
|
|
32
40
|
}
|
|
33
|
-
/** Every `@authenticated` and `@permission` that applies to one object field. */
|
|
41
|
+
/** Every `@authenticated`, `@fresh` and `@permission` that applies to one object field. */
|
|
34
42
|
export declare function readField(schema: GraphQLSchema, field: GraphQLFieldConfig<unknown, unknown>, fieldName: string, typeName: string): FieldDirectives;
|
|
35
43
|
//# sourceMappingURL=read.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/directives/read.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EACX,kBAAkB,EAGlB,aAAa,EACb,MAAM,SAAS,CAAC;AAEjB,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAChC,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;CACzC;AAED,gDAAgD;AAChD,MAAM,WAAW,aAAa;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAAC;CAC3C;AAED,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,aAAa,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACpD,yFAAyF;IACzF,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;CAC9C;AA0CD,
|
|
1
|
+
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/directives/read.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EACX,kBAAkB,EAGlB,aAAa,EACb,MAAM,SAAS,CAAC;AAEjB,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAChC,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;CACzC;AAED,8CAA8C;AAC9C,MAAM,WAAW,QAAQ;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,gDAAgD;AAChD,MAAM,WAAW,aAAa;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAAC;CAC3C;AAED,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,aAAa,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACpD,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC,yFAAyF;IACzF,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;CAC9C;AA0CD,2FAA2F;AAC3F,wBAAgB,SAAS,CACxB,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,kBAAkB,CAAC,OAAO,EAAE,OAAO,CAAC,EAC3C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACd,eAAe,CA6BjB"}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Decides, when the schema is built, what the directives on one field add up
|
|
3
3
|
* to — and refuses with a `TypeError` what no request could ever pass: a user
|
|
4
4
|
* type `auth` does not know, a `type: []`, restrictions that exclude each
|
|
5
|
-
* other,
|
|
6
|
-
* (`permission/validate.ts`).
|
|
5
|
+
* other, a `@fresh` whose `maxAge` is not above zero, or a `@permission` the
|
|
6
|
+
* model, the field or the wiring cannot answer (`permission/validate.ts`).
|
|
7
7
|
*/
|
|
8
8
|
import { type FieldShape, type PermissionCheck, type PermissionWiring } from './permission/validate';
|
|
9
9
|
import type { FieldDirectives } from './read';
|
|
@@ -18,6 +18,11 @@ export interface Requirement {
|
|
|
18
18
|
* narrows it: any signed-in user.
|
|
19
19
|
*/
|
|
20
20
|
readonly types: ReadonlySet<string> | null;
|
|
21
|
+
/**
|
|
22
|
+
* How long ago, in milliseconds, the session may have proved who it is at
|
|
23
|
+
* most — the smallest `@fresh` that applies; `null` when none does.
|
|
24
|
+
*/
|
|
25
|
+
readonly maxAgeMs: number | null;
|
|
21
26
|
/** The permissions asked after that, in order: every one must hold. */
|
|
22
27
|
readonly permissions: readonly PermissionCheck[];
|
|
23
28
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/directives/validate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACN,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,gBAAgB,EAErB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAoB,eAAe,
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/directives/validate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACN,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,gBAAgB,EAErB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAoB,eAAe,EAAY,MAAM,QAAQ,CAAC;AAG1E,sDAAsD;AACtD,MAAM,WAAW,WAAW;IAC3B,sCAAsC;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,uEAAuE;IACvE,QAAQ,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE,CAAC;CACjD;AAED,6FAA6F;AAC7F,MAAM,WAAW,KAAK;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC5B,KAAK,EAAE,UAAU,EACjB,UAAU,EAAE,eAAe,EAC3B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,gBAAgB,GACtB,WAAW,GAAG,IAAI,CAWpB"}
|
package/dist/fresh.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freshness: whether the request's session proved who it is recently enough
|
|
3
|
+
* — the check `@fresh` runs before a field, and `requireFresh()` in a
|
|
4
|
+
* resolver. Both are `@nxgt/janus`'s `assertFresh`, on the clock given to
|
|
5
|
+
* `useJanus({ clock })`.
|
|
6
|
+
*/
|
|
7
|
+
import { type Duration, type Session } from '@nxgt/janus';
|
|
8
|
+
/**
|
|
9
|
+
* The request's session, once it proved who it is less than `maxAgeMs` ago
|
|
10
|
+
* — or a denial: `UNAUTHENTICATED` 401 for an anonymous request,
|
|
11
|
+
* `STEP_UP_REQUIRED` 403 for an older session. A store that cannot answer is
|
|
12
|
+
* `SERVICE_UNAVAILABLE` 503, never either of them.
|
|
13
|
+
*/
|
|
14
|
+
export declare function freshSession(janus: {
|
|
15
|
+
session(): Promise<Session | null>;
|
|
16
|
+
}, maxAgeMs: number): Promise<Session>;
|
|
17
|
+
/**
|
|
18
|
+
* The request's session, once it proved who it is less than `maxAge` ago —
|
|
19
|
+
* signed in, or confirmed since by `auth.stepUp.confirm` — or a denial:
|
|
20
|
+
* `UNAUTHENTICATED` 401 for an anonymous request, `STEP_UP_REQUIRED` 403 for
|
|
21
|
+
* an older session, which tells the client to ask for a step-up.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* await requireFresh(ctx, '10m'); // before changing the e-mail
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* `maxAge` is a duration with its unit — `'10m'`, `'600s'` — never a bare
|
|
28
|
+
* number, which `@nxgt/janus` reads as milliseconds where `@fresh` reads
|
|
29
|
+
* seconds: a `TypeError`, as any other malformed duration.
|
|
30
|
+
*/
|
|
31
|
+
export declare function requireFresh(ctx: {
|
|
32
|
+
readonly janus: {
|
|
33
|
+
user(): Promise<unknown>;
|
|
34
|
+
session(): Promise<Session | null>;
|
|
35
|
+
};
|
|
36
|
+
}, maxAge: Exclude<Duration, number>): Promise<Session>;
|
|
37
|
+
//# sourceMappingURL=fresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fresh.d.ts","sourceRoot":"","sources":["../src/fresh.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAEN,KAAK,QAAQ,EAEb,KAAK,OAAO,EACZ,MAAM,aAAa,CAAC;AAIrB;;;;;GAKG;AACH,wBAAsB,YAAY,CACjC,KAAK,EAAE;IAAE,OAAO,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAA;CAAE,EAC7C,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC,CASlB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CACjC,GAAG,EAAE;IACJ,QAAQ,CAAC,KAAK,EAAE;QACf,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;QACzB,OAAO,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;KACnC,CAAC;CACF,EACD,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,OAAO,CAAC,CAQlB"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,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? })` —
|
|
5
|
-
* envelop plugin: `ctx.janus` on every request, and the directives —
|
|
6
|
-
* `@authenticated`, `@permission` — applied to the schema;
|
|
4
|
+
* - `useJanus({ auth, access?, type?, clock?, loaders?, conditions? })` —
|
|
5
|
+
* the envelop plugin: `ctx.janus` on every request, and the directives —
|
|
6
|
+
* `@authenticated`, `@fresh`, `@permission` — applied to the schema;
|
|
7
7
|
* - `janusTypeDefs` — the directives' SDL, also shipped as
|
|
8
8
|
* `graphql/janus.graphqls`;
|
|
9
9
|
* - `applyJanusDirectives(schema, { auth })` — the schema transform alone;
|
|
10
|
-
* - `requireUser(ctx, { type? })
|
|
11
|
-
* calls a resolver makes itself;
|
|
10
|
+
* - `requireUser(ctx, { type? })`, `requireFresh(ctx, maxAge)` and
|
|
11
|
+
* `can(ctx, permission, object)` — the calls a resolver makes itself;
|
|
12
12
|
* - `janusMaskError(fallback?)` — Yoga's `maskError`: a `JanusError` as its
|
|
13
13
|
* code and status, `STORE_FAILED` as 503 and never as 401 or 403.
|
|
14
14
|
* - `janusGraphQLError(error)` — a `JanusError` as the `GraphQLError` the
|
|
@@ -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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
523
|
-
|
|
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=
|
|
725
|
+
//# debugId=50FC1B1149A482D964756E2164756E21
|
|
645
726
|
//# sourceMappingURL=index.js.map
|