@nxgt/shared-graphql 2.1.0 → 3.0.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.
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 +1055 -927
  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 +47 -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,442 @@ 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);
510
- });
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
511
  }
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);
512
+ function refusal(onDeny, message) {
513
+ return denial(onDeny === "FORBIDDEN" ? ErrorCode3.Forbidden : ErrorCode3.NotFound, message);
514
+ }
515
+ // src/plugins/gateway-trust.ts
516
+ var GATEWAY_SECRET_HEADER = "x-gateway-secret";
517
+ var MIN_SECRET_LENGTH = 16;
518
+ function gatewaySecret({
519
+ secret,
520
+ header = GATEWAY_SECRET_HEADER
521
+ }) {
522
+ if (typeof secret !== "string" || secret.length < MIN_SECRET_LENGTH) {
523
+ throw new TypeError(`gatewaySecret(): \`secret\` must be a string of at least ${MIN_SECRET_LENGTH} characters — is its environment variable set?`);
524
+ }
525
+ const expected = new TextEncoder().encode(secret);
526
+ return (headers) => {
527
+ const sent = headers.get(header);
528
+ if (typeof sent !== "string")
529
+ return false;
530
+ return constantTimeEqual(new TextEncoder().encode(sent), expected);
531
+ };
532
+ }
533
+ function requireGatewayTrust(options, plugin) {
534
+ const trust = options?.trustedGateway;
535
+ if (typeof trust !== "function") {
536
+ throw new TypeError(`${plugin}: name the gateway allowed to set the caller — ${plugin.replace("()", "")}({ trustedGateway: gatewaySecret({ secret }) }). The request's extensions are written by the client; an API that resolves its own callers uses useOryAuth(ory)`);
537
+ }
538
+ return trust;
539
+ }
540
+ function principalOf(value) {
541
+ if (!value || typeof value !== "object")
542
+ return;
543
+ const sub = value.sub;
544
+ return typeof sub === "string" && sub.length > 0 ? value : undefined;
545
+ }
546
+ function constantTimeEqual(sent, expected) {
547
+ let difference = sent.length ^ expected.length;
548
+ for (let index = 0;index < expected.length; index++) {
549
+ difference |= (sent[index] ?? 0) ^ (expected[index] ?? 0);
550
+ }
551
+ return difference === 0;
552
+ }
553
+
554
+ // src/plugins/auth.ts
555
+ function useAuth(options) {
556
+ const trusted = requireGatewayTrust(options, "useAuth()");
557
+ return {
558
+ onContextBuilding: async ({ context, extendContext }) => {
559
+ const headers = context.request?.headers;
560
+ if (typeof headers?.get !== "function")
561
+ return;
562
+ if (!await trusted(headers))
563
+ return;
564
+ const user = principalOf(context.params?.extensions?.user);
565
+ if (!user)
566
+ return;
567
+ const token = context.params.extensions?.token;
568
+ extendContext({
569
+ user,
570
+ token: typeof token === "string" ? token : undefined
571
+ });
572
+ }
573
+ };
574
+ }
575
+ // src/plugins/authenticated.ts
576
+ function useAuthenticated(options = {}) {
577
+ return {
578
+ onSchemaChange: ({ schema, replaceSchema }) => {
579
+ const next = applyAuthenticated(schema, options);
580
+ if (next !== schema)
581
+ replaceSchema(next);
582
+ }
583
+ };
584
+ }
585
+ // src/plugins/extract-jwt.ts
586
+ function extractJwtPlugin(options) {
587
+ const trusted = requireGatewayTrust(options, "extractJwtPlugin()");
588
+ return {
589
+ async requestDidStart({ request, contextValue }) {
590
+ const headers = request.http?.headers;
591
+ if (!headers || !await trusted(headers))
592
+ return;
593
+ const payload = principalOf(request.extensions?.payload);
594
+ if (payload)
595
+ contextValue.jwt = { payload };
596
+ }
597
+ };
598
+ }
599
+ // src/plugins/keto-checker.ts
600
+ import { tuple } from "@nxgt/ory-sdk";
601
+ import DataLoader from "dataloader";
602
+ function createKetoChecks(ory) {
603
+ const loader = new DataLoader((questions) => ory.checkMany([...questions]), {
604
+ cacheKeyFn: ({ permission, subject }) => tuple(permission, subject)
519
605
  });
606
+ return (permission, subject) => loader.load({ permission, subject });
520
607
  }
