@reventlessdev/reventless-aws 3.0.0-alpha.298 → 3.0.0-alpha.300
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/CHANGELOG.md +14 -0
- package/package.json +9 -9
- package/src/Platform.res +1 -1
- package/src/Platform.res.mjs +2 -2
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +1 -1
- package/src/components/Api/AppSync_Adapter.res +32 -17
- package/src/components/Api/AppSync_Adapter.res.mjs +2 -2
- package/src/components/Api/AppSync_MergedApi.res +1 -1
- package/src/components/Api/AppSync_MergedApi.res.mjs +1 -1
- package/src/components/Api/AppSync_SdlDecorate.res +210 -12
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +173 -0
- package/tests/AppSync_SdlDecorateTest.res +55 -0
- package/tests/AppSync_SdlDecorateTest.res.mjs +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.300 (2026-08-14)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **core:** one contract for which refusal a platform gave ([7705c13](https://github.com/ReventlessDev/reventless-core/commit/7705c13ec63f41a3a659bc98cfb45b880fd5b222))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.299 (2026-08-14)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **aws:** refuse to push an AppSync schema that cannot be gated ([18e9279](https://github.com/ReventlessDev/reventless-core/commit/18e92793b1b42b4b94dc61e9e285918c67e62e8a))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.298 (2026-08-14)
|
|
7
21
|
|
|
8
22
|
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.300",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"@aws-sdk/s3-request-presigner": "3.970.0",
|
|
13
13
|
"sury": "11.0.0-alpha.4",
|
|
14
14
|
"uuid": "^13.0.0",
|
|
15
|
-
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.10",
|
|
16
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
15
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
16
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.10",
|
|
18
18
|
"@reventlessdev/rescript-node": "2.0.0-alpha.6",
|
|
19
19
|
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.75",
|
|
20
|
-
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
21
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.233",
|
|
22
20
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
23
|
-
"@reventlessdev/
|
|
24
|
-
"@reventlessdev/reventless-
|
|
25
|
-
"@reventlessdev/reventless-
|
|
26
|
-
"@reventlessdev/reventless-
|
|
21
|
+
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
22
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.141",
|
|
23
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.31",
|
|
24
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.98",
|
|
25
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.234",
|
|
26
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.113"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"rescript": "12.3.0",
|
package/src/Platform.res
CHANGED
package/src/Platform.res.mjs
CHANGED
|
@@ -220,7 +220,7 @@ function MakeWithConfig(Config) {
|
|
|
220
220
|
let awsRegion = Stdlib_Option.orElse(param[1], getFromDefault("cognitoRegion"));
|
|
221
221
|
return {
|
|
222
222
|
userPoolId: userPoolId,
|
|
223
|
-
defaultAction: "
|
|
223
|
+
defaultAction: "ALLOW",
|
|
224
224
|
awsRegion: awsRegion
|
|
225
225
|
};
|
|
226
226
|
});
|
|
@@ -1523,7 +1523,7 @@ function Make($star) {
|
|
|
1523
1523
|
let awsRegion = Stdlib_Option.orElse(param[1], getFromDefault("cognitoRegion"));
|
|
1524
1524
|
return {
|
|
1525
1525
|
userPoolId: userPoolId,
|
|
1526
|
-
defaultAction: "
|
|
1526
|
+
defaultAction: "ALLOW",
|
|
1527
1527
|
awsRegion: awsRegion
|
|
1528
1528
|
};
|
|
1529
1529
|
});
|
|
@@ -68,7 +68,7 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
68
68
|
~subIdField,
|
|
69
69
|
~idResolverConfigs: array<idResolverConfig>,
|
|
70
70
|
~idsResolverConfigs: array<idsResolverConfig>,
|
|
71
|
-
// AWS path enforces authorization via `@
|
|
71
|
+
// AWS path enforces authorization via `@aws_cognito_user_pools(cognito_groups: …)` on the
|
|
72
72
|
// SDL fields (see Stage E in docs/plans/host-ui-login-core.md). Accepted but
|
|
73
73
|
// ignored here so the in-memory and AWS resolver maker signatures stay aligned.
|
|
74
74
|
~authorization as _: Reventless.Authorization.permission,
|
|
@@ -185,10 +185,13 @@ let rec waitForMergeSuccess = async (
|
|
|
185
185
|
// When both are present on the same field, the spec-level permission wins
|
|
186
186
|
// (it is more specific). `AllowGroups([g1, g2, ...])` emits
|
|
187
187
|
// `@aws_cognito_user_pools(cognito_groups: ["g1", "g2", ...])`.
|
|
188
|
-
// `AllowAuthenticated` / `AllowAnonymous` emit
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
188
|
+
// `AllowAuthenticated` / `AllowAnonymous` emit the group-less
|
|
189
|
+
// `@aws_cognito_user_pools` — same reachability (with Cognito as primary auth
|
|
190
|
+
// any reaching request is already authenticated), but stated rather than left
|
|
191
|
+
// implicit, so `assertGateable` can tell "deliberately open" from "nobody
|
|
192
|
+
// stamped this". `AllowGroups([])` and `DenyAll` emit a sentinel `__deny_all__`
|
|
193
|
+
// group that no Cognito user can belong to — effectively blocking the field at
|
|
194
|
+
// the API layer.
|
|
192
195
|
//
|
|
193
196
|
// NOT `@aws_auth(...)`: that is the single-mode form, which AppSync ignores on a
|
|
194
197
|
// multi-auth API — which every API this adapter provisions is. See
|
|
@@ -422,14 +425,14 @@ let injectAwsAuth = (
|
|
|
422
425
|
})
|
|
423
426
|
}
|
|
424
427
|
|
|
425
|
-
// Injects
|
|
428
|
+
// Injects the Cognito group directive on ALL mutation, query, and subscription
|
|
426
429
|
// fields in a fragment. Used for the base fragment where all fields share the
|
|
427
430
|
// same authorization group.
|
|
428
431
|
//
|
|
429
432
|
// `~iamFieldNames` opts the named mutation/query fields into deploy-time IAM
|
|
430
433
|
// (dual-auth): those fields emit `@aws_cognito_user_pools(cognito_groups:
|
|
431
|
-
// ["<group>"]) @aws_iam
|
|
432
|
-
//
|
|
434
|
+
// ["<group>"]) @aws_iam`, keeping the same Cognito group gating while also
|
|
435
|
+
// admitting the SigV4 system caller.
|
|
433
436
|
// Subscriptions are never IAM-marked (the deploy caller does not subscribe).
|
|
434
437
|
// Canonical definition lives in the runtime-pure AppSync_SdlDecorate so the
|
|
435
438
|
// bundled AdminEventCollector Lambda's reactive push decorates the admin base
|
|
@@ -470,6 +473,13 @@ let stitchStandaloneWithAwsDirectives = (
|
|
|
470
473
|
// Last: every remaining undirectived type takes the group-less Cognito arm.
|
|
471
474
|
// After stampSharedIamTypes so the shared traversal types keep `@aws_iam`.
|
|
472
475
|
->AppSync_SdlDecorate.stampAllTypesCognito
|
|
476
|
+
// Fields the stitcher injects during assembly (`_noop`) are invisible to the
|
|
477
|
+
// fragment sweep above, so sweep the assembled document too.
|
|
478
|
+
->AppSync_SdlDecorate.stampUndirectivedRootFields
|
|
479
|
+
// Then refuse to hand back a schema that cannot be gated. `defaultAction: DENY`
|
|
480
|
+
// is unavailable on a multi-auth API, so this deploy-time check is the only
|
|
481
|
+
// fail-closed step there is — everything downstream trusts what it returns.
|
|
482
|
+
->AppSync_SdlDecorate.assertGateable
|
|
473
483
|
}
|
|
474
484
|
|
|
475
485
|
// ── Provider implementation ────────────────────────────────────────────────
|
|
@@ -534,16 +544,21 @@ let _makeApiResourceWith = (
|
|
|
534
544
|
// site — DomainApi, PlatformApi — is safe). A single Output yielding the
|
|
535
545
|
// {userPoolId, awsRegion, defaultAction} record AppSync expects.
|
|
536
546
|
//
|
|
537
|
-
// `
|
|
538
|
-
//
|
|
539
|
-
// directive fail closed — under `ALLOW` the group gate that was inert for the
|
|
540
|
-
// whole of `@aws_auth`'s life produced no symptom at all, because "no
|
|
541
|
-
// effective directive" and "open to everyone" were the same thing.
|
|
547
|
+
// `ALLOW` is forced, not chosen. `DENY` would make a missing directive fail
|
|
548
|
+
// closed, which is what we want — but AWS refuses the combination outright:
|
|
542
549
|
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
550
|
+
// BadRequestException: Additional authentication providers cannot be
|
|
551
|
+
// specified when setting DENY for top level user pool authentication type.
|
|
552
|
+
//
|
|
553
|
+
// Every API here configures AWS_IAM as an additional provider (the
|
|
554
|
+
// server-to-server lambdas need it), so DENY is unavailable for as long as
|
|
555
|
+
// that is true. Tried on a deploy 2026-08-14; all four APIs rejected it.
|
|
556
|
+
//
|
|
557
|
+
// The protection therefore rests entirely on every field and type carrying an
|
|
558
|
+
// explicit directive — `stampUndirectivedFields` + `stampAllTypesCognito` at
|
|
559
|
+
// the assembly choke point — with no backstop underneath. That makes
|
|
560
|
+
// `scripts/check-appsync-directive-coverage.mjs` load-bearing rather than
|
|
561
|
+
// advisory: run it against a DEPLOYED api after any change to the decoration.
|
|
547
562
|
let userPoolConfigOut = switch userPoolConfig {
|
|
548
563
|
| Some(config) => config
|
|
549
564
|
| None =>
|
|
@@ -552,7 +567,7 @@ let _makeApiResourceWith = (
|
|
|
552
567
|
{
|
|
553
568
|
userPoolId: c.userPoolId,
|
|
554
569
|
awsRegion: c.region,
|
|
555
|
-
defaultAction: AppSync.GraphQLApi.
|
|
570
|
+
defaultAction: AppSync.GraphQLApi.ALLOW,
|
|
556
571
|
}: AppSync.GraphQLApi.userPoolConfig
|
|
557
572
|
)
|
|
558
573
|
)
|
|
@@ -265,7 +265,7 @@ function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
|
|
|
265
265
|
function stitchStandaloneWithAwsDirectives(fragment) {
|
|
266
266
|
let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(fragment, []);
|
|
267
267
|
let fragment$1 = AppSync_SdlDecorate$ReventlessAws.stampUndirectivedFields(fragment);
|
|
268
|
-
return AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito(AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(GraphQL_Stitcher$ReventlessCore.stitchStandalone(fragment$1), sources)));
|
|
268
|
+
return AppSync_SdlDecorate$ReventlessAws.assertGateable(AppSync_SdlDecorate$ReventlessAws.stampUndirectivedRootFields(AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito(AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(GraphQL_Stitcher$ReventlessCore.stitchStandalone(fragment$1), sources)))));
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
function _makeApiResourceWith(name, schema, userPoolConfig, opts) {
|
|
@@ -288,7 +288,7 @@ function _makeApiResourceWith(name, schema, userPoolConfig, opts) {
|
|
|
288
288
|
};
|
|
289
289
|
let userPoolConfigOut = userPoolConfig !== undefined ? userPoolConfig : Auth_Cognito$ReventlessAws.make(name + `-auth`, undefined).apply(c => ({
|
|
290
290
|
userPoolId: c.userPoolId,
|
|
291
|
-
defaultAction: "
|
|
291
|
+
defaultAction: "ALLOW",
|
|
292
292
|
awsRegion: c.region
|
|
293
293
|
}));
|
|
294
294
|
let apiArgs_schema = Stdlib_Option.map(schema, prim => prim);
|
|
@@ -100,7 +100,7 @@ let make = (~name: string, ~opts: Pulumi.ComponentResource.options): t => {
|
|
|
100
100
|
): AppSync.GraphQLApi.userPoolConfig => {
|
|
101
101
|
userPoolId: c.userPoolId,
|
|
102
102
|
awsRegion: c.region,
|
|
103
|
-
defaultAction: AppSync.GraphQLApi.
|
|
103
|
+
defaultAction: AppSync.GraphQLApi.ALLOW,
|
|
104
104
|
})
|
|
105
105
|
|
|
106
106
|
let mergedApi = AppSync.GraphQLApi.make(
|
|
@@ -55,7 +55,7 @@ function make(name, opts) {
|
|
|
55
55
|
let authConfigOut = Auth_Cognito$ReventlessAws.make(name + `-auth`, undefined);
|
|
56
56
|
let userPoolConfigOut = authConfigOut.apply(c => ({
|
|
57
57
|
userPoolId: c.userPoolId,
|
|
58
|
-
defaultAction: "
|
|
58
|
+
defaultAction: "ALLOW",
|
|
59
59
|
awsRegion: c.region
|
|
60
60
|
}));
|
|
61
61
|
let mergedApi = new (Aws.appsync.GraphQLApi)(name, {
|
|
@@ -93,10 +93,10 @@ let formatCognitoGroupsDirective = (groups: array<string>): string => {
|
|
|
93
93
|
//
|
|
94
94
|
// Under `userPoolConfig.defaultAction: ALLOW` this is a no-op: an undirectived
|
|
95
95
|
// field is already reachable by any authenticated caller, so stamping it changes
|
|
96
|
-
// nothing. It exists so
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
96
|
+
// nothing. It exists so that "carries no directive" becomes a state we can
|
|
97
|
+
// detect and refuse at deploy time (`assertGateable`), rather than one that
|
|
98
|
+
// silently means "open to everyone" — which is exactly how the `@aws_auth`
|
|
99
|
+
// outage stayed invisible for as long as it did.
|
|
100
100
|
let cognitoOpenDirective = "@aws_cognito_user_pools"
|
|
101
101
|
|
|
102
102
|
// Multi-auth directive for a field/type that must accept BOTH Cognito and IAM.
|
|
@@ -171,9 +171,12 @@ let stampSharedIamTypes = (sdl: string): string =>
|
|
|
171
171
|
// several surfaces never reach it at all: the domain base document
|
|
172
172
|
// (`Platform_ping`), event-history queries, event-log `…_eventAppended`
|
|
173
173
|
// subscriptions, the upload presign/release mutations, `Platform_SetActiveRole`,
|
|
174
|
-
// and `geocode`. Those deployed with no directive —
|
|
175
|
-
//
|
|
176
|
-
//
|
|
174
|
+
// and `geocode`. Those deployed with no directive at all — reachable by any
|
|
175
|
+
// authenticated Cognito caller, and invisible, since `defaultAction: ALLOW` makes
|
|
176
|
+
// "no directive" and "open" the same thing. Sweeping here rather than at each
|
|
177
|
+
// emission site means a future injected field cannot silently miss the net;
|
|
178
|
+
// `assertGateable` below turns a miss into a failed deploy rather than an open
|
|
179
|
+
// field.
|
|
177
180
|
//
|
|
178
181
|
// Idempotent, and deliberately does not second-guess gating: it only fills in
|
|
179
182
|
// "open to any authenticated Cognito caller", which is what an undirectived
|
|
@@ -198,11 +201,10 @@ let stampUndirectivedFields = (
|
|
|
198
201
|
// stampSharedIamTypes so the shared traversal types keep their `@aws_iam` arm
|
|
199
202
|
// (this pass skips anything already carrying an `@aws_` directive).
|
|
200
203
|
//
|
|
201
|
-
// Types, not just fields:
|
|
202
|
-
//
|
|
203
|
-
// own gate
|
|
204
|
-
//
|
|
205
|
-
// stamp only restores the accessibility an undirectived type has today.
|
|
204
|
+
// Types, not just fields: response shaping walks `…Connection` → `…Edge` → node
|
|
205
|
+
// → nested state types, so a type that cannot be traversed fails a request that
|
|
206
|
+
// passed the field's own gate. Entry gating stays on the fields; a type stamp
|
|
207
|
+
// only restores the accessibility an undirectived type has today.
|
|
206
208
|
//
|
|
207
209
|
// Only `type` declarations take auth directives — `input`, `enum`, `union` and
|
|
208
210
|
// `interface` do not, and stamping them is a schema error.
|
|
@@ -225,6 +227,202 @@ let stampAllTypesCognito = (sdl: string): string =>
|
|
|
225
227
|
)
|
|
226
228
|
->Array.join("\n")
|
|
227
229
|
|
|
230
|
+
// ── Deploy-time gate invariant ──────────────────────────────────────────────
|
|
231
|
+
//
|
|
232
|
+
// Refuse to push a schema that cannot be gated. This is the fail-closed
|
|
233
|
+
// property `userPoolConfig.defaultAction: DENY` would have provided at REQUEST
|
|
234
|
+
// time — AWS rejects DENY alongside an additional auth provider
|
|
235
|
+
// ("Additional authentication providers cannot be specified when setting DENY
|
|
236
|
+
// for top level user pool authentication type", reproduced on create and
|
|
237
|
+
// update alike), and every API here configures AWS_IAM unconditionally. So the
|
|
238
|
+
// check moves one layer earlier, into our own deploy, where nothing can veto it.
|
|
239
|
+
//
|
|
240
|
+
// Two conditions, and the second is the one that matters:
|
|
241
|
+
//
|
|
242
|
+
// 1. A root field or object type carrying NO directive AppSync honours here
|
|
243
|
+
// is reachable by any authenticated Cognito caller under `ALLOW`.
|
|
244
|
+
// 2. ANY `@aws_auth` at all. It is the single-mode form, silently ignored on
|
|
245
|
+
// a multi-auth API — so its presence means something believes it is gated
|
|
246
|
+
// and is not.
|
|
247
|
+
//
|
|
248
|
+
// Condition 2 is why this is not merely "does it have a directive?". The
|
|
249
|
+
// original outage had `@aws_auth(cognito_groups: ["Admin"])` present on all 18
|
|
250
|
+
// admin fields for its entire life; a presence check would have passed it every
|
|
251
|
+
// single time. Only a check that knows WHICH directive the service honours
|
|
252
|
+
// would have failed that deploy.
|
|
253
|
+
let enforcedDirectives = ["@aws_cognito_user_pools", "@aws_iam"]
|
|
254
|
+
|
|
255
|
+
let hasEnforcedDirective = (s: string): bool =>
|
|
256
|
+
enforcedDirectives->Array.some(d => s->String.includes(d))
|
|
257
|
+
|
|
258
|
+
let rootOperationTypes = ["Query", "Mutation", "Subscription"]
|
|
259
|
+
|
|
260
|
+
let typeDeclNameOf = (line: string): option<string> =>
|
|
261
|
+
if line->String.startsWith("type ") {
|
|
262
|
+
let rest = line->String.slice(~start=5, ~end=line->String.length)
|
|
263
|
+
switch rest->String.search(%re("/[\s{]/")) {
|
|
264
|
+
| -1 => Some(rest)
|
|
265
|
+
| i => Some(rest->String.slice(~start=0, ~end=i))
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
None
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// A root-operation field line: exactly two spaces of indent then a name. Deeper
|
|
272
|
+
// indents are directive continuations of the field above.
|
|
273
|
+
let fieldNameOf = (line: string): option<string> =>
|
|
274
|
+
switch line->String.match(%re("/^ {2}(\w+)/")) {
|
|
275
|
+
| Some(groups) => groups->Array.get(1)->Option.flatMap(x => x)
|
|
276
|
+
| None => None
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Field sweep on the ASSEMBLED SDL, for fields the fragment sweep cannot see
|
|
280
|
+
// because the stitcher injects them during assembly — `_noop`, the placeholder
|
|
281
|
+
// mutation added to keep a mutation-less schema valid, is the standing example
|
|
282
|
+
// and was live on three deployed APIs.
|
|
283
|
+
//
|
|
284
|
+
// Groups continuation lines into field entries the way `assertGateable` does, so
|
|
285
|
+
// a directive already sitting on the following line counts as present.
|
|
286
|
+
let stampUndirectivedRootFields = (sdl: string): string => {
|
|
287
|
+
let out = []
|
|
288
|
+
let currentRoot = ref(None)
|
|
289
|
+
let buffer = ref([])
|
|
290
|
+
|
|
291
|
+
let flush = () => {
|
|
292
|
+
let entry = buffer.contents
|
|
293
|
+
if entry->Array.length > 0 {
|
|
294
|
+
entry->Array.forEach(l => out->Array.push(l))
|
|
295
|
+
if !hasEnforcedDirective(entry->Array.join("\n")) {
|
|
296
|
+
out->Array.push(` ${cognitoOpenDirective}`)
|
|
297
|
+
}
|
|
298
|
+
buffer.contents = []
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
sdl
|
|
303
|
+
->String.split("\n")
|
|
304
|
+
->Array.forEach(line =>
|
|
305
|
+
switch typeDeclNameOf(line) {
|
|
306
|
+
| Some(name) =>
|
|
307
|
+
flush()
|
|
308
|
+
currentRoot.contents = rootOperationTypes->Array.includes(name) ? Some(name) : None
|
|
309
|
+
out->Array.push(line)
|
|
310
|
+
| None =>
|
|
311
|
+
if line->String.startsWith("}") {
|
|
312
|
+
flush()
|
|
313
|
+
currentRoot.contents = None
|
|
314
|
+
out->Array.push(line)
|
|
315
|
+
} else if currentRoot.contents->Option.isSome {
|
|
316
|
+
switch fieldNameOf(line) {
|
|
317
|
+
| Some(_) =>
|
|
318
|
+
flush()
|
|
319
|
+
buffer.contents = [line]
|
|
320
|
+
| None =>
|
|
321
|
+
if buffer.contents->Array.length > 0 {
|
|
322
|
+
buffer.contents = buffer.contents->Array.concat([line])
|
|
323
|
+
} else {
|
|
324
|
+
out->Array.push(line)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
out->Array.push(line)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
flush()
|
|
333
|
+
out->Array.join("\n")
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let assertGateable = (sdl: string): string => {
|
|
337
|
+
let bareFields = []
|
|
338
|
+
let bareTypes = []
|
|
339
|
+
let inertDirectives = []
|
|
340
|
+
|
|
341
|
+
let currentRoot = ref(None)
|
|
342
|
+
// (fieldName, accumulated text incl. continuation lines)
|
|
343
|
+
let pending = ref(None)
|
|
344
|
+
|
|
345
|
+
let flush = () =>
|
|
346
|
+
switch pending.contents {
|
|
347
|
+
| Some((name, text)) =>
|
|
348
|
+
if !hasEnforcedDirective(text) {
|
|
349
|
+
bareFields->Array.push(name)
|
|
350
|
+
}
|
|
351
|
+
pending.contents = None
|
|
352
|
+
| None => ()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
sdl
|
|
356
|
+
->String.split("\n")
|
|
357
|
+
->Array.forEach(line => {
|
|
358
|
+
if line->String.includes("@aws_auth") {
|
|
359
|
+
inertDirectives->Array.push(line->String.trim)
|
|
360
|
+
}
|
|
361
|
+
switch typeDeclNameOf(line) {
|
|
362
|
+
| Some(name) =>
|
|
363
|
+
flush()
|
|
364
|
+
if !hasEnforcedDirective(line) {
|
|
365
|
+
bareTypes->Array.push(name)
|
|
366
|
+
}
|
|
367
|
+
currentRoot.contents = rootOperationTypes->Array.includes(name) ? Some(name) : None
|
|
368
|
+
| None =>
|
|
369
|
+
if line->String.startsWith("}") {
|
|
370
|
+
flush()
|
|
371
|
+
currentRoot.contents = None
|
|
372
|
+
} else if currentRoot.contents->Option.isSome {
|
|
373
|
+
switch fieldNameOf(line) {
|
|
374
|
+
| Some(fieldName) =>
|
|
375
|
+
flush()
|
|
376
|
+
pending.contents = Some((fieldName, line))
|
|
377
|
+
| None =>
|
|
378
|
+
switch pending.contents {
|
|
379
|
+
| Some((n, text)) => pending.contents = Some((n, text ++ "\n" ++ line))
|
|
380
|
+
| None => ()
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
flush()
|
|
387
|
+
|
|
388
|
+
let problems = []
|
|
389
|
+
if inertDirectives->Array.length > 0 {
|
|
390
|
+
problems->Array.push(
|
|
391
|
+
` ${inertDirectives->Array.length->Int.toString} field(s) carry @aws_auth, which AppSync IGNORES on a\n` ++
|
|
392
|
+
` multi-auth API (this adapter always adds AWS_IAM as an additional provider).\n` ++
|
|
393
|
+
` They are gated by nothing. Emit @aws_cognito_user_pools(cognito_groups: [...])\n` ++
|
|
394
|
+
` instead — see AppSync_SdlDecorate.formatCognitoGroupsDirective.\n` ++
|
|
395
|
+
` ${inertDirectives->Array.slice(~start=0, ~end=8)->Array.join("\n ")}`,
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
if bareFields->Array.length > 0 {
|
|
399
|
+
problems->Array.push(
|
|
400
|
+
` ${bareFields->Array.length->Int.toString} root field(s) carry no enforced auth directive, so they are\n` ++
|
|
401
|
+
` reachable by ANY authenticated Cognito caller:\n` ++
|
|
402
|
+
` ${bareFields->Array.join(", ")}`,
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
if bareTypes->Array.length > 0 {
|
|
406
|
+
problems->Array.push(
|
|
407
|
+
` ${bareTypes->Array.length->Int.toString} object type(s) carry no enforced auth directive:\n` ++
|
|
408
|
+
` ${bareTypes->Array.join(", ")}`,
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if problems->Array.length > 0 {
|
|
413
|
+
JsError.throwWithMessage(
|
|
414
|
+
`Refusing to push an AppSync schema that cannot be gated.\n\n` ++
|
|
415
|
+
problems->Array.join("\n\n") ++
|
|
416
|
+
`\n\nEvery field and type must carry @aws_cognito_user_pools (optionally with\n` ++
|
|
417
|
+
`cognito_groups) and/or @aws_iam. The sweep in stampUndirectivedFields /\n` ++
|
|
418
|
+
`stampAllTypesCognito should have covered this — a failure here means a schema\n` ++
|
|
419
|
+
`reached assembly by a path that bypasses them.\n` ++
|
|
420
|
+
`See docs/plans/appsync-group-authorization-unenforced.md.`,
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
sdl
|
|
424
|
+
}
|
|
425
|
+
|
|
228
426
|
// ── Merged-API canonical stamping ─────────────────────────────────────────────
|
|
229
427
|
// Under AppSync Merged APIs the admin source API owns the shared traversal
|
|
230
428
|
// types; `@canonical` makes its definition win over every plugin source's copy
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
5
|
import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
|
|
6
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
5
7
|
import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
|
|
6
8
|
|
|
7
9
|
function injectAwsSubscribe(sdl, sources) {
|
|
@@ -120,6 +122,170 @@ function stampAllTypesCognito(sdl) {
|
|
|
120
122
|
}).join("\n");
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
let enforcedDirectives = [
|
|
126
|
+
"@aws_cognito_user_pools",
|
|
127
|
+
"@aws_iam"
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
function hasEnforcedDirective(s) {
|
|
131
|
+
return enforcedDirectives.some(d => s.includes(d));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let rootOperationTypes = [
|
|
135
|
+
"Query",
|
|
136
|
+
"Mutation",
|
|
137
|
+
"Subscription"
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
function typeDeclNameOf(line) {
|
|
141
|
+
if (!line.startsWith("type ")) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
let rest = line.slice(5, line.length);
|
|
145
|
+
let i = rest.search(/[\s{]/);
|
|
146
|
+
if (i !== -1) {
|
|
147
|
+
return rest.slice(0, i);
|
|
148
|
+
} else {
|
|
149
|
+
return rest;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function fieldNameOf(line) {
|
|
154
|
+
let groups = line.match(/^ {2}(\w+)/);
|
|
155
|
+
if (!(groups == null)) {
|
|
156
|
+
return Stdlib_Option.flatMap(groups[1], x => x);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function stampUndirectivedRootFields(sdl) {
|
|
161
|
+
let out = [];
|
|
162
|
+
let currentRoot = {
|
|
163
|
+
contents: undefined
|
|
164
|
+
};
|
|
165
|
+
let buffer = {
|
|
166
|
+
contents: []
|
|
167
|
+
};
|
|
168
|
+
let flush = () => {
|
|
169
|
+
let entry = buffer.contents;
|
|
170
|
+
if (entry.length !== 0) {
|
|
171
|
+
entry.forEach(l => {
|
|
172
|
+
out.push(l);
|
|
173
|
+
});
|
|
174
|
+
if (!hasEnforcedDirective(entry.join("\n"))) {
|
|
175
|
+
out.push(` ` + cognitoOpenDirective);
|
|
176
|
+
}
|
|
177
|
+
buffer.contents = [];
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
sdl.split("\n").forEach(line => {
|
|
182
|
+
let name = typeDeclNameOf(line);
|
|
183
|
+
if (name !== undefined) {
|
|
184
|
+
flush();
|
|
185
|
+
currentRoot.contents = rootOperationTypes.includes(name) ? name : undefined;
|
|
186
|
+
out.push(line);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (line.startsWith("}")) {
|
|
190
|
+
flush();
|
|
191
|
+
currentRoot.contents = undefined;
|
|
192
|
+
out.push(line);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (Stdlib_Option.isSome(currentRoot.contents)) {
|
|
196
|
+
let match = fieldNameOf(line);
|
|
197
|
+
if (match !== undefined) {
|
|
198
|
+
flush();
|
|
199
|
+
buffer.contents = [line];
|
|
200
|
+
} else if (buffer.contents.length !== 0) {
|
|
201
|
+
buffer.contents = buffer.contents.concat([line]);
|
|
202
|
+
} else {
|
|
203
|
+
out.push(line);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
out.push(line);
|
|
208
|
+
});
|
|
209
|
+
flush();
|
|
210
|
+
return out.join("\n");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function assertGateable(sdl) {
|
|
214
|
+
let bareFields = [];
|
|
215
|
+
let bareTypes = [];
|
|
216
|
+
let inertDirectives = [];
|
|
217
|
+
let currentRoot = {
|
|
218
|
+
contents: undefined
|
|
219
|
+
};
|
|
220
|
+
let pending = {
|
|
221
|
+
contents: undefined
|
|
222
|
+
};
|
|
223
|
+
let flush = () => {
|
|
224
|
+
let match = pending.contents;
|
|
225
|
+
if (match !== undefined) {
|
|
226
|
+
if (!hasEnforcedDirective(match[1])) {
|
|
227
|
+
bareFields.push(match[0]);
|
|
228
|
+
}
|
|
229
|
+
pending.contents = undefined;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
sdl.split("\n").forEach(line => {
|
|
234
|
+
if (line.includes("@aws_auth")) {
|
|
235
|
+
inertDirectives.push(line.trim());
|
|
236
|
+
}
|
|
237
|
+
let name = typeDeclNameOf(line);
|
|
238
|
+
if (name !== undefined) {
|
|
239
|
+
flush();
|
|
240
|
+
if (!hasEnforcedDirective(line)) {
|
|
241
|
+
bareTypes.push(name);
|
|
242
|
+
}
|
|
243
|
+
currentRoot.contents = rootOperationTypes.includes(name) ? name : undefined;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (line.startsWith("}")) {
|
|
247
|
+
flush();
|
|
248
|
+
currentRoot.contents = undefined;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!Stdlib_Option.isSome(currentRoot.contents)) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
let fieldName = fieldNameOf(line);
|
|
255
|
+
if (fieldName !== undefined) {
|
|
256
|
+
flush();
|
|
257
|
+
pending.contents = [
|
|
258
|
+
fieldName,
|
|
259
|
+
line
|
|
260
|
+
];
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
let match = pending.contents;
|
|
264
|
+
if (match !== undefined) {
|
|
265
|
+
pending.contents = [
|
|
266
|
+
match[0],
|
|
267
|
+
match[1] + "\n" + line
|
|
268
|
+
];
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
flush();
|
|
273
|
+
let problems = [];
|
|
274
|
+
if (inertDirectives.length !== 0) {
|
|
275
|
+
problems.push(` ` + inertDirectives.length.toString() + ` field(s) carry @aws_auth, which AppSync IGNORES on a\n multi-auth API (this adapter always adds AWS_IAM as an additional provider).\n They are gated by nothing. Emit @aws_cognito_user_pools(cognito_groups: [...])\n instead — see AppSync_SdlDecorate.formatCognitoGroupsDirective.\n ` + inertDirectives.slice(0, 8).join("\n "));
|
|
276
|
+
}
|
|
277
|
+
if (bareFields.length !== 0) {
|
|
278
|
+
problems.push(` ` + bareFields.length.toString() + ` root field(s) carry no enforced auth directive, so they are\n reachable by ANY authenticated Cognito caller:\n ` + bareFields.join(", "));
|
|
279
|
+
}
|
|
280
|
+
if (bareTypes.length !== 0) {
|
|
281
|
+
problems.push(` ` + bareTypes.length.toString() + ` object type(s) carry no enforced auth directive:\n ` + bareTypes.join(", "));
|
|
282
|
+
}
|
|
283
|
+
if (problems.length !== 0) {
|
|
284
|
+
Stdlib_JsError.throwWithMessage(`Refusing to push an AppSync schema that cannot be gated.\n\n` + problems.join("\n\n") + `\n\nEvery field and type must carry @aws_cognito_user_pools (optionally with\ncognito_groups) and/or @aws_iam. The sweep in stampUndirectivedFields /\nstampAllTypesCognito should have covered this — a failure here means a schema\nreached assembly by a path that bypasses them.\nSee docs/plans/appsync-group-authorization-unenforced.md.`);
|
|
285
|
+
}
|
|
286
|
+
return sdl;
|
|
287
|
+
}
|
|
288
|
+
|
|
123
289
|
let canonicalTypeNames = [
|
|
124
290
|
"PageInfo",
|
|
125
291
|
"CommandAccepted",
|
|
@@ -167,6 +333,13 @@ export {
|
|
|
167
333
|
stampSharedIamTypes,
|
|
168
334
|
stampUndirectivedFields,
|
|
169
335
|
stampAllTypesCognito,
|
|
336
|
+
enforcedDirectives,
|
|
337
|
+
hasEnforcedDirective,
|
|
338
|
+
rootOperationTypes,
|
|
339
|
+
typeDeclNameOf,
|
|
340
|
+
fieldNameOf,
|
|
341
|
+
stampUndirectivedRootFields,
|
|
342
|
+
assertGateable,
|
|
170
343
|
canonicalTypeNames,
|
|
171
344
|
stampCanonicalTypes,
|
|
172
345
|
}
|
|
@@ -249,3 +249,58 @@ describe("AppSync_SdlDecorate.stampUndirectivedFields", () => {
|
|
|
249
249
|
)
|
|
250
250
|
})
|
|
251
251
|
})
|
|
252
|
+
|
|
253
|
+
// ── Deploy-time gate invariant ──────────────────────────────────────────────
|
|
254
|
+
//
|
|
255
|
+
// `defaultAction: DENY` — the request-time fail-closed switch — is unavailable:
|
|
256
|
+
// AWS refuses it alongside an additional auth provider, and every API here has
|
|
257
|
+
// AWS_IAM. So this check moves fail-closed into the deploy, where nothing can
|
|
258
|
+
// veto it: a schema that cannot be gated is never pushed.
|
|
259
|
+
|
|
260
|
+
describe("AppSync_SdlDecorate.assertGateable", () => {
|
|
261
|
+
let refuses = (sdl: string): bool =>
|
|
262
|
+
switch AppSync_SdlDecorate.assertGateable(sdl) {
|
|
263
|
+
| _ => false
|
|
264
|
+
| exception _ => true
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// The original outage, exactly. A "does it have a directive?" check passes
|
|
268
|
+
// this — @aws_auth was present on all 18 admin fields the whole time it was
|
|
269
|
+
// inert. Only knowing WHICH directive the service honours catches it.
|
|
270
|
+
testSync("refuses @aws_auth, the directive AppSync ignores here", () => {
|
|
271
|
+
let sdl = `type Query @aws_cognito_user_pools {\n Admin_Thing: String @aws_auth(cognito_groups: ["Admin"])\n}`
|
|
272
|
+
expect(refuses(sdl))->toBe(true)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
testSync("refuses a root field carrying no enforced directive", () => {
|
|
276
|
+
expect(refuses("type Query @aws_cognito_user_pools {\n Platform_ping: String\n}"))->toBe(true)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
testSync("refuses an object type carrying no enforced directive", () => {
|
|
280
|
+
let sdl =
|
|
281
|
+
"type Query @aws_cognito_user_pools {\n a: String @aws_cognito_user_pools\n}\ntype Product {\n id: ID!\n}"
|
|
282
|
+
expect(refuses(sdl))->toBe(true)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// Our real emission shape puts a mutation's directive on the FOLLOWING line.
|
|
286
|
+
testSync("accepts a directive on the line after the field", () => {
|
|
287
|
+
let sdl = "type Query @aws_cognito_user_pools {\n Platform_ping: String\n @aws_cognito_user_pools\n}"
|
|
288
|
+
expect(refuses(sdl))->toBe(false)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
testSync("accepts a group-gated field and an @aws_iam-only field", () => {
|
|
292
|
+
let sdl =
|
|
293
|
+
`type Mutation @aws_cognito_user_pools {\n Gated: String\n @aws_cognito_user_pools(cognito_groups: ["Admin"])\n Sys: String\n @aws_cognito_user_pools @aws_iam\n}`
|
|
294
|
+
expect(refuses(sdl))->toBe(false)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// The whole pipeline must satisfy its own invariant on the real admin base.
|
|
298
|
+
testSync("the assembled admin base passes", () => {
|
|
299
|
+
let decorated = AppSync_Adapter.injectAwsAuthAll(
|
|
300
|
+
ReventlessCore.Platform_AdminApi.baseFragment(~cloner=true),
|
|
301
|
+
~group="Admin",
|
|
302
|
+
)
|
|
303
|
+
let sdl = AppSync_Adapter.stitchStandaloneWithAwsDirectives(~fragment=decorated)
|
|
304
|
+
expect(sdl->String.includes("@aws_cognito_user_pools"))->toBe(true)
|
|
305
|
+
})
|
|
306
|
+
})
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
4
|
import * as AppSync_Adapter$ReventlessAws from "../src/components/Api/AppSync_Adapter.res.mjs";
|
|
5
5
|
import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
|
|
6
|
+
import * as Platform_AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminApi.res.mjs";
|
|
6
7
|
import * as AppSync_SdlDecorate$ReventlessAws from "../src/components/Api/AppSync_SdlDecorate.res.mjs";
|
|
7
8
|
|
|
8
9
|
let sources = [
|
|
@@ -192,6 +193,37 @@ globalThis.describe("AppSync_SdlDecorate.stampUndirectivedFields", () => {
|
|
|
192
193
|
});
|
|
193
194
|
});
|
|
194
195
|
|
|
196
|
+
globalThis.describe("AppSync_SdlDecorate.assertGateable", () => {
|
|
197
|
+
let refuses = sdl => {
|
|
198
|
+
try {
|
|
199
|
+
AppSync_SdlDecorate$ReventlessAws.assertGateable(sdl);
|
|
200
|
+
return false;
|
|
201
|
+
} catch (exn) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
globalThis.test("refuses @aws_auth, the directive AppSync ignores here", () => {
|
|
206
|
+
globalThis.expect(refuses(`type Query @aws_cognito_user_pools {\n Admin_Thing: String @aws_auth(cognito_groups: ["Admin"])\n}`)).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
globalThis.test("refuses a root field carrying no enforced directive", () => {
|
|
209
|
+
globalThis.expect(refuses("type Query @aws_cognito_user_pools {\n Platform_ping: String\n}")).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
globalThis.test("refuses an object type carrying no enforced directive", () => {
|
|
212
|
+
globalThis.expect(refuses("type Query @aws_cognito_user_pools {\n a: String @aws_cognito_user_pools\n}\ntype Product {\n id: ID!\n}")).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
globalThis.test("accepts a directive on the line after the field", () => {
|
|
215
|
+
globalThis.expect(refuses("type Query @aws_cognito_user_pools {\n Platform_ping: String\n @aws_cognito_user_pools\n}")).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
globalThis.test("accepts a group-gated field and an @aws_iam-only field", () => {
|
|
218
|
+
globalThis.expect(refuses(`type Mutation @aws_cognito_user_pools {\n Gated: String\n @aws_cognito_user_pools(cognito_groups: ["Admin"])\n Sys: String\n @aws_cognito_user_pools @aws_iam\n}`)).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
globalThis.test("the assembled admin base passes", () => {
|
|
221
|
+
let decorated = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(true), "Admin", undefined);
|
|
222
|
+
let sdl = AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(decorated);
|
|
223
|
+
globalThis.expect(sdl.includes("@aws_cognito_user_pools")).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
195
227
|
export {
|
|
196
228
|
sources,
|
|
197
229
|
stitchedSdl,
|