@reventlessdev/reventless-aws 3.0.0-alpha.222 → 3.0.0-alpha.224

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 (30) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +6 -6
  3. package/src/Platform.res +3 -3
  4. package/src/Platform.res.mjs +3 -3
  5. package/src/adapter/Adapter_Helpers.res +6 -0
  6. package/src/adapter/Adapter_Helpers.res.mjs +6 -0
  7. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res +1 -1
  8. package/src/adapter/CommandTopic/CommandTopicChannel_Helpers.res +65 -47
  9. package/src/adapter/CommandTopic/CommandTopicChannel_Helpers.res.mjs +52 -39
  10. package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res +61 -59
  11. package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res.mjs +10 -16
  12. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +32 -30
  13. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +20 -22
  14. package/src/adapter/Mcp/MCP_Lambda.res +4 -2
  15. package/src/adapter/Mcp/MCP_Lambda.res.mjs +3 -2
  16. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +1 -1
  17. package/src/adapter/Runtime/{AdminEventCollectorEntryPoint.mjs → EventCollectorEntryPoint.mjs} +5 -5
  18. package/src/components/Api/AppSync_SdlDecorate.res +1 -1
  19. package/src/plugin/cloner/ClonerRunner_Fargate.res +1 -1
  20. package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +2 -2
  21. package/src/plugin/runtime/PluginRuntime_Builder.res +9 -5
  22. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +3 -2
  23. package/src/util/Util_DeadLetterQueue.res +13 -3
  24. package/src/util/Util_DeadLetterQueue.res.mjs +2 -2
  25. package/tests/AppSync_AdapterTest.res +7 -7
  26. package/tests/AppSync_AdapterTest.res.mjs +8 -8
  27. package/tests/CommandTopicQueuePolicyTest.res +65 -0
  28. package/tests/CommandTopicQueuePolicyTest.res.mjs +53 -0
  29. package/tests/MCP_LambdaTest.res +1 -1
  30. package/tests/MCP_LambdaTest.res.mjs +2 -2
@@ -114,38 +114,40 @@ let make = (
114
114
 
115
115
  // SQS queue policy — allow SNS to send messages
116
116
  let snsResource = eventTopicOutputs.resources->Array.getUnsafe(0)
117
- let _ =
118
- (queue.arn, queue.id, snsResource.urn)
119
- ->Pulumi.Output.all3
120
- ->Pulumi.Output.apply(((queueArn, queueId, snsUrn)) => {
117
+ // The document is a *value* derived from resolved ARNs, so it is built in an
118
+ // apply. The QueuePolicy itself stays outside: passing `queue.id` as an Output
119
+ // is what registers the policy -> queue dependency, without which Pulumi has
120
+ // no ordering constraint and can delete the queue first on a replacement,
121
+ // leaving the policy's delete polling a queue that no longer exists.
122
+ let queuePolicyDocument =
123
+ (queue.arn, snsResource.urn)
124
+ ->Pulumi.Output.all2
125
+ ->Pulumi.Output.apply(((queueArn, snsUrn)) => {
121
126
  open PolicyDocument
122
- let _queuePolicy = SQS.QueuePolicy.make(
123
- ~name=name ++ "EventLogSubQueuePolicy",
124
- ~args={
125
- queueUrl: queueId->Pulumi.Input.make,
126
- policy: PolicyDocument.make(
127
- ~id=name ++ "EventLogSubQueuePolicy",
128
- ~statements=[
129
- {
130
- sid: "AllowSNSSend",
131
- principal: Principals({service: PrincipalIds([AWS.SNS.principal])}),
132
- effect: Allow,
133
- actions: Actions(["sqs:SendMessage"]),
134
- resources: Resource(queueArn),
135
- conditions: {
136
- arnEquals: [
137
- ("aws:SourceArn", ConditionValues([snsUrn])),
138
- ]->Dict.fromArray,
139
- },
140
- },
141
- ],
142
- )
143
- ->PolicyDocument.toJsonString
144
- ->Pulumi.Input.make,
145
- },
146
- ~opts=Some(opts),
147
- )
127
+ PolicyDocument.make(
128
+ ~id=name ++ "EventLogSubQueuePolicy",
129
+ ~statements=[
130
+ {
131
+ sid: "AllowSNSSend",
132
+ principal: Principals({service: PrincipalIds([AWS.SNS.principal])}),
133
+ effect: Allow,
134
+ actions: Actions(["sqs:SendMessage"]),
135
+ resources: Resource(queueArn),
136
+ conditions: {
137
+ arnEquals: [("aws:SourceArn", ConditionValues([snsUrn]))]->Dict.fromArray,
138
+ },
139
+ },
140
+ ],
141
+ )->PolicyDocument.toJsonString
148
142
  })
143
+ let _queuePolicy = SQS.QueuePolicy.make(
144
+ ~name=name ++ "EventLogSubQueuePolicy",
145
+ ~args={
146
+ queueUrl: queue.id->Pulumi.Output.asInput,
147
+ policy: queuePolicyDocument->Pulumi.Output.asInput,
148
+ },
149
+ ~opts=Some(opts),
150
+ )
149
151
 
150
152
  // SNS → SQS subscription (raw delivery so body IS the event JSON)
151
153
  let _subscription = Util_SQS.subscribeToSnsTopic(
@@ -85,30 +85,28 @@ function make(name, topicName, eventTopicOutputs, eventsApi, opts) {
85
85
  sqsManagedSseEnabled: false
86
86
  }, opts);
87
87
  let snsResource = eventTopicOutputs.resources[0];
88
- Pulumi.all([
88
+ let queuePolicyDocument = Pulumi.all([
89
89
  queue.arn,
90
- queue.id,
91
90
  snsResource.urn
92
- ]).apply(param => {
93
- new (Aws.sqs.QueuePolicy)(name + "EventLogSubQueuePolicy", {
94
- policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "EventLogSubQueuePolicy", [{
95
- Sid: "AllowSNSSend",
96
- Principal: {
97
- Service: [AWS$ReventlessAws.SNS.principal]
98
- },
99
- Effect: "Allow",
100
- Action: ["sqs:SendMessage"],
101
- Resource: param[0],
102
- Condition: {
103
- ArnEquals: Object.fromEntries([[
104
- "aws:SourceArn",
105
- [param[2]]
106
- ]])
107
- }
108
- }])),
109
- queueUrl: param[1]
110
- }, opts);
111
- });
91
+ ]).apply(param => PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "EventLogSubQueuePolicy", [{
92
+ Sid: "AllowSNSSend",
93
+ Principal: {
94
+ Service: [AWS$ReventlessAws.SNS.principal]
95
+ },
96
+ Effect: "Allow",
97
+ Action: ["sqs:SendMessage"],
98
+ Resource: param[0],
99
+ Condition: {
100
+ ArnEquals: Object.fromEntries([[
101
+ "aws:SourceArn",
102
+ [param[1]]
103
+ ]])
104
+ }
105
+ }])));
106
+ new (Aws.sqs.QueuePolicy)(name + "EventLogSubQueuePolicy", {
107
+ policy: queuePolicyDocument,
108
+ queueUrl: queue.id
109
+ }, opts);
112
110
  Util_SQS$ReventlessAws.subscribeToSnsTopic(queue, name + "EventLogSub", name, snsResource, opts);
