@reventlessdev/reventless-aws 3.0.0-alpha.252 → 3.0.0-alpha.254

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 (38) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +8 -8
  3. package/src/Platform.res +159 -21
  4. package/src/Platform.res.mjs +120 -8
  5. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +5 -0
  6. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +3 -0
  7. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +5 -0
  8. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +3 -0
  9. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +66 -8
  10. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +16 -3
  11. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +5 -0
  12. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +3 -0
  13. package/src/adapter/Task/TaskBucket_S3.res +11 -0
  14. package/src/adapter/Task/TaskBucket_S3.res.mjs +3 -0
  15. package/src/adapter/Upload/Upload_Claim_S3.res +361 -0
  16. package/src/adapter/Upload/Upload_Claim_S3.res.mjs +203 -0
  17. package/src/adapter/Upload/Upload_Claim_S3_Ops.res +279 -0
  18. package/src/adapter/Upload/Upload_Claim_S3_Ops.res.mjs +261 -0
  19. package/src/adapter/Upload/Upload_PendingTag.res +41 -0
  20. package/src/adapter/Upload/Upload_PendingTag.res.mjs +15 -0
  21. package/src/adapter/Upload/Upload_Presign_S3.res +12 -2
  22. package/src/adapter/Upload/Upload_Presign_S3.res.mjs +1 -0
  23. package/src/adapter/Upload/Upload_Presign_S3_Ops.res +17 -1
  24. package/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs +3 -1
  25. package/src/capability/Capability_ObjectStore_S3.res +42 -1
  26. package/src/capability/Capability_ObjectStore_S3.res.mjs +19 -1
  27. package/src/components/Api/AppSync_Adapter.res +36 -0
  28. package/src/components/Api/AppSync_Adapter.res.mjs +14 -0
  29. package/src/util/Util_LogRetention.res +86 -0
  30. package/src/util/Util_LogRetention.res.mjs +43 -0
  31. package/src/util/Util_StoreLayout.res +62 -9
  32. package/src/util/Util_StoreLayout.res.mjs +20 -2
  33. package/tests/Upload_ClaimTest.res +132 -0
  34. package/tests/Upload_ClaimTest.res.mjs +101 -0
  35. package/tests/Util_LogRetentionTest.res +122 -0
  36. package/tests/Util_LogRetentionTest.res.mjs +95 -0
  37. package/tests/Util_StoreLayoutTest.res +79 -3
  38. package/tests/Util_StoreLayoutTest.res.mjs +47 -2
