@nxgt/shared-graphql 2.0.0 → 2.1.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 (56) hide show
  1. package/README.md +128 -73
  2. package/dist/directives/check.d.ts +13 -21
  3. package/dist/directives/check.d.ts.map +1 -1
  4. package/dist/directives/deprecation.d.ts +7 -0
  5. package/dist/directives/deprecation.d.ts.map +1 -0
  6. package/dist/directives/index.d.ts +5 -0
  7. package/dist/directives/index.d.ts.map +1 -1
  8. package/dist/directives/paths.d.ts +20 -0
  9. package/dist/directives/paths.d.ts.map +1 -0
  10. package/dist/directives/permission.d.ts +25 -0
  11. package/dist/directives/permission.d.ts.map +1 -0
  12. package/dist/directives/requirements.d.ts +14 -0
  13. package/dist/directives/requirements.d.ts.map +1 -0
  14. package/dist/directives/scope.d.ts +19 -0
  15. package/dist/directives/scope.d.ts.map +1 -0
  16. package/dist/directives/sdl.d.ts +16 -0
  17. package/dist/directives/sdl.d.ts.map +1 -0
  18. package/dist/directives/validate.d.ts +29 -0
  19. package/dist/directives/validate.d.ts.map +1 -0
  20. package/dist/index.js +495 -139
  21. package/dist/index.js.map +21 -8
  22. package/dist/plugins/apply-keto-checks.d.ts +17 -0
  23. package/dist/plugins/apply-keto-checks.d.ts.map +1 -0
  24. package/dist/plugins/index.d.ts +3 -0
  25. package/dist/plugins/index.d.ts.map +1 -1
  26. package/dist/plugins/keto-checker.d.ts +24 -0
  27. package/dist/plugins/keto-checker.d.ts.map +1 -0
  28. package/dist/plugins/keto-checks.d.ts +8 -32
  29. package/dist/plugins/keto-checks.d.ts.map +1 -1
  30. package/dist/plugins/keto-helpers.d.ts +38 -0
  31. package/dist/plugins/keto-helpers.d.ts.map +1 -0
  32. package/dist/plugins/ory-auth.d.ts +2 -2
  33. package/dist/plugins/ory-auth.d.ts.map +1 -1
  34. package/dist/scalars/custom/utils.d.ts.map +1 -1
  35. package/dist/utils/errors/create-error.d.ts +3 -0
  36. package/dist/utils/errors/create-error.d.ts.map +1 -0
  37. package/dist/utils/errors/format-error.d.ts +11 -0
  38. package/dist/utils/errors/format-error.d.ts.map +1 -0
  39. package/dist/utils/errors/index.d.ts +5 -0
  40. package/dist/utils/errors/index.d.ts.map +1 -0
  41. package/dist/utils/errors/mask-error.d.ts +24 -0
  42. package/dist/utils/errors/mask-error.d.ts.map +1 -0
  43. package/dist/utils/errors/ory-unavailable.d.ts +19 -0
  44. package/dist/utils/errors/ory-unavailable.d.ts.map +1 -0
  45. package/dist/utils/index.d.ts +1 -1
  46. package/dist/utils/index.d.ts.map +1 -1
  47. package/docs/README.md +11 -0
  48. package/docs/guide/errors.md +104 -0
  49. package/docs/guide/permissions.md +231 -0
  50. package/docs/roadmap.md +70 -0
  51. package/docs/troubleshooting.md +170 -0
  52. package/graphql/directives/check.graphqls +6 -2
  53. package/graphql/directives/permission.graphqls +73 -0
  54. package/package.json +11 -9
  55. package/dist/utils/errors.utils.d.ts +0 -23
  56. package/dist/utils/errors.utils.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -5,31 +5,28 @@ export * from "graphql-subscriptions";
5
5
 
6
6
  // src/directives/check.ts
7
7
  import { getDirective } from "@graphql-tools/utils";
