spatialdds 0.5.2__tar.gz → 0.5.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. {spatialdds-0.5.2 → spatialdds-0.5.4}/PKG-INFO +46 -3
  2. {spatialdds-0.5.2 → spatialdds-0.5.4}/README.md +45 -2
  3. {spatialdds-0.5.2 → spatialdds-0.5.4}/pyproject.toml +1 -1
  4. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/__init__.py +1 -1
  5. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/server/gw_http.py +177 -22
  6. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds.egg-info/PKG-INFO +46 -3
  7. {spatialdds-0.5.2 → spatialdds-0.5.4}/setup.cfg +0 -0
  8. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/idl/core.idl +0 -0
  9. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/idl/discovery.idl +0 -0
  10. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/idl/types.idl +0 -0
  11. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/builtin_Time.json +0 -0
  12. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_anchors_AnchorDelta.json +0 -0
  13. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_anchors_AnchorEntry.json +0 -0
  14. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_anchors_AnchorSet.json +0 -0
  15. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_anchors_AnchorSetRequest.json +0 -0
  16. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_anchors_AnchorSetResponse.json +0 -0
  17. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_argeo_NodeGeo.json +0 -0
  18. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_argeo_QualityRequirements.json +0 -0
  19. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_argeo_VpsRequest.json +0 -0
  20. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_argeo_VpsResponse.json +0 -0
  21. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_common_AssetRef.json +0 -0
  22. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_common_FrameRef.json +0 -0
  23. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_common_KV.json +0 -0
  24. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_common_MetaKV.json +0 -0
  25. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_common_Vec2.json +0 -0
  26. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_Aabb3.json +0 -0
  27. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_BlobChunk.json +0 -0
  28. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_BlobRef.json +0 -0
  29. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_ComponentRef.json +0 -0
  30. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_Edge.json +0 -0
  31. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_EntityBinding.json +0 -0
  32. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_FrameTransform.json +0 -0
  33. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_FramedPose.json +0 -0
  34. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_GeoAnchor.json +0 -0
  35. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_GeoPose.json +0 -0
  36. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_NavSatStatus.json +0 -0
  37. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_Node.json +0 -0
  38. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_PlannedTrajectory.json +0 -0
  39. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_PlannedWaypoint.json +0 -0
  40. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_PoseSE3.json +0 -0
  41. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_SnapshotRequest.json +0 -0
  42. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_SnapshotResponse.json +0 -0
  43. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_TileKey.json +0 -0
  44. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_TileMeta.json +0 -0
  45. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_core_TilePatch.json +0 -0
  46. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_Announce.json +0 -0
  47. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_Capabilities.json +0 -0
  48. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_ContentAnnounce.json +0 -0
  49. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_CoverageElement.json +0 -0
  50. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_CoverageFilter.json +0 -0
  51. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_CoverageHint.json +0 -0
  52. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_CoverageQuery.json +0 -0
  53. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_CoverageResponse.json +0 -0
  54. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_Depart.json +0 -0
  55. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_KV.json +0 -0
  56. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_ProfileSupport.json +0 -0
  57. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_ServiceSummary.json +0 -0
  58. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_TopicMeta.json +0 -0
  59. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_Transform.json +0 -0
  60. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_disco_ValidityWindow.json +0 -0
  61. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_events_CrossingLine.json +0 -0
  62. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_events_SpatialEvent.json +0 -0
  63. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_events_SpatialZone.json +0 -0
  64. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_events_ZoneState.json +0 -0
  65. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_mapping_Edge.json +0 -0
  66. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_mapping_MapAlignment.json +0 -0
  67. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_mapping_MapEvent.json +0 -0
  68. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_mapping_MapMeta.json +0 -0
  69. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_mapping_MapQuality.json +0 -0
  70. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_owm_Entity.json +0 -0
  71. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_owm_ModelCommand.json +0 -0
  72. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_owm_ModelPose.json +0 -0
  73. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_semantics_Detection2D.json +0 -0
  74. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_semantics_Detection2DSet.json +0 -0
  75. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_semantics_Detection3D.json +0 -0
  76. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_semantics_Detection3DSet.json +0 -0
  77. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_semantics_FusedTrack.json +0 -0
  78. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_semantics_FusedTrackSet.json +0 -0
  79. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_semantics_Keypoint3D.json +0 -0
  80. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_semantics_KeypointLink.json +0 -0
  81. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_common_Axis.json +0 -0
  82. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_common_FrameHeader.json +0 -0
  83. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_common_FrameQuality.json +0 -0
  84. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_common_Linspace.json +0 -0
  85. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_common_ROI.json +0 -0
  86. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_common_ROIReply.json +0 -0
  87. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_common_ROIRequest.json +0 -0
  88. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_common_StreamMeta.json +0 -0
  89. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarDetection.json +0 -0
  90. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarDetectionSet.json +0 -0
  91. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarFrame.json +0 -0
  92. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarMeta.json +0 -0
  93. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadDetection.json +0 -0
  94. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadDetectionSet.json +0 -0
  95. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadSensorMeta.json +0 -0
  96. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadTensorFrame.json +0 -0
  97. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadTensorMeta.json +0 -0
  98. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioObservation.json +0 -0
  99. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioScan.json +0 -0
  100. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioSensorMeta.json +0 -0
  101. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamArraySet.json +0 -0
  102. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamFrame.json +0 -0
  103. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamMeta.json +0 -0
  104. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_vision_CamIntrinsics.json +0 -0
  105. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_vision_Keypoint2D.json +0 -0
  106. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_vision_Track2D.json +0 -0
  107. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionDetections.json +0 -0
  108. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionFrame.json +0 -0
  109. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionMeta.json +0 -0
  110. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_slam_frontend_CameraInfo.json +0 -0
  111. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_slam_frontend_Feature2D.json +0 -0
  112. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_slam_frontend_FeatureMatch.json +0 -0
  113. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_slam_frontend_KeyframeFeatures.json +0 -0
  114. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_slam_frontend_Landmark.json +0 -0
  115. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_slam_frontend_MatchSet.json +0 -0
  116. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_slam_frontend_TrackObs.json +0 -0
  117. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_slam_frontend_Tracklet.json +0 -0
  118. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_vio_FusedState.json +0 -0
  119. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_vio_ImuInfo.json +0 -0
  120. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_vio_ImuSample.json +0 -0
  121. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_vio_MagnetometerSample.json +0 -0
  122. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/schemas/spatial_vio_SensorFusionSample.json +0 -0
  123. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/artifacts/serializer_tables.json +0 -0
  124. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/canonical.py +0 -0
  125. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/canonjson.py +0 -0
  126. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/client.py +0 -0
  127. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/demo/assets/duck.gltf +0 -0
  128. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/demo/assets/fountain-label.txt +0 -0
  129. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/demo/assets/fountain-marker.gltf +0 -0
  130. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/demo/fountain-anchors.mcap +0 -0
  131. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/demo/manifests/content-fountain-label.json +0 -0
  132. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/demo/manifests/content-fountain-marker.json +0 -0
  133. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/errors.py +0 -0
  134. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/records.py +0 -0
  135. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/serve.py +0 -0
  136. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/server/__init__.py +0 -0
  137. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/server/commands.py +0 -0
  138. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/server/gateway.py +0 -0
  139. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/server/idlmodel.py +0 -0
  140. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/server/pin.py +0 -0
  141. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/server/publish.py +0 -0
  142. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/server/webjson.py +0 -0
  143. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds/server/xcdr2.py +0 -0
  144. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds.egg-info/SOURCES.txt +0 -0
  145. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds.egg-info/dependency_links.txt +0 -0
  146. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds.egg-info/entry_points.txt +0 -0
  147. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds.egg-info/requires.txt +0 -0
  148. {spatialdds-0.5.2 → spatialdds-0.5.4}/spatialdds.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spatialdds
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: SpatialDDS over plain HTTPS and a WebSocket: a client for the web binding (Appendix N), and the reference registry behind one command.
5
5
  Author: Open AR Cloud
