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

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.296 (2026-08-14)
7
+
8
+ ### Features
9
+
10
+ * **aws:** give every field and type an explicit Cognito directive ([37c1063](https://github.com/ReventlessDev/reventless-core/commit/37c1063560aaa6ee200e74d58b9de4d6881441fd))
11
+
12
+
6
13
  # 3.0.0-alpha.295 (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.295",
3
+ "version": "3.0.0-alpha.296",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -13,17 +13,17 @@
13
13
  "sury": "11.0.0-alpha.4",
14
14
  "uuid": "^13.0.0",
15
15
  "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.10",
16
- "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
17
16
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
17
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
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
21
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.140",
22
23
  "@reventlessdev/reventless-core": "3.0.0-alpha.233",
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
 
@@ -444,6 +463,9 @@ let stitchStandaloneWithAwsDirectives = (
444
463
  ReventlessCore.GraphQL_Stitcher.stitchStandalone(~fragment)
445
464
  ->AppSync_SdlDecorate.injectAwsSubscribe(~sources)
446
465
  ->stampSharedIamTypes
466
+ // Last: every remaining undirectived type takes the group-less Cognito arm.
467
+ // After stampSharedIamTypes so the shared traversal types keep `@aws_iam`.
468
+ ->AppSync_SdlDecorate.stampAllTypesCognito
447
469
  }
448
470
 
449
471
  // ── 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,7 @@ 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
+ return AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito(AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(GraphQL_Stitcher$ReventlessCore.stitchStandalone(fragment), sources)));
259
268
  }
260
269
 
261
270
  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,38 @@ 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
+ // Stamp every object `type` declaration that does not already carry an auth
166
+ // directive with the group-less Cognito arm. Run on the ASSEMBLED SDL, AFTER
167
+ // stampSharedIamTypes so the shared traversal types keep their `@aws_iam` arm
168
+ // (this pass skips anything already carrying an `@aws_` directive).
169
+ //
170
+ // Types, not just fields: on a multi-auth API with `defaultAction: DENY`,
171
+ // AppSync refuses an undirectived TYPE even when the caller passed the field's
172
+ // own gate — response shaping walks `…Connection` → `…Edge` → node → nested
173
+ // state types and dies one level in. Entry gating stays on the fields; a type
174
+ // stamp only restores the accessibility an undirectived type has today.
175
+ //
176
+ // Only `type` declarations take auth directives — `input`, `enum`, `union` and
177
+ // `interface` do not, and stamping them is a schema error.
178
+ let stampAllTypesCognito = (sdl: string): string =>
179
+ sdl
180
+ ->String.split("\n")
181
+ ->Array.map(line =>
182
+ if line->String.startsWith("type ") && !(line->String.includes("@aws_")) {
183
+ switch line->String.indexOfOpt("{") {
184
+ | Some(i) =>
185
+ line->String.slice(~start=0, ~end=i) ++
186
+ cognitoOpenDirective ++
187
+ " " ++
188
+ line->String.slice(~start=i, ~end=line->String.length)
189
+ | None => line
190
+ }
191
+ } else {
192
+ line
193
+ }
194
+ )
195
+ ->Array.join("\n")
196
+
153
197
  // ── Merged-API canonical stamping ─────────────────────────────────────────────
154
198
  // Under AppSync Merged APIs the admin source API owns the shared traversal
155
199
  // 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,20 @@ 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 stampAllTypesCognito(sdl) {
92
+ return sdl.split("\n").map(line => {
93
+ if (!(line.startsWith("type ") && !line.includes("@aws_"))) {
94
+ return line;
95
+ }
96
+ let i = Stdlib_String.indexOfOpt(line, "{");
97
+ if (i !== undefined) {
98
+ return line.slice(0, i) + cognitoOpenDirective + " " + line.slice(i, line.length);
99
+ } else {
100
+ return line;
101
+ }
102
+ }).join("\n");
103
+ }
104
+
89
105
  let canonicalTypeNames = [
90
106
  "PageInfo",
91
107
  "CommandAccepted",
@@ -126,10 +142,12 @@ function stampCanonicalTypes(sdl) {
126
142
  export {
127
143
  injectAwsSubscribe,
128
144
  formatCognitoGroupsDirective,
145
+ cognitoOpenDirective,
129
146
  formatDualAuthDirective,
130
147
  injectAwsAuthAll,
131
148
  sharedIamTypeNames,
132
149
  stampSharedIamTypes,
150
+ stampAllTypesCognito,
133
151
  canonicalTypeNames,
134
152
  stampCanonicalTypes,
135
153
  }
@@ -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,39 @@ 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
+ })
@@ -132,6 +132,26 @@ 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
+
135
155
  export {
136
156
  sources,
137
157
  stitchedSdl,