@reventlessdev/reventless-aws 3.0.0-alpha.251 → 3.0.0-alpha.253
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 +31 -0
- package/package.json +7 -7
- package/src/Platform.res +83 -69
- package/src/Platform.res.mjs +85 -104
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res +13 -11
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs +16 -21
- package/src/adapter/Task/TaskBucket_S3.res +11 -0
- package/src/adapter/Task/TaskBucket_S3.res.mjs +3 -0
- package/src/adapter/Upload/Upload_Presign_S3.res +179 -92
- package/src/adapter/Upload/Upload_Presign_S3.res.mjs +129 -39
- package/src/adapter/Upload/Upload_Presign_S3_Ops.res +231 -104
- package/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs +195 -75
- package/src/util/Util_DeadLetterQueue.res +26 -0
- package/src/util/Util_DeadLetterQueue.res.mjs +9 -0
- package/src/util/Util_StoreLayout.res +25 -9
- package/src/util/Util_StoreLayout.res.mjs +2 -2
- package/tests/Upload_ReleaseRuleTest.res +90 -0
- package/tests/Upload_ReleaseRuleTest.res.mjs +110 -0
- package/tests/Util_StoreLayoutTest.res +16 -3
- package/tests/Util_StoreLayoutTest.res.mjs +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,37 @@
|
|
|
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.253 (2026-08-01)
|
|
7
|
+
|
|
8
|
+
* feat(aws,core,spec)!: qualify store prefixes by plugin and refuse name collisions ([da39405](https://github.com/ReventlessDev/reventless-core/commit/da394059d9f8f981bf7adc79e2c1ce2b429e0267))
|
|
9
|
+
* feat(core,aws)!: name task buckets for their plugin and task ([05203cb](https://github.com/ReventlessDev/reventless-core/commit/05203cb3b0b019401a6f32cf78109f397b6799d5))
|
|
10
|
+
|
|
11
|
+
### BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* objects minted under the old bare prefix are orphaned and their
|
|
14
|
+
refs unresolvable. The migration is `seed:reset` for the owning plugin, then
|
|
15
|
+
re-seed. Legacy-prefix grandfathering was considered and deliberately dropped —
|
|
16
|
+
it would have added a permanent prefix SET across the deploy argument, both store
|
|
17
|
+
configs, the presign IAM fan-out, the release scope check and the stack output,
|
|
18
|
+
to spare a disposable stack one wipe.
|
|
19
|
+
* a bucket name change is a replace. TaskBucket_S3 now sets
|
|
20
|
+
forceDestroy on disposable stacks (Util_StoreLayout.protectionFor), so pr-*
|
|
21
|
+
stacks recreate cleanly; protected stacks must have the task bucket drained by
|
|
22
|
+
hand before the first deploy after this, or the replace fails BucketNotEmpty.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# 3.0.0-alpha.252 (2026-08-01)
|
|
27
|
+
|
|
28
|
+
### Bug Fixes
|
|
29
|
+
|
|
30
|
+
* **aws:** let a dead letter be seen ([e64949b](https://github.com/ReventlessDev/reventless-core/commit/e64949bb3f4965cb8e6fc7a2f04971290ebe43bf))
|
|
31
|
+
* **aws:** let the Function URL own CORS on the upload and geocoder services ([19d4b7f](https://github.com/ReventlessDev/reventless-core/commit/19d4b7f26f0925b7c659a1b37555e952bb7c7f46))
|
|
32
|
+
### Features
|
|
33
|
+
|
|
34
|
+
* **upload:** add the release half of the upload contract via the domain API ([448f887](https://github.com/ReventlessDev/reventless-core/commit/448f88714a5875be420f794a08d482fcf4ba8404))
|
|
35
|
+
|
|
36
|
+
|
|
6
37
|
# 3.0.0-alpha.251 (2026-08-01)
|
|
7
38
|
|
|
8
39
|
### Bug Fixes
|
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.253",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"uuid": "^13.0.0",
|
|
14
14
|
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.0",
|
|
15
15
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
16
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
17
16
|
"@reventlessdev/rescript-node": "2.0.0-alpha.0",
|
|
17
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
18
18
|
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.62",
|
|
19
19
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
20
20
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
21
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
22
|
-
"@reventlessdev/reventless-
|
|
23
|
-
"@reventlessdev/reventless-
|
|
24
|
-
"@reventlessdev/reventless-
|
|
25
|
-
"@reventlessdev/reventless-
|
|
21
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.200",
|
|
22
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.64",
|
|
23
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.117",
|
|
24
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.92",
|
|
25
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.30"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"rescript": "12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -67,7 +67,6 @@ type objectStoreEndpoint = {
|
|
|
67
67
|
// only known after the bucket resolves — and a plain string here is exactly
|
|
68
68
|
// how this shipped exporting a bucket name that no ARN matches.
|
|
69
69
|
bucketName: Pulumi.Output.t<string>,
|
|
70
|
-
uploadUrl: Pulumi.Output.t<string>,
|
|
71
70
|
// Public base URL the store is served from, when the platform serves it
|
|
72
71
|
// itself. `None` when a host-UI bundle is deployed — there the shell's own
|
|
73
72
|
// origin serves the store same-origin and a relative `/{prefix}/…` resolves
|
|
@@ -170,9 +169,15 @@ module MakeWithConfig = (
|
|
|
170
169
|
// "Relay node resolution" section of the merged-api plan (never resolved on
|
|
171
170
|
// any deployed AWS platform, zero consumers; the Node interface and global
|
|
172
171
|
// IDs stay).
|
|
172
|
+
// The upload service (route B) lives on the domain base, not the Admin-gated admin
|
|
173
|
+
// base: `Upload_Presign`/`Upload_Release` take the domain API's default
|
|
174
|
+
// `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 —
|
|
176
|
+
// unified mode carries the admin base here instead; it declares no stores in practice,
|
|
177
|
+
// so the upload resolvers below are never created there.)
|
|
173
178
|
let domainBaseFragment = ReventlessCore.GraphQL_Stitcher.encode({
|
|
174
|
-
types:
|
|
175
|
-
mutations:
|
|
179
|
+
types: ReventlessCore.Platform_AdminApi.uploadTypes,
|
|
180
|
+
mutations: ReventlessCore.Platform_AdminApi.uploadMutationFields,
|
|
176
181
|
queries: [" Platform_ping: String"],
|
|
177
182
|
subscriptions: [],
|
|
178
183
|
subscriptionSources: [],
|
|
@@ -1532,6 +1537,30 @@ module MakeWithConfig = (
|
|
|
1532
1537
|
: Array.concat(acc, [(plugin, store)])
|
|
1533
1538
|
)
|
|
1534
1539
|
|
|
1540
|
+
// Refuse two stores that cannot be told apart, BEFORE a single resource is
|
|
1541
|
+
// created. Left to the deploy, the same collision surfaces as a CloudFront
|
|
1542
|
+
// error about a duplicate path pattern — minutes in, naming a path pattern
|
|
1543
|
+
// rather than the two plugins — and only after the intermixed objects and
|
|
1544
|
+
// the over-broad presign grant have already been provisioned.
|
|
1545
|
+
//
|
|
1546
|
+
// Checked on the prefix, so it costs nothing to keep once prefixes are
|
|
1547
|
+
// qualified: a qualified prefix simply stops colliding, while the check
|
|
1548
|
+
// still guards any bare prefix that outlives the change.
|
|
1549
|
+
switch ReventlessCore.StorePrefixCollision.collisionsFor(
|
|
1550
|
+
~stores=declaredStores->Array.map(((plugin, store)) => {
|
|
1551
|
+
ReventlessCore.StorePrefixCollision.qualified: `${plugin}.${store}`,
|
|
1552
|
+
prefix: Util_StoreLayout.keyPrefixFor(~plugin, ~store),
|
|
1553
|
+
}),
|
|
1554
|
+
) {
|
|
1555
|
+
| [] => ()
|
|
1556
|
+
| collisions =>
|
|
1557
|
+
JsError.throwWithMessage(
|
|
1558
|
+
collisions
|
|
1559
|
+
->Array.map(ReventlessCore.StorePrefixCollision.collisionMessage)
|
|
1560
|
+
->Array.join("\n\n"),
|
|
1561
|
+
)
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1535
1564
|
if declaredStores->Array.length > 0 {
|
|
1536
1565
|
// The fail-open in `layoutFor` is only dangerous while it is silent: a
|
|
1537
1566
|
// production stack that is not on the prod list gets the shared layout and
|
|
@@ -1562,7 +1591,7 @@ module MakeWithConfig = (
|
|
|
1562
1591
|
~plugin,
|
|
1563
1592
|
~store,
|
|
1564
1593
|
)
|
|
1565
|
-
let keyPrefix = Util_StoreLayout.keyPrefixFor(~store)
|
|
1594
|
+
let keyPrefix = Util_StoreLayout.keyPrefixFor(~plugin, ~store)
|
|
1566
1595
|
let bucket = switch storeBuckets->Dict.get(bucketName) {
|
|
1567
1596
|
| Some(b) => b
|
|
1568
1597
|
| None =>
|
|
@@ -1585,17 +1614,14 @@ module MakeWithConfig = (
|
|
|
1585
1614
|
b
|
|
1586
1615
|
}
|
|
1587
1616
|
let storeHandle = bucket->Capability_ObjectStore_S3.underPrefix(~keyPrefix)
|
|
1588
|
-
// One presign service per store, scoped to that store's own prefix.
|
|
1589
|
-
let presign = Upload_Presign_S3.make(
|
|
1590
|
-
~name=`UploadPresign-${plugin}-${store}`,
|
|
1591
|
-
~bucketName=storeHandle.bucketName,
|
|
1592
|
-
~servedPrefix=storeHandle.keyPrefix,
|
|
1593
|
-
)
|
|
1594
1617
|
// `bucketName` is the layout's *logical* name and groups the served view
|
|
1595
1618
|
// below; `storeHandle.bucketName` is the physical one Pulumi resolves, and
|
|
1596
1619
|
// is what a consumer needs to build an ARN. Carrying both is the point —
|
|
1597
1620
|
// they are different strings and only one of them exists in S3.
|
|
1598
|
-
|
|
1621
|
+
//
|
|
1622
|
+
// Presigning is no longer per store: one platform-API Upload service (below)
|
|
1623
|
+
// covers every declared store, keyed by the qualified `{plugin}.{store}` name.
|
|
1624
|
+
(`${plugin}.${store}`, keyPrefix, bucketName, storeHandle.bucketName)
|
|
1599
1625
|
})
|
|
1600
1626
|
|
|
1601
1627
|
// Group the served view by bucket: one origin and one bucket policy per
|
|
@@ -1606,7 +1632,7 @@ module MakeWithConfig = (
|
|
|
1606
1632
|
->Option.map(b => {
|
|
1607
1633
|
ReventlessInfra.Platform.id: bucketName,
|
|
1608
1634
|
prefixes: declaredStoreServices
|
|
1609
|
-
->Array.filterMap(((_, prefix, bn, _
|
|
1635
|
+
->Array.filterMap(((_, prefix, bn, _)) => bn == bucketName ? Some(prefix) : None),
|
|
1610
1636
|
bucketId: b.bucketId,
|
|
1611
1637
|
bucketArn: b.bucketArn,
|
|
1612
1638
|
bucketRegionalDomainName: b.bucketRegionalDomainName,
|
|
@@ -1645,30 +1671,58 @@ module MakeWithConfig = (
|
|
|
1645
1671
|
keyPrefix,
|
|
1646
1672
|
_logicalBucketName,
|
|
1647
1673
|
physicalBucketName,
|
|
1648
|
-
uploadUrl,
|
|
1649
1674
|
)) => {
|
|
1650
1675
|
store,
|
|
1651
1676
|
keyPrefix,
|
|
1652
1677
|
bucketName: physicalBucketName->Pulumi.Output.fromInput,
|
|
1653
|
-
uploadUrl,
|
|
1654
1678
|
baseUrl: storeServingBaseUrl,
|
|
1655
1679
|
})
|
|
1656
1680
|
objectStoreEndpointsRef := declaredStoreEndpoints
|
|
1657
1681
|
|
|
1682
|
+
// One domain-API Upload service (route B1) covers every declared store plus the
|
|
1683
|
+
// legacy `hostUiBundle.uploadBucket`, keyed by the qualified `{plugin}.{store}` name
|
|
1684
|
+
// the caller passes as the `store` argument. On the domain API (not the Admin-gated
|
|
1685
|
+
// platform API — see the plan's "Which API" note) so any authenticated user can
|
|
1686
|
+
// mint/release. Created here in the outer scope — not inside the `hostUiBundle`
|
|
1687
|
+
// branch — because a platform whose UI ships from another stack still has declared
|
|
1688
|
+
// stores that need presigning. `uploads` keys the legacy store, matching the prefix
|
|
1689
|
+
// it has always served under.
|
|
1690
|
+
let uploadServiceStores =
|
|
1691
|
+
declaredStoreServices
|
|
1692
|
+
->Array.map(((qualified, keyPrefix, _logical, physicalBucketName)) => {
|
|
1693
|
+
Upload_Presign_S3.qualified: qualified,
|
|
1694
|
+
bucketName: physicalBucketName,
|
|
1695
|
+
servedPrefix: keyPrefix,
|
|
1696
|
+
})
|
|
1697
|
+
->Array.concat(
|
|
1698
|
+
switch hostUiBundle->Option.flatMap(c => c.uploadBucket) {
|
|
1699
|
+
| Some(store) => [
|
|
1700
|
+
{
|
|
1701
|
+
Upload_Presign_S3.qualified: store.keyPrefix,
|
|
1702
|
+
bucketName: store.bucketName,
|
|
1703
|
+
servedPrefix: store.keyPrefix,
|
|
1704
|
+
},
|
|
1705
|
+
]
|
|
1706
|
+
| None => []
|
|
1707
|
+
},
|
|
1708
|
+
)
|
|
1709
|
+
if uploadServiceStores->Array.length > 0 {
|
|
1710
|
+
let _uploadService = Upload_Presign_S3.make(
|
|
1711
|
+
~api=domainApi,
|
|
1712
|
+
~stores=uploadServiceStores,
|
|
1713
|
+
~opts={},
|
|
1714
|
+
)
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1658
1717
|
// Exported unconditionally — the provisioning above already is. Their only
|
|
1659
1718
|
// consumers used to sit inside `switch hostUiBundle`, which left a platform
|
|
1660
1719
|
// whose UI ships from another stack holding stores it could not name.
|
|
1661
1720
|
//
|
|
1662
1721
|
// Omitted entirely when nothing is declared, so a deployment with no
|
|
1663
|
-
// declared store keeps a byte-identical output set.
|
|
1722
|
+
// declared store keeps a byte-identical output set. The presign endpoint is no
|
|
1723
|
+
// longer exported — under route B the client calls the platform API's
|
|
1724
|
+
// `Upload_Presign` mutation, so there is no per-store URL to publish.
|
|
1664
1725
|
if declaredStoreEndpoints->Array.length > 0 {
|
|
1665
|
-
Pulumi.Pulumi.export(
|
|
1666
|
-
"uploadEndpoints",
|
|
1667
|
-
declaredStoreEndpoints
|
|
1668
|
-
->Array.map(e => e.uploadUrl->Pulumi.Output.apply(u => (e.store, JSON.Encode.string(u))))
|
|
1669
|
-
->Pulumi.Output.all
|
|
1670
|
-
->Pulumi.Output.apply(pairs => pairs->Dict.fromArray->JSON.Encode.object),
|
|
1671
|
-
)
|
|
1672
1726
|
Pulumi.Pulumi.export(
|
|
1673
1727
|
"objectStores",
|
|
1674
1728
|
declaredStoreEndpoints
|
|
@@ -1826,27 +1880,11 @@ module MakeWithConfig = (
|
|
|
1826
1880
|
| None => Pulumi.Output.make(None)
|
|
1827
1881
|
}
|
|
1828
1882
|
|
|
1829
|
-
//
|
|
1830
|
-
//
|
|
1831
|
-
// `
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
Upload_Presign_S3.make(~bucketName=store.bucketName).url->Pulumi.Output.apply(u => Some(u))
|
|
1835
|
-
| None => Pulumi.Output.make(None)
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
// Per-store presign endpoints, keyed by the store's qualified name. This
|
|
1839
|
-
// is what lets an upload input bind to the store its field *declares*
|
|
1840
|
-
// rather than inferring one from the field's name — the endpoint exists
|
|
1841
|
-
// per store, so there is something concrete to bind to.
|
|
1842
|
-
//
|
|
1843
|
-
// `uploadEndpoint` (singular) stays as the legacy service's URL: a shell
|
|
1844
|
-
// built before per-store binding reads it and is unaffected.
|
|
1845
|
-
let storeUploadEndpointsOutput: Pulumi.Output.t<array<(string, string)>> =
|
|
1846
|
-
declaredStoreServices
|
|
1847
|
-
->Array.map(((qualified, _, _, _, url)) => url->Pulumi.Output.apply(u => (qualified, u)))
|
|
1848
|
-
->Pulumi.Output.all
|
|
1849
|
-
|
|
1883
|
+
// Presign endpoints are no longer written to config.json: under route B the
|
|
1884
|
+
// client calls the platform API's `Upload_Presign` mutation (reachable from
|
|
1885
|
+
// `platformApiEndpoint`, already present) with the store it declares, so there
|
|
1886
|
+
// is no per-store URL to publish. The store's *serving* base URL still travels
|
|
1887
|
+
// in the `objectStores` stack output above.
|
|
1850
1888
|
let configJsonContent =
|
|
1851
1889
|
(
|
|
1852
1890
|
(
|
|
@@ -1857,14 +1895,12 @@ module MakeWithConfig = (
|
|
|
1857
1895
|
)->Pulumi.Output.all4,
|
|
1858
1896
|
domainEventsEndpointOutput,
|
|
1859
1897
|
geocoderEndpointOutput,
|
|
1860
|
-
(uploadEndpointOutput, storeUploadEndpointsOutput)->Pulumi.Output.all2,
|
|
1861
1898
|
)
|
|
1862
|
-
->Pulumi.Output.
|
|
1899
|
+
->Pulumi.Output.all3
|
|
1863
1900
|
->Pulumi.Output.apply(((
|
|
1864
1901
|
(domainEp, platformEp, poolId, clientId),
|
|
1865
1902
|
eventsEpOpt,
|
|
1866
1903
|
geocoderEpOpt,
|
|
1867
|
-
(uploadEpOpt, storeUploadEps),
|
|
1868
1904
|
)) => {
|
|
1869
1905
|
let fields = [
|
|
1870
1906
|
("apiEndpoint", JSON.Encode.string(domainEp)),
|
|
@@ -1897,29 +1933,7 @@ module MakeWithConfig = (
|
|
|
1897
1933
|
| Some(ep) => Array.concat(withEvents, [("geocoderEndpoint", JSON.Encode.string(ep))])
|
|
1898
1934
|
| None => withEvents
|
|
1899
1935
|
}
|
|
1900
|
-
|
|
1901
|
-
| Some(ep) => Array.concat(withGeocoder, [("uploadEndpoint", JSON.Encode.string(ep))])
|
|
1902
|
-
| None => withGeocoder
|
|
1903
|
-
}
|
|
1904
|
-
// Omitted entirely when nothing is declared, so a deployment without
|
|
1905
|
-
// declared stores writes a byte-identical config.json.
|
|
1906
|
-
let withStoreUploads = switch storeUploadEps {
|
|
1907
|
-
| [] => withUpload
|
|
1908
|
-
| eps =>
|
|
1909
|
-
Array.concat(
|
|
1910
|
-
withUpload,
|
|
1911
|
-
[
|
|
1912
|
-
(
|
|
1913
|
-
"uploadEndpoints",
|
|
1914
|
-
eps
|
|
1915
|
-
->Array.map(((qualified, url)) => (qualified, JSON.Encode.string(url)))
|
|
1916
|
-
->Dict.fromArray
|
|
1917
|
-
->JSON.Encode.object,
|
|
1918
|
-
),
|
|
1919
|
-
],
|
|
1920
|
-
)
|
|
1921
|
-
}
|
|
1922
|
-
withStoreUploads->Dict.fromArray->JSON.Encode.object->JSON.stringify
|
|
1936
|
+
withGeocoder->Dict.fromArray->JSON.Encode.object->JSON.stringify
|
|
1923
1937
|
})
|
|
1924
1938
|
|
|
1925
1939
|
let _ = PulumiAws.S3.BucketObject.make(
|