8
- import {
9
- assertRequirement
10
- } from "@nxgt/ory-sdk";
11
- var CHECK_DIRECTIVE_NAME = "check";
12
- function readChecks(schema, node, where) {
13
- const found = getDirective(schema, node, CHECK_DIRECTIVE_NAME) ?? [];
14
- return found.map((raw) => {
15
- const permissions = raw.permissions ?? [];
16
- assertRequirement(permissions, `@check on ${where}`);
17
- for (const group of permissions) {
18
- for (const term of group)
19
- assertReadablePath(term, where);
20
- }
21
- return {
22
- permissions,
23
- onDeny: raw.onDeny ?? "NOT_FOUND",
24
- message: raw.message
25
- };
26
- });
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`);
27
13
  }
14
+
15
+ // src/directives/paths.ts
16
+ var PATH = /^(args|parent|source)(\.[A-Za-z0-9_]+)+$/;
17
+ var DEFAULT_ID_PATH = "args.id";
28
18
  function assertReadablePath(term, where) {
29
- if (!/^(args|source)(\.[A-Za-z0-9_]+)+$/.test(term.id)) {
30
- throw new Error(`@check on ${where}: \`id\` must be "args.<path>" or "source.<path>", got ${JSON.stringify(term.id)}`);
19
+ if (!PATH.test(term.id)) {
20
+ throw new TypeError(`${where}: \`id\` must be "args.<path>", "parent.<path>" or "source.<path>", got ${JSON.stringify(term.id)}`);
31
21
  }
32
22
  }
23
+ function assertDeclaredArgument(term, argumentNames, where) {
24
+ const [root, name] = term.id.split(".");
25
+ if (root !== "args" || !name || argumentNames.includes(name))
26
+ return;
27
+ const declared = argumentNames.length ? argumentNames.join(", ") : "no arguments";
28
+ throw new TypeError(`${where}: \`id\` reads "${term.id}", but the field declares ${declared}`);
29
+ }
33
30
  function readPath(path, source, args) {
34
31
  const [root, ...rest] = path.split(".");
35
32
  let value = root === "args" ? args : source;
@@ -42,6 +39,78 @@ function objectIds(value) {
42
39
  const values = Array.isArray(value) ? value : [value];
43
40
  return values.filter((item) => typeof item === "string" && item.length > 0);
44
41
  }
42
+
43
+ // src/directives/scope.ts
44
+ function scopeOf(node, where, options) {
45
+ return {
46
+ where,
47
+ argumentNames: argumentNamesOf(node),
48
+ namespaces: options.namespaces
49
+ };
50
+ }
51
+ function argumentNamesOf(node) {
52
+ const field = node;
53
+ if (Array.isArray(field.args))
54
+ return field.args.map((arg) => arg.name);
55
+ if (field.args)
56
+ return Object.keys(field.args);
57
+ return field.arguments?.map((arg) => arg.name.value) ?? [];
58
+ }
59
+
60
+ // src/directives/validate.ts
61
+ import { assertRequirement } from "@nxgt/ory-sdk";
62
+ function assertRequirementShape(permissions, scope) {
63
+ const { where, argumentNames, namespaces } = scope;
64
+ try {
65
+ assertRequirement(permissions, where);
66
+ } catch (error) {
67
+ throw new TypeError(error.message);
68
+ }
69
+ for (const group of permissions) {
70
+ for (const term of group) {
71
+ assertReadablePath(term, where);
72
+ declaredOrReported(term, argumentNames, scope);
73
+ if (namespaces && !namespaces.includes(term.namespace)) {
74
+ throw new TypeError(`${where}: unknown namespace ${JSON.stringify(term.namespace)} — known: ${namespaces.join(", ")}`);
75
+ }
76
+ }
77
+ }
78
+ }
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
+
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
+ }
45
114
  // src/directives/federation.ts
46
115
  var FEDERATION_DIRECTIVES = `
47
116
  directive @external on FIELD_DEFINITION | OBJECT
@@ -56,6 +125,204 @@ directive @composeDirective(name: String!) repeatable on SCHEMA
56
125
  directive @interfaceObject on OBJECT
57
126
  directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
58
127
  `;