521
- // src/utils/errors/create-error.ts
522
- import { GraphQLError } from "graphql";
523
- function createGraphQLError(message, options) {
524
- return new GraphQLError(message, options);
608
+ // src/plugins/keto-checks.ts
609
+ function useKetoChecks(ory, options = {}) {
610
+ return {
611
+ onSchemaChange: ({ schema, replaceSchema }) => {
612
+ const next = applyKetoChecks(schema, options);
613
+ if (next !== schema)
614
+ replaceSchema(next);
615
+ },
616
+ onContextBuilding: ({ extendContext }) => {
617
+ extendContext({ ketoChecks: createKetoChecks(ory) });
618
+ }
619
+ };
525
620
  }
526
- // src/utils/errors/format-error.ts
527
- import {
528
- ApolloServerErrorCode,
529
- unwrapResolverError
530
- } from "@apollo/server/errors";
621
+ // src/plugins/keto-helpers.ts
622
+ import { ErrorCode as ErrorCode4 } from "@nxgt/shared-exceptions";
623
+ function requireUser(context) {
624
+ if (!context.user) {
625
+ throw denial(ErrorCode4.Unauthenticated);
626
+ }
627
+ return context.user;
628
+ }
629
+ async function can(context, question) {
630
+ const subject = context.ory?.subject;
631
+ if (!subject) {
632
+ throw denial(ErrorCode4.Unauthenticated);
633
+ }
634
+ if (!context.ketoChecks) {
635
+ throw new Error("can(): no checker on the context — useKetoChecks(ory) is not registered");
636
+ }
637
+ const permission = {
638
+ namespace: question.type,
639
+ object: question.id,
640
+ relation: question.name
641
+ };
642
+ return context.ketoChecks(permission, subject);
643
+ }
644
+ // src/plugins/ory-auth.ts
531
645
  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";
646
+ bearerOf
647
+ } from "@nxgt/ory-sdk";
648
+ import { claimsFromOryPrincipal } from "@nxgt/security/integrations/ory";
537
649
 
538
650
  // src/utils/errors/ory-unavailable.ts
539
- import { ErrorCode } from "@nxgt/shared-exceptions";
651
+ import { ErrorCode as ErrorCode5 } from "@nxgt/shared-exceptions";
540
652
  import { GraphQLError as GraphQLError2 } from "graphql";
541
653
  function isOryUnavailable(error) {
542
654
  if (!(error instanceof Error))
@@ -547,598 +659,192 @@ function isOryUnavailable(error) {
547
659
  function serviceUnavailableError(error) {
548
660
  return new GraphQLError2(`ory: ${error.service} is unavailable`, {
549
661
  extensions: {
550
- code: ErrorCode.ServiceUnavailable,
662
+ code: ErrorCode5.ServiceUnavailable,
551
663
  http: { status: 503 },
552
664
  debugMessage: error.message
553
665
  }
554
666
  });
555
667
  }
556
668
 
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
- });
669
+ // src/plugins/ory-auth.ts
670
+ function toPrincipal(ory) {
671
+ const email = ory.identity?.email;
672
+ return {
673
+ sub: ory.subject,
674
+ uid: ory.subject,
675
+ name: email ?? ory.clientId ?? ory.subject,
676
+ username: email,
677
+ clientId: ory.clientId,
678
+ scope: ory.scopes.join(" ") || undefined,
679
+ tokenType: ory.kind,
680
+ exp: ory.expiresAt ? Math.floor(ory.expiresAt.getTime() / 1000) : undefined,
681
+ authorities: [],
682
+ roles: []
629
683
  };
630
684
  }
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
- });
685
+ function oryUnavailableError(error) {
686
+ return serviceUnavailableError(error);
641
687
  }
