@nxgt/janus-graphql 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +248 -0
- package/dist/context.d.ts +63 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/directives/apply.d.ts +46 -0
- package/dist/directives/apply.d.ts.map +1 -0
- package/dist/directives/guard.d.ts +21 -0
- package/dist/directives/guard.d.ts.map +1 -0
- package/dist/directives/permission/enforce.d.ts +24 -0
- package/dist/directives/permission/enforce.d.ts.map +1 -0
- package/dist/directives/permission/model.d.ts +35 -0
- package/dist/directives/permission/model.d.ts.map +1 -0
- package/dist/directives/permission/path.d.ts +31 -0
- package/dist/directives/permission/path.d.ts.map +1 -0
- package/dist/directives/permission/validate.d.ts +42 -0
- package/dist/directives/permission/validate.d.ts.map +1 -0
- package/dist/directives/read.d.ts +35 -0
- package/dist/directives/read.d.ts.map +1 -0
- package/dist/directives/validate.d.ts +33 -0
- package/dist/directives/validate.d.ts.map +1 -0
- package/dist/directives/words.d.ts +7 -0
- package/dist/directives/words.d.ts.map +1 -0
- package/dist/errors.d.ts +44 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/helpers.d.ts +56 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +645 -0
- package/dist/index.js.map +24 -0
- package/dist/memo.d.ts +16 -0
- package/dist/memo.d.ts.map +1 -0
- package/dist/plugin.d.ts +32 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/sdl.d.ts +12 -0
- package/dist/sdl.d.ts.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/wiring.d.ts +56 -0
- package/dist/wiring.d.ts.map +1 -0
- package/docs/README.md +18 -0
- package/docs/guide/context.md +156 -0
- package/docs/guide/directives.md +312 -0
- package/docs/guide/errors.md +138 -0
- package/docs/guide/federation.md +70 -0
- package/docs/guide/telemetry.md +45 -0
- package/docs/roadmap.md +59 -0
- package/docs/troubleshooting.md +471 -0
- package/graphql/janus.graphqls +36 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
// src/directives/apply.ts
|
|
2
|
+
import { MapperKind, mapSchema } from "@graphql-tools/utils";
|
|
3
|
+
import { defaultFieldResolver } from "graphql";
|
|
4
|
+
|
|
5
|
+
// src/memo.ts
|
|
6
|
+
var NOTATION = /[@#]/;
|
|
7
|
+
function keyOf(subject, permission, object, options) {
|
|
8
|
+
if (subject === null || options?.ctx !== undefined)
|
|
9
|
+
return null;
|
|
10
|
+
if (NOTATION.test(object.id) || NOTATION.test(subject.id))
|
|
11
|
+
return null;
|
|
12
|
+
const set = typeof subject.relation === "string" ? `#${subject.relation}` : "";
|
|
13
|
+
return `${object.type}:${object.id}#${permission}@${subject.type}:${subject.id}${set}`;
|
|
14
|
+
}
|
|
15
|
+
function memoized(access) {
|
|
16
|
+
const can = access.can;
|
|
17
|
+
const answers = new Map;
|
|
18
|
+
const ask = async (subject, permission, object, options) => can.call(access, subject, permission, object, options);
|
|
19
|
+
return (subject, permission, object, options) => {
|
|
20
|
+
const key = keyOf(subject, permission, object, options);
|
|
21
|
+
if (key === null)
|
|
22
|
+
return ask(subject, permission, object, options);
|
|
23
|
+
const known = answers.get(key);
|
|
24
|
+
if (known !== undefined)
|
|
25
|
+
return known;
|
|
26
|
+
const answer = ask(subject, permission, object, options);
|
|
27
|
+
answers.set(key, answer);
|
|
28
|
+
answer.then(undefined, () => answers.delete(key));
|
|
29
|
+
return answer;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/context.ts
|
|
34
|
+
var checks = new WeakMap;
|
|
35
|
+
function createJanusContext(request, options) {
|
|
36
|
+
const { auth, access, type } = options;
|
|
37
|
+
let answer;
|
|
38
|
+
let asked = false;
|
|
39
|
+
const authenticated = () => {
|
|
40
|
+
if (!asked) {
|
|
41
|
+
asked = true;
|
|
42
|
+
answer = request === undefined ? Promise.reject(new TypeError("useJanus(): the GraphQL context has no request to authenticate — build the context from an HTTP request")) : auth.authenticate(request, type === undefined ? undefined : { type });
|
|
43
|
+
}
|
|
44
|
+
return answer;
|
|
45
|
+
};
|
|
46
|
+
const janus = Object.freeze({
|
|
47
|
+
user: () => authenticated().then((current) => current?.user ?? null),
|
|
48
|
+
session: () => authenticated().then((current) => current?.session ?? null),
|
|
49
|
+
...access === undefined ? {} : { access }
|
|
50
|
+
});
|
|
51
|
+
if (access !== undefined) {
|
|
52
|
+
const can = access.can;
|
|
53
|
+
checks.set(janus, {
|
|
54
|
+
memo: memoized(access),
|
|
55
|
+
fresh: (...args) => can.apply(access, args)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return janus;
|
|
59
|
+
}
|
|
60
|
+
function checkOf(janus, fresh = false) {
|
|
61
|
+
const built = checks.get(janus);
|
|
62
|
+
if (built !== undefined)
|
|
63
|
+
return fresh ? built.fresh : built.memo;
|
|
64
|
+
const access = janus.access;
|
|
65
|
+
return typeof access?.can === "function" ? access.can : null;
|
|
66
|
+
}
|
|
67
|
+
function janusOf(ctx, caller) {
|
|
68
|
+
const janus = ctx?.janus;
|
|
69
|
+
if (janus === undefined || typeof janus.user !== "function") {
|
|
70
|
+
throw new TypeError(`${caller}: ctx.janus is not set — add useJanus({ auth }) to the plugins`);
|
|
71
|
+
}
|
|
72
|
+
return janus;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/errors.ts
|
|
76
|
+
import { JanusError, statusOf } from "@nxgt/janus";
|
|
77
|
+
import { GraphQLError } from "graphql";
|
|
78
|
+
function denial(code, message) {
|
|
79
|
+
const [status, fallback] = denialOf(code);
|
|
80
|
+
return new GraphQLError(message ?? fallback, {
|
|
81
|
+
extensions: { code, http: { status } }
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function denialOf(code) {
|
|
85
|
+
switch (code) {
|
|
86
|
+
case "UNAUTHENTICATED":
|
|
87
|
+
return [401, "Not signed in"];
|
|
88
|
+
case "FORBIDDEN":
|
|
89
|
+
return [403, "Forbidden"];
|
|
90
|
+
case "NOT_FOUND":
|
|
91
|
+
return [404, "Not found"];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function janusGraphQLError(error) {
|
|
95
|
+
const status = statusOf(error.code);
|
|
96
|
+
const code = error.code === "STORE_FAILED" ? "SERVICE_UNAVAILABLE" : error.code;
|
|
97
|
+
return new GraphQLError(messageOf(status), {
|
|
98
|
+
originalError: error,
|
|
99
|
+
extensions: { code, http: { status }, ...actionable(error) }
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function messageOf(status) {
|
|
103
|
+
switch (status) {
|
|
104
|
+
case 400:
|
|
105
|
+
return "Invalid request";
|
|
106
|
+
case 401:
|
|
107
|
+
return "Invalid credentials";
|
|
108
|
+
case 403:
|
|
109
|
+
return "Forbidden";
|
|
110
|
+
case 404:
|
|
111
|
+
return "Not found";
|
|
112
|
+
case 409:
|
|
113
|
+
return "Conflict";
|
|
114
|
+
case 503:
|
|
115
|
+
return "The service is unavailable, retry later";
|
|
116
|
+
case 500:
|
|
117
|
+
case 501:
|
|
118
|
+
return "Internal server error";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function actionable(error) {
|
|
122
|
+
switch (error.code) {
|
|
123
|
+
case "USER_INVALID":
|
|
124
|
+
return { issues: error.issues ?? [] };
|
|
125
|
+
case "PASSWORD_TOO_SHORT":
|
|
126
|
+
return error.minLength === undefined ? {} : { minLength: error.minLength };
|
|
127
|
+
case "CODE_INVALID":
|
|
128
|
+
return error.attemptsLeft === undefined ? {} : { attemptsLeft: error.attemptsLeft };
|
|
129
|
+
default:
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function rethrown(error) {
|
|
134
|
+
throw error instanceof JanusError ? janusGraphQLError(error) : error;
|
|
135
|
+
}
|
|
136
|
+
function janusMaskError(fallback = masked) {
|
|
137
|
+
return (error, message, isDev) => {
|
|
138
|
+
const original = error instanceof GraphQLError ? error.originalError : error;
|
|
139
|
+
if (original instanceof JanusError) {
|
|
140
|
+
const answer = janusGraphQLError(original);
|
|
141
|
+
return error instanceof GraphQLError ? new GraphQLError(answer.message, {
|
|
142
|
+
nodes: error.nodes ?? null,
|
|
143
|
+
path: error.path ?? null,
|
|
144
|
+
originalError: original,
|
|
145
|
+
extensions: answer.extensions
|
|
146
|
+
}) : answer;
|
|
147
|
+
}
|
|
148
|
+
return fallback(error, message, isDev);
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function masked(error, message) {
|
|
152
|
+
if (error instanceof GraphQLError) {
|
|
153
|
+
const original = error.originalError;
|
|
154
|
+
if (original === undefined || original instanceof GraphQLError) {
|
|
155
|
+
return error;
|
|
156
|
+
}
|
|
157
|
+
return new GraphQLError(message, {
|
|
158
|
+
nodes: error.nodes ?? null,
|
|
159
|
+
path: error.path ?? null,
|
|
160
|
+
extensions: { code: "INTERNAL_SERVER_ERROR" }
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return new GraphQLError(message, {
|
|
164
|
+
extensions: { code: "INTERNAL_SERVER_ERROR" }
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/directives/permission/path.ts
|
|
169
|
+
var NAME = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
170
|
+
function parsePath(text) {
|
|
171
|
+
const [root, ...segments] = text.split(".");
|
|
172
|
+
if (root !== "args" && root !== "parent")
|
|
173
|
+
return null;
|
|
174
|
+
if (segments.length === 0 || !segments.every((name) => NAME.test(name))) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return { text, root, segments };
|
|
178
|
+
}
|
|
179
|
+
function targetsOf(path, parent, args) {
|
|
180
|
+
const containers = walk([path.root === "args" ? args : parent], path.segments.slice(0, -1));
|
|
181
|
+
if (containers === null)
|
|
182
|
+
return null;
|
|
183
|
+
const last = path.segments[path.segments.length - 1];
|
|
184
|
+
const held = path.root === "parent" && last === "id";
|
|
185
|
+
const targets = [];
|
|
186
|
+
for (const container of containers) {
|
|
187
|
+
for (const value of spread(container[last])) {
|
|
188
|
+
const id = idOf(value);
|
|
189
|
+
if (id === null)
|
|
190
|
+
return null;
|
|
191
|
+
targets.push({ id, holder: held ? container : null });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return path.root === "parent" && targets.length === 0 ? null : targets;
|
|
195
|
+
}
|
|
196
|
+
function walk(values, segments) {
|
|
197
|
+
let current = values;
|
|
198
|
+
for (const segment of segments) {
|
|
199
|
+
const next = [];
|
|
200
|
+
for (const value of current) {
|
|
201
|
+
if (typeof value !== "object" || value === null)
|
|
202
|
+
return null;
|
|
203
|
+
next.push(...spread(value[segment]));
|
|
204
|
+
}
|
|
205
|
+
current = next;
|
|
206
|
+
}
|
|
207
|
+
return current.every((value) => typeof value === "object" && value !== null) ? current : null;
|
|
208
|
+
}
|
|
209
|
+
function spread(value) {
|
|
210
|
+
return Array.isArray(value) ? value.flat(Number.POSITIVE_INFINITY) : [value];
|
|
211
|
+
}
|
|
212
|
+
function idOf(value) {
|
|
213
|
+
if (typeof value === "string")
|
|
214
|
+
return value;
|
|
215
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
216
|
+
return String(value);
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/directives/permission/enforce.ts
|
|
222
|
+
async function enforcePermission(permission, asking) {
|
|
223
|
+
const targets = targetsOf(permission.path, asking.parent, asking.args);
|
|
224
|
+
if (targets === null) {
|
|
225
|
+
warnNoId(permission);
|
|
226
|
+
throw denial("NOT_FOUND");
|
|
227
|
+
}
|
|
228
|
+
const objects = await Promise.all(targets.map((target) => objectOf(permission, target, asking.ctx))).then(undefined, rethrown);
|
|
229
|
+
const found = objects.filter((object) => object !== null);
|
|
230
|
+
if (found.length < objects.length)
|
|
231
|
+
throw denial("NOT_FOUND");
|
|
232
|
+
const answers = await Promise.all(found.map((object) => ask(permission, object, asking))).then(undefined, rethrown);
|
|
233
|
+
if (answers.includes(false))
|
|
234
|
+
throw denial(permission.onDeny);
|
|
235
|
+
}
|
|
236
|
+
async function ask(permission, object, { check, user, ctx }) {
|
|
237
|
+
const options = permission.condition === null ? undefined : { ctx: await permission.condition(object, ctx) };
|
|
238
|
+
return check(user, permission.name, object, options);
|
|
239
|
+
}
|
|
240
|
+
var UNNAMEABLE = /^$|[@#()]/;
|
|
241
|
+
var isNameable = (id) => !UNNAMEABLE.test(id) && !id.includes("\x00") && id.isWellFormed();
|
|
242
|
+
async function objectOf(permission, { id, holder }, ctx) {
|
|
243
|
+
const { type, load } = permission;
|
|
244
|
+
if (!isNameable(id))
|
|
245
|
+
return null;
|
|
246
|
+
if (holder !== null)
|
|
247
|
+
return view(holder, type, id);
|
|
248
|
+
if (load === null)
|
|
249
|
+
return { type, id };
|
|
250
|
+
const loaded = await load(id, ctx);
|
|
251
|
+
if (loaded === null)
|
|
252
|
+
return null;
|
|
253
|
+
if (typeof loaded !== "object") {
|
|
254
|
+
throw new TypeError(`${permission.label}: loaders.${type} answered ${typeof loaded} — answer the object, or null when there is none`);
|
|
255
|
+
}
|
|
256
|
+
return view(loaded, type, id);
|
|
257
|
+
}
|
|
258
|
+
function view(object, type, id) {
|
|
259
|
+
return new Proxy(Object.create(null), {
|
|
260
|
+
get: (_, key) => key === "type" ? type : key === "id" ? id : Reflect.get(object, key),
|
|
261
|
+
has: (_, key) => key === "type" || Reflect.has(object, key)
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
var warned = new WeakSet;
|
|
265
|
+
function warnNoId(permission) {
|
|
266
|
+
if (warned.has(permission))
|
|
267
|
+
return;
|
|
268
|
+
warned.add(permission);
|
|
269
|
+
process.emitWarning(`${permission.label} resolved no object id from ${permission.path.text} — answered NOT_FOUND`, { code: "JANUS_GRAPHQL_NO_OBJECT_ID" });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/directives/guard.ts
|
|
273
|
+
async function enforce(requirement, { parent, args, ctx, fresh }) {
|
|
274
|
+
const janus = janusOf(ctx, requirement.label);
|
|
275
|
+
const user = await janus.user().then(undefined, rethrown);
|
|
276
|
+
if (user === null)
|
|
277
|
+
throw denial("UNAUTHENTICATED");
|
|
278
|
+
if (requirement.types !== null && !requirement.types.has(user.type)) {
|
|
279
|
+
throw denial("FORBIDDEN");
|
|
280
|
+
}
|
|
281
|
+
const [first] = requirement.permissions;
|
|
282
|
+
if (first === undefined)
|
|
283
|
+
return;
|
|
284
|
+
const check = checkOf(janus, fresh);
|
|
285
|
+
if (check === null) {
|
|
286
|
+
throw new TypeError(`${first.label}: ctx.janus.access is not set — pass { access } to useJanus()`);
|
|
287
|
+
}
|
|
288
|
+
for (const permission of requirement.permissions) {
|
|
289
|
+
await enforcePermission(permission, { check, user, parent, args, ctx });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function guarded(resolve, requirement) {
|
|
293
|
+
return async (source, args, ctx, info) => {
|
|
294
|
+
await enforce(requirement, {
|
|
295
|
+
parent: source,
|
|
296
|
+
args,
|
|
297
|
+
ctx,
|
|
298
|
+
fresh: info.operation.operation === "subscription"
|
|
299
|
+
});
|
|
300
|
+
return resolve(source, args, ctx, info);
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/directives/read.ts
|
|
305
|
+
import { getDirective } from "@graphql-tools/utils";
|
|
306
|
+
function locations(schema, field, fieldName, typeName) {
|
|
307
|
+
const type = schema.getType(typeName);
|
|
308
|
+
const faces = type.getInterfaces();
|
|
309
|
+
const fields = [];
|
|
310
|
+
for (const face of faces) {
|
|
311
|
+
const own = face.getFields()[fieldName];
|
|
312
|
+
if (own !== undefined) {
|
|
313
|
+
fields.push({
|
|
314
|
+
where: `${face.name}.${fieldName}`,
|
|
315
|
+
onType: false,
|
|
316
|
+
node: own
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return [
|
|
321
|
+
...faces.map((face) => ({ where: face.name, onType: true, node: face })),
|
|
322
|
+
{ where: typeName, onType: true, node: type },
|
|
323
|
+
...fields,
|
|
324
|
+
{ where: `${typeName}.${fieldName}`, onType: false, node: field }
|
|
325
|
+
];
|
|
326
|
+
}
|
|
327
|
+
function readField(schema, field, fieldName, typeName) {
|
|
328
|
+
const authenticated = [];
|
|
329
|
+
const permission = [];
|
|
330
|
+
for (const { where, onType, node } of locations(schema, field, fieldName, typeName)) {
|
|
331
|
+
for (const args of getDirective(schema, node, "authenticated") ?? []) {
|
|
332
|
+
const types = args.type;
|
|
333
|
+
authenticated.push({ where, types: types ?? null });
|
|
334
|
+
}
|
|
335
|
+
for (const args of getDirective(schema, node, "permission") ?? []) {
|
|
336
|
+
permission.push({
|
|
337
|
+
where,
|
|
338
|
+
onType,
|
|
339
|
+
name: args.name,
|
|
340
|
+
type: args.type,
|
|
341
|
+
id: args.id ?? null,
|
|
342
|
+
onDeny: args.onDeny === "FORBIDDEN" ? "FORBIDDEN" : "NOT_FOUND"
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return { authenticated, permission };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/directives/words.ts
|
|
350
|
+
var PREFIX = "applyJanusDirectives()";
|
|
351
|
+
function on(where, field) {
|
|
352
|
+
return where === field ? field : `${where} (read by ${field})`;
|
|
353
|
+
}
|
|
354
|
+
function list(names) {
|
|
355
|
+
return names.map((name) => `'${name}'`).join(", ");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/directives/permission/model.ts
|
|
359
|
+
function modelOf(access) {
|
|
360
|
+
const model = access?.model;
|
|
361
|
+
if (!Array.isArray(model?.types) || model.definition?.types === undefined) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
return model;
|
|
365
|
+
}
|
|
366
|
+
function namesOf(model, type) {
|
|
367
|
+
const def = model.definition.types[type];
|
|
368
|
+
return [
|
|
369
|
+
...Object.keys(def?.related ?? {}),
|
|
370
|
+
...Object.keys(def?.permits ?? {})
|
|
371
|
+
];
|
|
372
|
+
}
|
|
373
|
+
function isFromField(value) {
|
|
374
|
+
return value?.kind === "fromField";
|
|
375
|
+
}
|
|
376
|
+
function fieldsOf(model, type) {
|
|
377
|
+
const related = model.definition.types[type]?.related ?? {};
|
|
378
|
+
return Object.values(related).filter(isFromField).map((def) => def.field);
|
|
379
|
+
}
|
|
380
|
+
function reachesCondition(model, type, name, seen = new Set) {
|
|
381
|
+
const key = `${type}.${name}`;
|
|
382
|
+
if (seen.has(key))
|
|
383
|
+
return false;
|
|
384
|
+
seen.add(key);
|
|
385
|
+
const def = model.definition.types[type];
|
|
386
|
+
for (const rule of def?.permits?.[name] ?? []) {
|
|
387
|
+
if (typeof rule !== "string")
|
|
388
|
+
return true;
|
|
389
|
+
const [relation, target] = rule.split("->");
|
|
390
|
+
if (target === undefined) {
|
|
391
|
+
if (reachesCondition(model, type, rule, seen))
|
|
392
|
+
return true;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
for (const next of arrowTargets(def?.related?.[relation])) {
|
|
396
|
+
if (reachesCondition(model, next, target, seen))
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
function arrowTargets(relation) {
|
|
403
|
+
if (isFromField(relation)) {
|
|
404
|
+
return [relation.subject];
|
|
405
|
+
}
|
|
406
|
+
if (!Array.isArray(relation))
|
|
407
|
+
return [];
|
|
408
|
+
return relation.filter((holder) => typeof holder === "string").map((holder) => holder.split("#")[0]);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/directives/permission/validate.ts
|
|
412
|
+
function permissionCheckOf(use, field, wiring) {
|
|
413
|
+
const label = `@permission on ${on(use.where, field.name)}`;
|
|
414
|
+
const refuse = (why) => new TypeError(`${PREFIX}: ${label} ${why}`);
|
|
415
|
+
const model = modelOf(wiring.access);
|
|
416
|
+
if (model === null) {
|
|
417
|
+
throw refuse("needs the permissions() instance — pass useJanus({ auth, access }), with access what permissions() answered");
|
|
418
|
+
}
|
|
419
|
+
checkNames(model, use, refuse);
|
|
420
|
+
const text = use.id ?? (use.onType ? "parent.id" : "args.id");
|
|
421
|
+
const path = parsePath(text);
|
|
422
|
+
if (path === null) {
|
|
423
|
+
throw refuse(`reads its id from '${text}', which is not args.<name> or parent.<name>`);
|
|
424
|
+
}
|
|
425
|
+
const [first] = path.segments;
|
|
426
|
+
if (path.root === "args" && !field.args.includes(first)) {
|
|
427
|
+
throw refuse(`reads ${path.root}.${first}, and ${field.name} takes no argument ${first}${use.id === null && use.onType === false ? ' — name the id with id: "parent.<field>" or id: "args.<name>"' : ""}`);
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
label,
|
|
431
|
+
name: use.name,
|
|
432
|
+
type: use.type,
|
|
433
|
+
onDeny: use.onDeny,
|
|
434
|
+
path,
|
|
435
|
+
load: loaderOf(model, use.type, path, wiring, refuse),
|
|
436
|
+
condition: conditionOf(model, use, wiring, refuse)
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function checkNames(model, use, refuse) {
|
|
440
|
+
if (!model.types.includes(use.type)) {
|
|
441
|
+
throw refuse(`names the object type '${use.type}', which is not one of ${list(model.types)}`);
|
|
442
|
+
}
|
|
443
|
+
const names = namesOf(model, use.type);
|
|
444
|
+
if (!names.includes(use.name)) {
|
|
445
|
+
throw refuse(`asks '${use.name}', which ${use.type} does not declare — it declares ${list(names)}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function loaderOf(model, type, path, wiring, refuse) {
|
|
449
|
+
const load = wiring.loaders?.[type];
|
|
450
|
+
if (load !== undefined && typeof load !== "function") {
|
|
451
|
+
throw refuse(`finds loaders.${type}, which is not a function`);
|
|
452
|
+
}
|
|
453
|
+
const held = path.root === "parent" && path.segments.at(-1) === "id";
|
|
454
|
+
if (held)
|
|
455
|
+
return null;
|
|
456
|
+
if (load !== undefined)
|
|
457
|
+
return load;
|
|
458
|
+
const fields = fieldsOf(model, type);
|
|
459
|
+
if (fields.length > 0) {
|
|
460
|
+
throw refuse(`reads the ${type}'s id from ${path.text}, and ${type} reads ${list(fields)} of the object itself (fromField) — pass useJanus({ loaders: { ${type}: (id, ctx) => … } })`);
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
function conditionOf(model, use, wiring, refuse) {
|
|
465
|
+
if (!reachesCondition(model, use.type, use.name))
|
|
466
|
+
return null;
|
|
467
|
+
const condition = wiring.conditions?.[use.type];
|
|
468
|
+
if (typeof condition !== "function") {
|
|
469
|
+
throw refuse(`asks '${use.name}' of ${use.type}, which reaches a when() — pass useJanus({ conditions: { ${use.type}: (object, ctx) => … } })`);
|
|
470
|
+
}
|
|
471
|
+
return condition;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/directives/validate.ts
|
|
475
|
+
function requirementOf(field, directives, known, wiring) {
|
|
476
|
+
const { authenticated, permission } = directives;
|
|
477
|
+
if (authenticated.length === 0 && permission.length === 0)
|
|
478
|
+
return null;
|
|
479
|
+
const [first] = authenticated.length > 0 ? authenticated : permission;
|
|
480
|
+
const name = authenticated.length > 0 ? "@authenticated" : "@permission";
|
|
481
|
+
return {
|
|
482
|
+
where: field.name,
|
|
483
|
+
label: `${name} on ${on(first?.where ?? field.name, field.name)}`,
|
|
484
|
+
types: typesOf(field.name, authenticated, known),
|
|
485
|
+
permissions: permission.map((use) => permissionCheckOf(use, field, wiring))
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function typesOf(field, uses, known) {
|
|
489
|
+
let types = null;
|
|
490
|
+
for (const { where, types: named } of uses) {
|
|
491
|
+
if (named === null)
|
|
492
|
+
continue;
|
|
493
|
+
if (named.length === 0) {
|
|
494
|
+
throw new TypeError(`${PREFIX}: @authenticated on ${on(where, field)} names no user type — leave type: out to admit any signed-in user`);
|
|
495
|
+
}
|
|
496
|
+
for (const name of named) {
|
|
497
|
+
if (!known.types.includes(name)) {
|
|
498
|
+
throw new TypeError(`${PREFIX}: @authenticated on ${on(where, field)} names the user type '${name}', which is not one of ${list(known.types)}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
types = types === null ? new Set(named) : new Set(named.filter((name) => types?.has(name)));
|
|
502
|
+
}
|
|
503
|
+
if (types !== null && types.size === 0) {
|
|
504
|
+
throw new TypeError(`${PREFIX}: the @authenticated on ${field} and on its type or interfaces admit no user type in common`);
|
|
505
|
+
}
|
|
506
|
+
return types;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/directives/apply.ts
|
|
510
|
+
function applyJanusDirectives(schema, options) {
|
|
511
|
+
const known = knownOf(options);
|
|
512
|
+
return mapSchema(schema, {
|
|
513
|
+
[MapperKind.OBJECT_FIELD]: (field, fieldName, typeName) => {
|
|
514
|
+
const requirement = requirementOf({
|
|
515
|
+
name: `${typeName}.${fieldName}`,
|
|
516
|
+
args: Object.keys(field.args ?? {})
|
|
517
|
+
}, readField(schema, field, fieldName, typeName), known, options);
|
|
518
|
+
if (requirement === null)
|
|
519
|
+
return field;
|
|
520
|
+
return {
|
|
521
|
+
...field,
|
|
522
|
+
resolve: guarded(field.resolve ?? defaultFieldResolver, requirement),
|
|
523
|
+
...field.subscribe === undefined ? {} : { subscribe: guarded(field.subscribe, requirement) }
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
function knownOf({ auth, type }) {
|
|
529
|
+
if (type === undefined)
|
|
530
|
+
return { types: auth.types };
|
|
531
|
+
if (!auth.types.includes(type)) {
|
|
532
|
+
throw new TypeError(`${PREFIX}: type '${type}' is not a user type of auth — it knows ${list(auth.types)}`);
|
|
533
|
+
}
|
|
534
|
+
return { types: [type] };
|
|
535
|
+
}
|
|
536
|
+
// src/helpers.ts
|
|
537
|
+
async function requireUser(ctx, options = {}) {
|
|
538
|
+
const { type } = options;
|
|
539
|
+
const allowed = typeof type === "string" ? [type] : type;
|
|
540
|
+
if (allowed?.length === 0) {
|
|
541
|
+
throw new TypeError("requireUser(): type is an empty list, which no user could pass — name at least one user type, or leave type out");
|
|
542
|
+
}
|
|
543
|
+
const user = await janusOf(ctx, "requireUser()").user().then(undefined, rethrown);
|
|
544
|
+
if (user === null)
|
|
545
|
+
throw denial("UNAUTHENTICATED");
|
|
546
|
+
if (allowed !== undefined && !allowed.includes(user.type)) {
|
|
547
|
+
throw denial("FORBIDDEN");
|
|
548
|
+
}
|
|
549
|
+
return user;
|
|
550
|
+
}
|
|
551
|
+
async function can(ctx, permission, object, ...options) {
|
|
552
|
+
const janus = janusOf(ctx, "can()");
|
|
553
|
+
const check = checkOf(janus);
|
|
554
|
+
if (check === null) {
|
|
555
|
+
throw new TypeError("can(): ctx.janus.access is not set — pass { access } to useJanus()");
|
|
556
|
+
}
|
|
557
|
+
const subject = await janus.user().then(undefined, rethrown);
|
|
558
|
+
return check(subject, permission, object, options[0]).then(undefined, rethrown);
|
|
559
|
+
}
|
|
560
|
+
// src/plugin.ts
|
|
561
|
+
function useJanus(options) {
|
|
562
|
+
const { auth, access, type, loaders, conditions } = options;
|
|
563
|
+
if (typeof auth?.authenticate !== "function") {
|
|
564
|
+
throw new TypeError("useJanus(): auth is not what janus() answered — pass { auth }");
|
|
565
|
+
}
|
|
566
|
+
if (access !== undefined && typeof access?.can !== "function") {
|
|
567
|
+
throw new TypeError("useJanus(): access is not what permissions() answered — pass { auth, access }");
|
|
568
|
+
}
|
|
569
|
+
const applied = new WeakSet;
|
|
570
|
+
return {
|
|
571
|
+
onContextBuilding: ({ context, extendContext }) => {
|
|
572
|
+
const { request } = context;
|
|
573
|
+
const janus = createJanusContext(request, {
|
|
574
|
+
auth,
|
|
575
|
+
access,
|
|
576
|
+
type
|
|
577
|
+
});
|
|
578
|
+
extendContext({ janus });
|
|
579
|
+
},
|
|
580
|
+
onSchemaChange: ({ schema, replaceSchema }) => {
|
|
581
|
+
if (applied.has(schema))
|
|
582
|
+
return;
|
|
583
|
+
const next = applyJanusDirectives(schema, {
|
|
584
|
+
auth,
|
|
585
|
+
...type === undefined ? {} : { type },
|
|
586
|
+
access,
|
|
587
|
+
loaders,
|
|
588
|
+
conditions
|
|
589
|
+
});
|
|
590
|
+
applied.add(next);
|
|
591
|
+
replaceSchema(next);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
// src/sdl.ts
|
|
596
|
+
var janusTypeDefs = `"""
|
|
597
|
+
Only a signed-in user reaches this field, or every field of this type or
|
|
598
|
+
interface. With \`type\`, only a user of one of these user types: any other is
|
|
599
|
+
answered FORBIDDEN.
|
|
600
|
+
"""
|
|
601
|
+
directive @authenticated(
|
|
602
|
+
type: [String!]
|
|
603
|
+
) on OBJECT | INTERFACE | FIELD_DEFINITION
|
|
604
|
+
|
|
605
|
+
"""
|
|
606
|
+
What a denied \`@permission\` answers.
|
|
607
|
+
"""
|
|
608
|
+
enum JanusPermissionDenial {
|
|
609
|
+
"""
|
|
610
|
+
404: the object is not told to exist. The default.
|
|
611
|
+
"""
|
|
612
|
+
NOT_FOUND
|
|
613
|
+
"""
|
|
614
|
+
403: the object exists, and this user may not do this to it.
|
|
615
|
+
"""
|
|
616
|
+
FORBIDDEN
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
"""
|
|
620
|
+
Only a user holding permission \`name\` on the object of \`type\` whose id \`id\`
|
|
621
|
+
reads reaches this field, or every field of this type or interface. \`id\` is
|
|
622
|
+
\`args.<path>\` or \`parent.<path>\`: \`args.id\` on a field and \`parent.id\` on a
|
|
623
|
+
type or an interface when absent. A list of ids requires the permission on
|
|
624
|
+
every one. Repeated, every one must hold, in the order written.
|
|
625
|
+
"""
|
|
626
|
+
directive @permission(
|
|
627
|
+
name: String!
|
|
628
|
+
type: String!
|
|
629
|
+
id: String
|
|
630
|
+
onDeny: JanusPermissionDenial! = NOT_FOUND
|
|
631
|
+
) repeatable on OBJECT | INTERFACE | FIELD_DEFINITION
|
|
632
|
+
`;
|
|
633
|
+
export {
|
|
634
|
+
applyJanusDirectives,
|
|
635
|
+
can,
|
|
636
|
+
denial,
|
|
637
|
+
janusGraphQLError,
|
|
638
|
+
janusMaskError,
|
|
639
|
+
janusTypeDefs,
|
|
640
|
+
requireUser,
|
|
641
|
+
useJanus
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
//# debugId=D0DEC0D35AFB92A464756E2164756E21
|
|
645
|
+
//# sourceMappingURL=index.js.map
|