@nxgt/shared-graphql 2.1.0 → 3.0.1

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.
Files changed (68) hide show
  1. package/README.md +112 -52
  2. package/dist/directives/authenticated.d.ts +42 -0
  3. package/dist/directives/authenticated.d.ts.map +1 -0
  4. package/dist/directives/federation.d.ts +6 -0
  5. package/dist/directives/federation.d.ts.map +1 -1
  6. package/dist/directives/index.d.ts +1 -2
  7. package/dist/directives/index.d.ts.map +1 -1
  8. package/dist/directives/permission.d.ts +20 -6
  9. package/dist/directives/permission.d.ts.map +1 -1
  10. package/dist/directives/removed-check.d.ts +15 -0
  11. package/dist/directives/removed-check.d.ts.map +1 -0
  12. package/dist/directives/scope.d.ts +3 -3
  13. package/dist/directives/scope.d.ts.map +1 -1
  14. package/dist/directives/sdl.d.ts +14 -4
  15. package/dist/directives/sdl.d.ts.map +1 -1
  16. package/dist/directives/validate.d.ts +8 -14
  17. package/dist/directives/validate.d.ts.map +1 -1
  18. package/dist/index.js +1037 -926
  19. package/dist/index.js.map +35 -31
  20. package/dist/integrations/hono.d.ts +2 -2
  21. package/dist/integrations/hono.d.ts.map +1 -1
  22. package/dist/plugins/apply-authenticated.d.ts +21 -0
  23. package/dist/plugins/apply-authenticated.d.ts.map +1 -0
  24. package/dist/plugins/apply-keto-checks.d.ts +10 -7
  25. package/dist/plugins/apply-keto-checks.d.ts.map +1 -1
  26. package/dist/plugins/auth.d.ts +17 -1
  27. package/dist/plugins/auth.d.ts.map +1 -1
  28. package/dist/plugins/authenticated.d.ts +14 -0
  29. package/dist/plugins/authenticated.d.ts.map +1 -0
  30. package/dist/plugins/extract-jwt.d.ts +19 -6
  31. package/dist/plugins/extract-jwt.d.ts.map +1 -1
  32. package/dist/plugins/field-guard.d.ts +30 -0
  33. package/dist/plugins/field-guard.d.ts.map +1 -0
  34. package/dist/plugins/gateway-trust.d.ts +15 -0
  35. package/dist/plugins/gateway-trust.d.ts.map +1 -0
  36. package/dist/plugins/index.d.ts +3 -0
  37. package/dist/plugins/index.d.ts.map +1 -1
  38. package/dist/plugins/keto-checks.d.ts +3 -2
  39. package/dist/plugins/keto-checks.d.ts.map +1 -1
  40. package/dist/plugins/keto-helpers.d.ts +1 -1
  41. package/dist/plugins/keto-helpers.d.ts.map +1 -1
  42. package/dist/plugins/ory-auth.d.ts +3 -3
  43. package/dist/shared.d.ts +1 -1
  44. package/dist/shared.d.ts.map +1 -1
  45. package/dist/utils/errors/denial.d.ts +20 -0
  46. package/dist/utils/errors/denial.d.ts.map +1 -0
  47. package/dist/utils/errors/format-error.d.ts +2 -1
  48. package/dist/utils/errors/format-error.d.ts.map +1 -1
  49. package/dist/utils/errors/index.d.ts +1 -0
  50. package/dist/utils/errors/index.d.ts.map +1 -1
  51. package/dist/utils/errors/mask-error.d.ts +2 -1
  52. package/dist/utils/errors/mask-error.d.ts.map +1 -1
  53. package/dist/utils/ws-context.d.ts +1 -1
  54. package/docs/README.md +8 -6
  55. package/docs/guide/authentication.md +187 -0
  56. package/docs/guide/errors.md +37 -5
  57. package/docs/guide/migrating-to-3.md +276 -0
  58. package/docs/guide/permissions.md +39 -50
  59. package/docs/roadmap.md +30 -29
  60. package/docs/troubleshooting.md +151 -48
  61. package/package.json +7 -11
  62. package/dist/directives/check.d.ts +0 -32
  63. package/dist/directives/check.d.ts.map +0 -1
  64. package/dist/directives/deprecation.d.ts +0 -7
  65. package/dist/directives/deprecation.d.ts.map +0 -1
  66. package/dist/directives/requirements.d.ts +0 -14
  67. package/dist/directives/requirements.d.ts.map +0 -1
  68. package/graphql/directives/check.graphqls +0 -90
package/dist/index.js CHANGED
@@ -3,15 +3,56 @@ import DataLoader3 from "dataloader";
3
3
 
4
4
  export * from "graphql-subscriptions";
5
5
 
6
- // src/directives/check.ts
6
+ // src/directives/authenticated.ts
7
7
  import { getDirective } from "@graphql-tools/utils";
