@nxgt/janus-graphql 0.3.0 → 0.5.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 +40 -12
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/errors.d.ts +30 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +97 -15
- package/dist/index.js.map +8 -6
- package/dist/plugin.d.ts +13 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/renewal.d.ts +30 -0
- package/dist/renewal.d.ts.map +1 -0
- package/dist/report.d.ts +22 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/types.d.ts +6 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +3 -3
- package/docs/guide/context.md +30 -2
- package/docs/guide/errors.md +85 -5
- package/docs/guide/subscriptions.md +5 -1
- package/docs/roadmap.md +16 -4
- package/docs/troubleshooting.md +55 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ that reads files — `node_modules/@nxgt/janus-graphql/graphql/janus.graphqls`.
|
|
|
79
79
|
|
|
80
80
|
| Export | What it is |
|
|
81
81
|
| --- | --- |
|
|
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` |
|
|
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`. Under Yoga, a session `authenticate` renewed is sent again as `Set-Cookie` on the response, to a request that presented it as the cookie |
|
|
83
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 |
|
|
84
84
|
| `ctx.janus.access` | The `permissions()` instance given to `useJanus()`. Absent from the type — and from the context — without one |
|
|
85
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` |
|
|
@@ -93,11 +93,11 @@ that reads files — `node_modules/@nxgt/janus-graphql/graphql/janus.graphqls`.
|
|
|
93
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` |
|
|
94
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
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 |
|
|
96
|
-
| `janusMaskError(fallback?)` | Yoga's `maskedErrors.maskError`: a `JanusError` a resolver let through answered with its code and status; anything else to `fallback` |
|
|
96
|
+
| `janusMaskError({ report?, fallback? })` | Yoga's `maskedErrors.maskError`: a `JanusError` a resolver let through answered with its code and status; anything else to `fallback`. `report` is called once with every `JanusError` answered 5xx — a directive's and a helper's too — and cannot change the answer. `janusMaskError(fallback)`, the fallback alone, still works |
|
|
97
97
|
| `janusGraphQLError(error)` | A `JanusError` as the `GraphQLError` the client reads: its code, its status, and only what the client can act on |
|
|
98
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 |
|
|
99
99
|
| `JanusContext<typeof auth, typeof access?, Type?>` | What `useJanus()` adds to the context, for your resolvers' `ctx` |
|
|
100
|
-
| `JanusOptions`, `JanusOnContext`, `JanusConnectionOptions`, `JanusConnection`, `ConnectionContext`, `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`, `JanusMaskErrorOptions` | The types of the arguments and answers above |
|
|
101
101
|
|
|
102
102
|
## Directives
|
|
103
103
|
|
|
@@ -207,11 +207,32 @@ every one carries a `code` and the HTTP status Yoga answers with:
|
|
|
207
207
|
|
|
208
208
|
The status sits in `extensions.http.status`, which Yoga reads to answer the
|
|
209
209
|
HTTP response and strips from the body. `USER_INVALID` adds `issues`,
|
|
210
|
-
`PASSWORD_TOO_SHORT` `minLength
|
|
210
|
+
`PASSWORD_TOO_SHORT` `minLength`, `CODE_INVALID` `attemptsLeft`, and a
|
|
211
|
+
throttled `CREDENTIALS_INVALID` `retryAfter` — with a `Retry-After` header in
|
|
212
|
+
`extensions.http.headers`, which Yoga answers with; nothing
|
|
211
213
|
else — never `reason`, `login`, a hash prefix or a cause. The message is a
|
|
212
214
|
fixed one per status, never the core's: read `code`. Detail, and the status of a
|
|
213
215
|
response with several errors: [the errors guide](docs/guide/errors.md).
|
|
214
216
|
|
|
217
|
+
The message carries nothing of the store's, and a directive answers an
|
|
218
|
+
outage itself, before any resolver of yours could log it. `report` sees
|
|
219
|
+
every `JanusError` answered 5xx, once, whichever path it took:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { maskError } from 'graphql-yoga';
|
|
223
|
+
|
|
224
|
+
createYoga({
|
|
225
|
+
schema,
|
|
226
|
+
plugins: [useJanus({ auth, access })],
|
|
227
|
+
maskedErrors: {
|
|
228
|
+
maskError: janusMaskError({
|
|
229
|
+
report: (error) => logger.error({ code: error.code, cause: error.cause }), // slot, operation: which call failed
|
|
230
|
+
fallback: maskError, // Yoga's own, for everything else
|
|
231
|
+
}),
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
215
236
|
## Traps
|
|
216
237
|
|
|
217
238
|
- **Without `janusMaskError()`, a `JanusError` from a resolver is Yoga's
|
|
@@ -229,10 +250,16 @@ response with several errors: [the errors guide](docs/guide/errors.md).
|
|
|
229
250
|
the highest `extensions.http.status` among the errors, so `{ open me }`
|
|
230
251
|
from an anonymous request is a 401 whose `data` still holds `open`. A client
|
|
231
252
|
reads `data` and `errors`, not only the status.
|
|
232
|
-
- **A renewed session is
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
253
|
+
- **A renewed session is sent back only on an HTTP response from Yoga.**
|
|
254
|
+
`useJanus()` sets it in Yoga's `onResponse`, which a server on envelop
|
|
255
|
+
alone never calls; and an operation over graphql-ws has no response at
|
|
256
|
+
all. There, `ctx.janus` still renews the session in the store, but the
|
|
257
|
+
browser keeps the cookie with its old expiry until the next renewal over
|
|
258
|
+
HTTP, a `renewAfter` later: a browser whose only traffic is the WebSocket
|
|
259
|
+
should renew through an HTTP route of yours.
|
|
260
|
+
- **An `auth` of your own needs `cookie` for the renewal.** A wrapper that
|
|
261
|
+
passes on only `authenticate` and `types` sends no renewed cookie; pass
|
|
262
|
+
`cookie: auth.cookie` along.
|
|
236
263
|
- **`useJanus({ type })` narrows the whole server.** A user of any other type
|
|
237
264
|
is anonymous everywhere, and a directive naming another type is refused
|
|
238
265
|
when the schema is built.
|
|
@@ -297,7 +324,7 @@ response with several errors: [the errors guide](docs/guide/errors.md).
|
|
|
297
324
|
|
|
298
325
|
## Type safety, counted
|
|
299
326
|
|
|
300
|
-
Thirty-
|
|
327
|
+
Thirty-seven plausible mistakes are refused by the compiler, each with a
|
|
301
328
|
`@ts-expect-error` case in `test/types/`:
|
|
302
329
|
|
|
303
330
|
- five in `context.ts`: reading the user or the session where either may be
|
|
@@ -310,10 +337,11 @@ Thirty-five plausible mistakes are refused by the compiler, each with a
|
|
|
310
337
|
permission the object's type does not declare, an object type the model
|
|
311
338
|
does not, an object without a field a `fromField` reads, a condition reached
|
|
312
339
|
with no `ctx`, a `ctx` of the wrong shape, and a context with no `access`;
|
|
313
|
-
-
|
|
340
|
+
- seven in `plugin.ts`: `useJanus()` given a user type the instance does not
|
|
314
341
|
know, something that is not what `janus()` answered, the `permissions()`
|
|
315
|
-
instance as `auth`,
|
|
316
|
-
to a user type the instance does not know
|
|
342
|
+
instance as `auth`, `applyJanusDirectives()` without `auth` or narrowed
|
|
343
|
+
to a user type the instance does not know, and `janusMaskError()` given a
|
|
344
|
+
misspelled `report` or one reading what a `JanusError` does not carry;
|
|
317
345
|
- five in `wiring.ts`: `useJanus()`'s `loaders` for an object type the model
|
|
318
346
|
does not declare, a loader answering an object without a field a
|
|
319
347
|
`fromField` reads, `conditions` for a type no `when()` is reached on, a
|
package/dist/context.d.ts
CHANGED
|
@@ -48,6 +48,12 @@ export interface ContextOptions {
|
|
|
48
48
|
* here asked.
|
|
49
49
|
*/
|
|
50
50
|
export declare function createJanusContext(request: RequestLike | undefined, options: ContextOptions): Built;
|
|
51
|
+
/**
|
|
52
|
+
* What `authenticate` answered for `janus`'s request, or `undefined` when it
|
|
53
|
+
* never ran — nothing asked `user()` or `session()`, or the context was not
|
|
54
|
+
* built here. Never runs it.
|
|
55
|
+
*/
|
|
56
|
+
export declare function answerOf(janus: object): Promise<Authenticated<AnyUser> | null> | undefined;
|
|
51
57
|
/**
|
|
52
58
|
* The clock `@fresh` and `requireFresh()` read the time from: the one given
|
|
53
59
|
* to `useJanus({ clock })`, or the system's.
|
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,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;
|
|
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;AAuB9C,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,CAqCP;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CACvB,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,SAAS,CAEpD;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"}
|
package/dist/errors.d.ts
CHANGED
|
@@ -12,8 +12,10 @@ export type DenialCode = 'UNAUTHENTICATED' | 'FORBIDDEN' | 'NOT_FOUND';
|
|
|
12
12
|
export declare function denial(code: DenialCode, message?: string): GraphQLError;
|
|
13
13
|
/**
|
|
14
14
|
* A `JanusError` as the client may read it: its code and status, and only
|
|
15
|
-
* what the client can act on — `issues`, `minLength`, `attemptsLeft
|
|
16
|
-
* `reason`, `login`, `slot` or a cause: those are for
|
|
15
|
+
* what the client can act on — `issues`, `minLength`, `attemptsLeft`,
|
|
16
|
+
* `retryAfter`. Never `reason`, `login`, `slot` or a cause: those are for
|
|
17
|
+
* your logs. A throttled sign-in also carries its `Retry-After` header in
|
|
18
|
+
* `http.headers`, which Yoga answers with.
|
|
17
19
|
*
|
|
18
20
|
* `STORE_FAILED` is `SERVICE_UNAVAILABLE` 503 — **an outage is never a
|
|
19
21
|
* denial** — and no message of the core's reaches the client.
|
|
@@ -26,6 +28,22 @@ export declare function janusGraphQLError(error: JanusError): GraphQLError;
|
|
|
26
28
|
export declare function rethrown(error: unknown): never;
|
|
27
29
|
/** Yoga's and envelop's `maskError` signature. */
|
|
28
30
|
export type MaskError = (error: unknown, message: string, isDev?: boolean) => Error;
|
|
31
|
+
/** What `janusMaskError()` takes. */
|
|
32
|
+
export interface JanusMaskErrorOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Called once with every `JanusError` answered 5xx — `STORE_FAILED`,
|
|
35
|
+
* `UNSUPPORTED`, `PERMISSION_DEPTH` — whichever path it took: a
|
|
36
|
+
* directive, `requireUser()`, `can()`, or a resolver that let it through.
|
|
37
|
+
* Its `slot`, `operation`, `reason` and cause are for your logs; the
|
|
38
|
+
* response never carries them.
|
|
39
|
+
*
|
|
40
|
+
* **It cannot change the answer**: one that throws, or rejects, is a
|
|
41
|
+
* `process.emitWarning`, and the 503 is sent all the same.
|
|
42
|
+
*/
|
|
43
|
+
readonly report?: (error: JanusError) => unknown;
|
|
44
|
+
/** Every error that is not a `JanusError`; see `janusMaskError()`. */
|
|
45
|
+
readonly fallback?: MaskError;
|
|
46
|
+
}
|
|
29
47
|
/**
|
|
30
48
|
* Yoga's `maskedErrors.maskError`: a `JanusError` a resolver let through is
|
|
31
49
|
* answered with its code and status — `STORE_FAILED` as 503, never masked
|
|
@@ -33,12 +51,19 @@ export type MaskError = (error: unknown, message: string, isDev?: boolean) => Er
|
|
|
33
51
|
*
|
|
34
52
|
* Without a `fallback`, a `GraphQLError` is kept and anything else becomes
|
|
35
53
|
* `message`, with no detail. Pass Yoga's own `maskError` to keep its
|
|
36
|
-
* development-mode details
|
|
54
|
+
* development-mode details — as the options' `fallback`, or alone, as
|
|
55
|
+
* before `report` existed:
|
|
37
56
|
*
|
|
38
57
|
* ```ts
|
|
39
58
|
* import { createYoga, maskError } from 'graphql-yoga';
|
|
40
|
-
* createYoga({
|
|
59
|
+
* createYoga({
|
|
60
|
+
* schema,
|
|
61
|
+
* plugins,
|
|
62
|
+
* maskedErrors: {
|
|
63
|
+
* maskError: janusMaskError({ report: (error) => logger.error(error), fallback: maskError }),
|
|
64
|
+
* },
|
|
65
|
+
* });
|
|
41
66
|
* ```
|
|
42
67
|
*/
|
|
43
|
-
export declare function janusMaskError(
|
|
68
|
+
export declare function janusMaskError(options?: JanusMaskErrorOptions | MaskError): MaskError;
|
|
44
69
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAmC,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAmC,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGvC,mFAAmF;AACnF,MAAM,MAAM,UAAU,GAAG,iBAAiB,GAAG,WAAW,GAAG,WAAW,CAAC;AAEvE,2EAA2E;AAC3E,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,YAAY,CAKvE;AAaD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,YAAY,CAQjE;AA0DD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAE9C;AAED,kDAAkD;AAClD,MAAM,MAAM,SAAS,GAAG,CACvB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO,KACX,KAAK,CAAC;AAEX,qCAAqC;AACrC,MAAM,WAAW,qBAAqB;IACrC;;;;;;;;;OASG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC;IACjD,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAC7B,OAAO,GAAE,qBAAqB,GAAG,SAAc,GAC7C,SAAS,CAqBX"}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* - `applyJanusDirectives(schema, { auth })` — the schema transform alone;
|
|
10
10
|
* - `requireUser(ctx, { type? })`, `requireFresh(ctx, maxAge)` and
|
|
11
11
|
* `can(ctx, permission, object)` — the calls a resolver makes itself;
|
|
12
|
-
* - `janusMaskError(fallback?)` — Yoga's `maskError`: a
|
|
13
|
-
* code and status, `STORE_FAILED` as 503 and never as
|
|
12
|
+
* - `janusMaskError({ report?, fallback? })` — Yoga's `maskError`: a
|
|
13
|
+
* `JanusError` as its code and status, `STORE_FAILED` as 503 and never as
|
|
14
|
+
* 401 or 403, and every 5xx handed to `report`.
|
|
14
15
|
* - `janusGraphQLError(error)` — a `JanusError` as the `GraphQLError` the
|
|
15
16
|
* client reads, and `denial(code, message?)` — a denial of your own.
|
|
16
17
|
* - `janusConnection({ auth, access?, type?, clock?, upgrade? })` —
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
export { janusConnection } from './connection/connection';
|
|
25
26
|
export type { ConnectionContext, JanusConnection, JanusConnectionOptions, } from './connection/types';
|
|
26
27
|
export { applyJanusDirectives, type JanusDirectivesOptions, } from './directives/apply';
|
|
27
|
-
export { type DenialCode, denial, janusGraphQLError, janusMaskError, type MaskError, } from './errors';
|
|
28
|
+
export { type DenialCode, denial, type JanusMaskErrorOptions, janusGraphQLError, janusMaskError, type MaskError, } from './errors';
|
|
28
29
|
export { requireFresh } from './fresh';
|
|
29
30
|
export { can, type RequireUserOptions, requireUser } from './helpers';
|
|
30
31
|
export { useJanus } from './plugin';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EACX,iBAAiB,EACjB,eAAe,EACf,sBAAsB,GACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACN,oBAAoB,EACpB,KAAK,sBAAsB,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACN,KAAK,UAAU,EACf,MAAM,EACN,KAAK,qBAAqB,EAC1B,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
|
@@ -34,6 +34,7 @@ function memoized(access) {
|
|
|
34
34
|
// src/context.ts
|
|
35
35
|
var checks = new WeakMap;
|
|
36
36
|
var clocks = new WeakMap;
|
|
37
|
+
var answers = new WeakMap;
|
|
37
38
|
function createJanusContext(request, options) {
|
|
38
39
|
const { auth, access, type, clock, caller = "useJanus()" } = options;
|
|
39
40
|
let answer;
|
|
@@ -59,8 +60,12 @@ function createJanusContext(request, options) {
|
|
|
59
60
|
}
|
|
60
61
|
if (clock !== undefined)
|
|
61
62
|
clocks.set(janus, clock);
|
|
63
|
+
answers.set(janus, () => asked ? answer : undefined);
|
|
62
64
|
return janus;
|
|
63
65
|
}
|
|
66
|
+
function answerOf(janus) {
|
|
67
|
+
return answers.get(janus)?.();
|
|
68
|
+
}
|
|
64
69
|
function clockOf(janus) {
|
|
65
70
|
return clocks.get(janus) ?? systemClock;
|
|
66
71
|
}
|
|
@@ -157,11 +162,40 @@ import { MapperKind, mapSchema } from "@graphql-tools/utils";
|
|
|
157
162
|
import { defaultFieldResolver } from "graphql";
|
|
158
163
|
|
|
159
164
|
// src/errors.ts
|
|
165
|
+
import { JanusError as JanusError2, statusOf as statusOf2 } from "@nxgt/janus";
|
|
166
|
+
import { GraphQLError as GraphQLError2 } from "graphql";
|
|
167
|
+
|
|
168
|
+
// src/report.ts
|
|
160
169
|
import { JanusError, statusOf } from "@nxgt/janus";
|
|
161
170
|
import { GraphQLError } from "graphql";
|
|
171
|
+
function janusErrorIn(error) {
|
|
172
|
+
let current = error;
|
|
173
|
+
while (current instanceof GraphQLError)
|
|
174
|
+
current = current.originalError;
|
|
175
|
+
return current instanceof JanusError ? current : null;
|
|
176
|
+
}
|
|
177
|
+
function reporting(report) {
|
|
178
|
+
const seen = new WeakSet;
|
|
179
|
+
return (error) => {
|
|
180
|
+
const janusError = janusErrorIn(error);
|
|
181
|
+
if (janusError === null || seen.has(janusError))
|
|
182
|
+
return;
|
|
183
|
+
if (statusOf(janusError.code) < 500)
|
|
184
|
+
return;
|
|
185
|
+
seen.add(janusError);
|
|
186
|
+
const warn = (failure) => process.emitWarning(`janusMaskError: report failed on ${janusError.code}: ${failure instanceof Error ? failure.name : typeof failure}`);
|
|
187
|
+
try {
|
|
188
|
+
Promise.resolve(report(janusError)).then(undefined, warn);
|
|
189
|
+
} catch (failure) {
|
|
190
|
+
warn(failure);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/errors.ts
|
|
162
196
|
function denial(code, message) {
|
|
163
197
|
const [status, fallback] = denialOf(code);
|
|
164
|
-
return new
|
|
198
|
+
return new GraphQLError2(message ?? fallback, {
|
|
165
199
|
extensions: { code, http: { status } }
|
|
166
200
|
});
|
|
167
201
|
}
|
|
@@ -176,11 +210,11 @@ function denialOf(code) {
|
|
|
176
210
|
}
|
|
177
211
|
}
|
|
178
212
|
function janusGraphQLError(error) {
|
|
179
|
-
const status =
|
|
213
|
+
const status = statusOf2(error.code);
|
|
180
214
|
const code = error.code === "STORE_FAILED" ? "SERVICE_UNAVAILABLE" : error.code;
|
|
181
|
-
return new
|
|
215
|
+
return new GraphQLError2(messageOf(status), {
|
|
182
216
|
originalError: error,
|
|
183
|
-
extensions: { code, http:
|
|
217
|
+
extensions: { code, http: httpOf(error, status), ...actionable(error) }
|
|
184
218
|
});
|
|
185
219
|
}
|
|
186
220
|
function messageOf(status) {
|
|
@@ -202,6 +236,9 @@ function messageOf(status) {
|
|
|
202
236
|
return "Internal server error";
|
|
203
237
|
}
|
|
204
238
|
}
|
|
239
|
+
function httpOf(error, status) {
|
|
240
|
+
return error.retryAfter === undefined ? { status } : { status, headers: { "Retry-After": String(error.retryAfter) } };
|
|
241
|
+
}
|
|
205
242
|
function actionable(error) {
|
|
206
243
|
switch (error.code) {
|
|
207
244
|
case "USER_INVALID":
|
|
@@ -210,19 +247,24 @@ function actionable(error) {
|
|
|
210
247
|
return error.minLength === undefined ? {} : { minLength: error.minLength };
|
|
211
248
|
case "CODE_INVALID":
|
|
212
249
|
return error.attemptsLeft === undefined ? {} : { attemptsLeft: error.attemptsLeft };
|
|
250
|
+
case "CREDENTIALS_INVALID":
|
|
251
|
+
return error.retryAfter === undefined ? {} : { retryAfter: error.retryAfter };
|
|
213
252
|
default:
|
|
214
253
|
return {};
|
|
215
254
|
}
|
|
216
255
|
}
|
|
217
256
|
function rethrown(error) {
|
|
218
|
-
throw error instanceof
|
|
257
|
+
throw error instanceof JanusError2 ? janusGraphQLError(error) : error;
|
|
219
258
|
}
|
|
220
|
-
function janusMaskError(
|
|
259
|
+
function janusMaskError(options = {}) {
|
|
260
|
+
const { report, fallback = masked } = typeof options === "function" ? { fallback: options } : options;
|
|
261
|
+
const reported = report === undefined ? undefined : reporting(report);
|
|
221
262
|
return (error, message, isDev) => {
|
|
222
|
-
|
|
223
|
-
|
|
263
|
+
reported?.(error);
|
|
264
|
+
const original = error instanceof GraphQLError2 ? error.originalError : error;
|
|
265
|
+
if (original instanceof JanusError2) {
|
|
224
266
|
const answer = janusGraphQLError(original);
|
|
225
|
-
return error instanceof
|
|
267
|
+
return error instanceof GraphQLError2 ? new GraphQLError2(answer.message, {
|
|
226
268
|
nodes: error.nodes ?? null,
|
|
227
269
|
path: error.path ?? null,
|
|
228
270
|
originalError: original,
|
|
@@ -233,18 +275,18 @@ function janusMaskError(fallback = masked) {
|
|
|
233
275
|
};
|
|
234
276
|
}
|
|
235
277
|
function masked(error, message) {
|
|
236
|
-
if (error instanceof
|
|
278
|
+
if (error instanceof GraphQLError2) {
|
|
237
279
|
const original = error.originalError;
|
|
238
|
-
if (original === undefined || original instanceof
|
|
280
|
+
if (original === undefined || original instanceof GraphQLError2) {
|
|
239
281
|
return error;
|
|
240
282
|
}
|
|
241
|
-
return new
|
|
283
|
+
return new GraphQLError2(message, {
|
|
242
284
|
nodes: error.nodes ?? null,
|
|
243
285
|
path: error.path ?? null,
|
|
244
286
|
extensions: { code: "INTERNAL_SERVER_ERROR" }
|
|
245
287
|
});
|
|
246
288
|
}
|
|
247
|
-
return new
|
|
289
|
+
return new GraphQLError2(message, {
|
|
248
290
|
extensions: { code: "INTERNAL_SERVER_ERROR" }
|
|
249
291
|
});
|
|
250
292
|
}
|
|
@@ -698,6 +740,42 @@ async function can(ctx, permission, object, ...options) {
|
|
|
698
740
|
const subject = await janus.user().then(undefined, rethrown);
|
|
699
741
|
return check(subject, permission, object, options[0]).then(undefined, rethrown);
|
|
700
742
|
}
|
|
743
|
+
// src/renewal.ts
|
|
744
|
+
var contexts = new WeakMap;
|
|
745
|
+
function track(request, janus) {
|
|
746
|
+
const known = contexts.get(request);
|
|
747
|
+
if (known === undefined)
|
|
748
|
+
contexts.set(request, [janus]);
|
|
749
|
+
else
|
|
750
|
+
known.push(janus);
|
|
751
|
+
}
|
|
752
|
+
async function resendRenewed({ request, response }, cookie) {
|
|
753
|
+
const known = contexts.get(request);
|
|
754
|
+
if (cookie === undefined || known === undefined)
|
|
755
|
+
return;
|
|
756
|
+
for (const janus of known) {
|
|
757
|
+
const current = await answerOf(janus)?.then((answer) => answer, () => null);
|
|
758
|
+
if (current?.renewed === true && cookieOf(request, cookie.name) === current.token && !sets(response, cookie.name)) {
|
|
759
|
+
response.headers.append("Set-Cookie", cookie.serialize(current.token, current.session));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function cookieOf(request, name) {
|
|
764
|
+
const header = request.headers.get("cookie");
|
|
765
|
+
if (header === null)
|
|
766
|
+
return null;
|
|
767
|
+
for (const pair of header.split(";")) {
|
|
768
|
+
const at = pair.indexOf("=");
|
|
769
|
+
if (at !== -1 && pair.slice(0, at).trim() === name) {
|
|
770
|
+
return pair.slice(at + 1).trim();
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
function sets(response, name) {
|
|
776
|
+
return response.headers.getSetCookie().some((value) => value.startsWith(`${name}=`));
|
|
777
|
+
}
|
|
778
|
+
|
|
701
779
|
// src/plugin.ts
|
|
702
780
|
function useJanus(options) {
|
|
703
781
|
const { auth, access, type, clock, loaders, conditions } = options;
|
|
@@ -712,6 +790,9 @@ function useJanus(options) {
|
|
|
712
790
|
type,
|
|
713
791
|
clock
|
|
714
792
|
});
|
|
793
|
+
if (typeof request === "object" && request !== null) {
|
|
794
|
+
track(request, janus);
|
|
795
|
+
}
|
|
715
796
|
extendContext({ janus });
|
|
716
797
|
},
|
|
717
798
|
onSchemaChange: ({ schema, replaceSchema }) => {
|
|
@@ -726,7 +807,8 @@ function useJanus(options) {
|
|
|
726
807
|
});
|
|
727
808
|
applied.add(next);
|
|
728
809
|
replaceSchema(next);
|
|
729
|
-
}
|
|
810
|
+
},
|
|
811
|
+
onResponse: (payload) => resendRenewed(payload, auth.cookie)
|
|
730
812
|
};
|
|
731
813
|
}
|
|
732
814
|
// src/sdl.ts
|
|
@@ -788,5 +870,5 @@ export {
|
|
|
788
870
|
useJanus
|
|
789
871
|
};
|
|
790
872
|
|
|
791
|
-
//# debugId=
|
|
873
|
+
//# debugId=7B97AB6C768D830E64756E2164756E21
|
|
792
874
|
//# sourceMappingURL=index.js.map
|