128
+ // src/directives/permission.ts
129
+ import { getDirective as getDirective2 } from "@graphql-tools/utils";
130
+ var PERMISSION_DIRECTIVE_NAME = "permission";
131
+ function readPermissions(schema, node, where, options = {}) {
132
+ if (!isOurs(schema))
133
+ return [];
134
+ const found = getDirective2(schema, node, PERMISSION_DIRECTIVE_NAME) ?? [];
135
+ const scope = scopeOf(node, `@permission on ${where}`, options);
136
+ return found.map((raw) => {
137
+ const args = permissionArgs(raw);
138
+ const permissions = [
139
+ [{ namespace: args.type, permit: args.name, id: args.id }]
140
+ ];
141
+ assertRequirementShape(permissions, scope);
142
+ return { permissions, onDeny: args.onDeny, message: args.message };
143
+ });
144
+ }
145
+ function permissionArgs(raw) {
146
+ return {
147
+ name: raw.name,
148
+ type: raw.type,
149
+ id: raw.id ?? DEFAULT_ID_PATH,
150
+ onDeny: raw.onDeny ?? "NOT_FOUND",
151
+ message: raw.message ?? undefined
152
+ };
153
+ }
154
+ function isOurs(schema) {
155
+ const declared = schema.getDirective(PERMISSION_DIRECTIVE_NAME);
156
+ const names = declared?.args.map((arg) => arg.name) ?? [];
157
+ return names.includes("name") && names.includes("type");
158
+ }
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
+ // 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
+ var PERMISSION_DIRECTIVE_SDL = `"""
263
+ What a refused @permission looks like from outside.
264
+ """
265
+ enum PermissionDenial {
266
+ """
267
+ The same answer as for an id that never existed, so ids cannot be probed.
268
+ The default.
269
+ """
270
+ NOT_FOUND
271
+
272
+ """
273
+ For a permission asked of an object the caller can already see: a viewer
274
+ asked to edit already knows it exists, and "you may not change it" is
275
+ honest.
276
+ """
277
+ FORBIDDEN
278
+ }
279
+
280
+ """
281
+ The permission a field requires: the caller must hold the permit \`name\` on the
282
+ \`type\` object whose id \`id\` points at.
283
+
284
+ Repeatable, and repeated ones are AND, evaluated in declaration order — the
285
+ 404-then-403 ladder:
286
+
287
+ updateNote(id: ID!, input: UpdateNoteInput!): Note!
288
+ @permission(name: "view", type: "Note")
289
+ @permission(name: "edit", type: "Note", onDeny: FORBIDDEN)
290
+
291
+ NOT for a field that answers a LIST the caller is entitled to: fold a Keto
292
+ query into the database filter before the read instead.
293
+ """
294
+ directive @permission(
295
+ """
296
+ The permit asked of Keto, e.g. "view". A plain relation works too; Keto
297
+ answers \`false\`, not an error, for a name it does not know.
298
+ """
299
+ name: String!
300
+
301
+ """
302
+ The Keto namespace, e.g. "Note" — from the stack's OPL document.
303
+ """
304
+ type: String!
305
+
306
+ """
307
+ Where the object id is read: \`args.<path>\` or \`parent.<path>\`, dotted
308
+ paths allowed, \`args.id\` when omitted. A value that turns out to be a LIST
309
+ requires the permit on every element.
310
+ """
311
+ id: String
312
+
313
+ onDeny: PermissionDenial! = NOT_FOUND
314
+
315
+ """
316
+ The i18n key the denial carries, e.g. "notes.errors.not-found". Defaults to
317
+ \`errors.not-found\` / \`errors.insufficient-permissions\`, the shared keys.
318
+ Set it when the service layer words the same refusal with a domain message,
319
+ so the wording does not tell a caller which layer refused.
320
+ """
321
+ message: String
322
+ ) repeatable on FIELD_DEFINITION
323
+ `;
324
+ var KETO_DIRECTIVES_SDL = `${CHECK_DIRECTIVE_SDL}
325
+ ${PERMISSION_DIRECTIVE_SDL}`;
59
326
  // src/integrations/hono.ts
60
327
  import { Hono } from "hono";
61
328
  import { contextStorage } from "hono/context-storage";
@@ -251,7 +518,12 @@ function createEntitiesReferenceLoader(model) {
251
518
  return ids.map((id) => map.get(id) ?? null);
252
519
  });
