@nxgt/shared-graphql 2.0.1 → 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 (79) hide show
  1. package/README.md +196 -81
  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 +5 -1
  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 +39 -0
  11. package/dist/directives/permission.d.ts.map +1 -0
  12. package/dist/directives/removed-check.d.ts +15 -0
  13. package/dist/directives/removed-check.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 +26 -0
  17. package/dist/directives/sdl.d.ts.map +1 -0
  18. package/dist/directives/validate.d.ts +23 -0
  19. package/dist/directives/validate.d.ts.map +1 -0
  20. package/dist/index.js +1048 -564
  21. package/dist/index.js.map +37 -20
  22. package/dist/integrations/hono.d.ts +2 -2
  23. package/dist/integrations/hono.d.ts.map +1 -1
  24. package/dist/plugins/apply-authenticated.d.ts +21 -0
  25. package/dist/plugins/apply-authenticated.d.ts.map +1 -0
  26. package/dist/plugins/apply-keto-checks.d.ts +20 -0
  27. package/dist/plugins/apply-keto-checks.d.ts.map +1 -0
  28. package/dist/plugins/auth.d.ts +17 -1
  29. package/dist/plugins/auth.d.ts.map +1 -1
  30. package/dist/plugins/authenticated.d.ts +14 -0
  31. package/dist/plugins/authenticated.d.ts.map +1 -0
  32. package/dist/plugins/extract-jwt.d.ts +19 -6
  33. package/dist/plugins/extract-jwt.d.ts.map +1 -1
  34. package/dist/plugins/field-guard.d.ts +30 -0
  35. package/dist/plugins/field-guard.d.ts.map +1 -0
  36. package/dist/plugins/gateway-trust.d.ts +47 -0
  37. package/dist/plugins/gateway-trust.d.ts.map +1 -0
  38. package/dist/plugins/index.d.ts +6 -0
  39. package/dist/plugins/index.d.ts.map +1 -1
  40. package/dist/plugins/keto-checker.d.ts +24 -0
  41. package/dist/plugins/keto-checker.d.ts.map +1 -0
  42. package/dist/plugins/keto-checks.d.ts +11 -34
  43. package/dist/plugins/keto-checks.d.ts.map +1 -1
  44. package/dist/plugins/keto-helpers.d.ts +38 -0
  45. package/dist/plugins/keto-helpers.d.ts.map +1 -0
  46. package/dist/plugins/ory-auth.d.ts +5 -5
  47. package/dist/plugins/ory-auth.d.ts.map +1 -1
  48. package/dist/scalars/custom/utils.d.ts.map +1 -1
  49. package/dist/shared.d.ts +1 -1
  50. package/dist/shared.d.ts.map +1 -1
  51. package/dist/utils/errors/create-error.d.ts +3 -0
  52. package/dist/utils/errors/create-error.d.ts.map +1 -0
  53. package/dist/utils/errors/denial.d.ts +20 -0
  54. package/dist/utils/errors/denial.d.ts.map +1 -0
  55. package/dist/utils/errors/format-error.d.ts +12 -0
  56. package/dist/utils/errors/format-error.d.ts.map +1 -0
  57. package/dist/utils/errors/index.d.ts +6 -0
  58. package/dist/utils/errors/index.d.ts.map +1 -0
  59. package/dist/utils/errors/mask-error.d.ts +25 -0
  60. package/dist/utils/errors/mask-error.d.ts.map +1 -0
  61. package/dist/utils/errors/ory-unavailable.d.ts +19 -0
  62. package/dist/utils/errors/ory-unavailable.d.ts.map +1 -0
  63. package/dist/utils/index.d.ts +1 -1
  64. package/dist/utils/index.d.ts.map +1 -1
  65. package/dist/utils/ws-context.d.ts +1 -1
  66. package/docs/README.md +13 -0
  67. package/docs/guide/authentication.md +187 -0
  68. package/docs/guide/errors.md +136 -0
  69. package/docs/guide/migrating-to-3.md +276 -0
  70. package/docs/guide/permissions.md +220 -0
  71. package/docs/roadmap.md +71 -0
  72. package/docs/troubleshooting.md +273 -0
  73. package/graphql/directives/permission.graphqls +73 -0
  74. package/package.json +11 -13
  75. package/dist/directives/check.d.ts +0 -40
  76. package/dist/directives/check.d.ts.map +0 -1
  77. package/dist/utils/errors.utils.d.ts +0 -23
  78. package/dist/utils/errors.utils.d.ts.map +0 -1
  79. package/graphql/directives/check.graphqls +0 -86
package/dist/index.js CHANGED
@@ -3,44 +3,41 @@ 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
- 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);
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(", ")}`);
20
23
  }
21
- return {
22
- permissions,
23
- onDeny: raw.onDeny ?? "NOT_FOUND",
24
- message: raw.message
25
- };
26
- });
27
- }
28
- 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)}`);
31
24
  }
25
+ return { types };
32
26
  }