6
6
  License: Apache-2.0
@@ -419,7 +419,7 @@ an object the app had already discarded. If you called `register()` before
419
419
  `make_app()`, 0.4.0 and 0.5.0 worked; if you called it after, the command lane
420
420
  advertised nothing and refused every POST.
421
421
 
422
- 0.5.2 makes `notify` safe from the thread its own docstring invited. It put
422
+ 0.5.2 made `notify` safe from the thread its own docstring invited. It put
423
423
  onto the subscriber queue directly, and `asyncio.Queue.put_nowait` from a
424
424
  foreign thread enqueues without waking the loop — so a backend reading a bus
425
425
  in a thread fed a parked subscriber nothing until the loop woke for some other
@@ -428,7 +428,50 @@ pumping 202 samples into two registered subscriber queues delivered 0 live
428
428
  events. If you call `notify` only from the serving loop, 0.5.0 and 0.5.1 were
429
429
  correct for you.
430
430
 
431
- ## Not in 0.5.2
431
+ 0.5.3 let a browser reach this gateway at all. `Authorization` is not a
432
+ CORS-safelisted request header, so a cross-origin request carrying one is
433
+ preceded by an `OPTIONS` preflight — and no `OPTIONS` was routed, so aiohttp
434
+ answered `405 Allow: GET,HEAD,POST` and the browser stopped there without
435
+ making the request. Two consequences, both found from outside by a real page
436
+ rather than by a gate here: **N.6 was unreachable from a browser for every
437
+ access class**, because a command always carries a credential and so always
438
+ preflights, which no reclassification can change; and a `public-cacheable`
439
+ `GET` that answers `200` with `Access-Control-Allow-Origin: *` failed outright
440
+ if the client volunteered a credential it did not need.
441
+
442
+ An authorized operator-scoped response now also carries the asking origin,
443
+ echoed, with `Vary: Origin, Authorization`. The previous reasoning was that a
444
+ gated resource should never be CORS-open because its credential "must not be
445
+ invited to attach ambiently" — right about the risk and wrong about the
446
+ mechanism. `Access-Control-Allow-Credentials` is what issues that invitation,
447
+ this package sets it nowhere, and a `Bearer` token the page holds and chooses
448
+ to attach is the opposite of ambient. Refusing the origin never kept a
449
+ credential out; it made an authorized `200` unreadable by the client that had
450
+ authorized itself. The body is still `no-store`, the credential is still
451
+ required, and nothing is newly reachable without one.
452
+
453
+ If your clients are not browsers, 0.5.0 through 0.5.2 were correct for you:
454
+ nothing else changes, and no response loses a header it had.
455
+
456
+ 0.5.4 finishes what 0.5.3 started, and the gap is instructive. 0.5.3 threaded
457
+ the asking origin through the three places that build a response from
458
+ `_cache_headers` and missed the two that build one by hand: the N.6 command
459
+ lane's own reply, and **every N.8 problem document**. So a command published
460
+ and the page could not read the outcome --- a cross-origin `fetch` rejects a
461
+ response with no allowed origin, so a client saw a failure for a command that
462
+ had in fact succeeded --- and a `401`, `409` or `400` was unreadable by the
463
+ client it was written for, which is N.8 failing at the one thing that
464
+ distinguishes it from a bare status.
465
+
466
+ The fix is a one-rule middleware rather than a fourth and fifth call site:
467
+ where a response names no origin and the request had one, echo it with
468
+ `Vary: Origin`. `_cache_headers` remains the place that decides cacheability
469
+ and reach together and is unchanged where it already acts. Threading it
470
+ explicitly was tried first and is what missed two paths.
471
+
472
+ If your clients are not browsers, nothing here changes anything for you.
473
+
474
+ ## Not in 0.5.4
432
475
 