253
520
  }
254
- // src/utils/errors.utils.ts
521
+ // src/utils/errors/create-error.ts
522
+ import { GraphQLError } from "graphql";
523
+ function createGraphQLError(message, options) {
524
+ return new GraphQLError(message, options);
525
+ }
526
+ // src/utils/errors/format-error.ts
255
527
  import {
256
528
  ApolloServerErrorCode,
257
529
  unwrapResolverError
@@ -259,25 +531,49 @@ import {
259
531
  import {
260
532
  translate as translateBase
261
533
  } from "@nxgt/i18n";
262
- import { OryUnavailable } from "@nxgt/ory-sdk";
263
- import { CustomException, ErrorCode } from "@nxgt/shared-exceptions";
534
+ import { CustomException, ErrorCode as ErrorCode2 } from "@nxgt/shared-exceptions";
264
535
  import { MONGO_UTILS, mongoose } from "@nxgt/shared-mongo";
265
- import { GraphQLError } from "graphql";
266
536
  import { kebabCase } from "lodash";
267
- function createGraphQLError(message, options) {
268
- return new GraphQLError(message, options);
537
+
538
+ // src/utils/errors/ory-unavailable.ts
539
+ import { ErrorCode } from "@nxgt/shared-exceptions";
540
+ import { GraphQLError as GraphQLError2 } from "graphql";
541
+ function isOryUnavailable(error) {
542
+ if (!(error instanceof Error))
543
+ return false;
544
+ const service = error.service;
545
+ return error.name === "OryUnavailable" && typeof service === "string";
546
+ }
547
+ function serviceUnavailableError(error) {
548
+ return new GraphQLError2(`ory: ${error.service} is unavailable`, {
549
+ extensions: {
550
+ code: ErrorCode.ServiceUnavailable,
551
+ http: { status: 503 },
552
+ debugMessage: error.message
553
+ }
554
+ });
269
555
  }
556
+
557
+ // src/utils/errors/format-error.ts
270
558
  function createFormatError(translate = translateBase, production) {
271
559
  return (formattedError, graphQLError) => {
272
560
  if (production && formattedError.extensions?.stacktrace) {
273
561
  formattedError.extensions.stacktrace = undefined;
274
562
  }
275
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
+ }
276
572
  if (error instanceof CustomException || error instanceof mongoose.MongooseError) {
277
573
  const exception = error instanceof mongoose.MongooseError ? MONGO_UTILS.castError(error) : error;
278
574
  return {
279
575
  ...formattedError,
280
- message: exception.errorCode === ErrorCode.ValidationError ? exception.message : translate(exception.message, exception.options),
576
+ message: exception.errorCode === ErrorCode2.ValidationError ? exception.message : translate(exception.message, exception.options),
281
577
  extensions: {
282
578
  ...formattedError.extensions,
283
579
  code: exception.errorCode,
@@ -306,45 +602,58 @@ function createFormatError(translate = translateBase, production) {
306
602
  return formattedError;
307
603
  };
308
604
  }
309
- var HTTP_STATUS_BY_CODE = {
310
- [ErrorCode.BadRequest]: 400,
311
- [ErrorCode.ValidationError]: 400,
312
- [ErrorCode.Unauthenticated]: 401,
313
- [ErrorCode.Forbidden]: 403,
314
- [ErrorCode.NotFound]: 404,
315
- [ErrorCode.Conflict]: 409,
316
- [ErrorCode.ServiceUnavailable]: 503
317
- };
318
- function createMaskError(translate = translateBase) {
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) {
319
613
  return (error, message, isDev) => {
320
- const original = error instanceof GraphQLError && error.originalError ? error.originalError : error;
321
- if (original instanceof OryUnavailable) {
322
- return new GraphQLError(`ory: ${original.service} is unavailable`, {
323
- extensions: {
324
- code: ErrorCode.ServiceUnavailable,
325
- http: { status: 503 },
326
- debugMessage: original.message
327
- }
328
- });
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);
329
619
  }
330
- if (original instanceof CustomException || original instanceof mongoose.MongooseError) {
331
- const exception = original instanceof mongoose.MongooseError ? MONGO_UTILS.castError(original) : original;
332
- const status = exception.code;
333
- return new GraphQLError(exception.errorCode === ErrorCode.ValidationError ? exception.message : translate(exception.message, exception.options), {
334
- extensions: {
335
- code: exception.errorCode,
336
- ...status ? { http: { status } } : {},
337
- ...exception.debugMessage ? { debugMessage: exception.debugMessage } : {}
338
- }
339
- });
340
- }
341
- if (error instanceof GraphQLError)
620
+ if (isOriginalGraphQLError(error))
342
621
  return error;
343
- return new GraphQLError(message, {
344
- extensions: isDev ? { code: ErrorCode.InternalServerError, debugMessage: String(error) } : { code: ErrorCode.InternalServerError }
622
+ return new GraphQLError3(message, {
623
+ ...locationOf(error),
624
+ extensions: isDev ? {
625
+ code: ErrorCode3.InternalServerError,
626
+ debugMessage: String(original)
627
+ } : { code: ErrorCode3.InternalServerError }
345
628
  });
346
629
  };
347
630
  }
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
+ });
641
+ }
642
+ function isOriginalGraphQLError(error) {
643
+ if (!(error instanceof GraphQLError3))
644
+ return false;
645
+ return error.originalError == null ? true : isOriginalGraphQLError(error.originalError);
646
+ }
647
+ function locationOf(error) {
648
+ if (!(error instanceof GraphQLError3))
649
+ return {};
650
+ return {
651
+ nodes: error.nodes,
652
+ source: error.source,
653
+ positions: error.positions,
654
+ path: error.path
655
+ };
656
+ }
348
657
  // src/utils/publish.ts
349
658
  import { createRedisEventTarget } from "@graphql-yoga/redis-event-target";
350
659
  import { createPubSub } from "graphql-yoga";
@@ -506,6 +815,74 @@ function createYogaHono(yoga, options) {
506
815
  app.use(yoga.graphqlEndpoint, honoYoga(yoga));
507
816
  return app;
508
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;
830
+ },
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
+ });
860
+ }
861
+ const check = ctx.ketoChecks;
862
+ if (!check) {
863
+ throw new Error(`${where}: no checker on the context — useKetoChecks(ory) is not registered`);
864
+ }
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
+ }
509
886
  // src/plugins/auth.ts
