@nxgt/shared-graphql 2.1.0 → 3.0.1

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 (68) hide show
  1. package/README.md +112 -52
  2. package/dist/directives/authenticated.d.ts +42 -0
  3. package/dist/directives/authenticated.d.ts.map +1 -0
  4. package/dist/directives/federation.d.ts +6 -0
  5. package/dist/directives/federation.d.ts.map +1 -1
  6. package/dist/directives/index.d.ts +1 -2
  7. package/dist/directives/index.d.ts.map +1 -1
  8. package/dist/directives/permission.d.ts +20 -6
  9. package/dist/directives/permission.d.ts.map +1 -1
  10. package/dist/directives/removed-check.d.ts +15 -0
  11. package/dist/directives/removed-check.d.ts.map +1 -0
  12. package/dist/directives/scope.d.ts +3 -3
  13. package/dist/directives/scope.d.ts.map +1 -1
  14. package/dist/directives/sdl.d.ts +14 -4
  15. package/dist/directives/sdl.d.ts.map +1 -1
  16. package/dist/directives/validate.d.ts +8 -14
  17. package/dist/directives/validate.d.ts.map +1 -1
  18. package/dist/index.js +1037 -926
  19. package/dist/index.js.map +35 -31
  20. package/dist/integrations/hono.d.ts +2 -2
  21. package/dist/integrations/hono.d.ts.map +1 -1
  22. package/dist/plugins/apply-authenticated.d.ts +21 -0
  23. package/dist/plugins/apply-authenticated.d.ts.map +1 -0
  24. package/dist/plugins/apply-keto-checks.d.ts +10 -7
  25. package/dist/plugins/apply-keto-checks.d.ts.map +1 -1
  26. package/dist/plugins/auth.d.ts +17 -1
  27. package/dist/plugins/auth.d.ts.map +1 -1
  28. package/dist/plugins/authenticated.d.ts +14 -0
  29. package/dist/plugins/authenticated.d.ts.map +1 -0
  30. package/dist/plugins/extract-jwt.d.ts +19 -6
  31. package/dist/plugins/extract-jwt.d.ts.map +1 -1
  32. package/dist/plugins/field-guard.d.ts +30 -0
  33. package/dist/plugins/field-guard.d.ts.map +1 -0
  34. package/dist/plugins/gateway-trust.d.ts +15 -0
  35. package/dist/plugins/gateway-trust.d.ts.map +1 -0
  36. package/dist/plugins/index.d.ts +3 -0
  37. package/dist/plugins/index.d.ts.map +1 -1
  38. package/dist/plugins/keto-checks.d.ts +3 -2
  39. package/dist/plugins/keto-checks.d.ts.map +1 -1
  40. package/dist/plugins/keto-helpers.d.ts +1 -1
  41. package/dist/plugins/keto-helpers.d.ts.map +1 -1
  42. package/dist/plugins/ory-auth.d.ts +3 -3
  43. package/dist/shared.d.ts +1 -1
  44. package/dist/shared.d.ts.map +1 -1
  45. package/dist/utils/errors/denial.d.ts +20 -0
  46. package/dist/utils/errors/denial.d.ts.map +1 -0
  47. package/dist/utils/errors/format-error.d.ts +2 -1
  48. package/dist/utils/errors/format-error.d.ts.map +1 -1
  49. package/dist/utils/errors/index.d.ts +1 -0
  50. package/dist/utils/errors/index.d.ts.map +1 -1
  51. package/dist/utils/errors/mask-error.d.ts +2 -1
  52. package/dist/utils/errors/mask-error.d.ts.map +1 -1
  53. package/dist/utils/ws-context.d.ts +1 -1
  54. package/docs/README.md +8 -6
  55. package/docs/guide/authentication.md +187 -0
  56. package/docs/guide/errors.md +37 -5
  57. package/docs/guide/migrating-to-3.md +276 -0
  58. package/docs/guide/permissions.md +39 -50
  59. package/docs/roadmap.md +30 -29
  60. package/docs/troubleshooting.md +151 -48
  61. package/package.json +7 -11
  62. package/dist/directives/check.d.ts +0 -32
  63. package/dist/directives/check.d.ts.map +0 -1
  64. package/dist/directives/deprecation.d.ts +0 -7
  65. package/dist/directives/deprecation.d.ts.map +0 -1
  66. package/dist/directives/requirements.d.ts +0 -14
  67. package/dist/directives/requirements.d.ts.map +0 -1
  68. package/graphql/directives/check.graphqls +0 -90
@@ -1,7 +1,7 @@
1
1
  import type { YogaInitialContext, YogaServerInstance } from 'graphql-yoga';
2
2
  import { Hono } from 'hono';