433
476
  Stated so nobody waits for them: no async API, no caching beyond ETag
434
477
  revalidation, no retry policy, no MCAP tooling beyond the journal the server
@@ -395,7 +395,7 @@ an object the app had already discarded. If you called `register()` before
395
395
  `make_app()`, 0.4.0 and 0.5.0 worked; if you called it after, the command lane
396
396
  advertised nothing and refused every POST.
397
397
 
398
- 0.5.2 makes `notify` safe from the thread its own docstring invited. It put
398
+ 0.5.2 made `notify` safe from the thread its own docstring invited. It put
399
399
  onto the subscriber queue directly, and `asyncio.Queue.put_nowait` from a
400
400
  foreign thread enqueues without waking the loop — so a backend reading a bus
401
401
  in a thread fed a parked subscriber nothing until the loop woke for some other
@@ -404,7 +404,50 @@ pumping 202 samples into two registered subscriber queues delivered 0 live
404
404
  events. If you call `notify` only from the serving loop, 0.5.0 and 0.5.1 were
405
405
  correct for you.
406
406
 
407
- ## Not in 0.5.2
407
+ 0.5.3 let a browser reach this gateway at all. `Authorization` is not a
408
+ CORS-safelisted request header, so a cross-origin request carrying one is
409
+ preceded by an `OPTIONS` preflight — and no `OPTIONS` was routed, so aiohttp
410
+ answered `405 Allow: GET,HEAD,POST` and the browser stopped there without
411
+ making the request. Two consequences, both found from outside by a real page
412
+ rather than by a gate here: **N.6 was unreachable from a browser for every
413
+ access class**, because a command always carries a credential and so always
414
+ preflights, which no reclassification can change; and a `public-cacheable`
415
+ `GET` that answers `200` with `Access-Control-Allow-Origin: *` failed outright
416
+ if the client volunteered a credential it did not need.
417
+
418
+ An authorized operator-scoped response now also carries the asking origin,
419
+ echoed, with `Vary: Origin, Authorization`. The previous reasoning was that a
420
+ gated resource should never be CORS-open because its credential "must not be
421
+ invited to attach ambiently" — right about the risk and wrong about the
422
+ mechanism. `Access-Control-Allow-Credentials` is what issues that invitation,
423
+ this package sets it nowhere, and a `Bearer` token the page holds and chooses
424
+ to attach is the opposite of ambient. Refusing the origin never kept a
425
+ credential out; it made an authorized `200` unreadable by the client that had
426
+ authorized itself. The body is still `no-store`, the credential is still
427
+ required, and nothing is newly reachable without one.
428
+
429
+ If your clients are not browsers, 0.5.0 through 0.5.2 were correct for you:
430
+ nothing else changes, and no response loses a header it had.
431
+
432
+ 0.5.4 finishes what 0.5.3 started, and the gap is instructive. 0.5.3 threaded
433
+ the asking origin through the three places that build a response from
434
+ `_cache_headers` and missed the two that build one by hand: the N.6 command
435
+ lane's own reply, and **every N.8 problem document**. So a command published
436
+ and the page could not read the outcome --- a cross-origin `fetch` rejects a
437
+ response with no allowed origin, so a client saw a failure for a command that
438
+ had in fact succeeded --- and a `401`, `409` or `400` was unreadable by the
439
+ client it was written for, which is N.8 failing at the one thing that
440
+ distinguishes it from a bare status.
441
+
442
+ The fix is a one-rule middleware rather than a fourth and fifth call site:
443
+ where a response names no origin and the request had one, echo it with
444
+ `Vary: Origin`. `_cache_headers` remains the place that decides cacheability
445
+ and reach together and is unchanged where it already acts. Threading it
446
+ explicitly was tried first and is what missed two paths.
447
+
448
+ If your clients are not browsers, nothing here changes anything for you.
449
+
450
+ ## Not in 0.5.4
408
451
 