510
887
  function useAuth() {
511
888
  return {
@@ -518,82 +895,32 @@ function useAuth() {
518
895
  };
519
896
  }
520
897
  // src/plugins/extract-jwt.ts
521
- import { logger } from "@nxgt/shared-logging";
898
+ import { logger as logger2 } from "@nxgt/shared-logging";
522
899
  var extractJwtPlugin = {
523
900
  async requestDidStart({ request, contextValue }) {
524
- logger.info(`[${request.extensions?.subgraphName}] Extracting JWT from request extensions: ${request.extensions?.payload?.sub}`, request.extensions);
901
+ logger2.info(`[${request.extensions?.subgraphName}] Extracting JWT from request extensions: ${request.extensions?.payload?.sub}`, request.extensions);
525
902
  contextValue.jwt = {
526
903
  payload: request.extensions?.payload
527
904
  };
528
905
  }
529
906
  };
530
- // src/plugins/keto-checks.ts
531
- import { MapperKind, mapSchema } from "@graphql-tools/utils";
532
- import {
533
- evaluateRequirement,
534
- tuple
535
- } from "@nxgt/ory-sdk";
536
- import { CustomException as CustomException2 } from "@nxgt/shared-exceptions";
907
+ // src/plugins/keto-checker.ts
908
+ import { tuple } from "@nxgt/ory-sdk";
537
909
  import DataLoader2 from "dataloader";
538
- import { defaultFieldResolver } from "graphql";
539
910
  function createKetoChecks(ory) {
540
911
  const loader = new DataLoader2((questions) => ory.checkMany([...questions]), {
541
912
  cacheKeyFn: ({ permission, subject }) => tuple(permission, subject)
542
913
  });
543
914
  return (permission, subject) => loader.load({ permission, subject });
544
915
  }
545
- function applyKetoChecks(schema) {
546
- return mapSchema(schema, {
547
- [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
548
- const where = `${typeName}.${fieldName}`;
549
- const checks = readChecks(schema, fieldConfig, where);
550
- if (checks.length === 0)
551
- return fieldConfig;
552
- const resolve = fieldConfig.resolve ?? defaultFieldResolver;
553
- return {
554
- ...fieldConfig,
555
- resolve: async (source, args, context, info) => {
556
- const ctx = context;
557
- const subject = ctx.ory?.subject;
558
- if (!subject) {
559
- throw CustomException2.unauthenticated({
560
- message: "errors.unauthenticated"
561
- });
562
- }
563
- const check = ctx.ketoChecks;
564
- if (!check) {
565
- throw new Error(`@check on ${where}: no checker on the context — useKetoChecks(ory) is not registered`);
566
- }
567
- for (const { permissions, onDeny, message } of checks) {
568
- const allowed = await evaluateRequirement(permissions, (term) => objectsOf(term, source, args, where), check, subject);
569
- if (!allowed)
570
- throw denial(onDeny, message);
571
- }
572
- return resolve(source, args, context, info);
573
- }
574
- };
575
- }
576
- });
577
- }
578
- function objectsOf(term, source, args, where) {
579
- const ids = objectIds(readPath(term.id, source, args));
580
- if (ids.length === 0) {
581
- throw new Error(`@check on ${where}: "${term.id}" resolved no object id`);
582
- }
583
- return ids;
584
- }
585
- function denial(onDeny, message) {
586
- return onDeny === "FORBIDDEN" ? CustomException2.forbidden({
587
- message: message ?? "errors.insufficient-permissions"
588
- }) : CustomException2.notFound({ message: message ?? "errors.not-found" });
589
- }
590
- function useKetoChecks(ory) {
916
+ // src/plugins/keto-checks.ts
917
+ function useKetoChecks(ory, options = {}) {
591
918
  const transformed = new WeakSet;
592
919
  return {
593
920
  onSchemaChange: ({ schema, replaceSchema }) => {
594
921
  if (transformed.has(schema))
595
922
  return;
596
- const next = applyKetoChecks(schema);
923
+ const next = applyKetoChecks(schema, options);
597
924
  transformed.add(next);
598
925
  replaceSchema(next);
599
926
  },
@@ -602,13 +929,38 @@ function useKetoChecks(ory) {
602
929
  }
603
930
  };
604
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
+ }
605
959
  // src/plugins/ory-auth.ts
606
960
  import {
607
- bearerOf,
608
- OryUnavailable as OryUnavailable2
961
+ bearerOf
609
962
  } from "@nxgt/ory-sdk";
610
963
  import { claimsFromOryPrincipal } from "@nxgt/security/integrations/ory";
611
- import { GraphQLError as GraphQLError2 } from "graphql";
612
964
  function toPrincipal(ory) {
613
965
  const email = ory.identity?.email;
614
966
  return {
@@ -625,20 +977,14 @@ function toPrincipal(ory) {
625
977
  };
626
978
  }
627
979
  function oryUnavailableError(error) {
628
- return new GraphQLError2(`ory: ${error.service} is unavailable`, {
629
- extensions: {
630
- code: "SERVICE_UNAVAILABLE",
631
- http: { status: 503 },
632
- debugMessage: error.message
633
- }
634
- });
980
+ return serviceUnavailableError(error);
635
981
  }
636
982
  async function resolveOryPrincipal(ory, headers) {
637
983
  let principal;
638
984
  try {
639
985
  principal = await ory.resolve(headers);
640
986
  } catch (error) {
641
- if (error instanceof OryUnavailable2)
987
+ if (isOryUnavailable(error))
642
988
  throw oryUnavailableError(error);
643
989
  throw error;
644
990
  }
@@ -747,15 +1093,15 @@ import {
747
1093
  } from "graphql-scalars";
748
1094
 
749
1095
  // src/scalars/custom/utils.ts
750
- import { CustomException as CustomException3 } from "@nxgt/shared-exceptions";
751
- import { logger as logger2 } from "@nxgt/shared-logging";
1096
+ import { CustomException as CustomException5 } from "@nxgt/shared-exceptions";
1097
+ import { logger as logger3 } from "@nxgt/shared-logging";
752
1098
  import { GraphQLScalarType as GraphQLScalarType2 } from "graphql";
753
1099
  function createScalarFrom(scalar, {
754
1100
  name,
755
1101
  description,
756
1102
  errorMessage
757
1103
  }) {
758
- const exception = CustomException3.badRequest({
1104
+ const exception = CustomException5.badRequest({
759
1105
  message: errorMessage
760
1106
  });
761
1107
  return new GraphQLScalarType2({
@@ -769,7 +1115,7 @@ function createScalarFrom(scalar, {
769
1115
  try {
770
1116
  return scalar.serialize(value);
771
1117
  } catch (e) {
772
- logger2.error(e);
1118
+ logger3.error(e);
773
1119
  throw exception;
774
1120
  }
775
1121
  },
@@ -780,18 +1126,18 @@ function createScalarFrom(scalar, {
780
1126
  try {
781
1127
  return scalar.parseValue(value);
782
1128
  } catch (e) {
783
- logger2.error(e);
1129
+ logger3.error(e);
784
1130
  throw exception;
785
1131
  }
786
1132
  },
787
- parseLiteral(ast) {
1133
+ parseLiteral(ast, variables) {
788
1134
  if (!errorMessage) {
789
- return scalar.parseLiteral(ast);
1135
+ return scalar.parseLiteral(ast, variables);
790
1136
  }
791
1137
  try {
792
- return scalar.parseLiteral(ast);
1138
+ return scalar.parseLiteral(ast, variables);
793
1139
  } catch (e) {
794
- logger2.error(e);
1140
+ logger3.error(e);
795
1141
  throw exception;
796
1142
  }
797
1143
  }
@@ -995,8 +1341,12 @@ directive @link(url: String!, import: [String!]) on SCHEMA
995
1341
  import graphqlUploadExpress from "graphql-upload/graphqlUploadExpress.mjs";
996
1342
  export {
997
1343
  CHECK_DIRECTIVE_NAME,
1344
+ CHECK_DIRECTIVE_SDL,
998
1345
  DataLoader3 as DataLoader,
999
1346
  FEDERATION_DIRECTIVES,
1347
+ KETO_DIRECTIVES_SDL,
1348
+ PERMISSION_DIRECTIVE_NAME,
1349
+ PERMISSION_DIRECTIVE_SDL,
1000
1350
  SCALARS_MAPPING,
1001
1351
  SCALAR_RESOLVERS,
1002
1352
  SHARED_SCHEMA_PATH,
@@ -1004,6 +1354,7 @@ export {
1004
1354
  applyKetoChecks,
1005
1355
  assertReadablePath,
1006
1356
  buildSubgraphSchema,
1357
+ can,
1007
1358
  createChangeResolvers,
1008
1359
  createChildrenConnectionDataLoader,
1009
1360
  createChildrenListDataLoader,
@@ -1022,15 +1373,20 @@ export {
1022
1373
  generateSchema,
1023
1374
  graphqlUploadExpress,
1024
1375
  honoYoga,
1376
+ isOryUnavailable,
1025
1377
  loadTypeDefs,
1026
1378
  objectIds,
1027
1379
  oryUnavailableError,
1028
1380
  readChecks,
1029
1381
  readPath,
1382
+ readPermissions,
1383
+ readRequirements,
1030
1384
  renderSandbox,
1385
+ requireUser,
1031
1386
  resolveOryPrincipal,
1032
1387
  resolveWsUser,
1033
1388
  sandboxExpolorer,
1389
+ serviceUnavailableError,
1034
1390
  setupYogaWebSocketServer,
1035
1391
  toPrincipal,
1036
1392
  useAuth,
@@ -1038,5 +1394,5 @@ export {
1038
1394
  useOryAuth
1039
1395
  };
1040
1396
 
1041
- //# debugId=6FBEA5C770B5605F64756E2164756E21
1397
+ //# debugId=E72214B01404888264756E2164756E21
1042
1398
  //# sourceMappingURL=index.js.map