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

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,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.297 (2026-08-14)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** sweep undirectived fields at the schema assembly choke point ([f0ed736](https://github.com/ReventlessDev/reventless-core/commit/f0ed7369ec4dddfb697a3c93115b12f310a491b0))
11
+
12
+
13
+ # 3.0.0-alpha.296 (2026-08-14)
14
+
15
+ ### Features
16
+
17
+ * **aws:** give every field and type an explicit Cognito directive ([37c1063](https://github.com/ReventlessDev/reventless-core/commit/37c1063560aaa6ee200e74d58b9de4d6881441fd))
18
+
19
+
6
20
  # 3.0.0-alpha.295 (2026-08-14)
7
21
 
8
22
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.295",
3
+ "version": "3.0.0-alpha.297",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -18,12 +18,12 @@
18
18
  "@reventlessdev/rescript-node": "2.0.0-alpha.6",
19
19
  "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.75",
20
20
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
21
- "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
21
  "@reventlessdev/reventless-core": "3.0.0-alpha.233",
22
+ "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
23
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.140",
23
24
  "@reventlessdev/reventless-postgres": "3.0.0-alpha.97",
24
- "@reventlessdev/reventless-interop": "3.0.0-alpha.31",
25
25
  "@reventlessdev/reventless-spec": "3.0.0-alpha.113",
26
- "@reventlessdev/reventless-infra": "3.0.0-alpha.140"
26
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.31"
27
27
  },
28
28
  "devDependencies": {
29
29
  "rescript": "12.3.0",
@@ -355,6 +355,12 @@ let injectAwsAuth = (
355
355
  }
356
356
  })
357
357
 
358
+ // A field with no group restriction gets the group-less Cognito directive
359
+ // rather than no directive at all. Same reachability under `defaultAction:
360
+ // ALLOW`; the difference only shows once the default is DENY, where an
361
+ // unstamped field is refused. See AppSync_SdlDecorate.cognitoOpenDirective.
362
+ let openDirective = AppSync_SdlDecorate.cognitoOpenDirective
363
+
358
364
  let augmentedMutations = parts.mutations->Array.map(field => {
359
365
  let fieldName = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
360
366
  let groups = mutationAuthMap->Dict.get(fieldName)
@@ -363,7 +369,7 @@ let injectAwsAuth = (
363
369
  } else {
364
370
  switch groups {
365
371
  | Some(groups) => `${field}\n ${_formatGroupsDirective(groups)}`
366
- | None => field
372
+ | None => `${field}\n ${openDirective}`
367
373
  }
368
374
  }
369
375
  })
@@ -379,11 +385,23 @@ let injectAwsAuth = (
379
385
  } else {
380
386
  switch groups {
381
387
  | Some(groups) => `${field} ${_formatGroupsDirective(groups)}`
382
- | None => field
388
+ | None => `${field} ${openDirective}`
383
389
  }
384
390
  }
385
391
  })
386
392
 
393
+ // Subscriptions carried no directive at all on this path — they are reads and
394
+ // need one for the same reason queries do. A subscription is never IAM-marked
395
+ // (the deploy caller does not subscribe), so it takes the Cognito arm only:
396
+ // the mutation's groups where the subscribed field has them, otherwise open.
397
+ let augmentedSubscriptions = parts.subscriptions->Array.map(field => {
398
+ let fieldName = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
399
+ switch mutationAuthMap->Dict.get(fieldName) {
400
+ | Some(groups) => `${field}\n ${_formatGroupsDirective(groups)}`
401
+ | None => `${field}\n ${openDirective}`
402
+ }
403
+ })
404
+
387
405
  // Type-level dual-auth for every type the callable entries' responses reach —
388
406
  // the node type, its Connection/Edge wrappers, and nested state types all
389
407
  // share the entry's returnTypeName prefix.
@@ -400,6 +418,7 @@ let injectAwsAuth = (
400
418
  types: augmentedTypes,
401
419
  mutations: augmentedMutations,
402
420
  queries: augmentedQueries,
421
+ subscriptions: augmentedSubscriptions,
403
422
  })
404
423
  }
405
424
 
@@ -441,9 +460,16 @@ let stitchStandaloneWithAwsDirectives = (
441
460
  ~baseFragment=fragment,
442
461
  ~pluginFragments=[],
443
462
  )
463
+ // Every AWS source-API document assembles through here, so this is the one
464
+ // place that can guarantee no field or type reaches a deployed schema without
465
+ // an auth directive — whichever path injected it.
466
+ let fragment = AppSync_SdlDecorate.stampUndirectivedFields(fragment)
444
467
  ReventlessCore.GraphQL_Stitcher.stitchStandalone(~fragment)
445
468
  ->AppSync_SdlDecorate.injectAwsSubscribe(~sources)