33
- function readPath(path, source, args) {
34
- const [root, ...rest] = path.split(".");
35
- let value = root === "args" ? args : source;
36
- for (const key of rest) {
37
- value = value?.[key];
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;
38
36
  }
39
- return value;
40
- }
41
- function objectIds(value) {
42
- const values = Array.isArray(value) ? value : [value];
43
- return values.filter((item) => typeof item === "string" && item.length > 0);
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 } : {};
44
41
  }
45
42
  // src/directives/federation.ts
46
43
  var FEDERATION_DIRECTIVES = `
@@ -56,559 +53,620 @@ directive @composeDirective(name: String!) repeatable on SCHEMA
56
53
  directive @interfaceObject on OBJECT
57
54
  directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
58
55
  `;
56
+ // src/directives/paths.ts
57
+ var PATH = /^(args|parent|source)(\.[A-Za-z0-9_]+)+$/;
58
+ var DEFAULT_ID_PATH = "args.id";
59
+ function assertReadablePath(term, where) {
60
+ if (!PATH.test(term.id)) {
61
+ throw new TypeError(`${where}: \`id\` must be "args.<path>", "parent.<path>" or "source.<path>", got ${JSON.stringify(term.id)}`);
62
+ }
63
+ }
64
+ function assertDeclaredArgument(term, argumentNames, where) {
65
+ const [root, name] = term.id.split(".");
66
+ if (root !== "args" || !name || argumentNames.includes(name))
67
+ return;
68
+ const declared = argumentNames.length ? argumentNames.join(", ") : "no arguments";
69
+ throw new TypeError(`${where}: \`id\` reads "${term.id}", but the field declares ${declared}`);
70
+ }
71
+ function readPath(path, source, args) {
72
+ const [root, ...rest] = path.split(".");
73
+ let value = root === "args" ? args : source;
74
+ for (const key of rest) {
75
+ value = value?.[key];
76
+ }
77
+ return value;
78
+ }
79
+ function objectIds(value) {
80
+ const values = Array.isArray(value) ? value : [value];
81
+ return values.filter((item) => typeof item === "string" && item.length > 0);
82
+ }
83
+ // src/directives/permission.ts
84
+ import { getDirective as getDirective2 } from "@graphql-tools/utils";
85
+
86
+ // src/directives/scope.ts
87
+ function scopeOf(node, where, options) {
88
+ return {
89
+ where,
90
+ argumentNames: argumentNamesOf(node),
91
+ namespaces: options.namespaces
92
+ };
93
+ }
94
+ function argumentNamesOf(node) {
95
+ const field = node;
96
+ if (Array.isArray(field.args))
97
+ return field.args.map((arg) => arg.name);
98
+ if (field.args)
99
+ return Object.keys(field.args);
100
+ return field.arguments?.map((arg) => arg.name.value) ?? [];
101
+ }
102
+
103
+ // src/directives/validate.ts
104
+ import { assertRequirement } from "@nxgt/ory-sdk";
105
+ function assertRequirementShape(permissions, scope) {
106
+ const { where, argumentNames, namespaces } = scope;
107
+ try {
108
+ assertRequirement(permissions, where);
109
+ } catch (error) {
110
+ throw new TypeError(error.message);
111
+ }
112
+ for (const group of permissions) {
113
+ for (const term of group) {
114
+ assertReadablePath(term, where);
115
+ assertDeclaredArgument(term, argumentNames, where);
116
+ if (namespaces && !namespaces.includes(term.namespace)) {
117
+ throw new TypeError(`${where}: unknown namespace ${JSON.stringify(term.namespace)} — known: ${namespaces.join(", ")}`);
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ // src/directives/permission.ts
124
+ var PERMISSION_DIRECTIVE_NAME = "permission";
125
+ function readPermissions(schema, node, where, options = {}) {
126
+ if (!isOurs(schema))
127
+ return [];
128
+ const found = getDirective2(schema, node, PERMISSION_DIRECTIVE_NAME) ?? [];
129
+ const scope = scopeOf(node, `@permission on ${where}`, options);
130
+ return found.map((raw) => {
131
+ const args = permissionArgs(raw);
132
+ const permissions = [
133
+ [{ namespace: args.type, permit: args.name, id: args.id }]
134
+ ];
135
+ assertRequirementShape(permissions, scope);
136
+ return { permissions, onDeny: args.onDeny, message: args.message };
137
+ });
138
+ }
139
+ function permissionArgs(raw) {
140
+ return {
141
+ name: raw.name,
142
+ type: raw.type,
143
+ id: raw.id ?? DEFAULT_ID_PATH,
144
+ onDeny: raw.onDeny ?? "NOT_FOUND",
145
+ message: raw.message ?? undefined
146
+ };
147
+ }
148
+ function isOurs(schema) {
149
+ const declared = schema.getDirective(PERMISSION_DIRECTIVE_NAME);
150
+ const names = declared?.args.map((arg) => arg.name) ?? [];
151
+ return names.includes("name") && names.includes("type");
152
+ }
153
+ // src/directives/sdl.ts
154
+ var PERMISSION_DIRECTIVE_SDL = `"""
155
+ What a refused @permission looks like from outside.
156
+ """
157
+ enum PermissionDenial {
158
+ """
159
+ The same answer as for an id that never existed, so ids cannot be probed.
160
+ The default.
161
+ """
162
+ NOT_FOUND
163
+
164
+ """
165
+ For a permission asked of an object the caller can already see: a viewer
166
+ asked to edit already knows it exists, and "you may not change it" is
167
+ honest.
168
+ """
169
+ FORBIDDEN
170
+ }
171
+
172
+ """
173
+ The permission a field requires: the caller must hold the permit \`name\` on the
174
+ \`type\` object whose id \`id\` points at.
175
+
176
+ Repeatable, and repeated ones are AND, evaluated in declaration order — the
177
+ 404-then-403 ladder:
178
+
179
+ updateNote(id: ID!, input: UpdateNoteInput!): Note!
180
+ @permission(name: "view", type: "Note")
181
+ @permission(name: "edit", type: "Note", onDeny: FORBIDDEN)
182
+
183
+ NOT for a field that answers a LIST the caller is entitled to: fold a Keto
184
+ query into the database filter before the read instead.
185
+ """
186
+ directive @permission(
187
+ """
188
+ The permit asked of Keto, e.g. "view". A plain relation works too; Keto
189
+ answers \`false\`, not an error, for a name it does not know.
190
+ """
191
+ name: String!
192
+
193
+ """
194
+ The Keto namespace, e.g. "Note" — from the stack's OPL document.
195
+ """
196
+ type: String!
197
+
198
+ """
199
+ Where the object id is read: \`args.<path>\` or \`parent.<path>\`, dotted
200
+ paths allowed, \`args.id\` when omitted. A value that turns out to be a LIST
201
+ requires the permit on every element.
202
+ """
203
+ id: String
204
+
205
+ onDeny: PermissionDenial! = NOT_FOUND
206
+
207
+ """
208
+ The i18n key the denial carries, e.g. "notes.errors.not-found". Defaults to
209
+ \`errors.not-found\` / \`errors.insufficient-permissions\`, the shared keys.
210
+ Set it when the service layer words the same refusal with a domain message,
211
+ so the wording does not tell a caller which layer refused.
212
+ """
213
+ message: String
214
+ ) repeatable on FIELD_DEFINITION
215
+ `;
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
+ `;
59
228
  // src/integrations/hono.ts
60
229
  import { Hono } from "hono";
61
230
  import { contextStorage } from "hono/context-storage";
62
231
  import { createMiddleware } from "hono/factory";
63
232
  import { languageDetector } from "hono/language";
64
233
 
65
- // 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";
66
298
  import {
67
- AUDIT_EVENT,
68
- CommitType
69
- } from "@nxgt/shared-mongo";
70
- import { withFilter } from "graphql-subscriptions";
71
- function createChangeResolvers(config) {
72
- const { entity, collection, service, pubsub } = config;
73
- 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
+ };
74
362
  return {
75
- Query: {
76
- [`${entity}Changes`]: async (_parent, args, context) => context.services.audit.findChangesPaginatedByCollection(collection, args),
77
- [`${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 }
78
367
  },
79
- Subscription: {
80
- [`${entity}Changed`]: {
81
- resolve: async (payload, _args, context) => ({
82
- id: context.services.audit.extractId(payload),
83
- data: payload.type !== CommitType.TERMINAL ? await context.services[service].findById(payload.state._id) : null,
84
- type: payload.type
85
- }),
86
- subscribe: withFilter(() => resolvePubSub().subscribe(AUDIT_EVENT), (payload, _variables, context) => context ? context.services.audit.matches(payload, { collection }) : false)
87
- }
88
- }
368
+ resolve: wrapped.resolve,
369
+ subscribe: wrapped.subscribe
89
370
  };
90
371
  }
91
- // src/utils/codegen.utils.ts
92
- var SCALARS_MAPPING = {
93
- ID: "any",
94
- DateTime: "any",
95
- Date: "any",
96
- Any: "any",
97
- BigInt: "number",
98
- EmailAddress: "string",
99
- GeoJSON: "any",
100
- HSL: "string",
101
- HSLA: "string",
102
- HexColorCode: "string",
103
- Hexadecimal: "string",
104
- IBAN: "string",
105
- IP: "string",
106
- Time: "string",
107
- LocalTime: "string",
108
- UUID: "string",
109
- PositiveInt: "number",
110
- NegativeInt: "number",
111
- NonPositiveInt: "number",
112
- NonNegativeInt: "number",
113
- PositiveFloat: "number",
114
- NegativeFloat: "number",
115
- NonPositiveFloat: "number",
116
- NonNegativeFloat: "number",
117
- NonEmptyString: "string",
118
- PostalCode: "string",
119
- RGB: "string",
120
- RGBA: "string",
121
- SESSN: "string",
122
- Timestamp: "string",
123
- URL: "string",
124
- USCurrency: "string",
125
- UTCOffset: "string",
126
- Long: "number",
127
- Short: "number",
128
- Byte: "number",
129
- BigDecimal: "string",
130
- BigInteger: "string",
131
- Char: "string",
132
- Locale: "string",
133
- CountryCode: "string",
134
- CountryName: "string",
135
- AccountNumber: "string",
136
- CurrencyCode: "string",
137
- Duration: "string",
138
- Url: "string",
139
- JSONObject: "Record<string, any>",
140
- JSON: "any",
141
- Void: "void",
142
- ObjectID: "any",
143
- Upload: "File | Blob",
144
- Email: "string",
145
- Port: "number",
146
- IPv4: "string",
147
- IPv6: "string",
148
- MAC: "string",
149
- Password: "string",
150
- PhoneNumber: "string",
151
- ZIPCode: "string",
152
- SSN: "string",
153
- GUID: "string",
154
- ISBN: "string",
155
- Latitude: "number",
156
- Longitude: "number",
157
- JWT: "string"
158
- };
159
- // src/utils/data-loader.utils.ts
160
- import { cast } from "@nxgt/shared/helpers";
161
- import {
162
- paginateList
163
- } from "@nxgt/shared-mongo";
164
- import DataLoader from "dataloader";
165
- import { isArray } from "lodash";
166
- function createDataLoader(batchLoadFn, options) {
167
- return new DataLoader(batchLoadFn, options);
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
+ };
168
378
  }
