@reventlessdev/reventless-aws 3.0.0-alpha.178 → 3.0.0-alpha.180

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.180 (2026-07-07)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **reventless-aws:** type-level dual-auth + derived query fields for system callers ([91e01f8](https://github.com/ReventlessDev/reventless-core/commit/91e01f82f43f64f07e7eb0a4aaa8cf5dc77a916e))
11
+
12
+
13
+ # 3.0.0-alpha.179 (2026-07-07)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **reventless-postgres:** keep [@pulumi](https://github.com/pulumi) out of the deployed Lambda runtime graph ([0f363cd](https://github.com/ReventlessDev/reventless-core/commit/0f363cd273590591c8f9353fa38ac8b6072e6c49))
18
+
19
+
6
20
  # 3.0.0-alpha.178 (2026-07-06)
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.178",
3
+ "version": "3.0.0-alpha.180",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -15,10 +15,10 @@
15
15
  "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.46",
16
16
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.14",
17
17
  "@reventlessdev/reventless-core": "3.0.0-alpha.143",
18
- "@reventlessdev/reventless-interop": "3.0.0-alpha.24",
19
18
  "@reventlessdev/reventless-infra": "3.0.0-alpha.89",
20
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.6",
21
- "@reventlessdev/reventless-spec": "3.0.0-alpha.67"
19
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.24",
20
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.67",
21
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.7"
22
22
  },
23
23
  "devDependencies": {
24
24
  "rescript": "^12.3.0",
package/src/Platform.res CHANGED
@@ -700,7 +700,7 @@ module MakeWithConfig = (
700
700
  let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
701
701
  ~baseFragment=adminBaseFragment,
702
702
  ~pluginFragments=[],
703
- )
703
+ )->AppSync_Adapter.stampSharedIamTypes
704
704
  (targetApi, adminBarrier)
705
705
  ->Pulumi.Output.all2
706
706
  ->Pulumi.Output.flatMap(((api, _)) =>
@@ -928,7 +928,7 @@ module MakeWithConfig = (
928
928
  let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
929
929
  ~baseFragment,
930
930
  ~pluginFragments=allPluginFragments,
931
- )
931
+ )->AppSync_Adapter.stampSharedIamTypes
932
932
  let currentHash = AppSync_Adapter.sha256Hex(sdl)
933
933
  let storedHash = switch tableNameOpt {
934
934
  | Some(tn) => await readSchemaHash(~tableName=tn, ~apiId)
@@ -1628,7 +1628,7 @@ module MakeWithConfig = (
1628
1628
  let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
1629
1629
  ~baseFragment,
1630
1630
  ~pluginFragments=fragments,
1631
- )
1631
+ )->AppSync_Adapter.stampSharedIamTypes
1632
1632
  await AppSync_Adapter.getClient()->AppSync_Adapter.startSchemaCreationRetrying({
1633
1633
  apiId,
1634
1634
  definition: sdl,
@@ -639,7 +639,7 @@ function MakeWithConfig(Config) {
639
639
  let runSchemaPush = () => writeAndScanFragments().then(async allPluginFragments => {
640
640
  let baseFragment;
641
641
  baseFragment = capturedDeployTarget === "Domain" && Config.splitApi ? emptyBaseFragment : AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(Config.cloner), "Admin", undefined);
642
- let sdl = GraphQL_Stitcher$ReventlessCore.stitch(baseFragment, allPluginFragments);
642
+ let sdl = AppSync_Adapter$ReventlessAws.stampSharedIamTypes(GraphQL_Stitcher$ReventlessCore.stitch(baseFragment, allPluginFragments));
643
643
  let currentHash = AppSync_Adapter$ReventlessAws.sha256Hex(sdl);
644
644
  let storedHash = tableNameOpt !== undefined ? await readSchemaHash(tableNameOpt, apiId) : undefined;
645
645
  let client = AppSync_Adapter$ReventlessAws.getClient();
@@ -694,7 +694,7 @@ function MakeWithConfig(Config) {
694
694
  let match = splitApiOutputsRef.contents;
695
695
  let targetApi = match !== undefined ? match.platformApi : domainApi;
696
696
  let adminBaseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(Config.cloner), "Admin", undefined);
697
- let sdl = GraphQL_Stitcher$ReventlessCore.stitch(adminBaseFragment, []);
697
+ let sdl = AppSync_Adapter$ReventlessAws.stampSharedIamTypes(GraphQL_Stitcher$ReventlessCore.stitch(adminBaseFragment, []));
698
698
  return Output$Pulumi.flatMap(Pulumi.all([
699
699
  targetApi,
700
700
  adminBarrier
@@ -1066,7 +1066,7 @@ function MakeWithConfig(Config) {
1066
1066
  }
1067
1067
  });
1068
1068
  let baseFragment = Config.splitApi ? emptyBaseFragment : AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(Config.cloner), "Admin", undefined);
1069
- let sdl = GraphQL_Stitcher$ReventlessCore.stitch(baseFragment, fragments);
1069
+ let sdl = AppSync_Adapter$ReventlessAws.stampSharedIamTypes(GraphQL_Stitcher$ReventlessCore.stitch(baseFragment, fragments));
1070
1070
  return await AppSync_Adapter$ReventlessAws.startSchemaCreationRetrying(AppSync_Adapter$ReventlessAws.getClient(), {
1071
1071
  apiId: apiId,
1072
1072
  definition: sdl
@@ -1870,7 +1870,7 @@ function Make($star) {
1870
1870
  let runSchemaPush = () => writeAndScanFragments().then(async allPluginFragments => {
1871
1871
  let baseFragment;
1872
1872
  baseFragment = capturedDeployTarget === "Domain" ? emptyBaseFragment : AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(false), "Admin", undefined);
1873
- let sdl = GraphQL_Stitcher$ReventlessCore.stitch(baseFragment, allPluginFragments);
1873
+ let sdl = AppSync_Adapter$ReventlessAws.stampSharedIamTypes(GraphQL_Stitcher$ReventlessCore.stitch(baseFragment, allPluginFragments));
1874
1874
  let currentHash = AppSync_Adapter$ReventlessAws.sha256Hex(sdl);
1875
1875
  let storedHash = tableNameOpt !== undefined ? await readSchemaHash(tableNameOpt, apiId) : undefined;
1876
1876
  let client = AppSync_Adapter$ReventlessAws.getClient();
@@ -1925,7 +1925,7 @@ function Make($star) {
1925
1925
  let match = splitApiOutputsRef.contents;
1926
1926
  let targetApi = match !== undefined ? match.platformApi : domainApi;
1927
1927
  let adminBaseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(false), "Admin", undefined);
1928
- let sdl = GraphQL_Stitcher$ReventlessCore.stitch(adminBaseFragment, []);
1928
+ let sdl = AppSync_Adapter$ReventlessAws.stampSharedIamTypes(GraphQL_Stitcher$ReventlessCore.stitch(adminBaseFragment, []));
1929
1929
  return Output$Pulumi.flatMap(Pulumi.all([
1930
1930
  targetApi,
1931
1931
  adminBarrier
@@ -2280,7 +2280,7 @@ function Make($star) {
2280
2280
  return;
2281
2281
  }
2282
2282
  });
2283
- let sdl = GraphQL_Stitcher$ReventlessCore.stitch(emptyBaseFragment, fragments);
2283
+ let sdl = AppSync_Adapter$ReventlessAws.stampSharedIamTypes(GraphQL_Stitcher$ReventlessCore.stitch(emptyBaseFragment, fragments));
2284
2284
  return await AppSync_Adapter$ReventlessAws.startSchemaCreationRetrying(AppSync_Adapter$ReventlessAws.getClient(), {
2285
2285
  apiId: apiId,
2286
2286
  definition: sdl
@@ -11,10 +11,12 @@
11
11
  let opsFor = (
12
12
  config: PgConnection.connectionConfig,
13
13
  ~logName: string,
14
- ~lockStrategy: ReventlessPostgres.DcbEventLogStorage_Postgres.lockStrategy=#AdvisoryLocks,
14
+ ~lockStrategy: ReventlessPostgres.DcbEventLogStorage_Postgres_Ops.lockStrategy=#AdvisoryLocks,
15
15
  ): ReventlessCore.DcbEventLog_Adapter.operations => {
16
16
  let pool = PgRuntime.poolFor(config)
17
- let (_name, ops, _storage) = ReventlessPostgres.DcbEventLogStorage_Postgres.makeStorage(
17
+ // Import the runtime-pure ops module (no @pulumi/pulumi) so this deployed-Lambda
18
+ // path never drags a deploy-time dep into its ESM import graph.
19
+ let (_name, ops) = ReventlessPostgres.DcbEventLogStorage_Postgres_Ops.makeOps(
18
20
  ~pool,
19
21
  ~name=logName,
20
22
  ~indexes=[],
@@ -1,12 +1,12 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as PgRuntime$ReventlessAws from "../Postgres/PgRuntime.res.mjs";
4
- import * as DcbEventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/DcbEventLogStorage_Postgres.res.mjs";
4
+ import * as DcbEventLogStorage_Postgres_Ops$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/DcbEventLogStorage_Postgres_Ops.res.mjs";
5
5
 
6
6
  function opsFor(config, logName, lockStrategyOpt) {
7
7
  let lockStrategy = lockStrategyOpt !== undefined ? lockStrategyOpt : "AdvisoryLocks";
8
8
  let pool = PgRuntime$ReventlessAws.poolFor(config);
9
- return DcbEventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, logName, [], undefined, undefined, undefined, lockStrategy, undefined)[1];
9
+ return DcbEventLogStorage_Postgres_Ops$ReventlessPostgres.makeOps(pool, logName, [], undefined, undefined, undefined, lockStrategy, undefined)[1];
10
10
  }
11
11
 
12
12
  export {
@@ -14,7 +14,9 @@ let opsFor = (
14
14
  ~logName: string,
15
15
  ): ReventlessCore.EventLog_Adapter.operations => {
16
16
  let pool = PgRuntime.poolFor(config)
17
- let (_name, ops, _storage) = ReventlessPostgres.EventLogStorage_Postgres.makeStorage(
17
+ // Import the runtime-pure ops module (no @pulumi/pulumi) so this deployed-Lambda
18
+ // path never drags a deploy-time dep into its ESM import graph.
19
+ let (_name, ops) = ReventlessPostgres.EventLogStorage_Postgres_Ops.makeOps(
18
20
  ~pool,
19
21
  ~name=logName,
20
22
  ~opts=(),
@@ -1,11 +1,11 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as PgRuntime$ReventlessAws from "../Postgres/PgRuntime.res.mjs";
4
- import * as EventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/EventLogStorage_Postgres.res.mjs";
4
+ import * as EventLogStorage_Postgres_Ops$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/EventLogStorage_Postgres_Ops.res.mjs";
5
5
 
6
6
  function opsFor(config, logName) {
7
7
  let pool = PgRuntime$ReventlessAws.poolFor(config);
8
- return EventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, logName, undefined, undefined)[1];
8
+ return EventLogStorage_Postgres_Ops$ReventlessPostgres.makeOps(pool, logName, undefined, undefined)[1];
9
9
  }
10
10
 
11
11
  export {
@@ -15,5 +15,7 @@ let opsFor = (
15
15
  ~subIdField: option<string>=?,
16
16
  ): ReventlessCore.QueryDb_Adapter.operations => {
17
17
  let pool = PgRuntime.poolFor(config)
18
- ReventlessPostgres.QueryDbStorage_Postgres.makeOperations(~pool, ~name, ~indexes, ~subIdField)
18
+ // Import the runtime-pure ops module (no @pulumi/pulumi) so this deployed-Lambda
19
+ // path never drags a deploy-time dep into its ESM import graph.
20
+ ReventlessPostgres.QueryDbStorage_Postgres_Ops.makeOperations(~pool, ~name, ~indexes, ~subIdField)
19
21
  }
@@ -1,12 +1,12 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as PgRuntime$ReventlessAws from "../Postgres/PgRuntime.res.mjs";
4
- import * as QueryDbStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/QueryDbStorage_Postgres.res.mjs";
4
+ import * as QueryDbStorage_Postgres_Ops$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/QueryDbStorage_Postgres_Ops.res.mjs";
5
5
 
6
6
  function opsFor(config, name, indexesOpt, subIdField) {
7
7
  let indexes = indexesOpt !== undefined ? indexesOpt : [];
8
8
  let pool = PgRuntime$ReventlessAws.poolFor(config);
9
- return QueryDbStorage_Postgres$ReventlessPostgres.makeOperations(pool, name, indexes, subIdField);
9
+ return QueryDbStorage_Postgres_Ops$ReventlessPostgres.makeOperations(pool, name, indexes, subIdField);
10
10
  }
11
11
 
12
12
  export {
@@ -311,6 +311,55 @@ let _formatDualAuthDirective = (groups: option<array<string>>): string => {
311
311
  `${cognito} @aws_iam`
312
312
  }
313
313
 
314
+ // ── Type-level dual-auth ─────────────────────────────────────────────────────
315
+ // On a multi-auth API, object TYPES without auth directives are accessible only
316
+ // via the default auth mode — a field-level `@aws_iam` admits the system caller
317
+ // to the top-level field, but response shaping then walks the return types
318
+ // (`…Connection` → `…Edge` → node type → nested state types) and dies with
319
+ // "Not Authorized to access <field> on type <T>" one level in. Types reachable
320
+ // from a systemCallable field therefore carry the bare multi-auth pair: the
321
+ // group-less Cognito arm matches the pre-existing accessibility of an
322
+ // undirectived type (any authenticated user; entry gating stays on the fields),
323
+ // the IAM arm admits the traversal. Only `type` declarations take auth
324
+ // directives (inputs/enums/interfaces do not).
325
+
326
+ // The declared name of a `type …` SDL declaration; None for input/enum/union/etc.
327
+ let _typeDeclName = (decl: string): option<string> =>
328
+ if decl->String.startsWith("type ") {
329
+ let rest = decl->String.slice(~start=5, ~end=decl->String.length)
330
+ let end = switch rest->String.search(%re("/[\s{]/")) {
331
+ | -1 => rest->String.length
332
+ | i => i
333
+ }
334
+ Some(rest->String.slice(~start=0, ~end))
335
+ } else {
336
+ None
337
+ }
338
+
339
+ // Insert the multi-auth pair into a type declaration header (before its `{`).
340
+ let _stampTypeDualAuth = (decl: string): string =>
341
+ switch decl->String.indexOfOpt("{") {
342
+ | Some(i) =>
343
+ decl->String.slice(~start=0, ~end=i) ++
344
+ "@aws_cognito_user_pools @aws_iam " ++
345
+ decl->String.slice(~start=i, ~end=decl->String.length)
346
+ | None => decl
347
+ }
348
+
349
+ // Shared traversal types every callable surface reaches — `PageInfo` (relay
350
+ // connections, injected by the stitcher) and the `CommandResult` members
351
+ // (mutation returns, deduped across fragments by the stitcher). They must be
352
+ // stamped exactly once on the ASSEMBLED SDL: stamping per-fragment would race
353
+ // the stitcher's first-wins dedupe against unstamped copies from sibling
354
+ // fragments. Unconditional — the API always configures AWS_IAM as an
355
+ // additional provider, so the directive is always valid.
356
+ let sharedIamTypeNames = ["PageInfo", "CommandAccepted", "CommandRejected", "CommandPending"]
357
+
358
+ let stampSharedIamTypes = (sdl: string): string =>
359
+ sharedIamTypeNames->Array.reduce(sdl, (acc, name) =>
360
+ acc->String.replace(`type ${name} {`, `type ${name} @aws_cognito_user_pools @aws_iam {`)
361
+ )
362
+
314
363
  let injectAwsAuth = (
315
364
  fragment: Reventless.Plugin.apiSchemaFragment,
316
365
  ~mutationEntries: array<ReventlessInfra.Api.mutationSchemaEntry>,
@@ -323,8 +372,15 @@ let injectAwsAuth = (
323
372
  // `authorization.group` per-entry single-group form.
324
373
  // Fields opted into deploy-time IAM invocation (dual-auth). Membership here
325
374
  // switches a field from the single-mode `@aws_auth` form to the multi-auth
326
- // `@aws_cognito_user_pools(...) @aws_iam` form.
375
+ // `@aws_cognito_user_pools(...) @aws_iam` form. Query entries mark by PREFIX
376
+ // (single/list field name) because the generator derives further fields from
377
+ // them — `…Items` (sub-id), `…ByIds`, `…By<Index>` (GSI) — which a system
378
+ // caller (reconcile reads) uses too. Type declarations reachable from a
379
+ // callable entry are stamped by returnTypeName prefix (see the type-level
380
+ // dual-auth note above).
327
381
  let iamFields: Dict.t<bool> = Dict.make()
382
+ let iamQueryFieldPrefixes: array<string> = []
383
+ let iamTypePrefixes: array<string> = []
328
384
 
329
385
  let mutationAuthMap: Dict.t<array<string>> = Dict.make()
330
386
  mutationEntries->Array.forEach(entry => {
@@ -359,6 +415,9 @@ let injectAwsAuth = (
359
415
  if entry.systemCallable->Option.getOr(false) {
360
416
  iamFields->Dict.set(entry.singleFieldName, true)
361
417
  iamFields->Dict.set(entry.listFieldName, true)
418
+ iamQueryFieldPrefixes->Array.push(entry.singleFieldName)
419
+ iamQueryFieldPrefixes->Array.push(entry.listFieldName)
420
+ iamTypePrefixes->Array.push(entry.returnTypeName)
362
421
  }
363
422
  switch entry.authorization {
364
423
  | Some({group}) =>
@@ -396,7 +455,10 @@ let injectAwsAuth = (
396
455
  let augmentedQueries = parts.queries->Array.map(field => {
397
456
  let fieldName = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
398
457
  let groups = queryAuthMap->Dict.get(fieldName)
399
- if iamFields->Dict.get(fieldName)->Option.getOr(false) {
458
+ let isIam =
459
+ iamFields->Dict.get(fieldName)->Option.getOr(false) ||
460
+ iamQueryFieldPrefixes->Array.some(p => fieldName->String.startsWith(p))
461
+ if isIam {
400
462
  `${field} ${_formatDualAuthDirective(groups)}`
401
463
  } else {
402
464
  switch groups {
@@ -406,10 +468,21 @@ let injectAwsAuth = (
406
468
  }
407
469
  })
408
470
 
471
+ // Type-level dual-auth for every type the callable entries' responses reach —
472
+ // the node type, its Connection/Edge wrappers, and nested state types all
473
+ // share the entry's returnTypeName prefix.
474
+ let augmentedTypes = parts.types->Array.map(decl =>
475
+ switch _typeDeclName(decl) {
476
+ | Some(name) if iamTypePrefixes->Array.some(p => name->String.startsWith(p)) =>
477
+ _stampTypeDualAuth(decl)
478
+ | _ => decl
479
+ }
480
+ )
481
+
409
482
  let encoded =
410
483
  JSON.Encode.object(
411
484
  Dict.fromArray([
412
- ("types", JSON.Encode.array(parts.types->Array.map(JSON.Encode.string))),
485
+ ("types", JSON.Encode.array(augmentedTypes->Array.map(JSON.Encode.string))),
413
486
  ("mutations", JSON.Encode.array(augmentedMutations->Array.map(JSON.Encode.string))),
414
487
  ("queries", JSON.Encode.array(augmentedQueries->Array.map(JSON.Encode.string))),
415
488
  (
@@ -546,7 +619,13 @@ let updateSchema = (
546
619
  // The base fragment contains core Plugin aggregate queries/mutations — all Admin-only.
547
620
  // Plugin fragments already have @aws_auth injected via generateFragment.
548
621
  let augmentedBaseFragment = injectAwsAuthAll(baseFragment, ~group="Admin")
549
- let sdl = ReventlessCore.GraphQL_Stitcher.stitch(~baseFragment=augmentedBaseFragment, ~pluginFragments)
622
+ // Shared traversal types are stamped once on the assembled SDL (post-stitch,
623
+ // post-dedupe) — see stampSharedIamTypes.
624
+ let sdl =
625
+ ReventlessCore.GraphQL_Stitcher.stitch(
626
+ ~baseFragment=augmentedBaseFragment,
627
+ ~pluginFragments,
628
+ )->stampSharedIamTypes
550
629
  // Resolve the API ID from the Output chain. In mock mode (tests) and in Lambda runtime
551
630
  // (where the Output is backed by already-known values), this completes synchronously.
552
631
  // The resulting promise wraps the AppSync SDK call.
@@ -4,8 +4,10 @@ import * as Effect from "@reventlessdev/rescript-effect/src/Effect.res.mjs";
4
4
  import * as Aws from "@pulumi/aws";
5
5
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
6
6
  import * as Nodecrypto from "node:crypto";
7
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
7
8
  import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
8
9
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
+ import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
9
11
  import * as Effect$1 from "effect/Effect";
10
12
  import * as Pulumi from "@pulumi/pulumi";
11
13
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
@@ -215,9 +217,41 @@ function _formatDualAuthDirective(groups) {
215
217
  return cognito + ` @aws_iam`;
216
218
  }
217
219
 
220
+ function _typeDeclName(decl) {
221
+ if (!decl.startsWith("type ")) {
222
+ return;
223
+ }
224
+ let rest = decl.slice(5, decl.length);
225
+ let i = rest.search(/[\s{]/);
226
+ let end = i !== -1 ? i : rest.length;
227
+ return rest.slice(0, end);
228
+ }
229
+
230
+ function _stampTypeDualAuth(decl) {
231
+ let i = Stdlib_String.indexOfOpt(decl, "{");
232
+ if (i !== undefined) {
233
+ return decl.slice(0, i) + "@aws_cognito_user_pools @aws_iam " + decl.slice(i, decl.length);
234
+ } else {
235
+ return decl;
236
+ }
237
+ }
238
+
239
+ let sharedIamTypeNames = [
240
+ "PageInfo",
241
+ "CommandAccepted",
242
+ "CommandRejected",
243
+ "CommandPending"
244
+ ];
245
+
246
+ function stampSharedIamTypes(sdl) {
247
+ return Stdlib_Array.reduce(sharedIamTypeNames, sdl, (acc, name) => acc.replace(`type ` + name + ` {`, `type ` + name + ` @aws_cognito_user_pools @aws_iam {`));
248
+ }
249
+
218
250
  function injectAwsAuth(fragment, mutationEntries, queryEntries) {
219
251
  let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
220
252
  let iamFields = {};
253
+ let iamQueryFieldPrefixes = [];
254
+ let iamTypePrefixes = [];
221
255
  let mutationAuthMap = {};
222
256
  mutationEntries.forEach(entry => {
223
257
  if (Stdlib_Option.getOr(entry.systemCallable, false)) {
@@ -252,6 +286,9 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
252
286
  if (Stdlib_Option.getOr(entry.systemCallable, false)) {
253
287
  iamFields[entry.singleFieldName] = true;
254
288
  iamFields[entry.listFieldName] = true;
289
+ iamQueryFieldPrefixes.push(entry.singleFieldName);
290
+ iamQueryFieldPrefixes.push(entry.listFieldName);
291
+ iamTypePrefixes.push(entry.returnTypeName);
255
292
  }
256
293
  let match = entry.authorization;
257
294
  if (match !== undefined) {
@@ -287,7 +324,8 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
287
324
  let augmentedQueries = parts.queries.map(field => {
288
325
  let fieldName = GraphQL_Stitcher$ReventlessCore.extractLeadingName(field);
289
326
  let groups = queryAuthMap[fieldName];
290
- if (Stdlib_Option.getOr(iamFields[fieldName], false)) {
327
+ let isIam = Stdlib_Option.getOr(iamFields[fieldName], false) || iamQueryFieldPrefixes.some(p => fieldName.startsWith(p));
328
+ if (isIam) {
291
329
  return field + ` ` + _formatDualAuthDirective(groups);
292
330
  } else if (groups !== undefined) {
293
331
  return field + ` ` + _formatGroupsDirective(groups);
@@ -295,10 +333,18 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
295
333
  return field;
296
334
  }
297
335
  });
336
+ let augmentedTypes = parts.types.map(decl => {
337
+ let name = _typeDeclName(decl);
338
+ if (name !== undefined && iamTypePrefixes.some(p => name.startsWith(p))) {
339
+ return _stampTypeDualAuth(decl);
340
+ } else {
341
+ return decl;
342
+ }
343
+ });
298
344
  let encoded = JSON.stringify(Object.fromEntries([
299
345
  [
300
346
  "types",
301
- parts.types.map(prim => prim)
347
+ augmentedTypes.map(prim => prim)
302
348
  ],
303
349
  [
304
350
  "mutations",
@@ -398,7 +444,7 @@ function generateFragment(mutationEntries, queryEntries) {
398
444
 
399
445
  function updateSchema(api, baseFragment, pluginFragments) {
400
446
  let augmentedBaseFragment = injectAwsAuthAll(baseFragment, "Admin", undefined);
401
- let sdl = GraphQL_Stitcher$ReventlessCore.stitch(augmentedBaseFragment, pluginFragments);
447
+ let sdl = stampSharedIamTypes(GraphQL_Stitcher$ReventlessCore.stitch(augmentedBaseFragment, pluginFragments));
402
448
  let resultPromise = {
403
449
  contents: Promise.resolve()
404
450
  };
@@ -426,6 +472,10 @@ export {
426
472
  _permissionToCognitoGroups,
427
473
  _formatGroupsDirective,
428
474
  _formatDualAuthDirective,
475
+ _typeDeclName,
476
+ _stampTypeDualAuth,
477
+ sharedIamTypeNames,
478
+ stampSharedIamTypes,
429
479
  injectAwsAuth,
430
480
  injectAwsAuthAll,
431
481
  makeApiResource,
@@ -452,6 +452,118 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
452
452
  | _ => JsError.throwWithMessage("missing query fields")
453
453
  }
454
454
  })
455
+
456
+ testSync("systemCallable dual-auth extends to derived query fields (Items/ByIds/By<Index>)", () => {
457
+ let entry: ReventlessInfra.Api.querySchemaEntry = {
458
+ singleFieldName: "p_Item",
459
+ listFieldName: "p_Items",
460
+ returnTypeName: "p_Item",
461
+ stateSchema: S.unknown,
462
+ authorization: None,
463
+ permission: Reventless.Authorization.AllowGroups(["Admin"]),
464
+ systemCallable: true,
465
+ }
466
+ let frag = makeFragment(
467
+ [],
468
+ [
469
+ "p_ItemItems(id: ID!, first: Int): p_ItemConnection!",
470
+ "p_ItemsByIds(ids: [ID!]!): [p_Item]!",
471
+ "p_ItemByOwner(id: ID!, first: Int): p_ItemConnection!",
472
+ "q_Unrelated(id: ID!): String",
473
+ ],
474
+ )
475
+ let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[], ~queryEntries=[entry])
476
+ let parts = decodeFragment(aug)
477
+ parts.queries->Array.forEach(field =>
478
+ if field->String.includes("q_Unrelated") {
479
+ expect(field)->not_->toContain("@aws_iam")
480
+ } else {
481
+ expect(field)->toContain("@aws_iam")
482
+ }
483
+ )
484
+ })
485
+ })
486
+
487
+ // ── Type-level dual-auth ─────────────────────────────────────────────────────
488
+ //
489
+ // On a multi-auth API a field directive alone is not enough: response shaping
490
+ // walks the return TYPES (Connection → Edge → node → nested state types), each
491
+ // of which is default-mode-only without its own directive. Types prefixed by a
492
+ // systemCallable entry's returnTypeName get the bare multi-auth pair; shared
493
+ // traversal types (PageInfo / CommandResult members) are stamped once on the
494
+ // assembled SDL.
495
+
496
+ describe("AppSync_Adapter — type-level dual-auth", () => {
497
+ let makeFragmentWithTypes = (types, queryFields) =>
498
+ ReventlessCore.GraphQL_Stitcher.encode({
499
+ types,
500
+ mutations: [],
501
+ queries: queryFields,
502
+ subscriptions: [],
503
+ })
504
+
505
+ let callableEntry: ReventlessInfra.Api.querySchemaEntry = {
506
+ singleFieldName: "p_Item",
507
+ listFieldName: "p_Items",
508
+ returnTypeName: "p_Item",
509
+ stateSchema: S.unknown,
510
+ authorization: None,
511
+ permission: Reventless.Authorization.AllowGroups(["Admin"]),
512
+ systemCallable: true,
513
+ }
514
+
515
+ let types = [
516
+ "type p_Item implements Node {\n id: ID!\n}",
517
+ "type p_ItemConnection {\n edges: [p_ItemEdge!]!\n}",
518
+ "type p_ItemEdge {\n node: p_Item!\n}",
519
+ "type p_ItemNested {\n x: String\n}",
520
+ "type q_Sibling {\n id: ID!\n}",
521
+ "input p_ItemItemsFilter {\n eq: String\n}",
522
+ ]
523
+
524
+ testSync("types prefixed by a callable entry's returnTypeName carry the multi-auth pair", () => {
525
+ let frag = makeFragmentWithTypes(types, ["p_Item(id: ID!): p_Item"])
526
+ let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[], ~queryEntries=[callableEntry])
527
+ let parts = decodeFragment(aug)
528
+ parts.types->Array.forEach(decl =>
529
+ if decl->String.startsWith("type p_Item") {
530
+ expect(decl)->toContain("@aws_cognito_user_pools @aws_iam {")
531
+ } else {
532
+ // The sibling view's type and the (non-authorizable) input stay untouched.
533
+ expect(decl)->not_->toContain("@aws_iam")
534
+ }
535
+ )
536
+ })
537
+
538
+ testSync("without a systemCallable entry no type is stamped", () => {
539
+ let plainEntry: ReventlessInfra.Api.querySchemaEntry = {
540
+ ...callableEntry,
541
+ systemCallable: false,
542
+ }
543
+ let frag = makeFragmentWithTypes(types, ["p_Item(id: ID!): p_Item"])
544
+ let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[], ~queryEntries=[plainEntry])
545
+ decodeFragment(aug).types->Array.forEach(decl => expect(decl)->not_->toContain("@aws_iam"))
546
+ })
547
+
548
+ testSync("stampSharedIamTypes marks PageInfo + CommandResult members on the assembled SDL", () => {
549
+ let sdl = [
550
+ "interface Node {\n id: ID!\n}",
551
+ "type PageInfo {\n hasNextPage: Boolean!\n}",
552
+ "union CommandResult = CommandAccepted | CommandRejected | CommandPending",
553
+ "type CommandAccepted {\n msgId: ID!\n}",
554
+ "type CommandRejected {\n msgId: ID!\n}",
555
+ "type CommandPending {\n msgId: ID!\n}",
556
+ "type Untouched {\n id: ID!\n}",
557
+ ]->Array.join("\n\n")
558
+ let stamped = AppSync_Adapter.stampSharedIamTypes(sdl)
559
+ expect(stamped)->toContain("type PageInfo @aws_cognito_user_pools @aws_iam {")
560
+ expect(stamped)->toContain("type CommandAccepted @aws_cognito_user_pools @aws_iam {")
561
+ expect(stamped)->toContain("type CommandRejected @aws_cognito_user_pools @aws_iam {")
562
+ expect(stamped)->toContain("type CommandPending @aws_cognito_user_pools @aws_iam {")
563
+ expect(stamped)->toContain("type Untouched {")
564
+ // The union declaration itself takes no directive.
565
+ expect(stamped)->toContain("union CommandResult = CommandAccepted | CommandRejected | CommandPending")
566
+ })
455
567
  })
456
568
 
457
569
  describe("AppSync_Adapter.injectAwsAuthAll — ~iamFieldNames", () => {
@@ -426,6 +426,107 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
426
426
  return Stdlib_JsError.throwWithMessage("missing query fields");
427
427
  }
428
428
  });
429
+ globalThis.test("systemCallable dual-auth extends to derived query fields (Items/ByIds/By<Index>)", () => {
430
+ let entry_permission = {
431
+ TAG: "AllowGroups",
432
+ _0: ["Admin"]
433
+ };
434
+ let entry_systemCallable = true;
435
+ let entry = {
436
+ singleFieldName: "p_Item",
437
+ listFieldName: "p_Items",
438
+ returnTypeName: "p_Item",
439
+ stateSchema: S.unknown,
440
+ authorization: undefined,
441
+ permission: entry_permission,
442
+ systemCallable: entry_systemCallable
443
+ };
444
+ let frag = makeFragment([], [
445
+ "p_ItemItems(id: ID!, first: Int): p_ItemConnection!",
446
+ "p_ItemsByIds(ids: [ID!]!): [p_Item]!",
447
+ "p_ItemByOwner(id: ID!, first: Int): p_ItemConnection!",
448
+ "q_Unrelated(id: ID!): String"
449
+ ]);
450
+ let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [], [entry]);
451
+ let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
452
+ parts.queries.forEach(field => {
453
+ if (field.includes("q_Unrelated")) {
454
+ globalThis.expect(field).not.toContain("@aws_iam");
455
+ } else {
456
+ globalThis.expect(field).toContain("@aws_iam");
457
+ }
458
+ });
459
+ });
460
+ });
461
+
462
+ globalThis.describe("AppSync_Adapter — type-level dual-auth", () => {
463
+ let makeFragmentWithTypes = (types, queryFields) => GraphQL_Stitcher$ReventlessCore.encode({
464
+ types: types,
465
+ mutations: [],
466
+ queries: queryFields,
467
+ subscriptions: []
468
+ });
469
+ let callableEntry_permission = {
470
+ TAG: "AllowGroups",
471
+ _0: ["Admin"]
472
+ };
473
+ let callableEntry_systemCallable = true;
474
+ let callableEntry = {
475
+ singleFieldName: "p_Item",
476
+ listFieldName: "p_Items",
477
+ returnTypeName: "p_Item",
478
+ stateSchema: S.unknown,
479
+ authorization: undefined,
480
+ permission: callableEntry_permission,
481
+ systemCallable: callableEntry_systemCallable
482
+ };
483
+ let types = [
484
+ "type p_Item implements Node {\n id: ID!\n}",
485
+ "type p_ItemConnection {\n edges: [p_ItemEdge!]!\n}",
486
+ "type p_ItemEdge {\n node: p_Item!\n}",
487
+ "type p_ItemNested {\n x: String\n}",
488
+ "type q_Sibling {\n id: ID!\n}",
489
+ "input p_ItemItemsFilter {\n eq: String\n}"
490
+ ];
491
+ globalThis.test("types prefixed by a callable entry's returnTypeName carry the multi-auth pair", () => {
492
+ let frag = makeFragmentWithTypes(types, ["p_Item(id: ID!): p_Item"]);
493
+ let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [], [callableEntry]);
494
+ let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
495
+ parts.types.forEach(decl => {
496
+ if (decl.startsWith("type p_Item")) {
497
+ globalThis.expect(decl).toContain("@aws_cognito_user_pools @aws_iam {");
498
+ } else {
499
+ globalThis.expect(decl).not.toContain("@aws_iam");
500
+ }
501
+ });
502
+ });
503
+ globalThis.test("without a systemCallable entry no type is stamped", () => {
504
+ let newrecord = {...callableEntry};
505
+ newrecord.systemCallable = false;
506
+ let frag = makeFragmentWithTypes(types, ["p_Item(id: ID!): p_Item"]);
507
+ let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [], [newrecord]);
508
+ GraphQL_Stitcher$ReventlessCore.decode(aug).types.forEach(decl => {
509
+ globalThis.expect(decl).not.toContain("@aws_iam");
510
+ });
511
+ });
512
+ globalThis.test("stampSharedIamTypes marks PageInfo + CommandResult members on the assembled SDL", () => {
513
+ let sdl = [
514
+ "interface Node {\n id: ID!\n}",
515
+ "type PageInfo {\n hasNextPage: Boolean!\n}",
516
+ "union CommandResult = CommandAccepted | CommandRejected | CommandPending",
517
+ "type CommandAccepted {\n msgId: ID!\n}",
518
+ "type CommandRejected {\n msgId: ID!\n}",
519
+ "type CommandPending {\n msgId: ID!\n}",
520
+ "type Untouched {\n id: ID!\n}"
521
+ ].join("\n\n");
522
+ let stamped = AppSync_Adapter$ReventlessAws.stampSharedIamTypes(sdl);
523
+ globalThis.expect(stamped).toContain("type PageInfo @aws_cognito_user_pools @aws_iam {");
524
+ globalThis.expect(stamped).toContain("type CommandAccepted @aws_cognito_user_pools @aws_iam {");
525
+ globalThis.expect(stamped).toContain("type CommandRejected @aws_cognito_user_pools @aws_iam {");
526
+ globalThis.expect(stamped).toContain("type CommandPending @aws_cognito_user_pools @aws_iam {");
527
+ globalThis.expect(stamped).toContain("type Untouched {");
528
+ globalThis.expect(stamped).toContain("union CommandResult = CommandAccepted | CommandRejected | CommandPending");
529
+ });
429
530
  });
430
531
 
431
532
  globalThis.describe("AppSync_Adapter.injectAwsAuthAll — ~iamFieldNames", () => {