446
469
  ->stampSharedIamTypes
470
+ // Last: every remaining undirectived type takes the group-less Cognito arm.
471
+ // After stampSharedIamTypes so the shared traversal types keep `@aws_iam`.
472
+ ->AppSync_SdlDecorate.stampAllTypesCognito
447
473
  }
448
474
 
449
475
  // ── Provider implementation ────────────────────────────────────────────────
@@ -216,7 +216,7 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
216
216
  } else if (groups !== undefined) {
217
217
  return field + `\n ` + AppSync_SdlDecorate$ReventlessAws.formatCognitoGroupsDirective(groups);
218
218
  } else {
219
- return field;
219
+ return field + `\n ` + AppSync_SdlDecorate$ReventlessAws.cognitoOpenDirective;
220
220
  }
221
221
  });
222
222
  let augmentedQueries = parts.queries.map(field => {
@@ -228,7 +228,16 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
228
228
  } else if (groups !== undefined) {
229
229
  return field + ` ` + AppSync_SdlDecorate$ReventlessAws.formatCognitoGroupsDirective(groups);
230
230
  } else {
231
- return field;
231
+ return field + ` ` + AppSync_SdlDecorate$ReventlessAws.cognitoOpenDirective;
232
+ }
233
+ });
234
+ let augmentedSubscriptions = parts.subscriptions.map(field => {
235
+ let fieldName = GraphQL_Stitcher$ReventlessCore.extractLeadingName(field);
236
+ let groups = mutationAuthMap[fieldName];
237
+ if (groups !== undefined) {
238
+ return field + `\n ` + AppSync_SdlDecorate$ReventlessAws.formatCognitoGroupsDirective(groups);
239
+ } else {
240
+ return field + `\n ` + AppSync_SdlDecorate$ReventlessAws.cognitoOpenDirective;
232
241
  }
233
242
  });
234
243
  let augmentedTypes = parts.types.map(decl => {
@@ -243,7 +252,7 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
243
252
  types: augmentedTypes,
244
253
  mutations: augmentedMutations,
245
254
  queries: augmentedQueries,
246
- subscriptions: parts.subscriptions,
255
+ subscriptions: augmentedSubscriptions,
247
256
  subscriptionSources: parts.subscriptionSources
248
257
  });
249
258
  }
@@ -255,7 +264,8 @@ function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
255
264
 
256
265
  function stitchStandaloneWithAwsDirectives(fragment) {
257
266
  let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(fragment, []);
258
- return AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(GraphQL_Stitcher$ReventlessCore.stitchStandalone(fragment), sources));
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)));
259
269
  }
260
270
 
