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

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,21 @@
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.298 (2026-08-14)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-aws
9
+
10
+
11
+
12
+
13
+
14
+ # 3.0.0-alpha.297 (2026-08-14)
15
+
16
+ ### Bug Fixes
17
+
18
+ * **aws:** sweep undirectived fields at the schema assembly choke point ([f0ed736](https://github.com/ReventlessDev/reventless-core/commit/f0ed7369ec4dddfb697a3c93115b12f310a491b0))
19
+
20
+
6
21
  # 3.0.0-alpha.296 (2026-08-14)
7
22
 
8
23
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.296",
3
+ "version": "3.0.0-alpha.298",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -13,14 +13,14 @@
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-jest": "1.0.0-alpha.10",
17
16
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
17
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
18
18
  "@reventlessdev/rescript-node": "2.0.0-alpha.6",
19
19
  "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.75",
20
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
21
20
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
- "@reventlessdev/reventless-infra": "3.0.0-alpha.140",
23
21
  "@reventlessdev/reventless-core": "3.0.0-alpha.233",
22
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
23
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.140",
24
24
  "@reventlessdev/reventless-postgres": "3.0.0-alpha.97",
25
25
  "@reventlessdev/reventless-spec": "3.0.0-alpha.113",
26
26
  "@reventlessdev/reventless-interop": "3.0.0-alpha.31"
package/src/Platform.res CHANGED
@@ -335,7 +335,7 @@ module MakeWithConfig = (
335
335
  {
336
336
  userPoolId,
337
337
  ?awsRegion,
338
- defaultAction: PulumiAws.AppSync.GraphQLApi.ALLOW,
338
+ defaultAction: PulumiAws.AppSync.GraphQLApi.DENY,
339
339
  }: PulumiAws.AppSync.GraphQLApi.userPoolConfig
340
340
  )
341
341
  })
@@ -220,7 +220,7 @@ function MakeWithConfig(Config) {
220
220
  let awsRegion = Stdlib_Option.orElse(param[1], getFromDefault("cognitoRegion"));
221
221
  return {
222
222
  userPoolId: userPoolId,
223
- defaultAction: "ALLOW",
223
+ defaultAction: "DENY",
224
224
  awsRegion: awsRegion
225
225
  };
226
226
  });
@@ -1523,7 +1523,7 @@ function Make($star) {
1523
1523
  let awsRegion = Stdlib_Option.orElse(param[1], getFromDefault("cognitoRegion"));
1524
1524
  return {
1525
1525
  userPoolId: userPoolId,
1526
- defaultAction: "ALLOW",
1526
+ defaultAction: "DENY",
1527
1527
  awsRegion: awsRegion
1528
1528
  };
1529
1529
  });
@@ -460,6 +460,10 @@ let stitchStandaloneWithAwsDirectives = (
460
460
  ~baseFragment=fragment,
461
461
  ~pluginFragments=[],
462
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)
463
467
  ReventlessCore.GraphQL_Stitcher.stitchStandalone(~fragment)
464
468
  ->AppSync_SdlDecorate.injectAwsSubscribe(~sources)
465
469
  ->stampSharedIamTypes
@@ -529,6 +533,17 @@ let _makeApiResourceWith = (
529
533
  // Auth_Cognito (cached inside Platform_Stack, so calling from each API call
530
534
  // site — DomainApi, PlatformApi — is safe). A single Output yielding the
531
535
  // {userPoolId, awsRegion, defaultAction} record AppSync expects.
536
+ //
537
+ // `DENY`, not `ALLOW`: an undirectived field or type is refused rather than
538
+ // served to any authenticated Cognito caller. This is what makes a missing
539
+ // directive fail closed — under `ALLOW` the group gate that was inert for the
540
+ // whole of `@aws_auth`'s life produced no symptom at all, because "no
541
+ // effective directive" and "open to everyone" were the same thing.
542
+ //
543
+ // Safe only because every field and type now carries an explicit directive
544
+ // (`stampUndirectivedFields` + `stampAllTypesCognito` at the assembly choke
545
+ // point). Verify with `scripts/check-appsync-directive-coverage.mjs` against a
546
+ // DEPLOYED api — it must report zero before this value is trusted.
532
547
  let userPoolConfigOut = switch userPoolConfig {
533
548
  | Some(config) => config
534
549
  | None =>
@@ -537,7 +552,7 @@ let _makeApiResourceWith = (
537
552
  {
538
553
  userPoolId: c.userPoolId,
539
554
  awsRegion: c.region,
540
- defaultAction: AppSync.GraphQLApi.ALLOW,
555
+ defaultAction: AppSync.GraphQLApi.DENY,
541
556
  }: AppSync.GraphQLApi.userPoolConfig
542
557
  )
543
558
  )
@@ -264,7 +264,8 @@ function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
264
264
 
265
265
  function stitchStandaloneWithAwsDirectives(fragment) {
266
266
  let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(fragment, []);
267
- return AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito(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)));
268
269
  }