169
- function createConnectionListDataLoader(path, model, options) {
170
- return createDataLoader(async (docs) => {
171
- const data = await model.find({
172
- ...options.filter ?? {},
173
- _id: docs.flatMap((doc) => {
174
- const ids = doc[path] ?? [];
175
- return isArray(ids) ? cast(ids.filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))) : [];
176
- })
177
- }).exec();
178
- return cast(docs.map((doc) => model.paginateList(data.filter((child) => cast(doc[path] ?? []).map((item) => item.toString()).includes(child._id.toString())), options)));
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
+ }
179
394
  });
395
+ return guardedNow > 0 ? mapped : schema;
180
396
  }
181
- function createChildrenConnectionDataLoader(path, model, options) {
182
- return createDataLoader(async (docs) => {
183
- const filter = {};
184
- filter[path] = docs.map((doc) => doc._id);
185
- const data = await model.find(filter).exec();
186
- return cast(docs.map((doc) => paginateList(data.filter((child) => doc._id?.toString() === child[path]?.toString()), options)));
187
- });
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);
188
414
  }
189
- function createSimpleListDataLoader(path, model, options) {
190
- return createDataLoader(async (docs) => {
191
- const filter = options?.filter ?? {};
192
- const ids = docs.flatMap((doc) => {
193
- const ids = doc[path] ?? [];
194
- return isArray(ids) ? cast(ids) : [];
195
- });
196
- filter._id = ids;
197
- const query = model.find(filter);
198
- const data = await query.exec();
199
- return cast(docs.map((doc) => {
200
- const result = data.filter((child) => cast(doc[path] ?? []).map((item) => item.toString()).includes(child._id.toString()));
201
- if (options?.skip && options?.limit) {
202
- return result.slice(options.skip, options.skip + options.limit);
203
- }
204
- if (options?.limit) {
205
- return result.slice(0, options.limit);
206
- }
207
- if (options?.skip) {
208
- return result.slice(options.skip);
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);
209
432
  }
210
- return result;
211
- }));
433
+ }
212
434
  });