409
452
  Stated so nobody waits for them: no async API, no caching beyond ETag
410
453
  revalidation, no retry policy, no MCAP tooling beyond the journal the server
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spatialdds"
7
- version = "0.5.2"
7
+ version = "0.5.4"
8
8
  description = "SpatialDDS over plain HTTPS and a WebSocket: a client for the web binding (Appendix N), and the reference registry behind one command."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -37,7 +37,7 @@ __all__ = [
37
37
  "content_manifest", "geo_anchor", "service_uri", "content_uri",
38
38
  ]
39
39
 
40
- __version__ = "0.5.2"
40
+ __version__ = "0.5.4"
41
41
  #: The SpatialDDS-spec commit whose Appendix N and generated artifacts this
42
42
  #: package implements. Package versions and specification versions move
43
43
  #: independently; this is the one that says what the wire looks like.
@@ -91,20 +91,55 @@ def _problem(status: int, slug: str, title: str, detail: str,
91
91
  headers={"Cache-Control": "no-store"})
92
92
 
93
93
 
94
- def _cache_headers(access: str, doc_ttl: int | None = None) -> dict[str, str]:
94
+ #: What a cross-origin reader may see beyond the body. Without it the ETag and
95
+ #: the access class are served and unreadable, which is the instrument failing
96
+ #: to report what it measures.
97
+ EXPOSE_HEADERS = ("ETag, X-SpatialDDS-Topic, X-SpatialDDS-Type, "
98
+ "X-SpatialDDS-Access")
99
+
100
+ #: Request headers a browser may send to this gateway. `Authorization` is the
101
+ #: one that matters and the one that is not CORS-safelisted, so a request
102
+ #: carrying it preflights; `Content-Type` is needed for any JSON body, and
103
+ #: `If-None-Match` for conditional GETs.
104
+ ALLOW_HEADERS = "Authorization, Content-Type, If-None-Match"
105
+
106
+ #: Methods this gateway serves. Stated once so the preflight answer and the
107
+ #: `Allow` header on a refusal cannot drift apart.
108
+ ALLOW_METHODS = "GET, HEAD, POST, PUT, DELETE, OPTIONS"
109
+
110
+ #: How long a browser may cache a preflight. Ten minutes: long enough that a
111
+ #: page does not preflight every command, short enough that changing the
112
+ #: policy takes effect inside a session.
113
+ PREFLIGHT_MAX_AGE = 600
114
+
115
+
116
+ def _cache_headers(access: str, doc_ttl: int | None = None,
117
+ origin: str | None = None) -> dict[str, str]:
95
118
  """Cacheability, reach, and auth decided together or not at all.
96
119
 
97
120
  The CORS header lives in this branch rather than in a middleware for the
98
121
  same reason the cache header does: a public-cacheable resource is one a
99
122
  client may read without presenting anything, so there is no origin that
100
- should be refused it, and an operator-scoped resource is gated by a
101
- credential a browser must not be invited to attach ambiently. Putting the
102
- two in one function makes the pairing impossible to drift apart.
103
-
104
- `Access-Control-Expose-Headers` is here because without it a cross-origin
105
- reader can see the body but not the ETag or the access class that describes
106
- it -- the headers would be served and unreadable, which is the instrument
107
- failing to report what it measures.
123
+ should be refused it. Putting the two in one function makes the pairing
124
+ impossible to drift apart.
125
+
126
+ **An operator-scoped response now carries an echoed origin too, and the
127
+ reasoning that said otherwise was close to right.** It read: gated by "a
128
+ credential a browser must not be invited to attach ambiently". The
129
+ invitation is real and `Access-Control-Allow-Origin` is not how it is
130
+ issued --- `Access-Control-Allow-Credentials` is. Cookies and HTTP auth
131
+ ride along only when that header says so and the client asks for
132
+ `credentials: 'include'`; this gateway sets it nowhere and never will, so
133
+ nothing here is ambient. A `Bearer` token in an `Authorization` header is
134
+ the opposite of ambient: the page had to hold it and choose to attach it.
135
+
136
+ Refusing the origin did not keep that credential out. It made an
137
+ authorized response unreadable by the client that authorized itself ---
138
+ measured from `SpatialDDS-demo`'s browser lane, where a page holding a
139
+ valid token could not read a word of a 200 it had earned. So the origin is
140
+ echoed rather than `*`, with `Vary: Origin`, which keeps a shared cache
141
+ from serving one origin's answer to another; the body stays `no-store`
142
+ regardless, and the credential is still required. No gate is weakened.
108
143
  """
