@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.
Files changed (41) hide show
  1. package/README.md +110 -21
  2. package/dist/connection/connection.d.ts +36 -0
  3. package/dist/connection/connection.d.ts.map +1 -0
  4. package/dist/connection/credential.d.ts +34 -0
  5. package/dist/connection/credential.d.ts.map +1 -0
  6. package/dist/connection/types.d.ts +57 -0
  7. package/dist/connection/types.d.ts.map +1 -0
  8. package/dist/context.d.ts +11 -2
  9. package/dist/context.d.ts.map +1 -1
  10. package/dist/directives/apply.d.ts +10 -9
  11. package/dist/directives/apply.d.ts.map +1 -1
  12. package/dist/directives/guard.d.ts +2 -1
  13. package/dist/directives/guard.d.ts.map +1 -1
  14. package/dist/directives/read.d.ts +9 -1
  15. package/dist/directives/read.d.ts.map +1 -1
  16. package/dist/directives/validate.d.ts +7 -2
  17. package/dist/directives/validate.d.ts.map +1 -1
  18. package/dist/fresh.d.ts +37 -0
  19. package/dist/fresh.d.ts.map +1 -0
  20. package/dist/index.d.ts +12 -5
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +170 -23
  23. package/dist/index.js.map +14 -10
  24. package/dist/options.d.ts +17 -0
  25. package/dist/options.d.ts.map +1 -0
  26. package/dist/plugin.d.ts +8 -3
  27. package/dist/plugin.d.ts.map +1 -1
  28. package/dist/sdl.d.ts.map +1 -1
  29. package/dist/types.d.ts +6 -1
  30. package/dist/types.d.ts.map +1 -1
  31. package/docs/README.md +5 -3
  32. package/docs/guide/context.md +36 -3
  33. package/docs/guide/directives.md +64 -5
  34. package/docs/guide/errors.md +2 -1
  35. package/docs/guide/federation.md +2 -1
  36. package/docs/guide/step-up.md +186 -0
  37. package/docs/guide/subscriptions.md +251 -0
  38. package/docs/roadmap.md +29 -9
  39. package/docs/troubleshooting.md +218 -11
  40. package/graphql/janus.graphqls +8 -0
  41. package/package.json +11 -2
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,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,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"}
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/directives/apply.ts
2
- import { MapperKind, mapSchema } from "@graphql-tools/utils";
3
- import { defaultFieldResolver } from "graphql";
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("useJanus(): the GraphQL context has no request to authenticate — build the context from an HTTP request")) : auth.authenticate(request, type === undefined ? undefined : { type });
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
- if (authenticated.length === 0 && permission.length === 0)
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?.where ?? field.name, field.name)}`,
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
- resolve: guarded(field.resolve ?? defaultFieldResolver, requirement),
523
- ...field.subscribe === undefined ? {} : { subscribe: guarded(field.subscribe, requirement) }
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
- 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
- }
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=D0DEC0D35AFB92A464756E2164756E21
791
+ //# debugId=C6BF0BFA8BF13A9C64756E2164756E21
645
792
  //# sourceMappingURL=index.js.map