213
435
  }
214
- function createChildrenListDataLoader(path, model, options) {
215
- return createDataLoader(async (docs) => {
216
- const filter = options?.filter ?? {};
217
- const key = path;
218
- filter[key] = docs.map((doc) => doc._id);
219
- const query = model.find(filter);
220
- const data = await query.exec();
221
- return cast(docs.map((doc) => {
222
- const result = data.filter((child) => child[path]?.toString() === doc._id.toString());
223
- if (options?.skip && options?.limit) {
224
- return result.slice(options.skip, options.skip + options.limit);
436
+ function callerTypeOf(ctx) {
437
+ return ctx.ory?.kind ?? ctx.user?.tokenType;
438
+ }
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`);
453
+ }
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;
225
470
  }
226
- if (options?.limit) {
227
- return result.slice(0, options.limit);
471
+ guardedNow += 1;
472
+ return guarded2(fieldConfig, requirements, where);
473
+ }
474
+ });
475
+ return guardedNow > 0 ? mapped : schema;
476
+ }
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`);
228
496
  }
229
- if (options?.skip) {
230
- 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);
231
501
  }
232
- return result;
233
- }));
502
+ }
234
503
  });
235
504
  }
236
- function createSingleChildDataLoader(path, model) {
237
- return createDataLoader(async (docs) => {
238
- const data = await model.find({
239
- _id: docs.map((doc) => cast(doc[path])).filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))
240
- }).exec();
241
- const map = new Map(data.map((doc) => [doc.id, doc]));
242
- return docs.map((doc) => map.get(doc[path]?.toString() ?? "") ?? null);
243
- });
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;
244
511
  }