113
111
  let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "EventLogSubRole", Pulumi.output(AWS$ReventlessAws.Lambda.principal), AWS_Tags$ReventlessAws.make(name + "EventLogSubRole", EventTopic$ReventlessCore.componentType, "Identity", undefined, name, undefined, undefined, undefined), opts);
114
112
  Pulumi.all([
@@ -119,8 +119,10 @@ let generateAdminConfig = (
119
119
  generateConfig(
120
120
  ~serverName=`${serverName}-admin`,
121
121
  ~serverVersion,
122
- ~pluginName="Admin",
123
- ~mutationEntries=ReventlessCore.AdminApi.mutationEntries(~cloner),
122
+ // Must match what Platform_Admin.construct registers — it forms the resource URI
123
+ // templates (`<pluginName>/<field>/{id}`) that MCP clients address.
124
+ ~pluginName=ReventlessCore.Platform_Admin_Structure.pluginId,
125
+ ~mutationEntries=ReventlessCore.Platform_AdminApi.mutationEntries(~cloner),
124
126
  ~queryEntries=ReventlessCore.PluginBaseFragment.queryEntries,
125
127
  ~commandTopicArns,
126
128
  ~queryDbTableNames,
@@ -11,11 +11,12 @@ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js
11
11
  import * as AWS$ReventlessAws from "../AWS.res.mjs";
12
12
  import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
13
13
  import * as Runtime$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Runtime/Runtime.res.mjs";
14
- import * as AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/AdminApi.res.mjs";
15
14
  import * as DynamoDb_Error$ReventlessAws from "../../errors/DynamoDb_Error.res.mjs";
15
+ import * as Platform_AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminApi.res.mjs";
16
16
  import * as PluginBaseFragment$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/api/PluginBaseFragment.res.mjs";
17
17
  import * as MCP_SchemaGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/MCP_SchemaGenerator.res.mjs";
18
18
  import * as Util_DynamoDb_Runtime$ReventlessAws from "../../util/Util_DynamoDb_Runtime.res.mjs";
19
+ import * as Platform_Admin_Structure$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin_Structure.res.mjs";
19
20
  import * as DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws from "../DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs";
20
21
  import * as CommandTopicChannel_SQS_RuntimeResMjs from "@reventlessdev/reventless-aws/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
21
22
  import * as CommandGenerator_CallbackResMjs from "@reventlessdev/reventless-core/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs";
@@ -69,7 +70,7 @@ function generateAdminConfig(serverName, serverVersion, clonerOpt, commandTopicA
69
70
  let cloner = clonerOpt !== undefined ? clonerOpt : false;
70
71
  let commandTopicArns = commandTopicArnsOpt !== undefined ? commandTopicArnsOpt : ({});
71
72
  let queryDbTableNames = queryDbTableNamesOpt !== undefined ? queryDbTableNamesOpt : ({});
72
- return generateConfig(serverName + `-admin`, serverVersion, "Admin", AdminApi$ReventlessCore.mutationEntries(cloner), PluginBaseFragment$ReventlessCore.queryEntries, undefined, commandTopicArns, queryDbTableNames, undefined);
73
+ return generateConfig(serverName + `-admin`, serverVersion, Platform_Admin_Structure$ReventlessCore.pluginId, Platform_AdminApi$ReventlessCore.mutationEntries(cloner), PluginBaseFragment$ReventlessCore.queryEntries, undefined, commandTopicArns, queryDbTableNames, undefined);
73
74
  }
74
75
 
75
76
  function extractEntityId(uri) {
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Drains a Postgres DCB log via `PgChangeFeed` and pushes each event — transformed
4
4
  // into the `{id, meta, event}` shape the plugin EventCollector SQS handler
5
- // consumes — onto that queue. The plugin EventCollector (`AdminEventCollectorEntryPoint`)
5
+ // consumes — onto that queue. The plugin EventCollector (`EventCollectorEntryPoint`)
6
6
  // then drives the full fan-out: read-model projections, aggregate command topics,
7
7
  // and the cross-plugin SNS EventTopic. See
8
8
  // docs/plans/aws-postgres-change-feed-bridge.md.
@@ -1,6 +1,6 @@
1
- // Plugin EventCollector Lambda entry point — shared between the admin and every
2
- // per-plugin EventCollector Lambda. Despite the historical "Admin" filename, this
3
- // module is plugin-agnostic: behaviour is driven entirely by HANDLER_CONFIG.
1
+ // Plugin EventCollector Lambda entry point — shared between the platform and every
2
+ // per-plugin EventCollector Lambda. This module is plugin-agnostic: behaviour is
3
+ // driven entirely by HANDLER_CONFIG.
4
4
  //
5
5
  // HANDLER_CONFIG schema (JSON in env var "HANDLER_CONFIG"):
6
6
  //
@@ -411,7 +411,7 @@ function loadUiFragments() {
411
411
  // Cross-plugin spec packages (e.g. "@reventlessdev/online-shop-hybrid-catalog-spec")
412
412
  // are bundled into the function asset under /var/task/node_modules/ by
413
413
  // PluginRuntime_Builder.forPluginEventCollector. This entry-point file, however,
414
- // lives in the Lambda LAYER at /opt/nodejs/node_modules/.../AdminEventCollectorEntryPoint.mjs,
414
+ // lives in the Lambda LAYER at /opt/nodejs/node_modules/.../EventCollectorEntryPoint.mjs,
415
415
  // so a bare `await import("@reventlessdev/online-shop-hybrid-catalog-spec/...")`
416
416
  // from here resolves through /opt/nodejs/node_modules/ and never reaches the
417
417
  // function asset. Anchoring createRequire at file:///var/task/index.mjs makes
@@ -585,7 +585,7 @@ async function buildHandler() {
585
585
  // it through ExtensionMapping.Make, and build per-extension Extension_Operations.
586
586
  const userExtensionHandlers = await Promise.all(config.extensions.map(async (ext) => {
587
587
  if (!ext.specModule || !ext.mappingsModule || !ext.delegateModule) {
588
- log.warn("extension '" + ext.name + "' missing module specifier(s); skipping. specModule=" + ext.specModule + ", mappingsModule=" + ext.mappingsModule + ", delegateModule=" + ext.delegateModule, { comp: "AdminEventCollectorEntryPoint" });
588
+ log.warn("extension '" + ext.name + "' missing module specifier(s); skipping. specModule=" + ext.specModule + ", mappingsModule=" + ext.mappingsModule + ", delegateModule=" + ext.delegateModule, { comp: "EventCollectorEntryPoint" });
589
589
  return null;
590
590
  }
591
591
  const epSpec = patchSpecId(await importFromAsset(ext.specModule));
@@ -7,7 +7,7 @@
7
7
  // STITCHED schema at push time — the additive counterpart of the neutral
8
8
  // emission, mirroring how `injectAwsAuthAll`/`stampSharedIamTypes` decorate
9
9
  // auth. Runtime-pure so the bundled Lambda entry points
10
- // (AdminEventCollectorEntryPoint.mjs) can import it without dragging
10
+ // (EventCollectorEntryPoint.mjs) can import it without dragging
11
11
  // deploy-time dependencies into the runtime module graph.
12
12
 
13
13
  /**
@@ -375,7 +375,7 @@ export function response(ctx) {
375
375
  }
376
376
  `->Pulumi.Input.make
377
377
 
378
- let field = ReventlessCore.AdminApi.cloneMutationEntry.fieldNames->Array.getUnsafe(0)
378
+ let field = ReventlessCore.Platform_AdminApi.cloneMutationEntry.fieldNames->Array.getUnsafe(0)
379
379
  let resolver = AppSync_Resolver_Retrying.makeUnitJsResolver(
380
380
  ~name=field,
381
381
  ~api,
@@ -11,11 +11,11 @@ import * as AWS$ReventlessAws from "../../adapter/AWS.res.mjs";
11
11
  import * as Cloner$ReventlessCore from "@reventlessdev/reventless-core/src/components/Cloner.res.mjs";
12
12
  import * as AWS_Tags$ReventlessAws from "../../adapter/AWS_Tags.res.mjs";
13
13
  import * as Util_Vpc$ReventlessAws from "../../util/Util_Vpc.res.mjs";
14
- import * as AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/AdminApi.res.mjs";
15
14
  import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
16
15
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
17
16
  import * as GetSecretVersion$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/SecretsManager/GetSecretVersion.res.mjs";
18
17
  import * as Util_AppSync$ReventlessAws from "../../util/Util_AppSync.res.mjs";
18
+ import * as Platform_AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminApi.res.mjs";
19
19
  import * as AppSync_Resolver_Retrying$ReventlessAws from "../../adapter/Api/AppSync_Resolver_Retrying.res.mjs";
20
20
 
21
21
  function make(name, api, fullQualifiedStackName, reventlessCiSecretUrn, secretUrns, opts) {
@@ -226,7 +226,7 @@ export const handler = async (event) => {
226
226
  },
227
227
  serviceRoleArn: dataSourceRole.arn
228
228
  }, opts);
229
- let field = AdminApi$ReventlessCore.cloneMutationEntry.fieldNames[0];
229
+ let field = Platform_AdminApi$ReventlessCore.cloneMutationEntry.fieldNames[0];
230
230
  let resolver = AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(field, api, dataSource.name, "Mutation", field, `import { util } from '@aws-appsync/utils';
231
231
  export function request(ctx) {
232
232
  return {
@@ -118,7 +118,7 @@ let registerConfig = (
118
118
 
119
119
  // PluginRuntime_Builder is a functor so that the caller can inject the EventCollectorChannel
120
120
  // implementation. All other runtime builders hardcode EventCollectorChannel.DynamoDbStream, but
121
- // the Plugin admin handler (AdminEventCollectorEntryPoint) is shared across DCB and aggregate
121
+ // the Plugin admin handler (EventCollectorEntryPoint) is shared across DCB and aggregate
122
122
  // plugins which may use different event log backends. The functor keeps the builder generic
123
123
  // while the concrete instantiation picks the right channel at the platform level.
124
124
  module Make = (
@@ -151,9 +151,11 @@ module Make = (
151
151
  let synthesizeAdminContext = (): ReventlessCore.Plugin_Helpers.eventCollectorContext => {
152
152
  let config = configRef.contents
153
153
  // Stable JSON literal — matches Reventless.Plugin.pluginDefinitionSchema
154
- // (optional fields encoded as null via js_nullable).
154
+ // (optional fields encoded as null via js_nullable). Must stay byte-aligned with
155
+ // the ReScript-built twin in `Platform_Admin.construct`'s `fakePluginDefinition`.
156
+ let platformId = ReventlessCore.Platform_Admin_Structure.pluginId
155
157
  let fakePluginDefinitionJson =
156
- `{"id":"Admin@INTERNAL","name":"Admin","version":"INTERNAL","extensionPoints":[],"extensions":[],"eventCollector":"NOT-SET","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"structure":null}`->Pulumi.Output.make
158
+ `{"id":"${platformId}@INTERNAL","name":"${platformId}","version":"INTERNAL","extensionPoints":[],"extensions":[],"eventCollector":"NOT-SET","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"structure":null}`->Pulumi.Output.make
157
159
  let adminEpEventTopicArn = switch config.eventTopicArn {
158
160
  | Some(arn) => arn
159
161
  | None => Pulumi.Output.make("NOT_AVAILABLE")
@@ -436,7 +438,7 @@ module Make = (
436
438
  [ext.specModule, ext.mappingsModule, ext.delegateModule]->Array.forEach(addPackageFor)
437
439
  })
438
440
  // Outgoing EPs: the entry point dynamic-imports ep.specModule and
439
- // ep.mappingsModule at cold start (AdminEventCollectorEntryPoint.mjs).
441
+ // ep.mappingsModule at cold start (EventCollectorEntryPoint.mjs).
440
442
  // Without this walk the EP spec package (e.g. <plugin>-spec) is missing
441
443
  // from the asset zip even though HANDLER_CONFIG references it, and the
442
444
  // Lambda crashes with MODULE_NOT_FOUND before any event is processed.
@@ -484,7 +486,9 @@ module Make = (
484
486
  // extension can emit RegisterUiFragment in the handshake answer.
485
487
  extraStringAssets->Dict.set("uiFragments.json", uiFragmentsJson)
486
488
  Util_Bundle.buildCodeArchive(
487
- ~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs",
489
+ // Path string, not a module reference — a rename that misses this stays green
490
+ // at build time and fails at bundle time or Lambda cold start.
491
+ ~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/EventCollectorEntryPoint.mjs",
488
492
  ~packageDirs,
489
493
  ~extraStringAssets,
490
494
  )
@@ -15,6 +15,7 @@ import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/sr
15
15
  import * as EventCollector$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventCollector/EventCollector.res.mjs";
16
16
  import * as EventLogBackend$ReventlessAws from "../../adapter/EventLog/EventLogBackend.res.mjs";
17
17
  import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
18
+ import * as Platform_Admin_Structure$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin_Structure.res.mjs";
18
19
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "../../adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs";
19
20
  import * as StateChangeSliceRuntime_Builder_Single$ReventlessAws from "../../adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs";
20
21
 
@@ -132,7 +133,7 @@ function Make(EventCollectorChannel) {
132
133
  };
133
134
  let synthesizeAdminContext = () => {
134
135
  let config = configRef.contents;
135
- let fakePluginDefinitionJson = Pulumi.output(`{"id":"Admin@INTERNAL","name":"Admin","version":"INTERNAL","extensionPoints":[],"extensions":[],"eventCollector":"NOT-SET","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"structure":null}`);
136
+ let fakePluginDefinitionJson = Pulumi.output(`{"id":"` + Platform_Admin_Structure$ReventlessCore.pluginId + `@INTERNAL","name":"` + Platform_Admin_Structure$ReventlessCore.pluginId + `","version":"INTERNAL","extensionPoints":[],"extensions":[],"eventCollector":"NOT-SET","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"structure":null}`);
136
137
  let arn = config.eventTopicArn;
137
138
  let adminEpEventTopicArn = arn !== undefined ? arn : Pulumi.output("NOT_AVAILABLE");
138
139
  return {
@@ -306,7 +307,7 @@ function Make(EventCollectorChannel) {
306
307
  let extraStringAssets = {};
307
308
  extraStringAssets["pluginDefinition.json"] = param[0];
308
309
  extraStringAssets["uiFragments.json"] = param[1];
309
- return Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs", packageDirs, extraStringAssets);
310
+ return Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/EventCollectorEntryPoint.mjs", packageDirs, extraStringAssets);
310
311
  });
311
312
  let codeOutput = bundleOutput.apply(b => b.code);
312
313
  let sourceCodeHashOutput = bundleOutput.apply(b => b.sourceCodeHash);
@@ -122,7 +122,7 @@ let createQueuePolicyDocument = (name, queueArn: string, handlerArn: string) =>
122
122
  let _ =
123
123
  (queue.id, queue.arn, fifoQueue.id, fifoQueue.arn, handler.arn)
124
124
  ->Pulumi.Output.all5
125
- ->Pulumi.Output.apply(((queueId, queueArn, fifoQueueId, fifoQueueArn, handlerArn)) => {
125
+ ->Pulumi.Output.apply(((_queueId, queueArn, _fifoQueueId, fifoQueueArn, handlerArn)) => {
126
126
  let lambdaPolicyDocument = PulumiAws.PolicyDocument.make(
127
127
  ~id=name ++ "SQSPolicy",
128
128
  ~statements=[
@@ -152,10 +152,20 @@ let _ =
152
152
  ~opts,
153
153
  )
154
154
 
155
+ // `queueUrl` takes the queue's Output, not the resolved `queueId` string:
156
+ // that is what registers the policy -> queue dependency, without which
157
+ // Pulumi has no ordering constraint and can delete the queue first on a
158
+ // replacement, leaving the policy's delete polling a queue that is gone.
159
+ //
160
+ // Unlike the channel helpers, these two stay inside the apply. This module
161
+ // creates its resources at import time and this apply resolves late enough
162
+ // to race Jest's teardown; reshaping it (dropping the now-unused ids to
163
+ // narrow the tuple) shifts that timing and crashes the AWS unit suites, so
164
+ // `_queueId` / `_fifoQueueId` are deliberately kept.
155
165
  let _attachQueuePolicy = PulumiAws.SQS.QueuePolicy.make(
156
166
  ~name,
157
167
  ~args={
158
- queueUrl: queueId->Pulumi.Input.make,
168
+ queueUrl: queue.id->Pulumi.Output.asInput,
159
169
  policy: createQueuePolicyDocument(name, queueArn, handlerArn),
160
170
  },
161
171
  ~opts=Some(opts),
@@ -163,7 +173,7 @@ let _ =
163
173
  let _attachFifoQueuePolicy = PulumiAws.SQS.QueuePolicy.make(
164
174
  ~name=nameFifo,
165
175
  ~args={
166
- queueUrl: fifoQueueId->Pulumi.Input.make,
176
+ queueUrl: fifoQueue.id->Pulumi.Output.asInput,
167
177
  policy: createQueuePolicyDocument(nameFifo, fifoQueueArn, handlerArn),
168
178
  },
169
179
  ~opts=Some(opts),
@@ -132,11 +132,11 @@ Pulumi.all([
132
132
  }, opts);
133
133
  new (Aws.sqs.QueuePolicy)(name, {
134
134
  policy: createQueuePolicyDocument(name, queueArn, handlerArn),
135
- queueUrl: param[0]
135
+ queueUrl: queue.id
136
136
  }, opts);
137
137
  new (Aws.sqs.QueuePolicy)(nameFifo, {
138
138
  policy: createQueuePolicyDocument(nameFifo, fifoQueueArn, handlerArn),
139
- queueUrl: param[2]
139
+ queueUrl: fifoQueue.id
140
140
  }, opts);
141
141
  });
142
142
 
@@ -23,7 +23,7 @@ let decodeFragment = (fragment: Reventless.Plugin.apiSchemaFragment) =>
23
23
  ReventlessCore.GraphQL_Stitcher.decode(fragment)
24
24
 
25
25
  describe("AppSync_Adapter.injectAwsAuthAll", () => {
26
- let baseFragment = ReventlessCore.AdminApi.baseFragment(~cloner=true)
26
+ let baseFragment = ReventlessCore.Platform_AdminApi.baseFragment(~cloner=true)
27
27
 
28
28
  testSync("adds @aws_auth directive to all mutation fields", () => {
29
29
  let augmented = AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin")
@@ -86,10 +86,10 @@ describe("AppSync_Adapter.injectAwsAuth", () => {
86
86
  testSync("injects auth only on entries with authorization", () => {
87
87
  // Build a fragment with known entries
88
88
  let mutationEntries: array<ReventlessInfra.Api.mutationSchemaEntry> =
89
- ReventlessCore.AdminApi.mutationEntries(~cloner=false)
89
+ ReventlessCore.Platform_AdminApi.mutationEntries(~cloner=false)
90
90
  let queryEntries = ReventlessCore.PluginBaseFragment.queryEntries
91
91
 
92
- let baseFragment = ReventlessCore.AdminApi.baseFragment(~cloner=false)
92
+ let baseFragment = ReventlessCore.Platform_AdminApi.baseFragment(~cloner=false)
93
93
  let augmented = AppSync_Adapter.injectAwsAuth(
94
94
  baseFragment,
95
95
  ~mutationEntries,
@@ -111,7 +111,7 @@ describe("AppSync_Adapter.injectAwsAuth", () => {
111
111
 
112
112
  describe("AppSync_Adapter.generateFragment", () => {
113
113
  testSync("produces fragment with auth directives from entries", () => {
114
- let mutationEntries = ReventlessCore.AdminApi.mutationEntries(~cloner=false)
114
+ let mutationEntries = ReventlessCore.Platform_AdminApi.mutationEntries(~cloner=false)
115
115
  let queryEntries = ReventlessCore.PluginBaseFragment.queryEntries
116
116
 
117
117
  let fragment = AppSync_Adapter.generateFragment(~mutationEntries, ~queryEntries)
@@ -570,7 +570,7 @@ describe("AppSync_Adapter — type-level dual-auth", () => {
570
570
  })
571
571
 
572
572
  describe("AppSync_Adapter.injectAwsAuthAll — ~iamFieldNames", () => {
573
- let baseFragment = ReventlessCore.AdminApi.baseFragment(~cloner=true)
573
+ let baseFragment = ReventlessCore.Platform_AdminApi.baseFragment(~cloner=true)
574
574
 
575
575
  testSync("marks only the named field dual-auth, leaving others single-mode", () => {
576
576
  // Pick a real mutation field name from the admin base fragment.
@@ -644,7 +644,7 @@ describe("Split mode — empty base fragment", () => {
644
644
  })
645
645
 
646
646
  testSync("stitching admin base without plugins produces only admin fields", () => {
647
- let adminBase = ReventlessCore.AdminApi.baseFragment(~cloner=false)
647
+ let adminBase = ReventlessCore.Platform_AdminApi.baseFragment(~cloner=false)
648
648
  let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
649
649
  ~baseFragment=adminBase,
650
650
  ~pluginFragments=[],
@@ -678,7 +678,7 @@ describe("Merged mode — canonical source documents", () => {
678
678
 
679
679
  let adminSourceSdl = assembleCanonicalSourceSdl(
680
680
  ~baseFragment=AppSync_Adapter.injectAwsAuthAll(
681
- ReventlessCore.AdminApi.baseFragment(~cloner=false),
681
+ ReventlessCore.Platform_AdminApi.baseFragment(~cloner=false),
682
682
  ~group="Admin",
683
683
  ),
684
684
  )
@@ -5,9 +5,9 @@ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
7
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
8
- import * as AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/AdminApi.res.mjs";
9
8
  import * as AppSync_Adapter$ReventlessAws from "../src/components/Api/AppSync_Adapter.res.mjs";
10
9
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
10
+ import * as Platform_AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminApi.res.mjs";
11
11
  import * as AppSync_SdlDecorate$ReventlessAws from "../src/components/Api/AppSync_SdlDecorate.res.mjs";
12
12
  import * as PluginBaseFragment$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/api/PluginBaseFragment.res.mjs";
13
13
 
@@ -28,7 +28,7 @@ globalThis.describe("AppSync_Adapter.sha256Hex", () => {
28
28
  let decodeFragment = GraphQL_Stitcher$ReventlessCore.decode;
29
29
 
30
30
  globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
31
- let baseFragment = AdminApi$ReventlessCore.baseFragment(true);
31
+ let baseFragment = Platform_AdminApi$ReventlessCore.baseFragment(true);
32
32
  globalThis.test("adds @aws_auth directive to all mutation fields", () => {
33
33
  let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
34
34
  let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
@@ -77,8 +77,8 @@ globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
77
77
 
78
78
  globalThis.describe("AppSync_Adapter.injectAwsAuth", () => {
79
79
  globalThis.test("injects auth only on entries with authorization", () => {
80
- let mutationEntries = AdminApi$ReventlessCore.mutationEntries(false);
81
- let baseFragment = AdminApi$ReventlessCore.baseFragment(false);
80
+ let mutationEntries = Platform_AdminApi$ReventlessCore.mutationEntries(false);
81
+ let baseFragment = Platform_AdminApi$ReventlessCore.baseFragment(false);
82
82
  let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuth(baseFragment, mutationEntries, PluginBaseFragment$ReventlessCore.queryEntries);
83
83
  let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
84
84
  let queryWithAuth = PluginBaseFragment$ReventlessCore.queryEntries.some(entry => Stdlib_Option.isSome(entry.authorization));
@@ -92,7 +92,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth", () => {
92
92
 
93
93
  globalThis.describe("AppSync_Adapter.generateFragment", () => {
94
94
  globalThis.test("produces fragment with auth directives from entries", () => {
95
- let mutationEntries = AdminApi$ReventlessCore.mutationEntries(false);
95
+ let mutationEntries = Platform_AdminApi$ReventlessCore.mutationEntries(false);
96
96
  let fragment = AppSync_Adapter$ReventlessAws.generateFragment(mutationEntries, PluginBaseFragment$ReventlessCore.queryEntries);
97
97
  let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
98
98
  globalThis.expect(parts.types.length).toBeGreaterThan(0);
@@ -536,7 +536,7 @@ globalThis.describe("AppSync_Adapter — type-level dual-auth", () => {
536
536
  });
537
537
 
538
538
  globalThis.describe("AppSync_Adapter.injectAwsAuthAll — ~iamFieldNames", () => {
539
- let baseFragment = AdminApi$ReventlessCore.baseFragment(true);
539
+ let baseFragment = Platform_AdminApi$ReventlessCore.baseFragment(true);
540
540
  globalThis.test("marks only the named field dual-auth, leaving others single-mode", () => {
541
541
  let firstMutationName = GraphQL_Stitcher$ReventlessCore.extractLeadingName(GraphQL_Stitcher$ReventlessCore.decode(baseFragment).mutations[0]);
542
542
  let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", [firstMutationName]);
@@ -600,7 +600,7 @@ globalThis.describe("Split mode — empty base fragment", () => {
600
600
  globalThis.expect(sdl).not.toContain("Platform_Plugin");
601
601
  });
602
602
  globalThis.test("stitching admin base without plugins produces only admin fields", () => {
603
- let adminBase = AdminApi$ReventlessCore.baseFragment(false);
603
+ let adminBase = Platform_AdminApi$ReventlessCore.baseFragment(false);
604
604
  let sdl = GraphQL_Stitcher$ReventlessCore.stitch(adminBase, []);
605
605
  globalThis.expect(sdl).toContain("Platform_Plugin");
606
606
  });
@@ -614,7 +614,7 @@ globalThis.describe("Merged mode — canonical source documents", () => {
614
614
  subscriptions: [],
615
615
  subscriptionSources: []
616
616
  });
617
- let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(false), "Admin", undefined);
617
+ let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(false), "Admin", undefined);
618
618
  let adminSourceSdl = AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(baseFragment));
619
619
  let domainBaseSourceSdl = AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(domainBaseFragment));
620
620
  globalThis.test("no AWS source document carries the global node query (dropped — see plan)", () => {
@@ -0,0 +1,65 @@
1
+ open JestGlobals
2
+
3
+ // Guards the CommandTopic queue policy's service-principal scoping. The
4
+ // EventBridge grant cannot name a single rule ARN — ScheduledPublisher creates
5
+ // rules at runtime and EventBridge authorises SQS targets against the queue's
6
+ // resource policy — so the condition keys are the only bound on who may send
7
+ // into a command topic. A dropped condition is invisible at deploy time.
8
+
9
+ let queueArn = "arn:aws:sqs:eu-west-1:123456789012:CatalogAggrCmdTopic-5150050"
10
+ let lambdaArn = "arn:aws:lambda:eu-west-1:123456789012:function:AllAggregates-1a2b3c4"
11
+
12
+ let policy =
13
+ CommandTopicChannel_Helpers.createQueuePolicyDocument(
14
+ ~name="CatalogAggrCmdTopic",
15
+ ~queueArn,
16
+ ~lambdaArn,
17
+ )->JSON.parseOrThrow
18
+
19
+ let field = (json, key) =>
20
+ json->JSON.Decode.object->Option.flatMap(o => o->Dict.get(key))
21
+
22
+ let statement = sid =>
23
+ policy
24
+ ->field("Statement")
25
+ ->Option.flatMap(JSON.Decode.array)
26
+ ->Option.getOr([])
27
+ ->Array.find(s => s->field("Sid")->Option.flatMap(JSON.Decode.string) == Some(sid))
28
+
29
+ let condition = (sid, operator, key) =>
30
+ statement(sid)
31
+ ->Option.flatMap(s => s->field("Condition"))
32
+ ->Option.flatMap(c => c->field(operator))
33
+ ->Option.flatMap(o => o->field(key))
34
+ ->Option.flatMap(JSON.Decode.string)
35
+
36
+ describe("CommandTopic queue policy — EventBridge grant", () => {
37
+ testSync("is scoped to this account", () => {
38
+ expect(
39
+ condition("AllowCloudWatchEventsToSendToQueue", "StringEquals", "aws:SourceAccount"),
40
+ )->toEqual(Some("123456789012"))
41
+ })
42
+
43
+ testSync("is scoped to EventBridge rule ARNs in this account", () => {
44
+ expect(condition("AllowCloudWatchEventsToSendToQueue", "ArnLike", "aws:SourceArn"))->toEqual(
45
+ Some("arn:aws:events:*:123456789012:rule/*"),
46
+ )
47
+ })
48
+
49
+ testSync("names the EventBridge service principal", () => {
50
+ let principal =
51
+ statement("AllowCloudWatchEventsToSendToQueue")
52
+ ->Option.flatMap(s => s->field("Principal"))
53
+ ->Option.flatMap(p => p->field("Service"))
54
+ ->Option.flatMap(JSON.Decode.string)
55
+ expect(principal)->toEqual(Some("events.amazonaws.com"))
56
+ })
57
+ })
58
+
59
+ describe("CommandTopic queue policy — Lambda grant", () => {
60
+ testSync("stays pinned to the consuming function ARN", () => {
61
+ expect(condition("AllowLambdaToAccessQueue", "ArnEquals", "AWS:SourceArn"))->toEqual(
62
+ Some(lambdaArn),
63
+ )
64
+ })
65
+ })
@@ -0,0 +1,53 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
6
+ import * as CommandTopicChannel_Helpers$ReventlessAws from "../src/adapter/CommandTopic/CommandTopicChannel_Helpers.res.mjs";
7
+
8
+ let queueArn = "arn:aws:sqs:eu-west-1:123456789012:CatalogAggrCmdTopic-5150050";
9
+
10
+ let lambdaArn = "arn:aws:lambda:eu-west-1:123456789012:function:AllAggregates-1a2b3c4";
11
+
12
+ let policy = JSON.parse(CommandTopicChannel_Helpers$ReventlessAws.createQueuePolicyDocument("CatalogAggrCmdTopic", queueArn, lambdaArn));
13
+
14
+ function field(json, key) {
15
+ return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o[key]);
16
+ }
17
+
18
+ function statement(sid) {
19
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(field(policy, "Statement"), Stdlib_JSON.Decode.array), []).find(s => Primitive_object.equal(Stdlib_Option.flatMap(field(s, "Sid"), Stdlib_JSON.Decode.string), sid));
20
+ }
21
+
22
+ function condition(sid, operator, key) {
23
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(statement(sid), s => field(s, "Condition")), c => field(c, operator)), o => field(o, key)), Stdlib_JSON.Decode.string);
24
+ }
25
+
26
+ globalThis.describe("CommandTopic queue policy — EventBridge grant", () => {
27
+ globalThis.test("is scoped to this account", () => {
28
+ globalThis.expect(condition("AllowCloudWatchEventsToSendToQueue", "StringEquals", "aws:SourceAccount")).toEqual("123456789012");
29
+ });
30
+ globalThis.test("is scoped to EventBridge rule ARNs in this account", () => {
31
+ globalThis.expect(condition("AllowCloudWatchEventsToSendToQueue", "ArnLike", "aws:SourceArn")).toEqual("arn:aws:events:*:123456789012:rule/*");
32
+ });
33
+ globalThis.test("names the EventBridge service principal", () => {
34
+ let principal = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(statement("AllowCloudWatchEventsToSendToQueue"), s => field(s, "Principal")), p => field(p, "Service")), Stdlib_JSON.Decode.string);
35
+ globalThis.expect(principal).toEqual("events.amazonaws.com");
36
+ });
37
+ });
38
+
39
+ globalThis.describe("CommandTopic queue policy — Lambda grant", () => {
40
+ globalThis.test("stays pinned to the consuming function ARN", () => {
41
+ globalThis.expect(condition("AllowLambdaToAccessQueue", "ArnEquals", "AWS:SourceArn")).toEqual(lambdaArn);
42
+ });
43
+ });
44
+
45
+ export {
46
+ queueArn,
47
+ lambdaArn,
48
+ policy,
49
+ field,
50
+ statement,
51
+ condition,
52
+ }
53
+ /* policy Not a pure module */
@@ -73,7 +73,7 @@ describe("MCP_Lambda.generateAdminConfig", () => {
73
73
  let arns = Dict.make()
74
74
  // Use a tool name that matches the first mutation field
75
75
  let firstEntry: ReventlessInfra.Api.mutationSchemaEntry =
76
- ReventlessCore.AdminApi.mutationEntries(~cloner=false)->Array.getUnsafe(0)
76
+ ReventlessCore.Platform_AdminApi.mutationEntries(~cloner=false)->Array.getUnsafe(0)
77
77
  let firstFieldName = firstEntry.fieldNames->Array.getUnsafe(0)
78
78
  arns->Dict.set(firstFieldName, "arn:aws:sqs:us-east-1:123:queue")
79
79