@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
package/CHANGELOG.md CHANGED
@@ -3,6 +3,29 @@
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.224 (2026-07-22)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** register the QueuePolicy -> Queue dependency so destroys order correctly ([81cf2b3](https://github.com/ReventlessDev/reventless-core/commit/81cf2b3198fea7ad280817d2414102089ba0f13d))
11
+ * **aws:** scope the CommandTopic queue policy's EventBridge send grant ([2f5f148](https://github.com/ReventlessDev/reventless-core/commit/2f5f148f4d73b2997633207f071c8d055960cc75))
12
+
13
+
14
+ # 3.0.0-alpha.223 (2026-07-22)
15
+
16
+ * refactor!: retire the stale "Admin" vocabulary for platform-owned resources ([2770bd9](https://github.com/ReventlessDev/reventless-core/commit/2770bd96f309142bdfc65b9f36bd1a3ef60641bd))
17
+
18
+ ### BREAKING CHANGES
19
+
20
+ * platform-owned resources are renamed Admin* -> Platform*
21
+ (PlatformDcbEventLog, PlatformDcbCmdTopic, PlatformEventColl). This replaces
22
+ a DynamoDB table, an SQS queue and an SNS topic — wipe and redeploy rather
23
+ than migrate. Both alpha AdminDcbEventLog tables held 0 items at the time of
24
+ the rename. MCP resource URI templates change from Admin/<field>/{id} to
25
+ Platform/<field>/{id}; MCP tool names are unaffected.
26
+
27
+
28
+
6
29
  # 3.0.0-alpha.222 (2026-07-22)
7
30
 
8
31
  * refactor!: retire the stale "core" vocabulary for platform things ([34e7480](https://github.com/ReventlessDev/reventless-core/commit/34e7480992bd58906a250b0a1ce6ff2c5ba45260))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.222",
3
+ "version": "3.0.0-alpha.224",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -9,16 +9,16 @@
9
9
  "sury": "11.0.0-alpha.4",
10
10
  "uuid": "^13.0.0",
11
11
  "@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.24",
12
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.57",
13
12
  "@reventlessdev/rescript-effect": "0.1.0-alpha.30",
14
13
  "@reventlessdev/rescript-jest": "1.0.0-alpha.9",
14
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.57",
15
15
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
16
16
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
17
- "@reventlessdev/reventless-core": "3.0.0-alpha.176",
18
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.40",
19
- "@reventlessdev/reventless-interop": "3.0.0-alpha.29",
20
17
  "@reventlessdev/reventless-infra": "3.0.0-alpha.104",
21
- "@reventlessdev/reventless-spec": "3.0.0-alpha.80"
18
+ "@reventlessdev/reventless-core": "3.0.0-alpha.177",
19
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.29",
20
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.80",
21
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.41"
22
22
  },
23
23
  "devDependencies": {
24
24
  "rescript": "12.3.0",
package/src/Platform.res CHANGED
@@ -154,7 +154,7 @@ module MakeWithConfig = (
154
154
  let adminSourceSdl = (): string =>
155
155
  assembleCanonicalSourceSdl(
156
156
  ~baseFragment=AppSync_Adapter.injectAwsAuthAll(
157
- ReventlessCore.AdminApi.baseFragment(~cloner=Config.cloner),
157
+ ReventlessCore.Platform_AdminApi.baseFragment(~cloner=Config.cloner),
158
158
  ~group="Admin",
159
159
  ),
160
160
  )
@@ -850,7 +850,7 @@ module MakeWithConfig = (
850
850
  // admin-prefixed `Platform_Plugin` / `Platform_Plugins` SDL fields declared by
851
851
  // PluginBaseFragment.queryEntries. Field-name alignment is handled by the
852
852
  // queryFieldNamesRegistry entries that Platform_Admin.construct populates
853
- // from AdminApi.queryEntries before this builder runs.
853
+ // from Platform_AdminApi.queryEntries before this builder runs.
854
854
  module PluginReadModel = ReadModel_Builder_Single_Stream.Make(
855
855
  ReventlessCore.PluginsReadModelSpec,
856
856
  PluginReadModelMappings,
@@ -1124,7 +1124,7 @@ module MakeWithConfig = (
1124
1124
 
1125
1125
  module PluginExtensionPoint = Plugin_ExtensionPoint_Builder.MakeWithConfig({
1126
1126
  // Cross-plugin SNS subscription management runs in the bundled
1127
- // AdminEventCollector Lambda (AdminEventCollectorEntryPoint.mjs), not in
1127
+ // AdminEventCollector Lambda (EventCollectorEntryPoint.mjs), not in
1128
1128
  // this deploy-time EP Lambda — which only handles incoming commands
1129
1129
  // (Heartbeat, ForwardCommand). None here keeps the deploy-time path
1130
1130
  // unchanged; the .mjs entry point supplies a real implementation.
@@ -15,7 +15,6 @@ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js
15
15
  import * as Plugin$ReventlessAws from "./components/Plugin.res.mjs";
16
16
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
17
17
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
18
- import * as AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/AdminApi.res.mjs";
19
18
  import * as Scheduler$ReventlessAws from "./components/Scheduler.res.mjs";
20
19
  import * as Aggregate$ReventlessCore from "@reventlessdev/reventless-core/src/components/Aggregate/Aggregate.res.mjs";
21
20
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
@@ -44,6 +43,7 @@ import * as NoEventMappings$ReventlessInfra from "@reventlessdev/reventless-infr
44
43
  import * as Util_HostUiDomain$ReventlessAws from "./util/Util_HostUiDomain.res.mjs";
45
44
  import * as DcbEventLogStorage$ReventlessAws from "./adapter/DcbEventLog/DcbEventLogStorage.res.mjs";
46
45
  import * as ExtensionMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionMapping.res.mjs";
46
+ import * as Platform_AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminApi.res.mjs";
47
47
  import * as PluginsProjection$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginsProjection.res.mjs";
48
48
  import * as StateTopic_AppSync$ReventlessAws from "./adapter/StateTopic/StateTopic_AppSync.res.mjs";
49
49
  import * as AppSync_SdlDecorate$ReventlessAws from "./components/Api/AppSync_SdlDecorate.res.mjs";
@@ -148,7 +148,7 @@ function MakeWithConfig(Config) {
148
148
  subscriptionSources: []
149
149
  });
150
150
  let adminSourceSdl = () => {
151
- let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(Config.cloner), "Admin", undefined);
151
+ let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(Config.cloner), "Admin", undefined);
152
152
  return AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(baseFragment));
153
153
  };
154
154
  let match;
@@ -1123,7 +1123,7 @@ function Make($star) {
1123
1123
  subscriptionSources: []
1124
1124
  });
1125
1125
  let adminSourceSdl = () => {
1126
- let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(false), "Admin", undefined);
1126
+ let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(false), "Admin", undefined);
1127
1127
  return AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(baseFragment));
1128
1128
  };
1129
1129
  let match;
@@ -1,3 +1,9 @@
1
+ // SQS queue ARN is `arn:aws:sqs:<region>:<accountId>:<name>` — segment index 4
2
+ // (0-based). Used to scope queue-policy service principals to this account via
3
+ // the aws:SourceAccount condition key.
4
+ let accountIdOfQueueArn = (queueArn: string) =>
5
+ queueArn->String.split(":")->Array.get(4)->Option.getOr("")
6
+
1
7
  let dynamoDbStreamResources = eventTopicResources =>
2
8
  eventTopicResources->ReventlessCore.Util.Adapter.filterSupportedResolvedResources([
3
9
  AWS.DynamoDbStream.service,
@@ -1,8 +1,13 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
3
4
  import * as AWS$ReventlessAws from "./AWS.res.mjs";
4
5
  import * as Util_Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Adapter.res.mjs";
5
6
 
7
+ function accountIdOfQueueArn(queueArn) {
8
+ return Stdlib_Option.getOr(queueArn.split(":")[4], "");
9
+ }
10
+
6
11
  function dynamoDbStreamResources(eventTopicResources) {
7
12
  return Util_Adapter$ReventlessCore.filterSupportedResolvedResources(eventTopicResources, [AWS$ReventlessAws.DynamoDbStream.service]);
8
13
  }
@@ -29,6 +34,7 @@ function dynamoDbResources(resources) {
29
34
  }
30
35
 
31
36
  export {
37
+ accountIdOfQueueArn,
32
38
  dynamoDbStreamResources,
33
39
  snsResources,
34
40
  sqsResources,
@@ -190,7 +190,7 @@ let makeDcb = (
190
190
  ~fieldNames: array<string>,
191
191
  ~tags: array<string>,
192
192
  // Admin (onAdminApi) DCB slices expose ONLY their mutation fields (declared in the
193
- // static AdminApi.baseFragment). The static base does NOT declare their `on<Field>`
193
+ // static Platform_AdminApi.baseFragment). The static base does NOT declare their `on<Field>`
194
194
  // subscription counterparts, so creating Source-C subscription resolvers would fail
195
195
  // with CreateResolver NotFound. Suppress subscriptions for admin; plugins keep them
196
196
  // (their generated fragment declares the matching subscription fields).
@@ -1,55 +1,73 @@
1
1
  open PulumiAws.PolicyDocument
2
2
  open Adapter_Helpers
3
3
 
4
+ let createQueuePolicyDocument = (~name, ~queueArn: string, ~lambdaArn: string) => {
5
+ let accountId = queueArn->accountIdOfQueueArn
6
+ PulumiAws.PolicyDocument.make(
7
+ ~id=name ++ "QueuePolicy",
8
+ ~statements=[
9
+ {
10
+ sid: "AllowLambdaToAccessQueue",
11
+ effect: Allow,
12
+ principal: Principals({
13
+ service: PrincipalId(AWS.Lambda.principal),
14
+ }),
15
+ actions: Actions(["sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes"]),
16
+ resources: Resource(queueArn),
17
+ conditions: {
18
+ arnEquals: [("AWS:SourceArn", ConditionValue(lambdaArn))]->Dict.fromArray,
19
+ },
20
+ },
21
+ {
22
+ // ScheduledPublisher creates EventBridge rules at runtime
23
+ // (PutRule/PutTargets) targeting this queue directly, and EventBridge
24
+ // authorises SQS targets against the queue's resource policy — so the
25
+ // grant cannot name a single rule ARN. Scope it to rules in this
26
+ // account instead: SourceAccount closes the cross-account hole,
27
+ // ArnLike narrows the in-account surface to Events rules.
28
+ sid: "AllowCloudWatchEventsToSendToQueue",
29
+ effect: Allow,
30
+ principal: Principals({
31
+ service: PrincipalId(AWS.CloudwatchEventRule.principal),
32
+ }),
33
+ actions: Actions(["sqs:SendMessage"]),
34
+ resources: Resource(queueArn),
35
+ conditions: {
36
+ stringEquals: [("aws:SourceAccount", ConditionValue(accountId))]->Dict.fromArray,
37
+ arnLike: [
38
+ ("aws:SourceArn", ConditionValue(`arn:aws:events:*:${accountId}:rule/*`)),
39
+ ]->Dict.fromArray,
40
+ },
41
+ },
42
+ ],
43
+ )->toJsonString
44
+ }
45
+
4
46
  let createQueuePolicy = (
5
47
  queue: PulumiAws.SQS.Queue.t,
6
48
  name,
7
49
  lambda: Pulumi.Output.t<PulumiAws.Lambda.Function.t>,
8
50
  opts,
9
51
  ) => {
10
- let _ =
11
- (queue.arn, queue.id, lambda->Pulumi.Output.flatMap(lambda => lambda.arn))
12
- ->Pulumi.Output.all3
13
- ->Pulumi.Output.apply(((queueArn, queueId, lambdaArn)) => {
14
- let queuePolicyDocument =
15
- PulumiAws.PolicyDocument.make(
16
- ~id=name ++ "QueuePolicy",
17
- ~statements=[
18
- {
19
- sid: "AllowLambdaToAccessQueue",
20
- effect: Allow,
21
- principal: Principals({
22
- service: PrincipalId("lambda.amazonaws.com"),
23
- }),
24
- actions: Actions([
25
- "sqs:ReceiveMessage",
26
- "sqs:DeleteMessage",
27
- "sqs:GetQueueAttributes",
28
- ]),
29
- resources: Resource(queueArn),
30
- conditions: {
31
- arnEquals: [("AWS:SourceArn", ConditionValue(lambdaArn))]->Dict.fromArray,
32
- },
33
- },
34
- {
35
- sid: "AllowCloudWatchEventsToSendToQueue",
36
- effect: Allow,
37
- principal: Principals({
38
- service: PrincipalId("events.amazonaws.com"),
39
- }),
40
- actions: Actions(["sqs:SendMessage"]),
41
- resources: Resource(queueArn),
42
- },
43
- ],
44
- )
45
- ->toJsonString
46
- ->Pulumi.Input.make
47
- let _queuePolicy = PulumiAws.SQS.QueuePolicy.make(
48
- ~name,
49
- ~args={queueUrl: queueId->Pulumi.Input.make, policy: queuePolicyDocument},
50
- ~opts=Some(opts),
51
- )
52
- })
52
+ // The document is a *value* derived from resolved ARNs, so it is built in an
53
+ // apply. The QueuePolicy itself stays outside: passing `queue.id` as an Output
54
+ // is what registers the policy -> queue dependency, without which Pulumi has
55
+ // no ordering constraint and can delete the queue first on a replacement,
56
+ // leaving the policy's delete polling a queue that no longer exists.
57
+ let queuePolicyDocument =
58
+ (queue.arn, lambda->Pulumi.Output.flatMap(lambda => lambda.arn))
59
+ ->Pulumi.Output.all2
60
+ ->Pulumi.Output.apply(((queueArn, lambdaArn)) =>
61
+ createQueuePolicyDocument(~name, ~queueArn, ~lambdaArn)
62
+ )
63
+ let _queuePolicy = PulumiAws.SQS.QueuePolicy.make(
64
+ ~name,
65
+ ~args={
66
+ queueUrl: queue.id->Pulumi.Output.asInput,
67
+ policy: queuePolicyDocument->Pulumi.Output.asInput,
68
+ },
69
+ ~opts=Some(opts),
70
+ )
53
71
  }
54
72
 
55
73
  let createLambdaPolicy = (
@@ -100,10 +118,10 @@ let createLambdaPolicy = (
100
118
  "dynamodb:UpdateItem",
101
119
  "dynamodb:DeleteItem",
102
120
  "dynamodb:BatchWriteItem",
103
- // Phase 7 cross-partition fences emit read-only
104
- // `ConditionCheck` entries inside `TransactWriteItems`
105
- // when a write doesn't touch the cross-partition tag's
106
- // partition itself; this needs its own IAM action.
121
+ // Cross-partition fences emit read-only `ConditionCheck`
122
+ // entries inside `TransactWriteItems` when a write
123
+ // doesn't touch the cross-partition tag's partition
124
+ // itself; this needs its own IAM action.
107
125
  "dynamodb:ConditionCheckItem",
108
126
  ]),
109
127
  resources: Resources(
@@ -5,53 +5,65 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
5
  import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
6
6
  import * as Pulumi from "@pulumi/pulumi";
7
7
  import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
8
+ import * as AWS$ReventlessAws from "../AWS.res.mjs";
8
9
  import * as Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Adapter.res.mjs";
9
10
  import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
10
11
  import * as Adapter_Helpers$ReventlessAws from "../Adapter_Helpers.res.mjs";
11
12
  import * as Util_EventSourceMapping$ReventlessAws from "../../util/Util_EventSourceMapping.res.mjs";
12
13
 
14
+ function createQueuePolicyDocument(name, queueArn, lambdaArn) {
15
+ let accountId = Adapter_Helpers$ReventlessAws.accountIdOfQueueArn(queueArn);
16
+ return PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "QueuePolicy", [
17
+ {
18
+ Sid: "AllowLambdaToAccessQueue",
19
+ Principal: {
20
+ Service: AWS$ReventlessAws.Lambda.principal
21
+ },
22
+ Effect: "Allow",
23
+ Action: [
24
+ "sqs:ReceiveMessage",
25
+ "sqs:DeleteMessage",
26
+ "sqs:GetQueueAttributes"
27
+ ],
28
+ Resource: queueArn,
29
+ Condition: {
30
+ ArnEquals: Object.fromEntries([[
31
+ "AWS:SourceArn",
32
+ lambdaArn
33
+ ]])
34
+ }
35
+ },
36
+ {
37
+ Sid: "AllowCloudWatchEventsToSendToQueue",
38
+ Principal: {
39
+ Service: AWS$ReventlessAws.CloudwatchEventRule.principal
40
+ },
41
+ Effect: "Allow",
42
+ Action: ["sqs:SendMessage"],
43
+ Resource: queueArn,
44
+ Condition: {
45
+ StringEquals: Object.fromEntries([[
46
+ "aws:SourceAccount",
47
+ accountId
48
+ ]]),
49
+ ArnLike: Object.fromEntries([[
50
+ "aws:SourceArn",
51
+ `arn:aws:events:*:` + accountId + `:rule/*`
52
+ ]])
53
+ }
54
+ }
55
+ ]));
56
+ }
57
+
13
58
  function createQueuePolicy(queue, name, lambda, opts) {
14
- Pulumi.all([
59
+ let queuePolicyDocument = Pulumi.all([
15
60
  queue.arn,
16
- queue.id,
17
61
  Output$Pulumi.flatMap(lambda, lambda => lambda.arn)
18
- ]).apply(param => {
19
- let queueArn = param[0];
20
- let queuePolicyDocument = PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "QueuePolicy", [
21
- {
22
- Sid: "AllowLambdaToAccessQueue",
23
- Principal: {
24
- Service: "lambda.amazonaws.com"
25
- },
26
- Effect: "Allow",
27
- Action: [
28
- "sqs:ReceiveMessage",
29
- "sqs:DeleteMessage",
30
- "sqs:GetQueueAttributes"
31
- ],
32
- Resource: queueArn,
33
- Condition: {
34
- ArnEquals: Object.fromEntries([[
35
- "AWS:SourceArn",
36
- param[2]
37
- ]])
38
- }
39
- },
40
- {
41
- Sid: "AllowCloudWatchEventsToSendToQueue",
42
- Principal: {
43
- Service: "events.amazonaws.com"
44
- },
45
- Effect: "Allow",
46
- Action: ["sqs:SendMessage"],
47
- Resource: queueArn
48
- }
49
- ]));
50
- new (Aws.sqs.QueuePolicy)(name, {
51
- policy: queuePolicyDocument,
52
- queueUrl: param[1]
53
- }, opts);
54
- });
62
+ ]).apply(param => createQueuePolicyDocument(name, param[0], param[1]));
63
+ new (Aws.sqs.QueuePolicy)(name, {
64
+ policy: queuePolicyDocument,
65
+ queueUrl: queue.id
66
+ }, opts);
55
67
  }
56
68
 
57
69
  function createLambdaPolicy(lambdaRole, name, queue, resources, opts) {
@@ -138,6 +150,7 @@ function subscribeLambda2SqsTopic(batchSize, lambda, name, queue, opts) {
138
150
  }
139
151
 
140
152
  export {
153
+ createQueuePolicyDocument,
141
154
  createQueuePolicy,
142
155
  createLambdaPolicy,
143
156
  subscribeLambda2SqsTopic,
@@ -10,64 +10,62 @@ let toResources = (eventTopics: ReventlessCore.EventTopic.allOutputs) =>
10
10
  ->Array.flatMap(outputs => outputs.resources)
11
11
  ->ReventlessCore.Adapter.resourcesToResolvedOutput
12
12
 
13
- let createQueuePolicy = (queue: PulumiAws.SQS.Queue.t, name, _resources, opts) => {
14
- let _ =
15
- (queue.arn, queue.id)
16
- ->Pulumi.Output.all2
17
- ->Pulumi.Output.apply(((queueArn, queueId)) => {
18
- // SQS queue ARN is `arn:aws:sqs:<region>:<accountId>:<name>` — segment
19
- // index 4 (0-based). Used to scope cross-plugin SNS sources to this
20
- // account (aws:SourceAccount) and to topic names following the
21
- // Reventless EventTopic naming convention (aws:SourceArn arnLike).
22
- let accountId = queueArn->String.split(":")->Array.get(4)->Option.getOr("")
23
- let queuePolicyDocument =
24
- PulumiAws.PolicyDocument.make(
25
- ~id=name ++ "QueuePolicy",
26
- ~statements=[
27
- // Single statement allows SendMessage from any SNS topic owned by
28
- // this AWS account whose name matches the Reventless EventTopic
29
- // naming convention. Replaces the previous per-topic arnEquals
30
- // list so cross-plugin SNS subscriptions created at runtime by the
31
- // admin's manageSubscriptions hook (Phase 3 Step 1) are accepted
32
- // without requiring a redeploy of the receiving plugin. Security
33
- // boundary: SourceAccount condition keeps third-party-account
34
- // topics out; ArnLike + SNS service principal further narrows
35
- // accepted senders to in-account EventTopic resources.
36
- {
37
- sid: "AllowReceiveSnsEvents",
38
- principal: Principals({
39
- service: PrincipalIds([AWS.SNS.principal]),
40
- }),
41
- effect: Allow,
42
- actions: Actions(["sqs:SendMessage"]),
43
- resources: Resource(queueArn),
44
- conditions: {
45
- stringEquals: [
46
- ("aws:SourceAccount", ConditionValue(accountId)),
47
- ]->Dict.fromArray,
48
- arnLike: [
49
- ("aws:SourceArn", ConditionValue(`arn:aws:sns:*:${accountId}:*EventTopic-*`)),
50
- ]->Dict.fromArray,
51
- },
52
- },
53
- ],
54
- )
55
- ->toJsonString
56
- ->Pulumi.Input.make
57
- let _queuePolicy = {
58
- PulumiAws.SQS.QueuePolicy.make(
59
- ~name,
60
- ~args={
61
- queueUrl: queueId->Pulumi.Input.make,
62
- policy: queuePolicyDocument,
13
+ let createQueuePolicy = (queue: PulumiAws.SQS.Queue.t, name, opts) => {
14
+ // The document is a *value* derived from the resolved ARN, so it is built in
15
+ // an apply. The QueuePolicy itself stays outside: passing `queue.id` as an
16
+ // Output is what registers the policy -> queue dependency, without which
17
+ // Pulumi has no ordering constraint and can delete the queue first on a
18
+ // replacement, leaving the policy's delete polling a queue that is gone.
19
+ let queuePolicyDocument = queue.arn->Pulumi.Output.apply(queueArn => {
20
+ // Scopes cross-plugin SNS sources to this account (aws:SourceAccount) and
21
+ // to topic names following the Reventless EventTopic naming convention
22
+ // (aws:SourceArn arnLike).
23
+ let accountId = queueArn->accountIdOfQueueArn
24
+ PulumiAws.PolicyDocument.make(
25
+ ~id=name ++ "QueuePolicy",
26
+ ~statements=[
27
+ // Single statement allows SendMessage from any SNS topic owned by
28
+ // this AWS account whose name matches the Reventless EventTopic
29
+ // naming convention. A per-topic arnEquals list would require a
30
+ // redeploy of the receiving plugin whenever the platform's
31
+ // manageSubscriptions hook creates a cross-plugin SNS subscription at
32
+ // runtime. Security boundary: SourceAccount keeps third-party-account
33
+ // topics out; ArnLike + SNS service principal further narrows accepted
34
+ // senders to in-account EventTopic resources.
35
+ {
36
+ sid: "AllowReceiveSnsEvents",
37
+ principal: Principals({
38
+ service: PrincipalIds([AWS.SNS.principal]),
39
+ }),
40
+ effect: Allow,
41
+ actions: Actions(["sqs:SendMessage"]),
42
+ resources: Resource(queueArn),
43
+ conditions: {
44
+ stringEquals: [("aws:SourceAccount", ConditionValue(accountId))]->Dict.fromArray,
45
+ arnLike: [
46
+ ("aws:SourceArn", ConditionValue(`arn:aws:sns:*:${accountId}:*EventTopic-*`)),
47
+ ]->Dict.fromArray,
63
48
  },
64
- ~opts=Some(opts),
65
- )
66
- }
67
- })
49
+ },
50
+ ],
51
+ )->toJsonString
52
+ })
53
+ let _queuePolicy = PulumiAws.SQS.QueuePolicy.make(
54
+ ~name,
55
+ ~args={
56
+ queueUrl: queue.id->Pulumi.Output.asInput,
57
+ policy: queuePolicyDocument->Pulumi.Output.asInput,
58
+ },
59
+ ~opts=Some(opts),
60
+ )
68
61
  }
69
62
 
70
- let subscribeQueue2SnsTopic = (queue, name, resources: array<ReventlessInfra.Adapter.resolvedResource>, opts) => {
63
+ let subscribeQueue2SnsTopic = (
64
+ queue,
65
+ name,
66
+ resources: array<ReventlessInfra.Adapter.resolvedResource>,
67
+ opts,
68
+ ) => {
71
69
  let _snsTopicSubscriptions = resources->Array.map(resource => {
72
70
  log.debug(~comp="EventCollector", `subscribeToSnsTopic: ${name} -> ${resource.name}`)
73
71
  let subscription = Util_SQS.subscribeToSnsTopic(
@@ -82,17 +80,21 @@ let subscribeQueue2SnsTopic = (queue, name, resources: array<ReventlessInfra.Ada
82
80
  }
83
81
 
84
82
  let connectSqsQueue2SnsTopics = (queue: PulumiAws.SQS.Queue.t, name, eventTopics, opts) => {
83
+ // The policy grants SendMessage to any in-account EventTopic by naming
84
+ // convention, so it does not depend on the resolved topic resources and is
85
+ // created outside the apply. Only the SNS subscriptions need them.
86
+ queue->createQueuePolicy(name, opts)
85
87
  let _ =
86
88
  (eventTopics->toResources, queue.id)
87
89
  ->Pulumi.Output.all2
88
90
  ->Pulumi.Output.apply(((eventTopicResources, queueId)) => {
89
91
  log.debug(
90
92
  ~comp="EventCollector",
91
- `connectSqsQueue2SnsTopics ${queueId}: ${eventTopicResources->Array.length->Int.toString} topic resource(s)`,
93
+ `connectSqsQueue2SnsTopics ${queueId}: ${eventTopicResources
94
+ ->Array.length
95
+ ->Int.toString} topic resource(s)`,
92
96
  )
93
- let snsResources = eventTopicResources->snsResources
94
- queue->createQueuePolicy(name, snsResources, opts)
95
- queue->subscribeQueue2SnsTopic(name, snsResources, opts)
97
+ queue->subscribeQueue2SnsTopic(name, eventTopicResources->snsResources, opts)
96
98
  })
97
99
  }
98
100
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  import * as Aws from "@pulumi/aws";
4
4
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
- import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
5
  import * as Pulumi from "@pulumi/pulumi";
7
6
  import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
8
7
  import * as AWS$ReventlessAws from "../AWS.res.mjs";
@@ -20,14 +19,10 @@ function toResources(eventTopics) {
20
19
  return Adapter$ReventlessCore.resourcesToResolvedOutput(Object.values(eventTopics).flatMap(outputs => outputs.resources));
21
20
  }
22
21
 
23
- function createQueuePolicy(queue, name, _resources, opts) {
24
- Pulumi.all([
25
- queue.arn,
26
- queue.id
27
- ]).apply(param => {
28
- let queueArn = param[0];
29
- let accountId = Stdlib_Option.getOr(queueArn.split(":")[4], "");
30
- let queuePolicyDocument = PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "QueuePolicy", [{
22
+ function createQueuePolicy(queue, name, opts) {
23
+ let queuePolicyDocument = queue.arn.apply(queueArn => {
24
+ let accountId = Adapter_Helpers$ReventlessAws.accountIdOfQueueArn(queueArn);
25
+ return PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "QueuePolicy", [{
31
26
  Sid: "AllowReceiveSnsEvents",
32
27
  Principal: {
33
28
  Service: [AWS$ReventlessAws.SNS.principal]
@@ -46,11 +41,11 @@ function createQueuePolicy(queue, name, _resources, opts) {
46
41
  ]])
47
42
  }
48
43
  }]));
49
- new (Aws.sqs.QueuePolicy)(name, {
50
- policy: queuePolicyDocument,
51
- queueUrl: param[1]
52
- }, opts);
53
44
  });
45
+ new (Aws.sqs.QueuePolicy)(name, {
46
+ policy: queuePolicyDocument,
47
+ queueUrl: queue.id
48
+ }, opts);
54
49
  }
55
50
 
56
51
  function subscribeQueue2SnsTopic(queue, name, resources, opts) {
@@ -62,15 +57,14 @@ function subscribeQueue2SnsTopic(queue, name, resources, opts) {
62
57
  }
63
58
 
64
59
  function connectSqsQueue2SnsTopics(queue, name, eventTopics, opts) {
60
+ createQueuePolicy(queue, name, opts);
65
61
  Pulumi.all([
66
62
  toResources(eventTopics),
67
63
  queue.id
68
64
  ]).apply(param => {
69
65
  let eventTopicResources = param[0];
70
66
  log.debug("EventCollector", undefined, `connectSqsQueue2SnsTopics ` + param[1] + `: ` + eventTopicResources.length.toString() + ` topic resource(s)`);
71
- let snsResources = Adapter_Helpers$ReventlessAws.snsResources(eventTopicResources);
72
- createQueuePolicy(queue, name, snsResources, opts);
73
- subscribeQueue2SnsTopic(queue, name, snsResources, opts);
67
+ subscribeQueue2SnsTopic(queue, name, Adapter_Helpers$ReventlessAws.snsResources(eventTopicResources), opts);
74
68
  });
75
69
  }
76
70