@reventlessdev/reventless-aws 3.0.0-alpha.172 → 3.0.0-alpha.174

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +6 -6
  3. package/src/Platform.res +77 -7
  4. package/src/Platform.res.mjs +82 -8
  5. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +1 -1
  6. package/src/adapter/DcbEventLog/DcbBackend.res +78 -0
  7. package/src/adapter/DcbEventLog/DcbBackend.res.mjs +53 -0
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage.res +20 -0
  9. package/src/adapter/DcbEventLog/DcbEventLogStorage.res.mjs +21 -1
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +43 -0
  11. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res.mjs +21 -0
  12. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res +26 -0
  13. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs +15 -0
  14. package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res +23 -0
  15. package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res.mjs +14 -0
  16. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +188 -0
  17. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +141 -0
  18. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +72 -0
  19. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +40 -0
  20. package/src/adapter/Runtime/AggregateEntryPoint.mjs +27 -9
  21. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +3 -3
  22. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res.mjs +3 -3
  23. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res.mjs +1 -1
  24. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res.mjs +1 -1
  25. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +1 -1
  26. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +1 -1
  27. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +1 -1
  28. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +20 -6
  29. package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +1 -1
  30. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +1 -1
  31. package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +1 -1
  32. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +52 -0
  33. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +38 -0
  34. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +20 -2
  35. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +1 -1
  36. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +84 -6
  37. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +49 -4
  38. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +1 -1
  39. package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res.mjs +1 -1
  40. package/src/components/Api/AppSync_Adapter.res +72 -8
  41. package/src/components/Api/AppSync_Adapter.res.mjs +45 -6
  42. package/src/components/Plugin.res +1 -1
  43. package/src/components/Plugin.res.mjs +2 -2
  44. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
  45. package/src/plugin/runtime/PluginRuntime_Builder.res +20 -0
  46. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +10 -2
  47. package/tests/AppSync_AdapterTest.res +140 -0
  48. package/tests/AppSync_AdapterTest.res.mjs +182 -6
  49. package/tests/PgChangeFeedRelay_RuntimeTest.res +75 -0
  50. package/tests/PgChangeFeedRelay_RuntimeTest.res.mjs +135 -0
@@ -266,6 +266,21 @@ let getClient = () =>
266
266
  // reaching request is already authenticated). `AllowGroups([])` and
267
267
  // `DenyAll` emit a sentinel `__deny_all__` group that no Cognito user can
268
268
  // belong to — effectively blocking the field at the API layer.
269
+ //
270
+ // ── Dual-auth (Cognito + IAM) for deploy-time system callers ────────────────
271
+ // A field flagged `systemCallable` (via `mutationSchemaEntry`/`querySchemaEntry`)
272
+ // must be reachable by BOTH the console UI (Cognito) and a deploy-time system
273
+ // caller signing SigV4 with ambient AWS credentials (`Util_AppSync_Caller`,
274
+ // the AWS_IAM additional auth provider). `@aws_auth(...)` is the single-mode
275
+ // directive form and does not admit IAM on a multi-auth API. For these fields
276
+ // we emit the multi-auth form instead:
277
+ // @aws_cognito_user_pools(cognito_groups: [...]) @aws_iam
278
+ // The Cognito arm preserves the field's existing group gating (or, when the
279
+ // field carries no Cognito group restriction, a bare `@aws_cognito_user_pools`
280
+ // keeps it open to any authenticated Cognito user); the `@aws_iam` arm admits
281
+ // the system caller. IAM access must be constrained by the API resource policy
282
+ // and a least-privilege deploy-role policy — see
283
+ // `docs/guides/appsync-iam-system-caller.md`.
269
284
 