109
144
  if access == gateway.PUBLIC:
110
145
  # N.4: where a public-cacheable resource is a manifest carrying its own
@@ -113,15 +148,116 @@ def _cache_headers(access: str, doc_ttl: int | None = None) -> dict[str, str]:
113
148
  age = gateway.PUBLIC_MAX_AGE if doc_ttl is None else doc_ttl
114
149
  return {"Cache-Control": f"public, max-age={age}",
115
150
  "Access-Control-Allow-Origin": "*",
116
- "Access-Control-Expose-Headers":
117
- "ETag, X-SpatialDDS-Topic, X-SpatialDDS-Type, "
118
- "X-SpatialDDS-Access"}
119
- # Operator-scoped. Never cacheable and never CORS-open, by the same
120
- # decision that gated it. `doc_ttl` is ignored here deliberately: N.4
121
- # grants no lifetime to a gated resource whatever `ttl_sec` it carries,
122
- # because the cache that would honour it is the shared one the gate exists
123
- # to keep the body out of.
124
- return {"Cache-Control": "no-store", "Vary": "Authorization"}
151
+ "Access-Control-Expose-Headers": EXPOSE_HEADERS}
152
+ # Operator-scoped. Never cacheable, by the same decision that gated it.
153
+ # `doc_ttl` is ignored here deliberately: N.4 grants no lifetime to a gated
154
+ # resource whatever `ttl_sec` it carries, because the cache that would
155
+ # honour it is the shared one the gate exists to keep the body out of.
156
+ headers = {"Cache-Control": "no-store", "Vary": "Authorization"}
157
+ if origin:
158
+ # Echoed, not `*`: a wildcard on a response that varies by credential
159
+ # is the combination a shared cache can get wrong, and there is no
160
+ # reason to name an origin this request did not come from.
161
+ headers["Access-Control-Allow-Origin"] = origin
162
+ headers["Vary"] = "Origin, Authorization"
163
+ headers["Access-Control-Expose-Headers"] = EXPOSE_HEADERS
164
+ return headers
165
+
166
+
167
+ @web.middleware
168
+ async def _reach(request: web.Request, handler) -> web.StreamResponse:
169
+ """Make sure every answer names the origin that asked for it.
170
+
171
+ **A backstop, not the policy.** `_cache_headers` remains the single place
172
+ that decides cacheability and reach together, and where it has already set
173
+ `Access-Control-Allow-Origin` this changes nothing. What it covers is the
174
+ responses that set no reach at all, because they are built by hand rather
175
+ than through that function --- and the reason it exists is that threading
176
+ the origin explicitly through every construction site was tried first, in
177
+ 0.5.3, and missed two:
178
+
179
+ * `post_command`'s `202`. The command published and the page could not
180
+ read the outcome: a cross-origin `fetch` rejects a response with no
181
+ allowed origin, so `bindingCommand` in the demo's client landed in its
182
+ `catch` with no status at all, for a command that had in fact succeeded.
183
+ * **Every N.8 problem document.** `_problem` set `no-store` and nothing
184
+ else, so a `401`, `409` or `400` was unreadable by the client it was
185
+ written for. N.8 exists to hand a client a *reason*; a reason a browser
186
+ cannot read is the surface failing at the one thing that distinguishes
187
+ it from a bare status.
188
+
189
+ Both were found by curling the running demo after 0.5.3 shipped, not by
190
+ this repository's gates --- the same way the original defect was found,
191
+ which is the lesson repeating rather than a new one.
192
+
193
+ Only the origin is echoed here, and only when absent. `Vary: Origin` goes
194
+ with it so a shared cache cannot serve one origin's answer to another; the
195
+ `Expose-Headers` set is added for the same reason it is on a GET, since a
196
+ reader that cannot see the ETag cannot revalidate. `Allow-Credentials` is
197
+ still set nowhere.
198
+ """
199
+ response = await handler(request)
200
+ origin = request.headers.get("Origin")
201
+ if origin and "Access-Control-Allow-Origin" not in response.headers:
202
+ response.headers["Access-Control-Allow-Origin"] = origin
203
+ vary = response.headers.get("Vary")
204
+ response.headers["Vary"] = f"{vary}, Origin" if vary else "Origin"
205
+ response.headers.setdefault("Access-Control-Expose-Headers",
206
+ EXPOSE_HEADERS)
207
+ return response
208
+
209
+
210
+ async def preflight(request: web.Request) -> web.Response:
211
+ """Answer the `OPTIONS` a browser sends before a credentialed request.
212
+
213
+ **Why this route has to exist, and why nothing in the specification said
214
+ so.** `Authorization` is not a CORS-safelisted request header, so a
215
+ cross-origin request carrying one is preceded by a preflight. This gateway
216
+ routed no `OPTIONS` at all, so aiohttp answered `405` with
217
+ `Allow: GET,HEAD,POST` --- and the browser stopped there, never making the
218
+ request. Two consequences, both measured against 0.5.2 from a page on
219
+ another origin:
220
+
221
+ * N.6 was unreachable from a browser for **every** access class, because a
222
+ command always carries a credential. Reclassifying a lane cannot help:
223
+ the preflight is triggered by the request, not by what the resource is.
224
+ * A `public-cacheable` GET that answers `200` with
225
+ `Access-Control-Allow-Origin: *` failed outright if the client
226
+ volunteered a credential it did not need.
227
+
228
+ Both are fixed here rather than in Appendix N, because nothing in N
229
+ forbade any of it --- a conforming endpoint could simply omit it, which is
230
+ the separate gap filed for the specification: a binding whose flagship
231
+ client is a browser should have to *state* its preflight behaviour.
232
+
233
+ `Allow-Credentials` is deliberately absent. It is the header that would
234
+ invite cookies and HTTP auth to ride along ambiently, and this gateway
235
+ wants the opposite: a credential the page holds and chooses to attach.
236
+
237
+ A bare `OPTIONS` with no `Origin` is not a preflight; it gets the same
238
+ `Allow` list as a courtesy, which also keeps `ALLOW_METHODS` the single
239
+ place that answer is written.
240
+ """
241
+ origin = request.headers.get("Origin")
242
+ headers = {"Allow": ALLOW_METHODS}
243
+ if origin:
244
+ headers.update({
245
+ "Access-Control-Allow-Origin": origin,
246
+ "Access-Control-Allow-Methods": ALLOW_METHODS,
247
+ # Echo what was asked for when it was asked, so a header this
248
+ # gateway has not thought of is refused rather than blanket-allowed,
249
+ # and fall back to the list above for a preflight that names none.
250
+ "Access-Control-Allow-Headers":
251
+ request.headers.get("Access-Control-Request-Headers")
252
+ or ALLOW_HEADERS,
253
+ "Access-Control-Max-Age": str(PREFLIGHT_MAX_AGE),
254
+ "Access-Control-Expose-Headers": EXPOSE_HEADERS,
255
+ # The answer depends on all three, so a cache must not reuse it
256
+ # across different ones.
257
+ "Vary": "Origin, Access-Control-Request-Method, "
258
+ "Access-Control-Request-Headers",
259
+ })
260
+ return web.Response(status=204, headers=headers)
125
261
 