261
271
  function _makeApiResourceWith(name, schema, userPoolConfig, opts) {
@@ -87,6 +87,18 @@ let formatCognitoGroupsDirective = (groups: array<string>): string => {
87
87
  `@aws_cognito_user_pools(cognito_groups: [${quoted}])`
88
88
  }
89
89
 
90
+ // The group-less Cognito directive: reachable by any authenticated Cognito
91
+ // caller. Emitted where a field or type is deliberately open — `AllowAuthenticated`,
92
+ // `AllowAnonymous`, a field carrying no permission at all, and every object type.
93
+ //
94
+ // Under `userPoolConfig.defaultAction: ALLOW` this is a no-op: an undirectived
95
+ // field is already reachable by any authenticated caller, so stamping it changes
96
+ // nothing. It exists so the default can be flipped to DENY, where "no directive"
97
+ // stops meaning "open" and starts meaning "refused" — at which point anything
98
+ // left unstamped breaks. Stamping first and flipping second is what keeps that
99
+ // flip from being a guess.
100
+ let cognitoOpenDirective = "@aws_cognito_user_pools"
101
+
90
102
  // Multi-auth directive for a field/type that must accept BOTH Cognito and IAM.
91
103
  // `groups=Some([...])` preserves Cognito group gating; `groups=None` keeps it
92
104
  // open to any authenticated Cognito user. `@aws_iam` admits the deploy-time
@@ -94,7 +106,7 @@ let formatCognitoGroupsDirective = (groups: array<string>): string => {
94
106
  let formatDualAuthDirective = (groups: option<array<string>>): string => {
95
107
  let cognito = switch groups {
96
108
  | Some(g) => formatCognitoGroupsDirective(g)
97
- | None => `@aws_cognito_user_pools`
109
+ | None => cognitoOpenDirective
98
110
  }
99
111
  `${cognito} @aws_iam`
100
112
  }
@@ -150,6 +162,69 @@ let stampSharedIamTypes = (sdl: string): string =>
150
162
  acc->String.replace(`type ${name} {`, `type ${name} @aws_cognito_user_pools @aws_iam {`)
151
163
  )
152
164
 
165
+ // Final field sweep, run at the assembly choke point on the FRAGMENT (where each
166
+ // element is one whole field, so "does it already carry a directive?" is a
167
+ // reliable question — on raw SDL text a directive placed on the following line
168
+ // would read as absent).
169
+ //
170
+ // `injectAwsAuth` only decorates fields it can pair with a schema entry, and
171
+ // several surfaces never reach it at all: the domain base document
172
+ // (`Platform_ping`), event-history queries, event-log `…_eventAppended`
173
+ // subscriptions, the upload presign/release mutations, `Platform_SetActiveRole`,
174
+ // and `geocode`. Those deployed with no directive — invisible under
175
+ // `defaultAction: ALLOW`, refused under DENY. Sweeping here rather than at each
176
+ // emission site means a future injected field cannot silently miss the net.
177
+ //
178
+ // Idempotent, and deliberately does not second-guess gating: it only fills in
179
+ // "open to any authenticated Cognito caller", which is what an undirectived
180
+ // field already means today.
181
+ let stampUndirectivedFields = (
182
+ fragment: Reventless.Plugin.apiSchemaFragment,
183
+ ): Reventless.Plugin.apiSchemaFragment => {
184
+ let parts = ReventlessCore.GraphQL_Stitcher.decode(fragment)
185
+ let stamp = (field: string): string =>
186
+ field->String.includes("@aws_") ? field : `${field}\n ${cognitoOpenDirective}`
187
+
188
+ ReventlessCore.GraphQL_Stitcher.encode({
189
+ ...parts,
190
+ mutations: parts.mutations->Array.map(stamp),
191
+ queries: parts.queries->Array.map(stamp),
192
+ subscriptions: parts.subscriptions->Array.map(stamp),
193
+ })
194
+ }
195
+
196
+ // Stamp every object `type` declaration that does not already carry an auth
197
+ // directive with the group-less Cognito arm. Run on the ASSEMBLED SDL, AFTER
198
+ // stampSharedIamTypes so the shared traversal types keep their `@aws_iam` arm
199
+ // (this pass skips anything already carrying an `@aws_` directive).
200
+ //
201
+ // Types, not just fields: on a multi-auth API with `defaultAction: DENY`,
202
+ // AppSync refuses an undirectived TYPE even when the caller passed the field's
203
+ // own gate — response shaping walks `…Connection` → `…Edge` → node → nested
204
+ // state types and dies one level in. Entry gating stays on the fields; a type
205
+ // stamp only restores the accessibility an undirectived type has today.
206
+ //
207
+ // Only `type` declarations take auth directives — `input`, `enum`, `union` and
208
+ // `interface` do not, and stamping them is a schema error.
209
+ let stampAllTypesCognito = (sdl: string): string =>
210
+ sdl
211
+ ->String.split("\n")
212
+ ->Array.map(line =>
213
+ if line->String.startsWith("type ") && !(line->String.includes("@aws_")) {
214
+ switch line->String.indexOfOpt("{") {
215
+ | Some(i) =>
216
+ line->String.slice(~start=0, ~end=i) ++
217
+ cognitoOpenDirective ++
218
+ " " ++
219
+ line->String.slice(~start=i, ~end=line->String.length)
220
+ | None => line
221
+ }
222
+ } else {
223
+ line
224
+ }
225
+ )
226
+ ->Array.join("\n")
227
+
153
228
  // ── Merged-API canonical stamping ─────────────────────────────────────────────
154
229
  // Under AppSync Merged APIs the admin source API owns the shared traversal
155
230
  // types; `@canonical` makes its definition win over every plugin source's copy
@@ -42,8 +42,10 @@ function formatCognitoGroupsDirective(groups) {
42
42
  return `@aws_cognito_user_pools(cognito_groups: [` + quoted + `])`;
43
43
  }
44
44
 
45
+ let cognitoOpenDirective = "@aws_cognito_user_pools";
46
+
45
47
  function formatDualAuthDirective(groups) {
46
- let cognito = groups !== undefined ? formatCognitoGroupsDirective(groups) : `@aws_cognito_user_pools`;
48
+ let cognito = groups !== undefined ? formatCognitoGroupsDirective(groups) : cognitoOpenDirective;
47
49
  return cognito + ` @aws_iam`;
48
50
  }
49
51
 
@@ -86,6 +88,38 @@ function stampSharedIamTypes(sdl) {
86
88
  return Stdlib_Array.reduce(sharedIamTypeNames, sdl, (acc, name) => acc.replace(`type ` + name + ` {`, `type ` + name + ` @aws_cognito_user_pools @aws_iam {`));
87
89
  }
88
90
 
91
+ function stampUndirectivedFields(fragment) {
92
+ let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
93
+ let stamp = field => {
94
+ if (field.includes("@aws_")) {
95
+ return field;
96
+ } else {
97
+ return field + `\n ` + cognitoOpenDirective;
98
+ }
99
+ };
100
+ return GraphQL_Stitcher$ReventlessCore.encode({
101
+ types: parts.types,
102
+ mutations: parts.mutations.map(stamp),
103
+ queries: parts.queries.map(stamp),
104
+ subscriptions: parts.subscriptions.map(stamp),
105
+ subscriptionSources: parts.subscriptionSources
106
+ });
107
+ }
108
+
109
+ function stampAllTypesCognito(sdl) {
110
+ return sdl.split("\n").map(line => {
111
+ if (!(line.startsWith("type ") && !line.includes("@aws_"))) {
112
+ return line;
113
+ }
114
+ let i = Stdlib_String.indexOfOpt(line, "{");
115
+ if (i !== undefined) {
116
+ return line.slice(0, i) + cognitoOpenDirective + " " + line.slice(i, line.length);
117
+ } else {
118
+ return line;
119
+ }
120
+ }).join("\n");
121
+ }
122
+
89
123
  let canonicalTypeNames = [
90
124
  "PageInfo",
91
125
  "CommandAccepted",
@@ -126,10 +160,13 @@ function stampCanonicalTypes(sdl) {
126
160
  export {
127
161
  injectAwsSubscribe,
128
162
  formatCognitoGroupsDirective,
163
+ cognitoOpenDirective,
129
164
  formatDualAuthDirective,
130
165
  injectAwsAuthAll,
131
166
  sharedIamTypeNames,
132
167
  stampSharedIamTypes,
168
+ stampUndirectivedFields,
169
+ stampAllTypesCognito,
133
170
  canonicalTypeNames,
134
171
  stampCanonicalTypes,
135
172
  }
@@ -218,7 +218,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
218
218
  )
219
219
  })
