@reventlessdev/reventless-aws 3.0.0-alpha.240 → 3.0.0-alpha.241
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 +7 -0
- package/package.json +4 -4
- package/src/Platform.res +119 -0
- package/src/Platform.res.mjs +142 -16
- package/src/plugin/stack/Plugin_Stack.res +190 -0
- package/src/plugin/stack/Plugin_Stack.res.mjs +116 -0
- package/src/util/Util_StoreLayout.res +32 -0
- package/src/util/Util_StoreLayout.res.mjs +13 -0
- package/tests/Util_StoreLayoutTest.res +40 -0
- package/tests/Util_StoreLayoutTest.res.mjs +18 -0
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.241 (2026-07-29)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **platform:** serve and publish declared object stores without a host UI ([9cc4700](https://github.com/ReventlessDev/reventless-core/commit/9cc4700dc96f05e3d35fbc912a4c7be7a5531337))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# 3.0.0-alpha.240 (2026-07-28)
|
|
7
14
|
|
|
8
15
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.241",
|
|
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-jest": "1.0.0-alpha.9",
|
|
16
15
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.31",
|
|
17
16
|
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.61",
|
|
18
17
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
18
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
19
19
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
20
20
|
"@reventlessdev/reventless-core": "3.0.0-alpha.190",
|
|
21
21
|
"@reventlessdev/reventless-infra": "3.0.0-alpha.108",
|
|
22
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
22
23
|
"@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"
|
|
24
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.83"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"rescript": "12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -44,6 +44,40 @@ let splitApiOutputsRef: ref<option<splitApiOutputs>> = ref(None)
|
|
|
44
44
|
Call after `makePlatform` — returns `None` in unified mode. */
|
|
45
45
|
let getSplitApiOutputs = () => splitApiOutputsRef.contents
|
|
46
46
|
|
|
47
|
+
// Object stores provisioned from field declarations — populated by
|
|
48
|
+
// deployPlatform. Access via getObjectStoreEndpoints() after it has been called.
|
|
49
|
+
//
|
|
50
|
+
// A platform whose UI ships from its own stack needs each store's presign
|
|
51
|
+
// endpoint, and `deployPlatform` is the only place that knows it. Without this
|
|
52
|
+
// the store is provisioned and unnameable: the stores are built unconditionally,
|
|
53
|
+
// but every consumer of them used to sit inside `switch hostUiBundle`. Same
|
|
54
|
+
// reason `getSplitApiOutputs` exists — a root cannot re-export what it cannot
|
|
55
|
+
// reach.
|
|
56
|
+
type objectStoreEndpoint = {
|
|
57
|
+
// The store's qualified `{plugin}.{store}` name. The same string
|
|
58
|
+
// `pluginStructure.requiredStores` carries, so a UI's binding key and the
|
|
59
|
+
// declaration's identity are one value rather than two that can drift.
|
|
60
|
+
store: string,
|
|
61
|
+
// The path this store's keys are rooted at, and therefore the path it must be
|
|
62
|
+
// served under for a minted `/{key}` ref to resolve.
|
|
63
|
+
keyPrefix: string,
|
|
64
|
+
bucketName: string,
|
|
65
|
+
uploadUrl: Pulumi.Output.t<string>,
|
|
66
|
+
// Public base URL the store is served from, when the platform serves it
|
|
67
|
+
// itself. `None` when a host-UI bundle is deployed — there the shell's own
|
|
68
|
+
// origin serves the store same-origin and a relative `/{prefix}/…` resolves
|
|
69
|
+
// without a base.
|
|
70
|
+
baseUrl: option<Pulumi.Output.t<string>>,
|
|
71
|
+
}
|
|
72
|
+
let objectStoreEndpointsRef: ref<array<objectStoreEndpoint>> = ref([])
|
|
73
|
+
|
|
74
|
+
/** Returns the object stores this platform provisioned from field declarations.
|
|
75
|
+
|
|
76
|
+
Empty when nothing is declared — a legitimate answer rather than a
|
|
77
|
+
call-order mistake, so this returns `[]` instead of throwing the way
|
|
78
|
+
`getApiConfig` does. */
|
|
79
|
+
let getObjectStoreEndpoints = () => objectStoreEndpointsRef.contents
|
|
80
|
+
|
|
47
81
|
module MakeWithConfig = (
|
|
48
82
|
Config: {
|
|
49
83
|
let splitApi: bool
|
|
@@ -1570,6 +1604,91 @@ module MakeWithConfig = (
|
|
|
1570
1604
|
})
|
|
1571
1605
|
)
|
|
1572
1606
|
|
|
1607
|
+
// ── Serving the declared stores ───────────────────────────────────────
|
|
1608
|
+
//
|
|
1609
|
+
// A store's bucket blocks public policy and takes its read grant only from
|
|
1610
|
+
// a distribution's BucketPolicy, so "provisioned" and "readable" are two
|
|
1611
|
+
// different things. When a host UI bundle is deployed, its distribution
|
|
1612
|
+
// fronts the stores same-origin (below) and a relative `/{prefix}/…`
|
|
1613
|
+
// resolves with no base URL. When it is not, the platform fronts them
|
|
1614
|
+
// itself — otherwise every declared store is write-only.
|
|
1615
|
+
//
|
|
1616
|
+
// Never both: two distributions fronting one bucket would each write the
|
|
1617
|
+
// bucket's single allowed policy and silently unpick the other's grant.
|
|
1618
|
+
// `Util_StoreLayout.servingFor` is where that exclusion is decided, so it is
|
|
1619
|
+
// one testable choice rather than a condition spelled out here.
|
|
1620
|
+
let storeServingBaseUrl: option<Pulumi.Output.t<string>> = switch Util_StoreLayout.servingFor(
|
|
1621
|
+
~hasHostUiBundle=hostUiBundle->Option.isSome,
|
|
1622
|
+
~declaredBucketCount=declaredServedBuckets->Array.length,
|
|
1623
|
+
) {
|
|
1624
|
+
| NoStores | HostShell => None
|
|
1625
|
+
| PlatformOwned =>
|
|
1626
|
+
Some(
|
|
1627
|
+
Plugin_Stack.makeServedBucketDistribution(
|
|
1628
|
+
~name="object-stores",
|
|
1629
|
+
~servedBuckets=declaredServedBuckets,
|
|
1630
|
+
),
|
|
1631
|
+
)
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
let declaredStoreEndpoints = declaredStoreServices->Array.map(((
|
|
1635
|
+
store,
|
|
1636
|
+
keyPrefix,
|
|
1637
|
+
bucketName,
|
|
1638
|
+
uploadUrl,
|
|
1639
|
+
)) => {store, keyPrefix, bucketName, uploadUrl, baseUrl: storeServingBaseUrl})
|
|
1640
|
+
objectStoreEndpointsRef := declaredStoreEndpoints
|
|
1641
|
+
|
|
1642
|
+
// Exported unconditionally — the provisioning above already is. Their only
|
|
1643
|
+
// consumers used to sit inside `switch hostUiBundle`, which left a platform
|
|
1644
|
+
// whose UI ships from another stack holding stores it could not name.
|
|
1645
|
+
//
|
|
1646
|
+
// Omitted entirely when nothing is declared, so a deployment with no
|
|
1647
|
+
// declared store keeps a byte-identical output set.
|
|
1648
|
+
if declaredStoreEndpoints->Array.length > 0 {
|
|
1649
|
+
Pulumi.Pulumi.export(
|
|
1650
|
+
"uploadEndpoints",
|
|
1651
|
+
declaredStoreEndpoints
|
|
1652
|
+
->Array.map(e => e.uploadUrl->Pulumi.Output.apply(u => (e.store, JSON.Encode.string(u))))
|
|
1653
|
+
->Pulumi.Output.all
|
|
1654
|
+
->Pulumi.Output.apply(pairs => pairs->Dict.fromArray->JSON.Encode.object),
|
|
1655
|
+
)
|
|
1656
|
+
Pulumi.Pulumi.export(
|
|
1657
|
+
"objectStores",
|
|
1658
|
+
declaredStoreEndpoints
|
|
1659
|
+
->Array.map(e =>
|
|
1660
|
+
switch e.baseUrl {
|
|
1661
|
+
| Some(u) => u->Pulumi.Output.apply(b => (e, Some(b)))
|
|
1662
|
+
| None => Pulumi.Output.make((e, None))
|
|
1663
|
+
}
|
|
1664
|
+
)
|
|
1665
|
+
->Pulumi.Output.all
|
|
1666
|
+
->Pulumi.Output.apply(pairs =>
|
|
1667
|
+
pairs
|
|
1668
|
+
->Array.map(((e, baseUrl)) => (
|
|
1669
|
+
e.store,
|
|
1670
|
+
[
|
|
1671
|
+
("bucketName", JSON.Encode.string(e.bucketName)),
|
|
1672
|
+
("keyPrefix", JSON.Encode.string(e.keyPrefix)),
|
|
1673
|
+
]
|
|
1674
|
+
// Absent when the host shell serves the store: there the object is
|
|
1675
|
+
// addressable same-origin and a base URL would be a second, and
|
|
1676
|
+
// wrong, way to reach it.
|
|
1677
|
+
->Array.concat(
|
|
1678
|
+
switch baseUrl {
|
|
1679
|
+
| Some(b) => [("baseUrl", JSON.Encode.string(b))]
|
|
1680
|
+
| None => []
|
|
1681
|
+
},
|
|
1682
|
+
)
|
|
1683
|
+
->Dict.fromArray
|
|
1684
|
+
->JSON.Encode.object,
|
|
1685
|
+
))
|
|
1686
|
+
->Dict.fromArray
|
|
1687
|
+
->JSON.Encode.object
|
|
1688
|
+
),
|
|
1689
|
+
)
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1573
1692
|
// Host UI shell deployment — opt-in via ~hostUiBundle. The shell SPA is
|
|
1574
1693
|
// hosted on its own CloudFront distribution; `config.json` is generated
|
|
1575
1694
|
// at deploy time with the resolved API endpoints, region, and Cognito
|
package/src/Platform.res.mjs
CHANGED
|
@@ -116,6 +116,14 @@ function getSplitApiOutputs() {
|
|
|
116
116
|
return splitApiOutputsRef.contents;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
let objectStoreEndpointsRef = {
|
|
120
|
+
contents: []
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
function getObjectStoreEndpoints() {
|
|
124
|
+
return objectStoreEndpointsRef.contents;
|
|
125
|
+
}
|
|
126
|
+
|
|
119
127
|
function MakeWithConfig(Config) {
|
|
120
128
|
Stdlib_Option.forEach(Config.commandHandlerConfig.aggregates, param => {
|
|
121
129
|
Stdlib_Option.forEach(param.sync, AggregateRuntime_Builder_Single$ReventlessAws.setConfig);
|
|
@@ -941,18 +949,76 @@ function MakeWithConfig(Config) {
|
|
|
941
949
|
bucketArn: b.bucketArn,
|
|
942
950
|
bucketRegionalDomainName: b.bucketRegionalDomainName
|
|
943
951
|
})));
|
|
952
|
+
let match$1 = Util_StoreLayout$ReventlessAws.servingFor(Stdlib_Option.isSome(hostUiBundle), declaredServedBuckets.length);
|
|
953
|
+
let storeServingBaseUrl;
|
|
954
|
+
switch (match$1) {
|
|
955
|
+
case "NoStores" :
|
|
956
|
+
case "HostShell" :
|
|
957
|
+
storeServingBaseUrl = undefined;
|
|
958
|
+
break;
|
|
959
|
+
case "PlatformOwned" :
|
|
960
|
+
storeServingBaseUrl = Plugin_Stack$ReventlessAws.makeServedBucketDistribution("object-stores", declaredServedBuckets, undefined);
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
let declaredStoreEndpoints = declaredStoreServices.map(param => ({
|
|
964
|
+
store: param[0],
|
|
965
|
+
keyPrefix: param[1],
|
|
966
|
+
bucketName: param[2],
|
|
967
|
+
uploadUrl: param[3],
|
|
968
|
+
baseUrl: storeServingBaseUrl
|
|
969
|
+
}));
|
|
970
|
+
objectStoreEndpointsRef.contents = declaredStoreEndpoints;
|
|
971
|
+
if (declaredStoreEndpoints.length !== 0) {
|
|
972
|
+
Pulumi$Pulumi.$$export("uploadEndpoints", Pulumi.all(declaredStoreEndpoints.map(e => e.uploadUrl.apply(u => [
|
|
973
|
+
e.store,
|
|
974
|
+
u
|
|
975
|
+
]))).apply(pairs => Object.fromEntries(pairs)));
|
|
976
|
+
Pulumi$Pulumi.$$export("objectStores", Pulumi.all(declaredStoreEndpoints.map(e => {
|
|
977
|
+
let u = e.baseUrl;
|
|
978
|
+
if (u !== undefined) {
|
|
979
|
+
return u.apply(b => [
|
|
980
|
+
e,
|
|
981
|
+
b
|
|
982
|
+
]);
|
|
983
|
+
} else {
|
|
984
|
+
return Pulumi.output([
|
|
985
|
+
e,
|
|
986
|
+
undefined
|
|
987
|
+
]);
|
|
988
|
+
}
|
|
989
|
+
})).apply(pairs => Object.fromEntries(pairs.map(param => {
|
|
990
|
+
let baseUrl = param[1];
|
|
991
|
+
let e = param[0];
|
|
992
|
+
return [
|
|
993
|
+
e.store,
|
|
994
|
+
Object.fromEntries([
|
|
995
|
+
[
|
|
996
|
+
"bucketName",
|
|
997
|
+
e.bucketName
|
|
998
|
+
],
|
|
999
|
+
[
|
|
1000
|
+
"keyPrefix",
|
|
1001
|
+
e.keyPrefix
|
|
1002
|
+
]
|
|
1003
|
+
].concat(baseUrl !== undefined ? [[
|
|
1004
|
+
"baseUrl",
|
|
1005
|
+
baseUrl
|
|
1006
|
+
]] : []))
|
|
1007
|
+
];
|
|
1008
|
+
}))));
|
|
1009
|
+
}
|
|
944
1010
|
if (hostUiBundle !== undefined) {
|
|
945
|
-
let match$
|
|
946
|
-
let match$
|
|
1011
|
+
let match$2 = Util_LocalConfig$ReventlessAws.get("hostUiBaseDomain");
|
|
1012
|
+
let match$3 = Util_LocalConfig$ReventlessAws.get("hostUiHostedZoneId");
|
|
947
1013
|
let customDomain;
|
|
948
|
-
if (match$
|
|
1014
|
+
if (match$2 !== undefined && match$3 !== undefined) {
|
|
949
1015
|
let stack = Pulumi.getStack();
|
|
950
1016
|
let baseName = Stdlib_Option.getOr(Util_LocalConfig$ReventlessAws.get("hostUiBaseName"), Pulumi.getProject());
|
|
951
1017
|
let prodStacks = Stdlib_Option.getOr(Stdlib_Option.map(Util_LocalConfig$ReventlessAws.get("hostUiProdStacks"), Util_HostUiDomain$ReventlessAws.parseProdStacks), Util_HostUiDomain$ReventlessAws.defaultProdStacks);
|
|
952
|
-
let fqdn = Util_HostUiDomain$ReventlessAws.deriveFqdn(baseName, stack, match$
|
|
1018
|
+
let fqdn = Util_HostUiDomain$ReventlessAws.deriveFqdn(baseName, stack, match$2, prodStacks);
|
|
953
1019
|
customDomain = {
|
|
954
1020
|
fqdn: fqdn,
|
|
955
|
-
hostedZoneId: match$
|
|
1021
|
+
hostedZoneId: match$3
|
|
956
1022
|
};
|
|
957
1023
|
} else {
|
|
958
1024
|
customDomain = undefined;
|
|
@@ -967,11 +1033,11 @@ function MakeWithConfig(Config) {
|
|
|
967
1033
|
bucketRegionalDomainName: store.bucketRegionalDomainName
|
|
968
1034
|
}] : []
|
|
969
1035
|
).concat(declaredServedBuckets);
|
|
970
|
-
let match$
|
|
1036
|
+
let match$4 = 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, [
|
|
971
1037
|
"config.json",
|
|
972
1038
|
"ui-hints.json"
|
|
973
1039
|
], customDomain, servedBuckets);
|
|
974
|
-
let bucketName = match$
|
|
1040
|
+
let bucketName = match$4.bucketName;
|
|
975
1041
|
let regionStr = Stdlib_Option.getOr(new Pulumi.Config("aws").get("region"), "unknown");
|
|
976
1042
|
let cognitoPool = Platform_Stack$ReventlessAws.resolveCognitoUserPool();
|
|
977
1043
|
let domainEventsEndpointOutput = domainEventsApiOpt !== undefined ? AppSync_EventsApi$ReventlessAws.httpEndpoint(domainEventsApiOpt).apply(ep => ep + "/event") : Pulumi.output(undefined);
|
|
@@ -1083,7 +1149,7 @@ function MakeWithConfig(Config) {
|
|
|
1083
1149
|
contentType: "application/json"
|
|
1084
1150
|
});
|
|
1085
1151
|
}
|
|
1086
|
-
Pulumi$Pulumi.$$export("hostShellUrl", match$
|
|
1152
|
+
Pulumi$Pulumi.$$export("hostShellUrl", match$4.distributionUrl);
|
|
1087
1153
|
}
|
|
1088
1154
|
return Pulumi$Pulumi.getOutputs();
|
|
1089
1155
|
};
|
|
@@ -2027,18 +2093,76 @@ function Make($star) {
|
|
|
2027
2093
|
bucketArn: b.bucketArn,
|
|
2028
2094
|
bucketRegionalDomainName: b.bucketRegionalDomainName
|
|
2029
2095
|
})));
|
|
2096
|
+
let match$1 = Util_StoreLayout$ReventlessAws.servingFor(Stdlib_Option.isSome(hostUiBundle), declaredServedBuckets.length);
|
|
2097
|
+
let storeServingBaseUrl;
|
|
2098
|
+
switch (match$1) {
|
|
2099
|
+
case "NoStores" :
|
|
2100
|
+
case "HostShell" :
|
|
2101
|
+
storeServingBaseUrl = undefined;
|
|
2102
|
+
break;
|
|
2103
|
+
case "PlatformOwned" :
|
|
2104
|
+
storeServingBaseUrl = Plugin_Stack$ReventlessAws.makeServedBucketDistribution("object-stores", declaredServedBuckets, undefined);
|
|
2105
|
+
break;
|
|
2106
|
+
}
|
|
2107
|
+
let declaredStoreEndpoints = declaredStoreServices.map(param => ({
|
|
2108
|
+
store: param[0],
|
|
2109
|
+
keyPrefix: param[1],
|
|
2110
|
+
bucketName: param[2],
|
|
2111
|
+
uploadUrl: param[3],
|
|
2112
|
+
baseUrl: storeServingBaseUrl
|
|
2113
|
+
}));
|
|
2114
|
+
objectStoreEndpointsRef.contents = declaredStoreEndpoints;
|
|
2115
|
+
if (declaredStoreEndpoints.length !== 0) {
|
|
2116
|
+
Pulumi$Pulumi.$$export("uploadEndpoints", Pulumi.all(declaredStoreEndpoints.map(e => e.uploadUrl.apply(u => [
|
|
2117
|
+
e.store,
|
|
2118
|
+
u
|
|
2119
|
+
]))).apply(pairs => Object.fromEntries(pairs)));
|
|
2120
|
+
Pulumi$Pulumi.$$export("objectStores", Pulumi.all(declaredStoreEndpoints.map(e => {
|
|
2121
|
+
let u = e.baseUrl;
|
|
2122
|
+
if (u !== undefined) {
|
|
2123
|
+
return u.apply(b => [
|
|
2124
|
+
e,
|
|
2125
|
+
b
|
|
2126
|
+
]);
|
|
2127
|
+
} else {
|
|
2128
|
+
return Pulumi.output([
|
|
2129
|
+
e,
|
|
2130
|
+
undefined
|
|
2131
|
+
]);
|
|
2132
|
+
}
|
|
2133
|
+
})).apply(pairs => Object.fromEntries(pairs.map(param => {
|
|
2134
|
+
let baseUrl = param[1];
|
|
2135
|
+
let e = param[0];
|
|
2136
|
+
return [
|
|
2137
|
+
e.store,
|
|
2138
|
+
Object.fromEntries([
|
|
2139
|
+
[
|
|
2140
|
+
"bucketName",
|
|
2141
|
+
e.bucketName
|
|
2142
|
+
],
|
|
2143
|
+
[
|
|
2144
|
+
"keyPrefix",
|
|
2145
|
+
e.keyPrefix
|
|
2146
|
+
]
|
|
2147
|
+
].concat(baseUrl !== undefined ? [[
|
|
2148
|
+
"baseUrl",
|
|
2149
|
+
baseUrl
|
|
2150
|
+
]] : []))
|
|
2151
|
+
];
|
|
2152
|
+
}))));
|
|
2153
|
+
}
|
|
2030
2154
|
if (hostUiBundle !== undefined) {
|
|
2031
|
-
let match$
|
|
2032
|
-
let match$
|
|
2155
|
+
let match$2 = Util_LocalConfig$ReventlessAws.get("hostUiBaseDomain");
|
|
2156
|
+
let match$3 = Util_LocalConfig$ReventlessAws.get("hostUiHostedZoneId");
|
|
2033
2157
|
let customDomain;
|
|
2034
|
-
if (match$
|
|
2158
|
+
if (match$2 !== undefined && match$3 !== undefined) {
|
|
2035
2159
|
let stack = Pulumi.getStack();
|
|
2036
2160
|
let baseName = Stdlib_Option.getOr(Util_LocalConfig$ReventlessAws.get("hostUiBaseName"), Pulumi.getProject());
|
|
2037
2161
|
let prodStacks = Stdlib_Option.getOr(Stdlib_Option.map(Util_LocalConfig$ReventlessAws.get("hostUiProdStacks"), Util_HostUiDomain$ReventlessAws.parseProdStacks), Util_HostUiDomain$ReventlessAws.defaultProdStacks);
|
|
2038
|
-
let fqdn = Util_HostUiDomain$ReventlessAws.deriveFqdn(baseName, stack, match$
|
|
2162
|
+
let fqdn = Util_HostUiDomain$ReventlessAws.deriveFqdn(baseName, stack, match$2, prodStacks);
|
|
2039
2163
|
customDomain = {
|
|
2040
2164
|
fqdn: fqdn,
|
|
2041
|
-
hostedZoneId: match$
|
|
2165
|
+
hostedZoneId: match$3
|
|
2042
2166
|
};
|
|
2043
2167
|
} else {
|
|
2044
2168
|
customDomain = undefined;
|
|
@@ -2053,11 +2177,11 @@ function Make($star) {
|
|
|
2053
2177
|
bucketRegionalDomainName: store.bucketRegionalDomainName
|
|
2054
2178
|
}] : []
|
|
2055
2179
|
).concat(declaredServedBuckets);
|
|
2056
|
-
let match$
|
|
2180
|
+
let match$4 = 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, [
|
|
2057
2181
|
"config.json",
|
|
2058
2182
|
"ui-hints.json"
|
|
2059
2183
|
], customDomain, servedBuckets);
|
|
2060
|
-
let bucketName = match$
|
|
2184
|
+
let bucketName = match$4.bucketName;
|
|
2061
2185
|
let regionStr = Stdlib_Option.getOr(new Pulumi.Config("aws").get("region"), "unknown");
|
|
2062
2186
|
let cognitoPool = Platform_Stack$ReventlessAws.resolveCognitoUserPool();
|
|
2063
2187
|
let domainEventsEndpointOutput = domainEventsApiOpt !== undefined ? AppSync_EventsApi$ReventlessAws.httpEndpoint(domainEventsApiOpt).apply(ep => ep + "/event") : Pulumi.output(undefined);
|
|
@@ -2169,7 +2293,7 @@ function Make($star) {
|
|
|
2169
2293
|
contentType: "application/json"
|
|
2170
2294
|
});
|
|
2171
2295
|
}
|
|
2172
|
-
Pulumi$Pulumi.$$export("hostShellUrl", match$
|
|
2296
|
+
Pulumi$Pulumi.$$export("hostShellUrl", match$4.distributionUrl);
|
|
2173
2297
|
}
|
|
2174
2298
|
return Pulumi$Pulumi.getOutputs();
|
|
2175
2299
|
};
|
|
@@ -2321,6 +2445,8 @@ export {
|
|
|
2321
2445
|
getApiConfig,
|
|
2322
2446
|
splitApiOutputsRef,
|
|
2323
2447
|
getSplitApiOutputs,
|
|
2448
|
+
objectStoreEndpointsRef,
|
|
2449
|
+
getObjectStoreEndpoints,
|
|
2324
2450
|
MakeWithConfig,
|
|
2325
2451
|
Make,
|
|
2326
2452
|
}
|
|
@@ -538,3 +538,193 @@ let makeUiBundleDistribution = (
|
|
|
538
538
|
bucketName: bucket.bucket,
|
|
539
539
|
}
|
|
540
540
|
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
Front a set of served buckets with a CloudFront distribution of their own, with
|
|
544
|
+
no SPA bundle behind it.
|
|
545
|
+
|
|
546
|
+
`makeUiBundleDistribution` can already serve buckets, but only as a side car to
|
|
547
|
+
a UI bundle. A platform that deploys no shell — because its UI ships from its
|
|
548
|
+
own stack — still provisions stores and then has nothing to serve them from:
|
|
549
|
+
the bucket is created with an all-true public-access block and takes its read
|
|
550
|
+
grant solely from a distribution's `BucketPolicy`, so with no distribution the
|
|
551
|
+
objects are unreachable by anything.
|
|
552
|
+
|
|
553
|
+
Serving lives with the stack that owns the buckets, and that is not a style
|
|
554
|
+
preference. **S3 permits exactly one bucket policy per bucket.** If the read
|
|
555
|
+
grant were written by whichever stack happened to build a distribution, two
|
|
556
|
+
distributions fronting one store would silently overwrite each other's grant —
|
|
557
|
+
deploying green and 404ing the loser's objects. Keeping the policy next to the
|
|
558
|
+
bucket makes a second writer impossible rather than merely discouraged.
|
|
559
|
+
|
|
560
|
+
Returns the public base URL. A consumer needs that and the store's prefix to
|
|
561
|
+
address an object; it needs no bucket identity at all, which is both a smaller
|
|
562
|
+
contract and one that cannot collide.
|
|
563
|
+
*/
|
|
564
|
+
let makeServedBucketDistribution = (
|
|
565
|
+
~name: string,
|
|
566
|
+
~servedBuckets: array<ReventlessInfra.Platform.servedBucket>,
|
|
567
|
+
~customDomain: option<customDomain>=?,
|
|
568
|
+
): Pulumi.Output.t<string> => {
|
|
569
|
+
let oac = PulumiAws.CloudFront.OriginAccessControl.make(
|
|
570
|
+
~name=name ++ "-oac",
|
|
571
|
+
~args={
|
|
572
|
+
originAccessControlOriginType: Pulumi.Input.make("s3"),
|
|
573
|
+
signingBehavior: Pulumi.Input.make("always"),
|
|
574
|
+
signingProtocol: Pulumi.Input.make("sigv4"),
|
|
575
|
+
},
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
let originIdFor = (id: string): string => "served-" ++ id
|
|
579
|
+
|
|
580
|
+
// CloudFront requires a default behavior, and with no bundle origin there is
|
|
581
|
+
// nothing neutral to point it at. The first served bucket takes it; every
|
|
582
|
+
// real path is matched by its own `{prefix}/*` behavior first, so the default
|
|
583
|
+
// is only reached by a request for a path no store claims — which 404s at S3,
|
|
584
|
+
// the correct answer.
|
|
585
|
+
let defaultOriginId = switch servedBuckets->Array.get(0) {
|
|
586
|
+
| Some(sb) => originIdFor(sb.id)
|
|
587
|
+
| None =>
|
|
588
|
+
JsError.throwWithMessage(
|
|
589
|
+
"Plugin_Stack.makeServedBucketDistribution: no served buckets — call only when at least one store is declared",
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
let viewerCertificate: PulumiAws.CloudFront.Distribution.viewerCertificate = switch customDomain {
|
|
594
|
+
| None => {
|
|
595
|
+
cloudfrontDefaultCertificate: Pulumi.Input.make(true),
|
|
596
|
+
}
|
|
597
|
+
| Some({fqdn, hostedZoneId: _}) =>
|
|
598
|
+
let usEast1 = _getUsEast1Provider()
|
|
599
|
+
let cert = PulumiAws.Acm.Certificate.make(
|
|
600
|
+
~name=name ++ "-cert",
|
|
601
|
+
~args={
|
|
602
|
+
domainName: Pulumi.Input.make(fqdn),
|
|
603
|
+
validationMethod: Pulumi.Input.make("DNS"),
|
|
604
|
+
},
|
|
605
|
+
~opts={provider: usEast1},
|
|
606
|
+
)
|
|
607
|
+
{
|
|
608
|
+
acmCertificateArn: cert.arn->Pulumi.Output.asInput,
|
|
609
|
+
sslSupportMethod: Pulumi.Input.make("sni-only"),
|
|
610
|
+
minimumProtocolVersion: Pulumi.Input.make("TLSv1.2_2021"),
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
let distribution = PulumiAws.CloudFront.Distribution.make(
|
|
615
|
+
~name=name ++ "-cdn",
|
|
616
|
+
~args={
|
|
617
|
+
enabled: Pulumi.Input.make(true),
|
|
618
|
+
aliases: ?switch customDomain {
|
|
619
|
+
| None => None
|
|
620
|
+
| Some({fqdn}) => Some(Pulumi.Input.make([fqdn]))
|
|
621
|
+
},
|
|
622
|
+
origins: oac.id->Pulumi.Output.apply(oacId =>
|
|
623
|
+
servedBuckets->Array.map(sb => {
|
|
624
|
+
PulumiAws.CloudFront.Distribution.domainName: sb.bucketRegionalDomainName,
|
|
625
|
+
originId: Pulumi.Input.make(originIdFor(sb.id)),
|
|
626
|
+
originAccessControlId: Pulumi.Input.make(oacId),
|
|
627
|
+
})
|
|
628
|
+
)
|
|
629
|
+
->Pulumi.Output.asInput,
|
|
630
|
+
defaultCacheBehavior: Pulumi.Input.make(
|
|
631
|
+
(
|
|
632
|
+
{
|
|
633
|
+
targetOriginId: Pulumi.Input.make(defaultOriginId),
|
|
634
|
+
viewerProtocolPolicy: Pulumi.Input.make("redirect-to-https"),
|
|
635
|
+
allowedMethods: Pulumi.Input.make(["GET", "HEAD"]),
|
|
636
|
+
cachedMethods: Pulumi.Input.make(["GET", "HEAD"]),
|
|
637
|
+
cachePolicyId: Pulumi.Input.make(cachingOptimizedPolicyId),
|
|
638
|
+
}: PulumiAws.CloudFront.Distribution.defaultCacheBehavior
|
|
639
|
+
),
|
|
640
|
+
),
|
|
641
|
+
// One behavior per served prefix. Store objects have immutable uuid keys,
|
|
642
|
+
// so the long-TTL CachingOptimized policy is safe.
|
|
643
|
+
orderedCacheBehaviors: Pulumi.Input.make(
|
|
644
|
+
servedBuckets->Array.flatMap(sb =>
|
|
645
|
+
sb.prefixes->Array.map(prefix => {
|
|
646
|
+
PulumiAws.CloudFront.Distribution.pathPattern: Pulumi.Input.make(prefix ++ "/*"),
|
|
647
|
+
targetOriginId: Pulumi.Input.make(originIdFor(sb.id)),
|
|
648
|
+
viewerProtocolPolicy: Pulumi.Input.make("redirect-to-https"),
|
|
649
|
+
allowedMethods: Pulumi.Input.make(["GET", "HEAD"]),
|
|
650
|
+
cachedMethods: Pulumi.Input.make(["GET", "HEAD"]),
|
|
651
|
+
cachePolicyId: Pulumi.Input.make(cachingOptimizedPolicyId),
|
|
652
|
+
})
|
|
653
|
+
),
|
|
654
|
+
),
|
|
655
|
+
restrictions: Pulumi.Input.make({
|
|
656
|
+
PulumiAws.CloudFront.Distribution.geoRestriction: Pulumi.Input.make({
|
|
657
|
+
PulumiAws.CloudFront.Distribution.restrictionType: Pulumi.Input.make("none"),
|
|
658
|
+
}),
|
|
659
|
+
}),
|
|
660
|
+
viewerCertificate: Pulumi.Input.make(viewerCertificate),
|
|
661
|
+
comment: Pulumi.Input.make(name ++ " object store CDN"),
|
|
662
|
+
tags: AWS.Tags.make(
|
|
663
|
+
~name=name ++ "-cdn",
|
|
664
|
+
~kind=ReventlessCore.ComponentType.Platform,
|
|
665
|
+
~role=Hosting,
|
|
666
|
+
~scope=Platform,
|
|
667
|
+
),
|
|
668
|
+
},
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
switch customDomain {
|
|
672
|
+
| None => ()
|
|
673
|
+
| Some({fqdn, hostedZoneId}) =>
|
|
674
|
+
let _ = PulumiAws.Route53.Record.make(
|
|
675
|
+
~name=name ++ "-domain-alias",
|
|
676
|
+
~args={
|
|
677
|
+
zoneId: Pulumi.Input.make(hostedZoneId),
|
|
678
|
+
name: Pulumi.Input.make(fqdn),
|
|
679
|
+
type_: Pulumi.Input.make("A"),
|
|
680
|
+
aliases: [
|
|
681
|
+
(
|
|
682
|
+
{
|
|
683
|
+
name: distribution.domainName->Pulumi.Output.asInput,
|
|
684
|
+
zoneId: Pulumi.Input.make(cloudFrontAliasZoneId),
|
|
685
|
+
evaluateTargetHealth: Pulumi.Input.make(false),
|
|
686
|
+
}: PulumiAws.Route53.Record.alias
|
|
687
|
+
),
|
|
688
|
+
]->Pulumi.Input.make,
|
|
689
|
+
},
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// One policy per bucket — see the module doc above for why this is here and
|
|
694
|
+
// not in the consuming stack.
|
|
695
|
+
servedBuckets->Array.forEach(sb => {
|
|
696
|
+
let _ = PulumiAws.S3.BucketPolicy.make(
|
|
697
|
+
~name=name ++ "-served-" ++ sb.id ++ "-policy",
|
|
698
|
+
~args={
|
|
699
|
+
bucket: sb.bucketId,
|
|
700
|
+
policy: (sb.bucketArn->Pulumi.Output.fromInput, distribution.arn)
|
|
701
|
+
->Pulumi.Output.all2
|
|
702
|
+
->Pulumi.Output.apply(((bucketArn, distributionArn)) =>
|
|
703
|
+
{
|
|
704
|
+
"Version": "2012-10-17",
|
|
705
|
+
"Statement": [
|
|
706
|
+
{
|
|
707
|
+
"Sid": "AllowCloudFrontServicePrincipal",
|
|
708
|
+
"Effect": "Allow",
|
|
709
|
+
"Principal": {"Service": "cloudfront.amazonaws.com"},
|
|
710
|
+
"Action": "s3:GetObject",
|
|
711
|
+
"Resource": bucketArn ++ "/*",
|
|
712
|
+
"Condition": {
|
|
713
|
+
"StringEquals": {"AWS:SourceArn": distributionArn},
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
}
|
|
718
|
+
->JSON.stringifyAny
|
|
719
|
+
->Option.getUnsafe
|
|
720
|
+
)
|
|
721
|
+
->Pulumi.Output.asInput,
|
|
722
|
+
},
|
|
723
|
+
)
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
switch customDomain {
|
|
727
|
+
| Some({fqdn}) => Pulumi.Output.make("https://" ++ fqdn)
|
|
728
|
+
| None => distribution.domainName->Pulumi.Output.apply(d => "https://" ++ d)
|
|
729
|
+
}
|
|
730
|
+
}
|
|
@@ -303,6 +303,121 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
|
|
|
303
303
|
};
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
function makeServedBucketDistribution(name, servedBuckets, customDomain) {
|
|
307
|
+
let oac = new (Aws.cloudfront.OriginAccessControl)(name + "-oac", {
|
|
308
|
+
originAccessControlOriginType: "s3",
|
|
309
|
+
signingBehavior: "always",
|
|
310
|
+
signingProtocol: "sigv4"
|
|
311
|
+
});
|
|
312
|
+
let sb = servedBuckets[0];
|
|
313
|
+
let defaultOriginId = sb !== undefined ? "served-" + sb.id : Stdlib_JsError.throwWithMessage("Plugin_Stack.makeServedBucketDistribution: no served buckets — call only when at least one store is declared");
|
|
314
|
+
let viewerCertificate;
|
|
315
|
+
if (customDomain !== undefined) {
|
|
316
|
+
let usEast1 = _getUsEast1Provider();
|
|
317
|
+
let cert = new (Aws.acm.Certificate)(name + "-cert", {
|
|
318
|
+
domainName: customDomain.fqdn,
|
|
319
|
+
validationMethod: "DNS"
|
|
320
|
+
}, {
|
|
321
|
+
provider: Primitive_option.some(usEast1)
|
|
322
|
+
});
|
|
323
|
+
viewerCertificate = {
|
|
324
|
+
acmCertificateArn: cert.arn,
|
|
325
|
+
sslSupportMethod: "sni-only",
|
|
326
|
+
minimumProtocolVersion: "TLSv1.2_2021"
|
|
327
|
+
};
|
|
328
|
+
} else {
|
|
329
|
+
viewerCertificate = {
|
|
330
|
+
cloudfrontDefaultCertificate: true
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
let distribution = new (Aws.cloudfront.Distribution)(name + "-cdn", {
|
|
334
|
+
enabled: true,
|
|
335
|
+
aliases: customDomain !== undefined ? [customDomain.fqdn] : undefined,
|
|
336
|
+
origins: oac.id.apply(oacId => servedBuckets.map(sb => ({
|
|
337
|
+
domainName: sb.bucketRegionalDomainName,
|
|
338
|
+
originId: "served-" + sb.id,
|
|
339
|
+
originAccessControlId: oacId
|
|
340
|
+
}))),
|
|
341
|
+
defaultCacheBehavior: {
|
|
342
|
+
targetOriginId: defaultOriginId,
|
|
343
|
+
viewerProtocolPolicy: "redirect-to-https",
|
|
344
|
+
allowedMethods: [
|
|
345
|
+
"GET",
|
|
346
|
+
"HEAD"
|
|
347
|
+
],
|
|
348
|
+
cachedMethods: [
|
|
349
|
+
"GET",
|
|
350
|
+
"HEAD"
|
|
351
|
+
],
|
|
352
|
+
cachePolicyId: cachingOptimizedPolicyId
|
|
353
|
+
},
|
|
354
|
+
orderedCacheBehaviors: servedBuckets.flatMap(sb => sb.prefixes.map(prefix => ({
|
|
355
|
+
pathPattern: prefix + "/*",
|
|
356
|
+
targetOriginId: "served-" + sb.id,
|
|
357
|
+
viewerProtocolPolicy: "redirect-to-https",
|
|
358
|
+
allowedMethods: [
|
|
359
|
+
"GET",
|
|
360
|
+
"HEAD"
|
|
361
|
+
],
|
|
362
|
+
cachedMethods: [
|
|
363
|
+
"GET",
|
|
364
|
+
"HEAD"
|
|
365
|
+
],
|
|
366
|
+
cachePolicyId: cachingOptimizedPolicyId
|
|
367
|
+
}))),
|
|
368
|
+
restrictions: {
|
|
369
|
+
geoRestriction: {
|
|
370
|
+
restrictionType: "none"
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
viewerCertificate: viewerCertificate,
|
|
374
|
+
comment: name + " object store CDN",
|
|
375
|
+
tags: AWS_Tags$ReventlessAws.make(name + "-cdn", "Platform", "Hosting", "Platform", undefined, undefined, undefined, undefined)
|
|
376
|
+
});
|
|
377
|
+
if (customDomain !== undefined) {
|
|
378
|
+
new (Aws.route53.Record)(name + "-domain-alias", {
|
|
379
|
+
zoneId: customDomain.hostedZoneId,
|
|
380
|
+
name: customDomain.fqdn,
|
|
381
|
+
type: "A",
|
|
382
|
+
aliases: [{
|
|
383
|
+
name: distribution.domainName,
|
|
384
|
+
zoneId: cloudFrontAliasZoneId,
|
|
385
|
+
evaluateTargetHealth: false
|
|
386
|
+
}]
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
servedBuckets.forEach(sb => {
|
|
390
|
+
new (Aws.s3.BucketPolicy)(name + "-served-" + sb.id + "-policy", {
|
|
391
|
+
bucket: sb.bucketId,
|
|
392
|
+
policy: Pulumi.all([
|
|
393
|
+
sb.bucketArn,
|
|
394
|
+
distribution.arn
|
|
395
|
+
]).apply(param => JSON.stringify({
|
|
396
|
+
Version: "2012-10-17",
|
|
397
|
+
Statement: [{
|
|
398
|
+
Sid: "AllowCloudFrontServicePrincipal",
|
|
399
|
+
Effect: "Allow",
|
|
400
|
+
Principal: {
|
|
401
|
+
Service: "cloudfront.amazonaws.com"
|
|
402
|
+
},
|
|
403
|
+
Action: "s3:GetObject",
|
|
404
|
+
Resource: param[0] + "/*",
|
|
405
|
+
Condition: {
|
|
406
|
+
StringEquals: {
|
|
407
|
+
"AWS:SourceArn": param[1]
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}]
|
|
411
|
+
}))
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
if (customDomain !== undefined) {
|
|
415
|
+
return Pulumi.output("https://" + customDomain.fqdn);
|
|
416
|
+
} else {
|
|
417
|
+
return distribution.domainName.apply(d => "https://" + d);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
306
421
|
export {
|
|
307
422
|
log,
|
|
308
423
|
cachingOptimizedPolicyId,
|
|
@@ -313,5 +428,6 @@ export {
|
|
|
313
428
|
cacheControlFor,
|
|
314
429
|
invalidateDistribution,
|
|
315
430
|
makeUiBundleDistribution,
|
|
431
|
+
makeServedBucketDistribution,
|
|
316
432
|
}
|
|
317
433
|
/* log Not a pure module */
|
|
@@ -34,6 +34,16 @@ type protection =
|
|
|
34
34
|
| Protected
|
|
35
35
|
| Unprotected
|
|
36
36
|
|
|
37
|
+
/** Which distribution fronts the declared stores. */
|
|
38
|
+
type serving =
|
|
39
|
+
/** No store is declared, so nothing is served. */
|
|
40
|
+
| NoStores
|
|
41
|
+
/** The host shell's own distribution serves them same-origin, so a minted
|
|
42
|
+
`/{prefix}/…` ref resolves relative and there is no base URL. */
|
|
43
|
+
| HostShell
|
|
44
|
+
/** The platform fronts them itself, because no host shell is deployed. */
|
|
45
|
+
| PlatformOwned
|
|
46
|
+
|
|
37
47
|
/**
|
|
38
48
|
Stack-name prefixes whose stacks are disposable.
|
|
39
49
|
|
|
@@ -102,3 +112,25 @@ The cost is a slightly redundant prefix inside a dedicated bucket
|
|
|
102
112
|
(`catalog-productImages/productImages/…`). Take the redundancy.
|
|
103
113
|
*/
|
|
104
114
|
let keyPrefixFor = (~store: string): string => store
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
Who serves the declared stores — and the answer is never "both".
|
|
118
|
+
|
|
119
|
+
A store's bucket blocks public policy and takes its read grant solely from a
|
|
120
|
+
distribution's `BucketPolicy`. **S3 permits exactly one bucket policy per
|
|
121
|
+
bucket**, so two distributions fronting one store would each write that single
|
|
122
|
+
policy and silently unpick the other's grant: green deploy, 404s afterwards.
|
|
123
|
+
Making this one function's return a three-way choice is what keeps "both" from
|
|
124
|
+
being expressible.
|
|
125
|
+
|
|
126
|
+
The polarity is the useful part. Provisioning a store and serving it are
|
|
127
|
+
separate; before this, serving happened only as a side car to a host-UI bundle,
|
|
128
|
+
so a platform whose UI shipped from its own stack provisioned stores that
|
|
129
|
+
nothing could read.
|
|
130
|
+
*/
|
|
131
|
+
let servingFor = (~hasHostUiBundle: bool, ~declaredBucketCount: int): serving =>
|
|
132
|
+
switch (hasHostUiBundle, declaredBucketCount) {
|
|
133
|
+
| (_, 0) => NoStores
|
|
134
|
+
| (true, _) => HostShell
|
|
135
|
+
| (false, _) => PlatformOwned
|
|
136
|
+
}
|
|
@@ -32,11 +32,24 @@ function keyPrefixFor(store) {
|
|
|
32
32
|
return store;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function servingFor(hasHostUiBundle, declaredBucketCount) {
|
|
36
|
+
if (declaredBucketCount !== 0) {
|
|
37
|
+
if (hasHostUiBundle) {
|
|
38
|
+
return "HostShell";
|
|
39
|
+
} else {
|
|
40
|
+
return "PlatformOwned";
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
return "NoStores";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
export {
|
|
36
48
|
defaultEphemeralPrefixes,
|
|
37
49
|
layoutFor,
|
|
38
50
|
protectionFor,
|
|
39
51
|
bucketNameFor,
|
|
40
52
|
keyPrefixFor,
|
|
53
|
+
servingFor,
|
|
41
54
|
}
|
|
42
55
|
/* No side effect */
|
|
@@ -40,6 +40,46 @@ describe("Util_StoreLayout.layoutFor", () => {
|
|
|
40
40
|
)
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
+
describe("Util_StoreLayout.servingFor", () => {
|
|
44
|
+
testSync("no declared store means nothing to serve", () =>
|
|
45
|
+
expect(Util_StoreLayout.servingFor(~hasHostUiBundle=false, ~declaredBucketCount=0))->toEqual(
|
|
46
|
+
Util_StoreLayout.NoStores,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
testSync("a host shell serves the stores from its own origin", () =>
|
|
51
|
+
expect(Util_StoreLayout.servingFor(~hasHostUiBundle=true, ~declaredBucketCount=1))->toEqual(
|
|
52
|
+
Util_StoreLayout.HostShell,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// The case that was previously unrepresentable and produced a write-only
|
|
57
|
+
// store: stores are provisioned unconditionally, but serving used to happen
|
|
58
|
+
// only as a side car to a host-UI bundle.
|
|
59
|
+
testSync("no host shell means the platform serves them itself", () =>
|
|
60
|
+
expect(Util_StoreLayout.servingFor(~hasHostUiBundle=false, ~declaredBucketCount=1))->toEqual(
|
|
61
|
+
Util_StoreLayout.PlatformOwned,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
// A bucket carries exactly one policy, so two distributions fronting one
|
|
66
|
+
// store would unpick each other's read grant. The three-way return is what
|
|
67
|
+
// makes "both" unrepresentable — asserted so it stays that way.
|
|
68
|
+
testSync("a host shell wins even with several buckets — never both", () =>
|
|
69
|
+
expect(Util_StoreLayout.servingFor(~hasHostUiBundle=true, ~declaredBucketCount=3))->toEqual(
|
|
70
|
+
Util_StoreLayout.HostShell,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
// Declaring nothing outranks having a shell: with no store there is no
|
|
75
|
+
// bucket, no policy and no origin, whoever is deployed.
|
|
76
|
+
testSync("no store outranks a host shell", () =>
|
|
77
|
+
expect(Util_StoreLayout.servingFor(~hasHostUiBundle=true, ~declaredBucketCount=0))->toEqual(
|
|
78
|
+
Util_StoreLayout.NoStores,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
|
|
43
83
|
describe("Util_StoreLayout.protectionFor", () => {
|
|
44
84
|
// The pairing a single layout-driven switch would have got wrong: alpha
|
|
45
85
|
// shares a bucket and is still protected.
|
|
@@ -27,6 +27,24 @@ globalThis.describe("Util_StoreLayout.layoutFor", () => {
|
|
|
27
27
|
});
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
globalThis.describe("Util_StoreLayout.servingFor", () => {
|
|
31
|
+
globalThis.test("no declared store means nothing to serve", () => {
|
|
32
|
+
globalThis.expect(Util_StoreLayout$ReventlessAws.servingFor(false, 0)).toEqual("NoStores");
|
|
33
|
+
});
|
|
34
|
+
globalThis.test("a host shell serves the stores from its own origin", () => {
|
|
35
|
+
globalThis.expect(Util_StoreLayout$ReventlessAws.servingFor(true, 1)).toEqual("HostShell");
|
|
36
|
+
});
|
|
37
|
+
globalThis.test("no host shell means the platform serves them itself", () => {
|
|
38
|
+
globalThis.expect(Util_StoreLayout$ReventlessAws.servingFor(false, 1)).toEqual("PlatformOwned");
|
|
39
|
+
});
|
|
40
|
+
globalThis.test("a host shell wins even with several buckets — never both", () => {
|
|
41
|
+
globalThis.expect(Util_StoreLayout$ReventlessAws.servingFor(true, 3)).toEqual("HostShell");
|
|
42
|
+
});
|
|
43
|
+
globalThis.test("no store outranks a host shell", () => {
|
|
44
|
+
globalThis.expect(Util_StoreLayout$ReventlessAws.servingFor(true, 0)).toEqual("NoStores");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
30
48
|
globalThis.describe("Util_StoreLayout.protectionFor", () => {
|
|
31
49
|
globalThis.test("alpha shares a bucket and is still protected", () => {
|
|
32
50
|
globalThis.expect(Util_StoreLayout$ReventlessAws.layoutFor("alpha", Util_HostUiDomain$ReventlessAws.defaultProdStacks)).toEqual("SharedBucket");
|