@reventlessdev/reventless-aws 3.0.0-alpha.239 → 3.0.0-alpha.240

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
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.240 (2026-07-28)
7
+
8
+ ### Features
9
+
10
+ * **platform:** provision object stores from the fields that declare them ([b5e2a1e](https://github.com/ReventlessDev/reventless-core/commit/b5e2a1ec88099941c113e4963f9c4b346b96b0d6))
11
+
12
+
6
13
  # 3.0.0-alpha.239 (2026-07-28)
7
14
 
8
15
  **Note:** Version bump only for package @reventlessdev/reventless-aws
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.239",
3
+ "version": "3.0.0-alpha.240",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -12,16 +12,16 @@
12
12
  "sury": "11.0.0-alpha.4",
13
13
  "uuid": "^13.0.0",
14
14
  "@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.25",
15
- "@reventlessdev/rescript-effect": "0.1.0-alpha.31",
16
15
  "@reventlessdev/rescript-jest": "1.0.0-alpha.9",
16
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.31",
17
17
  "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.61",
18
18
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
19
- "@reventlessdev/reventless-core": "3.0.0-alpha.189",
20
- "@reventlessdev/reventless-infra": "3.0.0-alpha.107",
21
- "@reventlessdev/reventless-interop": "3.0.0-alpha.29",
22
19
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
23
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.53",
24
- "@reventlessdev/reventless-spec": "3.0.0-alpha.82"
20
+ "@reventlessdev/reventless-core": "3.0.0-alpha.190",
21
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.108",
22
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.54",
23
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.83",
24
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.29"
25
25
  },
26
26
  "devDependencies": {
27
27
  "rescript": "12.3.0",
package/src/Platform.res CHANGED
@@ -1046,7 +1046,11 @@ module MakeWithConfig = (
1046
1046
  platformMergeGate: Pulumi.Output.t<unit>,
1047
1047
  }
1048
1048
 
1049
- let deployPlatform = (~version, ~hostUiBundle: option<hostUiBundleConfig>=?) => {
1049
+ let deployPlatform = (
1050
+ ~version,
1051
+ ~hostUiBundle: option<hostUiBundleConfig>=?,
1052
+ ~capabilities: array<ReventlessInfra.Platform.capability>=[],
1053
+ ) => {
1050
1054
  log.info(~comp="Platform:deployPlatform", `v${version}`)
1051
1055
  let scheduler = makeScheduler()
1052
1056
  hooks.scheduler := Some(scheduler)
@@ -1452,6 +1456,120 @@ module MakeWithConfig = (
1452
1456
  })
1453
1457
  })
1454
1458
 
1459
+ // ── Declared object stores ────────────────────────────────────────────
1460
+ //
1461
+ // The store a field declares is the store that gets provisioned. Before
1462
+ // this, a `@storageRef("productImages")` annotation and a hand-written
1463
+ // bucket name were two unrelated strings: deleting the annotation left the
1464
+ // bucket, and adding one provisioned nothing.
1465
+ //
1466
+ // Provisioned here, in the platform deploy, rather than in each plugin's
1467
+ // stack. Three things force it: the serving CDN lives here, the presign
1468
+ // services live here, and a shared-layout bucket has several plugins'
1469
+ // stores in it, so no single plugin stack can own it. Ownership is carried
1470
+ // in the tags instead — a per-store bucket is tagged to the plugin whose
1471
+ // field declared it; a shared bucket is platform substrate, because that is
1472
+ // what it is.
1473
+ let stackName = Pulumi.Pulumi.getStackName()
1474
+ let storeLayout = Util_StoreLayout.layoutFor(
1475
+ ~stack=stackName,
1476
+ ~prodStacks=Util_HostUiDomain.resolveProdStacks(),
1477
+ )
1478
+ let storeProtection = Util_StoreLayout.protectionFor(~stack=stackName)
1479
+
1480
+ // Dedup by `(plugin, store)` — that pair is a store's identity, and many
1481
+ // fields legitimately name one store.
1482
+ let declaredStores =
1483
+ capabilities
1484
+ ->Array.filterMap(c =>
1485
+ switch c {
1486
+ | ObjectStore({plugin, store}) => Some((plugin, store))
1487
+ | Geocoding => None
1488
+ }
1489
+ )
1490
+ ->Array.reduce([], (acc, (plugin, store)) =>
1491
+ acc->Array.some(((p, s)) => p == plugin && s == store)
1492
+ ? acc
1493
+ : Array.concat(acc, [(plugin, store)])
1494
+ )
1495
+
1496
+ if declaredStores->Array.length > 0 {
1497
+ // The fail-open in `layoutFor` is only dangerous while it is silent: a
1498
+ // production stack that is not on the prod list gets the shared layout and
1499
+ // nothing errors. Log the choice on every deploy so it surfaces the first
1500
+ // time it happens rather than at an audit.
1501
+ log.info(
1502
+ ~comp="Platform:deployPlatform",
1503
+ `object stores: ${switch storeLayout {
1504
+ | PerStore => "bucket per store"
1505
+ | SharedBucket => "shared bucket, {store}/… prefixes"
1506
+ }}, ${switch storeProtection {
1507
+ | Protected => "protected"
1508
+ | Unprotected => "unprotected (disposable stack)"
1509
+ }} — stack ${stackName}`,
1510
+ )
1511
+ }
1512
+
1513
+ // One bucket per *physical name*, which is one per store under `PerStore`
1514
+ // and one for the whole stack under `SharedBucket`. Grouped rather than
1515
+ // mapped because those cardinalities differ, and a bucket carries only one
1516
+ // policy and one CDN origin however many stores live in it.
1517
+ let storeBuckets: Dict.t<ReventlessInfra.Platform.objectStore> = Dict.make()
1518
+ let storeBucketOrder: array<string> = []
1519
+ let declaredStoreServices = declaredStores->Array.map(((plugin, store)) => {
1520
+ let bucketName = Util_StoreLayout.bucketNameFor(
1521
+ ~layout=storeLayout,
1522
+ ~stack=stackName,
1523
+ ~plugin,
1524
+ ~store,
1525
+ )
1526
+ let keyPrefix = Util_StoreLayout.keyPrefixFor(~store)
1527
+ let bucket = switch storeBuckets->Dict.get(bucketName) {
1528
+ | Some(b) => b
1529
+ | None =>
1530
+ let b = Capability_ObjectStore_S3.make(
1531
+ ~name=bucketName,
1532
+ ~keyPrefix,
1533
+ // A shared bucket belongs to no single plugin; a dedicated one does.
1534
+ ~plugin=?switch storeLayout {
1535
+ | PerStore => Some(plugin)
1536
+ | SharedBucket => None
1537
+ },
1538
+ ~protect=storeProtection == Protected,
1539
+ // A protected bucket blocks `pulumi destroy`, so protecting a
1540
+ // disposable stack would leak exactly the buckets sharing exists to
1541
+ // save. The two settings are opposite faces of one decision.
1542
+ ~forceDestroy=storeProtection == Unprotected,
1543
+ )
1544
+ storeBuckets->Dict.set(bucketName, b)
1545
+ storeBucketOrder->Array.push(bucketName)
1546
+ b
1547
+ }
1548
+ let storeHandle = bucket->Capability_ObjectStore_S3.underPrefix(~keyPrefix)
1549
+ // One presign service per store, scoped to that store's own prefix.
1550
+ let presign = Upload_Presign_S3.make(
1551
+ ~name=`UploadPresign-${plugin}-${store}`,
1552
+ ~bucketName=storeHandle.bucketName,
1553
+ ~servedPrefix=storeHandle.keyPrefix,
1554
+ )
1555
+ (`${plugin}.${store}`, keyPrefix, bucketName, presign.url)
1556
+ })
1557
+
1558
+ // Group the served view by bucket: one origin and one bucket policy per
1559
+ // bucket, carrying every prefix served from it.
1560
+ let declaredServedBuckets = storeBucketOrder->Array.filterMap(bucketName =>
1561
+ storeBuckets
1562
+ ->Dict.get(bucketName)
1563
+ ->Option.map(b => {
1564
+ ReventlessInfra.Platform.id: bucketName,
1565
+ prefixes: declaredStoreServices
1566
+ ->Array.filterMap(((_, prefix, bn, _)) => bn == bucketName ? Some(prefix) : None),
1567
+ bucketId: b.bucketId,
1568
+ bucketArn: b.bucketArn,
1569
+ bucketRegionalDomainName: b.bucketRegionalDomainName,
1570
+ })
1571
+ )
1572
+
1455
1573
  // Host UI shell deployment — opt-in via ~hostUiBundle. The shell SPA is
1456
1574
  // hosted on its own CloudFront distribution; `config.json` is generated
1457
1575
  // at deploy time with the resolved API endpoints, region, and Cognito
@@ -1494,17 +1612,27 @@ module MakeWithConfig = (
1494
1612
  // roots its keys at, read from the service's own constant. This is the
1495
1613
  // whole reason the store arrives as one value: the app no longer restates
1496
1614
  // the prefix, so it cannot restate it wrongly.
1497
- let servedBuckets = switch cfg.uploadBucket {
1498
- | Some(store) => [
1499
- {
1500
- ReventlessInfra.Platform.prefix: Upload_Presign_S3.defaultServedPrefix,
1501
- bucketId: store.bucketId,
1502
- bucketArn: store.bucketArn,
1503
- bucketRegionalDomainName: store.bucketRegionalDomainName,
1504
- },
1505
- ]
1506
- | None => []
1507
- }
1615
+ //
1616
+ // The legacy hand-configured store is served alongside the declared ones,
1617
+ // not replaced by them. Refs live in an append-only event log, so the
1618
+ // `/uploads/…` values already written there can never be rewritten;
1619
+ // `uploads` is therefore treated as a store that happens to predate the
1620
+ // declaration, and keeps its prefix permanently.
1621
+ let servedBuckets = Array.concat(
1622
+ switch cfg.uploadBucket {
1623
+ | Some(store) => [
1624
+ {
1625
+ ReventlessInfra.Platform.id: store.keyPrefix,
1626
+ prefixes: [store.keyPrefix],
1627
+ bucketId: store.bucketId,
1628
+ bucketArn: store.bucketArn,
1629
+ bucketRegionalDomainName: store.bucketRegionalDomainName,
1630
+ },
1631
+ ]
1632
+ | None => []
1633
+ },
1634
+ declaredServedBuckets,
1635
+ )
1508
1636
 
1509
1637
  let {distributionUrl, bucketName} = Plugin_Stack.makeUiBundleDistribution(
1510
1638
  ~pluginId="host-ui",
@@ -1569,6 +1697,18 @@ module MakeWithConfig = (
1569
1697
  | None => Pulumi.Output.make(None)
1570
1698
  }
1571
1699
 
1700
+ // Per-store presign endpoints, keyed by the store's qualified name. This
1701
+ // is what lets an upload input bind to the store its field *declares*
1702
+ // rather than inferring one from the field's name — the endpoint exists
1703
+ // per store, so there is something concrete to bind to.
1704
+ //
1705
+ // `uploadEndpoint` (singular) stays as the legacy service's URL: a shell
1706
+ // built before per-store binding reads it and is unaffected.
1707
+ let storeUploadEndpointsOutput: Pulumi.Output.t<array<(string, string)>> =
1708
+ declaredStoreServices
1709
+ ->Array.map(((qualified, _, _, url)) => url->Pulumi.Output.apply(u => (qualified, u)))
1710
+ ->Pulumi.Output.all
1711
+
1572
1712
  let configJsonContent =
1573
1713
  (
1574
1714
  (
@@ -1579,14 +1719,14 @@ module MakeWithConfig = (
1579
1719
  )->Pulumi.Output.all4,
1580
1720
  domainEventsEndpointOutput,
1581
1721
  geocoderEndpointOutput,
1582
- uploadEndpointOutput,
1722
+ (uploadEndpointOutput, storeUploadEndpointsOutput)->Pulumi.Output.all2,
1583
1723
  )
1584
1724
  ->Pulumi.Output.all4
1585
1725
  ->Pulumi.Output.apply(((
1586
1726
  (domainEp, platformEp, poolId, clientId),
1587
1727
  eventsEpOpt,
1588
1728
  geocoderEpOpt,
1589
- uploadEpOpt,
1729
+ (uploadEpOpt, storeUploadEps),
1590
1730
  )) => {
1591
1731
  let fields = [
1592
1732
  ("apiEndpoint", JSON.Encode.string(domainEp)),
@@ -1623,7 +1763,25 @@ module MakeWithConfig = (
1623
1763
  | Some(ep) => Array.concat(withGeocoder, [("uploadEndpoint", JSON.Encode.string(ep))])
1624
1764
  | None => withGeocoder
1625
1765
  }
1626
- withUpload->Dict.fromArray->JSON.Encode.object->JSON.stringify
1766
+ // Omitted entirely when nothing is declared, so a deployment without
1767
+ // declared stores writes a byte-identical config.json.
1768
+ let withStoreUploads = switch storeUploadEps {
1769
+ | [] => withUpload
1770
+ | eps =>
1771
+ Array.concat(
1772
+ withUpload,
1773
+ [
1774
+ (
1775
+ "uploadEndpoints",
1776
+ eps
1777
+ ->Array.map(((qualified, url)) => (qualified, JSON.Encode.string(url)))
1778
+ ->Dict.fromArray
1779
+ ->JSON.Encode.object,
1780
+ ),
1781
+ ],
1782
+ )
1783
+ }
1784
+ withStoreUploads->Dict.fromArray->JSON.Encode.object->JSON.stringify
1627
1785
  })
1628
1786
 
1629
1787
  let _ = PulumiAws.S3.BucketObject.make(
@@ -36,6 +36,7 @@ import * as PluginBehavior$ReventlessCore from "@reventlessdev/reventless-core/s
36
36
  import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
37
37
  import * as PgProjectionFeed$ReventlessAws from "./adapter/Postgres/PgProjectionFeed.res.mjs";
38
38
  import * as Util_LocalConfig$ReventlessAws from "./util/Util_LocalConfig.res.mjs";
39
+ import * as Util_StoreLayout$ReventlessAws from "./util/Util_StoreLayout.res.mjs";
39
40
  import * as IndexJs from "@pulumi/pulumi/runtime/index.js";
40
41
  import * as AppSync_EventsApi$ReventlessAws from "./adapter/Api/AppSync_EventsApi.res.mjs";
41
42
  import * as AppSync_MergedApi$ReventlessAws from "./components/Api/AppSync_MergedApi.res.mjs";
@@ -68,6 +69,7 @@ import * as UiFragments_Projection$ReventlessCore from "@reventlessdev/reventles
68
69
  import * as Aggregate_Builder_Single$ReventlessAws from "./components/Aggregate_Builder_Single.res.mjs";
69
70
  import * as ReadModel_Builder_Single$ReventlessAws from "./components/ReadModel_Builder_Single.res.mjs";
70
71
  import * as StateChangeSlice_Builder$ReventlessAws from "./components/StateChangeSlice_Builder.res.mjs";
72
+ import * as Capability_ObjectStore_S3$ReventlessAws from "./capability/Capability_ObjectStore_S3.res.mjs";
71
73
  import * as EventCollectorChannel_SQS$ReventlessAws from "./adapter/EventCollector/EventCollectorChannel_SQS.res.mjs";
72
74
  import * as PgChangeFeedRelay_Builder$ReventlessAws from "./adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs";
73
75
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "./adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs";
@@ -708,7 +710,8 @@ function MakeWithConfig(Config) {
708
710
  log.info("Platform", undefined, `v` + version);
709
711
  return Pervasives.failwith("makePlatform is not supported on AWS — deploy the platform with deployPlatform and each plugin with deployPlugin (merged-API composition).");
710
712
  };
711
- let deployPlatform = (version, hostUiBundle) => {
713
+ let deployPlatform = (version, hostUiBundle, capabilitiesOpt) => {
714
+ let capabilities = capabilitiesOpt !== undefined ? capabilitiesOpt : [];
712
715
  log.info("Platform:deployPlatform", undefined, `v` + version);
713
716
  let scheduler = makeScheduler();
714
717
  hooks_scheduler.contents = scheduler;
@@ -868,6 +871,76 @@ function MakeWithConfig(Config) {
868
871
  adminResources: param[4]
869
872
  });
870
873
  });
874
+ let stackName = Pulumi.getStack();
875
+ let storeLayout = Util_StoreLayout$ReventlessAws.layoutFor(stackName, Util_HostUiDomain$ReventlessAws.resolveProdStacks());
876
+ let storeProtection = Util_StoreLayout$ReventlessAws.protectionFor(stackName, undefined);
877
+ let declaredStores = Stdlib_Array.reduce(Stdlib_Array.filterMap(capabilities, c => {
878
+ if (typeof c !== "object") {
879
+ return;
880
+ } else {
881
+ return [
882
+ c.plugin,
883
+ c.store
884
+ ];
885
+ }
886
+ }), [], (acc, param) => {
887
+ let store = param[1];
888
+ let plugin = param[0];
889
+ if (acc.some(param => param[0] === plugin ? param[1] === store : false)) {
890
+ return acc;
891
+ } else {
892
+ return acc.concat([[
893
+ plugin,
894
+ store
895
+ ]]);
896
+ }
897
+ });
898
+ if (declaredStores.length !== 0) {
899
+ let tmp;
900
+ tmp = storeLayout === "PerStore" ? "bucket per store" : "shared bucket, {store}/… prefixes";
901
+ let tmp$1;
902
+ tmp$1 = storeProtection === "Protected" ? "protected" : "unprotected (disposable stack)";
903
+ log.info("Platform:deployPlatform", undefined, `object stores: ` + tmp + `, ` + tmp$1 + ` — stack ` + stackName);
904
+ }
905
+ let storeBuckets = {};
906
+ let storeBucketOrder = [];
907
+ let declaredStoreServices = declaredStores.map(param => {
908
+ let store = param[1];
909
+ let plugin = param[0];
910
+ let bucketName = Util_StoreLayout$ReventlessAws.bucketNameFor(storeLayout, stackName, plugin, store);
911
+ let keyPrefix = Util_StoreLayout$ReventlessAws.keyPrefixFor(store);
912
+ let b = storeBuckets[bucketName];
913
+ let bucket;
914
+ if (b !== undefined) {
915
+ bucket = b;
916
+ } else {
917
+ let tmp;
918
+ tmp = storeLayout === "PerStore" ? plugin : undefined;
919
+ let b$1 = Capability_ObjectStore_S3$ReventlessAws.make(bucketName, keyPrefix, tmp, storeProtection === "Protected", storeProtection === "Unprotected", undefined, undefined);
920
+ storeBuckets[bucketName] = b$1;
921
+ storeBucketOrder.push(bucketName);
922
+ bucket = b$1;
923
+ }
924
+ let storeHandle = Capability_ObjectStore_S3$ReventlessAws.underPrefix(bucket, keyPrefix);
925
+ let presign = Upload_Presign_S3$ReventlessAws.make(storeHandle.bucketName, undefined, storeHandle.keyPrefix, `UploadPresign-` + plugin + `-` + store, undefined);
926
+ return [
927
+ plugin + `.` + store,
928
+ keyPrefix,
929
+ bucketName,
930
+ presign.url
931
+ ];
932
+ });
933
+ let declaredServedBuckets = Stdlib_Array.filterMap(storeBucketOrder, bucketName => Stdlib_Option.map(storeBuckets[bucketName], b => ({
934
+ id: bucketName,
935
+ prefixes: Stdlib_Array.filterMap(declaredStoreServices, param => {
936
+ if (param[2] === bucketName) {
937
+ return param[1];
938
+ }
939
+ }),
940
+ bucketId: b.bucketId,
941
+ bucketArn: b.bucketArn,
942
+ bucketRegionalDomainName: b.bucketRegionalDomainName
943
+ })));
871
944
  if (hostUiBundle !== undefined) {
872
945
  let match$1 = Util_LocalConfig$ReventlessAws.get("hostUiBaseDomain");
873
946
  let match$2 = Util_LocalConfig$ReventlessAws.get("hostUiHostedZoneId");
@@ -885,12 +958,15 @@ function MakeWithConfig(Config) {
885
958
  customDomain = undefined;
886
959
  }
887
960
  let store = hostUiBundle.uploadBucket;
888
- let servedBuckets = store !== undefined ? [{
889
- prefix: Upload_Presign_S3$ReventlessAws.defaultServedPrefix,
890
- bucketId: store.bucketId,
891
- bucketArn: store.bucketArn,
892
- bucketRegionalDomainName: store.bucketRegionalDomainName
893
- }] : [];
961
+ let servedBuckets = (
962
+ store !== undefined ? [{
963
+ id: store.keyPrefix,
964
+ prefixes: [store.keyPrefix],
965
+ bucketId: store.bucketId,
966
+ bucketArn: store.bucketArn,
967
+ bucketRegionalDomainName: store.bucketRegionalDomainName
968
+ }] : []
969
+ ).concat(declaredServedBuckets);
894
970
  let match$3 = Plugin_Stack$ReventlessAws.makeUiBundleDistribution("host-ui", Stdlib_Option.getOr(hostUiBundle.bundleVersion, version), Stdlib_Option.getOr(hostUiBundle.assetsDir, Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-host-shell") + "/dist"), true, undefined, true, [
895
971
  "config.json",
896
972
  "ui-hints.json"
@@ -902,7 +978,14 @@ function MakeWithConfig(Config) {
902
978
  let index = hostUiBundle.geocoderPlaceIndex;
903
979
  let geocoderEndpointOutput = index !== undefined ? Geocoder_AwsLocation$ReventlessAws.make(index.indexName, undefined, undefined).url.apply(u => u) : Pulumi.output(undefined);
904
980
  let store$1 = hostUiBundle.uploadBucket;
905
- let uploadEndpointOutput = store$1 !== undefined ? Upload_Presign_S3$ReventlessAws.make(store$1.bucketName, undefined, undefined, undefined).url.apply(u => u) : Pulumi.output(undefined);
981
+ let uploadEndpointOutput = store$1 !== undefined ? Upload_Presign_S3$ReventlessAws.make(store$1.bucketName, undefined, undefined, undefined, undefined).url.apply(u => u) : Pulumi.output(undefined);
982
+ let storeUploadEndpointsOutput = Pulumi.all(declaredStoreServices.map(param => {
983
+ let qualified = param[0];
984
+ return param[3].apply(u => [
985
+ qualified,
986
+ u
987
+ ]);
988
+ }));
906
989
  let configJsonContent = Pulumi.all([
907
990
  Pulumi.all([
908
991
  resolvedDomainApiEndpoint,
@@ -912,20 +995,25 @@ function MakeWithConfig(Config) {
912
995
  ]),
913
996
  domainEventsEndpointOutput,
914
997
  geocoderEndpointOutput,
915
- uploadEndpointOutput
998
+ Pulumi.all([
999
+ uploadEndpointOutput,
1000
+ storeUploadEndpointsOutput
1001
+ ])
916
1002
  ]).apply(param => {
917
- let uploadEpOpt = param[3];
1003
+ let match = param[3];
1004
+ let storeUploadEps = match[1];
1005
+ let uploadEpOpt = match[0];
918
1006
  let geocoderEpOpt = param[2];
919
1007
  let eventsEpOpt = param[1];
920
- let match = param[0];
1008
+ let match$1 = param[0];
921
1009
  let fields = [
922
1010
  [
923
1011
  "apiEndpoint",
924
- match[0]
1012
+ match$1[0]
925
1013
  ],
926
1014
  [
927
1015
  "platformApiEndpoint",
928
- match[1]
1016
+ match$1[1]
929
1017
  ],
930
1018
  [
931
1019
  "region",
@@ -937,11 +1025,11 @@ function MakeWithConfig(Config) {
937
1025
  ],
938
1026
  [
939
1027
  "cognitoUserPoolId",
940
- match[2]
1028
+ match$1[2]
941
1029
  ],
942
1030
  [
943
1031
  "cognitoClientId",
944
- match[3]
1032
+ match$1[3]
945
1033
  ],
946
1034
  [
947
1035
  "liveUpdates",
@@ -970,7 +1058,14 @@ function MakeWithConfig(Config) {
970
1058
  "uploadEndpoint",
971
1059
  uploadEpOpt
972
1060
  ]]) : withGeocoder;
973
- return JSON.stringify(Object.fromEntries(withUpload));
1061
+ let withStoreUploads = storeUploadEps.length !== 0 ? withUpload.concat([[
1062
+ "uploadEndpoints",
1063
+ Object.fromEntries(storeUploadEps.map(param => [
1064
+ param[0],
1065
+ param[1]
1066
+ ]))
1067
+ ]]) : withUpload;
1068
+ return JSON.stringify(Object.fromEntries(withStoreUploads));
974
1069
  });
975
1070
  new (Aws.s3.BucketObject)("host-ui-config-json", {
976
1071
  bucket: bucketName,
@@ -1724,7 +1819,8 @@ function Make($star) {
1724
1819
  log.info("Platform", undefined, `v` + version);
1725
1820
  return Pervasives.failwith("makePlatform is not supported on AWS — deploy the platform with deployPlatform and each plugin with deployPlugin (merged-API composition).");
1726
1821
  };
1727
- let deployPlatform = (version, hostUiBundle) => {
1822
+ let deployPlatform = (version, hostUiBundle, capabilitiesOpt) => {
1823
+ let capabilities = capabilitiesOpt !== undefined ? capabilitiesOpt : [];
1728
1824
  log.info("Platform:deployPlatform", undefined, `v` + version);
1729
1825
  let scheduler = makeScheduler();
1730
1826
  hooks_scheduler.contents = scheduler;
@@ -1861,6 +1957,76 @@ function Make($star) {
1861
1957
  adminResources: param[4]
1862
1958
  });
1863
1959
  });
1960
+ let stackName = Pulumi.getStack();
1961
+ let storeLayout = Util_StoreLayout$ReventlessAws.layoutFor(stackName, Util_HostUiDomain$ReventlessAws.resolveProdStacks());
1962
+ let storeProtection = Util_StoreLayout$ReventlessAws.protectionFor(stackName, undefined);
1963
+ let declaredStores = Stdlib_Array.reduce(Stdlib_Array.filterMap(capabilities, c => {
1964
+ if (typeof c !== "object") {
1965
+ return;
1966
+ } else {
1967
+ return [
1968
+ c.plugin,
1969
+ c.store
1970
+ ];
1971
+ }
1972
+ }), [], (acc, param) => {
1973
+ let store = param[1];
1974
+ let plugin = param[0];
1975
+ if (acc.some(param => param[0] === plugin ? param[1] === store : false)) {
1976
+ return acc;
1977
+ } else {
1978
+ return acc.concat([[
1979
+ plugin,
1980
+ store
1981
+ ]]);
1982
+ }
1983
+ });
1984
+ if (declaredStores.length !== 0) {
1985
+ let tmp;
1986
+ tmp = storeLayout === "PerStore" ? "bucket per store" : "shared bucket, {store}/… prefixes";
1987
+ let tmp$1;
1988
+ tmp$1 = storeProtection === "Protected" ? "protected" : "unprotected (disposable stack)";
1989
+ log.info("Platform:deployPlatform", undefined, `object stores: ` + tmp + `, ` + tmp$1 + ` — stack ` + stackName);
1990
+ }
1991
+ let storeBuckets = {};
1992
+ let storeBucketOrder = [];
1993
+ let declaredStoreServices = declaredStores.map(param => {
1994
+ let store = param[1];
1995
+ let plugin = param[0];
1996
+ let bucketName = Util_StoreLayout$ReventlessAws.bucketNameFor(storeLayout, stackName, plugin, store);
1997
+ let keyPrefix = Util_StoreLayout$ReventlessAws.keyPrefixFor(store);
1998
+ let b = storeBuckets[bucketName];
1999
+ let bucket;
2000
+ if (b !== undefined) {
2001
+ bucket = b;
2002
+ } else {
2003
+ let tmp;
2004
+ tmp = storeLayout === "PerStore" ? plugin : undefined;
2005
+ let b$1 = Capability_ObjectStore_S3$ReventlessAws.make(bucketName, keyPrefix, tmp, storeProtection === "Protected", storeProtection === "Unprotected", undefined, undefined);
2006
+ storeBuckets[bucketName] = b$1;
2007
+ storeBucketOrder.push(bucketName);
2008
+ bucket = b$1;
2009
+ }
2010
+ let storeHandle = Capability_ObjectStore_S3$ReventlessAws.underPrefix(bucket, keyPrefix);
2011
+ let presign = Upload_Presign_S3$ReventlessAws.make(storeHandle.bucketName, undefined, storeHandle.keyPrefix, `UploadPresign-` + plugin + `-` + store, undefined);
2012
+ return [
2013
+ plugin + `.` + store,
2014
+ keyPrefix,
2015
+ bucketName,
2016
+ presign.url
2017
+ ];
2018
+ });
2019
+ let declaredServedBuckets = Stdlib_Array.filterMap(storeBucketOrder, bucketName => Stdlib_Option.map(storeBuckets[bucketName], b => ({
2020
+ id: bucketName,
2021
+ prefixes: Stdlib_Array.filterMap(declaredStoreServices, param => {
2022
+ if (param[2] === bucketName) {
2023
+ return param[1];
2024
+ }
2025
+ }),
2026
+ bucketId: b.bucketId,
2027
+ bucketArn: b.bucketArn,
2028
+ bucketRegionalDomainName: b.bucketRegionalDomainName
2029
+ })));
1864
2030
  if (hostUiBundle !== undefined) {
1865
2031
  let match$1 = Util_LocalConfig$ReventlessAws.get("hostUiBaseDomain");
1866
2032
  let match$2 = Util_LocalConfig$ReventlessAws.get("hostUiHostedZoneId");
@@ -1878,12 +2044,15 @@ function Make($star) {
1878
2044
  customDomain = undefined;
1879
2045
  }
1880
2046
  let store = hostUiBundle.uploadBucket;
1881
- let servedBuckets = store !== undefined ? [{
1882
- prefix: Upload_Presign_S3$ReventlessAws.defaultServedPrefix,
1883
- bucketId: store.bucketId,
1884
- bucketArn: store.bucketArn,
1885
- bucketRegionalDomainName: store.bucketRegionalDomainName
1886
- }] : [];
2047
+ let servedBuckets = (
2048
+ store !== undefined ? [{
2049
+ id: store.keyPrefix,
2050
+ prefixes: [store.keyPrefix],
2051
+ bucketId: store.bucketId,
2052
+ bucketArn: store.bucketArn,
2053
+ bucketRegionalDomainName: store.bucketRegionalDomainName
2054
+ }] : []
2055
+ ).concat(declaredServedBuckets);
1887
2056
  let match$3 = Plugin_Stack$ReventlessAws.makeUiBundleDistribution("host-ui", Stdlib_Option.getOr(hostUiBundle.bundleVersion, version), Stdlib_Option.getOr(hostUiBundle.assetsDir, Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-host-shell") + "/dist"), true, undefined, true, [
1888
2057
  "config.json",
1889
2058
  "ui-hints.json"
@@ -1895,7 +2064,14 @@ function Make($star) {
1895
2064
  let index = hostUiBundle.geocoderPlaceIndex;
1896
2065
  let geocoderEndpointOutput = index !== undefined ? Geocoder_AwsLocation$ReventlessAws.make(index.indexName, undefined, undefined).url.apply(u => u) : Pulumi.output(undefined);
1897
2066
  let store$1 = hostUiBundle.uploadBucket;
1898
- let uploadEndpointOutput = store$1 !== undefined ? Upload_Presign_S3$ReventlessAws.make(store$1.bucketName, undefined, undefined, undefined).url.apply(u => u) : Pulumi.output(undefined);
2067
+ let uploadEndpointOutput = store$1 !== undefined ? Upload_Presign_S3$ReventlessAws.make(store$1.bucketName, undefined, undefined, undefined, undefined).url.apply(u => u) : Pulumi.output(undefined);
2068
+ let storeUploadEndpointsOutput = Pulumi.all(declaredStoreServices.map(param => {
2069
+ let qualified = param[0];
2070
+ return param[3].apply(u => [
2071
+ qualified,
2072
+ u
2073
+ ]);
2074
+ }));
1899
2075
  let configJsonContent = Pulumi.all([
1900
2076
  Pulumi.all([
1901
2077
  resolvedDomainApiEndpoint,
@@ -1905,20 +2081,25 @@ function Make($star) {
1905
2081
  ]),
1906
2082
  domainEventsEndpointOutput,
1907
2083
  geocoderEndpointOutput,
1908
- uploadEndpointOutput
2084
+ Pulumi.all([
2085
+ uploadEndpointOutput,
2086
+ storeUploadEndpointsOutput
2087
+ ])
1909
2088
  ]).apply(param => {
1910
- let uploadEpOpt = param[3];
2089
+ let match = param[3];
2090
+ let storeUploadEps = match[1];
2091
+ let uploadEpOpt = match[0];
1911
2092
  let geocoderEpOpt = param[2];
1912
2093
  let eventsEpOpt = param[1];
1913
- let match = param[0];
2094
+ let match$1 = param[0];
1914
2095
  let fields = [
1915
2096
  [
1916
2097
  "apiEndpoint",
1917
- match[0]
2098
+ match$1[0]
1918
2099
  ],
1919
2100
  [
1920
2101
  "platformApiEndpoint",
1921
- match[1]
2102
+ match$1[1]
1922
2103
  ],
1923
2104
  [
1924
2105
  "region",
@@ -1930,11 +2111,11 @@ function Make($star) {
1930
2111
  ],
1931
2112
  [
1932
2113
  "cognitoUserPoolId",
1933
- match[2]
2114
+ match$1[2]
1934
2115
  ],
1935
2116
  [
1936
2117
  "cognitoClientId",
1937
- match[3]
2118
+ match$1[3]
1938
2119
  ],
1939
2120
  [
1940
2121
  "liveUpdates",
@@ -1963,7 +2144,14 @@ function Make($star) {
1963
2144
  "uploadEndpoint",
1964
2145
  uploadEpOpt
1965
2146
  ]]) : withGeocoder;
1966
- return JSON.stringify(Object.fromEntries(withUpload));
2147
+ let withStoreUploads = storeUploadEps.length !== 0 ? withUpload.concat([[
2148
+ "uploadEndpoints",
2149
+ Object.fromEntries(storeUploadEps.map(param => [
2150
+ param[0],
2151
+ param[1]
2152
+ ]))
2153
+ ]]) : withUpload;
2154
+ return JSON.stringify(Object.fromEntries(withStoreUploads));
1967
2155
  });
1968
2156
  new (Aws.s3.BucketObject)("host-ui-config-json", {
1969
2157
  bucket: bucketName,