270
285
  let _permissionToCognitoGroups = (
271
286
  permission: Reventless.Authorization.permission,
@@ -282,6 +297,20 @@ let _formatGroupsDirective = (groups: array<string>): string => {
282
297
  `@aws_auth(cognito_groups: [${quoted}])`
283
298
  }
284
299
 
300
+ // Multi-auth directive for a field that must accept BOTH Cognito and IAM.
301
+ // `groups=Some([...])` preserves Cognito group gating; `groups=None` keeps the
302
+ // field open to any authenticated Cognito user. `@aws_iam` admits the
303
+ // deploy-time SigV4 system caller. See the dual-auth note above.
304
+ let _formatDualAuthDirective = (groups: option<array<string>>): string => {
305
+ let cognito = switch groups {
306
+ | Some(g) =>
307
+ let quoted = g->Array.map(x => `"${x}"`)->Array.join(", ")
308
+ `@aws_cognito_user_pools(cognito_groups: [${quoted}])`
309
+ | None => `@aws_cognito_user_pools`
310
+ }
311
+ `${cognito} @aws_iam`
312
+ }
313
+
285
314
  let injectAwsAuth = (
286
315
  fragment: Reventless.Plugin.apiSchemaFragment,
287
316
  ~mutationEntries: array<ReventlessInfra.Api.mutationSchemaEntry>,
@@ -292,8 +321,16 @@ let injectAwsAuth = (
292
321
  // Build authorization lookup from mutation entries: fieldName -> groups.
293
322
  // Spec-level `fieldPermissions` takes precedence over the legacy
294
323
  // `authorization.group` per-entry single-group form.
324
+ // Fields opted into deploy-time IAM invocation (dual-auth). Membership here
325
+ // switches a field from the single-mode `@aws_auth` form to the multi-auth
326
+ // `@aws_cognito_user_pools(...) @aws_iam` form.
327
+ let iamFields: Dict.t<bool> = Dict.make()
328
+
295
329
  let mutationAuthMap: Dict.t<array<string>> = Dict.make()
296
330
  mutationEntries->Array.forEach(entry => {
331
+ if entry.systemCallable->Option.getOr(false) {
332
+ entry.fieldNames->Array.forEach(fieldName => iamFields->Dict.set(fieldName, true))
333
+ }
297
334
  switch entry.authorization {
298
335
  | Some({group}) =>
299
336
  entry.fieldNames->Array.forEach(fieldName =>
@@ -319,6 +356,10 @@ let injectAwsAuth = (
319
356
  // Spec-level `permission` takes precedence over the legacy authorization.
320
357
  let queryAuthMap: Dict.t<array<string>> = Dict.make()
321
358
  queryEntries->Array.forEach(entry => {
359
+ if entry.systemCallable->Option.getOr(false) {
360
+ iamFields->Dict.set(entry.singleFieldName, true)
361
+ iamFields->Dict.set(entry.listFieldName, true)
362
+ }
322
363
  switch entry.authorization {
323
364
  | Some({group}) =>
324
365
  queryAuthMap->Dict.set(entry.singleFieldName, [group])
@@ -341,17 +382,27 @@ let injectAwsAuth = (
341
382
 
342
383
  let augmentedMutations = parts.mutations->Array.map(field => {
343
384
  let fieldName = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
344
- switch mutationAuthMap->Dict.get(fieldName) {
345
- | Some(groups) => `${field}\n ${_formatGroupsDirective(groups)}`
346
- | None => field
385
+ let groups = mutationAuthMap->Dict.get(fieldName)
386
+ if iamFields->Dict.get(fieldName)->Option.getOr(false) {
387
+ `${field}\n ${_formatDualAuthDirective(groups)}`
388
+ } else {
389
+ switch groups {
390
+ | Some(groups) => `${field}\n ${_formatGroupsDirective(groups)}`
391
+ | None => field
392
+ }
347
393
  }
348
394
  })
349
395
 
350
396
  let augmentedQueries = parts.queries->Array.map(field => {
351
397
  let fieldName = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
352
- switch queryAuthMap->Dict.get(fieldName) {
353
- | Some(groups) => `${field} ${_formatGroupsDirective(groups)}`
354
- | None => field
398
+ let groups = queryAuthMap->Dict.get(fieldName)
399
+ if iamFields->Dict.get(fieldName)->Option.getOr(false) {
400
+ `${field} ${_formatDualAuthDirective(groups)}`
401
+ } else {
402
+ switch groups {
403
+ | Some(groups) => `${field} ${_formatGroupsDirective(groups)}`
404
+ | None => field
405
+ }
355
406
  }
356
407
  })
357
408
 
@@ -374,17 +425,30 @@ let injectAwsAuth = (
374
425
  // Injects @aws_auth with the given group on ALL mutation, query, and subscription
375
426
  // fields in a fragment. Used for the base fragment where all fields share the
376
427
  // same authorization group.
428
+ //
429
+ // `~iamFieldNames` opts the named mutation/query fields into deploy-time IAM
430
+ // (dual-auth): those fields emit `@aws_cognito_user_pools(cognito_groups:
431
+ // ["<group>"]) @aws_iam` instead of the single-mode `@aws_auth(...)`, keeping
432
+ // the same Cognito group gating while also admitting the SigV4 system caller.
433
+ // Subscriptions are never IAM-marked (the deploy caller does not subscribe).
377
434
  let injectAwsAuthAll = (
378
435
  fragment: Reventless.Plugin.apiSchemaFragment,
379
436
  ~group: string,
437
+ ~iamFieldNames: array<string>=[],
380
438
  ): Reventless.Plugin.apiSchemaFragment => {
381
439
  let parts = ReventlessCore.GraphQL_Stitcher.decode(fragment)
440
+ let isIam = (field: string): bool =>
441
+ iamFieldNames->Array.includes(ReventlessCore.GraphQL_Stitcher.extractLeadingName(field))
382
442
 
383
443
  let augmentedMutations = parts.mutations->Array.map(field =>
384
- `${field}\n @aws_auth(cognito_groups: ["${group}"])`
444
+ isIam(field)
445
+ ? `${field}\n ${_formatDualAuthDirective(Some([group]))}`
446
+ : `${field}\n @aws_auth(cognito_groups: ["${group}"])`
385
447
  )
386
448
  let augmentedQueries = parts.queries->Array.map(field =>
387
- `${field} @aws_auth(cognito_groups: ["${group}"])`
449
+ isIam(field)
450
+ ? `${field} ${_formatDualAuthDirective(Some([group]))}`
451
+ : `${field} @aws_auth(cognito_groups: ["${group}"])`
388
452
  )
389
453
  let augmentedSubscriptions = parts.subscriptions->Array.map(field =>
390
454
  `${field}\n @aws_auth(cognito_groups: ["${group}"])`
@@ -204,10 +204,27 @@ function _formatGroupsDirective(groups) {
204
204
  return `@aws_auth(cognito_groups: [` + quoted + `])`;
205
205
  }
206
206
 
207
+ function _formatDualAuthDirective(groups) {
208
+ let cognito;
209
+ if (groups !== undefined) {
210
+ let quoted = groups.map(x => `"` + x + `"`).join(", ");
211
+ cognito = `@aws_cognito_user_pools(cognito_groups: [` + quoted + `])`;
212
+ } else {
213
+ cognito = `@aws_cognito_user_pools`;
214
+ }
215
+ return cognito + ` @aws_iam`;
216
+ }
217
+
207
218
  function injectAwsAuth(fragment, mutationEntries, queryEntries) {
208
219
  let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
220
+ let iamFields = {};
209
221
  let mutationAuthMap = {};
210
222
  mutationEntries.forEach(entry => {
223
+ if (Stdlib_Option.getOr(entry.systemCallable, false)) {
224
+ entry.fieldNames.forEach(fieldName => {
225
+ iamFields[fieldName] = true;
226
+ });
227
+ }
211
228
  let match = entry.authorization;
212
229
  if (match !== undefined) {
213
230
  let group = match.group;
@@ -232,6 +249,10 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
232
249
  });
233
250
  let queryAuthMap = {};
234
251
  queryEntries.forEach(entry => {
252
+ if (Stdlib_Option.getOr(entry.systemCallable, false)) {
253
+ iamFields[entry.singleFieldName] = true;
254
+ iamFields[entry.listFieldName] = true;
255
+ }
235
256
  let match = entry.authorization;
236
257
  if (match !== undefined) {
237
258
  let group = match.group;
@@ -255,7 +276,9 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
255
276
  let augmentedMutations = parts.mutations.map(field => {
256
277
  let fieldName = GraphQL_Stitcher$ReventlessCore.extractLeadingName(field);
257
278
  let groups = mutationAuthMap[fieldName];
258
- if (groups !== undefined) {
279
+ if (Stdlib_Option.getOr(iamFields[fieldName], false)) {
280
+ return field + `\n ` + _formatDualAuthDirective(groups);
281
+ } else if (groups !== undefined) {
259
282
  return field + `\n ` + _formatGroupsDirective(groups);
260
283
  } else {
261
284
  return field;
@@ -264,7 +287,9 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
264
287
  let augmentedQueries = parts.queries.map(field => {
265
288
  let fieldName = GraphQL_Stitcher$ReventlessCore.extractLeadingName(field);
266
289
  let groups = queryAuthMap[fieldName];
267
- if (groups !== undefined) {
290
+ if (Stdlib_Option.getOr(iamFields[fieldName], false)) {
291
+ return field + ` ` + _formatDualAuthDirective(groups);
292
+ } else if (groups !== undefined) {
268
293
  return field + ` ` + _formatGroupsDirective(groups);
269
294
  } else {
270
295
  return field;
@@ -294,10 +319,23 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
294
319
  };
295
320
  }
296
321
 
297
- function injectAwsAuthAll(fragment, group) {
322
+ function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
323
+ let iamFieldNames = iamFieldNamesOpt !== undefined ? iamFieldNamesOpt : [];
298
324
  let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
299
- let augmentedMutations = parts.mutations.map(field => field + `\n @aws_auth(cognito_groups: ["` + group + `"])`);
300
- let augmentedQueries = parts.queries.map(field => field + ` @aws_auth(cognito_groups: ["` + group + `"])`);
325
+ let augmentedMutations = parts.mutations.map(field => {
326
+ if (iamFieldNames.includes(GraphQL_Stitcher$ReventlessCore.extractLeadingName(field))) {
327
+ return field + `\n ` + _formatDualAuthDirective([group]);
328
+ } else {
329
+ return field + `\n @aws_auth(cognito_groups: ["` + group + `"])`;
330
+ }
331
+ });
332
+ let augmentedQueries = parts.queries.map(field => {
333
+ if (iamFieldNames.includes(GraphQL_Stitcher$ReventlessCore.extractLeadingName(field))) {
334
+ return field + ` ` + _formatDualAuthDirective([group]);
335
+ } else {
336
+ return field + ` @aws_auth(cognito_groups: ["` + group + `"])`;
337
+ }
338
+ });
301
339
  let augmentedSubscriptions = parts.subscriptions.map(field => field + `\n @aws_auth(cognito_groups: ["` + group + `"])`);
302
340
  let encoded = JSON.stringify(Object.fromEntries([
303
341
  [
@@ -359,7 +397,7 @@ function generateFragment(mutationEntries, queryEntries) {
359
397
  }
360
398
 
361
399
  function updateSchema(api, baseFragment, pluginFragments) {
362
- let augmentedBaseFragment = injectAwsAuthAll(baseFragment, "Admin");
400
+ let augmentedBaseFragment = injectAwsAuthAll(baseFragment, "Admin", undefined);
363
401
  let sdl = GraphQL_Stitcher$ReventlessCore.stitch(augmentedBaseFragment, pluginFragments);
364
402
  let resultPromise = {
365
403
  contents: Promise.resolve()
@@ -387,6 +425,7 @@ export {
387
425
  getClient,
388
426
  _permissionToCognitoGroups,
389
427
  _formatGroupsDirective,
428
+ _formatDualAuthDirective,
390
429
  injectAwsAuth,
391
430
  injectAwsAuthAll,
392
431
  makeApiResource,
@@ -21,7 +21,7 @@ module Make = (HooksConfig: ReventlessCore.Plugin_Helpers.HooksConfig) => {
21
21
  CommandTopicRemoteChannel.SQS,
22
22
  HeartbeatRunner.CloudwatchEvents,
23
23
  PluginRuntime_Builder.Make(EventCollectorChannel),
24
- DcbEventLogStorage.DynamoDb,
24
+ DcbEventLogStorage.Selectable,
25
25
  EventTopicPublisher.DynamoDbStream,
26
26
  CommandTopicChannel.SQS_Sync,
27
27
  CommandTopicChannel.SQS_Async,
@@ -4,13 +4,13 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
4
  import * as Pulumi from "@pulumi/pulumi";
5
5
  import * as AppSync_Adapter$ReventlessAws from "./Api/AppSync_Adapter.res.mjs";
6
6
  import * as Plugin_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Builder.res.mjs";
7
+ import * as DcbEventLogStorage$ReventlessAws from "../adapter/DcbEventLog/DcbEventLogStorage.res.mjs";
7
8
  import * as Util_ResourceNaming$ReventlessAws from "../util/Util_ResourceNaming.res.mjs";
8
9
  import * as QueryEngine_DynamoDb$ReventlessAws from "../adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs";
9
10
  import * as PluginRuntime_Builder$ReventlessAws from "../plugin/runtime/PluginRuntime_Builder.res.mjs";
10
11
  import * as PluginRuntimeOperations$ReventlessAws from "../plugin/runtime/PluginRuntimeOperations.res.mjs";
11
12
  import * as EventCollectorChannel_SQS$ReventlessAws from "../adapter/EventCollector/EventCollectorChannel_SQS.res.mjs";
12
13
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "../adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs";
13
- import * as DcbEventLogStorage_DynamoDb$ReventlessAws from "../adapter/DcbEventLog/DcbEventLogStorage_DynamoDb.res.mjs";
14
14
  import * as CommandTopicChannel_SQS_Sync$ReventlessAws from "../adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs";
15
15
  import * as CommandTopicChannel_SQS_Async$ReventlessAws from "../adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs";
16
16
  import * as CommandTopicRemoteChannel_SQS$ReventlessAws from "../adapter/CommandTopic/CommandTopicRemoteChannel_SQS.res.mjs";
@@ -44,7 +44,7 @@ function Make(HooksConfig) {
44
44
  })(CommandTopicRemoteChannel_SQS$ReventlessAws)(HeartbeatRunner_CloudWatchEvents$ReventlessAws)(PluginRuntime_Builder$ReventlessAws.Make({
45
45
  make: EventCollectorChannel_SQS$ReventlessAws.make,
46
46
  connect: EventCollectorChannel_SQS$ReventlessAws.connect
47
- }))(DcbEventLogStorage_DynamoDb$ReventlessAws)(EventTopicPublisher_DynamoDbStream$ReventlessAws)({
47
+ }))(DcbEventLogStorage$ReventlessAws.Selectable)(EventTopicPublisher_DynamoDbStream$ReventlessAws)({
48
48
  make: CommandTopicChannel_SQS_Sync$ReventlessAws.make
49
49
  })({
50
50
  make: CommandTopicChannel_SQS_Async$ReventlessAws.make
@@ -66,7 +66,7 @@ function forCommandTopic(param, connect, memorySizeOpt, timeoutOpt, param$1, par
66
66
  });
67
67
  envVars["HANDLER_CONFIG"] = handlerConfigJson;
68
68
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs", {}, undefined);
69
- return connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, opts));
69
+ return connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, undefined, opts));
70
70
  }
71
71
 
72
72
  function finish() {
@@ -196,6 +196,26 @@ module Make = (
196
196
  let channelParts: Util.SQS.channelParts = Obj.magic(channel.parts)
197
197
  let queue = channelParts.queue
198
198
 
199
+ // B2.3d: if this plugin's DCB log is Postgres-backed, hand its EventCollector
200
+ // SQS queue to the change-feed relay registry. `resource.name` is the plugin
201
+ // component name (`<plugin>Plugin`), so the canonical log_name is
202
+ // `<plugin>DcbEventLog` — the key DcbEventLogStorage_Postgres registered under.
203
+ if DcbBackend.isPostgres() {
204
+ switch resource.name {
205
+ | Some(compName) =>
206
+ let pluginName =
207
+ compName->String.endsWith("Plugin")
208
+ ? compName->String.slice(~start=0, ~end=compName->String.length - 6)
209
+ : compName
210
+ DcbBackend.attachCollectorQueue(
211
+ ~logName=pluginName ++ "DcbEventLog",
212
+ ~url=queue.id,
213
+ ~arn=queue.arn,
214
+ )
215
+ | None => ()
216
+ }
217
+ }
218
+
199
219
  // Pull the per-EventCollector context registered by Plugin_Helpers.connect
200
220
  // (plugins) or synthesise an admin one if none registered.
201
221
  let context: ReventlessCore.Plugin_Helpers.eventCollectorContext =
@@ -7,6 +7,7 @@ import * as Pulumi from "@pulumi/pulumi";
7
7
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
8
8
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
9
9
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
10
+ import * as DcbBackend$ReventlessAws from "../../adapter/DcbEventLog/DcbBackend.res.mjs";
10
11
  import * as Heartbeat$ReventlessCore from "@reventlessdev/reventless-core/src/components/Heartbeat/Heartbeat.res.mjs";
11
12
  import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
12
13
  import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
@@ -166,6 +167,13 @@ function Make(EventCollectorChannel) {
166
167
  let channel = eventCollector.channel;
167
168
  let channelParts = channel.parts;
168
169
  let queue = channelParts.queue;
170
+ if (DcbBackend$ReventlessAws.isPostgres()) {
171
+ let compName = resource.__name;
172
+ if (compName !== undefined) {
173
+ let pluginName = compName.endsWith("Plugin") ? compName.slice(0, compName.length - 6 | 0) : compName;
174
+ DcbBackend$ReventlessAws.attachCollectorQueue(pluginName + "DcbEventLog", queue.id, queue.arn);
175
+ }
176
+ }
169
177
  let ctx = Plugin_Helpers$ReventlessCore.eventCollectorContextRef.contents[name];
170
178
  let context = ctx !== undefined ? ctx : synthesizeAdminContext();
171
179
  let envVars = {};
@@ -300,7 +308,7 @@ function Make(EventCollectorChannel) {
300
308
  });
301
309
  let codeOutput = bundleOutput.apply(b => b.code);
302
310
  let sourceCodeHashOutput = bundleOutput.apply(b => b.sourceCodeHash);
303
- let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, codeOutput, sourceCodeHashOutput, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, opts);
311
+ let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, codeOutput, sourceCodeHashOutput, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, undefined, opts);
304
312
  EventCollectorChannel.connect(name, [{
305
313
  channel: eventCollector.channel,
306
314
  eventTopics: eventTopics,
@@ -422,7 +430,7 @@ function Make(EventCollectorChannel) {
422
430
  envVars["PLUGIN_ID"] = Pulumi.output(hbConfig.pluginId);
423
431
  envVars["HEARTBEAT_TIMEOUT"] = Pulumi.output(hbConfig.heartbeatTimeout.toString());
424
432
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/HeartbeatEntryPoint.mjs", {}, undefined);
425
- connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, opts));
433
+ connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, undefined, opts));
426
434
  };
427
435
  let forDcbCommandTopic = (param, connect, $staropt$star, $staropt$star$1, dcbCommandTopic) => {
428
436
  $staropt$star !== undefined;
@@ -347,6 +347,146 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
347
347
  })
348
348
  })
349
349
 
350
+ // ── Dual-auth (Cognito + IAM) for deploy-time system callers ────────────────
351
+ //
352
+ // A field flagged `systemCallable` must be reachable by BOTH the console UI
353
+ // (Cognito) and the deploy-time SigV4 system caller (AWS_IAM). It switches from
354
+ // the single-mode `@aws_auth(...)` form to the multi-auth
355
+ // `@aws_cognito_user_pools(...) @aws_iam` form.
356
+
357
+ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
358
+ let makeFragment = (mutationFields, queryFields) =>
359
+ ReventlessCore.GraphQL_Stitcher.encode({
360
+ types: [],
361
+ mutations: mutationFields,
362
+ queries: queryFields,
363
+ subscriptions: [],
364
+ })
365
+
366
+ testSync("systemCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
367
+ let entry: ReventlessInfra.Api.mutationSchemaEntry = {
368
+ fieldNames: ["p_Sync"],
369
+ commandSchema: S.unknown,
370
+ fieldPermissions: Dict.fromArray([("p_Sync", Reventless.Authorization.AllowGroups(["Admin"]))]),
371
+ systemCallable: true,
372
+ }
373
+ let frag = makeFragment(["p_Sync(id: ID!): String"], [])
374
+ let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
375
+ let m = decodeFragment(aug).mutations->Array.getUnsafe(0)
376
+ expect(m)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
377
+ expect(m)->toContain("@aws_iam")
378
+ // Must NOT emit the single-mode form, which does not admit IAM.
379
+ expect(m)->not_->toContain("@aws_auth")
380
+ })
381
+
382
+ testSync("systemCallable mutation without a group restriction stays open to Cognito + IAM", () => {
383
+ let entry: ReventlessInfra.Api.mutationSchemaEntry = {
384
+ fieldNames: ["p_Sync"],
385
+ commandSchema: S.unknown,
386
+ fieldPermissions: Dict.fromArray([("p_Sync", Reventless.Authorization.AllowAuthenticated)]),
387
+ systemCallable: true,
388
+ }
389
+ let frag = makeFragment(["p_Sync(id: ID!): String"], [])
390
+ let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
391
+ let m = decodeFragment(aug).mutations->Array.getUnsafe(0)
392
+ // Bare @aws_cognito_user_pools (any authenticated Cognito user) + IAM.
393
+ expect(m)->toContain("@aws_cognito_user_pools @aws_iam")
394
+ expect(m)->not_->toContain("cognito_groups")
395
+ expect(m)->not_->toContain("@aws_auth")
396
+ })
397
+
398
+ testSync("a non-systemCallable sibling keeps single-mode @aws_auth (no @aws_iam)", () => {
399
+ let entry: ReventlessInfra.Api.mutationSchemaEntry = {
400
+ fieldNames: ["p_Sync", "p_Other"],
401
+ commandSchema: S.unknown,
402
+ fieldPermissions: Dict.fromArray([
403
+ ("p_Sync", Reventless.Authorization.AllowGroups(["Admin"])),
404
+ ("p_Other", Reventless.Authorization.AllowGroups(["Admin"])),
405
+ ]),
406
+ // systemCallable applies to every field in the entry; split the fields so only
407
+ // p_Sync opts in.
408
+ systemCallable: false,
409
+ }
410
+ let iamEntry: ReventlessInfra.Api.mutationSchemaEntry = {
411
+ fieldNames: ["p_Sync"],
412
+ commandSchema: S.unknown,
413
+ fieldPermissions: Dict.fromArray([("p_Sync", Reventless.Authorization.AllowGroups(["Admin"]))]),
414
+ systemCallable: true,
415
+ }
416
+ let frag = makeFragment(["p_Sync(id: ID!): String", "p_Other(id: ID!): String"], [])
417
+ let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry, iamEntry], ~queryEntries=[])
418
+ let parts = decodeFragment(aug)
419
+ switch parts.mutations->Array.find(f => f->String.includes("p_Other")) {
420
+ | Some(other) =>
421
+ expect(other)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
422
+ expect(other)->not_->toContain("@aws_iam")
423
+ | None => JsError.throwWithMessage("missing p_Other field")
424
+ }
425
+ })
426
+
427
+ testSync("systemCallable query applies dual-auth to BOTH single and list fields", () => {
428
+ let entry: ReventlessInfra.Api.querySchemaEntry = {
429
+ singleFieldName: "p_Item",
430
+ listFieldName: "p_Items",
431
+ returnTypeName: "p_Item",
432
+ stateSchema: S.unknown,
433
+ authorization: None,
434
+ permission: Reventless.Authorization.AllowGroups(["Admin"]),
435
+ systemCallable: true,
436
+ }
437
+ let frag = makeFragment(
438
+ [],
439
+ ["p_Item(id: ID!): Item", "p_Items(first: Int, after: String): ItemConnection!"],
440
+ )
441
+ let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[], ~queryEntries=[entry])
442
+ let parts = decodeFragment(aug)
443
+ switch (
444
+ parts.queries->Array.find(f => f->String.includes("p_Item(")),
445
+ parts.queries->Array.find(f => f->String.includes("p_Items(")),
446
+ ) {
447
+ | (Some(single), Some(list)) =>
448
+ expect(single)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
449
+ expect(single)->toContain("@aws_iam")
450
+ expect(list)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
451
+ expect(list)->toContain("@aws_iam")
452
+ | _ => JsError.throwWithMessage("missing query fields")
453
+ }
454
+ })
455
+ })
456
+
457
+ describe("AppSync_Adapter.injectAwsAuthAll — ~iamFieldNames", () => {
458
+ let baseFragment = ReventlessCore.AdminApi.baseFragment(~cloner=true)
459
+
460
+ testSync("marks only the named field dual-auth, leaving others single-mode", () => {
461
+ // Pick a real mutation field name from the admin base fragment.
462
+ let firstMutationName =
463
+ decodeFragment(baseFragment).mutations
464
+ ->Array.getUnsafe(0)
465
+ ->ReventlessCore.GraphQL_Stitcher.extractLeadingName
466
+ let augmented =
467
+ AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin", ~iamFieldNames=[firstMutationName])
468
+ let parts = decodeFragment(augmented)
469
+ parts.mutations->Array.forEach(field => {
470
+ let name = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
471
+ if name == firstMutationName {
472
+ expect(field)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
473
+ expect(field)->toContain("@aws_iam")
474
+ expect(field)->not_->toContain("@aws_auth")
475
+ } else {
476
+ expect(field)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
477
+ expect(field)->not_->toContain("@aws_iam")
478
+ }
479
+ })
480
+ })
481
+
482
+ testSync("default (no ~iamFieldNames) leaves every field single-mode @aws_auth", () => {
483
+ let augmented = AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin")
484
+ let parts = decodeFragment(augmented)
485
+ parts.mutations->Array.forEach(field => expect(field)->not_->toContain("@aws_iam"))
486
+ parts.queries->Array.forEach(field => expect(field)->not_->toContain("@aws_iam"))
487
+ })
488
+ })
489
+
350
490
  describe("Split mode — empty base fragment", () => {
351
491
  testSync("empty stitcher encode produces fragment with no fields", () => {
352
492
  let emptyFragment = ReventlessCore.GraphQL_Stitcher.encode({