642
- function isOriginalGraphQLError(error) {
643
- if (!(error instanceof GraphQLError3))
644
- return false;
645
- return error.originalError == null ? true : isOriginalGraphQLError(error.originalError);
688
+ async function resolveOryPrincipal(ory, headers) {
689
+ let principal;
690
+ try {
691
+ principal = await ory.resolve(headers);
692
+ } catch (error) {
693
+ if (isOryUnavailable(error))
694
+ throw oryUnavailableError(error);
695
+ throw error;
696
+ }
697
+ return {
698
+ ory: principal,
699
+ user: principal ? toPrincipal(principal) : undefined,
700
+ claims: principal ? claimsFromOryPrincipal(principal) : undefined,
701
+ token: bearerOf(headers) ?? undefined
702
+ };
646
703
  }
647
- function locationOf(error) {
648
- if (!(error instanceof GraphQLError3))
649
- return {};
704
+ function useOryAuth(ory) {
650
705
  return {
651
- nodes: error.nodes,
652
- source: error.source,
653
- positions: error.positions,
654
- path: error.path
706
+ onContextBuilding: async ({ context, extendContext }) => {
707
+ extendContext(await resolveOryPrincipal(ory, context.request.headers));
708
+ }
655
709
  };
656
710
  }
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 });
711
+ // src/scalars/any/any.scalars.ts
712
+ import { GraphQLScalarType, Kind } from "graphql";
713
+ var ANY_SCALAR = {
714
+ Any: new GraphQLScalarType({
715
+ name: "Any",
716
+ description: "Generic scalar to represent any type",
717
+ serialize(value) {
718
+ return value;
719
+ },
720
+ parseValue(value) {
721
+ return value;
722
+ },
723
+ parseLiteral(ast, variables) {
724
+ switch (ast.kind) {
725
+ case Kind.STRING:
726
+ case Kind.BOOLEAN:
727
+ return ast.value;
728
+ case Kind.INT:
729
+ case Kind.FLOAT:
730
+ return parseFloat(ast.value);
731
+ case Kind.OBJECT:
732
+ return Object.fromEntries(ast.fields.map((field) => [field.name, field.value]));
733
+ case Kind.LIST:
734
+ return ast.values.map((n) => this?.parseLiteral?.(n, variables));
735
+ case Kind.NULL:
736
+ return null;
737
+ case Kind.VARIABLE: {
738
+ const name = ast.name.value;
739
+ return variables ? variables[name] : undefined;
740
+ }
741
+ }
742
+ }
743
+ })
669
744
  };
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>
745
+ // src/scalars/custom/custom.scalars.ts
746
+ import {
747
+ AccountNumberResolver,
748
+ BigIntResolver,
749
+ CountryCodeResolver,
750
+ CountryNameResolver,
751
+ CuidResolver,
752
+ CurrencyResolver,
753
+ DateResolver,
754
+ DateTimeISOResolver,
755
+ DurationResolver,
756
+ EmailAddressResolver,
757
+ GeoJSONResolver,
758
+ GUIDResolver,
759
+ HexadecimalResolver,
760
+ HexColorCodeResolver,
761
+ HSLAResolver,
762
+ HSLResolver,
763
+ IBANResolver,
764
+ IPResolver,
765
+ IPv4Resolver,
766
+ IPv6Resolver,
767
+ ISBNResolver,
768
+ JSONObjectResolver,
769
+ JSONResolver,
770
+ JWTResolver,
771
+ LatitudeResolver,
772
+ LocaleResolver,
773
+ LongitudeResolver,
774
+ LongResolver,
775
+ MACResolver,
776
+ NegativeFloatResolver,
777
+ NegativeIntResolver,
778
+ NonEmptyStringResolver,
779
+ NonNegativeFloatResolver,
780
+ NonNegativeIntResolver,
781
+ NonPositiveFloatResolver,
782
+ NonPositiveIntResolver,
783
+ ObjectIDResolver,
784
+ PhoneNumberResolver,
785
+ PortResolver,
786
+ PositiveFloatResolver,
787
+ PositiveIntResolver,
788
+ PostalCodeResolver,
789
+ RGBAResolver,
790
+ RGBResolver,
791
+ SESSNResolver,
792
+ TimeResolver,
793
+ TimestampResolver,
794
+ URLResolver,
795
+ USCurrencyResolver,
796
+ UtcOffsetResolver,
797
+ UUIDResolver,
798
+ VoidResolver
799
+ } from "graphql-scalars";
700
800
 
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
801
+ // src/scalars/custom/utils.ts
802
+ import { CustomException } from "@nxgt/shared-exceptions";
803
+ import { logger } from "@nxgt/shared-logging";
804
+ import { GraphQLScalarType as GraphQLScalarType2 } from "graphql";
805
+ function createScalarFrom(scalar, {
806
+ name,
807
+ description,
808
+ errorMessage
745
809
  }) {
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);
810
+ const exception = CustomException.badRequest({
811
+ message: errorMessage
813
812
  });
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;
813
+ return new GraphQLScalarType2({
814
+ ...scalar,
815
+ name: name ?? scalar.name,
816
+ description: (description ?? scalar.description)?.replaceAll("`", "'"),
817
+ serialize(value) {
818
+ if (!errorMessage) {
819
+ return scalar.serialize(value);
820
+ }
821
+ try {
822
+ return scalar.serialize(value);
823
+ } catch (e) {
824
+ logger.error(e);
825
+ throw exception;
826
+ }
830
827
  },
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
- });
828
+ parseValue(value) {
829
+ if (!errorMessage) {
830
+ return scalar.parseValue(value);
860
831
  }
861
- const check = ctx.ketoChecks;
862
- if (!check) {
863
- throw new Error(`${where}: no checker on the context — useKetoChecks(ory) is not registered`);
832
+ try {
833
+ return scalar.parseValue(value);
834
+ } catch (e) {
835
+ logger.error(e);
836
+ throw exception;
864
837
  }
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;
838
+ },
839
+ parseLiteral(ast, variables) {
840
+ if (!errorMessage) {
841
+ return scalar.parseLiteral(ast, variables);
842
+ }
843
+ try {
844
+ return scalar.parseLiteral(ast, variables);
845
+ } catch (e) {
846
+ logger.error(e);
847
+ throw exception;
1142
848
  }
1143
849
  }
1144
850
  });