220
220
 
221
- testSync("AllowAuthenticated emits no directive", () => {
221
+ testSync("AllowAuthenticated emits the group-less Cognito directive", () => {
222
222
  let fp = Dict.fromArray([(
223
223
  "p_Add",
224
224
  Reventless.Authorization.AllowAuthenticated,
@@ -231,7 +231,11 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
231
231
  ~queryEntries=[],
232
232
  )
233
233
  let parts = decodeFragment(aug)
234
- expect(parts.mutations->Array.getUnsafe(0))->not_->toContain("@aws_cognito_user_pools")
234
+ // Open, not ungated: the field carries the group-less directive so it stays
235
+ // reachable once defaultAction flips to DENY, but names no group.
236
+ let m = parts.mutations->Array.getUnsafe(0)
237
+ expect(m)->toContain("@aws_cognito_user_pools")
238
+ expect(m)->not_->toContain("cognito_groups")
235
239
  })
236
240
 
237
241
  testSync("DenyAll emits sentinel __deny_all__ group", () => {
@@ -273,7 +277,9 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
273
277
  | None => JsError.throwWithMessage("missing archive field")
274
278
  }
275
279
  switch add {
276
- | Some(f) => expect(f)->not_->toContain("@aws_cognito_user_pools")
280
+ | Some(f) =>
281
+ expect(f)->toContain("@aws_cognito_user_pools")
282
+ expect(f)->not_->toContain("cognito_groups")
277
283
  | None => JsError.throwWithMessage("missing add field")
278
284
  }
279
285
  })
@@ -338,7 +344,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
338
344
  expect(m)->not_->toContain(`"Admin"`)
339
345
  })
340
346
 
341
- testSync("AllowAuthenticated on a field overrides the legacy authorization (removes directive)", () => {
347
+ testSync("AllowAuthenticated on a field overrides the legacy authorization (drops the group)", () => {
342
348
  let fp = Dict.fromArray([(
343
349
  "p_X",
344
350
  Reventless.Authorization.AllowAuthenticated,
@@ -359,7 +365,11 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
359
365
  ~queryEntries=[],
360
366
  )
361
367
  let parts = decodeFragment(aug)
362
- expect(parts.mutations->Array.getUnsafe(0))->not_->toContain("@aws_cognito_user_pools")
368
+ // The spec-level AllowAuthenticated wins over the legacy `authorization.group`:
369
+ // the field keeps a directive, but no longer names a group.
370
+ let m = parts.mutations->Array.getUnsafe(0)
371
+ expect(m)->toContain("@aws_cognito_user_pools")
372
+ expect(m)->not_->toContain("cognito_groups")
363
373
  })
364
374
  })
365
375
 
@@ -851,3 +861,56 @@ describe("AppSync_Adapter.waitForMergeSuccess", () => {
851
861
  expect(message.contents)->toContain("MERGE_IN_PROGRESS")
852
862
  })
853
863
  })
864
+
865
+ // ── Subscription stamping ───────────────────────────────────────────────────
866
+ //
867
+ // `injectAwsAuth` re-encoded `subscriptions` untouched, so every plugin
868
+ // subscription reached the deployed SDL with no auth directive at all — 22 of
869
+ // them on the hybrid domain API. Harmless under `defaultAction: ALLOW`, refused
870
+ // under DENY. A subscription is a read and takes the Cognito arm; it is never
871
+ // IAM-marked, because the deploy-time caller does not subscribe.
872
+
873
+ describe("AppSync_Adapter.injectAwsAuth — subscriptions", () => {
874
+ let makeFragment = subscriptionFields =>
875
+ ReventlessCore.GraphQL_Stitcher.encode({
876
+ types: [],
877
+ mutations: ["p_Ship(id: ID!): CommandResult!"],
878
+ queries: [],
879
+ subscriptions: subscriptionFields,
880
+ subscriptionSources: [],
881
+ })
882
+
883
+ let entry = (~fieldNames, ~fieldPermissions): ReventlessInfra.Api.mutationSchemaEntry => {
884
+ fieldNames,
885
+ commandSchema: S.unknown,
886
+ fieldPermissions,
887
+ }
888
+
889
+ testSync("an unannotated subscription gets the group-less Cognito directive", () => {
890
+ let frag = makeFragment(["onSomething: CommandResult"])
891
+ let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[], ~queryEntries=[])
892
+ let s = decodeFragment(aug).subscriptions->Array.getUnsafe(0)
893
+ expect(s)->toContain("@aws_cognito_user_pools")
894
+ expect(s)->not_->toContain("cognito_groups")
895
+ expect(s)->not_->toContain("@aws_iam")
896
+ })
897
+
898
+ testSync("a subscription named like a gated mutation inherits its groups", () => {
899
+ let fp = Dict.fromArray([("p_Ship", Reventless.Authorization.AllowGroups(["Fulfilment"]))])
900
+ let frag = makeFragment(["p_Ship: CommandResult"])
901
+ let aug = AppSync_Adapter.injectAwsAuth(
902
+ frag,
903
+ ~mutationEntries=[entry(~fieldNames=["p_Ship"], ~fieldPermissions=fp)],
904
+ ~queryEntries=[],
905
+ )
906
+ let s = decodeFragment(aug).subscriptions->Array.getUnsafe(0)
907
+ expect(s)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Fulfilment"])`)
908
+ expect(s)->not_->toContain("@aws_iam")
909
+ })
910
+
911
+ testSync("subscription fields survive the round-trip", () => {
912
+ let frag = makeFragment(["onA: CommandResult", "onB: CommandResult"])
913
+ let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[], ~queryEntries=[])
914
+ expect(decodeFragment(aug).subscriptions->Array.length)->toBe(2)
915
+ })
916
+ })
@@ -161,7 +161,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
161
161
  let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
