@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.
- package/README.md +196 -81
- package/dist/directives/authenticated.d.ts +42 -0
- package/dist/directives/authenticated.d.ts.map +1 -0
- package/dist/directives/federation.d.ts +6 -0
- package/dist/directives/federation.d.ts.map +1 -1
- package/dist/directives/index.d.ts +5 -1
- package/dist/directives/index.d.ts.map +1 -1
- package/dist/directives/paths.d.ts +20 -0
- package/dist/directives/paths.d.ts.map +1 -0
- package/dist/directives/permission.d.ts +39 -0
- package/dist/directives/permission.d.ts.map +1 -0
- package/dist/directives/removed-check.d.ts +15 -0
- package/dist/directives/removed-check.d.ts.map +1 -0
- package/dist/directives/scope.d.ts +19 -0
- package/dist/directives/scope.d.ts.map +1 -0
- package/dist/directives/sdl.d.ts +26 -0
- package/dist/directives/sdl.d.ts.map +1 -0
- package/dist/directives/validate.d.ts +23 -0
- package/dist/directives/validate.d.ts.map +1 -0
- package/dist/index.js +1048 -564
- package/dist/index.js.map +37 -20
- package/dist/integrations/hono.d.ts +2 -2
- package/dist/integrations/hono.d.ts.map +1 -1
- package/dist/plugins/apply-authenticated.d.ts +21 -0
- package/dist/plugins/apply-authenticated.d.ts.map +1 -0
- package/dist/plugins/apply-keto-checks.d.ts +20 -0
- package/dist/plugins/apply-keto-checks.d.ts.map +1 -0
- package/dist/plugins/auth.d.ts +17 -1
- package/dist/plugins/auth.d.ts.map +1 -1
- package/dist/plugins/authenticated.d.ts +14 -0
- package/dist/plugins/authenticated.d.ts.map +1 -0
- package/dist/plugins/extract-jwt.d.ts +19 -6
- package/dist/plugins/extract-jwt.d.ts.map +1 -1
- package/dist/plugins/field-guard.d.ts +30 -0
- package/dist/plugins/field-guard.d.ts.map +1 -0
- package/dist/plugins/gateway-trust.d.ts +47 -0
- package/dist/plugins/gateway-trust.d.ts.map +1 -0
- package/dist/plugins/index.d.ts +6 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/keto-checker.d.ts +24 -0
- package/dist/plugins/keto-checker.d.ts.map +1 -0
- package/dist/plugins/keto-checks.d.ts +11 -34
- package/dist/plugins/keto-checks.d.ts.map +1 -1
- package/dist/plugins/keto-helpers.d.ts +38 -0
- package/dist/plugins/keto-helpers.d.ts.map +1 -0
- package/dist/plugins/ory-auth.d.ts +5 -5
- package/dist/plugins/ory-auth.d.ts.map +1 -1
- package/dist/scalars/custom/utils.d.ts.map +1 -1
- package/dist/shared.d.ts +1 -1
- package/dist/shared.d.ts.map +1 -1
- package/dist/utils/errors/create-error.d.ts +3 -0
- package/dist/utils/errors/create-error.d.ts.map +1 -0
- package/dist/utils/errors/denial.d.ts +20 -0
- package/dist/utils/errors/denial.d.ts.map +1 -0
- package/dist/utils/errors/format-error.d.ts +12 -0
- package/dist/utils/errors/format-error.d.ts.map +1 -0
- package/dist/utils/errors/index.d.ts +6 -0
- package/dist/utils/errors/index.d.ts.map +1 -0
- package/dist/utils/errors/mask-error.d.ts +25 -0
- package/dist/utils/errors/mask-error.d.ts.map +1 -0
- package/dist/utils/errors/ory-unavailable.d.ts +19 -0
- package/dist/utils/errors/ory-unavailable.d.ts.map +1 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/ws-context.d.ts +1 -1
- package/docs/README.md +13 -0
- package/docs/guide/authentication.md +187 -0
- package/docs/guide/errors.md +136 -0
- package/docs/guide/migrating-to-3.md +276 -0
- package/docs/guide/permissions.md +220 -0
- package/docs/roadmap.md +71 -0
- package/docs/troubleshooting.md +273 -0
- package/graphql/directives/permission.graphqls +73 -0
- package/package.json +11 -13
- package/dist/directives/check.d.ts +0 -40
- package/dist/directives/check.d.ts.map +0 -1
- package/dist/utils/errors.utils.d.ts +0 -23
- package/dist/utils/errors.utils.d.ts.map +0 -1
- 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/
|
|
6
|
+
// src/directives/authenticated.ts
|
|
7
7
|
import { getDirective } from "@graphql-tools/utils";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
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/
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
363
|
+
...config,
|
|
364
|
+
extensions: {
|
|
365
|
+
...config.extensions,
|
|
366
|
+
[GUARDS]: { guards, ...base, wrapped }
|
|
78
367
|
},
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
|
|
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
|
-
|
|
211
|
-
}));
|
|
433
|
+
}
|
|
212
434
|
});
|
|
213
435
|
}
|
|
214
|
-
function
|
|
215
|
-
return
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
233
|
-
}));
|
|
502
|
+
}
|
|
234
503
|
});
|
|
235
504
|
}
|
|
236
|
-
function
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
246
|
-
return
|
|
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/
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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/
|
|
349
|
-
import {
|
|
350
|
-
import
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
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
|
-
|
|
413
|
-
function
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
|
427
|
-
|
|
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/
|
|
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
|
-
|
|
534
|
-
tuple
|
|
646
|
+
bearerOf
|
|
535
647
|
} from "@nxgt/ory-sdk";
|
|
536
|
-
import {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
|
546
|
-
return
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
751
|
-
import { logger
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1507
|
+
principalOf,
|
|
1029
1508
|
readPath,
|
|
1509
|
+
readPermissions,
|
|
1030
1510
|
renderSandbox,
|
|
1511
|
+
requireGatewayTrust,
|
|
1512
|
+
requireUser,
|
|
1031
1513
|
resolveOryPrincipal,
|
|
1032
1514
|
resolveWsUser,
|
|
1033
|
-
|
|
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=
|
|
1525
|
+
//# debugId=D1F8FC67079E823264756E2164756E21
|
|
1042
1526
|
//# sourceMappingURL=index.js.map
|