@@ -1328,8 +1034,7 @@ type Subscription {
1328
1034
  _empty: String
1329
1035
  }
1330
1036
 
1331
- directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
1332
-
1037
+ ${AUTHENTICATED_DIRECTIVE_SDL}
1333
1038
  directive @policy(policies: [[String!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
1334
1039
 
1335
1040
  directive @shareable on OBJECT | FIELD_DEFINITION
@@ -1339,11 +1044,429 @@ directive @link(url: String!, import: [String!]) on SCHEMA
1339
1044
  `;
1340
1045
  // src/upload.ts
1341
1046
  import graphqlUploadExpress from "graphql-upload/graphqlUploadExpress.mjs";
1047
+ // src/utils/change-resolvers.utils.ts
1048
+ import {
1049
+ AUDIT_EVENT,
1050
+ CommitType
1051
+ } from "@nxgt/shared-mongo";
1052
+ import { withFilter } from "graphql-subscriptions";
1053
+ function createChangeResolvers(config) {
1054
+ const { entity, collection, service, pubsub } = config;
1055
+ const resolvePubSub = () => typeof pubsub === "function" ? pubsub() : pubsub;
1056
+ return {
1057
+ Query: {
1058
+ [`${entity}Changes`]: async (_parent, args, context) => context.services.audit.findChangesPaginatedByCollection(collection, args),
1059
+ [`${entity}ChangesById`]: async (_parent, args, context) => context.services.audit.findChangesPaginatedByCollectionAndOid(args.id, collection, args)
1060
+ },
1061
+ Subscription: {
1062
+ [`${entity}Changed`]: {
1063
+ resolve: async (payload, _args, context) => ({
1064
+ id: context.services.audit.extractId(payload),
1065
+ data: payload.type !== CommitType.TERMINAL ? await context.services[service].findById(payload.state._id) : null,
1066
+ type: payload.type
1067
+ }),
1068
+ subscribe: withFilter(() => resolvePubSub().subscribe(AUDIT_EVENT), (payload, _variables, context) => context ? context.services.audit.matches(payload, { collection }) : false)
1069
+ }
1070
+ }
1071
+ };
1072
+ }
1073
+ // src/utils/codegen.utils.ts
1074
+ var SCALARS_MAPPING = {
1075
+ ID: "any",
1076
+ DateTime: "any",
1077
+ Date: "any",
1078
+ Any: "any",
1079
+ BigInt: "number",
1080
+ EmailAddress: "string",
1081
+ GeoJSON: "any",
1082
+ HSL: "string",
1083
+ HSLA: "string",
1084
+ HexColorCode: "string",
1085
+ Hexadecimal: "string",
1086
+ IBAN: "string",
1087
+ IP: "string",
1088
+ Time: "string",
1089
+ LocalTime: "string",
1090
+ UUID: "string",
1091
+ PositiveInt: "number",
1092
+ NegativeInt: "number",
1093
+ NonPositiveInt: "number",
1094
+ NonNegativeInt: "number",
1095
+ PositiveFloat: "number",
1096
+ NegativeFloat: "number",
1097
+ NonPositiveFloat: "number",
1098
+ NonNegativeFloat: "number",
1099
+ NonEmptyString: "string",
1100
+ PostalCode: "string",
1101
+ RGB: "string",
1102
+ RGBA: "string",
1103
+ SESSN: "string",
1104
+ Timestamp: "string",
1105
+ URL: "string",
1106
+ USCurrency: "string",
1107
+ UTCOffset: "string",
1108
+ Long: "number",
1109
+ Short: "number",
1110
+ Byte: "number",
1111
+ BigDecimal: "string",
1112
+ BigInteger: "string",
1113
+ Char: "string",
1114
+ Locale: "string",
1115
+ CountryCode: "string",
1116
+ CountryName: "string",
1117
+ AccountNumber: "string",
1118
+ CurrencyCode: "string",
1119
+ Duration: "string",
1120
+ Url: "string",
1121
+ JSONObject: "Record<string, any>",
1122
+ JSON: "any",
1123
+ Void: "void",
1124
+ ObjectID: "any",
1125
+ Upload: "File | Blob",
1126
+ Email: "string",
1127
+ Port: "number",
1128
+ IPv4: "string",
1129
+ IPv6: "string",
1130
+ MAC: "string",
1131
+ Password: "string",
1132
+ PhoneNumber: "string",
1133
+ ZIPCode: "string",
1134
+ SSN: "string",
1135
+ GUID: "string",
1136
+ ISBN: "string",
1137
+ Latitude: "number",
1138
+ Longitude: "number",
1139
+ JWT: "string"
1140
+ };
1141
+ // src/utils/data-loader.utils.ts
1142
+ import { cast } from "@nxgt/shared/helpers";
1143
+ import {
1144
+ paginateList
1145
+ } from "@nxgt/shared-mongo";
1146
+ import DataLoader2 from "dataloader";
1147
+ import { isArray } from "lodash";
1148
+ function createDataLoader(batchLoadFn, options) {
1149
+ return new DataLoader2(batchLoadFn, options);
1150
+ }
1151
+ function createConnectionListDataLoader(path, model, options) {
1152
+ return createDataLoader(async (docs) => {
1153
+ const data = await model.find({
1154
+ ...options.filter ?? {},
1155
+ _id: docs.flatMap((doc) => {
1156
+ const ids = doc[path] ?? [];
1157
+ return isArray(ids) ? cast(ids.filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))) : [];
1158
+ })
1159
+ }).exec();
1160
+ return cast(docs.map((doc) => model.paginateList(data.filter((child) => cast(doc[path] ?? []).map((item) => item.toString()).includes(child._id.toString())), options)));
1161
+ });
1162
+ }
1163
+ function createChildrenConnectionDataLoader(path, model, options) {
1164
+ return createDataLoader(async (docs) => {
1165
+ const filter = {};
1166
+ filter[path] = docs.map((doc) => doc._id);
1167
+ const data = await model.find(filter).exec();
1168
+ return cast(docs.map((doc) => paginateList(data.filter((child) => doc._id?.toString() === child[path]?.toString()), options)));
1169
+ });
1170
+ }
1171
+ function createSimpleListDataLoader(path, model, options) {
1172
+ return createDataLoader(async (docs) => {
1173
+ const filter = options?.filter ?? {};
1174
+ const ids = docs.flatMap((doc) => {
1175
+ const ids = doc[path] ?? [];
1176
+ return isArray(ids) ? cast(ids) : [];
1177
+ });
1178
+ filter._id = ids;
1179
+ const query = model.find(filter);
1180
+ const data = await query.exec();
1181
+ return cast(docs.map((doc) => {
1182
+ const result = data.filter((child) => cast(doc[path] ?? []).map((item) => item.toString()).includes(child._id.toString()));
1183
+ if (options?.skip && options?.limit) {
1184
+ return result.slice(options.skip, options.skip + options.limit);
1185
+ }
1186
+ if (options?.limit) {
1187
+ return result.slice(0, options.limit);
1188
+ }
1189
+ if (options?.skip) {
1190
+ return result.slice(options.skip);
1191
+ }
1192
+ return result;
1193
+ }));
1194
+ });
1195
+ }
1196
+ function createChildrenListDataLoader(path, model, options) {
1197
+ return createDataLoader(async (docs) => {
1198
+ const filter = options?.filter ?? {};
1199
+ const key = path;
1200
+ filter[key] = docs.map((doc) => doc._id);
1201
+ const query = model.find(filter);
1202
+ const data = await query.exec();
1203
+ return cast(docs.map((doc) => {
1204
+ const result = data.filter((child) => child[path]?.toString() === doc._id.toString());
1205
+ if (options?.skip && options?.limit) {
1206
+ return result.slice(options.skip, options.skip + options.limit);
1207
+ }
1208
+ if (options?.limit) {
1209
+ return result.slice(0, options.limit);
1210
+ }
1211
+ if (options?.skip) {
1212
+ return result.slice(options.skip);
1213
+ }
1214
+ return result;
1215
+ }));
1216
+ });
1217
+ }
1218
+ function createSingleChildDataLoader(path, model) {
1219
+ return createDataLoader(async (docs) => {
1220
+ const data = await model.find({
1221
+ _id: docs.map((doc) => cast(doc[path])).filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))
1222
+ }).exec();
1223
+ const map = new Map(data.map((doc) => [doc.id, doc]));
1224
+ return docs.map((doc) => map.get(doc[path]?.toString() ?? "") ?? null);
1225
+ });
1226
+ }
1227
+ function createEntitiesReferenceLoader(model) {
1228
+ return createDataLoader(async (ids) => {
1229
+ const docs = await model.find({
1230
+ _id: ids.filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))
1231
+ }).exec();
1232
+ const map = new Map(docs.map((doc) => [doc.id, doc]));
1233
+ return ids.map((id) => map.get(id) ?? null);
1234
+ });
1235
+ }
1236
+ // src/utils/errors/create-error.ts
1237
+ import { GraphQLError as GraphQLError3 } from "graphql";
1238
+ function createGraphQLError(message, options) {
1239
+ return new GraphQLError3(message, options);
1240
+ }
1241
+ // src/utils/errors/format-error.ts
1242
+ import {
1243
+ ApolloServerErrorCode,
1244
+ unwrapResolverError
1245
+ } from "@apollo/server/errors";
1246
+ import {
1247
+ translate as translateBase
1248
+ } from "@nxgt/i18n";
1249
+ import { CustomException as CustomException2, ErrorCode as ErrorCode6 } from "@nxgt/shared-exceptions";
1250
+ import { MONGO_UTILS, mongoose } from "@nxgt/shared-mongo";
1251
+ import { kebabCase } from "lodash";
1252
+ function createFormatError(translate = translateBase, production) {
1253
+ return (formattedError, graphQLError) => {
1254
+ if (production && formattedError.extensions?.stacktrace) {
1255
+ formattedError.extensions.stacktrace = undefined;
1256
+ }
1257
+ const error = unwrapResolverError(graphQLError);
1258
+ if (isOryUnavailable(error)) {
1259
+ const unavailable = serviceUnavailableError(error);
1260
+ return {
1261
+ ...formattedError,
1262
+ message: unavailable.message,
1263
+ extensions: { ...formattedError.extensions, ...unavailable.extensions }
1264
+ };
1265
+ }
1266
+ const key = denialMessageKey(error);
1267
+ if (key !== undefined) {
1268
+ return { ...formattedError, message: translate(key) };
1269
+ }
1270
+ if (error instanceof CustomException2 || error instanceof mongoose.MongooseError) {
1271
+ const exception = error instanceof mongoose.MongooseError ? MONGO_UTILS.castError(error) : error;
1272
+ return {
1273
+ ...formattedError,
1274
+ message: exception.errorCode === ErrorCode6.ValidationError ? exception.message : translate(exception.message, exception.options),
1275
+ extensions: {
1276
+ ...formattedError.extensions,
1277
+ code: exception.errorCode,
1278
+ debugMessage: exception.debugMessage
1279
+ }
1280
+ };
1281
+ }
1282
+ if ([
1283
+ ApolloServerErrorCode.GRAPHQL_VALIDATION_FAILED,
1284
+ ApolloServerErrorCode.BAD_REQUEST,
1285
+ ApolloServerErrorCode.BAD_USER_INPUT,
1286
+ ApolloServerErrorCode.GRAPHQL_PARSE_FAILED,
1287
+ ApolloServerErrorCode.OPERATION_RESOLUTION_FAILURE,
1288
+ ApolloServerErrorCode.PERSISTED_QUERY_NOT_FOUND,
1289
+ ApolloServerErrorCode.PERSISTED_QUERY_NOT_SUPPORTED
1290
+ ].includes(formattedError.extensions?.code)) {
1291
+ return {
1292
+ ...formattedError,
1293
+ message: translate(`errors.${kebabCase(formattedError.extensions?.code)}`, {}),
1294
+ extensions: {
1295
+ ...formattedError.extensions,
1296
+ debugMessage: formattedError.message
1297
+ }
1298
+ };
1299
+ }
1300
+ return formattedError;
1301
+ };
1302
+ }
1303
+ // src/utils/errors/mask-error.ts
1304
+ import {
1305
+ translate as translateBase2
1306
+ } from "@nxgt/i18n";
1307
+ import { CustomException as CustomException3, ErrorCode as ErrorCode7 } from "@nxgt/shared-exceptions";
1308
+ import { MONGO_UTILS as MONGO_UTILS2, mongoose as mongoose2 } from "@nxgt/shared-mongo";
1309
+ import { GraphQLError as GraphQLError4 } from "graphql";
1310
+ function createMaskError(translate = translateBase2) {
1311
+ return (error, message, isDev) => {
1312
+ const original = error instanceof GraphQLError4 && error.originalError ? error.originalError : error;
1313
+ if (isOryUnavailable(original))
1314
+ return serviceUnavailableError(original);
1315
+ const key = denialMessageKey(original);
1316
+ if (key !== undefined) {
1317
+ return new GraphQLError4(translate(key), {
1318
+ ...locationOf(error),
1319
+ extensions: original.extensions
1320
+ });
1321
+ }
1322
+ if (original instanceof CustomException3 || original instanceof mongoose2.MongooseError) {
1323
+ return exceptionError(original, translate);
1324
+ }
1325
+ if (isOriginalGraphQLError(error))
1326
+ return error;
1327
+ return new GraphQLError4(message, {
1328
+ ...locationOf(error),
1329
+ extensions: isDev ? {
1330
+ code: ErrorCode7.InternalServerError,
1331
+ debugMessage: String(original)
1332
+ } : { code: ErrorCode7.InternalServerError }
1333
+ });
1334
+ };
1335
+ }
1336
+ function exceptionError(original, translate) {
1337
+ const exception = original instanceof mongoose2.MongooseError ? MONGO_UTILS2.castError(original) : original;
1338
+ const status = exception.code;
1339
+ return new GraphQLError4(exception.errorCode === ErrorCode7.ValidationError ? exception.message : translate(exception.message, exception.options), {
1340
+ extensions: {
1341
+ code: exception.errorCode,
1342
+ ...status ? { http: { status } } : {},
1343
+ ...exception.debugMessage ? { debugMessage: exception.debugMessage } : {}
1344
+ }
1345
+ });
1346
+ }
1347
+ function isOriginalGraphQLError(error) {
1348
+ if (!(error instanceof GraphQLError4))
1349
+ return false;
1350
+ return error.originalError == null ? true : isOriginalGraphQLError(error.originalError);
1351
+ }
1352
+ function locationOf(error) {
1353
+ if (!(error instanceof GraphQLError4))
1354
+ return {};
1355
+ return {
1356
+ nodes: error.nodes,
1357
+ source: error.source,
1358
+ positions: error.positions,
1359
+ path: error.path
1360
+ };
1361
+ }
1362
+ // src/utils/publish.ts
1363
+ import { createRedisEventTarget } from "@graphql-yoga/redis-event-target";
1364
+ import { createPubSub } from "graphql-yoga";
1365
+ var createYogaPubSub = ({
1366
+ publishClient,
1367
+ subscribeClient
1368
+ }) => {
1369
+ const eventTarget = createRedisEventTarget({
1370
+ publishClient,
1371
+ subscribeClient
1372
+ });
1373
+ return createPubSub({ eventTarget });
1374
+ };
1375
+ // src/utils/schema.utils.ts
1376
+ import { existsSync } from "node:fs";
1377
+ import { dirname, join } from "node:path";
1378
+ import { fileURLToPath } from "node:url";
1379
+ import { buildSubgraphSchema as buildSubgraphSchemaBase } from "@apollo/subgraph";
1380
+ import { loadFilesSync } from "@graphql-tools/load-files";
1381
+ import { mergeTypeDefs } from "@graphql-tools/merge";
1382
+ import { pruneSchema } from "@graphql-tools/utils";
1383
+ import { printSchema } from "graphql";
1384
+ function packageRoot() {
1385
+ let dir = dirname(fileURLToPath(import.meta.url));
1386
+ while (!existsSync(join(dir, "package.json"))) {
1387
+ const parent = dirname(dir);
1388
+ if (parent === dir)
1389
+ throw new Error("@nxgt/shared-graphql: no package root");
1390
+ dir = parent;
1391
+ }
1392
+ return dir;
1393
+ }
1394
+ var SHARED_SCHEMA_PATH = join(packageRoot(), "graphql/**/*.graphqls");
1395
+ function loadTypeDefs(...paths) {
1396
+ const typeDefs = mergeTypeDefs([
1397
+ ...paths.flatMap((path) => loadFilesSync(path, {
1398
+ recursive: true
1399
+ }))
1400
+ ]);
1401
+ return typeDefs;
1402
+ }
1403
+ function buildSubgraphSchema(modulesOrSdl) {
1404
+ return pruneSchema(buildSubgraphSchemaBase(modulesOrSdl), {
1405
+ skipUnusedTypesPruning: false
1406
+ });
1407
+ }
1408
+ async function generateSchema(output, ...paths) {
1409
+ await Bun.write(output, printSchema(buildSubgraphSchema(loadTypeDefs(...paths))));
1410
+ }
1411
+ // src/utils/websocket.ts
1412
+ import { createServer } from "node:http";
1413
+ import { useServer } from "graphql-ws/use/ws";
1414
+ import { WebSocketServer } from "ws";
1415
+ function setupYogaWebSocketServer({
1416
+ yoga,
1417
+ context
1418
+ }) {
1419
+ const httpServer = createServer(yoga);
1420
+ const wsServer = new WebSocketServer({
1421
+ server: httpServer,
1422
+ path: yoga.graphqlEndpoint
1423
+ });
1424
+ useServer({
1425
+ execute: (args) => args.rootValue.execute(args),
1426
+ subscribe: (args) => args.rootValue.subscribe(args),
1427
+ onSubscribe: async (ctx, _id, params) => {
1428
+ const { schema, execute, subscribe, contextFactory, parse, validate } = yoga.getEnveloped({
1429
+ ...ctx,
1430
+ req: ctx.extra.request,
1431
+ socket: ctx.extra.socket,
1432
+ params
1433
+ });
1434
+ const args = {
1435
+ schema,
1436
+ operationName: params.operationName,
1437
+ document: parse(params.query),
1438
+ variableValues: params.variables,
1439
+ contextValue: await contextFactory(),
1440
+ rootValue: {
1441
+ execute,
1442
+ subscribe
1443
+ }
1444
+ };
1445
+ const errors = validate(args.schema, args.document);
1446
+ if (errors.length)
1447
+ return errors;
1448
+ return args;
1449
+ },
1450
+ context
1451
+ }, wsServer);
1452
+ const startServer = (port) => new Promise((resolve) => httpServer.listen({ port }, resolve));
1453
+ return { yoga, httpServer, startServer };
1454
+ }
1455
+ // src/utils/ws-context.ts
1456
+ async function resolveWsUser(connectionParams, authClient) {
1457
+ const rawToken = connectionParams?.authorization ?? connectionParams?.Authorization;
1458
+ const token = rawToken?.replace("Bearer ", "");
1459
+ const { data } = token ? await authClient.POST("/oauth/introspect", { body: { token } }) : { data: null };
1460
+ const user = data?.active ? { ...data, name: data.sub } : undefined;
1461
+ return { user, token };
1462
+ }
1342
1463
  export {
1343
- CHECK_DIRECTIVE_NAME,
1344
- CHECK_DIRECTIVE_SDL,
1464
+ AUTHENTICATED_DIRECTIVE_NAME,
1465
+ AUTHENTICATED_DIRECTIVE_SDL,
1466
+ CALLER_TYPES,
1345
1467
  DataLoader3 as DataLoader,
1346
1468
  FEDERATION_DIRECTIVES,
1469
+ GATEWAY_SECRET_HEADER,
1347
1470
  KETO_DIRECTIVES_SDL,
1348
1471
  PERMISSION_DIRECTIVE_NAME,
1349
1472
  PERMISSION_DIRECTIVE_SDL,
@@ -1351,6 +1474,7 @@ export {
1351
1474
  SCALAR_RESOLVERS,
1352
1475
  SHARED_SCHEMA_PATH,
1353
1476
  SHARED_TYPE_DEFS,
1477
+ applyAuthenticated,
1354
1478
  applyKetoChecks,
1355
1479
  assertReadablePath,
1356
1480
  buildSubgraphSchema,
@@ -1369,7 +1493,10 @@ export {
1369
1493
  createSingleChildDataLoader,
1370
1494
  createYogaHono,
1371
1495
  createYogaPubSub,
1496
+ denial,
1497
+ denialMessageKey,
1372
1498
  extractJwtPlugin,
1499
+ gatewaySecret,
1373
1500
  generateSchema,
1374
1501
  graphqlUploadExpress,
1375
1502
  honoYoga,
@@ -1377,22 +1504,23 @@ export {
1377
1504
  loadTypeDefs,
1378
1505
  objectIds,
1379
1506
  oryUnavailableError,
1380
- readChecks,
1507
+ principalOf,
1381
1508
  readPath,
1382
1509
  readPermissions,
1383
- readRequirements,
1384
1510
  renderSandbox,
1511
+ requireGatewayTrust,
1385
1512
  requireUser,
1386
1513
  resolveOryPrincipal,
1387
1514
  resolveWsUser,
1388
- sandboxExpolorer,
1515
+ sandboxExplorer,
1389
1516
  serviceUnavailableError,
1390
1517
  setupYogaWebSocketServer,
1391
1518
  toPrincipal,
1392
1519
  useAuth,
1520
+ useAuthenticated,
1393
1521
  useKetoChecks,
1394
1522
  useOryAuth
1395
1523
  };
1396
1524
 
1397
- //# debugId=E72214B01404888264756E2164756E21
1525
+ //# debugId=D1F8FC67079E823264756E2164756E21
1398
1526
  //# sourceMappingURL=index.js.map