@nxgt/janus-graphql 0.2.0 → 0.4.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
@@ -3,9 +3,9 @@
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
5
  asks, fields and types guarded by `@authenticated`, `@fresh` and
6
- `@permission`, and
7
- every error answered with the status it deserves — an outage as 503, never
8
- as 401, 403 or 404.
6
+ `@permission`, the
7
+ same guards on subscriptions over graphql-ws, and every error answered with
8
+ the status it deserves — an outage as 503, never as 401, 403 or 404.
9
9
 
10
10
  An [envelop](https://the-guild.dev/graphql/envelop) plugin, so it runs in
11
11
  [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) and any server built
@@ -52,7 +52,7 @@ bun add @nxgt/janus-graphql @nxgt/janus graphql @graphql-tools/utils @envelop/co
52
52
  bun add -d typescript # 6
53
53
  ```
54
54
 
55
- Every peer is required:
55
+ Every peer is required but `graphql-ws`:
56
56
 
57
57
  | Peer | Range | Why |
58
58
  | --- | --- | --- |
@@ -61,11 +61,12 @@ Every peer is required:
61
61
  | `@graphql-tools/utils` | `>=10.0.0 <13` | `mapSchema` and `getDirective`, which apply the directives |
62
62
  | `@envelop/core` | `^5.0.0` | Types only — the `Plugin` `useJanus()` answers. Yoga already brings it |
63
63
  | `typescript` | `^6.0.3` | As for `@nxgt/janus` |
64
+ | `graphql-ws` | `^6.0.0`, **optional** | Only for subscriptions over a WebSocket, with `janusConnection()`. Nothing here imports it. For `graphql` 17, a `graphql-ws` whose peer range includes it — 6.3.0 does |
64
65
 
65
66
  The floors are tested, not claimed: the package's specs and its typecheck run
66
- on `graphql` 16.9.0, `@graphql-tools/utils` 10.0.0 and `@envelop/core` 5.0.0
67
- as well, together, with a single copy of `graphql`, in the Floors job, on
68
- every CI run.
67
+ on `graphql` 16.9.0, `@graphql-tools/utils` 10.0.0, `@envelop/core` 5.0.0
68
+ and `graphql-ws` 6.0.0 as well, together, with a single copy of `graphql`,
69
+ in the Floors job, on every CI run.
69
70
 
70
71
  Like `@nxgt/janus`, it expects `"moduleResolution": "bundler"`: the
71
72
  declarations import without extensions, so `nodenext` is not supported.
@@ -78,7 +79,7 @@ that reads files — `node_modules/@nxgt/janus-graphql/graphql/janus.graphqls`.
78
79
 
79
80
  | Export | What it is |
80
81
  | --- | --- |
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` |
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 |
82
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 |
83
84
  | `ctx.janus.access` | The `permissions()` instance given to `useJanus()`. Absent from the type — and from the context — without one |
84
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` |
@@ -91,11 +92,12 @@ that reads files — `node_modules/@nxgt/janus-graphql/graphql/janus.graphqls`.
91
92
  | `requireUser(ctx, { type? })` | The signed-in user, narrowed to `type` — one or a non-empty list — or a denial: `UNAUTHENTICATED`, `FORBIDDEN` |
92
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` |
93
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 |
94
- | `janusMaskError(fallback?)` | Yoga's `maskedErrors.maskError`: a `JanusError` a resolver let through answered with its code and status; anything else to `fallback` |
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({ 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 |
95
97
  | `janusGraphQLError(error)` | A `JanusError` as the `GraphQLError` the client reads: its code, its status, and only what the client can act on |
96
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 |
97
99
  | `JanusContext<typeof auth, typeof access?, Type?>` | What `useJanus()` adds to the context, for your resolvers' `ctx` |
98
- | `JanusOptions`, `JanusOnContext`, `JanusDirectivesOptions`, `Loaders`, `Conditions`, `LoadedObject`, `RequireUserOptions`, `Auth`, `UserOfAuth`, `DenialCode`, `MaskError` | The types of the arguments and answers above |
100
+ | `JanusOptions`, `JanusOnContext`, `JanusConnectionOptions`, `JanusConnection`, `ConnectionContext`, `JanusDirectivesOptions`, `Loaders`, `Conditions`, `LoadedObject`, `RequireUserOptions`, `Auth`, `UserOfAuth`, `DenialCode`, `MaskError`, `JanusMaskErrorOptions` | The types of the arguments and answers above |
99
101
 
100
102
  ## Directives
101
103
 
@@ -145,6 +147,46 @@ model does not declare, a malformed `id:`, an argument the field does not
145
147
  take, a missing loader or condition, a `maxAge` not above zero — is a
146
148
  `TypeError` at start-up naming the field. Detail: [the directives guide](docs/guide/directives.md).
147
149
 
150
+ ## Subscriptions over graphql-ws
151
+
152
+ Yoga's recommended graphql-ws setup, with `onConnect` added:
153
+
154
+ ```ts
155
+ import { janusConnection } from '@nxgt/janus-graphql';
156
+ import { useServer } from 'graphql-ws/use/ws';
157
+
158
+ useServer(
159
+ {
160
+ onConnect: janusConnection({ auth }).onConnect, // no valid session: closed 4403
161
+ execute: (args: any) => args.rootValue.execute(args),
162
+ subscribe: (args: any) => args.rootValue.subscribe(args),
163
+ onSubscribe: async (ctx, _id, params) => {
164
+ const { schema, execute, subscribe, contextFactory, parse, validate } =
165
+ yoga.getEnveloped({ ...ctx, req: ctx.extra.request, socket: ctx.extra.socket, params });
166
+ const args = {
167
+ schema,
168
+ operationName: params.operationName,
169
+ document: parse(params.query),
170
+ variableValues: params.variables,
171
+ contextValue: await contextFactory(), // useJanus() finds the connection by ctx.extra
172
+ rootValue: { execute, subscribe },
173
+ };
174
+ const errors = validate(args.schema, args.document);
175
+ return errors.length ? errors : args;
176
+ },
177
+ },
178
+ wsServer,
179
+ );
180
+ ```
181
+
182
+ A browser's session cookie travels on the upgrade request; any other client
183
+ sends `connectionParams: { authorization: 'Bearer <token>' }`, which is
184
+ read first. Each operation is authenticated again from that credential
185
+ when it subscribes: `@authenticated` and `@fresh` are checked then, and
186
+ `@permission` on every event. A session revoked mid-stream is refused at
187
+ the next subscribe, not at the next event. Detail, Bun, and a server
188
+ without Yoga: [the subscriptions guide](docs/guide/subscriptions.md).
189
+
148
190
  ## Errors
149
191
 
150
192
  This package defines **no error class**. A denial is a `GraphQLError`, and
@@ -170,6 +212,25 @@ else — never `reason`, `login`, a hash prefix or a cause. The message is a
170
212
  fixed one per status, never the core's: read `code`. Detail, and the status of a
171
213
  response with several errors: [the errors guide](docs/guide/errors.md).
172
214
 
215
+ The message carries nothing of the store's, and a directive answers an
216
+ outage itself, before any resolver of yours could log it. `report` sees
217
+ every `JanusError` answered 5xx, once, whichever path it took:
218
+
219
+ ```ts
220
+ import { maskError } from 'graphql-yoga';
221
+
222
+ createYoga({
223
+ schema,
224
+ plugins: [useJanus({ auth, access })],
225
+ maskedErrors: {
226
+ maskError: janusMaskError({
227
+ report: (error) => logger.error({ code: error.code, cause: error.cause }), // slot, operation: which call failed
228
+ fallback: maskError, // Yoga's own, for everything else
229
+ }),
230
+ },
231
+ });
232
+ ```
233
+
173
234
  ## Traps
174
235
 
175
236
  - **Without `janusMaskError()`, a `JanusError` from a resolver is Yoga's
@@ -187,10 +248,16 @@ response with several errors: [the errors guide](docs/guide/errors.md).
187
248
  the highest `extensions.http.status` among the errors, so `{ open me }`
188
249
  from an anonymous request is a 401 whose `data` still holds `open`. A client
189
250
  reads `data` and `errors`, not only the status.
190
- - **A renewed session is not sent back as a cookie.** `authenticate` renews
191
- a sliding session in passing, and nothing here sets `Set-Cookie` on the
192
- GraphQL response: a browser keeps the cookie with its old expiry. Sign in
193
- and renew through your HTTP routes, or send a bearer token.
251
+ - **A renewed session is sent back only on an HTTP response from Yoga.**
252
+ `useJanus()` sets it in Yoga's `onResponse`, which a server on envelop
253
+ alone never calls; and an operation over graphql-ws has no response at
254
+ all. There, `ctx.janus` still renews the session in the store, but the
255
+ browser keeps the cookie with its old expiry until the next renewal over
256
+ HTTP, a `renewAfter` later: a browser whose only traffic is the WebSocket
257
+ should renew through an HTTP route of yours.
258
+ - **An `auth` of your own needs `cookie` for the renewal.** A wrapper that
259
+ passes on only `authenticate` and `types` sends no renewed cookie; pass
260
+ `cookie: auth.cookie` along.
194
261
  - **`useJanus({ type })` narrows the whole server.** A user of any other type
195
262
  is anonymous everywhere, and a directive naming another type is refused
196
263
  when the schema is built.
@@ -218,8 +285,19 @@ response with several errors: [the errors guide](docs/guide/errors.md).
218
285
  on every one, and one denial denies the field; to answer only the items a
219
286
  user may see, ask `access.list()` for their ids.
220
287
  - **A context built without a request cannot authenticate.** `ctx.janus.user()`
221
- rejects with a `TypeError` — a transport that is not HTTP, such as a
222
- WebSocket, needs its own wiring, which is on the [roadmap](docs/roadmap.md).
288
+ rejects with a `TypeError` — a transport that is not HTTP, or a graphql-ws
289
+ operation whose connection no `janusConnection().onConnect` accepted, or
290
+ whose context was built without Yoga's `...ctx` spread, which carries
291
+ `ctx.extra`.
292
+ - **A session revoked while a socket is open keeps its running streams.**
293
+ An operation authenticates once, when it subscribes, and each event's
294
+ `@authenticated` reads that answer; the next subscribe, and a
295
+ reconnection, are refused. Close the
296
+ socket yourself to cut it at once. `@permission` sees a `revoke()` at the
297
+ next event.
298
+ - **Bun's graphql-ws `extra` holds no request.** Without
299
+ `janusConnection({ upgrade })`, a browser's cookie is never read there,
300
+ and only `connectionParams` authenticates.
223
301
  - **`@fresh` reads seconds; `requireFresh()` a duration.** `@fresh(maxAge:
224
302
  10)` is ten seconds, not ten minutes — write `600`. `requireFresh(ctx,
225
303
  '10m')` takes the unit, and refuses a bare number, which `@nxgt/janus`
@@ -238,13 +316,13 @@ response with several errors: [the errors guide](docs/guide/errors.md).
238
316
 
239
317
  ## Documentation
240
318
 
241
- - [Guides](docs/README.md) — the directives, the step-up over GraphQL, the context and the lazy user, errors as statuses, telemetry, and federation
319
+ - [Guides](docs/README.md) — the directives, the step-up over GraphQL, subscriptions over graphql-ws, the context and the lazy user, errors as statuses, telemetry, and federation
242
320
  - [Troubleshooting](docs/troubleshooting.md) — by the message or status you see
243
321
  - [Roadmap](docs/roadmap.md) — what is next, and what is not planned
244
322
 
245
323
  ## Type safety, counted
246
324
 
247
- Twenty-nine plausible mistakes are refused by the compiler, each with a
325
+ Thirty-seven plausible mistakes are refused by the compiler, each with a
248
326
  `@ts-expect-error` case in `test/types/`:
249
327
 
250
328
  - five in `context.ts`: reading the user or the session where either may be
@@ -257,10 +335,11 @@ Twenty-nine plausible mistakes are refused by the compiler, each with a
257
335
  permission the object's type does not declare, an object type the model
258
336
  does not, an object without a field a `fromField` reads, a condition reached
259
337
  with no `ctx`, a `ctx` of the wrong shape, and a context with no `access`;
260
- - five in `plugin.ts`: `useJanus()` given a user type the instance does not
338
+ - seven in `plugin.ts`: `useJanus()` given a user type the instance does not
261
339
  know, something that is not what `janus()` answered, the `permissions()`
262
- instance as `auth`, and `applyJanusDirectives()` without `auth` or narrowed
263
- to a user type the instance does not know;
340
+ instance as `auth`, `applyJanusDirectives()` without `auth` or narrowed
341
+ to a user type the instance does not know, and `janusMaskError()` given a
342
+ misspelled `report` or one reading what a `JanusError` does not carry;
264
343
  - five in `wiring.ts`: `useJanus()`'s `loaders` for an object type the model
265
344
  does not declare, a loader answering an object without a field a
266
345
  `fromField` reads, `conditions` for a type no `when()` is reached on, a
@@ -268,9 +347,16 @@ Twenty-nine plausible mistakes are refused by the compiler, each with a
268
347
  `access`;
269
348
  - five in `fresh.ts`: `requireFresh()` given a bare number, a duration
270
349
  written as prose, no `maxAge` at all, or the request instead of the
271
- context, and `useJanus()` given a timestamp as its `clock`.
272
-
273
- `plugin.ts` also holds this README's quick start, which must keep compiling.
350
+ context, and `useJanus()` given a timestamp as its `clock`;
351
+ - six in `connection.ts`: `janusConnection()` given a user type the
352
+ instance does not know, the `permissions()` instance as `auth`, or a
353
+ timestamp as its `clock`; `ctx.janus.access` from a connection given no
354
+ `access`, a field of another user type once narrowed, and an `upgrade`
355
+ answering a token rather than a request.
356
+
357
+ `plugin.ts` also holds this README's quick start, and `connection.ts` the
358
+ graphql-ws wiring — Yoga's recipe, without Yoga, and Bun's `upgrade` —
359
+ against graphql-ws's own types; both must keep compiling.
274
360
 
275
361
  ## Licence
276
362
 
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `janusConnection()`: authenticated subscriptions over graphql-ws.
3
+ */
4
+ import type { Auth, UserOfAuth } from '../types';
5
+ import type { JanusConnection, JanusConnectionOptions } from './types';
6
+ /**
7
+ * The pieces of graphql-ws's `useServer()` that authenticate a WebSocket
8
+ * connection, for Yoga's recommended setup:
9
+ *
10
+ * ```ts
11
+ * const connection = janusConnection({ auth });
12
+ * useServer({
13
+ * onConnect: connection.onConnect,
14
+ * execute: (args) => args.rootValue.execute(args),
15
+ * subscribe: (args) => args.rootValue.subscribe(args),
16
+ * onSubscribe: async (ctx, _id, params) => { … yoga.getEnveloped({ ...ctx, … }) … },
17
+ * }, wsServer);
18
+ * ```
19
+ *
20
+ * `onConnect` reads the connection's credential — `connectionParams`'
21
+ * `authorization`, else the upgrade request's headers and cookie — and
22
+ * refuses it unless it authenticates, as a user of `type` when it is given.
23
+ * Each operation on the connection then gets a `ctx.janus` of its own from
24
+ * `useJanus()`, authenticated again from that credential when a field asks:
25
+ * `@authenticated`, `@fresh` and `@permission` hold as over HTTP, and a
26
+ * session revoked since the connection opened is refused at the next
27
+ * subscribe.
28
+ *
29
+ * A server without Yoga passes `context` too, and applies the directives
30
+ * with `applyJanusDirectives()`.
31
+ */
32
+ export declare function janusConnection<A extends Auth<{
33
+ readonly type: string;
34
+ readonly id: string;
35
+ }>, P extends object | undefined = undefined, const T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']>(options: JanusConnectionOptions<A, P, T>): JanusConnection<A, P, T>;
36
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/connection/connection.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAgB,UAAU,EAAE,MAAM,UAAU,CAAC;AAE/D,OAAO,KAAK,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,eAAe,CAC9B,CAAC,SAAS,IAAI,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,EAC9D,CAAC,SAAS,MAAM,GAAG,SAAS,GAAG,SAAS,EACxC,KAAK,CAAC,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAC5D,OAAO,EAAE,sBAAsB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAqCpE"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The credential a graphql-ws connection presents, and the connections
3
+ * `janusConnection().onConnect` accepted: what each operation on one is
4
+ * authenticated from.
5
+ */
6
+ import type { RequestLike } from '@nxgt/janus';
7
+ import type { ConnectionContext } from './types';
8
+ /** Where the upgrade request is, when the transport does not say `extra.request`. */
9
+ export type Upgrade = (ctx: ConnectionContext) => RequestLike | undefined;
10
+ /**
11
+ * The connection's credential, as `auth.authenticate` reads one:
12
+ *
13
+ * 1. `connectionParams.authorization`, when it is a `Bearer` token — a
14
+ * non-browser client's `'Bearer <token>'`;
15
+ * 2. otherwise the upgrade request itself, read as any request is —
16
+ * `Authorization`, then `X-Session-Token`, then the session cookie a
17
+ * browser sends with it.
18
+ *
19
+ * **The first present wins, not the first valid one**, as in `@nxgt/janus`:
20
+ * a lapsed token in `connectionParams` beside a live cookie is refused. An
21
+ * `authorization` of another scheme (`Basic`) is not a session credential,
22
+ * as in `@nxgt/janus`, and does not count. `null` when there is nothing to
23
+ * read, or an `authorization` that is not a string — refused, as a
24
+ * client's mistake.
25
+ */
26
+ export declare function credentialOf(ctx: ConnectionContext, upgrade: Upgrade | undefined): RequestLike | null;
27
+ /** Remembers `credential` as the one `ctx`'s connection presented. */
28
+ export declare function accept(ctx: ConnectionContext, credential: RequestLike): void;
29
+ /**
30
+ * The credential of the connection a context was built for, or `undefined`
31
+ * for one no `onConnect` accepted — and for every HTTP request.
32
+ */
33
+ export declare function acceptedOf(context: unknown): RequestLike | undefined;
34
+ //# sourceMappingURL=credential.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential.d.ts","sourceRoot":"","sources":["../../src/connection/credential.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,qFAAqF;AACrF,MAAM,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,iBAAiB,KAAK,WAAW,GAAG,SAAS,CAAC;AAK1E;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC3B,GAAG,EAAE,iBAAiB,EACtB,OAAO,EAAE,OAAO,GAAG,SAAS,GAC1B,WAAW,GAAG,IAAI,CAcpB;AAWD,sEAAsE;AACtE,wBAAgB,MAAM,CAAC,GAAG,EAAE,iBAAiB,EAAE,UAAU,EAAE,WAAW,QAQrE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAIpE"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The public types of `janusConnection()`: what it takes, and the pieces
3
+ * it answers for graphql-ws's `useServer()`. Typed by their shape, so the
4
+ * package imports nothing from graphql-ws. Nothing here runs.
5
+ */
6
+ import type { Clock, RequestLike } from '@nxgt/janus';
7
+ import type { JanusContext, UserOfAuth } from '../types';
8
+ /**
9
+ * The part of graphql-ws's `Context` read here: the `connectionParams` the
10
+ * client sent with `connection_init`, and the transport's `extra` —
11
+ * `{ socket, request }` for `graphql-ws/use/ws`.
12
+ */
13
+ export interface ConnectionContext {
14
+ readonly connectionParams?: Readonly<Record<string, unknown>> | undefined;
15
+ readonly extra: unknown;
16
+ }
17
+ /** What `janusConnection()` takes. */
18
+ export interface JanusConnectionOptions<A, P, T extends string> {
19
+ /** What `janus()` answered. */
20
+ readonly auth: A;
21
+ /**
22
+ * What `permissions()` answered: `ctx.janus.access` in the context
23
+ * `context()` builds. `useJanus()`'s own when Yoga builds it.
24
+ */
25
+ readonly access?: P;
26
+ /** Only a user of this type may connect; any other is refused. */
27
+ readonly type?: T;
28
+ /** The clock `@fresh` reads in the context `context()` builds. */
29
+ readonly clock?: Clock;
30
+ /**
31
+ * The upgrade request of a connection, for a transport whose `extra`
32
+ * does not carry it as `request` — Bun's is `{ socket }`:
33
+ * `upgrade: (ctx) => ctx.extra.socket.data.request`, with the request
34
+ * passed to `server.upgrade(req, { data: { request: req } })`.
35
+ *
36
+ * A method, so `ctx` may be annotated with the transport's own `extra`.
37
+ */
38
+ upgrade?(ctx: ConnectionContext): RequestLike | undefined;
39
+ }
40
+ /** What `janusConnection()` answers: the pieces of graphql-ws's `useServer()`. */
41
+ export interface JanusConnection<A, P = undefined, T extends UserOfAuth<A>['type'] = UserOfAuth<A>['type']> {
42
+ /**
43
+ * `useServer({ onConnect })`: accepts a connection whose credential
44
+ * authenticates, and answers `false` for any other — graphql-ws closes
45
+ * it `4403: Forbidden`. An outage rejects, never `false`: the transport
46
+ * closes it `4500`, and the client does not retry it as a refusal.
47
+ */
48
+ readonly onConnect: (ctx: ConnectionContext) => Promise<boolean>;
49
+ /**
50
+ * `useServer({ context })`, for a server without Yoga: `ctx.janus` for
51
+ * one operation, authenticated from the connection's credential. Yoga's
52
+ * recipe builds the context itself, and `useJanus()` finds the
53
+ * connection there: leave this out.
54
+ */
55
+ readonly context: (ctx: ConnectionContext) => JanusContext<A, P, T>;
56
+ }
57
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/connection/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEzD;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAC1E,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,MAAM;IAC7D,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACpB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClB,kEAAkE;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB;;;;;;;OAOG;IACH,OAAO,CAAC,CAAC,GAAG,EAAE,iBAAiB,GAAG,WAAW,GAAG,SAAS,CAAC;CAC1D;AAED,kFAAkF;AAClF,MAAM,WAAW,eAAe,CAC/B,CAAC,EACD,CAAC,GAAG,SAAS,EACb,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAEvD;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;CACpE"}
package/dist/context.d.ts CHANGED
@@ -34,17 +34,26 @@ export interface ContextOptions {
34
34
  } | undefined;
35
35
  readonly type?: string | undefined;
36
36
  readonly clock?: Clock | undefined;
37
+ /** The call the consumer wrote, named by the no-request `TypeError`. */
38
+ readonly caller?: string;
37
39
  }
38
40
  /**
39
41
  * `ctx.janus` for one request. `authenticate` runs the first time `user()`
40
42
  * or `session()` is asked, once, and its answer — or its failure — is the
41
43
  * answer of both from then on.
42
44
  *
43
- * A context with no request — a transport that is not HTTP — answers a
45
+ * A context with no request — a transport that is not HTTP, or a graphql-ws
46
+ * connection no `janusConnection().onConnect` accepted — answers a
44
47
  * `TypeError` from `user()`, not `null`: anonymous is an answer, and nothing
45
48
  * here asked.
46
49
  */
47
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;
48
57
  /**
49
58
  * The clock `@fresh` and `requireFresh()` read the time from: the one given
50
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;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"}
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
@@ -26,6 +26,22 @@ export declare function janusGraphQLError(error: JanusError): GraphQLError;
26
26
  export declare function rethrown(error: unknown): never;
27
27
  /** Yoga's and envelop's `maskError` signature. */
28
28
  export type MaskError = (error: unknown, message: string, isDev?: boolean) => Error;
29
+ /** What `janusMaskError()` takes. */
30
+ export interface JanusMaskErrorOptions {
31
+ /**
32
+ * Called once with every `JanusError` answered 5xx — `STORE_FAILED`,
33
+ * `UNSUPPORTED`, `PERMISSION_DEPTH` — whichever path it took: a
34
+ * directive, `requireUser()`, `can()`, or a resolver that let it through.
35
+ * Its `slot`, `operation`, `reason` and cause are for your logs; the
36
+ * response never carries them.
37
+ *
38
+ * **It cannot change the answer**: one that throws, or rejects, is a
39
+ * `process.emitWarning`, and the 503 is sent all the same.
40
+ */
41
+ readonly report?: (error: JanusError) => unknown;
42
+ /** Every error that is not a `JanusError`; see `janusMaskError()`. */
43
+ readonly fallback?: MaskError;
44
+ }
29
45
  /**
30
46
  * Yoga's `maskedErrors.maskError`: a `JanusError` a resolver let through is
31
47
  * answered with its code and status — `STORE_FAILED` as 503, never masked
@@ -33,12 +49,19 @@ export type MaskError = (error: unknown, message: string, isDev?: boolean) => Er
33
49
  *
34
50
  * Without a `fallback`, a `GraphQLError` is kept and anything else becomes
35
51
  * `message`, with no detail. Pass Yoga's own `maskError` to keep its
36
- * development-mode details:
52
+ * development-mode details — as the options' `fallback`, or alone, as
53
+ * before `report` existed:
37
54
  *
38
55
  * ```ts
39
56
  * import { createYoga, maskError } from 'graphql-yoga';
40
- * createYoga({ schema, plugins, maskedErrors: { maskError: janusMaskError(maskError) } });
57
+ * createYoga({
58
+ * schema,
59
+ * plugins,
60
+ * maskedErrors: {
61
+ * maskError: janusMaskError({ report: (error) => logger.error(error), fallback: maskError }),
62
+ * },
63
+ * });
41
64
  * ```
42
65
  */
43
- export declare function janusMaskError(fallback?: MaskError): MaskError;
66
+ export declare function janusMaskError(options?: JanusMaskErrorOptions | MaskError): MaskError;
44
67
  //# 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;;;;;;;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,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,16 +9,23 @@
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.
17
+ * - `janusConnection({ auth, access?, type?, clock?, upgrade? })` —
18
+ * subscriptions over graphql-ws: `onConnect` and `context` for its
19
+ * `useServer()`, a connection authenticated from its `connectionParams`
20
+ * or its upgrade request.
16
21
  *
17
22
  * It defines no error class: a denial is a `GraphQLError`, and what it lets
18
23
  * through is `@nxgt/janus`'s own.
19
24
  */
25
+ export { janusConnection } from './connection/connection';
26
+ export type { ConnectionContext, JanusConnection, JanusConnectionOptions, } from './connection/types';
20
27
  export { applyJanusDirectives, type JanusDirectivesOptions, } from './directives/apply';
21
- export { type DenialCode, denial, janusGraphQLError, janusMaskError, type MaskError, } from './errors';
28
+ export { type DenialCode, denial, type JanusMaskErrorOptions, janusGraphQLError, janusMaskError, type MaskError, } from './errors';
22
29
  export { requireFresh } from './fresh';
23
30
  export { can, type RequireUserOptions, requireUser } from './helpers';
24
31
  export { useJanus } from './plugin';
@@ -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,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"}