126
262
 
127
263
  def _authorized(request: web.Request, token: str | None) -> bool:
@@ -145,7 +281,9 @@ def _json_response(request: web.Request, doc: Any, access: str,
145
281
  doc_ttl: int | None = None) -> web.Response:
146
282
  """One JSON resource, with its ETag, its cache class, and 304 handling."""
147
283
  etag = _etag(doc)
148
- headers = {"ETag": etag, **_cache_headers(access, doc_ttl),
284
+ headers = {"ETag": etag,
285
+ **_cache_headers(access, doc_ttl,
286
+ request.headers.get("Origin")),
149
287
  **(extra or {})}
150
288
  if request.headers.get("If-None-Match", "") == etag:
151
289
  return web.Response(status=304, headers=headers)
@@ -473,7 +611,9 @@ def _written(request: web.Request, doc: Any, created: bool,
473
611
  if not created else web.Response(
474
612
  body=_body(doc), status=201, content_type="application/json",
475
613
  headers={"ETag": _etag(doc),
476
- **_cache_headers(gateway.OPERATOR), **extra})
614
+ **_cache_headers(gateway.OPERATOR, None,
615
+ request.headers.get("Origin")),
616
+ **extra})
477
617
 
478
618
 
479
619
  def _running_loop():
@@ -741,7 +881,9 @@ async def delete_instance(request: web.Request) -> web.Response:
741
881
  {"event": "dispose", "topic": topic, "key": key,
742
882
  "reason": reason})