269
270
 
270
271
  function _makeApiResourceWith(name, schema, userPoolConfig, opts) {
@@ -287,7 +288,7 @@ function _makeApiResourceWith(name, schema, userPoolConfig, opts) {
287
288
  };
288
289
  let userPoolConfigOut = userPoolConfig !== undefined ? userPoolConfig : Auth_Cognito$ReventlessAws.make(name + `-auth`, undefined).apply(c => ({
289
290
  userPoolId: c.userPoolId,
290
- defaultAction: "ALLOW",
291
+ defaultAction: "DENY",
291
292
  awsRegion: c.region
292
293
  }));
293
294
  let apiArgs_schema = Stdlib_Option.map(schema, prim => prim);
@@ -100,7 +100,7 @@ let make = (~name: string, ~opts: Pulumi.ComponentResource.options): t => {
100
100
  ): AppSync.GraphQLApi.userPoolConfig => {
101
101
  userPoolId: c.userPoolId,
102
102
  awsRegion: c.region,
103
- defaultAction: AppSync.GraphQLApi.ALLOW,
103
+ defaultAction: AppSync.GraphQLApi.DENY,
104
104
  })
105
105
 
106
106
  let mergedApi = AppSync.GraphQLApi.make(
@@ -55,7 +55,7 @@ function make(name, opts) {
55
55
  let authConfigOut = Auth_Cognito$ReventlessAws.make(name + `-auth`, undefined);
56
56
  let userPoolConfigOut = authConfigOut.apply(c => ({
57
57
  userPoolId: c.userPoolId,
58
- defaultAction: "ALLOW",
58
+ defaultAction: "DENY",
59
59
  awsRegion: c.region
60
60
  }));
61
61
  let mergedApi = new (Aws.appsync.GraphQLApi)(name, {
@@ -162,6 +162,37 @@ let stampSharedIamTypes = (sdl: string): string =>
162
162
  acc->String.replace(`type ${name} {`, `type ${name} @aws_cognito_user_pools @aws_iam {`)
163
163
  )
164
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
+
165
196
  // Stamp every object `type` declaration that does not already carry an auth
166
197
  // directive with the group-less Cognito arm. Run on the ASSEMBLED SDL, AFTER
167
198
  // stampSharedIamTypes so the shared traversal types keep their `@aws_iam` arm
@@ -88,6 +88,24 @@ function stampSharedIamTypes(sdl) {
88
88
  return Stdlib_Array.reduce(sharedIamTypeNames, sdl, (acc, name) => acc.replace(`type ` + name + ` {`, `type ` + name + ` @aws_cognito_user_pools @aws_iam {`));
89
89
  }
90
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
+
91
109
  function stampAllTypesCognito(sdl) {
92
110
  return sdl.split("\n").map(line => {
93
111
  if (!(line.startsWith("type ") && !line.includes("@aws_"))) {
@@ -147,6 +165,7 @@ export {
147
165
  injectAwsAuthAll,
148
166
  sharedIamTypeNames,
149
167
  stampSharedIamTypes,
168
+ stampUndirectivedFields,
150
169
  stampAllTypesCognito,
151
170
  canonicalTypeNames,
152
171
  stampCanonicalTypes,
@@ -188,3 +188,64 @@ describe("AppSync_SdlDecorate.stampAllTypesCognito", () => {
188
188
  expect(AppSync_SdlDecorate.stampAllTypesCognito(once))->toBe(once)
189
189
  })
190
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
+ })
@@ -152,6 +152,46 @@ globalThis.describe("AppSync_SdlDecorate.stampAllTypesCognito", () => {
152
152
  });
153
153
  });
154
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
+
155
195
  export {
156
196
  sources,
157
197
  stitchedSdl,