@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 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.251",
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.198",
22
- "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
23
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.62",
24
- "@reventlessdev/reventless-infra": "3.0.0-alpha.115",
25
- "@reventlessdev/reventless-spec": "3.0.0-alpha.90"
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
- (`${plugin}.${store}`, keyPrefix, bucketName, storeHandle.bucketName, presign.url)
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, _, _)) => bn == bucketName ? Some(prefix) : None),
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
- // Optional public upload presign Function URL, provisioned against the
1830
- // configured store; its resolved URL becomes config.json's
1831
- // `uploadEndpoint`. No store Output<None> field omitted below.
1832
- let uploadEndpointOutput: Pulumi.Output.t<option<string>> = switch cfg.uploadBucket {
1833
- | Some(store) =>
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.all4
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
- let withUpload = switch uploadEpOpt {
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(