743
883
  return web.Response(status=204,
744
- headers=_cache_headers(gateway.OPERATOR))
884
+ headers=_cache_headers(
885
+ gateway.OPERATOR, None,
886
+ request.headers.get("Origin")))
745
887
 
746
888
 
747
889
  async def put_manifest(request: web.Request) -> web.Response:
@@ -902,7 +1044,7 @@ def make_app(state: gateway.State, base: str, token: str | None,
902
1044
  embedder passes `publish_enabled=False, live_subscriptions=True` and gets
903
1045
  the live lane with no write surface at all.
904
1046
  """
905
- app = web.Application()
1047
+ app = web.Application(middlewares=[_reach])
906
1048
  app["state"] = state
907
1049
  app["base"] = base.rstrip("/")
908
1050
  app["token"] = token
@@ -959,5 +1101,18 @@ def make_app(state: gateway.State, base: str, token: str | None,
959
1101
  web.delete("/{scene}/{stream}/{type}/{version}/{key}",
960
1102
  delete_instance),
961
1103
  web.put(gateway.RESOLVE_PATH, put_manifest),
1104
+ # The preflight, on every path that can be asked for cross-origin
1105
+ # with a credential or a JSON body. Mirrors the routes above rather
1106
+ # than using a catch-all, so a path this gateway does not serve still
1107
+ # 404s on OPTIONS instead of promising a method nobody implements.
1108
+ web.options(f"{WELL_KNOWN}/schemas", preflight),
1109
+ web.options(f"{WELL_KNOWN}/schemas/{{type}}", preflight),
1110
+ web.options(f"{WELL_KNOWN}/search", preflight),
1111
+ web.options(f"{WELL_KNOWN}/bootstrap", preflight),
1112
+ web.options(f"{WELL_KNOWN}/resolver", preflight),
1113
+ web.options(gateway.RESOLVE_PATH, preflight),
1114
+ web.options(f"{ASSET_PATH}/{{name}}", preflight),
1115
+ web.options("/{scene}/{stream}/{type}/{version}", preflight),
1116
+ web.options("/{scene}/{stream}/{type}/{version}/{key}", preflight),
962
1117
  ])
963
1118
  return app
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spatialdds
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: SpatialDDS over plain HTTPS and a WebSocket: a client for the web binding (Appendix N), and the reference registry behind one command.
5
5
  Author: Open AR Cloud
6
6
  License: Apache-2.0
@@ -419,7 +419,7 @@ an object the app had already discarded. If you called `register()` before
419
419
  `make_app()`, 0.4.0 and 0.5.0 worked; if you called it after, the command lane
420
420
  advertised nothing and refused every POST.
421
421
 
422
- 0.5.2 makes `notify` safe from the thread its own docstring invited. It put
422
+ 0.5.2 made `notify` safe from the thread its own docstring invited. It put
423
423
  onto the subscriber queue directly, and `asyncio.Queue.put_nowait` from a
424
424
  foreign thread enqueues without waking the loop — so a backend reading a bus
425
425
  in a thread fed a parked subscriber nothing until the loop woke for some other
@@ -428,7 +428,50 @@ pumping 202 samples into two registered subscriber queues delivered 0 live
428
428
  events. If you call `notify` only from the serving loop, 0.5.0 and 0.5.1 were
429
429
  correct for you.
430
430
 
431
- ## Not in 0.5.2
431
+ 0.5.3 let a browser reach this gateway at all. `Authorization` is not a
432
+ CORS-safelisted request header, so a cross-origin request carrying one is
433
+ preceded by an `OPTIONS` preflight — and no `OPTIONS` was routed, so aiohttp
434
+ answered `405 Allow: GET,HEAD,POST` and the browser stopped there without
435
+ making the request. Two consequences, both found from outside by a real page
436
+ rather than by a gate here: **N.6 was unreachable from a browser for every
437
+ access class**, because a command always carries a credential and so always
438
+ preflights, which no reclassification can change; and a `public-cacheable`
439
+ `GET` that answers `200` with `Access-Control-Allow-Origin: *` failed outright
440
+ if the client volunteered a credential it did not need.
441
+
442
+ An authorized operator-scoped response now also carries the asking origin,
443
+ echoed, with `Vary: Origin, Authorization`. The previous reasoning was that a
444
+ gated resource should never be CORS-open because its credential "must not be
445
+ invited to attach ambiently" — right about the risk and wrong about the
446
+ mechanism. `Access-Control-Allow-Credentials` is what issues that invitation,
447
+ this package sets it nowhere, and a `Bearer` token the page holds and chooses
448
+ to attach is the opposite of ambient. Refusing the origin never kept a
449
+ credential out; it made an authorized `200` unreadable by the client that had
450
+ authorized itself. The body is still `no-store`, the credential is still
451
+ required, and nothing is newly reachable without one.
452
+
453
+ If your clients are not browsers, 0.5.0 through 0.5.2 were correct for you:
454
+ nothing else changes, and no response loses a header it had.
455
+
456
+ 0.5.4 finishes what 0.5.3 started, and the gap is instructive. 0.5.3 threaded
457
+ the asking origin through the three places that build a response from
458
+ `_cache_headers` and missed the two that build one by hand: the N.6 command
459
+ lane's own reply, and **every N.8 problem document**. So a command published
460
+ and the page could not read the outcome --- a cross-origin `fetch` rejects a
461
+ response with no allowed origin, so a client saw a failure for a command that
462
+ had in fact succeeded --- and a `401`, `409` or `400` was unreadable by the
463
+ client it was written for, which is N.8 failing at the one thing that
464
+ distinguishes it from a bare status.
465
+
466
+ The fix is a one-rule middleware rather than a fourth and fifth call site:
467
+ where a response names no origin and the request had one, echo it with
468
+ `Vary: Origin`. `_cache_headers` remains the place that decides cacheability
469
+ and reach together and is unchanged where it already acts. Threading it
470
+ explicitly was tried first and is what missed two paths.
471
+
472
+ If your clients are not browsers, nothing here changes anything for you.
473
+
474
+ ## Not in 0.5.4
432
475
 
433
476
  Stated so nobody waits for them: no async API, no caching beyond ETag
434
477
  revalidation, no retry policy, no MCAP tooling beyond the journal the server
File without changes