@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 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` and `CODE_INVALID` `attemptsLeft`; nothing
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 not sent back as a cookie.** `authenticate` renews
233
- a sliding session in passing, and nothing here sets `Set-Cookie` on the
234
- GraphQL response: a browser keeps the cookie with its old expiry. Sign in
235
- and renew through your HTTP routes, or send a bearer token.
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-five plausible mistakes are refused by the compiler, each with a
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
- - five in `plugin.ts`: `useJanus()` given a user type the instance does not
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`, and `applyJanusDirectives()` without `auth` or narrowed
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.
@@ -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;AAa9C,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IACnC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,WAAW,GAAG,SAAS,EAChC,OAAO,EAAE,cAAc,GACrB,KAAK,CAoCP;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAE5C;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,UAAQ,GAAG,KAAK,GAAG,IAAI,CAMlE;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAQ3D"}
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`. Never
16
- * `reason`, `login`, `slot` or a cause: those are for your logs.
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({ schema, plugins, maskedErrors: { maskError: janusMaskError(maskError) } });
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(fallback?: MaskError): MaskError;
68
+ export declare function janusMaskError(options?: JanusMaskErrorOptions | MaskError): MaskError;
44
69
  //# sourceMappingURL=errors.d.ts.map
@@ -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;AAEvC,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;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,YAAY,CAQjE;AA4CD;;;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;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,QAAQ,GAAE,SAAkB,GAAG,SAAS,CAiBtE"}
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 `JanusError` as its
13
- * code and status, `STORE_FAILED` as 503 and never as 401 or 403.
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';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;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,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"}
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 GraphQLError(message ?? fallback, {
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 = statusOf(error.code);
213
+ const status = statusOf2(error.code);
180
214
  const code = error.code === "STORE_FAILED" ? "SERVICE_UNAVAILABLE" : error.code;
181
- return new GraphQLError(messageOf(status), {
215
+ return new GraphQLError2(messageOf(status), {
182
216
  originalError: error,
183
- extensions: { code, http: { status }, ...actionable(error) }
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 JanusError ? janusGraphQLError(error) : error;
257
+ throw error instanceof JanusError2 ? janusGraphQLError(error) : error;
219
258
  }
220
- function janusMaskError(fallback = masked) {
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
- const original = error instanceof GraphQLError ? error.originalError : error;
223
- if (original instanceof JanusError) {
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 GraphQLError ? new GraphQLError(answer.message, {
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 GraphQLError) {
278
+ if (error instanceof GraphQLError2) {
237
279
  const original = error.originalError;
238
- if (original === undefined || original instanceof GraphQLError) {
280
+ if (original === undefined || original instanceof GraphQLError2) {
239
281
  return error;
240
282
  }
241
- return new GraphQLError(message, {
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 GraphQLError(message, {
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=C6BF0BFA8BF13A9C64756E2164756E21
873
+ //# debugId=7B97AB6C768D830E64756E2164756E21
792
874
  //# sourceMappingURL=index.js.map