@nxgt/shared-graphql 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -81
- package/dist/directives/authenticated.d.ts +42 -0
- package/dist/directives/authenticated.d.ts.map +1 -0
- package/dist/directives/federation.d.ts +6 -0
- package/dist/directives/federation.d.ts.map +1 -1
- package/dist/directives/index.d.ts +5 -1
- package/dist/directives/index.d.ts.map +1 -1
- package/dist/directives/paths.d.ts +20 -0
- package/dist/directives/paths.d.ts.map +1 -0
- package/dist/directives/permission.d.ts +39 -0
- package/dist/directives/permission.d.ts.map +1 -0
- package/dist/directives/removed-check.d.ts +15 -0
- package/dist/directives/removed-check.d.ts.map +1 -0
- package/dist/directives/scope.d.ts +19 -0
- package/dist/directives/scope.d.ts.map +1 -0
- package/dist/directives/sdl.d.ts +26 -0
- package/dist/directives/sdl.d.ts.map +1 -0
- package/dist/directives/validate.d.ts +23 -0
- package/dist/directives/validate.d.ts.map +1 -0
- package/dist/index.js +1048 -564
- package/dist/index.js.map +37 -20
- package/dist/integrations/hono.d.ts +2 -2
- package/dist/integrations/hono.d.ts.map +1 -1
- package/dist/plugins/apply-authenticated.d.ts +21 -0
- package/dist/plugins/apply-authenticated.d.ts.map +1 -0
- package/dist/plugins/apply-keto-checks.d.ts +20 -0
- package/dist/plugins/apply-keto-checks.d.ts.map +1 -0
- package/dist/plugins/auth.d.ts +17 -1
- package/dist/plugins/auth.d.ts.map +1 -1
- package/dist/plugins/authenticated.d.ts +14 -0
- package/dist/plugins/authenticated.d.ts.map +1 -0
- package/dist/plugins/extract-jwt.d.ts +19 -6
- package/dist/plugins/extract-jwt.d.ts.map +1 -1
- package/dist/plugins/field-guard.d.ts +30 -0
- package/dist/plugins/field-guard.d.ts.map +1 -0
- package/dist/plugins/gateway-trust.d.ts +47 -0
- package/dist/plugins/gateway-trust.d.ts.map +1 -0
- package/dist/plugins/index.d.ts +6 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/keto-checker.d.ts +24 -0
- package/dist/plugins/keto-checker.d.ts.map +1 -0
- package/dist/plugins/keto-checks.d.ts +11 -34
- package/dist/plugins/keto-checks.d.ts.map +1 -1
- package/dist/plugins/keto-helpers.d.ts +38 -0
- package/dist/plugins/keto-helpers.d.ts.map +1 -0
- package/dist/plugins/ory-auth.d.ts +5 -5
- package/dist/plugins/ory-auth.d.ts.map +1 -1
- package/dist/scalars/custom/utils.d.ts.map +1 -1
- package/dist/shared.d.ts +1 -1
- package/dist/shared.d.ts.map +1 -1
- package/dist/utils/errors/create-error.d.ts +3 -0
- package/dist/utils/errors/create-error.d.ts.map +1 -0
- package/dist/utils/errors/denial.d.ts +20 -0
- package/dist/utils/errors/denial.d.ts.map +1 -0
- package/dist/utils/errors/format-error.d.ts +12 -0
- package/dist/utils/errors/format-error.d.ts.map +1 -0
- package/dist/utils/errors/index.d.ts +6 -0
- package/dist/utils/errors/index.d.ts.map +1 -0
- package/dist/utils/errors/mask-error.d.ts +25 -0
- package/dist/utils/errors/mask-error.d.ts.map +1 -0
- package/dist/utils/errors/ory-unavailable.d.ts +19 -0
- package/dist/utils/errors/ory-unavailable.d.ts.map +1 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/ws-context.d.ts +1 -1
- package/docs/README.md +13 -0
- package/docs/guide/authentication.md +187 -0
- package/docs/guide/errors.md +136 -0
- package/docs/guide/migrating-to-3.md +276 -0
- package/docs/guide/permissions.md +220 -0
- package/docs/roadmap.md +71 -0
- package/docs/troubleshooting.md +273 -0
- package/graphql/directives/permission.graphqls +73 -0
- package/package.json +11 -13
- package/dist/directives/check.d.ts +0 -40
- package/dist/directives/check.d.ts.map +0 -1
- package/dist/utils/errors.utils.d.ts +0 -23
- package/dist/utils/errors.utils.d.ts.map +0 -1
- package/graphql/directives/check.graphqls +0 -86
package/README.md
CHANGED
|
@@ -4,15 +4,26 @@ The GraphQL layer: Yoga + Hono wiring, the federation subgraph builder, shared
|
|
|
4
4
|
scalars and directives, dataloaders, subscriptions over Redis, upload handling,
|
|
5
5
|
and the SDL every service merges into its own schema.
|
|
6
6
|
|
|
7
|
+
**Upgrading from 2.x?** Read [Migrating to 3.0](./docs/guide/migrating-to-3.md)
|
|
8
|
+
— the caller is no longer read from a request body a client writes, `@check`
|
|
9
|
+
is gone, and denials are `GraphQLError`s.
|
|
10
|
+
|
|
7
11
|
## Install
|
|
8
12
|
|
|
9
13
|
```bash
|
|
10
14
|
bun add @nxgt/shared-graphql
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
Public on npmjs; no token needed to install.
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
Public on npmjs; no token needed to install. Peers:
|
|
18
|
+
|
|
19
|
+
| Peer | Range | Why |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| `graphql` | `^16.9.0 \|\| ^17.0.0` | one copy for your schema and this package's transforms; the suite runs on both majors |
|
|
22
|
+
| `@nxgt/ory-sdk` | `>=0.1.0 <1` | `useOryAuth`, `useKetoChecks`, `can`; a 1.x is admitted once it is tested |
|
|
23
|
+
| `stx-sdk` | `>=1.1.0` | `./security`'s policy types |
|
|
24
|
+
| `typescript` | `^6.0.3` | pinned across every `@nxgt/*` package — the set is unsatisfiable if one of them widens it |
|
|
25
|
+
|
|
26
|
+
The long version of each section below is in [`docs/`](./docs/README.md).
|
|
16
27
|
|
|
17
28
|
## Subpaths
|
|
18
29
|
|
|
@@ -31,9 +42,13 @@ walking up to the nearest `package.json` — the bundle is `dist/index.js`, the
|
|
|
31
42
|
source is `src/utils/schema.utils.ts`, and no single relative path serves both.
|
|
32
43
|
|
|
33
44
|
```ts
|
|
34
|
-
import {
|
|
45
|
+
import { fileURLToPath } from 'node:url';
|
|
46
|
+
import { loadTypeDefs, SHARED_SCHEMA_PATH } from '@nxgt/shared-graphql';
|
|
35
47
|
|
|
36
|
-
const typeDefs = loadTypeDefs(
|
|
48
|
+
const typeDefs = loadTypeDefs(
|
|
49
|
+
SHARED_SCHEMA_PATH,
|
|
50
|
+
fileURLToPath(new URL('../**/*.graphqls', import.meta.url)),
|
|
51
|
+
);
|
|
37
52
|
```
|
|
38
53
|
|
|
39
54
|
```ts
|
|
@@ -46,116 +61,201 @@ schema: ['./src/**/*.graphqls', SHARED_SCHEMA_PATH],
|
|
|
46
61
|
A relative path into this package's `src/` will not work from an install — it
|
|
47
62
|
is not published, and it was not there in the first place.
|
|
48
63
|
|
|
49
|
-
`SHARED_TYPE_DEFS` is a string of the shared directives
|
|
50
|
-
`@policy`, `@shareable`, `@link`) plus empty root
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
`SHARED_TYPE_DEFS` is a string of the shared directives
|
|
65
|
+
(`@authenticated(type:)`, `@policy`, `@shareable`, `@link`) plus empty root
|
|
66
|
+
types. `@permission` is not in it: it lives in `graphql/directives/`, so a
|
|
67
|
+
subgraph that builds through `buildSubgraphSchema` and never loads
|
|
68
|
+
`SHARED_TYPE_DEFS` still sees it through `SHARED_SCHEMA_PATH`. For a schema
|
|
69
|
+
assembled in code, the declarations ship as strings —
|
|
70
|
+
`PERMISSION_DIRECTIVE_SDL` (also exported as `KETO_DIRECTIVES_SDL`), held
|
|
71
|
+
equal to its file by a spec, and `AUTHENTICATED_DIRECTIVE_SDL`, which has no
|
|
72
|
+
file: it lives in `SHARED_TYPE_DEFS` and in this string only.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { AUTHENTICATED_DIRECTIVE_SDL, PERMISSION_DIRECTIVE_SDL } from '@nxgt/shared-graphql';
|
|
76
|
+
|
|
77
|
+
createSchema({ typeDefs: [AUTHENTICATED_DIRECTIVE_SDL, PERMISSION_DIRECTIVE_SDL, typeDefs], resolvers });
|
|
78
|
+
```
|
|
55
79
|
|
|
56
80
|
`buildSubgraphSchema` wraps Apollo's builder and prunes unused types.
|
|
57
81
|
|
|
58
|
-
##
|
|
82
|
+
## Who is calling
|
|
83
|
+
|
|
84
|
+
The caller comes from a source the server verified — never from the request
|
|
85
|
+
body alone.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// An API that authenticates its own callers: Kratos session, Hydra token.
|
|
89
|
+
createYoga({ plugins: [useOryAuth(ory), useAuthenticated()] });
|
|
90
|
+
|
|
91
|
+
// A subgraph behind a gateway that resolved the caller and put it in `extensions`.
|
|
92
|
+
const trustedGateway = gatewaySecret({ secret: process.env.GATEWAY_SECRET! });
|
|
93
|
+
createYoga({ plugins: [useAuth({ trustedGateway }), useAuthenticated()] });
|
|
94
|
+
new ApolloServer({ plugins: [extractJwtPlugin({ trustedGateway })] });
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- `useOryAuth(ory)` resolves the caller through Kratos or Hydra, and sets
|
|
98
|
+
`user`, `claims`, `token` and `ory`. An Ory outage is a 503, never an
|
|
99
|
+
anonymous caller.
|
|
100
|
+
- `useAuth()` and `extractJwtPlugin()` read the caller a gateway put in the
|
|
101
|
+
GraphQL request's `extensions` — **only** for a request `trustedGateway`
|
|
102
|
+
vouches for. `gatewaySecret({ secret, header? })` holds for a request whose
|
|
103
|
+
`x-gateway-secret` header carries the secret (compared in constant time,
|
|
104
|
+
16 characters at least). Anything else leaves the context without a caller.
|
|
105
|
+
Without a `trustedGateway`, both throw when the server starts.
|
|
106
|
+
|
|
107
|
+
### `@authenticated(type: [String!])`
|
|
108
|
+
|
|
109
|
+
```graphql
|
|
110
|
+
me: User @authenticated
|
|
111
|
+
webhooks: [Webhook!]! @authenticated(type: ["token"])
|
|
112
|
+
type Staff @authenticated(type: ["session"]) { … }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`useAuthenticated(options?)` enforces it: no caller is `UNAUTHENTICATED`
|
|
116
|
+
(401), a caller of a type `type:` does not name is `FORBIDDEN` (403). The type
|
|
117
|
+
is Ory's `kind` — `session` or `token` — or `user.tokenType` without Ory;
|
|
118
|
+
`useAuthenticated({ types: [...CALLER_TYPES, 'service'] })` names other
|
|
119
|
+
values. The field's, its type's and its interfaces' directives are AND-ed; a
|
|
120
|
+
scalar's or an enum's guards every field returning it; a subscription is
|
|
121
|
+
refused before its stream opens. Refused at build, naming where the directive
|
|
122
|
+
sits: `type: []`, an unknown type, and restrictions with nothing in common.
|
|
123
|
+
|
|
124
|
+
Federation declares `@authenticated` with no argument, and a subgraph imports
|
|
125
|
+
that declaration. `useAuthenticated` reads that shape as "any caller", so keep
|
|
126
|
+
federation's declaration in a subgraph; `type:` is for the schemas this
|
|
127
|
+
package's `SHARED_TYPE_DEFS` builds. Details: [the authentication
|
|
128
|
+
guide](./docs/guide/authentication.md).
|
|
129
|
+
|
|
130
|
+
## `@permission` — the permission a field requires
|
|
59
131
|
|
|
60
132
|
`@authenticated` asks whether anyone is calling. `@policy` asks whether they
|
|
61
133
|
*carry* an authority. Neither can ask what an Ory-native API needs to know:
|
|
62
134
|
**may this caller `view` `Note:n1`** — a question about one object, answered by
|
|
63
|
-
Keto.
|
|
64
|
-
|
|
65
|
-
`@check` asks it. The declaration ships in `graphql/directives/check.graphqls`,
|
|
66
|
-
so any schema built from `SHARED_SCHEMA_PATH` already has it; `useKetoChecks(ory)`
|
|
67
|
-
is what answers it.
|
|
135
|
+
Keto. `@permission` asks it; `useKetoChecks(ory)` answers it.
|
|
68
136
|
|
|
69
137
|
```graphql
|
|
70
|
-
note(id: ID!): Note!
|
|
71
|
-
@check(permissions: [[{ namespace: "Note", permit: "view" }]])
|
|
138
|
+
note(id: ID!): Note! @permission(name: "view", type: "Note")
|
|
72
139
|
|
|
73
140
|
updateNote(id: ID!, input: UpdateNoteInput!): Note!
|
|
74
|
-
@
|
|
75
|
-
@
|
|
141
|
+
@permission(name: "view", type: "Note")
|
|
142
|
+
@permission(name: "edit", type: "Note", onDeny: FORBIDDEN)
|
|
143
|
+
|
|
144
|
+
owner: Person @permission(name: "view", type: "Person", id: "parent.ownerId")
|
|
76
145
|
```
|
|
77
146
|
|
|
78
147
|
```ts
|
|
79
|
-
plugins: [useOryAuth(ory), useKetoChecks(ory
|
|
148
|
+
plugins: [useOryAuth(ory), useKetoChecks(ory, { namespaces: ['Note', 'Person'] })]
|
|
80
149
|
```
|
|
81
150
|
|
|
82
|
-
`
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
151
|
+
- `name` is the permit, `type` the Keto namespace, `id` where the object id
|
|
152
|
+
is read: `args.<path>` (default `args.id`) or `parent.<path>`. A **list**
|
|
153
|
+
value requires the permit on every element.
|
|
154
|
+
- **Repeated, they are AND**, in declaration order, each with its own
|
|
155
|
+
`onDeny`: a stranger fails `view` and gets `NOT_FOUND` (the default), so an
|
|
156
|
+
id cannot be probed; a viewer passes it, fails `edit`, and gets `FORBIDDEN`.
|
|
157
|
+
- `message:` sets the i18n key a denial carries (shared `errors.not-found` /
|
|
158
|
+
`errors.insufficient-permissions` otherwise). Word it like the service
|
|
159
|
+
layer that guards the same object, or the wording tells a caller which
|
|
160
|
+
layer refused.
|
|
161
|
+
- An OR belongs in the Keto model — a permit that unions two relations — not
|
|
162
|
+
in the schema.
|
|
163
|
+
|
|
164
|
+
**Refused when the schema is built**, as a `TypeError` naming `Type.field`: a
|
|
165
|
+
path naming no root, an `args.<name>` the field does not declare, a namespace
|
|
166
|
+
outside `namespaces` (when you pass them), a guard on an interface field,
|
|
167
|
+
where no resolver runs, and a `@check` — removed in 3.0 — left in the schema.
|
|
168
|
+
|
|
169
|
+
`@permission` is read only when its declaration has `name` and `type` — a
|
|
170
|
+
schema with its own `@permission` of another shape keeps it. Loading this
|
|
171
|
+
package's `graphql/` next to such a declaration merges the two, though: see
|
|
172
|
+
[troubleshooting](./docs/troubleshooting.md).
|
|
173
|
+
|
|
174
|
+
**An outage is never a denial.** A Keto failure throws `OryUnavailable`, which
|
|
175
|
+
`createMaskError` answers `503 SERVICE_UNAVAILABLE`.
|
|
176
|
+
|
|
177
|
+
Every question goes through a per-request memo: distinct questions are
|
|
178
|
+
batched into one `POST /relation-tuples/batch/check`, identical ones asked
|
|
179
|
+
once, a failed batch not remembered.
|
|
180
|
+
|
|
181
|
+
### From a resolver: `requireUser` and `can`
|
|
104
182
|
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
183
|
+
```ts
|
|
184
|
+
import { can, denial, type OryGraphQLContext, requireUser } from '@nxgt/shared-graphql';
|
|
185
|
+
import { ErrorCode } from '@nxgt/shared-exceptions';
|
|
186
|
+
|
|
187
|
+
async function archive(_: unknown, { id }: { id: string }, ctx: OryGraphQLContext) {
|
|
188
|
+
const user = requireUser(ctx); // UNAUTHENTICATED (401) when nobody is calling
|
|
189
|
+
if (!(await can(ctx, { name: 'edit', type: 'Note', id }))) {
|
|
190
|
+
throw denial(ErrorCode.Forbidden, 'notes.errors.read-only');
|
|
191
|
+
}
|
|
192
|
+
return notes.archive(id, user.sub);
|
|
193
|
+
}
|
|
111
194
|
```
|
|
112
195
|
|
|
196
|
+
`can` answers Keto's `true` or `false` through the same memo as the
|
|
197
|
+
directives; an outage throws, never answers `false`.
|
|
198
|
+
|
|
113
199
|
### What it does not cover, on purpose
|
|
114
200
|
|
|
115
201
|
A field that answers a **list** the caller is entitled to. "Which notes may I
|
|
116
|
-
see" is not a check — it is a Keto query
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
202
|
+
see" is not a check — it is a Keto query folded into the database filter
|
|
203
|
+
before the read. A directive there would have to fetch everything and filter
|
|
204
|
+
after, which makes `totalCount` and the cursors lie.
|
|
205
|
+
|
|
206
|
+
### A shipped directive is not a composed directive
|
|
120
207
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
208
|
+
Shipping the SDL is enough for a standalone Yoga schema. It is **not** enough
|
|
209
|
+
for a subgraph that federation composes: that subgraph needs
|
|
210
|
+
`@composeDirective(name: "@permission")` and the directive in its own `@link`
|
|
211
|
+
import list. Without them the composition drops it silently — the supergraph
|
|
212
|
+
SDL comes out valid, the field loses its guard, and nothing fails.
|
|
126
213
|
|
|
127
|
-
|
|
128
|
-
an `id` naming neither root, and `permissions: [[]]` — a conjunction over no
|
|
129
|
-
terms is vacuously true, so it would admit everyone while looking guarded.
|
|
214
|
+
## Errors
|
|
130
215
|
|
|
131
|
-
A
|
|
132
|
-
`
|
|
216
|
+
A denial — from `@permission`, `@authenticated`, `requireUser` or `can`, or
|
|
217
|
+
your own `denial(code, message?)` — is a `GraphQLError` with
|
|
218
|
+
`extensions { code, http { status } }`: `UNAUTHENTICATED` 401, `FORBIDDEN` 403,
|
|
219
|
+
`NOT_FOUND` 404. Any Yoga or Apollo server answers it with that status. To
|
|
220
|
+
translate its message and map the rest, register:
|
|
133
221
|
|
|
134
|
-
|
|
222
|
+
```ts
|
|
223
|
+
createYoga({ maskedErrors: { maskError: createMaskError(translate) } });
|
|
224
|
+
new ApolloServer({ formatError: createFormatError(translate) });
|
|
225
|
+
```
|
|
135
226
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
227
|
+
Under Yoga, `createMaskError` translates a denial's key with your
|
|
228
|
+
`translate`; turns a `CustomException` into a `GraphQLError` with its code,
|
|
229
|
+
status and translated message; `OryUnavailable` into `SERVICE_UNAVAILABLE`
|
|
230
|
+
503, recognised even from a second copy of `@nxgt/ory-sdk`; and a plain
|
|
231
|
+
`Error` a resolver threw into the mask message, as Yoga's default does.
|
|
232
|
+
|
|
233
|
+
Under Apollo, `createFormatError` sets the `code` and the translated message
|
|
234
|
+
(and `SERVICE_UNAVAILABLE` for an outage, with `http.status` in `extensions`
|
|
235
|
+
only — `formatError` cannot change the transport status). It masks nothing
|
|
236
|
+
Apollo would not.
|
|
142
237
|
|
|
143
238
|
## Plugins and context
|
|
144
239
|
|
|
145
240
|
| Export | What it does |
|
|
146
241
|
| --- | --- |
|
|
147
|
-
| `useOryAuth(ory)` | Yoga plugin: resolve the caller, set `
|
|
148
|
-
| `
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
242
|
+
| `useOryAuth(ory)` | Yoga plugin: resolve the caller through Kratos or Hydra, set `user`, `claims`, `token`, `ory` |
|
|
243
|
+
| `useAuth({ trustedGateway })` | Yoga plugin: the caller a trusted gateway put in `extensions` — `user`, `token` |
|
|
244
|
+
| `extractJwtPlugin({ trustedGateway })` | Apollo plugin: the payload a trusted gateway put in `extensions.payload`, on `context.jwt` |
|
|
245
|
+
| `gatewaySecret({ secret, header? })` | the stock `trustedGateway`: a shared secret in a header |
|
|
246
|
+
| `useAuthenticated(options?)` | Yoga plugin: enforce `@authenticated(type:)`; `CALLER_TYPES` is the default `types` |
|
|
247
|
+
| `applyAuthenticated(schema, options?)` | the same transform on an already-built schema |
|
|
248
|
+
| `useKetoChecks(ory, options?)` | Yoga plugin: the per-request Keto memo, and the `@permission` transform |
|
|
249
|
+
| `applyKetoChecks(schema, options?)` | the same transform on an already-built schema |
|
|
250
|
+
| `requireUser(ctx)`, `can(ctx, question)` | the caller or 401; Keto's answer through the memo |
|
|
251
|
+
| `denial(code, message?)` | a refusal as a `GraphQLError` with its status |
|
|
152
252
|
|
|
153
253
|
`GraphQLBaseContext.user` is `TokenPrincipal` — the caller as the access token
|
|
154
254
|
describes them (`sub`, `uid`, `scope`). It is not `Principal`, which is the
|
|
155
255
|
header-derived shape used by the REST services.
|
|
156
256
|
|
|
157
257
|
`createYogaHono` / `honoYoga` (from this package's integrations) mount Yoga on
|
|
158
|
-
Hono. `
|
|
258
|
+
Hono. `sandboxExplorer` serves Apollo Sandbox. Subscriptions go over Redis
|
|
159
259
|
(`graphql-subscriptions` is re-exported). `DataLoader` is re-exported so a
|
|
160
260
|
subgraph does not take a second copy.
|
|
161
261
|
|
|
@@ -164,11 +264,26 @@ SDL.
|
|
|
164
264
|
|
|
165
265
|
## Things that bite
|
|
166
266
|
|
|
167
|
-
- **`@
|
|
267
|
+
- **`@permission` on a list field is the wrong tool.** Filter before the read.
|
|
268
|
+
- **`useAuth()` without the gateway's header reads no caller.** A gateway
|
|
269
|
+
that does not send `x-gateway-secret` on every subgraph request makes every
|
|
270
|
+
caller anonymous. Send it from the gateway, not from clients.
|
|
271
|
+
- **An object type's `@authenticated` guards its fields, not the field that
|
|
272
|
+
returns it** (a scalar's or an enum's does guard the fields returning it).
|
|
273
|
+
Put the directive on the field too when the lookup itself must not run for
|
|
274
|
+
an anonymous caller.
|
|
168
275
|
- **Do not import `graphql-subscriptions` from `graphql-subscriptions`.** Take
|
|
169
276
|
it from this package, same reason mongoose comes from `@nxgt/shared-mongo`.
|
|
170
|
-
- **The sandbox helper is spelled `sandboxExpolorer`.** That is the export
|
|
171
|
-
name. A corrected spelling is a breaking change, not a typo fix in the
|
|
172
|
-
consumer.
|
|
173
277
|
- **`stx-sdk` is required.** Unlike `@nxgt/security`, this package does not
|
|
174
278
|
mark it optional.
|
|
279
|
+
|
|
280
|
+
## Documentation
|
|
281
|
+
|
|
282
|
+
| Page | Read it when |
|
|
283
|
+
| --- | --- |
|
|
284
|
+
| [Migrating to 3.0](./docs/guide/migrating-to-3.md) | you upgrade from 2.x |
|
|
285
|
+
| [Authentication](./docs/guide/authentication.md) | you wire `useOryAuth`, `useAuth` behind a gateway, or `@authenticated` |
|
|
286
|
+
| [Permissions](./docs/guide/permissions.md) | you guard a field with `@permission`, or ask Keto from a resolver |
|
|
287
|
+
| [Errors](./docs/guide/errors.md) | you decide what a client receives, or write your own mask |
|
|
288
|
+
| [Troubleshooting](./docs/troubleshooting.md) | you have an error message in hand |
|
|
289
|
+
| [Roadmap](./docs/roadmap.md) | you want to know what is next |
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { GraphQLSchema } from 'graphql';
|
|
2
|
+
import type { FieldNode } from './scope';
|
|
3
|
+
export declare const AUTHENTICATED_DIRECTIVE_NAME = "authenticated";
|
|
4
|
+
/**
|
|
5
|
+
* The kinds of caller `@authenticated(type:)` can name by default: Ory's
|
|
6
|
+
* `OryPrincipal.kind` — a Kratos `session`, or an OAuth2 access `token`
|
|
7
|
+
* (introspected by Hydra, whether a person's or a `client_credentials` one).
|
|
8
|
+
*/
|
|
9
|
+
export declare const CALLER_TYPES: readonly ["session", "token"];
|
|
10
|
+
export type AuthenticatedOptions = {
|
|
11
|
+
/**
|
|
12
|
+
* Every value a `type:` may name, when the caller's type is not Ory's
|
|
13
|
+
* `kind` — `TokenPrincipal.tokenType` behind a gateway, say. A `type`
|
|
14
|
+
* outside it is refused at build. `CALLER_TYPES` by default.
|
|
15
|
+
*/
|
|
16
|
+
types?: readonly string[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* What the `@authenticated`s that apply to one field demand together: a
|
|
20
|
+
* caller, and — when `types` is set — one of those types. Every directive
|
|
21
|
+
* that applies must hold, so `types` is their intersection.
|
|
22
|
+
*/
|
|
23
|
+
export type CallerRequirement = {
|
|
24
|
+
types?: readonly string[];
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The `@authenticated` on a field, a type or an interface, read and
|
|
28
|
+
* validated: `null` when there is none, `{}` for any caller, `{ types }` for
|
|
29
|
+
* a caller of one of them.
|
|
30
|
+
*
|
|
31
|
+
* Federation declares `@authenticated` with no argument. A schema that
|
|
32
|
+
* imports that declaration has no `type` to read, and every `@authenticated`
|
|
33
|
+
* in it means "any caller" — the shape is accepted as it is.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readAuthenticated(schema: GraphQLSchema, node: FieldNode, where: string, known: readonly string[]): CallerRequirement | null;
|
|
36
|
+
/**
|
|
37
|
+
* Every requirement that applies to one field, AND-ed: a caller when any
|
|
38
|
+
* applies, and the types all of them admit. Refused at build when they admit
|
|
39
|
+
* no type in common — no request could pass.
|
|
40
|
+
*/
|
|
41
|
+
export declare function combineRequirements(found: readonly (CallerRequirement | null)[], where: string): CallerRequirement | null;
|
|
42
|
+
//# sourceMappingURL=authenticated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authenticated.d.ts","sourceRoot":"","sources":["../../src/directives/authenticated.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,eAAO,MAAM,4BAA4B,kBAAkB,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,YAAY,+BAAgC,CAAC;AAE1D,MAAM,MAAM,oBAAoB,GAAG;IAClC;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAAE,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAE9D;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,SAAS,MAAM,EAAE,GACtB,iBAAiB,GAAG,IAAI,CAmB1B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,EAAE,EAC5C,KAAK,EAAE,MAAM,GACX,iBAAiB,GAAG,IAAI,CAc1B"}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation's own directive declarations, as a subgraph imports them.
|
|
3
|
+
* `@authenticated` keeps federation's shape — no argument — which
|
|
4
|
+
* `useAuthenticated` reads as "any caller". `AUTHENTICATED_DIRECTIVE_SDL`, in
|
|
5
|
+
* `SHARED_TYPE_DEFS`, is the one with `type:`.
|
|
6
|
+
*/
|
|
1
7
|
export declare const FEDERATION_DIRECTIVES = "\ndirective @external on FIELD_DEFINITION | OBJECT\ndirective @requires(fields: FieldSet!) on FIELD_DEFINITION\ndirective @provides(fields: FieldSet!) on FIELD_DEFINITION\ndirective @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE\ndirective @shareable repeatable on OBJECT | FIELD_DEFINITION\ndirective @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION\ndirective @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION\ndirective @override(from: String!) on FIELD_DEFINITION\ndirective @composeDirective(name: String!) repeatable on SCHEMA\ndirective @interfaceObject on OBJECT\ndirective @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM\n";
|
|
2
8
|
//# sourceMappingURL=federation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"federation.d.ts","sourceRoot":"","sources":["../../src/directives/federation.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,45BAYjC,CAAC"}
|
|
1
|
+
{"version":3,"file":"federation.d.ts","sourceRoot":"","sources":["../../src/directives/federation.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,45BAYjC,CAAC"}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { AUTHENTICATED_DIRECTIVE_NAME, type AuthenticatedOptions, CALLER_TYPES, } from './authenticated';
|
|
2
2
|
export * from './federation';
|
|
3
|
+
export { assertReadablePath, objectIds, readPath } from './paths';
|
|
4
|
+
export * from './permission';
|
|
5
|
+
export type { ReadOptions } from './scope';
|
|
6
|
+
export * from './sdl';
|
|
3
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/directives/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/directives/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,4BAA4B,EAC5B,KAAK,oBAAoB,EACzB,YAAY,GACZ,MAAM,iBAAiB,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAClE,cAAc,cAAc,CAAC;AAC7B,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,cAAc,OAAO,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PermissionTerm } from '@nxgt/ory-sdk';
|
|
2
|
+
/** The id a term reads when it names none. */
|
|
3
|
+
export declare const DEFAULT_ID_PATH = "args.id";
|
|
4
|
+
/**
|
|
5
|
+
* Validated when the schema is transformed, and not when a request arrives: a
|
|
6
|
+
* path naming no root is a wiring mistake, and it should stop the server from
|
|
7
|
+
* booting rather than 500 on the one query nobody tried.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assertReadablePath(term: PermissionTerm, where: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* `args.<name>` must name an argument the field declares. Graphql-js never
|
|
12
|
+
* puts an undeclared argument on `args`, so a typo here resolves no object on
|
|
13
|
+
* every request — refused at build instead, naming the arguments there are.
|
|
14
|
+
*/
|
|
15
|
+
export declare function assertDeclaredArgument(term: PermissionTerm, argumentNames: readonly string[], where: string): void;
|
|
16
|
+
/** Reads `args.x` / `parent.x.y` / `source.x.y` off a resolver's inputs. */
|
|
17
|
+
export declare function readPath(path: string, source: unknown, args: Record<string, unknown>): unknown;
|
|
18
|
+
/** The ids one term has to clear: one value, or every element of a list. */
|
|
19
|
+
export declare function objectIds(value: unknown): string[];
|
|
20
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/directives/paths.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAYpD,8CAA8C;AAC9C,eAAO,MAAM,eAAe,YAAY,CAAC;AAEzC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,QAMrE;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACrC,IAAI,EAAE,cAAc,EACpB,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,KAAK,EAAE,MAAM,QAUb;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CACvB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,OAAO,CAOT;AAED,4EAA4E;AAC5E,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAKlD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PermissionRequirement } from '@nxgt/ory-sdk';
|
|
2
|
+
import type { GraphQLSchema } from 'graphql';
|
|
3
|
+
import { type FieldNode, type ReadOptions } from './scope';
|
|
4
|
+
/**
|
|
5
|
+
* `@permission(name:, type:, id:, onDeny:, message:)` — one Keto question per
|
|
6
|
+
* directive, the vocabulary `@nxgt/janus-graphql` uses. Declared in
|
|
7
|
+
* `graphql/directives/permission.graphqls` and `PERMISSION_DIRECTIVE_SDL`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const PERMISSION_DIRECTIVE_NAME = "permission";
|
|
10
|
+
export type PermissionDenial = 'NOT_FOUND' | 'FORBIDDEN';
|
|
11
|
+
/** One `@permission` as written, after its defaults. */
|
|
12
|
+
export type PermissionArgs = {
|
|
13
|
+
name: string;
|
|
14
|
+
type: string;
|
|
15
|
+
id: string;
|
|
16
|
+
onDeny: PermissionDenial;
|
|
17
|
+
message?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* One `@permission` as the transform answers it: the one-term requirement
|
|
21
|
+
* `[[{ namespace: type, permit: name, id }]]` that `evaluateRequirement`
|
|
22
|
+
* takes, its denial, and its i18n key.
|
|
23
|
+
*/
|
|
24
|
+
export type FieldPermission = {
|
|
25
|
+
permissions: PermissionRequirement;
|
|
26
|
+
onDeny: PermissionDenial;
|
|
27
|
+
/** i18n key; the shared `errors.*` one when the field does not say. */
|
|
28
|
+
message?: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Every `@permission` on a field, in declaration order, validated.
|
|
32
|
+
*
|
|
33
|
+
* Repeatable, so this is a list and the order is load-bearing: `view` then
|
|
34
|
+
* `edit` is what turns a denial into 404 for a stranger and 403 for a viewer.
|
|
35
|
+
* The `id` and `onDeny` defaults are applied here as well as in the SDL, which
|
|
36
|
+
* graphql 17 does not always hand to `getDirective`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function readPermissions(schema: GraphQLSchema, node: FieldNode, where: string, options?: ReadOptions): FieldPermission[];
|
|
39
|
+
//# sourceMappingURL=permission.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../../src/directives/permission.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAW,MAAM,SAAS,CAAC;AAGpE;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,eAAe,CAAC;AAEtD,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,WAAW,CAAC;AAEzD,wDAAwD;AACxD,MAAM,MAAM,cAAc,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,gBAAgB,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG;IAC7B,WAAW,EAAE,qBAAqB,CAAC;IACnC,MAAM,EAAE,gBAAgB,CAAC;IACzB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC9B,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,WAAgB,GACvB,eAAe,EAAE,CAanB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GraphQLSchema } from 'graphql';
|
|
2
|
+
import type { FieldNode } from './scope';
|
|
3
|
+
/**
|
|
4
|
+
* `@check`, the list-of-lists form, was removed in 3.0. This package no longer
|
|
5
|
+
* ships its declaration, so a schema that still writes one either fails to
|
|
6
|
+
* build (`Unknown directive "@check"`) or declares it itself — and then the
|
|
7
|
+
* transform, which no longer reads it, would leave the field open in silence.
|
|
8
|
+
*
|
|
9
|
+
* So a field carrying a `@check` whose declaration has `permissions` — the
|
|
10
|
+
* shape 2.x shipped — is refused when the schema is built, naming the field
|
|
11
|
+
* and the rewrite. A `@check` of another shape is the schema's own and is left
|
|
12
|
+
* alone, as a foreign `@permission` is.
|
|
13
|
+
*/
|
|
14
|
+
export declare function refuseRemovedCheck(schema: GraphQLSchema, node: FieldNode, where: string): void;
|
|
15
|
+
//# sourceMappingURL=removed-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"removed-check.d.ts","sourceRoot":"","sources":["../../src/directives/removed-check.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CACjC,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,MAAM,QAQb"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { getDirective } from '@graphql-tools/utils';
|
|
2
|
+
import type { RequirementScope } from './validate';
|
|
3
|
+
/** A field definition or a field config — whatever `getDirective` reads. */
|
|
4
|
+
export type FieldNode = Parameters<typeof getDirective>[1];
|
|
5
|
+
/** What a schema may add to the build-time checks of `@permission`. */
|
|
6
|
+
export type ReadOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* The namespaces of the stack's OPL document. When given, a `type`
|
|
9
|
+
* outside it is refused at build.
|
|
10
|
+
*/
|
|
11
|
+
namespaces?: readonly string[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The scope a field's requirements are validated in: the message prefix, the
|
|
15
|
+
* arguments the field declares — read from a field config's `args` map or a
|
|
16
|
+
* definition node's `arguments` list — and the known namespaces.
|
|
17
|
+
*/
|
|
18
|
+
export declare function scopeOf(node: FieldNode, where: string, options: ReadOptions): RequirementScope;
|
|
19
|
+
//# sourceMappingURL=scope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../src/directives/scope.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,4EAA4E;AAC5E,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;AAE3D,uEAAuE;AACvE,MAAM,MAAM,WAAW,GAAG;IACzB;;;OAGG;IACH,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,OAAO,CACtB,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,WAAW,GAClB,gBAAgB,CAMlB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The directive declarations `graphql/directives/*.graphqls` ship, as strings,
|
|
3
|
+
* for a schema assembled in code rather than loaded from `SHARED_SCHEMA_PATH`
|
|
4
|
+
* — an edge runtime with no file system, or a test.
|
|
5
|
+
*
|
|
6
|
+
* The files are the source; these are copies of their declarations, without
|
|
7
|
+
* the `#` comments, and `sdl.spec.ts` holds each equal to its file once both
|
|
8
|
+
* are parsed and printed. Edit the file, then this.
|
|
9
|
+
*/
|
|
10
|
+
/** `@permission` and its `PermissionDenial` enum. */
|
|
11
|
+
export declare const PERMISSION_DIRECTIVE_SDL = "\"\"\"\nWhat a refused @permission looks like from outside.\n\"\"\"\nenum PermissionDenial {\n\t\"\"\"\n\tThe same answer as for an id that never existed, so ids cannot be probed.\n\tThe default.\n\t\"\"\"\n\tNOT_FOUND\n\n\t\"\"\"\n\tFor a permission asked of an object the caller can already see: a viewer\n\tasked to edit already knows it exists, and \"you may not change it\" is\n\thonest.\n\t\"\"\"\n\tFORBIDDEN\n}\n\n\"\"\"\nThe permission a field requires: the caller must hold the permit `name` on the\n`type` object whose id `id` points at.\n\nRepeatable, and repeated ones are AND, evaluated in declaration order \u2014 the\n404-then-403 ladder:\n\n updateNote(id: ID!, input: UpdateNoteInput!): Note!\n @permission(name: \"view\", type: \"Note\")\n @permission(name: \"edit\", type: \"Note\", onDeny: FORBIDDEN)\n\nNOT for a field that answers a LIST the caller is entitled to: fold a Keto\nquery into the database filter before the read instead.\n\"\"\"\ndirective @permission(\n\t\"\"\"\n\tThe permit asked of Keto, e.g. \"view\". A plain relation works too; Keto\n\tanswers `false`, not an error, for a name it does not know.\n\t\"\"\"\n\tname: String!\n\n\t\"\"\"\n\tThe Keto namespace, e.g. \"Note\" \u2014 from the stack's OPL document.\n\t\"\"\"\n\ttype: String!\n\n\t\"\"\"\n\tWhere the object id is read: `args.<path>` or `parent.<path>`, dotted\n\tpaths allowed, `args.id` when omitted. A value that turns out to be a LIST\n\trequires the permit on every element.\n\t\"\"\"\n\tid: String\n\n\tonDeny: PermissionDenial! = NOT_FOUND\n\n\t\"\"\"\n\tThe i18n key the denial carries, e.g. \"notes.errors.not-found\". Defaults to\n\t`errors.not-found` / `errors.insufficient-permissions`, the shared keys.\n\tSet it when the service layer words the same refusal with a domain message,\n\tso the wording does not tell a caller which layer refused.\n\t\"\"\"\n\tmessage: String\n) repeatable on FIELD_DEFINITION\n";
|
|
12
|
+
/**
|
|
13
|
+
* Every directive `useKetoChecks` answers, for
|
|
14
|
+
* `createSchema({ typeDefs: [KETO_DIRECTIVES_SDL, …] })`. Since `@check` was
|
|
15
|
+
* removed in 3.0, that is `@permission` alone.
|
|
16
|
+
*/
|
|
17
|
+
export declare const KETO_DIRECTIVES_SDL = "\"\"\"\nWhat a refused @permission looks like from outside.\n\"\"\"\nenum PermissionDenial {\n\t\"\"\"\n\tThe same answer as for an id that never existed, so ids cannot be probed.\n\tThe default.\n\t\"\"\"\n\tNOT_FOUND\n\n\t\"\"\"\n\tFor a permission asked of an object the caller can already see: a viewer\n\tasked to edit already knows it exists, and \"you may not change it\" is\n\thonest.\n\t\"\"\"\n\tFORBIDDEN\n}\n\n\"\"\"\nThe permission a field requires: the caller must hold the permit `name` on the\n`type` object whose id `id` points at.\n\nRepeatable, and repeated ones are AND, evaluated in declaration order \u2014 the\n404-then-403 ladder:\n\n updateNote(id: ID!, input: UpdateNoteInput!): Note!\n @permission(name: \"view\", type: \"Note\")\n @permission(name: \"edit\", type: \"Note\", onDeny: FORBIDDEN)\n\nNOT for a field that answers a LIST the caller is entitled to: fold a Keto\nquery into the database filter before the read instead.\n\"\"\"\ndirective @permission(\n\t\"\"\"\n\tThe permit asked of Keto, e.g. \"view\". A plain relation works too; Keto\n\tanswers `false`, not an error, for a name it does not know.\n\t\"\"\"\n\tname: String!\n\n\t\"\"\"\n\tThe Keto namespace, e.g. \"Note\" \u2014 from the stack's OPL document.\n\t\"\"\"\n\ttype: String!\n\n\t\"\"\"\n\tWhere the object id is read: `args.<path>` or `parent.<path>`, dotted\n\tpaths allowed, `args.id` when omitted. A value that turns out to be a LIST\n\trequires the permit on every element.\n\t\"\"\"\n\tid: String\n\n\tonDeny: PermissionDenial! = NOT_FOUND\n\n\t\"\"\"\n\tThe i18n key the denial carries, e.g. \"notes.errors.not-found\". Defaults to\n\t`errors.not-found` / `errors.insufficient-permissions`, the shared keys.\n\tSet it when the service layer words the same refusal with a domain message,\n\tso the wording does not tell a caller which layer refused.\n\t\"\"\"\n\tmessage: String\n) repeatable on FIELD_DEFINITION\n";
|
|
18
|
+
/**
|
|
19
|
+
* `@authenticated`, with the `type:` `useAuthenticated` reads: any caller
|
|
20
|
+
* without it, a caller of one of the types with it. Part of
|
|
21
|
+
* `SHARED_TYPE_DEFS`, and not in `graphql/`: a federation subgraph loads
|
|
22
|
+
* `SHARED_SCHEMA_PATH` and imports federation's own `@authenticated`, which
|
|
23
|
+
* takes no argument — a shape `useAuthenticated` reads as "any caller".
|
|
24
|
+
*/
|
|
25
|
+
export declare const AUTHENTICATED_DIRECTIVE_SDL = "\"\"\"\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";
|
|
26
|
+
//# sourceMappingURL=sdl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdl.d.ts","sourceRoot":"","sources":["../../src/directives/sdl.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,qDAAqD;AACrD,eAAO,MAAM,wBAAwB,u5DA6DpC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,u5DAA2B,CAAC;AAE5D;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,sUAUvC,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type PermissionRequirement } from '@nxgt/ory-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* What a requirement is checked against when the schema is built.
|
|
4
|
+
*
|
|
5
|
+
* `namespaces` is optional: without it any namespace is taken on trust. With
|
|
6
|
+
* it — the namespaces of the stack's OPL document — a misspelt `type` stops
|
|
7
|
+
* the server from booting instead of answering `false` for ever, which Keto
|
|
8
|
+
* does, without an error, for a namespace it does not know.
|
|
9
|
+
*/
|
|
10
|
+
export type RequirementScope = {
|
|
11
|
+
/** `@permission on Query.note` — the directive and the field, for the message. */
|
|
12
|
+
where: string;
|
|
13
|
+
argumentNames: readonly string[];
|
|
14
|
+
namespaces?: readonly string[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Every mistake a requirement can carry that is visible without a request, as
|
|
18
|
+
* a `TypeError` naming the field: an empty name or type, a path naming no
|
|
19
|
+
* root, an argument the field does not declare, and a namespace the model
|
|
20
|
+
* does not have.
|
|
21
|
+
*/
|
|
22
|
+
export declare function assertRequirementShape(permissions: PermissionRequirement, scope: RequirementScope): void;
|
|
23
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/directives/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAG9E;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC9B,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACrC,WAAW,EAAE,qBAAqB,EAClC,KAAK,EAAE,gBAAgB,QAmBvB"}
|