package/CHANGELOG.md CHANGED
@@ -3,6 +3,34 @@
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.254 (2026-08-02)
7
+
8
+ ### Features
9
+
10
+ * **aws,core,doc:** environment-tiered CloudWatch log retention and levels ([8032023](https://github.com/ReventlessDev/reventless-core/commit/803202305a7d352484b8df5e58df03c92ca58b5f))
11
+ * **aws,core,spec,seed-aws:** expire uploads nobody committed a reference to ([f63e84c](https://github.com/ReventlessDev/reventless-core/commit/f63e84c1a11cc350b799a6f69a2e7427cf1ea6e9))
12
+
13
+
14
+ # 3.0.0-alpha.253 (2026-08-01)
15
+
16
+ * feat(aws,core,spec)!: qualify store prefixes by plugin and refuse name collisions ([da39405](https://github.com/ReventlessDev/reventless-core/commit/da394059d9f8f981bf7adc79e2c1ce2b429e0267))
17
+ * feat(core,aws)!: name task buckets for their plugin and task ([05203cb](https://github.com/ReventlessDev/reventless-core/commit/05203cb3b0b019401a6f32cf78109f397b6799d5))
18
+
19
+ ### BREAKING CHANGES
20
+
21
+ * objects minted under the old bare prefix are orphaned and their
22
+ refs unresolvable. The migration is `seed:reset` for the owning plugin, then
23
+ re-seed. Legacy-prefix grandfathering was considered and deliberately dropped —
24
+ it would have added a permanent prefix SET across the deploy argument, both store
25
+ configs, the presign IAM fan-out, the release scope check and the stack output,
26
+ to spare a disposable stack one wipe.
27
+ * a bucket name change is a replace. TaskBucket_S3 now sets
28
+ forceDestroy on disposable stacks (Util_StoreLayout.protectionFor), so pr-*
29
+ stacks recreate cleanly; protected stacks must have the task bucket drained by
30
+ hand before the first deploy after this, or the replace fails BucketNotEmpty.
31
+
32
+
33
+
6
34
  # 3.0.0-alpha.252 (2026-08-01)
7
35
 
8
36
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.252",
3
+ "version": "3.0.0-alpha.254",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -11,18 +11,18 @@
11
11
  "@aws-sdk/s3-request-presigner": "3.970.0",
12
12
  "sury": "11.0.0-alpha.4",
13
13
  "uuid": "^13.0.0",
14
- "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.0",
15
- "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
14
+ "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.1",
16
15
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
16
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
17
17
  "@reventlessdev/rescript-node": "2.0.0-alpha.0",
18
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.62",
18
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.63",
19
19
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
20
20
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
21
- "@reventlessdev/reventless-infra": "3.0.0-alpha.116",
21
+ "@reventlessdev/reventless-core": "3.0.0-alpha.201",
22
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.118",
23
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.65",
22
24
  "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
23
- "@reventlessdev/reventless-spec": "3.0.0-alpha.91",
24
- "@reventlessdev/reventless-core": "3.0.0-alpha.199",
25
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.63"
25
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.93"
26
26
  },
27
27
  "devDependencies": {
28
28
  "rescript": "12.3.0",
package/src/Platform.res CHANGED
@@ -157,6 +157,58 @@ module MakeWithConfig = (
157
157
  ->Pulumi.Config.get("stack")
158
158
  ->Option.map(stack => Pulumi.StackReference.make(stack))
159
159
 
160
+ // The declared object stores, as the upload claimer needs them: qualified
161
+ // name, physical bucket, served prefix.
162
+ //
163
+ // Stores are provisioned by the *platform* deploy; the claimer is wired into
164
+ // the *plugin* stack, because that is where the event log streams it reads
165
+ // exist. So the two halves meet here — across the stack reference in plugin
166
+ // mode, and through the platform's own in-process record when platform and
167
+ // plugin are one program.
168
+ //
169
+ // Read lazily (a thunk, not a value) for the monolithic case: `deployPlatform`
170
+ // fills `objectStoreEndpointsRef` as it provisions, and this is called during
171
+ // a later plugin build.
172
+ let claimStores = (): Pulumi.Output.t<array<Upload_Claim_S3.storeConfig>> =>
173
+ switch platformStackRef {
174
+ | Some(stackRef) =>
175
+ // The `objectStores` export: `{ "<plugin>.<store>": {bucketName, keyPrefix} }`.
176
+ // A platform that provisions none omits the export entirely, which reads
177
+ // as "no store to claim in" — the claimer then builds nothing, rather
178
+ // than building something that can reach nothing.
179
+ (stackRef->Pulumi.StackReference.getOutput("objectStores"): Pulumi.Output.t<option<JSON.t>>)
180
+ ->Pulumi.Output.apply(objectStores =>
181
+ objectStores
182
+ ->Option.flatMap(JSON.Decode.object)
183
+ ->Option.getOr(Dict.make())
184
+ ->Dict.toArray
185
+ ->Array.filterMap(((qualified, v)) =>
186
+ v
187
+ ->JSON.Decode.object
188
+ ->Option.flatMap(o =>
189
+ switch (
190
+ o->Dict.get("bucketName")->Option.flatMap(JSON.Decode.string),
191
+ o->Dict.get("keyPrefix")->Option.flatMap(JSON.Decode.string),
192
+ ) {
193
+ | (Some(bucketName), Some(servedPrefix)) =>
194
+ Some({Upload_Claim_S3.qualified: qualified, bucketName, servedPrefix})
195
+ | _ => None
196
+ }
197
+ )
198
+ )
199
+ )
200
+ | None =>
201
+ objectStoreEndpointsRef.contents
202
+ ->Array.map(e =>
203
+ e.bucketName->Pulumi.Output.apply(bucketName => {
204
+ Upload_Claim_S3.qualified: e.store,
205
+ bucketName,
206
+ servedPrefix: e.keyPrefix,
207
+ })
208
+ )
209
+ ->Pulumi.Output.all
210
+ }
211
+
160
212
  // ── Merged-mode source SDL assembly (merged-api plan, Phase 3) ────────────
161
213
  // Empty base fragment — no types, no mutations, no queries. Used by the
162
214
  // plugin Api in split mode (so plugin schema has no core fields) and as the
@@ -172,7 +224,7 @@ module MakeWithConfig = (
172
224
  // The upload service (route B) lives on the domain base, not the Admin-gated admin
173
225
  // base: `Upload_Presign`/`Upload_Release` take the domain API's default
174
226
  // `AllowAuthenticated` auth so any authenticated user can mint/release their own
175
- // uploads. See [docs/plans/upload-release-path.md] § "Which API". (Split mode only —
227
+ // uploads. See [docs/plans/done/upload-release-path.md] § "Which API". (Split mode only —
176
228
  // unified mode carries the admin base here instead; it declares no stores in practice,
177
229
  // so the upload resolvers below are never created there.)
178
230
  let domainBaseFragment = ReventlessCore.GraphQL_Stitcher.encode({
@@ -763,7 +815,7 @@ module MakeWithConfig = (
763
815
  // Only active when the Events API resource exists (platform/monolithic mode).
764
816
  subscriptionInfraHook: ?domainEventsApiOpt->Option.map(eventsApi =>
765
817
  (params: ReventlessCore.Plugin_Helpers.subscriptionInfraParams) => {
766
- let {allQueryDbs, allEventTopics, eventLogEntries, opts} = params
818
+ let {pluginName, allQueryDbs, allEventTopics, eventLogEntries, opts} = params
767
819
  let customOpts =
768
820
  opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
769
821
  // StateTopic Lambda per stream-enabled QueryDb — publishes row changes
@@ -799,19 +851,34 @@ module MakeWithConfig = (
799
851
  let isSns =
800
852
  EventTopicPublisher_SNS.snsRegistry->Set.has(entry.displayName) ||
801
853
  EventTopicPublisher_SNS.snsRegistry->Set.has(entry.busKey)
802
- let eventTopicOutputs =
803
- isSns
804
- ? allEventTopics
805
- ->Dict.get(entry.displayName)
806
- ->Option.orElse(allEventTopics->Dict.get(entry.busKey))
807
- : None
808
- eventTopicOutputs->Option.forEach(outputs =>
809
- EventLogSubscription_AppSync.make(
810
- ~name=entry.displayName,
811
- ~topicName=entry.displayName,
854
+ let topicOutputs =
855
+ allEventTopics
856
+ ->Dict.get(entry.displayName)
857
+ ->Option.orElse(allEventTopics->Dict.get(entry.busKey))
858
+ isSns
859
+ ? topicOutputs->Option.forEach(outputs =>
860
+ EventLogSubscription_AppSync.make(
861
+ ~name=entry.displayName,
862
+ ~topicName=entry.displayName,
863
+ ~eventTopicOutputs=outputs,
864
+ ~eventsApi,
865
+ ~opts=customOpts,
866
+ )
867
+ )
868
+ : ()
869
+ // Register this event log with the upload claimer if — and only if —
870
+ // its events declare a `@storageRef` field. An event log that declares
871
+ // none is not registered, so a platform whose plugins declare no store
872
+ // provisions no claimer at all. `isSns` is the same synchronous answer
873
+ // the subscription above turns on, so the two cannot disagree about
874
+ // which channel an event log publishes through.
875
+ topicOutputs->Option.forEach(outputs =>
876
+ Upload_Claim_S3.make(
877
+ ~plugin=pluginName,
878
+ ~eventLogName=entry.displayName,
879
+ ~eventSchema=entry.eventSchema,
812
880
  ~eventTopicOutputs=outputs,
813
- ~eventsApi,
814
- ~opts=customOpts,
881
+ ~isStreamBacked=!isSns,
815
882
  )
816
883
  )
817
884
  })
@@ -822,6 +889,9 @@ module MakeWithConfig = (
822
889
  // empty because the hook hasn't run yet. Admin's hook fires synchronously,
823
890
  // but the call site is unified here so admin and plugins share one path.
824
891
  StateTopic_AppSync.finish(~eventsApi, ~opts={})
892
+ // Same timing constraint, same reason: the claimer's registry is filled
893
+ // by the loop just above and drained here.
894
+ Upload_Claim_S3.finish(~plugin=pluginName, ~stores=claimStores, ~opts=customOpts)
825
895
  }
826
896
  ),
827
897
  }
@@ -1063,12 +1133,15 @@ module MakeWithConfig = (
1063
1133
  // omitted (byte-identical config.json).
1064
1134
  geocoderPlaceIndex?: ReventlessInfra.Platform.geocoderIndex,
1065
1135
  // Optional object store for direct-to-S3 uploads, as returned by
1066
- // `Capability_ObjectStore_S3.make`. When set, the deploy provisions the
1067
- // presign service (Upload_Presign_S3) against it, threads that service's URL
1068
- // into config.json as `uploadEndpoint`, and fronts the store read-only on
1069
- // the host-shell CloudFront distribution under the presign service's own
1070
- // prefix (a `{prefix}/*` ordered cache behavior + origin + scoped
1071
- // BucketPolicy, private, OAC-read).
1136
+ // `Capability_ObjectStore_S3.make`. When set, the deploy registers it with
1137
+ // the platform's one Upload service (`Upload_Presign_S3`) under the `uploads`
1138
+ // store key, and fronts the store read-only on the host-shell CloudFront
1139
+ // distribution under that prefix (a `{prefix}/*` ordered cache behavior +
1140
+ // origin + scoped BucketPolicy, private, OAC-read).
1141
+ //
1142
+ // No endpoint is written into config.json: under route B a caller mints
1143
+ // through the domain API's `Upload_Presign` mutation, naming the store, so
1144
+ // there is no per-store URL to publish or to resolve.
1072
1145
  //
1073
1146
  // One field replaces the former `enableUploads` / `uploadBucketName` /
1074
1147
  // `servedBuckets` trio, which were three ways of saying one thing and left
@@ -1537,6 +1610,30 @@ module MakeWithConfig = (
1537
1610
  : Array.concat(acc, [(plugin, store)])
1538
1611
  )
1539
1612
 
1613
+ // Refuse two stores that cannot be told apart, BEFORE a single resource is
1614
+ // created. Left to the deploy, the same collision surfaces as a CloudFront
1615
+ // error about a duplicate path pattern — minutes in, naming a path pattern
1616
+ // rather than the two plugins — and only after the intermixed objects and
1617
+ // the over-broad presign grant have already been provisioned.
1618
+ //
1619
+ // Checked on the prefix, so it costs nothing to keep once prefixes are
1620
+ // qualified: a qualified prefix simply stops colliding, while the check
1621
+ // still guards any bare prefix that outlives the change.
1622
+ switch ReventlessCore.StorePrefixCollision.collisionsFor(
1623
+ ~stores=declaredStores->Array.map(((plugin, store)) => {
1624
+ ReventlessCore.StorePrefixCollision.qualified: `${plugin}.${store}`,
1625
+ prefix: Util_StoreLayout.keyPrefixFor(~plugin, ~store),
1626
+ }),
1627
+ ) {
1628
+ | [] => ()
1629
+ | collisions =>
1630
+ JsError.throwWithMessage(
1631
+ collisions
1632
+ ->Array.map(ReventlessCore.StorePrefixCollision.collisionMessage)
1633
+ ->Array.join("\n\n"),
1634
+ )
1635
+ }
1636
+
1540
1637
  if declaredStores->Array.length > 0 {
1541
1638
  // The fail-open in `layoutFor` is only dangerous while it is silent: a
1542
1639
  // production stack that is not on the prod list gets the shared layout and
@@ -1560,6 +1657,46 @@ module MakeWithConfig = (
1560
1657
  // policy and one CDN origin however many stores live in it.
1561
1658
  let storeBuckets: Dict.t<ReventlessInfra.Platform.objectStore> = Dict.make()
1562
1659
  let storeBucketOrder: array<string> = []
1660
+
1661
+ // Which stores expire their never-claimed uploads, grouped by the bucket
1662
+ // they live in. Computed before any bucket is created, because a bucket
1663
+ // carries its lifecycle rules from birth and a shared-layout bucket is
1664
+ // created once for several stores — the first store through the loop below
1665
+ // would otherwise be the only one whose setting was ever read.
1666
+ //
1667
+ // Off unless a deployment names the store. See `Util_StoreLayout.pendingExpiryFor`
1668
+ // for why this is a deployment's setting and not a declaration's, and why
1669
+ // there is deliberately no default.
1670
+ let pendingExpiryConfig = Util_LocalConfig.get("pendingUploadExpiryDays")
1671
+ let pendingExpiryByBucket: Dict.t<array<Capability_ObjectStore_S3.pendingExpiry>> = Dict.make()
1672
+ declaredStores->Array.forEach(((plugin, store)) =>
1673
+ Util_StoreLayout.pendingExpiryFor(
1674
+ ~config=pendingExpiryConfig,
1675
+ ~store=`${plugin}.${store}`,
1676
+ )->Option.forEach(days => {
1677
+ let bucketName = Util_StoreLayout.bucketNameFor(
1678
+ ~layout=storeLayout,
1679
+ ~stack=stackName,
1680
+ ~plugin,
1681
+ ~store,
1682
+ )
1683
+ let prefix = Util_StoreLayout.keyPrefixFor(~plugin, ~store)
1684
+ pendingExpiryByBucket->Dict.set(
1685
+ bucketName,
1686
+ pendingExpiryByBucket
1687
+ ->Dict.get(bucketName)
1688
+ ->Option.getOr([])
1689
+ ->Array.concat([{Capability_ObjectStore_S3.prefix, days}]),
1690
+ )
1691
+ log.info(
1692
+ ~comp="Platform:deployPlatform",
1693
+ `object store ${plugin}.${store}: never-claimed uploads under ${prefix}/ expire after ` ++
1694
+ `${days->Int.toString} days. Claimed objects and objects minted before the claim ` ++
1695
+ `component existed carry no pending tag and are outside the rule.`,
1696
+ )
1697
+ })
1698
+ )
1699
+
1563
1700
  let declaredStoreServices = declaredStores->Array.map(((plugin, store)) => {
1564
1701
  let bucketName = Util_StoreLayout.bucketNameFor(
1565
1702
  ~layout=storeLayout,
@@ -1567,7 +1704,7 @@ module MakeWithConfig = (
1567
1704
  ~plugin,
1568
1705
  ~store,
1569
1706
  )
1570
- let keyPrefix = Util_StoreLayout.keyPrefixFor(~store)
1707
+ let keyPrefix = Util_StoreLayout.keyPrefixFor(~plugin, ~store)
1571
1708
  let bucket = switch storeBuckets->Dict.get(bucketName) {
1572
1709
  | Some(b) => b
1573
1710
  | None =>
@@ -1584,6 +1721,7 @@ module MakeWithConfig = (
1584
1721
  // disposable stack would leak exactly the buckets sharing exists to
1585
1722
  // save. The two settings are opposite faces of one decision.
1586
1723
  ~forceDestroy=storeProtection == Unprotected,
1724
+ ~expirePending=pendingExpiryByBucket->Dict.get(bucketName)->Option.getOr([]),
1587
1725
  )
1588
1726
  storeBuckets->Dict.set(bucketName, b)
1589
1727
  storeBucketOrder->Array.push(bucketName)
@@ -35,6 +35,7 @@ import * as EventLogBackend$ReventlessAws from "./adapter/EventLog/EventLogBacke
35
35
  import * as Platform_Admin$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin.res.mjs";
36
36
  import * as PluginBehavior$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginBehavior.res.mjs";
37
37
  import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
38
+ import * as Upload_Claim_S3$ReventlessAws from "./adapter/Upload/Upload_Claim_S3.res.mjs";
38
39
  import * as PgProjectionFeed$ReventlessAws from "./adapter/Postgres/PgProjectionFeed.res.mjs";
39
40
  import * as Util_LocalConfig$ReventlessAws from "./util/Util_LocalConfig.res.mjs";
40
41
  import * as Util_StoreLayout$ReventlessAws from "./util/Util_StoreLayout.res.mjs";
@@ -59,6 +60,7 @@ import * as Geocoder_AwsLocation$ReventlessAws from "./adapter/Geocoder/Geocoder
59
60
  import * as QueryEngine_DynamoDb$ReventlessAws from "./adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs";
60
61
  import * as PluginRuntime_Builder$ReventlessAws from "./plugin/runtime/PluginRuntime_Builder.res.mjs";
61
62
  import * as PluginsReadModelSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs";
63
+ import * as StorePrefixCollision$ReventlessCore from "@reventlessdev/reventless-core/src/util/StorePrefixCollision.res.mjs";
62
64
  import * as ExtensionPoint_Builder$ReventlessAws from "./components/ExtensionPoint_Builder.res.mjs";
63
65
  import * as StateViewSlice_Builder$ReventlessAws from "./components/StateViewSlice_Builder.res.mjs";
64
66
  import * as Task_Builder_PerBucket$ReventlessAws from "./components/Task_Builder_PerBucket.res.mjs";
@@ -155,6 +157,30 @@ function MakeWithConfig(Config) {
155
157
  contents: "Domain"
156
158
  };
157
159
  let platformStackRef = Stdlib_Option.map(new Pulumi.Config("platform").get("stack"), stack => new Pulumi.StackReference(stack));
160
+ let claimStores = () => {
161
+ if (platformStackRef !== undefined) {
162
+ return Primitive_option.valFromOption(platformStackRef).getOutput("objectStores").apply(objectStores => Stdlib_Array.filterMap(Object.entries(Stdlib_Option.getOr(Stdlib_Option.flatMap(objectStores, Stdlib_JSON.Decode.object), {})), param => {
163
+ let qualified = param[0];
164
+ return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(param[1]), o => {
165
+ let match = Stdlib_Option.flatMap(o["bucketName"], Stdlib_JSON.Decode.string);
166
+ let match$1 = Stdlib_Option.flatMap(o["keyPrefix"], Stdlib_JSON.Decode.string);
167
+ if (match !== undefined && match$1 !== undefined) {
168
+ return {
169
+ qualified: qualified,
170
+ bucketName: match,
171
+ servedPrefix: match$1
172
+ };
173
+ }
174
+ });
175
+ }));
176
+ } else {
177
+ return Pulumi.all(objectStoreEndpointsRef.contents.map(e => e.bucketName.apply(bucketName => ({
178
+ qualified: e.store,
179
+ bucketName: bucketName,
180
+ servedPrefix: e.keyPrefix
181
+ }))));
182
+ }
183
+ };
158
184
  let domainBaseFragment = GraphQL_Stitcher$ReventlessCore.encode({
159
185
  types: Platform_AdminApi$ReventlessCore.uploadTypes,
160
186
  mutations: Platform_AdminApi$ReventlessCore.uploadMutationFields,
@@ -456,6 +482,7 @@ function MakeWithConfig(Config) {
456
482
  let hooks_subscriptionInfraHook = Stdlib_Option.map(domainEventsApiOpt, eventsApi => (params => {
457
483
  let allEventTopics = params.allEventTopics;
458
484
  let allQueryDbs = params.allQueryDbs;
485
+ let pluginName = params.pluginName;
459
486
  let customOpts = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(params.opts);
460
487
  Stdlib_Dict.forEachWithKey(allQueryDbs, (_queryDbOutputs, readModelName) => {
461
488
  if (!QueryDbStorage_DynamoDbStream$ReventlessAws.streamRegistry.has(readModelName)) {
@@ -466,10 +493,14 @@ function MakeWithConfig(Config) {
466
493
  });
467
494
  params.eventLogEntries.forEach(entry => {
468
495
  let isSns = EventTopicPublisher_SNS$ReventlessAws.snsRegistry.has(entry.displayName) || EventTopicPublisher_SNS$ReventlessAws.snsRegistry.has(entry.busKey);
469
- let eventTopicOutputs = isSns ? Stdlib_Option.orElse(allEventTopics[entry.displayName], allEventTopics[entry.busKey]) : undefined;
470
- Stdlib_Option.forEach(eventTopicOutputs, outputs => EventLogSubscription_AppSync$ReventlessAws.make(entry.displayName, entry.displayName, outputs, eventsApi, customOpts));
496
+ let topicOutputs = Stdlib_Option.orElse(allEventTopics[entry.displayName], allEventTopics[entry.busKey]);
497
+ if (isSns) {
498
+ Stdlib_Option.forEach(topicOutputs, outputs => EventLogSubscription_AppSync$ReventlessAws.make(entry.displayName, entry.displayName, outputs, eventsApi, customOpts));
499
+ }
500
+ Stdlib_Option.forEach(topicOutputs, outputs => Upload_Claim_S3$ReventlessAws.make(pluginName, entry.displayName, entry.eventSchema, outputs, !isSns));
471
501
  });
472
502
  StateTopic_AppSync$ReventlessAws.finish(eventsApi, {});
503
+ Upload_Claim_S3$ReventlessAws.finish(pluginName, claimStores, undefined, customOpts);
473
504
  }));
474
505
  let hooks_preResolversSchemaHook = (name, version, pluginFragment) => {
475
506
  let sdl = AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(pluginFragment);
@@ -904,6 +935,17 @@ function MakeWithConfig(Config) {
904
935
  ]]);
905
936
  }
906
937
  });
938
+ let collisions = StorePrefixCollision$ReventlessCore.collisionsFor(declaredStores.map(param => {
939
+ let store = param[1];
940
+ let plugin = param[0];
941
+ return {
942
+ qualified: plugin + `.` + store,
943
+ prefix: Util_StoreLayout$ReventlessAws.keyPrefixFor(plugin, store)
944
+ };
945
+ }));
946
+ if (collisions.length !== 0) {
947
+ Stdlib_JsError.throwWithMessage(collisions.map(StorePrefixCollision$ReventlessCore.collisionMessage).join("\n\n"));
948
+ }
907
949
  if (declaredStores.length !== 0) {
908
950
  let tmp;
909
951
  tmp = storeLayout === "PerStore" ? "bucket per store" : "shared bucket, {store}/… prefixes";
@@ -913,11 +955,26 @@ function MakeWithConfig(Config) {
913
955
  }
914
956
  let storeBuckets = {};
915
957
  let storeBucketOrder = [];
958
+ let pendingExpiryConfig = Util_LocalConfig$ReventlessAws.get("pendingUploadExpiryDays");
959
+ let pendingExpiryByBucket = {};
960
+ declaredStores.forEach(param => {
961
+ let store = param[1];
962
+ let plugin = param[0];
963
+ Stdlib_Option.forEach(Util_StoreLayout$ReventlessAws.pendingExpiryFor(pendingExpiryConfig, plugin + `.` + store), days => {
964
+ let bucketName = Util_StoreLayout$ReventlessAws.bucketNameFor(storeLayout, stackName, plugin, store);
965
+ let prefix = Util_StoreLayout$ReventlessAws.keyPrefixFor(plugin, store);
966
+ pendingExpiryByBucket[bucketName] = Stdlib_Option.getOr(pendingExpiryByBucket[bucketName], []).concat([{
967
+ prefix: prefix,
968
+ days: days
969
+ }]);
970
+ log.info("Platform:deployPlatform", undefined, `object store ` + plugin + `.` + store + `: never-claimed uploads under ` + prefix + `/ expire after ` + (days.toString() + ` days. Claimed objects and objects minted before the claim `) + `component existed carry no pending tag and are outside the rule.`);
971
+ });
972
+ });
916
973
  let declaredStoreServices = declaredStores.map(param => {
917
974
  let store = param[1];
918
975
  let plugin = param[0];
919
976
  let bucketName = Util_StoreLayout$ReventlessAws.bucketNameFor(storeLayout, stackName, plugin, store);
920
- let keyPrefix = Util_StoreLayout$ReventlessAws.keyPrefixFor(store);
977
+ let keyPrefix = Util_StoreLayout$ReventlessAws.keyPrefixFor(plugin, store);
921
978
  let b = storeBuckets[bucketName];
922
979
  let bucket;
923
980
  if (b !== undefined) {
@@ -925,7 +982,7 @@ function MakeWithConfig(Config) {
925
982
  } else {
926
983
  let tmp;
927
984
  tmp = storeLayout === "PerStore" ? plugin : undefined;
928
- let b$1 = Capability_ObjectStore_S3$ReventlessAws.make(bucketName, keyPrefix, tmp, storeProtection === "Protected", storeProtection === "Unprotected", undefined, undefined);
985
+ let b$1 = Capability_ObjectStore_S3$ReventlessAws.make(bucketName, keyPrefix, tmp, storeProtection === "Protected", storeProtection === "Unprotected", Stdlib_Option.getOr(pendingExpiryByBucket[bucketName], []), undefined, undefined);
929
986
  storeBuckets[bucketName] = b$1;
930
987
  storeBucketOrder.push(bucketName);
931
988
  bucket = b$1;
@@ -1312,6 +1369,30 @@ function Make($star) {
1312
1369
  contents: "Domain"
1313
1370
  };
1314
1371
  let platformStackRef = Stdlib_Option.map(new Pulumi.Config("platform").get("stack"), stack => new Pulumi.StackReference(stack));
1372
+ let claimStores = () => {
1373
+ if (platformStackRef !== undefined) {
1374
+ return Primitive_option.valFromOption(platformStackRef).getOutput("objectStores").apply(objectStores => Stdlib_Array.filterMap(Object.entries(Stdlib_Option.getOr(Stdlib_Option.flatMap(objectStores, Stdlib_JSON.Decode.object), {})), param => {
1375
+ let qualified = param[0];
1376
+ return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(param[1]), o => {
1377
+ let match = Stdlib_Option.flatMap(o["bucketName"], Stdlib_JSON.Decode.string);
1378
+ let match$1 = Stdlib_Option.flatMap(o["keyPrefix"], Stdlib_JSON.Decode.string);
1379
+ if (match !== undefined && match$1 !== undefined) {
1380
+ return {
1381
+ qualified: qualified,
1382
+ bucketName: match,
1383
+ servedPrefix: match$1
1384
+ };
1385
+ }
1386
+ });
1387
+ }));
1388
+ } else {
1389
+ return Pulumi.all(objectStoreEndpointsRef.contents.map(e => e.bucketName.apply(bucketName => ({
1390
+ qualified: e.store,
1391
+ bucketName: bucketName,
1392
+ servedPrefix: e.keyPrefix
1393
+ }))));
1394
+ }
1395
+ };
1315
1396
  let domainBaseFragment = GraphQL_Stitcher$ReventlessCore.encode({
1316
1397
  types: Platform_AdminApi$ReventlessCore.uploadTypes,
1317
1398
  mutations: Platform_AdminApi$ReventlessCore.uploadMutationFields,
@@ -1613,6 +1694,7 @@ function Make($star) {
1613
1694
  let hooks_subscriptionInfraHook = Stdlib_Option.map(domainEventsApiOpt, eventsApi => (params => {
1614
1695
  let allEventTopics = params.allEventTopics;
1615
1696
  let allQueryDbs = params.allQueryDbs;
1697
+ let pluginName = params.pluginName;
1616
1698
  let customOpts = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(params.opts);
1617
1699
  Stdlib_Dict.forEachWithKey(allQueryDbs, (_queryDbOutputs, readModelName) => {
1618
1700
  if (!QueryDbStorage_DynamoDbStream$ReventlessAws.streamRegistry.has(readModelName)) {
@@ -1623,10 +1705,14 @@ function Make($star) {
1623
1705
  });
1624
1706
  params.eventLogEntries.forEach(entry => {
1625
1707
  let isSns = EventTopicPublisher_SNS$ReventlessAws.snsRegistry.has(entry.displayName) || EventTopicPublisher_SNS$ReventlessAws.snsRegistry.has(entry.busKey);
1626
- let eventTopicOutputs = isSns ? Stdlib_Option.orElse(allEventTopics[entry.displayName], allEventTopics[entry.busKey]) : undefined;
1627
- Stdlib_Option.forEach(eventTopicOutputs, outputs => EventLogSubscription_AppSync$ReventlessAws.make(entry.displayName, entry.displayName, outputs, eventsApi, customOpts));
1708
+ let topicOutputs = Stdlib_Option.orElse(allEventTopics[entry.displayName], allEventTopics[entry.busKey]);
1709
+ if (isSns) {
1710
+ Stdlib_Option.forEach(topicOutputs, outputs => EventLogSubscription_AppSync$ReventlessAws.make(entry.displayName, entry.displayName, outputs, eventsApi, customOpts));
1711
+ }
1712
+ Stdlib_Option.forEach(topicOutputs, outputs => Upload_Claim_S3$ReventlessAws.make(pluginName, entry.displayName, entry.eventSchema, outputs, !isSns));
1628
1713
  });
1629
1714
  StateTopic_AppSync$ReventlessAws.finish(eventsApi, {});
1715
+ Upload_Claim_S3$ReventlessAws.finish(pluginName, claimStores, undefined, customOpts);
1630
1716
  }));
1631
1717
  let hooks_preResolversSchemaHook = (name, version, pluginFragment) => {
1632
1718
  let sdl = AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(pluginFragment);
@@ -2038,6 +2124,17 @@ function Make($star) {
2038
2124
  ]]);
2039
2125
  }
2040
2126
  });
2127
+ let collisions = StorePrefixCollision$ReventlessCore.collisionsFor(declaredStores.map(param => {
2128
+ let store = param[1];
2129
+ let plugin = param[0];
2130
+ return {
2131
+ qualified: plugin + `.` + store,
2132
+ prefix: Util_StoreLayout$ReventlessAws.keyPrefixFor(plugin, store)
2133
+ };
2134
+ }));
2135
+ if (collisions.length !== 0) {
2136
+ Stdlib_JsError.throwWithMessage(collisions.map(StorePrefixCollision$ReventlessCore.collisionMessage).join("\n\n"));
2137
+ }
2041
2138
  if (declaredStores.length !== 0) {
2042
2139
  let tmp;
2043
2140
  tmp = storeLayout === "PerStore" ? "bucket per store" : "shared bucket, {store}/… prefixes";
@@ -2047,11 +2144,26 @@ function Make($star) {
2047
2144
  }
2048
2145
  let storeBuckets = {};
2049
2146
  let storeBucketOrder = [];
2147
+ let pendingExpiryConfig = Util_LocalConfig$ReventlessAws.get("pendingUploadExpiryDays");
2148
+ let pendingExpiryByBucket = {};
2149
+ declaredStores.forEach(param => {
2150
+ let store = param[1];
2151
+ let plugin = param[0];
2152
+ Stdlib_Option.forEach(Util_StoreLayout$ReventlessAws.pendingExpiryFor(pendingExpiryConfig, plugin + `.` + store), days => {
2153
+ let bucketName = Util_StoreLayout$ReventlessAws.bucketNameFor(storeLayout, stackName, plugin, store);
2154
+ let prefix = Util_StoreLayout$ReventlessAws.keyPrefixFor(plugin, store);
2155
+ pendingExpiryByBucket[bucketName] = Stdlib_Option.getOr(pendingExpiryByBucket[bucketName], []).concat([{
2156
+ prefix: prefix,
2157
+ days: days
2158
+ }]);
2159
+ log.info("Platform:deployPlatform", undefined, `object store ` + plugin + `.` + store + `: never-claimed uploads under ` + prefix + `/ expire after ` + (days.toString() + ` days. Claimed objects and objects minted before the claim `) + `component existed carry no pending tag and are outside the rule.`);
2160
+ });
2161
+ });
2050
2162
  let declaredStoreServices = declaredStores.map(param => {
2051
2163
  let store = param[1];
2052
2164
  let plugin = param[0];
2053
2165
  let bucketName = Util_StoreLayout$ReventlessAws.bucketNameFor(storeLayout, stackName, plugin, store);
2054
- let keyPrefix = Util_StoreLayout$ReventlessAws.keyPrefixFor(store);
2166
+ let keyPrefix = Util_StoreLayout$ReventlessAws.keyPrefixFor(plugin, store);
2055
2167
  let b = storeBuckets[bucketName];
2056
2168
  let bucket;
2057
2169
  if (b !== undefined) {
@@ -2059,7 +2171,7 @@ function Make($star) {
2059
2171
  } else {
2060
2172
  let tmp;
2061
2173
  tmp = storeLayout === "PerStore" ? plugin : undefined;
2062
- let b$1 = Capability_ObjectStore_S3$ReventlessAws.make(bucketName, keyPrefix, tmp, storeProtection === "Protected", storeProtection === "Unprotected", undefined, undefined);
2174
+ let b$1 = Capability_ObjectStore_S3$ReventlessAws.make(bucketName, keyPrefix, tmp, storeProtection === "Protected", storeProtection === "Unprotected", Stdlib_Option.getOr(pendingExpiryByBucket[bucketName], []), undefined, undefined);
2063
2175
  storeBuckets[bucketName] = b$1;
2064
2176
  storeBucketOrder.push(bucketName);
2065
2177
  bucket = b$1;
@@ -298,6 +298,11 @@ let finish = () =>
298
298
  }
299
299
  })
300
300
  )
301
+ // First-class log-level override → LOG_LEVEL, winning over any generic
302
+ // envVars entry. When unset, makeFromCodeAsset applies the tier default.
303
+ cfg.logLevel->Option.forEach(level =>
304
+ envVars->Dict.set("LOG_LEVEL", level->Pulumi.Output.make->Pulumi.Output.asInput)
305
+ )
301
306
 
302
307
  // Build AssetArchive: static re-export + user packages
303
308
  let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
@@ -197,6 +197,9 @@ function finish() {
197
197
  return;
198
198
  }
199
199
  }));
200
+ Stdlib_Option.forEach(cfg.logLevel, level => {
201
+ envVars["LOG_LEVEL"] = Pulumi.output(level);
202
+ });
200
203
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AggregateEntryPoint.mjs", packageDirs, undefined);
201
204
  Stdlib_Option.forEach(cfg.sqsBatchSize, CommandTopicChannel_SQS_Sync$ReventlessAws.setBatchSize);
202
205
  let vpcConfig = pgSelection !== undefined ? pgSelection.securityGroupId.apply(sgId => ({
@@ -276,6 +276,11 @@ let finish = () =>
276
276
  }
277
277
  })
278
278
  )
279
+ // First-class log-level override → LOG_LEVEL, winning over any generic
280
+ // envVars entry. When unset, makeFromCodeAsset applies the tier default.
281
+ cfg.logLevel->Option.forEach(level =>
282
+ envVars->Dict.set("LOG_LEVEL", level->Pulumi.Output.make->Pulumi.Output.asInput)
283
+ )
279
284
 
280
285
  // Build AssetArchive: static re-export + user packages
281
286
  let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
@@ -186,6 +186,9 @@ function finish() {
186
186
  return;
187
187
  }
188
188
  }));
189
+ Stdlib_Option.forEach(cfg.logLevel, level => {
190
+ envVars["LOG_LEVEL"] = Pulumi.output(level);
191
+ });
189
192
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AggregateEntryPoint.mjs", packageDirs, undefined);
190
193
  Stdlib_Option.forEach(cfg.sqsBatchSize, CommandTopicChannel_SQS_Async$ReventlessAws.setBatchSize);
191
194
  let vpcConfig = pgSelection !== undefined ? pgSelection.securityGroupId.apply(sgId => ({