245
- function createEntitiesReferenceLoader(model) {
246
- return createDataLoader(async (ids) => {
247
- const docs = await model.find({
248
- _id: ids.filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))
249
- }).exec();
250
- const map = new Map(docs.map((doc) => [doc.id, doc]));
251
- return ids.map((id) => map.get(id) ?? null);
252
- });
512
+ function refusal(onDeny, message) {
513
+ return denial(onDeny === "FORBIDDEN" ? ErrorCode3.Forbidden : ErrorCode3.NotFound, message);
253
514
  }
254
- // src/utils/errors.utils.ts
255
- import {
256
- ApolloServerErrorCode,
257
- unwrapResolverError
258
- } from "@apollo/server/errors";
259
- import {
260
- translate as translateBase
261
- } from "@nxgt/i18n";
262
- import { OryUnavailable } from "@nxgt/ory-sdk";
263
- import { CustomException, ErrorCode } from "@nxgt/shared-exceptions";
264
- import { MONGO_UTILS, mongoose } from "@nxgt/shared-mongo";
265
- import { GraphQLError } from "graphql";
266
- import { kebabCase } from "lodash";
267
- function createGraphQLError(message, options) {
268
- return new GraphQLError(message, options);
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
+ };
269
532
  }
270
- function createFormatError(translate = translateBase, production) {
271
- return (formattedError, graphQLError) => {
272
- if (production && formattedError.extensions?.stacktrace) {
273
- formattedError.extensions.stacktrace = undefined;
274
- }
275
- const error = unwrapResolverError(graphQLError);
276
- if (error instanceof CustomException || error instanceof mongoose.MongooseError) {
277
- const exception = error instanceof mongoose.MongooseError ? MONGO_UTILS.castError(error) : error;
278
- return {
279
- ...formattedError,
280
- message: exception.errorCode === ErrorCode.ValidationError ? exception.message : translate(exception.message, exception.options),
281
- extensions: {
282
- ...formattedError.extensions,
283
- code: exception.errorCode,
284
- debugMessage: exception.debugMessage
285
- }
286
- };
287
- }
288
- if ([
289
- ApolloServerErrorCode.GRAPHQL_VALIDATION_FAILED,
290
- ApolloServerErrorCode.BAD_REQUEST,
291
- ApolloServerErrorCode.BAD_USER_INPUT,
292
- ApolloServerErrorCode.GRAPHQL_PARSE_FAILED,
293
- ApolloServerErrorCode.OPERATION_RESOLUTION_FAILURE,
294
- ApolloServerErrorCode.PERSISTED_QUERY_NOT_FOUND,
295
- ApolloServerErrorCode.PERSISTED_QUERY_NOT_SUPPORTED
296
- ].includes(formattedError.extensions?.code)) {
297
- return {
298
- ...formattedError,
299
- message: translate(`errors.${kebabCase(formattedError.extensions?.code)}`, {}),
300
- extensions: {
301
- ...formattedError.extensions,
302
- debugMessage: formattedError.message
303
- }
304
- };
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
+ });
305
572
  }
306
- return formattedError;
307
573
  };
308
574
  }
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) {
319
- 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
- });
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);
329
582
  }
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
- });
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 };
340
596
  }
341
- if (error instanceof GraphQLError)
342
- return error;
343
- return new GraphQLError(message, {
344
- extensions: isDev ? { code: ErrorCode.InternalServerError, debugMessage: String(error) } : { code: ErrorCode.InternalServerError }
345
- });
346
597
  };
347
598
  }
348
- // src/utils/publish.ts
349
- import { createRedisEventTarget } from "@graphql-yoga/redis-event-target";
350
- import { createPubSub } from "graphql-yoga";
351
- var createYogaPubSub = ({
352
- publishClient,
353
- subscribeClient
354
- }) => {
355
- const eventTarget = createRedisEventTarget({
356
- publishClient,
357
- subscribeClient
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)
358
605
  });
