@nxgt/janus-graphql 0.1.1 → 0.3.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 +110 -21
- package/dist/connection/connection.d.ts +36 -0
- package/dist/connection/connection.d.ts.map +1 -0
- package/dist/connection/credential.d.ts +34 -0
- package/dist/connection/credential.d.ts.map +1 -0
- package/dist/connection/types.d.ts +57 -0
- package/dist/connection/types.d.ts.map +1 -0
- package/dist/context.d.ts +11 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/directives/apply.d.ts +10 -9
- package/dist/directives/apply.d.ts.map +1 -1
- package/dist/directives/guard.d.ts +2 -1
- package/dist/directives/guard.d.ts.map +1 -1
- package/dist/directives/read.d.ts +9 -1
- package/dist/directives/read.d.ts.map +1 -1
- package/dist/directives/validate.d.ts +7 -2
- package/dist/directives/validate.d.ts.map +1 -1
- package/dist/fresh.d.ts +37 -0
- package/dist/fresh.d.ts.map +1 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +170 -23
- package/dist/index.js.map +14 -10
- package/dist/options.d.ts +17 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/plugin.d.ts +8 -3
- package/dist/plugin.d.ts.map +1 -1
- package/dist/sdl.d.ts.map +1 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +5 -3
- package/docs/guide/context.md +36 -3
- package/docs/guide/directives.md +64 -5
- package/docs/guide/errors.md +2 -1
- package/docs/guide/federation.md +2 -1
- package/docs/guide/step-up.md +186 -0
- package/docs/guide/subscriptions.md +251 -0
- package/docs/roadmap.md +29 -9
- package/docs/troubleshooting.md +218 -11
- package/graphql/janus.graphqls +8 -0
- package/package.json +11 -2
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EACX,iBAAiB,EACjB,eAAe,EACf,sBAAsB,GACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACN,oBAAoB,EACpB,KAAK,sBAAsB,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACN,KAAK,UAAU,EACf,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,KAAK,SAAS,GACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,GAAG,EAAE,KAAK,kBAAkB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,YAAY,EACX,IAAI,EACJ,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,UAAU,GACV,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
// src/context.ts
|
|
2
|
+
import {
|
|
3
|
+
systemClock
|
|
4
|
+
} from "@nxgt/janus";
|
|
4
5
|
|
|
5
6
|
// src/memo.ts
|
|
6
7
|
var NOTATION = /[@#]/;
|
|
@@ -32,14 +33,15 @@ function memoized(access) {
|
|
|
32
33
|
|
|
33
34
|
// src/context.ts
|
|
34
35
|
var checks = new WeakMap;
|
|
36
|
+
var clocks = new WeakMap;
|
|
35
37
|
function createJanusContext(request, options) {
|
|
36
|
-
const { auth, access, type } = options;
|
|
38
|
+
const { auth, access, type, clock, caller = "useJanus()" } = options;
|
|
37
39
|
let answer;
|
|
38
40
|
let asked = false;
|
|
39
41
|
const authenticated = () => {
|
|
40
42
|
if (!asked) {
|
|
41
43
|
asked = true;
|
|
42
|
-
answer = request === undefined ? Promise.reject(new TypeError(
|
|
44
|
+
answer = request === undefined ? Promise.reject(new TypeError(`${caller}: the GraphQL context has no request to authenticate — build the context from an HTTP request, or accept a graphql-ws connection with janusConnection().onConnect`)) : auth.authenticate(request, type === undefined ? undefined : { type });
|
|
43
45
|
}
|
|
44
46
|
return answer;
|
|
45
47
|
};
|
|
@@ -55,8 +57,13 @@ function createJanusContext(request, options) {
|
|
|
55
57
|
fresh: (...args) => can.apply(access, args)
|
|
56
58
|
});
|
|
57
59
|
}
|
|
60
|
+
if (clock !== undefined)
|
|
61
|
+
clocks.set(janus, clock);
|
|
58
62
|
return janus;
|
|
59
63
|
}
|
|
64
|
+
function clockOf(janus) {
|
|
65
|
+
return clocks.get(janus) ?? systemClock;
|
|
66
|
+
}
|
|
60
67
|
function checkOf(janus, fresh = false) {
|
|
61
68
|
const built = checks.get(janus);
|
|
62
69
|
if (built !== undefined)
|
|
@@ -72,6 +79,83 @@ function janusOf(ctx, caller) {
|
|
|
72
79
|
return janus;
|
|
73
80
|
}
|
|
74
81
|
|
|
82
|
+
// src/options.ts
|
|
83
|
+
function checkWiring(caller, { auth, access, clock }) {
|
|
84
|
+
if (typeof auth?.authenticate !== "function") {
|
|
85
|
+
throw new TypeError(`${caller}: auth is not what janus() answered — pass { auth }`);
|
|
86
|
+
}
|
|
87
|
+
if (access !== undefined && typeof access?.can !== "function") {
|
|
88
|
+
throw new TypeError(`${caller}: access is not what permissions() answered — pass { auth, access }`);
|
|
89
|
+
}
|
|
90
|
+
if (clock !== undefined && typeof clock?.now !== "function") {
|
|
91
|
+
throw new TypeError(`${caller}: clock is not a Clock — pass the clock given to janus(), or leave it out for the system clock`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/connection/credential.ts
|
|
96
|
+
var BEARER = /^\s*bearer(?:\s|$)/i;
|
|
97
|
+
function credentialOf(ctx, upgrade) {
|
|
98
|
+
const params = ctx.connectionParams;
|
|
99
|
+
if (params !== undefined && Object.hasOwn(params, "authorization")) {
|
|
100
|
+
const { authorization } = params;
|
|
101
|
+
if (typeof authorization !== "string")
|
|
102
|
+
return null;
|
|
103
|
+
if (BEARER.test(authorization))
|
|
104
|
+
return { authorization };
|
|
105
|
+
}
|
|
106
|
+
const request = upgrade === undefined ? ctx.extra?.request : upgrade(ctx);
|
|
107
|
+
return request ?? null;
|
|
108
|
+
}
|
|
109
|
+
var accepted = new WeakMap;
|
|
110
|
+
function accept(ctx, credential) {
|
|
111
|
+
const { extra } = ctx;
|
|
112
|
+
if (typeof extra !== "object" || extra === null) {
|
|
113
|
+
throw new TypeError("janusConnection(): the connection's extra is not an object — pass the context graphql-ws gave onConnect");
|
|
114
|
+
}
|
|
115
|
+
accepted.set(extra, credential);
|
|
116
|
+
}
|
|
117
|
+
function acceptedOf(context) {
|
|
118
|
+
const extra = context?.extra;
|
|
119
|
+
if (typeof extra !== "object" || extra === null)
|
|
120
|
+
return;
|
|
121
|
+
return accepted.get(extra);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/connection/connection.ts
|
|
125
|
+
function janusConnection(options) {
|
|
126
|
+
const { auth, access, type, clock } = options;
|
|
127
|
+
checkWiring("janusConnection()", { auth, access, clock });
|
|
128
|
+
if (options.upgrade !== undefined && typeof options.upgrade !== "function") {
|
|
129
|
+
throw new TypeError("janusConnection(): upgrade is not a function — pass (ctx) => the upgrade request, or leave it out to read ctx.extra.request");
|
|
130
|
+
}
|
|
131
|
+
const upgrade = options.upgrade?.bind(options);
|
|
132
|
+
const wiring = {
|
|
133
|
+
auth,
|
|
134
|
+
access,
|
|
135
|
+
type,
|
|
136
|
+
clock,
|
|
137
|
+
caller: "janusConnection().context"
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
onConnect: async (ctx) => {
|
|
141
|
+
const credential = credentialOf(ctx, upgrade);
|
|
142
|
+
if (credential === null)
|
|
143
|
+
return false;
|
|
144
|
+
const current = await auth.authenticate(credential, type === undefined ? undefined : { type });
|
|
145
|
+
if (current === null)
|
|
146
|
+
return false;
|
|
147
|
+
accept(ctx, credential);
|
|
148
|
+
return true;
|
|
149
|
+
},
|
|
150
|
+
context: (ctx) => ({
|
|
151
|
+
janus: createJanusContext(acceptedOf(ctx), wiring)
|
|
152
|
+
})
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// src/directives/apply.ts
|
|
156
|
+
import { MapperKind, mapSchema } from "@graphql-tools/utils";
|
|
157
|
+
import { defaultFieldResolver } from "graphql";
|
|
158
|
+
|
|
75
159
|
// src/errors.ts
|
|
76
160
|
import { JanusError, statusOf } from "@nxgt/janus";
|
|
77
161
|
import { GraphQLError } from "graphql";
|
|
@@ -165,6 +249,30 @@ function masked(error, message) {
|
|
|
165
249
|
});
|
|
166
250
|
}
|
|
167
251
|
|
|
252
|
+
// src/fresh.ts
|
|
253
|
+
import {
|
|
254
|
+
assertFresh,
|
|
255
|
+
parseDuration
|
|
256
|
+
} from "@nxgt/janus";
|
|
257
|
+
async function freshSession(janus, maxAgeMs) {
|
|
258
|
+
const session = await janus.session().then(undefined, rethrown);
|
|
259
|
+
if (session === null)
|
|
260
|
+
throw denial("UNAUTHENTICATED");
|
|
261
|
+
try {
|
|
262
|
+
assertFresh(session, maxAgeMs, clockOf(janus));
|
|
263
|
+
} catch (error) {
|
|
264
|
+
rethrown(error);
|
|
265
|
+
}
|
|
266
|
+
return session;
|
|
267
|
+
}
|
|
268
|
+
async function requireFresh(ctx, maxAge) {
|
|
269
|
+
if (typeof maxAge !== "string") {
|
|
270
|
+
throw new TypeError("requireFresh(): maxAge is a duration with its unit, such as '10m' — a bare number would be milliseconds, where @fresh reads seconds");
|
|
271
|
+
}
|
|
272
|
+
const maxAgeMs = parseDuration(maxAge, "requireFresh(): maxAge");
|
|
273
|
+
return freshSession(janusOf(ctx, "requireFresh()"), maxAgeMs);
|
|
274
|
+
}
|
|
275
|
+
|
|
168
276
|
// src/directives/permission/path.ts
|
|
169
277
|
var NAME = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
170
278
|
function parsePath(text) {
|
|
@@ -278,6 +386,9 @@ async function enforce(requirement, { parent, args, ctx, fresh }) {
|
|
|
278
386
|
if (requirement.types !== null && !requirement.types.has(user.type)) {
|
|
279
387
|
throw denial("FORBIDDEN");
|
|
280
388
|
}
|
|
389
|
+
if (requirement.maxAgeMs !== null) {
|
|
390
|
+
await freshSession(janus, requirement.maxAgeMs);
|
|
391
|
+
}
|
|
281
392
|
const [first] = requirement.permissions;
|
|
282
393
|
if (first === undefined)
|
|
283
394
|
return;
|
|
@@ -326,12 +437,16 @@ function locations(schema, field, fieldName, typeName) {
|
|
|
326
437
|
}
|
|
327
438
|
function readField(schema, field, fieldName, typeName) {
|
|
328
439
|
const authenticated = [];
|
|
440
|
+
const fresh = [];
|
|
329
441
|
const permission = [];
|
|
330
442
|
for (const { where, onType, node } of locations(schema, field, fieldName, typeName)) {
|
|
331
443
|
for (const args of getDirective(schema, node, "authenticated") ?? []) {
|
|
332
444
|
const types = args.type;
|
|
333
445
|
authenticated.push({ where, types: types ?? null });
|
|
334
446
|
}
|
|
447
|
+
for (const args of getDirective(schema, node, "fresh") ?? []) {
|
|
448
|
+
fresh.push({ where, maxAge: args.maxAge });
|
|
449
|
+
}
|
|
335
450
|
for (const args of getDirective(schema, node, "permission") ?? []) {
|
|
336
451
|
permission.push({
|
|
337
452
|
where,
|
|
@@ -343,7 +458,7 @@ function readField(schema, field, fieldName, typeName) {
|
|
|
343
458
|
});
|
|
344
459
|
}
|
|
345
460
|
}
|
|
346
|
-
return { authenticated, permission };
|
|
461
|
+
return { authenticated, fresh, permission };
|
|
347
462
|
}
|
|
348
463
|
|
|
349
464
|
// src/directives/words.ts
|
|
@@ -473,15 +588,15 @@ function conditionOf(model, use, wiring, refuse) {
|
|
|
473
588
|
|
|
474
589
|
// src/directives/validate.ts
|
|
475
590
|
function requirementOf(field, directives, known, wiring) {
|
|
476
|
-
const { authenticated, permission } = directives;
|
|
477
|
-
|
|
591
|
+
const { authenticated, fresh, permission } = directives;
|
|
592
|
+
const [name, first] = firstOf(directives);
|
|
593
|
+
if (first === undefined)
|
|
478
594
|
return null;
|
|
479
|
-
const [first] = authenticated.length > 0 ? authenticated : permission;
|
|
480
|
-
const name = authenticated.length > 0 ? "@authenticated" : "@permission";
|
|
481
595
|
return {
|
|
482
596
|
where: field.name,
|
|
483
|
-
label: `${name} on ${on(first
|
|
597
|
+
label: `${name} on ${on(first.where, field.name)}`,
|
|
484
598
|
types: typesOf(field.name, authenticated, known),
|
|
599
|
+
maxAgeMs: maxAgeOf(field.name, fresh),
|
|
485
600
|
permissions: permission.map((use) => permissionCheckOf(use, field, wiring))
|
|
486
601
|
};
|
|
487
602
|
}
|
|
@@ -505,6 +620,27 @@ function typesOf(field, uses, known) {
|
|
|
505
620
|
}
|
|
506
621
|
return types;
|
|
507
622
|
}
|
|
623
|
+
function firstOf({
|
|
624
|
+
authenticated,
|
|
625
|
+
fresh,
|
|
626
|
+
permission
|
|
627
|
+
}) {
|
|
628
|
+
if (authenticated.length > 0)
|
|
629
|
+
return ["@authenticated", authenticated[0]];
|
|
630
|
+
if (fresh.length > 0)
|
|
631
|
+
return ["@fresh", fresh[0]];
|
|
632
|
+
return ["@permission", permission[0]];
|
|
633
|
+
}
|
|
634
|
+
function maxAgeOf(field, uses) {
|
|
635
|
+
let seconds = null;
|
|
636
|
+
for (const { where, maxAge } of uses) {
|
|
637
|
+
if (!Number.isSafeInteger(maxAge) || maxAge <= 0) {
|
|
638
|
+
throw new TypeError(`${PREFIX}: @fresh on ${on(where, field)} asks maxAge: ${maxAge} — write a number of seconds above zero, such as maxAge: 600 for ten minutes`);
|
|
639
|
+
}
|
|
640
|
+
seconds = seconds === null ? maxAge : Math.min(seconds, maxAge);
|
|
641
|
+
}
|
|
642
|
+
return seconds === null ? null : seconds * 1000;
|
|
643
|
+
}
|
|
508
644
|
|
|
509
645
|
// src/directives/apply.ts
|
|
510
646
|
function applyJanusDirectives(schema, options) {
|
|
@@ -517,10 +653,15 @@ function applyJanusDirectives(schema, options) {
|
|
|
517
653
|
}, readField(schema, field, fieldName, typeName), known, options);
|
|
518
654
|
if (requirement === null)
|
|
519
655
|
return field;
|
|
656
|
+
const resolve = field.resolve ?? defaultFieldResolver;
|
|
657
|
+
if (field.subscribe === undefined) {
|
|
658
|
+
return { ...field, resolve: guarded(resolve, requirement) };
|
|
659
|
+
}
|
|
660
|
+
const onEvent = { ...requirement, maxAgeMs: null };
|
|
520
661
|
return {
|
|
521
662
|
...field,
|
|
522
|
-
|
|
523
|
-
|
|
663
|
+
subscribe: guarded(field.subscribe, requirement),
|
|
664
|
+
resolve: guarded(resolve, onEvent)
|
|
524
665
|
};
|
|
525
666
|
}
|
|
526
667
|
});
|
|
@@ -559,21 +700,17 @@ async function can(ctx, permission, object, ...options) {
|
|
|
559
700
|
}
|
|
560
701
|
// src/plugin.ts
|
|
561
702
|
function useJanus(options) {
|
|
562
|
-
const { auth, access, type, loaders, conditions } = options;
|
|
563
|
-
|
|
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
|
-
}
|
|
703
|
+
const { auth, access, type, clock, loaders, conditions } = options;
|
|
704
|
+
checkWiring("useJanus()", { auth, access, clock });
|
|
569
705
|
const applied = new WeakSet;
|
|
570
706
|
return {
|
|
571
707
|
onContextBuilding: ({ context, extendContext }) => {
|
|
572
708
|
const { request } = context;
|
|
573
|
-
const janus = createJanusContext(request, {
|
|
709
|
+
const janus = createJanusContext(request ?? acceptedOf(context), {
|
|
574
710
|
auth,
|
|
575
711
|
access,
|
|
576
|
-
type
|
|
712
|
+
type,
|
|
713
|
+
clock
|
|
577
714
|
});
|
|
578
715
|
extendContext({ janus });
|
|
579
716
|
},
|
|
@@ -602,6 +739,14 @@ directive @authenticated(
|
|
|
602
739
|
type: [String!]
|
|
603
740
|
) on OBJECT | INTERFACE | FIELD_DEFINITION
|
|
604
741
|
|
|
742
|
+
"""
|
|
743
|
+
Only a session that proved who it is less than \`maxAge\` seconds ago reaches
|
|
744
|
+
this field, or every field of this type or interface: signed in, or confirmed
|
|
745
|
+
since by a step-up. Anonymous is answered UNAUTHENTICATED, an older session
|
|
746
|
+
STEP_UP_REQUIRED. Where several apply, the smallest \`maxAge\` holds.
|
|
747
|
+
"""
|
|
748
|
+
directive @fresh(maxAge: Int!) on OBJECT | INTERFACE | FIELD_DEFINITION
|
|
749
|
+
|
|
605
750
|
"""
|
|
606
751
|
What a denied \`@permission\` answers.
|
|
607
752
|
"""
|
|
@@ -634,12 +779,14 @@ export {
|
|
|
634
779
|
applyJanusDirectives,
|
|
635
780
|
can,
|
|
636
781
|
denial,
|
|
782
|
+
janusConnection,
|
|
637
783
|
janusGraphQLError,
|
|
638
784
|
janusMaskError,
|
|
639
785
|
janusTypeDefs,
|
|
786
|
+
requireFresh,
|
|
640
787
|
requireUser,
|
|
641
788
|
useJanus
|
|
642
789
|
};
|
|
643
790
|
|
|
644
|
-
//# debugId=
|
|
791
|
+
//# debugId=C6BF0BFA8BF13A9C64756E2164756E21
|
|
645
792
|
//# sourceMappingURL=index.js.map
|