3
- import { type RenderSandboxOptions } from '../utils';
4
- export declare function sandboxExpolorer(options: RenderSandboxOptions): import("hono").MiddlewareHandler<any, string, {}, Response>;
3
+ import { type RenderSandboxOptions } from '../utils/sandbox';
4
+ export declare function sandboxExplorer(options: RenderSandboxOptions): import("hono").MiddlewareHandler<any, string, {}, Response>;
5
5
  type HonoYogaOptions = {
6
6
  sandbox?: RenderSandboxOptions & {
7
7
  endpoint?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../../src/integrations/hono.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,EAAE,KAAK,oBAAoB,EAAiB,MAAM,UAAU,CAAC;AAEpE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,oBAAoB,+DAI7D;AAED,KAAK,eAAe,GAAG;IACtB,OAAO,CAAC,EAAE,oBAAoB,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACvD,CAAC;AAEF,wBAAgB,QAAQ,CACvB,aAAa,SAAS,OAAO,CAAC,kBAAkB,CAAC,EACjD,WAAW,SAAS,EAAE,EACrB,IAAI,EAAE,kBAAkB,CAAC,aAAa,EAAE,WAAW,CAAC,+DAMrD;AAED,wBAAgB,cAAc,CAC7B,aAAa,SAAS,OAAO,CAAC,kBAAkB,CAAC,EACjD,WAAW,SAAS,EAAE,EAEtB,IAAI,EAAE,kBAAkB,CAAC,aAAa,EAAE,WAAW,CAAC,EACpD,OAAO,CAAC,EAAE,eAAe,8EA0BzB"}
1
+ {"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../../src/integrations/hono.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,EAAE,KAAK,oBAAoB,EAAiB,MAAM,kBAAkB,CAAC;AAE5E,wBAAgB,eAAe,CAAC,OAAO,EAAE,oBAAoB,+DAI5D;AAED,KAAK,eAAe,GAAG;IACtB,OAAO,CAAC,EAAE,oBAAoB,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACvD,CAAC;AAEF,wBAAgB,QAAQ,CACvB,aAAa,SAAS,OAAO,CAAC,kBAAkB,CAAC,EACjD,WAAW,SAAS,EAAE,EACrB,IAAI,EAAE,kBAAkB,CAAC,aAAa,EAAE,WAAW,CAAC,+DAMrD;AAED,wBAAgB,cAAc,CAC7B,aAAa,SAAS,OAAO,CAAC,kBAAkB,CAAC,EACjD,WAAW,SAAS,EAAE,EAEtB,IAAI,EAAE,kBAAkB,CAAC,aAAa,EAAE,WAAW,CAAC,EACpD,OAAO,CAAC,EAAE,eAAe,8EA0BzB"}
@@ -0,0 +1,21 @@
1
+ import { type GraphQLSchema } from 'graphql';
2
+ import { type AuthenticatedOptions } from '../directives/authenticated';
3
+ /**
4
+ * Wraps every object field that an `@authenticated` applies to — on the field,
5
+ * on its type, on one of its type's interfaces or on that interface's field,
6
+ * or on the scalar or enum it returns — so the caller is checked before its
7
+ * resolver (and a subscription's `subscribe`) runs: none is
8
+ * `UNAUTHENTICATED` (401), one of a type no `type:` names is `FORBIDDEN`
9
+ * (403). Every one that applies must hold.
10
+ *
11
+ * Read and validated when the schema is built: `type: []`, a type outside
12
+ * `options.types` (`session`, `token` by default) and restrictions with no
13
+ * type in common each throw a `TypeError` naming `Type.field`.
14
+ *
15
+ * The caller is `context.user`, which `useOryAuth` and `useAuth` set; its
16
+ * type is `context.ory.kind`, or `context.user.tokenType` without Ory. A field
17
+ * already guarded is left alone, and a schema with none left to guard is
18
+ * returned as it is, so the plugin's `onSchemaChange` settles.
19
+ */
20
+ export declare function applyAuthenticated(schema: GraphQLSchema, options?: AuthenticatedOptions): GraphQLSchema;
21
+ //# sourceMappingURL=apply-authenticated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-authenticated.d.ts","sourceRoot":"","sources":["../../src/plugins/apply-authenticated.ts"],"names":[],"mappings":"AAEA,OAAO,EAEN,KAAK,aAAa,EAKlB,MAAM,SAAS,CAAC;AACjB,OAAO,EACN,KAAK,oBAAoB,EAKzB,MAAM,6BAA6B,CAAC;AAUrC;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CACjC,MAAM,EAAE,aAAa,EACrB,OAAO,GAAE,oBAAyB,GAChC,aAAa,CAaf"}
@@ -1,17 +1,20 @@
1
- import { type GraphQLSchema } from 'graphql';
1
+ import type { GraphQLSchema } from 'graphql';
2
2
  import { type ReadOptions } from '../directives';
3
3
  /**
4
- * Wraps every field carrying `@check` or `@permission` so the permission is
5
- * answered before its resolver runs.
4
+ * Wraps every field carrying `@permission` so the permission is answered
5
+ * before its resolver runs.
6
6
  *
7
7
  * Every requirement is read and validated here, when the schema is built: a
8
8
  * malformed path, an argument the field does not declare, an empty group, a
9
- * namespace outside `options.namespaces` — and a guard on an interface field,
10
- * which no resolver ever runs through — each throw a `TypeError` naming
11
- * `Type.field`, so the server does not boot.
9
+ * namespace outside `options.namespaces`, a guard on an interface field,
10
+ * which no resolver ever runs through, and a `@check` — removed in 3.0 — each
11
+ * throw a `TypeError` naming `Type.field`, so the server does not boot.
12
12
  *
13
13
  * Modelled on `applyGraphqlPolicy` in `@nxgt/security`. Exported on its own
14
- * because a schema transform is far easier to test than a plugin.
14
+ * because a schema transform is far easier to test than a plugin. A field it
15
+ * already guarded is left as it is, and a schema with no field left to guard
16
+ * is returned as it is — so it never wraps a field twice, and the plugin's
17
+ * `onSchemaChange` settles.
15
18
  */
16
19
  export declare function applyKetoChecks(schema: GraphQLSchema, options?: ReadOptions): GraphQLSchema;
17
20
  //# sourceMappingURL=apply-keto-checks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"apply-keto-checks.d.ts","sourceRoot":"","sources":["../../src/plugins/apply-keto-checks.ts"],"names":[],"mappings":"AAGA,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,SAAS,CAAC;AACjB,OAAO,EAIN,KAAK,WAAW,EAIhB,MAAM,eAAe,CAAC;AAOvB;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC9B,MAAM,EAAE,aAAa,EACrB,OAAO,GAAE,WAAgB,GACvB,aAAa,CAkBf"}
1
+ {"version":3,"file":"apply-keto-checks.d.ts","sourceRoot":"","sources":["../../src/plugins/apply-keto-checks.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAsB,aAAa,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAGN,KAAK,WAAW,EAGhB,MAAM,eAAe,CAAC;AAWvB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC9B,MAAM,EAAE,aAAa,EACrB,OAAO,GAAE,WAAgB,GACvB,aAAa,CAmBf"}
@@ -1,4 +1,20 @@
1
1
  import type { Plugin } from 'graphql-yoga';
2
2
  import type { GraphQLBaseContext } from '../types';
3
- export declare function useAuth(): Plugin<GraphQLBaseContext>;
3
+ import { type GatewayTrustOptions } from './gateway-trust';
4
+ export type UseAuthOptions = GatewayTrustOptions;
5
+ /**
6
+ * The caller a gateway resolved, read from the GraphQL request's `extensions`
7
+ * — `user` and `token` — and ONLY for a request `trustedGateway` vouches for.
8
+ *
9
+ * `extensions` sits in the request body, which any client writes. A request
10
+ * without the gateway's proof leaves the context as it was: no `user`, no
11
+ * `token`, whatever its `extensions` claim. Without a `trustedGateway` this
12
+ * throws when it is called, so a server cannot boot trusting the body.
13
+ *
14
+ * useAuth({ trustedGateway: gatewaySecret({ secret: env.GATEWAY_SECRET }) })
15
+ *
16
+ * An API that resolves its own callers — a Kratos session, a Hydra token —
17
+ * uses `useOryAuth(ory)` instead.
18
+ */
19
+ export declare function useAuth(options: UseAuthOptions): Plugin<GraphQLBaseContext>;
4
20
  //# sourceMappingURL=auth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/plugins/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,wBAAgB,OAAO,IAAI,MAAM,CAAC,kBAAkB,CAAC,CASpD"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/plugins/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,EACN,KAAK,mBAAmB,EAGxB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,MAAM,cAAc,GAAG,mBAAmB,CAAC;AAEjD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAmB3E"}
@@ -0,0 +1,14 @@
1
+ import type { Plugin } from 'graphql-yoga';
2
+ import type { AuthenticatedOptions } from '../directives/authenticated';
3
+ import type { GraphQLBaseContext } from '../types';
4
+ import type { OryContext } from './ory-auth';
5
+ /**
6
+ * Enforces `@authenticated` and `@authenticated(type:)` on every schema the
7
+ * server uses. Goes after the plugin that sets the caller — `useOryAuth(ory)`
8
+ * or `useAuth({ trustedGateway })`.
9
+ *
10
+ * `applyAuthenticated` returns a schema with no field left to guard as it is,
11
+ * so the replacement settles alongside `useKetoChecks` whatever their order.
12
+ */
13
+ export declare function useAuthenticated(options?: AuthenticatedOptions): Plugin<GraphQLBaseContext & OryContext>;
14
+ //# sourceMappingURL=authenticated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authenticated.d.ts","sourceRoot":"","sources":["../../src/plugins/authenticated.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC/B,OAAO,GAAE,oBAAyB,GAChC,MAAM,CAAC,kBAAkB,GAAG,UAAU,CAAC,CAOzC"}
@@ -1,9 +1,22 @@
1
+ import type { ApolloServerPlugin } from '@apollo/server';
1
2
  import type { TokenPrincipal } from '@nxgt/shared';
2
- export declare const extractJwtPlugin: {
3
- requestDidStart({ request, contextValue }: import("@apollo/server").GraphQLRequestContext<{
4
- jwt?: {
5
- payload: TokenPrincipal;
6
- };
7
- }>): Promise<void>;
3
+ import { type GatewayTrustOptions } from './gateway-trust';
4
+ export type ExtractJwtOptions = GatewayTrustOptions;
5
+ /** What `extractJwtPlugin()` sets on an Apollo context. */
6
+ export type JwtContext = {
7
+ jwt?: {
8
+ payload: TokenPrincipal;
9
+ };
8
10
  };
11
+ /**
12
+ * Apollo Server's counterpart of `useAuth()`: the JWT payload a gateway
13
+ * verified and forwarded in `request.extensions.payload`, copied onto
14
+ * `context.jwt` — ONLY for a request `trustedGateway` vouches for.
15
+ *
16
+ * A request without the gateway's proof leaves `context.jwt` as the context
17
+ * function built it, whatever its `extensions` claim.
18
+ *
19
+ * plugins: [extractJwtPlugin({ trustedGateway: gatewaySecret({ secret }) })]
20
+ */
21
+ export declare function extractJwtPlugin(options: ExtractJwtOptions): ApolloServerPlugin<JwtContext>;
9
22
  //# sourceMappingURL=extract-jwt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"extract-jwt.d.ts","sourceRoot":"","sources":["../../src/plugins/extract-jwt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD,eAAO,MAAM,gBAAgB;;cAWtB;YAAE,OAAO,EAAE,cAAc,CAAA;SAAE;;CAChC,CAAC"}
1
+ {"version":3,"file":"extract-jwt.d.ts","sourceRoot":"","sources":["../../src/plugins/extract-jwt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EACN,KAAK,mBAAmB,EAGxB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,MAAM,iBAAiB,GAAG,mBAAmB,CAAC;AAEpD,2DAA2D;AAC3D,MAAM,MAAM,UAAU,GAAG;IACxB,GAAG,CAAC,EAAE;QAAE,OAAO,EAAE,cAAc,CAAA;KAAE,CAAC;CAClC,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC/B,OAAO,EAAE,iBAAiB,GACxB,kBAAkB,CAAC,UAAU,CAAC,CAUhC"}
@@ -0,0 +1,30 @@
1
+ import { type GraphQLFieldConfig } from 'graphql';
2
+ type FieldConfig = GraphQLFieldConfig<unknown, unknown>;
3
+ /** What a guard asks before a field runs; it throws to refuse. */
4
+ export type FieldCheck = (source: unknown, args: Record<string, unknown>, context: unknown) => void | Promise<void>;
5
+ /**
6
+ * One transform's guard on one field: its mark, its rank (lower runs first),
7
+ * its check, and whether the check can run before a subscription's stream is
8
+ * opened — only when it reads nothing off an event (`parent.<path>`).
9
+ */
10
+ export type FieldGuard = {
11
+ mark: string;
12
+ rank: number;
13
+ check: FieldCheck;
14
+ onSubscribe: boolean;
15
+ };
16
+ /**
17
+ * Whether the transform owning `mark` guards this field as it stands. A
18
+ * merged schema's unguarded half, or a field whose resolver was replaced,
19
+ * is not — so a transform run again wraps it, never one twice.
20
+ */
21
+ export declare function isGuarded(config: FieldConfig, mark: string): boolean;
22
+ /**
23
+ * The field with `guard` added. Every guard runs by rank — `@authenticated`
24
+ * before `@permission`, whichever transform ran first — before the resolver;
25
+ * those with `onSubscribe` also run before `subscribe`, so a refused
26
+ * subscription opens no stream.
27
+ */
28
+ export declare function guardField(config: FieldConfig, guard: FieldGuard): FieldConfig;
29
+ export {};
30
+ //# sourceMappingURL=field-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-guard.d.ts","sourceRoot":"","sources":["../../src/plugins/field-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,kBAAkB,EAEvB,MAAM,SAAS,CAAC;AAEjB,KAAK,WAAW,GAAG,kBAAkB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAGxD,kEAAkE;AAClE,MAAM,MAAM,UAAU,GAAG,CACxB,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,OAAO,KACZ,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;CACrB,CAAC;AA6BF;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACzB,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,GACf,WAAW,CA6Bb"}
@@ -0,0 +1,15 @@
1
+ import { type GatewayTrust, type GatewayTrustOptions } from '@nxgt/security/gateway';
2
+ import type { TokenPrincipal } from '@nxgt/shared';
3
+ export { GATEWAY_SECRET_HEADER, type GatewaySecretOptions, type GatewayTrust, type GatewayTrustOptions, gatewaySecret, type HeaderReader, } from '@nxgt/security/gateway';
4
+ /**
5
+ * The `trustedGateway` a plugin was given, or a `TypeError` naming the plugin:
6
+ * without one there is no caller it could read, and a plugin that silently
7
+ * reads none is a server whose every caller is anonymous.
8
+ */
9
+ export declare function requireGatewayTrust(options: Partial<GatewayTrustOptions> | undefined, plugin: string): GatewayTrust;
10
+ /**
11
+ * The caller a trusted gateway put in `extensions`, when it is shaped like
12
+ * one: an object with a non-empty string `sub`. Anything else names nobody.
13
+ */
14
+ export declare function principalOf(value: unknown): TokenPrincipal | undefined;
15
+ //# sourceMappingURL=gateway-trust.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway-trust.d.ts","sourceRoot":"","sources":["../../src/plugins/gateway-trust.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,mBAAmB,EAExB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAInD,OAAO,EACN,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,aAAa,EACb,KAAK,YAAY,GACjB,MAAM,wBAAwB,CAAC;AAEhC;;;;GAIG;AACH,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,OAAO,CAAC,mBAAmB,CAAC,GAAG,SAAS,EACjD,MAAM,EAAE,MAAM,GACZ,YAAY,CAMd;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,GAAG,SAAS,CAMtE"}
@@ -1,6 +1,9 @@
1
+ export * from './apply-authenticated';
1
2
  export * from './apply-keto-checks';
2
3
  export * from './auth';
4
+ export * from './authenticated';
3
5
  export * from './extract-jwt';
6
+ export * from './gateway-trust';
4
7
  export * from './keto-checker';
5
8
  export * from './keto-checks';
6
9
  export * from './keto-helpers';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,QAAQ,CAAC;AACvB,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
@@ -14,8 +14,9 @@ export type KetoChecksOptions = ReadOptions;
14
14
  * Goes after `useOryAuth(ory)`, which is what puts `ory.subject` there.
15
15
  *
16
16
  * `replaceSchema` inside `onSchemaChange` re-enters this hook with the new
17
- * schema, so transformed schemas are remembered and passed through. Without
18
- * the guard this loops until the stack gives out.
17
+ * schema, and with every schema another plugin makes of it. `applyKetoChecks`
18
+ * returns a schema with no field left to guard as it is, so the replacement settles
19
+ * instead of looping, alongside `useAuthenticated` whatever their order.
19
20
  *
20
21
  * `OryUnavailable` is deliberately not caught anywhere here: a Keto outage is
21
22
  * a 503 through `createMaskError`, never a denial.
@@ -1 +1 @@
1
- {"version":3,"file":"keto-checks.d.ts","sourceRoot":"","sources":["../../src/plugins/keto-checks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAEzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAC;AAE5C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC5B,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,iBAAsB,GAC7B,MAAM,CAAC,kBAAkB,GAAG,UAAU,GAAG,iBAAiB,CAAC,CAc7D"}
1
+ {"version":3,"file":"keto-checks.d.ts","sourceRoot":"","sources":["../../src/plugins/keto-checks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAC;AAE5C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC5B,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,iBAAsB,GAC7B,MAAM,CAAC,kBAAkB,GAAG,UAAU,GAAG,iBAAiB,CAAC,CAU7D"}
@@ -19,7 +19,7 @@ export type CanQuestion = {
19
19
  id: string;
20
20
  };
21
21
  /**
22
- * The caller, or an `UNAUTHENTICATED` refusal (401 through `createMaskError`).
22
+ * The caller, or an `UNAUTHENTICATED` refusal: a `GraphQLError` answered 401.
23
23
  * An absent caller is not an error to swallow in a resolver that needs one.
24
24
  */
25
25
  export declare function requireUser(context: {
@@ -1 +1 @@
1
- {"version":3,"file":"keto-helpers.d.ts","sourceRoot":"","sources":["../../src/plugins/keto-helpers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,GACjD,UAAU,GACV,iBAAiB,CAAC;AAEnB,yDAAyD;AACzD,MAAM,MAAM,WAAW,GAAG;IACzB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,EAAE,EAAE,MAAM,CAAC;CACX,CAAC;AAEF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE;IACpC,IAAI,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;CAC7B,GAAG,cAAc,CAOjB;AAED;;;;;;;;GAQG;AACH,wBAAsB,GAAG,CACxB,OAAO,EAAE,UAAU,GAAG,iBAAiB,EACvC,QAAQ,EAAE,WAAW,GACnB,OAAO,CAAC,OAAO,CAAC,CAkBlB"}
1
+ {"version":3,"file":"keto-helpers.d.ts","sourceRoot":"","sources":["../../src/plugins/keto-helpers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,GACjD,UAAU,GACV,iBAAiB,CAAC;AAEnB,yDAAyD;AACzD,MAAM,MAAM,WAAW,GAAG;IACzB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,EAAE,EAAE,MAAM,CAAC;CACX,CAAC;AAEF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE;IACpC,IAAI,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;CAC7B,GAAG,cAAc,CAKjB;AAED;;;;;;;;GAQG;AACH,wBAAsB,GAAG,CACxB,OAAO,EAAE,UAAU,GAAG,iBAAiB,EACvC,QAAQ,EAAE,WAAW,GACnB,OAAO,CAAC,OAAO,CAAC,CAgBlB"}
@@ -60,9 +60,9 @@ export declare function resolveOryPrincipal(ory: Ory, headers: Headers): Promise
60
60
  /**
61
61
  * `useAuth()` for an Ory-native API: resolves the caller through Kratos
62
62
  * (session cookie, session token) or Hydra (Bearer, introspected) and puts
63
- * `user`, `claims`, `token` and `ory` on the context. Wire it exactly where the
64
- * standalone APIs wire `useAuth()`, ahead of `useGenericAuth` — which then
65
- * enforces `@authenticated` from `context.user` unchanged.
63
+ * `user`, `claims`, `token` and `ory` on the context. Wire it ahead of
64
+ * `useAuthenticated()`, which enforces `@authenticated` from `context.user`
65
+ * and `context.ory.kind`.
66
66
  *
67
67
  * The `Ory` instance comes from the app's `createOry()` — one per process —
68
68
  * and is the same one the services use for `isAllowed`.
package/dist/shared.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const SHARED_TYPE_DEFS = "\ntype Query {\n\t_empty: String\n}\ntype Mutation {\n\t_empty: String\n}\ntype Subscription {\n\t_empty: String\n}\n\ndirective @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM\n\ndirective @policy(policies: [[String!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM\n\ndirective @shareable on OBJECT | FIELD_DEFINITION\n\ndirective @link(url: String!, import: [String!]) on SCHEMA\n\n";
1
+ export declare const SHARED_TYPE_DEFS = "\ntype Query {\n\t_empty: String\n}\ntype Mutation {\n\t_empty: String\n}\ntype Subscription {\n\t_empty: String\n}\n\n\"\"\"\nA signed-in caller \u2014 of one of the types `type` names, when it names some.\n\"\"\"\ndirective @authenticated(\n\t\"\"\"\n\tThe kinds of caller admitted: \"session\" or \"token\" by default. Any caller\n\twhen omitted.\n\t\"\"\"\n\ttype: [String!]\n) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM\n\ndirective @policy(policies: [[String!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM\n\ndirective @shareable on OBJECT | FIELD_DEFINITION\n\ndirective @link(url: String!, import: [String!]) on SCHEMA\n\n";
2
2
  //# sourceMappingURL=shared.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,2aAmB5B,CAAC"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB,ypBAkB5B,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { ErrorCode } from '@nxgt/shared-exceptions';
2
+ import { GraphQLError } from 'graphql';
3
+ /** The refusals a guard answers, each with its HTTP status. */
4
+ export type DenialCode = ErrorCode.Unauthenticated | ErrorCode.Forbidden | ErrorCode.NotFound;
5
+ /**
6
+ * A refusal, as the `GraphQLError` a client reads: `extensions.code` and
7
+ * `extensions.http.status` — `UNAUTHENTICATED` 401, `FORBIDDEN` 403,
8
+ * `NOT_FOUND` 404 — so any server answers it with its status, with or without
9
+ * `createMaskError`. What `@permission`, `@authenticated`, `requireUser` and
10
+ * `can` throw.
11
+ *
12
+ * `message` is an i18n key (the shared `errors.*` one by default). It is
13
+ * translated here with `@nxgt/i18n`'s resources, in the request's language
14
+ * when one is known; `createMaskError(translate)` and
15
+ * `createFormatError(translate)` translate it again with yours.
16
+ */
17
+ export declare function denial(code: DenialCode, message?: string): GraphQLError;
18
+ /** The i18n key of a `denial()`, or `undefined` for any other error. */
19
+ export declare function denialMessageKey(error: unknown): string | undefined;
20
+ //# sourceMappingURL=denial.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"denial.d.ts","sourceRoot":"","sources":["../../../src/utils/errors/denial.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,+DAA+D;AAC/D,MAAM,MAAM,UAAU,GACnB,SAAS,CAAC,eAAe,GACzB,SAAS,CAAC,SAAS,GACnB,SAAS,CAAC,QAAQ,CAAC;AAsBtB;;;;;;;;;;;GAWG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,YAAY,CAOvE;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAInE"}
@@ -5,7 +5,8 @@ import type { GraphQLBaseContext } from '../../types';
5
5
  * Apollo Server's `formatError`, taught this repo's exceptions — the Apollo
6
6
  * counterpart of `createMaskError`. `OryUnavailable` answers
7
7
  * `SERVICE_UNAVAILABLE`, never a denial and never an opaque
8
- * `INTERNAL_SERVER_ERROR`.
8
+ * `INTERNAL_SERVER_ERROR`; a `denial()` keeps its code, and its message is
9
+ * translated with `translate`.
9
10
  */
10
11
  export declare function createFormatError<K extends LocaleKey, C extends GraphQLBaseContext = GraphQLBaseContext>(translate?: (message: K, context?: TranslationContext) => string, production?: boolean): ApolloServerOptions<C>['formatError'];
11
12
  //# sourceMappingURL=format-error.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"format-error.d.ts","sourceRoot":"","sources":["../../../src/utils/errors/format-error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAK1D,OAAO,EACN,KAAK,SAAS,EACd,KAAK,kBAAkB,EAEvB,MAAM,YAAY,CAAC;AAIpB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGtD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAChC,CAAC,SAAS,SAAS,EACnB,CAAC,SAAS,kBAAkB,GAAG,kBAAkB,EAEjD,SAAS,GAAE,CACV,OAAO,EAAE,CAAC,EACV,OAAO,CAAC,EAAE,kBAAkB,KACxB,MAAsB,EAC3B,UAAU,CAAC,EAAE,OAAO,GAClB,mBAAmB,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CA4DvC"}
1
+ {"version":3,"file":"format-error.d.ts","sourceRoot":"","sources":["../../../src/utils/errors/format-error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAK1D,OAAO,EACN,KAAK,SAAS,EACd,KAAK,kBAAkB,EAEvB,MAAM,YAAY,CAAC;AAIpB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAItD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAChC,CAAC,SAAS,SAAS,EACnB,CAAC,SAAS,kBAAkB,GAAG,kBAAkB,EAEjD,SAAS,GAAE,CACV,OAAO,EAAE,CAAC,EACV,OAAO,CAAC,EAAE,kBAAkB,KACxB,MAAsB,EAC3B,UAAU,CAAC,EAAE,OAAO,GAClB,mBAAmB,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAgEvC"}
@@ -1,4 +1,5 @@
1
1
  export * from './create-error';
2
+ export * from './denial';
2
3
  export * from './format-error';
3
4
  export * from './mask-error';
4
5
  export * from './ory-unavailable';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/errors/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/errors/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC"}
@@ -12,7 +12,8 @@ type Translate<K extends LocaleKey> = (message: K, context?: TranslationContext)
12
12
  * (`UNAUTHENTICATED` 401, `FORBIDDEN` 403, `NOT_FOUND` 404); a Mongoose error
13
13
  * goes through `castError` first, as `createFormatError` does for Apollo.
14
14
  * `OryUnavailable` (@nxgt/ory-sdk) becomes a 503 `SERVICE_UNAVAILABLE`, never
15
- * a denial. Everything else is masked as Yoga's default masks it: a
15
+ * a denial. A `denial()` — what the directives, `requireUser` and `can` throw
16
+ * — keeps its code and status, and its message is translated with `translate`. Everything else is masked as Yoga's default masks it: a
16
17
  * `GraphQLError` thrown on purpose — or raised by validation — passes, and one
17
18
  * that only wraps a plain `Error` a resolver threw is replaced by `message`,
18
19
  * so an internal message never reaches the client.
@@ -1 +1 @@
1
- {"version":3,"file":"mask-error.d.ts","sourceRoot":"","sources":["../../../src/utils/errors/mask-error.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,SAAS,EACd,KAAK,kBAAkB,EAEvB,MAAM,YAAY,CAAC;AAIpB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C,KAAK,SAAS,CAAC,CAAC,SAAS,SAAS,IAAI,CACrC,OAAO,EAAE,CAAC,EACV,OAAO,CAAC,EAAE,kBAAkB,KACxB,MAAM,CAAC;AAEZ;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,SAAS,EAClD,SAAS,GAAE,SAAS,CAAC,CAAC,CAAiB,GACrC,SAAS,CA2BX"}
1
+ {"version":3,"file":"mask-error.d.ts","sourceRoot":"","sources":["../../../src/utils/errors/mask-error.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,SAAS,EACd,KAAK,kBAAkB,EAEvB,MAAM,YAAY,CAAC;AAIpB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAI9C,KAAK,SAAS,CAAC,CAAC,SAAS,SAAS,IAAI,CACrC,OAAO,EAAE,CAAC,EACV,OAAO,CAAC,EAAE,kBAAkB,KACxB,MAAM,CAAC;AAEZ;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,SAAS,EAClD,SAAS,GAAE,SAAS,CAAC,CAAC,CAAiB,GACrC,SAAS,CAmCX"}
@@ -8,7 +8,7 @@ export interface ResolvedWsUser {
8
8
  }
9
9
  /**
10
10
  * Resolves the authenticated TokenPrincipal for a `graphql-ws` connection - the
11
- * WS-transport equivalent of the HTTP-path `useAuth()`/`useGenericAuth()`
11
+ * WS-transport equivalent of the HTTP-path `useOryAuth()` / `useAuth()`
12
12
  * plugins. There is no gateway hop for WS connections, so each app performs
13
13
  * the OAuth introspection call itself using its own `auth` client.
14
14
  *
package/docs/README.md CHANGED
@@ -3,9 +3,11 @@
3
3
  The package's [README](../README.md) is the short version, with one example
4
4
  per section. These pages are the long one.
5
5
 
6
- - [Guide: permissions](./guide/permissions.md) — `@permission`, `@check`,
7
- `useKetoChecks`, build-time validation, `requireUser` and `can`
8
- - [Guide: errors](./guide/errors.md) — `createMaskError`, `createFormatError`,
9
- the codes and statuses, and why an outage is a 503
10
- - [Troubleshooting](./troubleshooting.md) — each error by the message you see
11
- - [Roadmap](./roadmap.md) — what is next, including the planned major
6
+ | Page | Read it when |
7
+ | --- | --- |
8
+ | [Migrating to 3.0](./guide/migrating-to-3.md) | you upgrade from 2.x: every break, with the code before and after |
9
+ | [Authentication](./guide/authentication.md) | you wire `useOryAuth`, `useAuth` behind a trusted gateway, or `@authenticated(type:)` |
10
+ | [Permissions](./guide/permissions.md) | you guard a field with `@permission`, or call `requireUser` / `can` |
11
+ | [Errors](./guide/errors.md) | you decide what a client receives: denials, `createMaskError`, `createFormatError`, outages |
12
+ | [Troubleshooting](./troubleshooting.md) | you have an error message in hand |
13
+ | [Roadmap](./roadmap.md) | you want to know what is next, and what shipped in 3.0 |
@@ -0,0 +1,187 @@
1
+ # Authentication: who is calling, and `@authenticated`
2
+
3
+ A resolver reads the caller from `context.user`. This page is where that value
4
+ comes from, which sources are trusted, and how `@authenticated` refuses a
5
+ missing caller or one of the wrong kind.
6
+
7
+ ## The rule: a verified source, never the body alone
8
+
9
+ The GraphQL request's `extensions` travel in the request body, which any
10
+ client writes. So the caller comes from one of two places:
11
+
12
+ | Source | Plugin | Verified by |
13
+ | --- | --- | --- |
14
+ | a Kratos session or a Hydra access token | `useOryAuth(ory)` | Ory, server-side, on every request |
15
+ | what a gateway resolved and put in `extensions` | `useAuth()` / `extractJwtPlugin()` | the gateway's proof — `trustedGateway` — on every request |
16
+
17
+ Anything else leaves the context without a caller.
18
+
19
+ ## `useOryAuth(ory)` — an API that authenticates its own callers
20
+
21
+ ```ts
22
+ import { createOry } from '@nxgt/ory-sdk';
23
+ import { useAuthenticated, useOryAuth } from '@nxgt/shared-graphql';
24
+
25
+ const ory = createOry({ /* … */ });
26
+
27
+ createYoga({ plugins: [useOryAuth(ory), useAuthenticated()] });
28
+ ```
29
+
30
+ It reads `Authorization: Bearer`, then `X-Session-Token`, then the Kratos
31
+ cookie, and puts `user`, `claims`, `token` and `ory` on the context. An Ory
32
+ outage throws a 503 `SERVICE_UNAVAILABLE`, never an anonymous caller.
33
+
34
+ ## `useAuth()` and `extractJwtPlugin()` — behind a gateway
35
+
36
+ A gateway that authenticated the caller can forward it to subgraphs in the
37
+ request's `extensions`: `user` and `token` for `useAuth()` (Yoga),
38
+ `payload` for `extractJwtPlugin()` (Apollo Server). Both read it **only** when
39
+ `trustedGateway` holds for the request:
40
+
41
+ ```ts
42
+ import { extractJwtPlugin, gatewaySecret, useAuth } from '@nxgt/shared-graphql';
43
+
44
+ const trustedGateway = gatewaySecret({ secret: process.env.GATEWAY_SECRET! });
45
+
46
+ createYoga({ plugins: [useAuth({ trustedGateway })] });
47
+ new ApolloServer({ plugins: [extractJwtPlugin({ trustedGateway })] });
48
+ ```
49
+
50
+ ### `gatewaySecret({ secret, header? })`
51
+
52
+ Holds for a request whose `header` — `x-gateway-secret` by default — carries
53
+ `secret`, compared in constant time. The gateway adds it to every subgraph
54
+ request; a client that reaches the service directly does not know it, and its
55
+ `extensions` are ignored.
56
+
57
+ | Option | Default | Meaning |
58
+ | --- | --- | --- |
59
+ | `secret` | — | shared with the gateway only; **at least 16 characters**, or `gatewaySecret` throws when it is built |
60
+ | `header` | `x-gateway-secret` | the header the gateway sends it in |
61
+
62
+ The length check is there for the day `GATEWAY_SECRET` is unset: the server
63
+ stops at boot instead of trusting an empty header.
64
+
65
+ ### Another proof
66
+
67
+ `trustedGateway` is any function of the request's headers:
68
+
69
+ ```ts
70
+ type GatewayTrust = (headers: { get(name: string): string | null | undefined }) => boolean | Promise<boolean>;
71
+
72
+ // Your proxy terminates mTLS and sets this header only for the gateway's certificate.
73
+ useAuth({ trustedGateway: (headers) => headers.get('x-client-cert-subject') === 'CN=gateway' });
74
+ ```
75
+
76
+ It receives a fetch `Headers` under Yoga and Apollo's `HeaderMap` under
77
+ Apollo Server; both answer `get`.
78
+
79
+ ### What is read
80
+
81
+ - `useAuth()`: `extensions.user` when it is an object with a non-empty string
82
+ `sub`, and `extensions.token` when it is a string. Otherwise nothing.
83
+ - `extractJwtPlugin()`: `extensions.payload`, under the same `sub` rule, on
84
+ `context.jwt.payload`.
85
+ - A request that fails the proof leaves `user`, `token` and `jwt` as they
86
+ were — another plugin's caller is not erased.
87
+ - Over WebSocket, `useAuth()` reads no caller: the proof needs a fetch
88
+ `Request`, which a graphql-ws context does not hold. Resolve the caller per
89
+ connection with `resolveWsUser` instead.
90
+
91
+ Without a `trustedGateway`, both throw a `TypeError` when called — see
92
+ [troubleshooting](../troubleshooting.md).
93
+
94
+ ## `@authenticated(type: [String!])`
95
+
96
+ ```graphql
97
+ directive @authenticated(type: [String!]) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
98
+ ```
99
+
100
+ Declared in `SHARED_TYPE_DEFS` and in `AUTHENTICATED_DIRECTIVE_SDL`; enforced
101
+ by `useAuthenticated()`, or `applyAuthenticated(schema)` on a built schema.
102
+
103
+ ```graphql
104
+ type Query {
105
+ me: User @authenticated # any caller
106
+ webhooks: [Webhook!]! @authenticated(type: ["token"]) # a machine client, not a session
107
+ }
108
+
109
+ interface Node @authenticated { id: ID! }
110
+ type Staff implements Node @authenticated(type: ["session"]) { id: ID!, name: String }
111
+ ```
112
+
113
+ | Caller | Answer |
114
+ | --- | --- |
115
+ | none (`context.user` unset) | `UNAUTHENTICATED`, HTTP 401 |
116
+ | of a type no applicable `type:` names | `FORBIDDEN`, HTTP 403 |
117
+ | otherwise | the resolver runs |
118
+
119
+ - **The caller's type** is `context.ory.kind` — `session` for a Kratos
120
+ session, `token` for an access token — or `context.user.tokenType` when no
121
+ Ory principal is on the context.
122
+ - **Every directive that applies is AND-ed**: the field's, its type's, its
123
+ type's interfaces', and those interfaces' same field. `Staff.name` above
124
+ needs a caller (`Node`) who is a session (`Staff`).
125
+ - **A scalar's or an enum's directive guards every field returning it** —
126
+ `scalar Secret @authenticated` refuses `Query.secret: Secret` to an
127
+ anonymous caller, as federation's router reads it.
128
+ - **A subscription is refused before its stream opens**: the check runs
129
+ before the field's `subscribe`, and again before each event is resolved.
130
+ - **A type's directive guards its fields, not the field returning it.**
131
+ `Query.staff` runs for an anonymous caller; the error lands on
132
+ `staff.name`. Put `@authenticated` on the field too when the lookup must not
133
+ run.
134
+
135
+ ### Other caller types
136
+
137
+ ```ts
138
+ import { CALLER_TYPES, useAuthenticated } from '@nxgt/shared-graphql';
139
+
140
+ useAuthenticated({ types: [...CALLER_TYPES, 'service'] });
141
+ ```
142
+
143
+ `types` lists every value a `type:` may name; `CALLER_TYPES` —
144
+ `['session', 'token']` — is the default. Behind a gateway, it is the set
145
+ of `tokenType`s your gateway writes.
146
+
147
+ ### Refused at build
148
+
149
+ `applyAuthenticated` throws a `TypeError` for a directive no request could
150
+ pass, naming where it sits — `Query.me` on a field, `Staff` on a type,
151
+ `Secret` on a scalar:
152
+
153
+ | Mistake | Message starts |
154
+ | --- | --- |
155
+ | `type: []` | ``@authenticated on Query.me: `type: []` admits no caller`` |
156
+ | a type outside `types` | `@authenticated on Query.me: unknown type "staff" — known: session, token` |
157
+ | restrictions with no type in common | ``@authenticated on Note.body: the field's, its type's and its interfaces' `type`s have none in common`` |
158
+
159
+ ### Federation's `@authenticated`
160
+
161
+ Federation defines its own `@authenticated`, with **no argument**, and a
162
+ subgraph imports it:
163
+
164
+ ```graphql
165
+ extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@authenticated"])
166
+ ```
167
+
168
+ `useAuthenticated` accepts that shape: an `@authenticated` whose declaration
169
+ has no `type` means "any caller", and is enforced as such. So in a subgraph,
170
+ keep federation's declaration — `FEDERATION_DIRECTIVES` carries it unchanged
171
+ — and do not load `SHARED_TYPE_DEFS` beside it. `type:` is for a schema that
172
+ declares `@authenticated` through `SHARED_TYPE_DEFS` or
173
+ `AUTHENTICATED_DIRECTIVE_SDL`.
174
+
175
+ ### Beside `useKetoChecks`
176
+
177
+ Both plugins replace the schema. Each marks the fields it guarded and leaves a
178
+ marked field alone, so the two settle on one schema in either order — and a
179
+ schema merged from a guarded half and an unguarded one gets the second half
180
+ guarded when it is transformed again, as is a field whose resolver was
181
+ replaced since. On a field carrying both,
182
+ `@authenticated` is checked before `@permission` whatever the plugin order,
183
+ so a caller of the wrong type is `FORBIDDEN` before Keto is asked:
184
+
185
+ ```ts
186
+ plugins: [useOryAuth(ory), useAuthenticated(), useKetoChecks(ory)]
187
+ ```