359
- return createPubSub({ eventTarget });
360
- };
361
- // src/utils/sandbox.ts
362
- import { html } from "hono/html";
363
- var renderSandbox = ({
364
- port = 8080,
365
- hostname = "localhost",
366
- graphqlEndpoint = "graphql",
367
- protocol = "http",
368
- title = "Sandbox Explorer"
369
- }) => html`
370
- <title>${title}</title>
371
- <div id="sandbox" style="position:absolute;top:0;right:0;bottom:0;left:0"></div>
372
- <script src="https://embeddable-sandbox.cdn.apollographql.com/_latest/embeddable-sandbox.umd.production.min.js"></script>
373
- <script>
374
- new window.EmbeddedSandbox({
375
- target: "#sandbox",
376
- // Pass through your server href if you are embedding on an endpoint.
377
- // Otherwise, you can pass whatever endpoint you want Sandbox to start up with here.
378
- initialEndpoint: "${protocol}://${hostname}:${port}/${graphqlEndpoint}",
379
- handleRequest: (endpointUrl, options) => {
380
- return fetch(endpointUrl, {
381
- ...options,
382
- headers: {
383
- ...options.headers
384
- },
385
- })
386
- },
387
- hideCookieToggle: true,
388
- });
389
- // advanced options: https://www.apollographql.com/docs/studio/explorer/sandbox#embedding-sandbox
390
- </script>
391
-
392
- `;
393
- // src/utils/schema.utils.ts
394
- import { existsSync } from "node:fs";
395
- import { dirname, join } from "node:path";
396
- import { fileURLToPath } from "node:url";
397
- import { buildSubgraphSchema as buildSubgraphSchemaBase } from "@apollo/subgraph";
398
- import { loadFilesSync } from "@graphql-tools/load-files";
399
- import { mergeTypeDefs } from "@graphql-tools/merge";
400
- import { pruneSchema } from "@graphql-tools/utils";
401
- import { printSchema } from "graphql";
402
- function packageRoot() {
403
- let dir = dirname(fileURLToPath(import.meta.url));
404
- while (!existsSync(join(dir, "package.json"))) {
405
- const parent = dirname(dir);
406
- if (parent === dir)
407
- throw new Error("@nxgt/shared-graphql: no package root");
408
- dir = parent;
409
- }
410
- return dir;
606
+ return (permission, subject) => loader.load({ permission, subject });
411
607
  }
412
- var SHARED_SCHEMA_PATH = join(packageRoot(), "graphql/**/*.graphqls");
413
- function loadTypeDefs(...paths) {
414
- const typeDefs = mergeTypeDefs([
415
- ...paths.flatMap((path) => loadFilesSync(path, {
416
- recursive: true
417
- }))
418
- ]);
419
- return typeDefs;
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
+ };
420
620
  }
421
- function buildSubgraphSchema(modulesOrSdl) {
422
- return pruneSchema(buildSubgraphSchemaBase(modulesOrSdl), {
423
- skipUnusedTypesPruning: false
424
- });
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;
425
628
  }
426
- async function generateSchema(output, ...paths) {
427
- await Bun.write(output, printSchema(buildSubgraphSchema(loadTypeDefs(...paths))));
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);
428
643
  }
429
- // src/utils/websocket.ts
430
- import { createServer } from "node:http";
431
- import { useServer } from "graphql-ws/use/ws";
432
- import { WebSocketServer } from "ws";
433
- function setupYogaWebSocketServer({
434
- yoga,
435
- context
436
- }) {
437
- const httpServer = createServer(yoga);
438
- const wsServer = new WebSocketServer({
439
- server: httpServer,
440
- path: yoga.graphqlEndpoint
441
- });
442
- useServer({
443
- execute: (args) => args.rootValue.execute(args),
444
- subscribe: (args) => args.rootValue.subscribe(args),
445
- onSubscribe: async (ctx, _id, params) => {
446
- const { schema, execute, subscribe, contextFactory, parse, validate } = yoga.getEnveloped({
447
- ...ctx,
448
- req: ctx.extra.request,
449
- socket: ctx.extra.socket,
450
- params
451
- });
452
- const args = {
453
- schema,
454
- operationName: params.operationName,
455
- document: parse(params.query),
456
- variableValues: params.variables,
457
- contextValue: await contextFactory(),
458
- rootValue: {
459
- execute,
460
- subscribe
461
- }
462
- };
463
- const errors = validate(args.schema, args.document);
464
- if (errors.length)
465
- return errors;
466
- return args;
467
- },
468
- context
469
- }, wsServer);
470
- const startServer = (port) => new Promise((resolve) => httpServer.listen({ port }, resolve));
471
- return { yoga, httpServer, startServer };
472
- }
473
- // src/utils/ws-context.ts
474
- async function resolveWsUser(connectionParams, authClient) {
475
- const rawToken = connectionParams?.authorization ?? connectionParams?.Authorization;
476
- const token = rawToken?.replace("Bearer ", "");
477
- const { data } = token ? await authClient.POST("/oauth/introspect", { body: { token } }) : { data: null };
478
- const user = data?.active ? { ...data, name: data.sub } : undefined;
479
- return { user, token };
480
- }
481
- // src/integrations/hono.ts
482
- function sandboxExpolorer(options) {
483
- return createMiddleware(async (ctx) => {
484
- return ctx.html(renderSandbox(options));
485
- });
486
- }
487
- function honoYoga(yoga) {
488
- return createMiddleware(async (ctx) => {
489
- const response = await yoga.fetch(ctx.req.raw);
490
- return response;
491
- });
492
- }
493
- function createYogaHono(yoga, options) {
494
- const app = new Hono;
495
- app.use(contextStorage());
496
- app.use(languageDetector({
497
- order: ["header"],
498
- supportedLanguages: ["en", "fr"],
499
- fallbackLanguage: "en",
500
- caches: []
501
- }));
502
- app.get("/health", (ctx) => {
503
- return ctx.json({ status: "ok" }, 200);
504
- });
505
- app.get(options?.sandbox?.endpoint || "sandbox", sandboxExpolorer(options?.sandbox || {}));
506
- app.use(yoga.graphqlEndpoint, honoYoga(yoga));
507
- return app;
508
- }
509
- // src/plugins/auth.ts
510
- function useAuth() {
511
- return {
512
- onContextBuilding: async ({ context, extendContext }) => {
513
- extendContext({
514
- user: context.params.extensions?.user,
515
- token: context.params.extensions?.token
516
- });
517
- }
518
- };
519
- }
520
- // src/plugins/extract-jwt.ts
521
- import { logger } from "@nxgt/shared-logging";
522
- var extractJwtPlugin = {
523
- async requestDidStart({ request, contextValue }) {
524
- logger.info(`[${request.extensions?.subgraphName}] Extracting JWT from request extensions: ${request.extensions?.payload?.sub}`, request.extensions);
525
- contextValue.jwt = {
526
- payload: request.extensions?.payload
527
- };
528
- }
529
- };
530
- // src/plugins/keto-checks.ts
531
- import { MapperKind, mapSchema } from "@graphql-tools/utils";
644
+ // src/plugins/ory-auth.ts
532
645
  import {
533
- evaluateRequirement,
534
- tuple
646
+ bearerOf
535
647
  } from "@nxgt/ory-sdk";