8
-
9
- // src/directives/deprecation.ts
10
- import { logger } from "@nxgt/shared-logging";
11
- function warnCheckMistake(message) {
12
- logger.warn(`${message} — @check still boots with this; the next major refuses it, as @permission does`);
8
+ var AUTHENTICATED_DIRECTIVE_NAME = "authenticated";
9
+ var CALLER_TYPES = ["session", "token"];
10
+ function readAuthenticated(schema, node, where, known) {
11
+ const [found] = getDirective(schema, node, AUTHENTICATED_DIRECTIVE_NAME) ?? [];
12
+ if (!found)
13
+ return null;
14
+ const types = found.type;
15
+ if (types === undefined || types === null)
16
+ return {};
17
+ if (!Array.isArray(types) || types.length === 0) {
18
+ throw new TypeError(`@authenticated on ${where}: \`type: []\` admits no caller — name a type, or drop \`type\``);
19
+ }
20
+ for (const type of types) {
21
+ if (!known.includes(type)) {
22
+ throw new TypeError(`@authenticated on ${where}: unknown type ${JSON.stringify(type)} — known: ${known.join(", ")}`);
23
+ }
24
+ }
25
+ return { types };
13
26
  }
14
-
27
+ function combineRequirements(found, where) {
28
+ const applying = found.filter((one) => !!one);
29
+ if (applying.length === 0)
30
+ return null;
31
+ let types;
32
+ for (const { types: these } of applying) {
33
+ if (!these)
34
+ continue;
35
+ types = types ? types.filter((type) => these.includes(type)) : these;
36
+ }
37
+ if (types?.length === 0) {
38
+ throw new TypeError(`@authenticated on ${where}: the field's, its type's and its interfaces' \`type\`s have none in common — no caller could pass`);
39
+ }
40
+ return types ? { types } : {};
41
+ }
42
+ // src/directives/federation.ts
43
+ var FEDERATION_DIRECTIVES = `
44
+ directive @external on FIELD_DEFINITION | OBJECT
45
+ directive @requires(fields: FieldSet!) on FIELD_DEFINITION
46
+ directive @provides(fields: FieldSet!) on FIELD_DEFINITION
47
+ directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
48
+ directive @shareable repeatable on OBJECT | FIELD_DEFINITION
49
+ directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
50
+ directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
51
+ directive @override(from: String!) on FIELD_DEFINITION
52
+ directive @composeDirective(name: String!) repeatable on SCHEMA
53
+ directive @interfaceObject on OBJECT
54
+ directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
55
+ `;
15
56
  // src/directives/paths.ts
16
57
  var PATH = /^(args|parent|source)(\.[A-Za-z0-9_]+)+$/;
17
58
  var DEFAULT_ID_PATH = "args.id";
@@ -39,6 +80,8 @@ function objectIds(value) {
39
80
  const values = Array.isArray(value) ? value : [value];
40
81
  return values.filter((item) => typeof item === "string" && item.length > 0);
41
82
  }
83
+ // src/directives/permission.ts
84
+ import { getDirective as getDirective2 } from "@graphql-tools/utils";
42
85
 
43
86
  // src/directives/scope.ts
44
87
  function scopeOf(node, where, options) {
@@ -69,64 +112,15 @@ function assertRequirementShape(permissions, scope) {
69
112
  for (const group of permissions) {
70
113
  for (const term of group) {
71
114
  assertReadablePath(term, where);
72
- declaredOrReported(term, argumentNames, scope);
115
+ assertDeclaredArgument(term, argumentNames, where);
73
116
  if (namespaces && !namespaces.includes(term.namespace)) {
74
117
  throw new TypeError(`${where}: unknown namespace ${JSON.stringify(term.namespace)} — known: ${namespaces.join(", ")}`);
75
118
  }
76
119
  }
77
120
  }
78
121
  }
79
- function declaredOrReported(term, argumentNames, scope) {
80
- if (!scope.lenient) {
81
- assertDeclaredArgument(term, argumentNames, scope.where);
82
- return;
83
- }
84
- try {
85
- assertDeclaredArgument(term, argumentNames, scope.where);
86
- } catch (error) {
87
- scope.lenient(error.message);
88
- }
89
- }
90
122
 
91
- // src/directives/check.ts
92
- var CHECK_DIRECTIVE_NAME = "check";
93
- function readChecks(schema, node, where, options = {}) {
94
- const found = getDirective(schema, node, CHECK_DIRECTIVE_NAME) ?? [];
95
- const scope = {
96
- ...scopeOf(node, `@check on ${where}`, options),
97
- lenient: warnCheckMistake
98
- };
99
- return found.map((raw) => {
100
- const permissions = withDefaultIds(raw.permissions);
101
- assertRequirementShape(permissions, scope);
102
- return {
103
- permissions,
104
- onDeny: raw.onDeny ?? "NOT_FOUND",
105
- message: raw.message ?? undefined
106
- };
107
- });
108
- }
109
- function withDefaultIds(raw) {
110
- if (!Array.isArray(raw))
111
- return raw;
112
- return raw.map((group) => Array.isArray(group) ? group.map((term) => term && typeof term === "object" ? { ...term, id: term.id ?? DEFAULT_ID_PATH } : term) : group);
113
- }
114
- // src/directives/federation.ts
115
- var FEDERATION_DIRECTIVES = `
116
- directive @external on FIELD_DEFINITION | OBJECT
117
- directive @requires(fields: FieldSet!) on FIELD_DEFINITION
118
- directive @provides(fields: FieldSet!) on FIELD_DEFINITION
119
- directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
120
- directive @shareable repeatable on OBJECT | FIELD_DEFINITION
121
- directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
122
- directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
123
- directive @override(from: String!) on FIELD_DEFINITION
124
- directive @composeDirective(name: String!) repeatable on SCHEMA
125
- directive @interfaceObject on OBJECT
126
- directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
127
- `;
128
123
  // src/directives/permission.ts
129
- import { getDirective as getDirective2 } from "@graphql-tools/utils";
130
124
  var PERMISSION_DIRECTIVE_NAME = "permission";
131
125
  function readPermissions(schema, node, where, options = {}) {
132
126
  if (!isOurs(schema))
@@ -156,109 +150,7 @@ function isOurs(schema) {
156
150
  const names = declared?.args.map((arg) => arg.name) ?? [];
157
151
  return names.includes("name") && names.includes("type");
158
152
  }
159
- // src/directives/requirements.ts
160
- function readRequirements(schema, node, where, options = {}) {
161
- const queues = {
162
- [CHECK_DIRECTIVE_NAME]: readChecks(schema, node, where, options),
163
- [PERMISSION_DIRECTIVE_NAME]: readPermissions(schema, node, where, options)
164
- };
165
- const ordered = [];
166
- for (const directive of directivesOf(node)) {
167
- const next = queues[directive.name.value]?.shift();
168
- if (next)
169
- ordered.push(next);
170
- }
171
- return [
172
- ...ordered,
173
- ...queues[CHECK_DIRECTIVE_NAME] ?? [],
174
- ...queues[PERMISSION_DIRECTIVE_NAME] ?? []
175
- ];
176
- }
177
- function directivesOf(node) {
178
- const field = node;
179
- return field.astNode?.directives ?? field.directives ?? [];
180
- }
181
153
  // src/directives/sdl.ts
182
- var CHECK_DIRECTIVE_SDL = `"""
183
- One question, with the object left as a path instead of a value.
184
- """
185
- input CheckPermission {
186
- """
187
- The Keto namespace, e.g. "Note" — from the stack's OPL document.
188
- """
189
- namespace: String!
190
-
191
- """
192
- The permit asked of it, e.g. "view". A plain relation works too; Keto
193
- answers \`false\`, not an error, for a name it does not know.
194
- """
195
- permit: String!
196
-
197
- """
198
- Where the object id is read: \`args.<path>\`, or \`parent.<path>\` /
199
- \`source.<path>\` (the same root under two names), dotted paths allowed. A value that turns out to be a LIST requires the permit on every
200
- element — that is what a mutation taking \`ids: [ID!]!\` means.
201
- """
202
- id: String = "args.id"
203
- }
204
-
205
- """
206
- What a denial looks like from outside.
207
- """
208
- enum CheckDenial {
209
- """
210
- The same answer as for an id that never existed, so ids cannot be probed.
211
- The default, and the one to keep unless the caller already knows the object
212
- is there.
213
- """
214
- NOT_FOUND
215
-
216
- """
217
- For a second check on an object the caller can already see: a viewer asked
218
- to edit already knows it exists, and "you may not change it" is honest.
219
- """
220
- FORBIDDEN
221
- }
222
-
223
- """
224
- Deprecated: write \`@permission(name:, type:)\` in a new schema — one Keto
225
- question per directive, repeated for AND, with OR kept in the Keto model. This
226
- form keeps working and is evaluated in declaration order with @permission.
227
-
228
- The permission a field requires, in disjunctive normal form: the OUTER list is
229
- OR, the INNER list is AND. \`[[A, B], [C]]\` reads "(A and B) or C" — the same
230
- shape \`@policy(policies: [["ADMIN"]])\` uses.
231
-
232
- Repeatable, and evaluated in declaration order, each with its own \`onDeny\`.
233
- That is how the 404-then-403 ladder is written:
234
-
235
- updateNote(id: ID!, input: UpdateNoteInput!): Note!
236
- @check(permissions: [[{ namespace: "Note", permit: "view" }]])
237
- @check(permissions: [[{ namespace: "Note", permit: "edit" }]], onDeny: FORBIDDEN)
238
-
239
- NOT for a field that answers a LIST the caller is entitled to. "Which notes may
240
- I see" is not a check, it is a Keto query folded into the database filter
241
- before the read. A directive there would have to fetch everything and filter
242
- after, which makes \`totalCount\` and the cursors lie.
243
- """
244
- directive @check(
245
- permissions: [[CheckPermission!]!]!
246
- onDeny: CheckDenial! = NOT_FOUND
247
-
248
- """
249
- The i18n key the denial carries, e.g. "notes.errors.not-found". Defaults to
250
- \`errors.not-found\` / \`errors.insufficient-permissions\`, the shared keys.
251
-
252
- Set it whenever the API's own service layer answers the same refusal with a
253
- domain message. Two layers guard these fields — the directive, and the
254
- \`require<M>Access\` the service calls — and if they word the same 404
255
- differently, the wording tells a caller WHICH one refused: a generic message
256
- means "you may not", a domain one means "it is gone". That is precisely the
257
- distinction NOT_FOUND exists to hide.
258
- """
259
- message: String
260
- ) repeatable on FIELD_DEFINITION
261
- `;
262
154
  var PERMISSION_DIRECTIVE_SDL = `"""
263
155
  What a refused @permission looks like from outside.
264
156
  """
@@ -321,222 +213,425 @@ directive @permission(
321
213
  message: String
322
214
  ) repeatable on FIELD_DEFINITION
323
215
  `;
324
- var KETO_DIRECTIVES_SDL = `${CHECK_DIRECTIVE_SDL}
325
- ${PERMISSION_DIRECTIVE_SDL}`;
216
+ var KETO_DIRECTIVES_SDL = PERMISSION_DIRECTIVE_SDL;
217
+ var AUTHENTICATED_DIRECTIVE_SDL = `"""
218
+ A signed-in caller — of one of the types \`type\` names, when it names some.
219
+ """
220
+ directive @authenticated(
221
+ """
222
+ The kinds of caller admitted: "session" or "token" by default. Any caller
223
+ when omitted.
224
+ """
225
+ type: [String!]
226
+ ) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
227
+ `;
326
228
  // src/integrations/hono.ts
327
229
  import { Hono } from "hono";
328
230
  import { contextStorage } from "hono/context-storage";
329
231
  import { createMiddleware } from "hono/factory";
330
232
  import { languageDetector } from "hono/language";
331
233
 
332
- // src/utils/change-resolvers.utils.ts
234
+ // src/utils/sandbox.ts
235
+ import { html } from "hono/html";
236
+ var renderSandbox = ({
237
+ port = 8080,
238
+ hostname = "localhost",
239
+ graphqlEndpoint = "graphql",
240
+ protocol = "http",
241
+ title = "Sandbox Explorer"
242
+ }) => html`
243
+ <title>${title}</title>
244
+ <div id="sandbox" style="position:absolute;top:0;right:0;bottom:0;left:0"></div>
245
+ <script src="https://embeddable-sandbox.cdn.apollographql.com/_latest/embeddable-sandbox.umd.production.min.js"></script>
246
+ <script>
247
+ new window.EmbeddedSandbox({
248
+ target: "#sandbox",
249
+ // Pass through your server href if you are embedding on an endpoint.
250
+ // Otherwise, you can pass whatever endpoint you want Sandbox to start up with here.
251
+ initialEndpoint: "${protocol}://${hostname}:${port}/${graphqlEndpoint}",
252
+ handleRequest: (endpointUrl, options) => {
253
+ return fetch(endpointUrl, {
254
+ ...options,
255
+ headers: {
256
+ ...options.headers
257
+ },
258
+ })
259
+ },
260
+ hideCookieToggle: true,
261
+ });
262
+ // advanced options: https://www.apollographql.com/docs/studio/explorer/sandbox#embedding-sandbox
263
+ </script>
264
+
265
+ `;
266
+
267
+ // src/integrations/hono.ts
268
+ function sandboxExplorer(options) {
269
+ return createMiddleware(async (ctx) => {
270
+ return ctx.html(renderSandbox(options));
271
+ });
272
+ }
273
+ function honoYoga(yoga) {
274
+ return createMiddleware(async (ctx) => {
275
+ const response = await yoga.fetch(ctx.req.raw);
276
+ return response;
277
+ });
278
+ }
279
+ function createYogaHono(yoga, options) {
280
+ const app = new Hono;
281
+ app.use(contextStorage());
282
+ app.use(languageDetector({
283
+ order: ["header"],
284
+ supportedLanguages: ["en", "fr"],
285
+ fallbackLanguage: "en",
286
+ caches: []
287
+ }));
288
+ app.get("/health", (ctx) => {
289
+ return ctx.json({ status: "ok" }, 200);
290
+ });
291
+ app.get(options?.sandbox?.endpoint || "sandbox", sandboxExplorer(options?.sandbox || {}));
292
+ app.use(yoga.graphqlEndpoint, honoYoga(yoga));
293
+ return app;
294
+ }
295
+ // src/plugins/apply-authenticated.ts
296
+ import { MapperKind, mapSchema } from "@graphql-tools/utils";
297
+ import { ErrorCode as ErrorCode2 } from "@nxgt/shared-exceptions";
333
298
  import {
334
- AUDIT_EVENT,
335
- CommitType
336
- } from "@nxgt/shared-mongo";
337
- import { withFilter } from "graphql-subscriptions";
338
- function createChangeResolvers(config) {
339
- const { entity, collection, service, pubsub } = config;
340
- const resolvePubSub = () => typeof pubsub === "function" ? pubsub() : pubsub;
299
+ getNamedType,
300
+ isEnumType,
301
+ isObjectType,
302
+ isScalarType
303
+ } from "graphql";
304
+
305
+ // src/utils/errors/denial.ts
306
+ import { translate } from "@nxgt/i18n";
307
+ import { ErrorCode } from "@nxgt/shared-exceptions";
308
+ import { GraphQLError } from "graphql";
309
+ var STATUS = {
310
+ [ErrorCode.Unauthenticated]: 401,
311
+ [ErrorCode.Forbidden]: 403,
312
+ [ErrorCode.NotFound]: 404
313
+ };
314
+ var DEFAULT_MESSAGE = {
315
+ [ErrorCode.Unauthenticated]: "errors.unauthenticated",
316
+ [ErrorCode.Forbidden]: "errors.insufficient-permissions",
317
+ [ErrorCode.NotFound]: "errors.not-found"
318
+ };
319
+ var MESSAGE_KEY = Symbol.for("@nxgt/shared-graphql/denial-message");
320
+ function denial(code, message) {
321
+ const key = message ?? DEFAULT_MESSAGE[code];
322
+ const error = new GraphQLError(translate(key), {
323
+ extensions: { code, http: { status: STATUS[code] } }
324
+ });
325
+ Object.defineProperty(error, MESSAGE_KEY, { value: key });
326
+ return error;
327
+ }
328
+ function denialMessageKey(error) {
329
+ if (!error || typeof error !== "object")
330
+ return;
331
+ const key = error[MESSAGE_KEY];
332
+ return typeof key === "string" ? key : undefined;
333
+ }
334
+
335
+ // src/plugins/field-guard.ts
336
+ import {
337
+ defaultFieldResolver
338
+ } from "graphql";
339
+ var GUARDS = "@nxgt/shared-graphql:guards";
340
+ function intactGuards(config) {
341
+ const record = config.extensions?.[GUARDS];
342
+ if (!record || record.wrapped.resolve !== config.resolve)
343
+ return;
344
+ if (record.wrapped.subscribe !== config.subscribe)
345
+ return;
346
+ return record;
347
+ }
348
+ function isGuarded(config, mark) {
349
+ return !!intactGuards(config)?.guards.some((guard) => guard.mark === mark);
350
+ }
351
+ function guardField(config, guard) {
352
+ const previous = intactGuards(config);
353
+ const base = previous ? { resolve: previous.resolve, subscribe: previous.subscribe } : {
354
+ resolve: config.resolve ?? defaultFieldResolver,
355
+ subscribe: config.subscribe
356
+ };
357
+ const guards = [...previous?.guards ?? [], guard].sort((a, b) => a.rank - b.rank);
358
+ const wrapped = {
359
+ resolve: checked(guards, base.resolve),
360
+ subscribe: base.subscribe && checked(guards.filter((one) => one.onSubscribe), base.subscribe)
361
+ };
341
362
  return {
342
- Query: {
343
- [`${entity}Changes`]: async (_parent, args, context) => context.services.audit.findChangesPaginatedByCollection(collection, args),
344
- [`${entity}ChangesById`]: async (_parent, args, context) => context.services.audit.findChangesPaginatedByCollectionAndOid(args.id, collection, args)
363
+ ...config,
364
+ extensions: {
365
+ ...config.extensions,
366
+ [GUARDS]: { guards, ...base, wrapped }
345
367
  },
346
- Subscription: {
347
- [`${entity}Changed`]: {
348
- resolve: async (payload, _args, context) => ({
349
- id: context.services.audit.extractId(payload),
350
- data: payload.type !== CommitType.TERMINAL ? await context.services[service].findById(payload.state._id) : null,
351
- type: payload.type
352
- }),
353
- subscribe: withFilter(() => resolvePubSub().subscribe(AUDIT_EVENT), (payload, _variables, context) => context ? context.services.audit.matches(payload, { collection }) : false)
368
+ resolve: wrapped.resolve,
369
+ subscribe: wrapped.subscribe
370
+ };
371
+ }
372
+ function checked(guards, next) {
373
+ return async (source, args, context, info) => {
374
+ for (const { check } of guards)
375
+ await check(source, args, context);
376
+ return next(source, args, context, info);
377
+ };
378
+ }
379
+
380
+ // src/plugins/apply-authenticated.ts
381
+ var MARK = "@nxgt/shared-graphql:authenticated";
382
+ function applyAuthenticated(schema, options = {}) {
383
+ const known = options.types ?? CALLER_TYPES;
384
+ let guardedNow = 0;
385
+ const mapped = mapSchema(schema, {
386
+ [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
387
+ const where = `${typeName}.${fieldName}`;
388
+ const requirement = requirementOf(schema, fieldConfig, where, known);
389
+ if (!requirement || isGuarded(fieldConfig, MARK))
390
+ return fieldConfig;
391
+ guardedNow += 1;
392
+ return guarded(fieldConfig, requirement);
393
+ }
394
+ });
395
+ return guardedNow > 0 ? mapped : schema;
396
+ }
397
+ function requirementOf(schema, fieldConfig, where, known) {
398
+ const [typeName = "", fieldName = ""] = where.split(".");
399
+ const type = schema.getType(typeName);
400
+ const interfaces = isObjectType(type) ? type.getInterfaces() : [];
401
+ const read = (node, at) => readAuthenticated(schema, node, at, known);
402
+ return combineRequirements([
403
+ read(fieldConfig, where),
404
+ type ? read(type, typeName) : null,
405
+ ...interfaces.flatMap((iface) => {
406
+ const field = iface.getFields()[fieldName];
407
+ return [
408
+ read(iface, iface.name),
409
+ field ? read(field, `${iface.name}.${fieldName}`) : null
410
+ ];
411
+ }),
412
+ leafRequirement(schema, fieldConfig, read)
413
+ ], where);
414
+ }
415
+ function leafRequirement(schema, fieldConfig, read) {
416
+ const leaf = schema.getType(getNamedType(fieldConfig.type).name);
417
+ if (!leaf || !(isScalarType(leaf) || isEnumType(leaf)))
418
+ return null;
419
+ return read(leaf, leaf.name);
420
+ }
421
+ function guarded(fieldConfig, { types }) {
422
+ return guardField(fieldConfig, {
423
+ mark: MARK,
424
+ rank: 0,
425
+ onSubscribe: true,
426
+ check: (_source, _args, context) => {
427
+ const ctx = context ?? {};
428
+ if (!ctx.user)
429
+ throw denial(ErrorCode2.Unauthenticated);
430
+ if (types && !types.includes(callerTypeOf(ctx) ?? "")) {
431
+ throw denial(ErrorCode2.Forbidden);
354
432
  }
355
433
  }
356
- };
434
+ });
357
435
  }
358
- // src/utils/codegen.utils.ts
359
- var SCALARS_MAPPING = {
360
- ID: "any",
361
- DateTime: "any",
362
- Date: "any",
363
- Any: "any",
364
- BigInt: "number",
365
- EmailAddress: "string",
366
- GeoJSON: "any",
367
- HSL: "string",
368
- HSLA: "string",
369
- HexColorCode: "string",
370
- Hexadecimal: "string",
371
- IBAN: "string",
372
- IP: "string",
373
- Time: "string",
374
- LocalTime: "string",
375
- UUID: "string",
376
- PositiveInt: "number",
377
- NegativeInt: "number",
378
- NonPositiveInt: "number",
379
- NonNegativeInt: "number",
380
- PositiveFloat: "number",
381
- NegativeFloat: "number",
382
- NonPositiveFloat: "number",
383
- NonNegativeFloat: "number",
384
- NonEmptyString: "string",
385
- PostalCode: "string",
386
- RGB: "string",
387
- RGBA: "string",
388
- SESSN: "string",
389
- Timestamp: "string",
390
- URL: "string",
391
- USCurrency: "string",
392
- UTCOffset: "string",
393
- Long: "number",
394
- Short: "number",
395
- Byte: "number",
396
- BigDecimal: "string",
397
- BigInteger: "string",
398
- Char: "string",
399
- Locale: "string",
400
- CountryCode: "string",
401
- CountryName: "string",
402
- AccountNumber: "string",
403
- CurrencyCode: "string",
404
- Duration: "string",
405
- Url: "string",
406
- JSONObject: "Record<string, any>",
407
- JSON: "any",
408
- Void: "void",
409
- ObjectID: "any",
410
- Upload: "File | Blob",
411
- Email: "string",
412
- Port: "number",
413
- IPv4: "string",
414
- IPv6: "string",
415
- MAC: "string",
416
- Password: "string",
417
- PhoneNumber: "string",
418
- ZIPCode: "string",
419
- SSN: "string",
420
- GUID: "string",
421
- ISBN: "string",
422
- Latitude: "number",
423
- Longitude: "number",
424
- JWT: "string"
425
- };
426
- // src/utils/data-loader.utils.ts
427
- import { cast } from "@nxgt/shared/helpers";
428
- import {
429
- paginateList
430
- } from "@nxgt/shared-mongo";
431
- import DataLoader from "dataloader";
432
- import { isArray } from "lodash";
433
- function createDataLoader(batchLoadFn, options) {
434
- return new DataLoader(batchLoadFn, options);
435
- }
436
- function createConnectionListDataLoader(path, model, options) {
437
- return createDataLoader(async (docs) => {
438
- const data = await model.find({
439
- ...options.filter ?? {},
440
- _id: docs.flatMap((doc) => {
441
- const ids = doc[path] ?? [];
442
- return isArray(ids) ? cast(ids.filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))) : [];
443
- })
444
- }).exec();
445
- return cast(docs.map((doc) => model.paginateList(data.filter((child) => cast(doc[path] ?? []).map((item) => item.toString()).includes(child._id.toString())), options)));
446
- });
436
+ function callerTypeOf(ctx) {
437
+ return ctx.ory?.kind ?? ctx.user?.tokenType;
447
438
  }
448
- function createChildrenConnectionDataLoader(path, model, options) {
449
- return createDataLoader(async (docs) => {
450
- const filter = {};
451
- filter[path] = docs.map((doc) => doc._id);
452
- const data = await model.find(filter).exec();
453
- return cast(docs.map((doc) => paginateList(data.filter((child) => doc._id?.toString() === child[path]?.toString()), options)));
454
- });
439
+ // src/plugins/apply-keto-checks.ts
440
+ import { MapperKind as MapperKind2, mapSchema as mapSchema2 } from "@graphql-tools/utils";
441
+ import { evaluateRequirement } from "@nxgt/ory-sdk";
442
+ import { ErrorCode as ErrorCode3 } from "@nxgt/shared-exceptions";
443
+
444
+ // src/directives/removed-check.ts
445
+ import { getDirective as getDirective3 } from "@graphql-tools/utils";
446
+ function refuseRemovedCheck(schema, node, where) {
447
+ const declared = schema.getDirective("check");
448
+ if (!declared?.args.some((arg) => arg.name === "permissions"))
449
+ return;
450
+ if (!getDirective3(schema, node, "check")?.length)
451
+ return;
452
+ throw new TypeError(`@check on ${where}: @check was removed in @nxgt/shared-graphql 3.0 — write one @permission(name: "<permit>", type: "<namespace>") per term, and move an OR into the Keto model`);
455
453
  }
456
- function createSimpleListDataLoader(path, model, options) {
457
- return createDataLoader(async (docs) => {
458
- const filter = options?.filter ?? {};
459
- const ids = docs.flatMap((doc) => {
460
- const ids = doc[path] ?? [];
461
- return isArray(ids) ? cast(ids) : [];
462
- });
463
- filter._id = ids;
464
- const query = model.find(filter);
465
- const data = await query.exec();
466
- return cast(docs.map((doc) => {
467
- const result = data.filter((child) => cast(doc[path] ?? []).map((item) => item.toString()).includes(child._id.toString()));
468
- if (options?.skip && options?.limit) {
469
- return result.slice(options.skip, options.skip + options.limit);
470
- }
471
- if (options?.limit) {
472
- return result.slice(0, options.limit);
473
- }
474
- if (options?.skip) {
475
- return result.slice(options.skip);
454
+
455
+ // src/plugins/apply-keto-checks.ts
456
+ var MARK2 = "@nxgt/shared-graphql:keto-checks";
457
+ function applyKetoChecks(schema, options = {}) {
458
+ let guardedNow = 0;
459
+ const mapped = mapSchema2(schema, {
460
+ [MapperKind2.INTERFACE_FIELD]: (fieldConfig, fieldName, typeName) => {
461
+ refuseOnInterface(schema, fieldConfig, `${typeName}.${fieldName}`);
462
+ return fieldConfig;
463
+ },
464
+ [MapperKind2.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
465
+ const where = `${typeName}.${fieldName}`;
466
+ refuseRemovedCheck(schema, fieldConfig, where);
467
+ const requirements = readPermissions(schema, fieldConfig, where, options);
468
+ if (requirements.length === 0 || isGuarded(fieldConfig, MARK2)) {
469
+ return fieldConfig;
476
470
  }
477
- return result;
478
- }));
471
+ guardedNow += 1;
472
+ return guarded2(fieldConfig, requirements, where);
473
+ }
479
474
  });
475
+ return guardedNow > 0 ? mapped : schema;
480
476
  }
481
- function createChildrenListDataLoader(path, model, options) {
482
- return createDataLoader(async (docs) => {
483
- const filter = options?.filter ?? {};
484
- const key = path;
485
- filter[key] = docs.map((doc) => doc._id);
486
- const query = model.find(filter);
487
- const data = await query.exec();
488
- return cast(docs.map((doc) => {
489
- const result = data.filter((child) => child[path]?.toString() === doc._id.toString());
490
- if (options?.skip && options?.limit) {
491
- return result.slice(options.skip, options.skip + options.limit);
492
- }
493
- if (options?.limit) {
494
- return result.slice(0, options.limit);
477
+ function refuseOnInterface(schema, fieldConfig, where) {
478
+ refuseRemovedCheck(schema, fieldConfig, where);
479
+ if (readPermissions(schema, fieldConfig, where).length > 0) {
480
+ throw new TypeError(`${where}: @permission on an interface field guards nothing — no resolver runs there. Put it on each implementing type's field`);
481
+ }
482
+ }
483
+ function guarded2(fieldConfig, requirements, where) {
484
+ return guardField(fieldConfig, {
485
+ mark: MARK2,
486
+ rank: 1,
487
+ onSubscribe: requirements.every(({ permissions }) => permissions.every((group) => group.every((term) => term.id.startsWith("args.")))),
488
+ check: async (source, args, context) => {
489
+ const ctx = context;
490
+ const subject = ctx.ory?.subject;
491
+ if (!subject)
492
+ throw denial(ErrorCode3.Unauthenticated);
493
+ const check = ctx.ketoChecks;
494
+ if (!check) {
495
+ throw new Error(`${where}: no checker on the context — useKetoChecks(ory) is not registered`);
495
496
  }
496
- if (options?.skip) {
497
- return result.slice(options.skip);
497
+ for (const { permissions, onDeny, message } of requirements) {
498
+ const allowed = await evaluateRequirement(permissions, (term) => objectsOf(term, source, args, where), check, subject);
499
+ if (!allowed)
500
+ throw refusal(onDeny, message);
498
501
  }
499
- return result;
500
- }));
502
+ }
501
503
  });
502
504
  }
503
- function createSingleChildDataLoader(path, model) {
504
- return createDataLoader(async (docs) => {
505
- const data = await model.find({
506
- _id: docs.map((doc) => cast(doc[path])).filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))
507
- }).exec();
508
- const map = new Map(data.map((doc) => [doc.id, doc]));
509
- return docs.map((doc) => map.get(doc[path]?.toString() ?? "") ?? null);
505
+ function objectsOf(term, source, args, where) {
506
+ const ids = objectIds(readPath(term.id, source, args));
507
+ if (ids.length === 0) {
508
+ throw new Error(`${where}: "${term.id}" resolved no object id`);
509
+ }
510
+ return ids;
511
+ }
512
+ function refusal(onDeny, message) {
513
+ return denial(onDeny === "FORBIDDEN" ? ErrorCode3.Forbidden : ErrorCode3.NotFound, message);
514
+ }
515
+ // src/plugins/gateway-trust.ts
516
+ import {
517
+ requireGatewayTrust as requireTrust
518
+ } from "@nxgt/security/gateway";
519
+ import {
520
+ GATEWAY_SECRET_HEADER,
521
+ gatewaySecret
522
+ } from "@nxgt/security/gateway";
523
+ function requireGatewayTrust(options, plugin) {
524
+ return requireTrust(options, {
525
+ caller: plugin,
526
+ reads: "the request's extensions",
527
+ alternative: "useOryAuth(ory)"
510
528
  });
511
529
  }
512
- function createEntitiesReferenceLoader(model) {
513
- return createDataLoader(async (ids) => {
514
- const docs = await model.find({
515
- _id: ids.filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))
516
- }).exec();
517
- const map = new Map(docs.map((doc) => [doc.id, doc]));
518
- return ids.map((id) => map.get(id) ?? null);
530
+ function principalOf(value) {
531
+ if (!value || typeof value !== "object")
532
+ return;
533
+ const sub = value.sub;
534
+ return typeof sub === "string" && sub.length > 0 ? value : undefined;
535
+ }
536
+
537
+ // src/plugins/auth.ts
538
+ function useAuth(options) {
539
+ const trusted = requireGatewayTrust(options, "useAuth()");
540
+ return {
541
+ onContextBuilding: async ({ context, extendContext }) => {
542
+ const headers = context.request?.headers;
543
+ if (typeof headers?.get !== "function")
544
+ return;
545
+ if (!await trusted(headers))
546
+ return;
547
+ const user = principalOf(context.params?.extensions?.user);
548
+ if (!user)
549
+ return;
550
+ const token = context.params.extensions?.token;
551
+ extendContext({
552
+ user,
553
+ token: typeof token === "string" ? token : undefined
554
+ });
555
+ }
556
+ };
557
+ }
558
+ // src/plugins/authenticated.ts
559
+ function useAuthenticated(options = {}) {
560
+ return {
561
+ onSchemaChange: ({ schema, replaceSchema }) => {
562
+ const next = applyAuthenticated(schema, options);
563
+ if (next !== schema)
564
+ replaceSchema(next);
565
+ }
566
+ };
567
+ }
568
+ // src/plugins/extract-jwt.ts
569
+ function extractJwtPlugin(options) {
570
+ const trusted = requireGatewayTrust(options, "extractJwtPlugin()");
571
+ return {
572
+ async requestDidStart({ request, contextValue }) {
573
+ const headers = request.http?.headers;
574
+ if (!headers || !await trusted(headers))
575
+ return;
576
+ const payload = principalOf(request.extensions?.payload);
577
+ if (payload)
578
+ contextValue.jwt = { payload };
579
+ }
580
+ };
581
+ }
582
+ // src/plugins/keto-checker.ts
583
+ import { tuple } from "@nxgt/ory-sdk";
584
+ import DataLoader from "dataloader";
585
+ function createKetoChecks(ory) {
586
+ const loader = new DataLoader((questions) => ory.checkMany([...questions]), {
587
+ cacheKeyFn: ({ permission, subject }) => tuple(permission, subject)
519
588
  });
589
+ return (permission, subject) => loader.load({ permission, subject });
520
590
  }
521
- // src/utils/errors/create-error.ts
522
- import { GraphQLError } from "graphql";
523
- function createGraphQLError(message, options) {
524
- return new GraphQLError(message, options);
591
+ // src/plugins/keto-checks.ts
592
+ function useKetoChecks(ory, options = {}) {
593
+ return {
594
+ onSchemaChange: ({ schema, replaceSchema }) => {
595
+ const next = applyKetoChecks(schema, options);
596
+ if (next !== schema)
597
+ replaceSchema(next);
598
+ },
599
+ onContextBuilding: ({ extendContext }) => {
600
+ extendContext({ ketoChecks: createKetoChecks(ory) });
601
+ }
602
+ };
525
603
  }
526
- // src/utils/errors/format-error.ts
527
- import {
528
- ApolloServerErrorCode,
529
- unwrapResolverError
530
- } from "@apollo/server/errors";
604
+ // src/plugins/keto-helpers.ts
605
+ import { ErrorCode as ErrorCode4 } from "@nxgt/shared-exceptions";
606
+ function requireUser(context) {
607
+ if (!context.user) {
608
+ throw denial(ErrorCode4.Unauthenticated);
609
+ }
610
+ return context.user;
611
+ }
612
+ async function can(context, question) {
613
+ const subject = context.ory?.subject;
614
+ if (!subject) {
615
+ throw denial(ErrorCode4.Unauthenticated);
616
+ }
617
+ if (!context.ketoChecks) {
618
+ throw new Error("can(): no checker on the context — useKetoChecks(ory) is not registered");
619
+ }
620
+ const permission = {
621
+ namespace: question.type,
622
+ object: question.id,
623
+ relation: question.name
624
+ };
625
+ return context.ketoChecks(permission, subject);
626
+ }
627
+ // src/plugins/ory-auth.ts
531
628
  import {
532
- translate as translateBase
533
- } from "@nxgt/i18n";
534
- import { CustomException, ErrorCode as ErrorCode2 } from "@nxgt/shared-exceptions";
535
- import { MONGO_UTILS, mongoose } from "@nxgt/shared-mongo";
536
- import { kebabCase } from "lodash";
629
+ bearerOf
630
+ } from "@nxgt/ory-sdk";
631
+ import { claimsFromOryPrincipal } from "@nxgt/security/integrations/ory";
537
632
 
538
633
  // src/utils/errors/ory-unavailable.ts
539
- import { ErrorCode } from "@nxgt/shared-exceptions";
634
+ import { ErrorCode as ErrorCode5 } from "@nxgt/shared-exceptions";
540
635
  import { GraphQLError as GraphQLError2 } from "graphql";
541
636
  function isOryUnavailable(error) {
542
637
  if (!(error instanceof Error))
@@ -547,598 +642,192 @@ function isOryUnavailable(error) {
547
642
  function serviceUnavailableError(error) {
548
643
  return new GraphQLError2(`ory: ${error.service} is unavailable`, {
549
644
  extensions: {
550
- code: ErrorCode.ServiceUnavailable,
645
+ code: ErrorCode5.ServiceUnavailable,
551
646
  http: { status: 503 },
552
647
  debugMessage: error.message
553
648
  }
554
649
  });
555
650
  }
556
651
 
557
- // src/utils/errors/format-error.ts
558
- function createFormatError(translate = translateBase, production) {
559
- return (formattedError, graphQLError) => {
560
- if (production && formattedError.extensions?.stacktrace) {
561
- formattedError.extensions.stacktrace = undefined;
562
- }
563
- const error = unwrapResolverError(graphQLError);
564
- if (isOryUnavailable(error)) {
565
- const unavailable = serviceUnavailableError(error);
566
- return {
567
- ...formattedError,
568
- message: unavailable.message,
569
- extensions: { ...formattedError.extensions, ...unavailable.extensions }
570
- };
571
- }
572
- if (error instanceof CustomException || error instanceof mongoose.MongooseError) {
573
- const exception = error instanceof mongoose.MongooseError ? MONGO_UTILS.castError(error) : error;
574
- return {
575
- ...formattedError,
576
- message: exception.errorCode === ErrorCode2.ValidationError ? exception.message : translate(exception.message, exception.options),
577
- extensions: {
578
- ...formattedError.extensions,
579
- code: exception.errorCode,
580
- debugMessage: exception.debugMessage
581
- }
582
- };
583
- }
584
- if ([
585
- ApolloServerErrorCode.GRAPHQL_VALIDATION_FAILED,
586
- ApolloServerErrorCode.BAD_REQUEST,
587
- ApolloServerErrorCode.BAD_USER_INPUT,
588
- ApolloServerErrorCode.GRAPHQL_PARSE_FAILED,
589
- ApolloServerErrorCode.OPERATION_RESOLUTION_FAILURE,
590
- ApolloServerErrorCode.PERSISTED_QUERY_NOT_FOUND,
591
- ApolloServerErrorCode.PERSISTED_QUERY_NOT_SUPPORTED
592
- ].includes(formattedError.extensions?.code)) {
593
- return {
594
- ...formattedError,
595
- message: translate(`errors.${kebabCase(formattedError.extensions?.code)}`, {}),
596
- extensions: {
597
- ...formattedError.extensions,
598
- debugMessage: formattedError.message
599
- }
600
- };
601
- }
602
- return formattedError;
603
- };
604
- }
605
- // src/utils/errors/mask-error.ts
606
- import {
607
- translate as translateBase2
608
- } from "@nxgt/i18n";
609
- import { CustomException as CustomException2, ErrorCode as ErrorCode3 } from "@nxgt/shared-exceptions";
610
- import { MONGO_UTILS as MONGO_UTILS2, mongoose as mongoose2 } from "@nxgt/shared-mongo";
611
- import { GraphQLError as GraphQLError3 } from "graphql";
612
- function createMaskError(translate = translateBase2) {
613
- return (error, message, isDev) => {
614
- const original = error instanceof GraphQLError3 && error.originalError ? error.originalError : error;
615
- if (isOryUnavailable(original))
616
- return serviceUnavailableError(original);
617
- if (original instanceof CustomException2 || original instanceof mongoose2.MongooseError) {
618
- return exceptionError(original, translate);
619
- }
620
- if (isOriginalGraphQLError(error))
621
- return error;
622
- return new GraphQLError3(message, {
623
- ...locationOf(error),
624
- extensions: isDev ? {
625
- code: ErrorCode3.InternalServerError,
626
- debugMessage: String(original)
627
- } : { code: ErrorCode3.InternalServerError }
628
- });
652
+ // src/plugins/ory-auth.ts
653
+ function toPrincipal(ory) {
654
+ const email = ory.identity?.email;
655
+ return {
656
+ sub: ory.subject,
657
+ uid: ory.subject,
658
+ name: email ?? ory.clientId ?? ory.subject,
659
+ username: email,
660
+ clientId: ory.clientId,
661
+ scope: ory.scopes.join(" ") || undefined,
662
+ tokenType: ory.kind,
663
+ exp: ory.expiresAt ? Math.floor(ory.expiresAt.getTime() / 1000) : undefined,
664
+ authorities: [],
665
+ roles: []
629
666
  };
630
667
  }
631
- function exceptionError(original, translate) {
632
- const exception = original instanceof mongoose2.MongooseError ? MONGO_UTILS2.castError(original) : original;
633
- const status = exception.code;
634
- return new GraphQLError3(exception.errorCode === ErrorCode3.ValidationError ? exception.message : translate(exception.message, exception.options), {
635
- extensions: {
636
- code: exception.errorCode,
637
- ...status ? { http: { status } } : {},
638
- ...exception.debugMessage ? { debugMessage: exception.debugMessage } : {}
639
- }
640
- });
668
+ function oryUnavailableError(error) {
669
+ return serviceUnavailableError(error);
641
670
  }
642
- function isOriginalGraphQLError(error) {
643
- if (!(error instanceof GraphQLError3))
644
- return false;
645
- return error.originalError == null ? true : isOriginalGraphQLError(error.originalError);
671
+ async function resolveOryPrincipal(ory, headers) {
672
+ let principal;
673
+ try {
674
+ principal = await ory.resolve(headers);
675
+ } catch (error) {
676
+ if (isOryUnavailable(error))
677
+ throw oryUnavailableError(error);
678
+ throw error;
679
+ }
680
+ return {
681
+ ory: principal,
682
+ user: principal ? toPrincipal(principal) : undefined,
683
+ claims: principal ? claimsFromOryPrincipal(principal) : undefined,
684
+ token: bearerOf(headers) ?? undefined
685
+ };
646
686
  }
647
- function locationOf(error) {
648
- if (!(error instanceof GraphQLError3))
649
- return {};
687
+ function useOryAuth(ory) {
650
688
  return {
651
- nodes: error.nodes,
652
- source: error.source,
653
- positions: error.positions,
654
- path: error.path
689
+ onContextBuilding: async ({ context, extendContext }) => {
690
+ extendContext(await resolveOryPrincipal(ory, context.request.headers));
691
+ }
655
692
  };
656
693
  }
657
- // src/utils/publish.ts
658
- import { createRedisEventTarget } from "@graphql-yoga/redis-event-target";
659
- import { createPubSub } from "graphql-yoga";
660
- var createYogaPubSub = ({
661
- publishClient,
662
- subscribeClient
663
- }) => {
664
- const eventTarget = createRedisEventTarget({
665
- publishClient,
666
- subscribeClient
667
- });
668
- return createPubSub({ eventTarget });
694
+ // src/scalars/any/any.scalars.ts
695
+ import { GraphQLScalarType, Kind } from "graphql";
696
+ var ANY_SCALAR = {
697
+ Any: new GraphQLScalarType({
698
+ name: "Any",
699
+ description: "Generic scalar to represent any type",
700
+ serialize(value) {
701
+ return value;
702
+ },
703
+ parseValue(value) {
704
+ return value;
705
+ },
706
+ parseLiteral(ast, variables) {
707
+ switch (ast.kind) {
708
+ case Kind.STRING:
709
+ case Kind.BOOLEAN:
710
+ return ast.value;
711
+ case Kind.INT:
712
+ case Kind.FLOAT:
713
+ return parseFloat(ast.value);
714
+ case Kind.OBJECT:
715
+ return Object.fromEntries(ast.fields.map((field) => [field.name, field.value]));
716
+ case Kind.LIST:
717
+ return ast.values.map((n) => this?.parseLiteral?.(n, variables));
718
+ case Kind.NULL:
719
+ return null;
720
+ case Kind.VARIABLE: {
721
+ const name = ast.name.value;
722
+ return variables ? variables[name] : undefined;
723
+ }
724
+ }
725
+ }
726
+ })
669
727
  };
670
- // src/utils/sandbox.ts
671
- import { html } from "hono/html";
672
- var renderSandbox = ({
673
- port = 8080,
674
- hostname = "localhost",
675
- graphqlEndpoint = "graphql",
676
- protocol = "http",
677
- title = "Sandbox Explorer"
678
- }) => html`
679
- <title>${title}</title>
680
- <div id="sandbox" style="position:absolute;top:0;right:0;bottom:0;left:0"></div>
681
- <script src="https://embeddable-sandbox.cdn.apollographql.com/_latest/embeddable-sandbox.umd.production.min.js"></script>
682
- <script>
683
- new window.EmbeddedSandbox({
684
- target: "#sandbox",
685
- // Pass through your server href if you are embedding on an endpoint.
686
- // Otherwise, you can pass whatever endpoint you want Sandbox to start up with here.
687
- initialEndpoint: "${protocol}://${hostname}:${port}/${graphqlEndpoint}",
688
- handleRequest: (endpointUrl, options) => {
689
- return fetch(endpointUrl, {
690
- ...options,
691
- headers: {
692
- ...options.headers
693
- },
694
- })
695
- },
696
- hideCookieToggle: true,
697
- });
698
- // advanced options: https://www.apollographql.com/docs/studio/explorer/sandbox#embedding-sandbox
699
- </script>
728
+ // src/scalars/custom/custom.scalars.ts
729
+ import {
730
+ AccountNumberResolver,
731
+ BigIntResolver,
732
+ CountryCodeResolver,
733
+ CountryNameResolver,
734
+ CuidResolver,
735
+ CurrencyResolver,
736
+ DateResolver,
737
+ DateTimeISOResolver,
738
+ DurationResolver,
739
+ EmailAddressResolver,
740
+ GeoJSONResolver,
741
+ GUIDResolver,
742
+ HexadecimalResolver,
743
+ HexColorCodeResolver,
744
+ HSLAResolver,
745
+ HSLResolver,
746
+ IBANResolver,
747
+ IPResolver,
748
+ IPv4Resolver,
749
+ IPv6Resolver,
750
+ ISBNResolver,
751
+ JSONObjectResolver,
752
+ JSONResolver,
753
+ JWTResolver,
754
+ LatitudeResolver,
755
+ LocaleResolver,
756
+ LongitudeResolver,
757
+ LongResolver,
758
+ MACResolver,
759
+ NegativeFloatResolver,
760
+ NegativeIntResolver,
761
+ NonEmptyStringResolver,
762
+ NonNegativeFloatResolver,
763
+ NonNegativeIntResolver,
764
+ NonPositiveFloatResolver,
765
+ NonPositiveIntResolver,
766
+ ObjectIDResolver,
767
+ PhoneNumberResolver,
768
+ PortResolver,
769
+ PositiveFloatResolver,
770
+ PositiveIntResolver,
771
+ PostalCodeResolver,
772
+ RGBAResolver,
773
+ RGBResolver,
774
+ SESSNResolver,
775
+ TimeResolver,
776
+ TimestampResolver,
777
+ URLResolver,
778
+ USCurrencyResolver,
779
+ UtcOffsetResolver,
780
+ UUIDResolver,
781
+ VoidResolver
782
+ } from "graphql-scalars";
700
783
 
701
- `;
702
- // src/utils/schema.utils.ts
703
- import { existsSync } from "node:fs";
704
- import { dirname, join } from "node:path";
705
- import { fileURLToPath } from "node:url";
706
- import { buildSubgraphSchema as buildSubgraphSchemaBase } from "@apollo/subgraph";
707
- import { loadFilesSync } from "@graphql-tools/load-files";
708
- import { mergeTypeDefs } from "@graphql-tools/merge";
709
- import { pruneSchema } from "@graphql-tools/utils";
710
- import { printSchema } from "graphql";
711
- function packageRoot() {
712
- let dir = dirname(fileURLToPath(import.meta.url));
713
- while (!existsSync(join(dir, "package.json"))) {
714
- const parent = dirname(dir);
715
- if (parent === dir)
716
- throw new Error("@nxgt/shared-graphql: no package root");
717
- dir = parent;
718
- }
719
- return dir;
720
- }
721
- var SHARED_SCHEMA_PATH = join(packageRoot(), "graphql/**/*.graphqls");
722
- function loadTypeDefs(...paths) {
723
- const typeDefs = mergeTypeDefs([
724
- ...paths.flatMap((path) => loadFilesSync(path, {
725
- recursive: true
726
- }))
727
- ]);
728
- return typeDefs;
729
- }
730
- function buildSubgraphSchema(modulesOrSdl) {
731
- return pruneSchema(buildSubgraphSchemaBase(modulesOrSdl), {
732
- skipUnusedTypesPruning: false
733
- });
734
- }
735
- async function generateSchema(output, ...paths) {
736
- await Bun.write(output, printSchema(buildSubgraphSchema(loadTypeDefs(...paths))));
737
- }
738
- // src/utils/websocket.ts
739
- import { createServer } from "node:http";
740
- import { useServer } from "graphql-ws/use/ws";
741
- import { WebSocketServer } from "ws";
742
- function setupYogaWebSocketServer({
743
- yoga,
744
- context
784
+ // src/scalars/custom/utils.ts
785
+ import { CustomException } from "@nxgt/shared-exceptions";
786
+ import { logger } from "@nxgt/shared-logging";
787
+ import { GraphQLScalarType as GraphQLScalarType2 } from "graphql";
788
+ function createScalarFrom(scalar, {
789
+ name,
790
+ description,
791
+ errorMessage
745
792
  }) {
746
- const httpServer = createServer(yoga);
747
- const wsServer = new WebSocketServer({
748
- server: httpServer,
749
- path: yoga.graphqlEndpoint
750
- });
751
- useServer({
752
- execute: (args) => args.rootValue.execute(args),
753
- subscribe: (args) => args.rootValue.subscribe(args),
754
- onSubscribe: async (ctx, _id, params) => {
755
- const { schema, execute, subscribe, contextFactory, parse, validate } = yoga.getEnveloped({
756
- ...ctx,
757
- req: ctx.extra.request,
758
- socket: ctx.extra.socket,
759
- params
760
- });
761
- const args = {
762
- schema,
763
- operationName: params.operationName,
764
- document: parse(params.query),
765
- variableValues: params.variables,
766
- contextValue: await contextFactory(),
767
- rootValue: {
768
- execute,
769
- subscribe
770
- }
771
- };
772
- const errors = validate(args.schema, args.document);
773
- if (errors.length)
774
- return errors;
775
- return args;
776
- },
777
- context
778
- }, wsServer);
779
- const startServer = (port) => new Promise((resolve) => httpServer.listen({ port }, resolve));
780
- return { yoga, httpServer, startServer };
781
- }
782
- // src/utils/ws-context.ts
783
- async function resolveWsUser(connectionParams, authClient) {
784
- const rawToken = connectionParams?.authorization ?? connectionParams?.Authorization;
785
- const token = rawToken?.replace("Bearer ", "");
786
- const { data } = token ? await authClient.POST("/oauth/introspect", { body: { token } }) : { data: null };
787
- const user = data?.active ? { ...data, name: data.sub } : undefined;
788
- return { user, token };
789
- }
790
- // src/integrations/hono.ts
791
- function sandboxExpolorer(options) {
792
- return createMiddleware(async (ctx) => {
793
- return ctx.html(renderSandbox(options));
794
- });
795
- }
796
- function honoYoga(yoga) {
797
- return createMiddleware(async (ctx) => {
798
- const response = await yoga.fetch(ctx.req.raw);
799
- return response;
800
- });
801
- }
802
- function createYogaHono(yoga, options) {
803
- const app = new Hono;
804
- app.use(contextStorage());
805
- app.use(languageDetector({
806
- order: ["header"],
807
- supportedLanguages: ["en", "fr"],
808
- fallbackLanguage: "en",
809
- caches: []
810
- }));
811
- app.get("/health", (ctx) => {
812
- return ctx.json({ status: "ok" }, 200);
793
+ const exception = CustomException.badRequest({
794
+ message: errorMessage
813
795
  });
814
- app.get(options?.sandbox?.endpoint || "sandbox", sandboxExpolorer(options?.sandbox || {}));
815
- app.use(yoga.graphqlEndpoint, honoYoga(yoga));
816
- return app;
817
- }
818
- // src/plugins/apply-keto-checks.ts
819
- import { getDirective as getDirective3, MapperKind, mapSchema } from "@graphql-tools/utils";
820
- import { evaluateRequirement } from "@nxgt/ory-sdk";
821
- import { CustomException as CustomException3 } from "@nxgt/shared-exceptions";
822
- import {
823
- defaultFieldResolver
824
- } from "graphql";
825
- function applyKetoChecks(schema, options = {}) {
826
- return mapSchema(schema, {
827
- [MapperKind.INTERFACE_FIELD]: (fieldConfig, fieldName, typeName) => {
828
- refuseOnInterface(schema, fieldConfig, `${typeName}.${fieldName}`);
829
- return fieldConfig;
796
+ return new GraphQLScalarType2({
797
+ ...scalar,
798
+ name: name ?? scalar.name,
799
+ description: (description ?? scalar.description)?.replaceAll("`", "'"),
800
+ serialize(value) {
801
+ if (!errorMessage) {
802
+ return scalar.serialize(value);
803
+ }
804
+ try {
805
+ return scalar.serialize(value);
806
+ } catch (e) {
807
+ logger.error(e);
808
+ throw exception;
809
+ }
830
810
  },
831
- [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
832
- const where = `${typeName}.${fieldName}`;
833
- const requirements = readRequirements(schema, fieldConfig, where, options);
834
- if (requirements.length === 0)
835
- return fieldConfig;
836
- return guarded(fieldConfig, requirements, where);
837
- }
838
- });
839
- }
840
- function refuseOnInterface(schema, fieldConfig, where) {
841
- const message = `${where}: @check / @permission on an interface field guards nothing — no resolver runs there. Put it on each implementing type's field`;
842
- if (readPermissions(schema, fieldConfig, where).length > 0) {
843
- throw new TypeError(message);
844
- }
845
- if (getDirective3(schema, fieldConfig, CHECK_DIRECTIVE_NAME)?.length) {
846
- warnCheckMistake(message);
847
- }
848
- }
849
- function guarded(fieldConfig, requirements, where) {
850
- const resolve = fieldConfig.resolve ?? defaultFieldResolver;
851
- return {
852
- ...fieldConfig,
853
- resolve: async (source, args, context, info) => {
854
- const ctx = context;
855
- const subject = ctx.ory?.subject;
856
- if (!subject) {
857
- throw CustomException3.unauthenticated({
858
- message: "errors.unauthenticated"
859
- });
811
+ parseValue(value) {
812
+ if (!errorMessage) {
813
+ return scalar.parseValue(value);
860
814
  }
861
- const check = ctx.ketoChecks;
862
- if (!check) {
863
- throw new Error(`${where}: no checker on the context — useKetoChecks(ory) is not registered`);
815
+ try {
816
+ return scalar.parseValue(value);
817
+ } catch (e) {
818
+ logger.error(e);
819
+ throw exception;
864
820
  }
865
- for (const { permissions, onDeny, message } of requirements) {
866
- const allowed = await evaluateRequirement(permissions, (term) => objectsOf(term, source, args, where), check, subject);
867
- if (!allowed)
868
- throw denial(onDeny, message);
869
- }
870
- return resolve(source, args, context, info);
871
- }
872
- };
873
- }
874
- function objectsOf(term, source, args, where) {
875
- const ids = objectIds(readPath(term.id, source, args));
876
- if (ids.length === 0) {
877
- throw new Error(`${where}: "${term.id}" resolved no object id`);
878
- }
879
- return ids;
880
- }
881
- function denial(onDeny, message) {
882
- return onDeny === "FORBIDDEN" ? CustomException3.forbidden({
883
- message: message ?? "errors.insufficient-permissions"
884
- }) : CustomException3.notFound({ message: message ?? "errors.not-found" });
885
- }
886
- // src/plugins/auth.ts
887
- function useAuth() {
888
- return {
889
- onContextBuilding: async ({ context, extendContext }) => {
890
- extendContext({
891
- user: context.params.extensions?.user,
892
- token: context.params.extensions?.token
893
- });
894
- }
895
- };
896
- }
897
- // src/plugins/extract-jwt.ts
898
- import { logger as logger2 } from "@nxgt/shared-logging";
899
- var extractJwtPlugin = {
900
- async requestDidStart({ request, contextValue }) {
901
- logger2.info(`[${request.extensions?.subgraphName}] Extracting JWT from request extensions: ${request.extensions?.payload?.sub}`, request.extensions);
902
- contextValue.jwt = {
903
- payload: request.extensions?.payload
904
- };
905
- }
906
- };
907
- // src/plugins/keto-checker.ts
908
- import { tuple } from "@nxgt/ory-sdk";
909
- import DataLoader2 from "dataloader";
910
- function createKetoChecks(ory) {
911
- const loader = new DataLoader2((questions) => ory.checkMany([...questions]), {
912
- cacheKeyFn: ({ permission, subject }) => tuple(permission, subject)
913
- });
914
- return (permission, subject) => loader.load({ permission, subject });
915
- }
916
- // src/plugins/keto-checks.ts
917
- function useKetoChecks(ory, options = {}) {
918
- const transformed = new WeakSet;
919
- return {
920
- onSchemaChange: ({ schema, replaceSchema }) => {
921
- if (transformed.has(schema))
922
- return;
923
- const next = applyKetoChecks(schema, options);
924
- transformed.add(next);
925
- replaceSchema(next);
926
- },
927
- onContextBuilding: ({ extendContext }) => {
928
- extendContext({ ketoChecks: createKetoChecks(ory) });
929
- }
930
- };
931
- }
932
- // src/plugins/keto-helpers.ts
933
- import { CustomException as CustomException4 } from "@nxgt/shared-exceptions";
934
- function requireUser(context) {
935
- if (!context.user) {
936
- throw CustomException4.unauthenticated({
937
- message: "errors.unauthenticated"
938
- });
939
- }
940
- return context.user;
941
- }
942
- async function can(context, question) {
943
- const subject = context.ory?.subject;
944
- if (!subject) {
945
- throw CustomException4.unauthenticated({
946
- message: "errors.unauthenticated"
947
- });
948
- }
949
- if (!context.ketoChecks) {
950
- throw new Error("can(): no checker on the context — useKetoChecks(ory) is not registered");
951
- }
952
- const permission = {
953
- namespace: question.type,
954
- object: question.id,
955
- relation: question.name
956
- };
957
- return context.ketoChecks(permission, subject);
958
- }
959
- // src/plugins/ory-auth.ts
960
- import {
961
- bearerOf
962
- } from "@nxgt/ory-sdk";
963
- import { claimsFromOryPrincipal } from "@nxgt/security/integrations/ory";
964
- function toPrincipal(ory) {
965
- const email = ory.identity?.email;
966
- return {
967
- sub: ory.subject,
968
- uid: ory.subject,
969
- name: email ?? ory.clientId ?? ory.subject,
970
- username: email,
971
- clientId: ory.clientId,
972
- scope: ory.scopes.join(" ") || undefined,
973
- tokenType: ory.kind,
974
- exp: ory.expiresAt ? Math.floor(ory.expiresAt.getTime() / 1000) : undefined,
975
- authorities: [],
976
- roles: []
977
- };
978
- }
979
- function oryUnavailableError(error) {
980
- return serviceUnavailableError(error);
981
- }
982
- async function resolveOryPrincipal(ory, headers) {
983
- let principal;
984
- try {
985
- principal = await ory.resolve(headers);
986
- } catch (error) {
987
- if (isOryUnavailable(error))
988
- throw oryUnavailableError(error);
989
- throw error;
990
- }
991
- return {
992
- ory: principal,
993
- user: principal ? toPrincipal(principal) : undefined,
994
- claims: principal ? claimsFromOryPrincipal(principal) : undefined,
995
- token: bearerOf(headers) ?? undefined
996
- };
997
- }
998
- function useOryAuth(ory) {
999
- return {
1000
- onContextBuilding: async ({ context, extendContext }) => {
1001
- extendContext(await resolveOryPrincipal(ory, context.request.headers));
1002
- }
1003
- };
1004
- }
1005
- // src/scalars/any/any.scalars.ts
1006
- import { GraphQLScalarType, Kind } from "graphql";
1007
- var ANY_SCALAR = {
1008
- Any: new GraphQLScalarType({
1009
- name: "Any",
1010
- description: "Generic scalar to represent any type",
1011
- serialize(value) {
1012
- return value;
1013
- },
1014
- parseValue(value) {
1015
- return value;
1016
- },
1017
- parseLiteral(ast, variables) {
1018
- switch (ast.kind) {
1019
- case Kind.STRING:
1020
- case Kind.BOOLEAN:
1021
- return ast.value;
1022
- case Kind.INT:
1023
- case Kind.FLOAT:
1024
- return parseFloat(ast.value);
1025
- case Kind.OBJECT:
1026
- return Object.fromEntries(ast.fields.map((field) => [field.name, field.value]));
1027
- case Kind.LIST:
1028
- return ast.values.map((n) => this?.parseLiteral?.(n, variables));
1029
- case Kind.NULL:
1030
- return null;
1031
- case Kind.VARIABLE: {
1032
- const name = ast.name.value;
1033
- return variables ? variables[name] : undefined;
1034
- }
1035
- }
1036
- }
1037
- })
1038
- };
1039
- // src/scalars/custom/custom.scalars.ts
1040
- import {
1041
- AccountNumberResolver,
1042
- BigIntResolver,
1043
- CountryCodeResolver,
1044
- CountryNameResolver,
1045
- CuidResolver,
1046
- CurrencyResolver,
1047
- DateResolver,
1048
- DateTimeISOResolver,
1049
- DurationResolver,
1050
- EmailAddressResolver,
1051
- GeoJSONResolver,
1052
- GUIDResolver,
1053
- HexadecimalResolver,
1054
- HexColorCodeResolver,
1055
- HSLAResolver,
1056
- HSLResolver,
1057
- IBANResolver,
1058
- IPResolver,
1059
- IPv4Resolver,
1060
- IPv6Resolver,
1061
- ISBNResolver,
1062
- JSONObjectResolver,
1063
- JSONResolver,
1064
- JWTResolver,
1065
- LatitudeResolver,
1066
- LocaleResolver,
1067
- LongitudeResolver,
1068
- LongResolver,
1069
- MACResolver,
1070
- NegativeFloatResolver,
1071
- NegativeIntResolver,
1072
- NonEmptyStringResolver,
1073
- NonNegativeFloatResolver,
1074
- NonNegativeIntResolver,
1075
- NonPositiveFloatResolver,
1076
- NonPositiveIntResolver,
1077
- ObjectIDResolver,
1078
- PhoneNumberResolver,
1079
- PortResolver,
1080
- PositiveFloatResolver,
1081
- PositiveIntResolver,
1082
- PostalCodeResolver,
1083
- RGBAResolver,
1084
- RGBResolver,
1085
- SESSNResolver,
1086
- TimeResolver,
1087
- TimestampResolver,
1088
- URLResolver,
1089
- USCurrencyResolver,
1090
- UtcOffsetResolver,
1091
- UUIDResolver,
1092
- VoidResolver
1093
- } from "graphql-scalars";
1094
-
1095
- // src/scalars/custom/utils.ts
1096
- import { CustomException as CustomException5 } from "@nxgt/shared-exceptions";
1097
- import { logger as logger3 } from "@nxgt/shared-logging";
1098
- import { GraphQLScalarType as GraphQLScalarType2 } from "graphql";
1099
- function createScalarFrom(scalar, {
1100
- name,
1101
- description,
1102
- errorMessage
1103
- }) {
1104
- const exception = CustomException5.badRequest({
1105
- message: errorMessage
1106
- });
1107
- return new GraphQLScalarType2({
1108
- ...scalar,
1109
- name: name ?? scalar.name,
1110
- description: (description ?? scalar.description)?.replaceAll("`", "'"),
1111
- serialize(value) {
1112
- if (!errorMessage) {
1113
- return scalar.serialize(value);
1114
- }
1115
- try {
1116
- return scalar.serialize(value);
1117
- } catch (e) {
1118
- logger3.error(e);
1119
- throw exception;
1120
- }
1121
- },
1122
- parseValue(value) {
1123
- if (!errorMessage) {
1124
- return scalar.parseValue(value);
1125
- }
1126
- try {
1127
- return scalar.parseValue(value);
1128
- } catch (e) {
1129
- logger3.error(e);
1130
- throw exception;
1131
- }
1132
- },
1133
- parseLiteral(ast, variables) {
1134
- if (!errorMessage) {
1135
- return scalar.parseLiteral(ast, variables);
1136
- }
1137
- try {
1138
- return scalar.parseLiteral(ast, variables);
1139
- } catch (e) {
1140
- logger3.error(e);
1141
- throw exception;
821
+ },
822
+ parseLiteral(ast, variables) {
823
+ if (!errorMessage) {
824
+ return scalar.parseLiteral(ast, variables);
825
+ }
826
+ try {
827
+ return scalar.parseLiteral(ast, variables);
828
+ } catch (e) {
829
+ logger.error(e);
830
+ throw exception;
1142
831
  }
1143
832
  }
1144
833
  });
@@ -1328,8 +1017,7 @@ type Subscription {
1328
1017
  _empty: String
1329
1018
  }
1330
1019
 
1331
- directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
1332
-
1020
+ ${AUTHENTICATED_DIRECTIVE_SDL}
1333
1021
  directive @policy(policies: [[String!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
1334
1022
 
1335
1023
  directive @shareable on OBJECT | FIELD_DEFINITION
@@ -1339,11 +1027,429 @@ directive @link(url: String!, import: [String!]) on SCHEMA
1339
1027
  `;
1340
1028
  // src/upload.ts
1341
1029
  import graphqlUploadExpress from "graphql-upload/graphqlUploadExpress.mjs";
1030
+ // src/utils/change-resolvers.utils.ts
1031
+ import {
1032
+ AUDIT_EVENT,
1033
+ CommitType
1034
+ } from "@nxgt/shared-mongo";
1035
+ import { withFilter } from "graphql-subscriptions";
1036
+ function createChangeResolvers(config) {
1037
+ const { entity, collection, service, pubsub } = config;
1038
+ const resolvePubSub = () => typeof pubsub === "function" ? pubsub() : pubsub;
1039
+ return {
1040
+ Query: {
1041
+ [`${entity}Changes`]: async (_parent, args, context) => context.services.audit.findChangesPaginatedByCollection(collection, args),
1042
+ [`${entity}ChangesById`]: async (_parent, args, context) => context.services.audit.findChangesPaginatedByCollectionAndOid(args.id, collection, args)
1043
+ },
1044
+ Subscription: {
1045
+ [`${entity}Changed`]: {
1046
+ resolve: async (payload, _args, context) => ({
1047
+ id: context.services.audit.extractId(payload),
1048
+ data: payload.type !== CommitType.TERMINAL ? await context.services[service].findById(payload.state._id) : null,
1049
+ type: payload.type
1050
+ }),
1051
+ subscribe: withFilter(() => resolvePubSub().subscribe(AUDIT_EVENT), (payload, _variables, context) => context ? context.services.audit.matches(payload, { collection }) : false)
1052
+ }
1053
+ }
1054
+ };
1055
+ }
1056
+ // src/utils/codegen.utils.ts
1057
+ var SCALARS_MAPPING = {
1058
+ ID: "any",
1059
+ DateTime: "any",
1060
+ Date: "any",
1061
+ Any: "any",
1062
+ BigInt: "number",
1063
+ EmailAddress: "string",
1064
+ GeoJSON: "any",
1065
+ HSL: "string",
1066
+ HSLA: "string",
1067
+ HexColorCode: "string",
1068
+ Hexadecimal: "string",
1069
+ IBAN: "string",
1070
+ IP: "string",
1071
+ Time: "string",
1072
+ LocalTime: "string",
1073
+ UUID: "string",
1074
+ PositiveInt: "number",
1075
+ NegativeInt: "number",
1076
+ NonPositiveInt: "number",
1077
+ NonNegativeInt: "number",
1078
+ PositiveFloat: "number",
1079
+ NegativeFloat: "number",
1080
+ NonPositiveFloat: "number",
1081
+ NonNegativeFloat: "number",
1082
+ NonEmptyString: "string",
1083
+ PostalCode: "string",
1084
+ RGB: "string",
1085
+ RGBA: "string",
1086
+ SESSN: "string",
1087
+ Timestamp: "string",
1088
+ URL: "string",
1089
+ USCurrency: "string",
1090
+ UTCOffset: "string",
1091
+ Long: "number",
1092
+ Short: "number",
1093
+ Byte: "number",
1094
+ BigDecimal: "string",
1095
+ BigInteger: "string",
1096
+ Char: "string",
1097
+ Locale: "string",
1098
+ CountryCode: "string",
1099
+ CountryName: "string",
1100
+ AccountNumber: "string",
1101
+ CurrencyCode: "string",
1102
+ Duration: "string",
1103
+ Url: "string",
1104
+ JSONObject: "Record<string, any>",
1105
+ JSON: "any",
1106
+ Void: "void",
1107
+ ObjectID: "any",
1108
+ Upload: "File | Blob",
1109
+ Email: "string",
1110
+ Port: "number",
1111
+ IPv4: "string",
1112
+ IPv6: "string",
1113
+ MAC: "string",
1114
+ Password: "string",
1115
+ PhoneNumber: "string",
1116
+ ZIPCode: "string",
1117
+ SSN: "string",
1118
+ GUID: "string",
1119
+ ISBN: "string",
1120
+ Latitude: "number",
1121
+ Longitude: "number",
1122
+ JWT: "string"
1123
+ };
1124
+ // src/utils/data-loader.utils.ts
1125
+ import { cast } from "@nxgt/shared/helpers";
1126
+ import {
1127
+ paginateList
1128
+ } from "@nxgt/shared-mongo";
1129
+ import DataLoader2 from "dataloader";
1130
+ import { isArray } from "lodash";
1131
+ function createDataLoader(batchLoadFn, options) {
1132
+ return new DataLoader2(batchLoadFn, options);
1133
+ }
1134
+ function createConnectionListDataLoader(path, model, options) {
1135
+ return createDataLoader(async (docs) => {
1136
+ const data = await model.find({
1137
+ ...options.filter ?? {},
1138
+ _id: docs.flatMap((doc) => {
1139
+ const ids = doc[path] ?? [];
1140
+ return isArray(ids) ? cast(ids.filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))) : [];
1141
+ })
1142
+ }).exec();
1143
+ return cast(docs.map((doc) => model.paginateList(data.filter((child) => cast(doc[path] ?? []).map((item) => item.toString()).includes(child._id.toString())), options)));
1144
+ });
1145
+ }
1146
+ function createChildrenConnectionDataLoader(path, model, options) {
1147
+ return createDataLoader(async (docs) => {
1148
+ const filter = {};
1149
+ filter[path] = docs.map((doc) => doc._id);
1150
+ const data = await model.find(filter).exec();
1151
+ return cast(docs.map((doc) => paginateList(data.filter((child) => doc._id?.toString() === child[path]?.toString()), options)));
1152
+ });
1153
+ }
1154
+ function createSimpleListDataLoader(path, model, options) {
1155
+ return createDataLoader(async (docs) => {
1156
+ const filter = options?.filter ?? {};
1157
+ const ids = docs.flatMap((doc) => {
1158
+ const ids = doc[path] ?? [];
1159
+ return isArray(ids) ? cast(ids) : [];
1160
+ });
1161
+ filter._id = ids;
1162
+ const query = model.find(filter);
1163
+ const data = await query.exec();
1164
+ return cast(docs.map((doc) => {
1165
+ const result = data.filter((child) => cast(doc[path] ?? []).map((item) => item.toString()).includes(child._id.toString()));
1166
+ if (options?.skip && options?.limit) {
1167
+ return result.slice(options.skip, options.skip + options.limit);
1168
+ }
1169
+ if (options?.limit) {
1170
+ return result.slice(0, options.limit);
1171
+ }
1172
+ if (options?.skip) {
1173
+ return result.slice(options.skip);
1174
+ }
1175
+ return result;
1176
+ }));
1177
+ });
1178
+ }
1179
+ function createChildrenListDataLoader(path, model, options) {
1180
+ return createDataLoader(async (docs) => {
1181
+ const filter = options?.filter ?? {};
1182
+ const key = path;
1183
+ filter[key] = docs.map((doc) => doc._id);
1184
+ const query = model.find(filter);
1185
+ const data = await query.exec();
1186
+ return cast(docs.map((doc) => {
1187
+ const result = data.filter((child) => child[path]?.toString() === doc._id.toString());
1188
+ if (options?.skip && options?.limit) {
1189
+ return result.slice(options.skip, options.skip + options.limit);
1190
+ }
1191
+ if (options?.limit) {
1192
+ return result.slice(0, options.limit);
1193
+ }
1194
+ if (options?.skip) {
1195
+ return result.slice(options.skip);
1196
+ }
1197
+ return result;
1198
+ }));
1199
+ });
1200
+ }
1201
+ function createSingleChildDataLoader(path, model) {
1202
+ return createDataLoader(async (docs) => {
1203
+ const data = await model.find({
1204
+ _id: docs.map((doc) => cast(doc[path])).filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))
1205
+ }).exec();
1206
+ const map = new Map(data.map((doc) => [doc.id, doc]));
1207
+ return docs.map((doc) => map.get(doc[path]?.toString() ?? "") ?? null);
1208
+ });
1209
+ }
1210
+ function createEntitiesReferenceLoader(model) {
1211
+ return createDataLoader(async (ids) => {
1212
+ const docs = await model.find({
1213
+ _id: ids.filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))
1214
+ }).exec();
1215
+ const map = new Map(docs.map((doc) => [doc.id, doc]));
1216
+ return ids.map((id) => map.get(id) ?? null);
1217
+ });
1218
+ }
1219
+ // src/utils/errors/create-error.ts
1220
+ import { GraphQLError as GraphQLError3 } from "graphql";
1221
+ function createGraphQLError(message, options) {
1222
+ return new GraphQLError3(message, options);
1223
+ }
1224
+ // src/utils/errors/format-error.ts
1225
+ import {
1226
+ ApolloServerErrorCode,
1227
+ unwrapResolverError
1228
+ } from "@apollo/server/errors";
1229
+ import {
1230
+ translate as translateBase
1231
+ } from "@nxgt/i18n";
1232
+ import { CustomException as CustomException2, ErrorCode as ErrorCode6 } from "@nxgt/shared-exceptions";
1233
+ import { MONGO_UTILS, mongoose } from "@nxgt/shared-mongo";
1234
+ import { kebabCase } from "lodash";
1235
+ function createFormatError(translate = translateBase, production) {
1236
+ return (formattedError, graphQLError) => {
1237
+ if (production && formattedError.extensions?.stacktrace) {
1238
+ formattedError.extensions.stacktrace = undefined;
1239
+ }
1240
+ const error = unwrapResolverError(graphQLError);
1241
+ if (isOryUnavailable(error)) {
1242
+ const unavailable = serviceUnavailableError(error);
1243
+ return {
1244
+ ...formattedError,
1245
+ message: unavailable.message,
1246
+ extensions: { ...formattedError.extensions, ...unavailable.extensions }
1247
+ };
1248
+ }
1249
+ const key = denialMessageKey(error);
1250
+ if (key !== undefined) {
1251
+ return { ...formattedError, message: translate(key) };
1252
+ }
1253
+ if (error instanceof CustomException2 || error instanceof mongoose.MongooseError) {
1254
+ const exception = error instanceof mongoose.MongooseError ? MONGO_UTILS.castError(error) : error;
1255
+ return {
1256
+ ...formattedError,
1257
+ message: exception.errorCode === ErrorCode6.ValidationError ? exception.message : translate(exception.message, exception.options),
1258
+ extensions: {
1259
+ ...formattedError.extensions,
1260
+ code: exception.errorCode,
1261
+ debugMessage: exception.debugMessage
1262
+ }
1263
+ };
1264
+ }
1265
+ if ([
1266
+ ApolloServerErrorCode.GRAPHQL_VALIDATION_FAILED,
1267
+ ApolloServerErrorCode.BAD_REQUEST,
1268
+ ApolloServerErrorCode.BAD_USER_INPUT,
1269
+ ApolloServerErrorCode.GRAPHQL_PARSE_FAILED,
1270
+ ApolloServerErrorCode.OPERATION_RESOLUTION_FAILURE,
1271
+ ApolloServerErrorCode.PERSISTED_QUERY_NOT_FOUND,
1272
+ ApolloServerErrorCode.PERSISTED_QUERY_NOT_SUPPORTED
1273
+ ].includes(formattedError.extensions?.code)) {
1274
+ return {
1275
+ ...formattedError,
1276
+ message: translate(`errors.${kebabCase(formattedError.extensions?.code)}`, {}),
1277
+ extensions: {
1278
+ ...formattedError.extensions,
1279
+ debugMessage: formattedError.message
1280
+ }
1281
+ };
1282
+ }
1283
+ return formattedError;
1284
+ };
1285
+ }
1286
+ // src/utils/errors/mask-error.ts
1287
+ import {
1288
+ translate as translateBase2
1289
+ } from "@nxgt/i18n";
1290
+ import { CustomException as CustomException3, ErrorCode as ErrorCode7 } from "@nxgt/shared-exceptions";
1291
+ import { MONGO_UTILS as MONGO_UTILS2, mongoose as mongoose2 } from "@nxgt/shared-mongo";
1292
+ import { GraphQLError as GraphQLError4 } from "graphql";
1293
+ function createMaskError(translate = translateBase2) {
1294
+ return (error, message, isDev) => {
1295
+ const original = error instanceof GraphQLError4 && error.originalError ? error.originalError : error;
1296
+ if (isOryUnavailable(original))
1297
+ return serviceUnavailableError(original);
1298
+ const key = denialMessageKey(original);
1299
+ if (key !== undefined) {
1300
+ return new GraphQLError4(translate(key), {
1301
+ ...locationOf(error),
1302
+ extensions: original.extensions
1303
+ });
1304
+ }
1305
+ if (original instanceof CustomException3 || original instanceof mongoose2.MongooseError) {
1306
+ return exceptionError(original, translate);
1307
+ }
1308
+ if (isOriginalGraphQLError(error))
1309
+ return error;
1310
+ return new GraphQLError4(message, {
1311
+ ...locationOf(error),
1312
+ extensions: isDev ? {
1313
+ code: ErrorCode7.InternalServerError,
1314
+ debugMessage: String(original)
1315
+ } : { code: ErrorCode7.InternalServerError }
1316
+ });
1317
+ };
1318
+ }
1319
+ function exceptionError(original, translate) {
1320
+ const exception = original instanceof mongoose2.MongooseError ? MONGO_UTILS2.castError(original) : original;
1321
+ const status = exception.code;
1322
+ return new GraphQLError4(exception.errorCode === ErrorCode7.ValidationError ? exception.message : translate(exception.message, exception.options), {
1323
+ extensions: {
1324
+ code: exception.errorCode,
1325
+ ...status ? { http: { status } } : {},
1326
+ ...exception.debugMessage ? { debugMessage: exception.debugMessage } : {}
1327
+ }
1328
+ });
1329
+ }
1330
+ function isOriginalGraphQLError(error) {
1331
+ if (!(error instanceof GraphQLError4))
1332
+ return false;
1333
+ return error.originalError == null ? true : isOriginalGraphQLError(error.originalError);
1334
+ }
1335
+ function locationOf(error) {
1336
+ if (!(error instanceof GraphQLError4))
1337
+ return {};
1338
+ return {
1339
+ nodes: error.nodes,
1340
+ source: error.source,
1341
+ positions: error.positions,
1342
+ path: error.path
1343
+ };
1344
+ }
1345
+ // src/utils/publish.ts
1346
+ import { createRedisEventTarget } from "@graphql-yoga/redis-event-target";
1347
+ import { createPubSub } from "graphql-yoga";
1348
+ var createYogaPubSub = ({
1349
+ publishClient,
1350
+ subscribeClient
1351
+ }) => {
1352
+ const eventTarget = createRedisEventTarget({
1353
+ publishClient,
1354
+ subscribeClient
1355
+ });
1356
+ return createPubSub({ eventTarget });
1357
+ };
1358
+ // src/utils/schema.utils.ts
1359
+ import { existsSync } from "node:fs";
1360
+ import { dirname, join } from "node:path";
1361
+ import { fileURLToPath } from "node:url";
1362
+ import { buildSubgraphSchema as buildSubgraphSchemaBase } from "@apollo/subgraph";
1363
+ import { loadFilesSync } from "@graphql-tools/load-files";
1364
+ import { mergeTypeDefs } from "@graphql-tools/merge";
1365
+ import { pruneSchema } from "@graphql-tools/utils";
1366
+ import { printSchema } from "graphql";
1367
+ function packageRoot() {
1368
+ let dir = dirname(fileURLToPath(import.meta.url));
1369
+ while (!existsSync(join(dir, "package.json"))) {
1370
+ const parent = dirname(dir);
1371
+ if (parent === dir)
1372
+ throw new Error("@nxgt/shared-graphql: no package root");
1373
+ dir = parent;
1374
+ }
1375
+ return dir;
1376
+ }
1377
+ var SHARED_SCHEMA_PATH = join(packageRoot(), "graphql/**/*.graphqls");
1378
+ function loadTypeDefs(...paths) {
1379
+ const typeDefs = mergeTypeDefs([
1380
+ ...paths.flatMap((path) => loadFilesSync(path, {
1381
+ recursive: true
1382
+ }))
1383
+ ]);
1384
+ return typeDefs;
1385
+ }
1386
+ function buildSubgraphSchema(modulesOrSdl) {
1387
+ return pruneSchema(buildSubgraphSchemaBase(modulesOrSdl), {
1388
+ skipUnusedTypesPruning: false
1389
+ });
1390
+ }
1391
+ async function generateSchema(output, ...paths) {
1392
+ await Bun.write(output, printSchema(buildSubgraphSchema(loadTypeDefs(...paths))));
1393
+ }
1394
+ // src/utils/websocket.ts
1395
+ import { createServer } from "node:http";
1396
+ import { useServer } from "graphql-ws/use/ws";
1397
+ import { WebSocketServer } from "ws";
1398
+ function setupYogaWebSocketServer({
1399
+ yoga,
1400
+ context
1401
+ }) {
1402
+ const httpServer = createServer(yoga);
1403
+ const wsServer = new WebSocketServer({
1404
+ server: httpServer,
1405
+ path: yoga.graphqlEndpoint
1406
+ });
1407
+ useServer({
1408
+ execute: (args) => args.rootValue.execute(args),
1409
+ subscribe: (args) => args.rootValue.subscribe(args),
1410
+ onSubscribe: async (ctx, _id, params) => {
1411
+ const { schema, execute, subscribe, contextFactory, parse, validate } = yoga.getEnveloped({
1412
+ ...ctx,
1413
+ req: ctx.extra.request,
1414
+ socket: ctx.extra.socket,
1415
+ params
1416
+ });
1417
+ const args = {
1418
+ schema,
1419
+ operationName: params.operationName,
1420
+ document: parse(params.query),
1421
+ variableValues: params.variables,
1422
+ contextValue: await contextFactory(),
1423
+ rootValue: {
1424
+ execute,
1425
+ subscribe
1426
+ }
1427
+ };
1428
+ const errors = validate(args.schema, args.document);
1429
+ if (errors.length)
1430
+ return errors;
1431
+ return args;
1432
+ },
1433
+ context
1434
+ }, wsServer);
1435
+ const startServer = (port) => new Promise((resolve) => httpServer.listen({ port }, resolve));
1436
+ return { yoga, httpServer, startServer };
1437
+ }
1438
+ // src/utils/ws-context.ts
1439
+ async function resolveWsUser(connectionParams, authClient) {
1440
+ const rawToken = connectionParams?.authorization ?? connectionParams?.Authorization;
1441
+ const token = rawToken?.replace("Bearer ", "");
1442
+ const { data } = token ? await authClient.POST("/oauth/introspect", { body: { token } }) : { data: null };
1443
+ const user = data?.active ? { ...data, name: data.sub } : undefined;
1444
+ return { user, token };
1445
+ }
1342
1446
  export {
1343
- CHECK_DIRECTIVE_NAME,
1344
- CHECK_DIRECTIVE_SDL,
1447
+ AUTHENTICATED_DIRECTIVE_NAME,
1448
+ AUTHENTICATED_DIRECTIVE_SDL,
1449
+ CALLER_TYPES,
1345
1450
  DataLoader3 as DataLoader,
1346
1451
  FEDERATION_DIRECTIVES,
1452
+ GATEWAY_SECRET_HEADER,
1347
1453
  KETO_DIRECTIVES_SDL,
1348
1454
  PERMISSION_DIRECTIVE_NAME,
1349
1455
  PERMISSION_DIRECTIVE_SDL,
@@ -1351,6 +1457,7 @@ export {
1351
1457
  SCALAR_RESOLVERS,
1352
1458
  SHARED_SCHEMA_PATH,
1353
1459
  SHARED_TYPE_DEFS,
1460
+ applyAuthenticated,
1354
1461
  applyKetoChecks,
1355
1462
  assertReadablePath,
1356
1463
  buildSubgraphSchema,
@@ -1369,7 +1476,10 @@ export {
1369
1476
  createSingleChildDataLoader,
1370
1477
  createYogaHono,
1371
1478
  createYogaPubSub,
1479
+ denial,
1480
+ denialMessageKey,
1372
1481
  extractJwtPlugin,
1482
+ gatewaySecret,
1373
1483
  generateSchema,
1374
1484
  graphqlUploadExpress,
1375
1485
  honoYoga,
@@ -1377,22 +1487,23 @@ export {
1377
1487
  loadTypeDefs,
1378
1488
  objectIds,
1379
1489
  oryUnavailableError,
1380
- readChecks,
1490
+ principalOf,
1381
1491
  readPath,
1382
1492
  readPermissions,
1383
- readRequirements,
1384
1493
  renderSandbox,
1494
+ requireGatewayTrust,
1385
1495
  requireUser,
1386
1496
  resolveOryPrincipal,
1387
1497
  resolveWsUser,
1388
- sandboxExpolorer,
1498
+ sandboxExplorer,
1389
1499
  serviceUnavailableError,
1390
1500
  setupYogaWebSocketServer,
1391
1501
  toPrincipal,
1392
1502
  useAuth,
1503
+ useAuthenticated,
1393
1504
  useKetoChecks,
1394
1505
  useOryAuth
1395
1506
  };
1396
1507
 
1397
- //# debugId=E72214B01404888264756E2164756E21
1508
+ //# debugId=A1D566582D44C98864756E2164756E21
1398
1509
  //# sourceMappingURL=index.js.map