@reventlessdev/reventless-aws 3.0.0-alpha.294 → 3.0.0-alpha.295

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 CHANGED
@@ -3,6 +3,13 @@
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.295 (2026-08-14)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** enforce Cognito group gates that AppSync was silently ignoring ([1392638](https://github.com/ReventlessDev/reventless-core/commit/13926388c097500606f75c387630a19ee1a3aa71))
11
+
12
+
6
13
  # 3.0.0-alpha.294 (2026-08-14)
7
14
 
8
15
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.294",
3
+ "version": "3.0.0-alpha.295",
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.9",
16
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
15
+ "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.10",
17
16
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
18
- "@reventlessdev/rescript-node": "2.0.0-alpha.5",
19
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.74",
17
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
18
+ "@reventlessdev/rescript-node": "2.0.0-alpha.6",
19
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.75",
20
20
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
21
21
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
- "@reventlessdev/reventless-core": "3.0.0-alpha.232",
23
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.96",
24
- "@reventlessdev/reventless-infra": "3.0.0-alpha.139",
22
+ "@reventlessdev/reventless-core": "3.0.0-alpha.233",
23
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.97",
25
24
  "@reventlessdev/reventless-interop": "3.0.0-alpha.31",
26
- "@reventlessdev/reventless-spec": "3.0.0-alpha.112"
25
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.113",
26
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.140"
27
27
  },
28
28
  "devDependencies": {
29
29
  "rescript": "12.3.0",
@@ -6,13 +6,20 @@
6
6
  // ([Auth_ActiveRoleTrigger.res] bundles it and attaches it to the user pool).
7
7
  //
8
8
  // The narrowing has to happen here rather than in our own enforcement code
9
- // because `@authorize(AllowGroups([...]))` compiles to `@aws_auth`, which AppSync
9
+ // because `@authorize(AllowGroups([...]))` compiles to a field directive AppSync
10
10
  // evaluates against `cognito:groups` **before any of our code executes**. A
11
11
  // request header we invent could scope reads correctly and still leave every
12
12
  // group-gated mutation callable — right about the data, wrong about the writes.
13
13
  // Overriding the group claim is the one place every enforcement point already
14
14
  // looks.
15
15
  //
16
+ // That premise was false for as long as the directive was emitted as the
17
+ // single-mode `@aws_auth(...)`, which AppSync ignores on a multi-auth API — the
18
+ // gate was open and this trigger narrowed a claim nothing read for authorization.
19
+ // The directive is now `@aws_cognito_user_pools(...)`, which the service does
20
+ // enforce, so the reasoning above holds again. See
21
+ // `AppSync_SdlDecorate.formatCognitoGroupsDirective`.
22
+ //
16
23
  // **Version 1 of the trigger, deliberately.** Group override and added ID-token
17
24
  // claims are `V1_0` capabilities; `V2_0`/`V3_0` buy access-token customisation
18
25
  // this does not need and are gated behind the Essentials and Plus feature plans.
@@ -171,9 +171,9 @@ let rec waitForMergeSuccess = async (
171
171
  }
172
172
  }
173
173
 
174
- // ── @aws_auth directive injection ─────────────────────────────────────────
175
- // Injects @aws_auth(cognito_groups: [...]) directives into SDL field strings
176
- // based on authorization metadata from schema entries.
174
+ // ── Cognito group directive injection ─────────────────────────────────────
175
+ // Injects @aws_cognito_user_pools(cognito_groups: [...]) directives into SDL
176
+ // field strings based on authorization metadata from schema entries.
177
177
  //
178
178
  // Two sources are merged:
179
179
  // 1. The legacy `authorization?: {tableName, group}` field — single group
@@ -184,19 +184,22 @@ let rec waitForMergeSuccess = async (
184
184
  //
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
- // `@aws_auth(cognito_groups: ["g1", "g2", ...])`. `AllowAuthenticated` /
188
- // `AllowAnonymous` emit no directive (with Cognito as primary auth, any
189
- // reaching request is already authenticated). `AllowGroups([])` and
190
- // `DenyAll` emit a sentinel `__deny_all__` group that no Cognito user can
187
+ // `@aws_cognito_user_pools(cognito_groups: ["g1", "g2", ...])`.
188
+ // `AllowAuthenticated` / `AllowAnonymous` emit no directive (with Cognito as
189
+ // primary auth, any reaching request is already authenticated). `AllowGroups([])`
190
+ // and `DenyAll` emit a sentinel `__deny_all__` group that no Cognito user can
191
191
  // belong to — effectively blocking the field at the API layer.
192
192
  //
193
+ // NOT `@aws_auth(...)`: that is the single-mode form, which AppSync ignores on a
194
+ // multi-auth API — which every API this adapter provisions is. See
195
+ // `AppSync_SdlDecorate.formatCognitoGroupsDirective` for the full rationale and
196
+ // the observation that established it.
197
+ //
193
198
  // ── Dual-auth (Cognito + IAM) for deploy-time system callers ────────────────
194
199
  // A field flagged `systemCallable` (via `mutationSchemaEntry`/`querySchemaEntry`)
195
200
  // must be reachable by BOTH the console UI (Cognito) and a deploy-time system
196
201
  // caller signing SigV4 with ambient AWS credentials (`Util_AppSync_Caller`,
197
- // the AWS_IAM additional auth provider). `@aws_auth(...)` is the single-mode
198
- // directive form and does not admit IAM on a multi-auth API. For these fields
199
- // we emit the multi-auth form instead:
202
+ // the AWS_IAM additional auth provider). For these fields we append the IAM arm:
200
203
  // @aws_cognito_user_pools(cognito_groups: [...]) @aws_iam
201
204
  // The Cognito arm preserves the field's existing group gating (or, when the
202
205
  // field carries no Cognito group restriction, a bare `@aws_cognito_user_pools`
@@ -215,24 +218,17 @@ let _permissionToCognitoGroups = (
215
218
  | AllowAuthenticated | AllowAnonymous => None
216
219
  }
217
220
 
218
- let _formatGroupsDirective = (groups: array<string>): string => {
219
- let quoted = groups->Array.map(g => `"${g}"`)->Array.join(", ")
220
- `@aws_auth(cognito_groups: [${quoted}])`
221
- }
221
+ // Both directive forms are defined once in the runtime-pure AppSync_SdlDecorate
222
+ // so this deploy path and the bundled AdminEventCollector Lambda's reactive push
223
+ // cannot drift. The Cognito-only arm carries the rationale for why it is
224
+ // `@aws_cognito_user_pools(...)` and never `@aws_auth(...)`.
225
+ let _formatGroupsDirective = AppSync_SdlDecorate.formatCognitoGroupsDirective
222
226
 
223
227
  // Multi-auth directive for a field that must accept BOTH Cognito and IAM.
224
228
  // `groups=Some([...])` preserves Cognito group gating; `groups=None` keeps the
225
229
  // field open to any authenticated Cognito user. `@aws_iam` admits the
226
230
  // deploy-time SigV4 system caller. See the dual-auth note above.
227
- let _formatDualAuthDirective = (groups: option<array<string>>): string => {
228
- let cognito = switch groups {
229
- | Some(g) =>
230
- let quoted = g->Array.map(x => `"${x}"`)->Array.join(", ")
231
- `@aws_cognito_user_pools(cognito_groups: [${quoted}])`
232
- | None => `@aws_cognito_user_pools`
233
- }
234
- `${cognito} @aws_iam`
235
- }
231
+ let _formatDualAuthDirective = AppSync_SdlDecorate.formatDualAuthDirective
236
232
 
237
233
  // ── Type-level dual-auth ─────────────────────────────────────────────────────
238
234
  // On a multi-auth API, object TYPES without auth directives are accessible only
@@ -126,22 +126,6 @@ function _permissionToCognitoGroups(permission) {
126
126
  }
127
127
  }
128
128
 
129
- function _formatGroupsDirective(groups) {
130
- let quoted = groups.map(g => `"` + g + `"`).join(", ");
131
- return `@aws_auth(cognito_groups: [` + quoted + `])`;
132
- }
133
-
134
- function _formatDualAuthDirective(groups) {
135
- let cognito;
136
- if (groups !== undefined) {
137
- let quoted = groups.map(x => `"` + x + `"`).join(", ");
138
- cognito = `@aws_cognito_user_pools(cognito_groups: [` + quoted + `])`;
139
- } else {
140
- cognito = `@aws_cognito_user_pools`;
141
- }
142
- return cognito + ` @aws_iam`;
143
- }
144
-
145
129
  function _typeDeclName(decl) {
146
130
  if (!decl.startsWith("type ")) {
147
131
  return;
@@ -228,9 +212,9 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
228
212
  let fieldName = GraphQL_Stitcher$ReventlessCore.extractLeadingName(field);
229
213
  let groups = mutationAuthMap[fieldName];
230
214
  if (Stdlib_Option.getOr(iamFields[fieldName], false)) {
231
- return field + `\n ` + _formatDualAuthDirective(groups);
215
+ return field + `\n ` + AppSync_SdlDecorate$ReventlessAws.formatDualAuthDirective(groups);
232
216
  } else if (groups !== undefined) {
233
- return field + `\n ` + _formatGroupsDirective(groups);
217
+ return field + `\n ` + AppSync_SdlDecorate$ReventlessAws.formatCognitoGroupsDirective(groups);
234
218
  } else {
235
219
  return field;
236
220
  }
@@ -240,9 +224,9 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
240
224
  let groups = queryAuthMap[fieldName];
241
225
  let isIam = Stdlib_Option.getOr(iamFields[fieldName], false) || iamQueryFieldPrefixes.some(p => fieldName.startsWith(p));
242
226
  if (isIam) {
243
- return field + ` ` + _formatDualAuthDirective(groups);
227
+ return field + ` ` + AppSync_SdlDecorate$ReventlessAws.formatDualAuthDirective(groups);
244
228
  } else if (groups !== undefined) {
245
- return field + ` ` + _formatGroupsDirective(groups);
229
+ return field + ` ` + AppSync_SdlDecorate$ReventlessAws.formatCognitoGroupsDirective(groups);
246
230
  } else {
247
231
  return field;
248
232
  }
@@ -346,6 +330,10 @@ function generateFragment(mutationEntries, queryEntries) {
346
330
  return injectAwsAuth(fragment, mutationEntries, queryEntries);
347
331
  }
348
332
 
333
+ let _formatGroupsDirective = AppSync_SdlDecorate$ReventlessAws.formatCognitoGroupsDirective;
334
+
335
+ let _formatDualAuthDirective = AppSync_SdlDecorate$ReventlessAws.formatDualAuthDirective;
336
+
349
337
  let stampSharedIamTypes = AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes;
350
338
 
351
339
  let primaryAuthenticationType = "AMAZON_COGNITO_USER_POOLS";
@@ -67,26 +67,43 @@ let injectAwsSubscribe = (
67
67
  // the deploy path without dragging Pulumi into the runtime graph. AppSync_Adapter
68
68
  // delegates its `injectAwsAuthAll` / `stampSharedIamTypes` to these definitions.
69
69
 
70
+ // Cognito group gate. `@aws_cognito_user_pools(cognito_groups: [...])` — NOT the
71
+ // single-mode `@aws_auth(cognito_groups: [...])`.
72
+ //
73
+ // `@aws_auth` is honoured only on an API whose sole authorization mode is
74
+ // AMAZON_COGNITO_USER_POOLS. This adapter always configures AWS_IAM as an
75
+ // additional provider (see AppSync_Adapter's api args — the server-to-server
76
+ // lambdas need it), so every API we provision is multi-auth and the service
77
+ // **silently ignores** `@aws_auth`: the schema deploys, the directive shows up
78
+ // in the deployed SDL, and it gates nothing. What is left is
79
+ // `userPoolConfig.defaultAction: ALLOW`, which admits every authenticated
80
+ // Cognito user — a group gate that fails open.
81
+ //
82
+ // Confirmed by observation against a deployed API: a user in no groups read
83
+ // every `@aws_auth`-gated field and reached a gated mutation's handler. See
84
+ // docs/plans/appsync-group-authorization-unenforced.md.
85
+ let formatCognitoGroupsDirective = (groups: array<string>): string => {
86
+ let quoted = groups->Array.map(g => `"${g}"`)->Array.join(", ")
87
+ `@aws_cognito_user_pools(cognito_groups: [${quoted}])`
88
+ }
89
+
70
90
  // Multi-auth directive for a field/type that must accept BOTH Cognito and IAM.
71
91
  // `groups=Some([...])` preserves Cognito group gating; `groups=None` keeps it
72
92
  // open to any authenticated Cognito user. `@aws_iam` admits the deploy-time
73
93
  // SigV4 system caller.
74
94
  let formatDualAuthDirective = (groups: option<array<string>>): string => {
75
95
  let cognito = switch groups {
76
- | Some(g) =>
77
- let quoted = g->Array.map(x => `"${x}"`)->Array.join(", ")
78
- `@aws_cognito_user_pools(cognito_groups: [${quoted}])`
96
+ | Some(g) => formatCognitoGroupsDirective(g)
79
97
  | None => `@aws_cognito_user_pools`
80
98
  }
81
99
  `${cognito} @aws_iam`
82
100
  }
83
101
 
84
- // Injects @aws_auth with the given group on ALL mutation, query, and subscription
85
- // fields in a fragment. `~iamFieldNames` opts the named mutation/query fields into
86
- // deploy-time IAM (dual-auth): those fields emit
87
- // `@aws_cognito_user_pools(cognito_groups: ["<group>"]) @aws_iam` instead of the
88
- // single-mode `@aws_auth(...)`, keeping the same Cognito gating while also admitting
89
- // the SigV4 system caller. Subscriptions are never IAM-marked.
102
+ // Injects the Cognito group gate on ALL mutation, query, and subscription fields
103
+ // in a fragment. `~iamFieldNames` opts the named mutation/query fields into
104
+ // deploy-time IAM as well, appending the `@aws_iam` arm. Subscriptions are never
105
+ // IAM-marked (the deploy caller does not subscribe) but still carry the Cognito
106
+ // gate a subscription is a read.
90
107
  let injectAwsAuthAll = (
91
108
  fragment: Reventless.Plugin.apiSchemaFragment,
92
109
  ~group: string,
@@ -95,19 +112,18 @@ let injectAwsAuthAll = (
95
112
  let parts = ReventlessCore.GraphQL_Stitcher.decode(fragment)
96
113
  let isIam = (field: string): bool =>
97
114
  iamFieldNames->Array.includes(ReventlessCore.GraphQL_Stitcher.extractLeadingName(field))
115
+ let cognitoOnly = formatCognitoGroupsDirective([group])
98
116
 
99
117
  let augmentedMutations = parts.mutations->Array.map(field =>
100
118
  isIam(field)
101
119
  ? `${field}\n ${formatDualAuthDirective(Some([group]))}`
102
- : `${field}\n @aws_auth(cognito_groups: ["${group}"])`
120
+ : `${field}\n ${cognitoOnly}`
103
121
  )
104
122
  let augmentedQueries = parts.queries->Array.map(field =>
105
- isIam(field)
106
- ? `${field} ${formatDualAuthDirective(Some([group]))}`
107
- : `${field} @aws_auth(cognito_groups: ["${group}"])`
123
+ isIam(field) ? `${field} ${formatDualAuthDirective(Some([group]))}` : `${field} ${cognitoOnly}`
108
124
  )
109
125
  let augmentedSubscriptions = parts.subscriptions->Array.map(field =>
110
- `${field}\n @aws_auth(cognito_groups: ["${group}"])`
126
+ `${field}\n ${cognitoOnly}`
111
127
  )
112
128
 
113
129
  ReventlessCore.GraphQL_Stitcher.encode({
@@ -37,35 +37,35 @@ function injectAwsSubscribe(sdl, sources) {
37
37
  return before + decorated + after;
38
38
  }
39
39
 
40
+ function formatCognitoGroupsDirective(groups) {
41
+ let quoted = groups.map(g => `"` + g + `"`).join(", ");
42
+ return `@aws_cognito_user_pools(cognito_groups: [` + quoted + `])`;
43
+ }
44
+
40
45
  function formatDualAuthDirective(groups) {
41
- let cognito;
42
- if (groups !== undefined) {
43
- let quoted = groups.map(x => `"` + x + `"`).join(", ");
44
- cognito = `@aws_cognito_user_pools(cognito_groups: [` + quoted + `])`;
45
- } else {
46
- cognito = `@aws_cognito_user_pools`;
47
- }
46
+ let cognito = groups !== undefined ? formatCognitoGroupsDirective(groups) : `@aws_cognito_user_pools`;
48
47
  return cognito + ` @aws_iam`;
49
48
  }
50
49
 
51
50
  function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
52
51
  let iamFieldNames = iamFieldNamesOpt !== undefined ? iamFieldNamesOpt : [];
53
52
  let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
53
+ let cognitoOnly = formatCognitoGroupsDirective([group]);
54
54
  let augmentedMutations = parts.mutations.map(field => {
55
55
  if (iamFieldNames.includes(GraphQL_Stitcher$ReventlessCore.extractLeadingName(field))) {
56
56
  return field + `\n ` + formatDualAuthDirective([group]);
57
57
  } else {
58
- return field + `\n @aws_auth(cognito_groups: ["` + group + `"])`;
58
+ return field + `\n ` + cognitoOnly;
59
59
  }
60
60
  });
61
61
  let augmentedQueries = parts.queries.map(field => {
62
62
  if (iamFieldNames.includes(GraphQL_Stitcher$ReventlessCore.extractLeadingName(field))) {
63
63
  return field + ` ` + formatDualAuthDirective([group]);
64
64
  } else {
65
- return field + ` @aws_auth(cognito_groups: ["` + group + `"])`;
65
+ return field + ` ` + cognitoOnly;
66
66
  }
67
67
  });
68
- let augmentedSubscriptions = parts.subscriptions.map(field => field + `\n @aws_auth(cognito_groups: ["` + group + `"])`);
68
+ let augmentedSubscriptions = parts.subscriptions.map(field => field + `\n ` + cognitoOnly);
69
69
  return GraphQL_Stitcher$ReventlessCore.encode({
70
70
  types: parts.types,
71
71
  mutations: augmentedMutations,
@@ -125,6 +125,7 @@ function stampCanonicalTypes(sdl) {
125
125
 
126
126
  export {
127
127
  injectAwsSubscribe,
128
+ formatCognitoGroupsDirective,
128
129
  formatDualAuthDirective,
129
130
  injectAwsAuthAll,
130
131
  sharedIamTypeNames,
@@ -25,19 +25,19 @@ let decodeFragment = (fragment: Reventless.Plugin.apiSchemaFragment) =>
25
25
  describe("AppSync_Adapter.injectAwsAuthAll", () => {
26
26
  let baseFragment = ReventlessCore.Platform_AdminApi.baseFragment(~cloner=true)
27
27
 
28
- testSync("adds @aws_auth directive to all mutation fields", () => {
28
+ testSync("adds the Cognito group directive to all mutation fields", () => {
29
29
  let augmented = AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin")
30
30
  let parts = decodeFragment(augmented)
31
31
  parts.mutations->Array.forEach(field => {
32
- expect(field)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
32
+ expect(field)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
33
33
  })
34
34
  })
35
35
 
36
- testSync("adds @aws_auth directive to all query fields", () => {
36
+ testSync("adds the Cognito group directive to all query fields", () => {
37
37
  let augmented = AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin")
38
38
  let parts = decodeFragment(augmented)
39
39
  parts.queries->Array.forEach(field => {
40
- expect(field)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
40
+ expect(field)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
41
41
  })
42
42
  })
43
43
 
@@ -61,15 +61,15 @@ describe("AppSync_Adapter.injectAwsAuthAll", () => {
61
61
  // admin base. That caused AppSync to reject the auto-generated resolver for
62
62
  // `Subscription.onPlatform_Plugin_Activate` (and Deactivate) with "Type not
63
63
  // found" at deploy time. The subscription fields must survive the round-trip
64
- // and pick up the same @aws_auth group as mutations/queries.
65
- testSync("preserves subscription fields with @aws_auth directive", () => {
64
+ // and pick up the same Cognito group directive as mutations/queries.
65
+ testSync("preserves subscription fields with the Cognito group directive", () => {
66
66
  let original = decodeFragment(baseFragment)
67
67
  let augmented = AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin")
68
68
  let parts = decodeFragment(augmented)
69
69
  expect(parts.subscriptions->Array.length)->toBe(original.subscriptions->Array.length)
70
70
  expect(parts.subscriptions->Array.length)->toBeGreaterThan(0)
71
71
  parts.subscriptions->Array.forEach(field => {
72
- expect(field)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
72
+ expect(field)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
73
73
  })
74
74
  })
75
75
 
@@ -80,6 +80,21 @@ describe("AppSync_Adapter.injectAwsAuthAll", () => {
80
80
  expect(joined)->toContain("onPlatform_Plugin_Activate")
81
81
  expect(joined)->toContain("onPlatform_Plugin_Deactivate")
82
82
  })
83
+
84
+ // Regression: the admin base was stamped with the single-mode
85
+ // `@aws_auth(cognito_groups: [...])`, which AppSync IGNORES on a multi-auth API
86
+ // — and every API this adapter provisions is multi-auth. A deployed probe found
87
+ // a Cognito user in no groups reading every admin field. The directive must be
88
+ // `@aws_cognito_user_pools(...)`, the form the service actually enforces.
89
+ // See docs/plans/appsync-group-authorization-unenforced.md.
90
+ testSync("never emits the single-mode @aws_auth form on any field", () => {
91
+ let augmented = AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin")
92
+ let parts = decodeFragment(augmented)
93
+ let everyField =
94
+ Array.concatMany(parts.mutations, [parts.queries, parts.subscriptions])->Array.join("\n")
95
+ expect(everyField)->not_->toContain("@aws_auth")
96
+ expect(everyField)->toContain("@aws_cognito_user_pools")
97
+ })
83
98
  })
84
99
 
85
100
  describe("AppSync_Adapter.injectAwsAuth", () => {
@@ -97,12 +112,12 @@ describe("AppSync_Adapter.injectAwsAuth", () => {
97
112
  )
98
113
  let parts = decodeFragment(augmented)
99
114
 
100
- // Query entries have authorization → should have @aws_auth
115
+ // Query entries have authorization → should carry the directive
101
116
  let queryWithAuth =
102
117
  queryEntries->Array.some(entry => entry.authorization->Option.isSome)
103
118
  if queryWithAuth {
104
119
  let hasAuthQuery = parts.queries->Array.some(field =>
105
- field->String.includes("@aws_auth")
120
+ field->String.includes("@aws_cognito_user_pools")
106
121
  )
107
122
  expect(hasAuthQuery)->toBe(true)
108
123
  }
@@ -124,11 +139,11 @@ describe("AppSync_Adapter.generateFragment", () => {
124
139
  })
125
140
  })
126
141
 
127
- // ── Stage E2: spec-level permission → @aws_auth directive ───────────────
142
+ // ── Stage E2: spec-level permission → Cognito group directive ───────────────
128
143
  //
129
144
  // Verifies that `injectAwsAuth` lifts the `Authorization.permission` values
130
145
  // (set by Plugin_Builder / Dcb_Builder from `@@reventless.authorize` /
131
- // `@authorize` PPX annotations) into `@aws_auth(cognito_groups: [...])`
146
+ // `@authorize` PPX annotations) into `@aws_cognito_user_pools(cognito_groups: [...])`
132
147
  // directives on the corresponding SDL fields.
133
148
 
134
149
  describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () => {
@@ -182,7 +197,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
182
197
  )
183
198
  let parts = decodeFragment(aug)
184
199
  let m = parts.mutations->Array.getUnsafe(0)
185
- expect(m)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
200
+ expect(m)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
186
201
  })
187
202
 
188
203
  testSync("AllowGroups multi-group emits comma-separated groups", () => {
@@ -199,7 +214,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
199
214
  )
200
215
  let parts = decodeFragment(aug)
201
216
  expect(parts.mutations->Array.getUnsafe(0))->toContain(
202
- `@aws_auth(cognito_groups: ["Admin", "Editor"])`,
217
+ `@aws_cognito_user_pools(cognito_groups: ["Admin", "Editor"])`,
203
218
  )
204
219
  })
205
220
 
@@ -216,7 +231,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
216
231
  ~queryEntries=[],
217
232
  )
218
233
  let parts = decodeFragment(aug)
219
- expect(parts.mutations->Array.getUnsafe(0))->not_->toContain("@aws_auth")
234
+ expect(parts.mutations->Array.getUnsafe(0))->not_->toContain("@aws_cognito_user_pools")
220
235
  })
221
236
 
222
237
  testSync("DenyAll emits sentinel __deny_all__ group", () => {
@@ -258,7 +273,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
258
273
  | None => JsError.throwWithMessage("missing archive field")
259
274
  }
260
275
  switch add {
261
- | Some(f) => expect(f)->not_->toContain("@aws_auth")
276
+ | Some(f) => expect(f)->not_->toContain("@aws_cognito_user_pools")
262
277
  | None => JsError.throwWithMessage("missing add field")
263
278
  }
264
279
  })
@@ -344,7 +359,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
344
359
  ~queryEntries=[],
345
360
  )
346
361
  let parts = decodeFragment(aug)
347
- expect(parts.mutations->Array.getUnsafe(0))->not_->toContain("@aws_auth")
362
+ expect(parts.mutations->Array.getUnsafe(0))->not_->toContain("@aws_cognito_user_pools")
348
363
  })
349
364
  })
350
365
 
@@ -397,7 +412,7 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
397
412
  expect(m)->not_->toContain("@aws_auth")
398
413
  })
399
414
 
400
- testSync("a non-systemCallable sibling keeps single-mode @aws_auth (no @aws_iam)", () => {
415
+ testSync("a non-systemCallable sibling stays Cognito-only (no @aws_iam)", () => {
401
416
  let entry: ReventlessInfra.Api.mutationSchemaEntry = {
402
417
  fieldNames: ["p_Sync", "p_Other"],
403
418
  commandSchema: S.unknown,
@@ -420,7 +435,7 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
420
435
  let parts = decodeFragment(aug)
421
436
  switch parts.mutations->Array.find(f => f->String.includes("p_Other")) {
422
437
  | Some(other) =>
423
- expect(other)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
438
+ expect(other)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
424
439
  expect(other)->not_->toContain("@aws_iam")
425
440
  | None => JsError.throwWithMessage("missing p_Other field")
426
441
  }
@@ -588,13 +603,13 @@ describe("AppSync_Adapter.injectAwsAuthAll — ~iamFieldNames", () => {
588
603
  expect(field)->toContain("@aws_iam")
589
604
  expect(field)->not_->toContain("@aws_auth")
590
605
  } else {
591
- expect(field)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
606
+ expect(field)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
592
607
  expect(field)->not_->toContain("@aws_iam")
593
608
  }
594
609
  })
595
610
  })
596
611
 
597
- testSync("default (no ~iamFieldNames) leaves every field single-mode @aws_auth", () => {
612
+ testSync("default (no ~iamFieldNames) leaves every field Cognito-only", () => {
598
613
  let augmented = AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin")
599
614
  let parts = decodeFragment(augmented)
600
615
  parts.mutations->Array.forEach(field => expect(field)->not_->toContain("@aws_iam"))
@@ -29,18 +29,18 @@ let decodeFragment = GraphQL_Stitcher$ReventlessCore.decode;
29
29
 
30
30
  globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
31
31
  let baseFragment = Platform_AdminApi$ReventlessCore.baseFragment(true);
32
- globalThis.test("adds @aws_auth directive to all mutation fields", () => {
32
+ globalThis.test("adds the Cognito group directive to all mutation fields", () => {
33
33
  let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
34
34
  let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
35
35
  parts.mutations.forEach(field => {
36
- globalThis.expect(field).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
36
+ globalThis.expect(field).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
37
37
  });
38
38
  });
39
- globalThis.test("adds @aws_auth directive to all query fields", () => {
39
+ globalThis.test("adds the Cognito group directive to all query fields", () => {
40
40
  let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
41
41
  let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
42
42
  parts.queries.forEach(field => {
43
- globalThis.expect(field).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
43
+ globalThis.expect(field).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
44
44
  });
45
45
  });
46
46
  globalThis.test("preserves type definitions unchanged", () => {
@@ -56,14 +56,14 @@ globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
56
56
  globalThis.expect(field).toContain(`cognito_groups: ["SuperAdmin"]`);
57
57
  });
58
58
  });
59
- globalThis.test("preserves subscription fields with @aws_auth directive", () => {
59
+ globalThis.test("preserves subscription fields with the Cognito group directive", () => {
60
60
  let original = GraphQL_Stitcher$ReventlessCore.decode(baseFragment);
61
61
  let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
62
62
  let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
63
63
  globalThis.expect(parts.subscriptions.length).toBe(original.subscriptions.length);
64
64
  globalThis.expect(parts.subscriptions.length).toBeGreaterThan(0);
65
65
  parts.subscriptions.forEach(field => {
66
- globalThis.expect(field).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
66
+ globalThis.expect(field).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
67
67
  });
68
68
  });
69
69
  globalThis.test("admin Plugin aggregate subscription fields survive the round-trip", () => {
@@ -73,6 +73,13 @@ globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
73
73
  globalThis.expect(joined).toContain("onPlatform_Plugin_Activate");
74
74
  globalThis.expect(joined).toContain("onPlatform_Plugin_Deactivate");
75
75
  });
76
+ globalThis.test("never emits the single-mode @aws_auth form on any field", () => {
77
+ let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
78
+ let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
79
+ let everyField = parts.mutations.concat(parts.queries, parts.subscriptions).join("\n");
80
+ globalThis.expect(everyField).not.toContain("@aws_auth");
81
+ globalThis.expect(everyField).toContain("@aws_cognito_user_pools");
82
+ });
76
83
  });
77
84
 
78
85
  globalThis.describe("AppSync_Adapter.injectAwsAuth", () => {
@@ -85,7 +92,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth", () => {
85
92
  if (!queryWithAuth) {
86
93
  return;
87
94
  }
88
- let hasAuthQuery = parts.queries.some(field => field.includes("@aws_auth"));
95
+ let hasAuthQuery = parts.queries.some(field => field.includes("@aws_cognito_user_pools"));
89
96
  globalThis.expect(hasAuthQuery).toBe(true);
90
97
  });
91
98
  });
@@ -135,7 +142,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
135
142
  let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
136
143
  let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
137
144
  let m = parts.mutations[0];
138
- globalThis.expect(m).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
145
+ globalThis.expect(m).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
139
146
  });
140
147
  globalThis.test("AllowGroups multi-group emits comma-separated groups", () => {
141
148
  let fp = Object.fromEntries([[
@@ -152,7 +159,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
152
159
  let frag = makeFragment(["p_X(id: ID!): String"], []);
153
160
  let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
154
161
  let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
155
- globalThis.expect(parts.mutations[0]).toContain(`@aws_auth(cognito_groups: ["Admin", "Editor"])`);
162
+ globalThis.expect(parts.mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "Editor"])`);
156
163
  });
157
164
  globalThis.test("AllowAuthenticated emits no directive", () => {
158
165
  let fp = Object.fromEntries([[
@@ -163,7 +170,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
163
170
  let frag = makeFragment(["p_Add(id: ID!): String"], []);
164
171
  let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
165
172
  let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
166
- globalThis.expect(parts.mutations[0]).not.toContain("@aws_auth");
173
+ globalThis.expect(parts.mutations[0]).not.toContain("@aws_cognito_user_pools");
167
174
  });
168
175
  globalThis.test("DenyAll emits sentinel __deny_all__ group", () => {
169
176
  let fp = Object.fromEntries([[
@@ -208,7 +215,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
208
215
  Stdlib_JsError.throwWithMessage("missing archive field");
209
216
  }
210
217
  if (add !== undefined) {
211
- globalThis.expect(add).not.toContain("@aws_auth");
218
+ globalThis.expect(add).not.toContain("@aws_cognito_user_pools");
212
219
  return;
213
220
  } else {
214
221
  return Stdlib_JsError.throwWithMessage("missing add field");
@@ -282,7 +289,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
282
289
  let frag = makeFragment(["p_X(id: ID!): String"], []);
283
290
  let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
284
291
  let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
285
- globalThis.expect(parts.mutations[0]).not.toContain("@aws_auth");
292
+ globalThis.expect(parts.mutations[0]).not.toContain("@aws_cognito_user_pools");
286
293
  });
287
294
  });
288
295
 
@@ -337,7 +344,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
337
344
  globalThis.expect(m).not.toContain("cognito_groups");
338
345
  globalThis.expect(m).not.toContain("@aws_auth");
339
346
  });
340
- globalThis.test("a non-systemCallable sibling keeps single-mode @aws_auth (no @aws_iam)", () => {
347
+ globalThis.test("a non-systemCallable sibling stays Cognito-only (no @aws_iam)", () => {
341
348
  let entry_fieldNames = [
342
349
  "p_Sync",
343
350
  "p_Other"
@@ -391,7 +398,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
391
398
  let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
392
399
  let other = parts.mutations.find(f => f.includes("p_Other"));
393
400
  if (other !== undefined) {
394
- globalThis.expect(other).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
401
+ globalThis.expect(other).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
395
402
  globalThis.expect(other).not.toContain("@aws_iam");
396
403
  return;
397
404
  } else {
@@ -548,12 +555,12 @@ globalThis.describe("AppSync_Adapter.injectAwsAuthAll — ~iamFieldNames", () =>
548
555
  globalThis.expect(field).toContain("@aws_iam");
549
556
  globalThis.expect(field).not.toContain("@aws_auth");
550
557
  } else {
551
- globalThis.expect(field).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
558
+ globalThis.expect(field).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
552
559
  globalThis.expect(field).not.toContain("@aws_iam");
553
560
  }
554
561
  });
555
562
  });
556
- globalThis.test("default (no ~iamFieldNames) leaves every field single-mode @aws_auth", () => {
563
+ globalThis.test("default (no ~iamFieldNames) leaves every field Cognito-only", () => {
557
564
  let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
558
565
  let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
559
566
  parts.mutations.forEach(field => {
@@ -31,7 +31,7 @@ type Mutation {
31
31
  type Subscription {
32
32
  onCatalog_AddProduct(id: ID): CommandResult
33
33
  onUIFragmentChange: UIFragmentChangeEvent
34
- @aws_auth(cognito_groups: ["Admin"])
34
+ @aws_cognito_user_pools(cognito_groups: ["Admin"])
35
35
  onCatalogEventLog_eventAppended: CatalogEventLogEvent
36
36
  }`
37
37
 
@@ -43,13 +43,13 @@ describe("AppSync_SdlDecorate.injectAwsSubscribe", () => {
43
43
  )
44
44
  })
45
45
 
46
- testSync("appends the many-mutations fan-in on a no-arg field, keeping @aws_auth", () => {
46
+ testSync("appends the many-mutations fan-in on a no-arg field, keeping the group directive", () => {
47
47
  let sdl = AppSync_SdlDecorate.injectAwsSubscribe(stitchedSdl, ~sources)
48
48
  expect(sdl)->toContain(
49
49
  `onUIFragmentChange: UIFragmentChangeEvent\n @aws_subscribe(mutations: ["Platform_UIFragmentRegistered", "Platform_UIFragmentUpdated", "Platform_UIFragmentDeregistered"])`,
50
50
  )
51
51
  // The auth directive line appended by injectAwsAuthAll survives.
52
- expect(sdl)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
52
+ expect(sdl)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
53
53
  })
54
54
 
55
55
  testSync("leaves unmapped fields (Source A) and non-Subscription blocks untouched", () => {
@@ -113,7 +113,7 @@ describe("AppSync_SdlDecorate.injectAwsAuthAll", () => {
113
113
  expect(parts.mutations->Array.getUnsafe(0))->toContain("@aws_iam")
114
114
  // A query NOT in iamFieldNames stays Cognito-only.
115
115
  let other = queryField("Other_Query")
116
- expect(other)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
116
+ expect(other)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
117
117
  expect(other->String.includes("@aws_iam"))->toBe(false)
118
118
  })
119
119
  })
@@ -35,7 +35,7 @@ type Mutation {
35
35
  type Subscription {
36
36
  onCatalog_AddProduct(id: ID): CommandResult
37
37
  onUIFragmentChange: UIFragmentChangeEvent
38
- @aws_auth(cognito_groups: ["Admin"])
38
+ @aws_cognito_user_pools(cognito_groups: ["Admin"])
39
39
  onCatalogEventLog_eventAppended: CatalogEventLogEvent
40
40
  }`;
41
41
 
@@ -44,10 +44,10 @@ globalThis.describe("AppSync_SdlDecorate.injectAwsSubscribe", () => {
44
44
  let sdl = AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(stitchedSdl, sources);
45
45
  globalThis.expect(sdl).toContain(`onCatalog_AddProduct(id: ID): CommandResult\n @aws_subscribe(mutations: ["Catalog_AddProduct"])`);
46
46
  });
47
- globalThis.test("appends the many-mutations fan-in on a no-arg field, keeping @aws_auth", () => {
47
+ globalThis.test("appends the many-mutations fan-in on a no-arg field, keeping the group directive", () => {
48
48
  let sdl = AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(stitchedSdl, sources);
49
49
  globalThis.expect(sdl).toContain(`onUIFragmentChange: UIFragmentChangeEvent\n @aws_subscribe(mutations: ["Platform_UIFragmentRegistered", "Platform_UIFragmentUpdated", "Platform_UIFragmentDeregistered"])`);
50
- globalThis.expect(sdl).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
50
+ globalThis.expect(sdl).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
51
51
  });
52
52
  globalThis.test("leaves unmapped fields (Source A) and non-Subscription blocks untouched", () => {
53
53
  let sdl = AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(stitchedSdl, sources);
@@ -100,7 +100,7 @@ globalThis.describe("AppSync_SdlDecorate.injectAwsAuthAll", () => {
100
100
  globalThis.expect(queryField("Platform_ApiFragments")).toContain("@aws_iam");
101
101
  globalThis.expect(parts.mutations[0]).toContain("@aws_iam");
102
102
  let other = queryField("Other_Query");
103
- globalThis.expect(other).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
103
+ globalThis.expect(other).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
104
104
  globalThis.expect(other.includes("@aws_iam")).toBe(false);
105
105
  });
106
106
  });