536
- import { CustomException as CustomException2 } from "@nxgt/shared-exceptions";
537
- import DataLoader2 from "dataloader";
538
- import { defaultFieldResolver } from "graphql";
539
- function createKetoChecks(ory) {
540
- const loader = new DataLoader2((questions) => ory.checkMany([...questions]), {
541
- cacheKeyFn: ({ permission, subject }) => tuple(permission, subject)
542
- });
543
- return (permission, subject) => loader.load({ permission, subject });
648
+ import { claimsFromOryPrincipal } from "@nxgt/security/integrations/ory";
649
+
650
+ // src/utils/errors/ory-unavailable.ts
651
+ import { ErrorCode as ErrorCode5 } from "@nxgt/shared-exceptions";
652
+ import { GraphQLError as GraphQLError2 } from "graphql";
653
+ function isOryUnavailable(error) {
654
+ if (!(error instanceof Error))
655
+ return false;
656
+ const service = error.service;
657
+ return error.name === "OryUnavailable" && typeof service === "string";
544
658
  }
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
- };
659
+ function serviceUnavailableError(error) {
660
+ return new GraphQLError2(`ory: ${error.service} is unavailable`, {
661
+ extensions: {
662
+ code: ErrorCode5.ServiceUnavailable,
663
+ http: { status: 503 },
664
+ debugMessage: error.message
575
665
  }
576
666
  });
577
667
  }
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) {
591
- const transformed = new WeakSet;
592
- return {
593
- onSchemaChange: ({ schema, replaceSchema }) => {
594
- if (transformed.has(schema))
595
- return;
596
- const next = applyKetoChecks(schema);
597
- transformed.add(next);
598
- replaceSchema(next);
599
- },
600
- onContextBuilding: ({ extendContext }) => {
601
- extendContext({ ketoChecks: createKetoChecks(ory) });
602
- }
603
- };
604
- }
668
+
605
669
  // src/plugins/ory-auth.ts
606
- import {
607
- bearerOf,
608
- OryUnavailable as OryUnavailable2
609
- } from "@nxgt/ory-sdk";
610
- import { claimsFromOryPrincipal } from "@nxgt/security/integrations/ory";
611
- import { GraphQLError as GraphQLError2 } from "graphql";
612
670
  function toPrincipal(ory) {
613
671
  const email = ory.identity?.email;
614
672
  return {
@@ -625,20 +683,14 @@ function toPrincipal(ory) {
625
683
  };
626
684
  }
627
685
  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
- });
686
+ return serviceUnavailableError(error);
635
687
  }