162
162
  globalThis.expect(parts.mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "Editor"])`);
163
163
  });
164
- globalThis.test("AllowAuthenticated emits no directive", () => {
164
+ globalThis.test("AllowAuthenticated emits the group-less Cognito directive", () => {
165
165
  let fp = Object.fromEntries([[
166
166
  "p_Add",
167
167
  "AllowAuthenticated"
@@ -170,7 +170,9 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
170
170
  let frag = makeFragment(["p_Add(id: ID!): String"], []);
171
171
  let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
172
172
  let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
173
- globalThis.expect(parts.mutations[0]).not.toContain("@aws_cognito_user_pools");
173
+ let m = parts.mutations[0];
174
+ globalThis.expect(m).toContain("@aws_cognito_user_pools");
175
+ globalThis.expect(m).not.toContain("cognito_groups");
174
176
  });
175
177
  globalThis.test("DenyAll emits sentinel __deny_all__ group", () => {
176
178
  let fp = Object.fromEntries([[
@@ -215,7 +217,8 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
215
217
  Stdlib_JsError.throwWithMessage("missing archive field");
216
218
  }
217
219
  if (add !== undefined) {
218
- globalThis.expect(add).not.toContain("@aws_cognito_user_pools");
220
+ globalThis.expect(add).toContain("@aws_cognito_user_pools");
221
+ globalThis.expect(add).not.toContain("cognito_groups");
219
222
  return;
220
223
  } else {
221
224
  return Stdlib_JsError.throwWithMessage("missing add field");
@@ -269,7 +272,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
269
272
  globalThis.expect(m).toContain(`cognito_groups: ["Manager"]`);
270
273
  globalThis.expect(m).not.toContain(`"Admin"`);
271
274
  });
272
- globalThis.test("AllowAuthenticated on a field overrides the legacy authorization (removes directive)", () => {
275
+ globalThis.test("AllowAuthenticated on a field overrides the legacy authorization (drops the group)", () => {
273
276
  let fp = Object.fromEntries([[
274
277
  "p_X",
275
278
  "AllowAuthenticated"
@@ -289,7 +292,9 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
289
292
  let frag = makeFragment(["p_X(id: ID!): String"], []);
290
293
  let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
291
294
  let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
292
- globalThis.expect(parts.mutations[0]).not.toContain("@aws_cognito_user_pools");
295
+ let m = parts.mutations[0];
296
+ globalThis.expect(m).toContain("@aws_cognito_user_pools");
297
+ globalThis.expect(m).not.toContain("cognito_groups");
293
298
  });
294
299
  });
295
300
 
@@ -746,6 +751,51 @@ globalThis.describe("AppSync_Adapter.waitForMergeSuccess", () => {
746
751
  });
747
752
  });
748
753
 
754
+ globalThis.describe("AppSync_Adapter.injectAwsAuth — subscriptions", () => {
755
+ let makeFragment = subscriptionFields => GraphQL_Stitcher$ReventlessCore.encode({
756
+ types: [],
757
+ mutations: ["p_Ship(id: ID!): CommandResult!"],
758
+ queries: [],
759
+ subscriptions: subscriptionFields,
760
+ subscriptionSources: []
761
+ });
762
+ let entry = (fieldNames, fieldPermissions) => ({
763
+ fieldNames: fieldNames,
764
+ commandSchema: S.unknown,
765
+ fieldPermissions: fieldPermissions
766
+ });
767
+ globalThis.test("an unannotated subscription gets the group-less Cognito directive", () => {
768
+ let frag = makeFragment(["onSomething: CommandResult"]);
769
+ let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [], []);
770
+ let s = GraphQL_Stitcher$ReventlessCore.decode(aug).subscriptions[0];
771
+ globalThis.expect(s).toContain("@aws_cognito_user_pools");
772
+ globalThis.expect(s).not.toContain("cognito_groups");
773
+ globalThis.expect(s).not.toContain("@aws_iam");
774
+ });
775
+ globalThis.test("a subscription named like a gated mutation inherits its groups", () => {
776
+ let fp = Object.fromEntries([[
777
+ "p_Ship",
778
+ {
779
+ TAG: "AllowGroups",
780
+ _0: ["Fulfilment"]
781
+ }
782
+ ]]);
783
+ let frag = makeFragment(["p_Ship: CommandResult"]);
784
+ let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry(["p_Ship"], fp)], []);
785
+ let s = GraphQL_Stitcher$ReventlessCore.decode(aug).subscriptions[0];
786
+ globalThis.expect(s).toContain(`@aws_cognito_user_pools(cognito_groups: ["Fulfilment"])`);
787
+ globalThis.expect(s).not.toContain("@aws_iam");
788
+ });
789
+ globalThis.test("subscription fields survive the round-trip", () => {
790
+ let frag = makeFragment([
791
+ "onA: CommandResult",
792
+ "onB: CommandResult"
793
+ ]);
794
+ let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [], []);
795
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode(aug).subscriptions.length).toBe(2);
796
+ });
797
+ });
798
+
749
799
  export {
750
800
  decodeFragment,
751
801
  }
@@ -152,3 +152,100 @@ describe("AppSync_SdlDecorate.stampCanonicalTypes", () => {
152
152
  expect(AppSync_SdlDecorate.stampCanonicalTypes(once))->toBe(once)
153
153
  })
154
154
  })
155
+
156
+ // ── Type-level Cognito stamping (defaultAction: DENY groundwork) ─────────────
157
+ //
158
+ // With `defaultAction: ALLOW` an undirectived type is reachable by any
159
+ // authenticated Cognito caller, so this pass changes nothing observable. It
160
+ // exists so the default can be flipped to DENY, where an undirectived type is
161
+ // refused mid-traversal even for a caller who passed the field's own gate.
162
+
163
+ describe("AppSync_SdlDecorate.stampAllTypesCognito", () => {
164
+ testSync("stamps an undirectived object type", () => {
165
+ let sdl = AppSync_SdlDecorate.stampAllTypesCognito("type Product {\n id: ID!\n}")
166
+ expect(sdl)->toContain("type Product @aws_cognito_user_pools {")
167
+ })
168
+
169
+ testSync("leaves an already-directived type alone, keeping its @aws_iam arm", () => {
170
+ let sdl =
171
+ AppSync_SdlDecorate.stampSharedIamTypes(
172
+ "type PageInfo {\n hasNextPage: Boolean!\n}",
173
+ )->AppSync_SdlDecorate.stampAllTypesCognito
174
+ expect(sdl)->toContain("type PageInfo @aws_cognito_user_pools @aws_iam {")
175
+ // Not double-stamped.
176
+ expect(sdl->String.includes("@aws_cognito_user_pools @aws_cognito_user_pools"))->toBe(false)
177
+ })
178
+
179
+ testSync("does not stamp input, enum, union or interface declarations", () => {
180
+ let sdl = AppSync_SdlDecorate.stampAllTypesCognito(
181
+ "input ProductFilter {\n id: ID\n}\n\nenum Status {\n Active\n}\n\nunion CommandResult = CommandAccepted | CommandRejected\n\ninterface Node {\n id: ID!\n}",
182
+ )
183
+ expect(sdl->String.includes("@aws_cognito_user_pools"))->toBe(false)
184
+ })
185
+
186
+ testSync("is idempotent", () => {
187
+ let once = AppSync_SdlDecorate.stampAllTypesCognito("type Product {\n id: ID!\n}")
188
+ expect(AppSync_SdlDecorate.stampAllTypesCognito(once))->toBe(once)
189
+ })
190
+ })
191
+
192
+ // ── Final field sweep ───────────────────────────────────────────────────────
193
+ //
194
+ // `injectAwsAuth` only decorates fields it can pair with a schema entry. Several
195
+ // surfaces never reach it: the domain base (`Platform_ping`), event-history
196
+ // queries, `…_eventAppended` subscriptions, upload mutations,
197
+ // `Platform_SetActiveRole`, `geocode`. A deployed-SDL count after the first
198
+ // sweep found 11 such fields still bare on the domain API. This pass is the net.
199
+
200
+ describe("AppSync_SdlDecorate.stampUndirectivedFields", () => {
201
+ let frag = (~mutations=[], ~queries=[], ~subscriptions=[], ()) =>
202
+ ReventlessCore.GraphQL_Stitcher.encode({
203
+ types: [],
204
+ mutations,
205
+ queries,
206
+ subscriptions,
207
+ subscriptionSources: [],
208
+ })
209
+
210
+ testSync("stamps a query no schema entry ever paired with", () => {
211
+ let out = AppSync_SdlDecorate.stampUndirectivedFields(
212
+ frag(~queries=[" Platform_ping: String"], ()),
213
+ )
214
+ let q = ReventlessCore.GraphQL_Stitcher.decode(out).queries->Array.getUnsafe(0)
215
+ expect(q)->toContain("@aws_cognito_user_pools")
216
+ expect(q)->not_->toContain("cognito_groups")
217
+ })
218
+
219
+ testSync("stamps an event-log subscription", () => {
220
+ let out = AppSync_SdlDecorate.stampUndirectivedFields(
221
+ frag(~subscriptions=[" onFoo_eventAppended: FooEvent"], ()),
222
+ )
223
+ expect(
224
+ ReventlessCore.GraphQL_Stitcher.decode(out).subscriptions->Array.getUnsafe(0),
225
+ )->toContain("@aws_cognito_user_pools")
226
+ })
227
+
228
+ testSync("leaves an already-gated field untouched, group and all", () => {
229
+ let gated = ` Secret_Read: String\n @aws_cognito_user_pools(cognito_groups: ["Admin"])`
230
+ let out = AppSync_SdlDecorate.stampUndirectivedFields(frag(~queries=[gated], ()))
231
+ let q = ReventlessCore.GraphQL_Stitcher.decode(out).queries->Array.getUnsafe(0)
232
+ expect(q)->toContain(`cognito_groups: ["Admin"]`)
233
+ expect(q->String.includes("@aws_cognito_user_pools @aws_cognito_user_pools"))->toBe(false)
234
+ })
235
+
236
+ testSync("does not strip an @aws_iam arm", () => {
237
+ let dual = " Sys_Sync: String\n @aws_cognito_user_pools @aws_iam"
238
+ let out = AppSync_SdlDecorate.stampUndirectivedFields(frag(~mutations=[dual], ()))
239
+ expect(
240
+ ReventlessCore.GraphQL_Stitcher.decode(out).mutations->Array.getUnsafe(0),
241
+ )->toContain("@aws_iam")
242
+ })
243
+
244
+ testSync("is idempotent", () => {
245
+ let once = AppSync_SdlDecorate.stampUndirectivedFields(frag(~queries=[" a: String"], ()))
246
+ let twice = AppSync_SdlDecorate.stampUndirectivedFields(once)
247
+ expect(ReventlessCore.GraphQL_Stitcher.decode(twice).queries->Array.getUnsafe(0))->toBe(
248
+ ReventlessCore.GraphQL_Stitcher.decode(once).queries->Array.getUnsafe(0),
249
+ )
250
+ })
251
+ })
@@ -132,6 +132,66 @@ globalThis.describe("AppSync_SdlDecorate.stampCanonicalTypes", () => {
132
132
  });
133
133
  });
134
134
 
135
+ globalThis.describe("AppSync_SdlDecorate.stampAllTypesCognito", () => {
136
+ globalThis.test("stamps an undirectived object type", () => {
137
+ let sdl = AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito("type Product {\n id: ID!\n}");
138
+ globalThis.expect(sdl).toContain("type Product @aws_cognito_user_pools {");
139
+ });
140
+ globalThis.test("leaves an already-directived type alone, keeping its @aws_iam arm", () => {
141
+ let sdl = AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito(AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes("type PageInfo {\n hasNextPage: Boolean!\n}"));
142
+ globalThis.expect(sdl).toContain("type PageInfo @aws_cognito_user_pools @aws_iam {");
143
+ globalThis.expect(sdl.includes("@aws_cognito_user_pools @aws_cognito_user_pools")).toBe(false);
144
+ });
145
+ globalThis.test("does not stamp input, enum, union or interface declarations", () => {
146
+ let sdl = AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito("input ProductFilter {\n id: ID\n}\n\nenum Status {\n Active\n}\n\nunion CommandResult = CommandAccepted | CommandRejected\n\ninterface Node {\n id: ID!\n}");
147
+ globalThis.expect(sdl.includes("@aws_cognito_user_pools")).toBe(false);
148
+ });
149
+ globalThis.test("is idempotent", () => {
150
+ let once = AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito("type Product {\n id: ID!\n}");
151
+ globalThis.expect(AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito(once)).toBe(once);
152
+ });
153
+ });
154
+
155
+ globalThis.describe("AppSync_SdlDecorate.stampUndirectivedFields", () => {
156
+ let frag = (mutationsOpt, queriesOpt, subscriptionsOpt, param) => {
157
+ let mutations = mutationsOpt !== undefined ? mutationsOpt : [];
158
+ let queries = queriesOpt !== undefined ? queriesOpt : [];
159
+ let subscriptions = subscriptionsOpt !== undefined ? subscriptionsOpt : [];
160
+ return GraphQL_Stitcher$ReventlessCore.encode({
161
+ types: [],
162
+ mutations: mutations,
163
+ queries: queries,
164
+ subscriptions: subscriptions,
165
+ subscriptionSources: []
166
+ });
167
+ };
168
+ globalThis.test("stamps a query no schema entry ever paired with", () => {
169
+ let out = AppSync_SdlDecorate$ReventlessAws.stampUndirectivedFields(frag(undefined, [" Platform_ping: String"], undefined, undefined));
170
+ let q = GraphQL_Stitcher$ReventlessCore.decode(out).queries[0];
171
+ globalThis.expect(q).toContain("@aws_cognito_user_pools");
172
+ globalThis.expect(q).not.toContain("cognito_groups");
173
+ });
174
+ globalThis.test("stamps an event-log subscription", () => {
175
+ let out = AppSync_SdlDecorate$ReventlessAws.stampUndirectivedFields(frag(undefined, undefined, [" onFoo_eventAppended: FooEvent"], undefined));
176
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode(out).subscriptions[0]).toContain("@aws_cognito_user_pools");
177
+ });
178
+ globalThis.test("leaves an already-gated field untouched, group and all", () => {
179
+ let out = AppSync_SdlDecorate$ReventlessAws.stampUndirectivedFields(frag(undefined, [` Secret_Read: String\n @aws_cognito_user_pools(cognito_groups: ["Admin"])`], undefined, undefined));
180
+ let q = GraphQL_Stitcher$ReventlessCore.decode(out).queries[0];
181
+ globalThis.expect(q).toContain(`cognito_groups: ["Admin"]`);
182
+ globalThis.expect(q.includes("@aws_cognito_user_pools @aws_cognito_user_pools")).toBe(false);
183
+ });
184
+ globalThis.test("does not strip an @aws_iam arm", () => {
185
+ let out = AppSync_SdlDecorate$ReventlessAws.stampUndirectivedFields(frag([" Sys_Sync: String\n @aws_cognito_user_pools @aws_iam"], undefined, undefined, undefined));
186
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode(out).mutations[0]).toContain("@aws_iam");
187
+ });
188
+ globalThis.test("is idempotent", () => {
189
+ let once = AppSync_SdlDecorate$ReventlessAws.stampUndirectivedFields(frag(undefined, [" a: String"], undefined, undefined));
190
+ let twice = AppSync_SdlDecorate$ReventlessAws.stampUndirectivedFields(once);
191
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode(twice).queries[0]).toBe(GraphQL_Stitcher$ReventlessCore.decode(once).queries[0]);
192
+ });
193
+ });
194
+
135
195
  export {
136
196
  sources,
137
197
  stitchedSdl,