636
688
  async function resolveOryPrincipal(ory, headers) {
637
689
  let principal;
638
690
  try {
639
691
  principal = await ory.resolve(headers);
640
692
  } catch (error) {
641
- if (error instanceof OryUnavailable2)
693
+ if (isOryUnavailable(error))
642
694
  throw oryUnavailableError(error);
643
695
  throw error;
644
696
  }
@@ -747,15 +799,15 @@ import {
747
799
  } from "graphql-scalars";
748
800
 
749
801
  // src/scalars/custom/utils.ts
750
- import { CustomException as CustomException3 } from "@nxgt/shared-exceptions";
751
- import { logger as logger2 } from "@nxgt/shared-logging";
802
+ import { CustomException } from "@nxgt/shared-exceptions";
803
+ import { logger } from "@nxgt/shared-logging";
752
804
  import { GraphQLScalarType as GraphQLScalarType2 } from "graphql";
753
805
  function createScalarFrom(scalar, {
754
806
  name,
755
807
  description,
756
808
  errorMessage
757
809
  }) {
758
- const exception = CustomException3.badRequest({
810
+ const exception = CustomException.badRequest({
759
811
  message: errorMessage
760
812
  });
761
813
  return new GraphQLScalarType2({
@@ -769,7 +821,7 @@ function createScalarFrom(scalar, {
769
821
  try {
770
822
  return scalar.serialize(value);
771
823
  } catch (e) {
772
- logger2.error(e);
824
+ logger.error(e);
773
825
  throw exception;
774
826
  }
775
827
  },
@@ -780,18 +832,18 @@ function createScalarFrom(scalar, {
780
832
  try {
781
833
  return scalar.parseValue(value);
782
834
  } catch (e) {
783
- logger2.error(e);
835
+ logger.error(e);
784
836
  throw exception;
785
837
  }
786
838
  },
787
- parseLiteral(ast) {
839
+ parseLiteral(ast, variables) {
788
840
  if (!errorMessage) {
789
- return scalar.parseLiteral(ast);
841
+ return scalar.parseLiteral(ast, variables);
790
842
  }
791
843
  try {
792
- return scalar.parseLiteral(ast);
844
+ return scalar.parseLiteral(ast, variables);
793
845
  } catch (e) {
794
- logger2.error(e);
846
+ logger.error(e);
795
847
  throw exception;
796
848
  }
797
849
  }
@@ -982,8 +1034,7 @@ type Subscription {
982
1034
  _empty: String
983
1035
  }
984
1036
 
985
- directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
986
-
1037
+ ${AUTHENTICATED_DIRECTIVE_SDL}
987
1038
  directive @policy(policies: [[String!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
988
1039
 
989
1040
  directive @shareable on OBJECT | FIELD_DEFINITION
@@ -993,17 +1044,441 @@ directive @link(url: String!, import: [String!]) on SCHEMA
993
1044
  `;
994
1045
  // src/upload.ts
995
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
+ }
996
1463
  export {
997
- CHECK_DIRECTIVE_NAME,
1464
+ AUTHENTICATED_DIRECTIVE_NAME,
1465
+ AUTHENTICATED_DIRECTIVE_SDL,
1466
+ CALLER_TYPES,
998
1467
  DataLoader3 as DataLoader,
999
1468
  FEDERATION_DIRECTIVES,
1469
+ GATEWAY_SECRET_HEADER,
1470
+ KETO_DIRECTIVES_SDL,
1471
+ PERMISSION_DIRECTIVE_NAME,
1472
+ PERMISSION_DIRECTIVE_SDL,
1000
1473
  SCALARS_MAPPING,
1001
1474
  SCALAR_RESOLVERS,
1002
1475
  SHARED_SCHEMA_PATH,
1003
1476
  SHARED_TYPE_DEFS,
1477
+ applyAuthenticated,
1004
1478
  applyKetoChecks,
1005
1479
  assertReadablePath,
1006
1480
  buildSubgraphSchema,
1481
+ can,
1007
1482
  createChangeResolvers,
1008
1483
  createChildrenConnectionDataLoader,
1009
1484
  createChildrenListDataLoader,
@@ -1018,25 +1493,34 @@ export {
1018
1493
  createSingleChildDataLoader,
1019
1494
  createYogaHono,
1020
1495
  createYogaPubSub,
1496
+ denial,
1497
+ denialMessageKey,
1021
1498
  extractJwtPlugin,
1499
+ gatewaySecret,
1022
1500
  generateSchema,
1023
1501
  graphqlUploadExpress,
1024
1502
  honoYoga,
1503
+ isOryUnavailable,
1025
1504
  loadTypeDefs,
1026
1505
  objectIds,
1027
1506
  oryUnavailableError,
1028
- readChecks,
1507
+ principalOf,
1029
1508
  readPath,
1509
+ readPermissions,
1030
1510
  renderSandbox,
1511
+ requireGatewayTrust,
1512
+ requireUser,
1031
1513
  resolveOryPrincipal,
1032
1514
  resolveWsUser,
1033
- sandboxExpolorer,
1515
+ sandboxExplorer,
1516
+ serviceUnavailableError,
1034
1517
  setupYogaWebSocketServer,
1035
1518
  toPrincipal,
1036
1519
  useAuth,
1520
+ useAuthenticated,
1037
1521
  useKetoChecks,
1038
1522
  useOryAuth
1039
1523
  };
1040
1524
 
1041
- //# debugId=6FBEA5C770B5605F64756E2164756E21
1525
+ //# debugId=D1F8FC67079E823264756E2164756E21
1042
1526
  //# sourceMappingURL=index.js.map