spatialdds 0.5.1__tar.gz → 0.5.3__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.1 → spatialdds-0.5.3}/PKG-INFO +36 -2
  2. {spatialdds-0.5.1 → spatialdds-0.5.3}/README.md +35 -1
  3. {spatialdds-0.5.1 → spatialdds-0.5.3}/pyproject.toml +1 -1
  4. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/__init__.py +1 -1
  5. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/server/gw_http.py +182 -25
  6. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds.egg-info/PKG-INFO +36 -2
  7. {spatialdds-0.5.1 → spatialdds-0.5.3}/setup.cfg +0 -0
  8. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/idl/core.idl +0 -0
  9. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/idl/discovery.idl +0 -0
  10. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/idl/types.idl +0 -0
  11. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/builtin_Time.json +0 -0
  12. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_anchors_AnchorDelta.json +0 -0
  13. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_anchors_AnchorEntry.json +0 -0
  14. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_anchors_AnchorSet.json +0 -0
  15. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_anchors_AnchorSetRequest.json +0 -0
  16. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_anchors_AnchorSetResponse.json +0 -0
  17. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_argeo_NodeGeo.json +0 -0
  18. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_argeo_QualityRequirements.json +0 -0
  19. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_argeo_VpsRequest.json +0 -0
  20. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_argeo_VpsResponse.json +0 -0
  21. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_common_AssetRef.json +0 -0
  22. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_common_FrameRef.json +0 -0
  23. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_common_KV.json +0 -0
  24. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_common_MetaKV.json +0 -0
  25. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_common_Vec2.json +0 -0
  26. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_Aabb3.json +0 -0
  27. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_BlobChunk.json +0 -0
  28. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_BlobRef.json +0 -0
  29. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_ComponentRef.json +0 -0
  30. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_Edge.json +0 -0
  31. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_EntityBinding.json +0 -0
  32. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_FrameTransform.json +0 -0
  33. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_FramedPose.json +0 -0
  34. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_GeoAnchor.json +0 -0
  35. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_GeoPose.json +0 -0
  36. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_NavSatStatus.json +0 -0
  37. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_Node.json +0 -0
  38. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_PlannedTrajectory.json +0 -0
  39. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_PlannedWaypoint.json +0 -0
  40. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_PoseSE3.json +0 -0
  41. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_SnapshotRequest.json +0 -0
  42. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_SnapshotResponse.json +0 -0
  43. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_TileKey.json +0 -0
  44. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_TileMeta.json +0 -0
  45. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_core_TilePatch.json +0 -0
  46. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_Announce.json +0 -0
  47. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_Capabilities.json +0 -0
  48. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_ContentAnnounce.json +0 -0
  49. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_CoverageElement.json +0 -0
  50. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_CoverageFilter.json +0 -0
  51. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_CoverageHint.json +0 -0
  52. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_CoverageQuery.json +0 -0
  53. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_CoverageResponse.json +0 -0
  54. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_Depart.json +0 -0
  55. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_KV.json +0 -0
  56. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_ProfileSupport.json +0 -0
  57. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_ServiceSummary.json +0 -0
  58. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_TopicMeta.json +0 -0
  59. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_Transform.json +0 -0
  60. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_disco_ValidityWindow.json +0 -0
  61. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_events_CrossingLine.json +0 -0
  62. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_events_SpatialEvent.json +0 -0
  63. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_events_SpatialZone.json +0 -0
  64. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_events_ZoneState.json +0 -0
  65. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_mapping_Edge.json +0 -0
  66. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_mapping_MapAlignment.json +0 -0
  67. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_mapping_MapEvent.json +0 -0
  68. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_mapping_MapMeta.json +0 -0
  69. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_mapping_MapQuality.json +0 -0
  70. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_owm_Entity.json +0 -0
  71. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_owm_ModelCommand.json +0 -0
  72. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_owm_ModelPose.json +0 -0
  73. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_semantics_Detection2D.json +0 -0
  74. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_semantics_Detection2DSet.json +0 -0
  75. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_semantics_Detection3D.json +0 -0
  76. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_semantics_Detection3DSet.json +0 -0
  77. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_semantics_FusedTrack.json +0 -0
  78. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_semantics_FusedTrackSet.json +0 -0
  79. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_semantics_Keypoint3D.json +0 -0
  80. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_semantics_KeypointLink.json +0 -0
  81. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_common_Axis.json +0 -0
  82. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_common_FrameHeader.json +0 -0
  83. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_common_FrameQuality.json +0 -0
  84. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_common_Linspace.json +0 -0
  85. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_common_ROI.json +0 -0
  86. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_common_ROIReply.json +0 -0
  87. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_common_ROIRequest.json +0 -0
  88. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_common_StreamMeta.json +0 -0
  89. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarDetection.json +0 -0
  90. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarDetectionSet.json +0 -0
  91. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarFrame.json +0 -0
  92. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_lidar_LidarMeta.json +0 -0
  93. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadDetection.json +0 -0
  94. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadDetectionSet.json +0 -0
  95. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadSensorMeta.json +0 -0
  96. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadTensorFrame.json +0 -0
  97. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_rad_RadTensorMeta.json +0 -0
  98. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioObservation.json +0 -0
  99. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioScan.json +0 -0
  100. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_radio_RadioSensorMeta.json +0 -0
  101. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamArraySet.json +0 -0
  102. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamFrame.json +0 -0
  103. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_rf_beam_RfBeamMeta.json +0 -0
  104. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_vision_CamIntrinsics.json +0 -0
  105. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_vision_Keypoint2D.json +0 -0
  106. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_vision_Track2D.json +0 -0
  107. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionDetections.json +0 -0
  108. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionFrame.json +0 -0
  109. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_sensing_vision_VisionMeta.json +0 -0
  110. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_slam_frontend_CameraInfo.json +0 -0
  111. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_slam_frontend_Feature2D.json +0 -0
  112. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_slam_frontend_FeatureMatch.json +0 -0
  113. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_slam_frontend_KeyframeFeatures.json +0 -0
  114. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_slam_frontend_Landmark.json +0 -0
  115. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_slam_frontend_MatchSet.json +0 -0
  116. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_slam_frontend_TrackObs.json +0 -0
  117. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_slam_frontend_Tracklet.json +0 -0
  118. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_vio_FusedState.json +0 -0
  119. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_vio_ImuInfo.json +0 -0
  120. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_vio_ImuSample.json +0 -0
  121. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_vio_MagnetometerSample.json +0 -0
  122. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/schemas/spatial_vio_SensorFusionSample.json +0 -0
  123. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/artifacts/serializer_tables.json +0 -0
  124. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/canonical.py +0 -0
  125. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/canonjson.py +0 -0
  126. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/client.py +0 -0
  127. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/demo/assets/duck.gltf +0 -0
  128. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/demo/assets/fountain-label.txt +0 -0
  129. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/demo/assets/fountain-marker.gltf +0 -0
  130. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/demo/fountain-anchors.mcap +0 -0
  131. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/demo/manifests/content-fountain-label.json +0 -0
  132. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/demo/manifests/content-fountain-marker.json +0 -0
  133. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/errors.py +0 -0
  134. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/records.py +0 -0
  135. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/serve.py +0 -0
  136. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/server/__init__.py +0 -0
  137. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/server/commands.py +0 -0
  138. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/server/gateway.py +0 -0
  139. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/server/idlmodel.py +0 -0
  140. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/server/pin.py +0 -0
  141. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/server/publish.py +0 -0
  142. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/server/webjson.py +0 -0
  143. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds/server/xcdr2.py +0 -0
  144. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds.egg-info/SOURCES.txt +0 -0
  145. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds.egg-info/dependency_links.txt +0 -0
  146. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds.egg-info/entry_points.txt +0 -0
  147. {spatialdds-0.5.1 → spatialdds-0.5.3}/spatialdds.egg-info/requires.txt +0 -0
  148. {spatialdds-0.5.1 → spatialdds-0.5.3}/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.1
3
+ Version: 0.5.3
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,41 @@ 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
- ## Not in 0.5.1
422
+ 0.5.2 made `notify` safe from the thread its own docstring invited. It put
423
+ onto the subscriber queue directly, and `asyncio.Queue.put_nowait` from a
424
+ foreign thread enqueues without waking the loop — so a backend reading a bus
425
+ in a thread fed a parked subscriber nothing until the loop woke for some other
426
+ reason. Busy endpoint, invisible; idle endpoint, silent. Measured: a bridge
427
+ pumping 202 samples into two registered subscriber queues delivered 0 live
428
+ events. If you call `notify` only from the serving loop, 0.5.0 and 0.5.1 were
429
+ correct for you.
430
+
431
+ 0.5.3 lets 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
+ ## Not in 0.5.3
423
457
 
424
458
  Stated so nobody waits for them: no async API, no caching beyond ETag
425
459
  revalidation, no retry policy, no MCAP tooling beyond the journal the server
@@ -395,7 +395,41 @@ 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
- ## Not in 0.5.1
398
+ 0.5.2 made `notify` safe from the thread its own docstring invited. It put
399
+ onto the subscriber queue directly, and `asyncio.Queue.put_nowait` from a
400
+ foreign thread enqueues without waking the loop — so a backend reading a bus
401
+ in a thread fed a parked subscriber nothing until the loop woke for some other
402
+ reason. Busy endpoint, invisible; idle endpoint, silent. Measured: a bridge
403
+ pumping 202 samples into two registered subscriber queues delivered 0 live
404
+ events. If you call `notify` only from the serving loop, 0.5.0 and 0.5.1 were
405
+ correct for you.
406
+
407
+ 0.5.3 lets 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
+ ## Not in 0.5.3
399
433
 
400
434
  Stated so nobody waits for them: no async API, no caching beyond ETag
401
435
  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.1"
7
+ version = "0.5.3"
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.1"
40
+ __version__ = "0.5.3"
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,73 @@ 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
+ async def preflight(request: web.Request) -> web.Response:
168
+ """Answer the `OPTIONS` a browser sends before a credentialed request.
169
+
170
+ **Why this route has to exist, and why nothing in the specification said
171
+ so.** `Authorization` is not a CORS-safelisted request header, so a
172
+ cross-origin request carrying one is preceded by a preflight. This gateway
173
+ routed no `OPTIONS` at all, so aiohttp answered `405` with
174
+ `Allow: GET,HEAD,POST` --- and the browser stopped there, never making the
175
+ request. Two consequences, both measured against 0.5.2 from a page on
176
+ another origin:
177
+
178
+ * N.6 was unreachable from a browser for **every** access class, because a
179
+ command always carries a credential. Reclassifying a lane cannot help:
180
+ the preflight is triggered by the request, not by what the resource is.
181
+ * A `public-cacheable` GET that answers `200` with
182
+ `Access-Control-Allow-Origin: *` failed outright if the client
183
+ volunteered a credential it did not need.
184
+
185
+ Both are fixed here rather than in Appendix N, because nothing in N
186
+ forbade any of it --- a conforming endpoint could simply omit it, which is
187
+ the separate gap filed for the specification: a binding whose flagship
188
+ client is a browser should have to *state* its preflight behaviour.
189
+
190
+ `Allow-Credentials` is deliberately absent. It is the header that would
191
+ invite cookies and HTTP auth to ride along ambiently, and this gateway
192
+ wants the opposite: a credential the page holds and chooses to attach.
193
+
194
+ A bare `OPTIONS` with no `Origin` is not a preflight; it gets the same
195
+ `Allow` list as a courtesy, which also keeps `ALLOW_METHODS` the single
196
+ place that answer is written.
197
+ """
198
+ origin = request.headers.get("Origin")
199
+ headers = {"Allow": ALLOW_METHODS}
200
+ if origin:
201
+ headers.update({
202
+ "Access-Control-Allow-Origin": origin,
203
+ "Access-Control-Allow-Methods": ALLOW_METHODS,
204
+ # Echo what was asked for when it was asked, so a header this
205
+ # gateway has not thought of is refused rather than blanket-allowed,
206
+ # and fall back to the list above for a preflight that names none.
207
+ "Access-Control-Allow-Headers":
208
+ request.headers.get("Access-Control-Request-Headers")
209
+ or ALLOW_HEADERS,
210
+ "Access-Control-Max-Age": str(PREFLIGHT_MAX_AGE),
211
+ "Access-Control-Expose-Headers": EXPOSE_HEADERS,
212
+ # The answer depends on all three, so a cache must not reuse it
213
+ # across different ones.
214
+ "Vary": "Origin, Access-Control-Request-Method, "
215
+ "Access-Control-Request-Headers",
216
+ })
217
+ return web.Response(status=204, headers=headers)
125
218
 
126
219
 
127
220
  def _authorized(request: web.Request, token: str | None) -> bool:
@@ -145,7 +238,9 @@ def _json_response(request: web.Request, doc: Any, access: str,
145
238
  doc_ttl: int | None = None) -> web.Response:
146
239
  """One JSON resource, with its ETag, its cache class, and 304 handling."""
147
240
  etag = _etag(doc)
148
- headers = {"ETag": etag, **_cache_headers(access, doc_ttl),
241
+ headers = {"ETag": etag,
242
+ **_cache_headers(access, doc_ttl,
243
+ request.headers.get("Origin")),
149
244
  **(extra or {})}
150
245
  if request.headers.get("If-None-Match", "") == etag:
151
246
  return web.Response(status=304, headers=headers)
@@ -473,7 +568,27 @@ def _written(request: web.Request, doc: Any, created: bool,
473
568
  if not created else web.Response(
474
569
  body=_body(doc), status=201, content_type="application/json",
475
570
  headers={"ETag": _etag(doc),
476
- **_cache_headers(gateway.OPERATOR), **extra})
571
+ **_cache_headers(gateway.OPERATOR, None,
572
+ request.headers.get("Origin")),
573
+ **extra})
574
+
575
+
576
+ def _running_loop():
577
+ """The loop running in *this* thread, or None. Never raises."""
578
+ try:
579
+ return asyncio.get_running_loop()
580
+ except RuntimeError:
581
+ return None
582
+
583
+
584
+ async def _remember_loop(app: web.Application) -> None:
585
+ """Record the serving loop, so `notify` can reach it from a thread.
586
+
587
+ An `on_startup` signal rather than `make_app`, because `make_app` is
588
+ ordinarily called before there is a loop to record -- an embedder builds
589
+ the app, then hands it to `run_app`.
590
+ """
591
+ app["loop"] = asyncio.get_running_loop()
477
592
 
478
593
 
479
594
  def notify(app: web.Application, topic: str, event: dict) -> None:
@@ -500,16 +615,38 @@ def notify(app: web.Application, topic: str, event: dict) -> None:
500
615
  "topic": topic,
501
616
  "key": key, "data": data})
502
617
 
503
- Synchronous on purpose: it only puts onto unbounded queues, so there is
504
- nothing to await, and a backend reading a DDS bus from a thread can call
505
- it without arranging a loop.
618
+ Synchronous on purpose: there is nothing to await, so a backend reading a
619
+ DDS bus from a thread can call it without arranging a loop. **That use is
620
+ now actually safe**, which it was not in 0.5.0 and 0.5.1: this put onto
621
+ the queue directly, and `asyncio.Queue.put_nowait` from a foreign thread
622
+ enqueues without waking the selector, so a parked subscriber resumed only
623
+ when the loop happened to wake for some other reason. On a busy endpoint
624
+ that is immediate and the bug invisible; on an idle one the events sat
625
+ unseen. The docstring promised the thread case before the code delivered
626
+ it, which is the worse half of the defect.
506
627
 
507
628
  Subscriptions only stay open to be woken when `live_subscriptions` is on
508
629
  --- see `make_app`. Calling this against an endpoint serving a finished
509
630
  recording is harmless and reaches nobody.
510
631
  """
632
+ loop = app.get("loop")
511
633
  for q in list(app["subs"].get(topic, ())):
512
- q.put_nowait(event)
634
+ if loop is None or loop is _running_loop():
635
+ q.put_nowait(event)
636
+ else:
637
+ # `asyncio.Queue.put_nowait` is not thread-safe, and the part
638
+ # that bites is not the enqueue -- it is the wakeup. It resolves
639
+ # the parked getter's future directly, and resolving a future
640
+ # from a foreign thread schedules the callback without waking
641
+ # the selector, so the subscriber's `await q.get()` resumes only
642
+ # when the loop next happens to wake for some other reason. On a
643
+ # busy endpoint that is immediate and the bug is invisible; on an
644
+ # idle one the events sit in the queue.
645
+ #
646
+ # Measured on 2026-10-17: a bridge pumping 202 samples into two
647
+ # registered subscriber queues delivered 0 live events, while the
648
+ # same call from the loop thread delivered every one.
649
+ loop.call_soon_threadsafe(q.put_nowait, event)
513
650
 
514
651
 
515
652
  async def _broadcast(request: web.Request, topic: str, event: dict) -> None:
@@ -701,7 +838,9 @@ async def delete_instance(request: web.Request) -> web.Response:
701
838
  {"event": "dispose", "topic": topic, "key": key,
702
839
  "reason": reason})
703
840
  return web.Response(status=204,
704
- headers=_cache_headers(gateway.OPERATOR))
841
+ headers=_cache_headers(
842
+ gateway.OPERATOR, None,
843
+ request.headers.get("Origin")))
705
844
 
706
845
 
707
846
  async def put_manifest(request: web.Request) -> web.Response:
@@ -888,6 +1027,11 @@ def make_app(state: gateway.State, base: str, token: str | None,
888
1027
  else commands.Registry())
889
1028
  app["journal"] = journal
890
1029
  app["subs"] = {}
1030
+ #: The serving loop, filled in at startup. `notify` needs it to hand an
1031
+ #: event across a thread boundary safely; None until then, which is
1032
+ #: correct -- nothing can be subscribed before the app is running.
1033
+ app["loop"] = None
1034
+ app.on_startup.append(_remember_loop)
891
1035
  app["assets"] = assets
892
1036
  app.add_routes([
893
1037
  web.get("/", root),
@@ -914,5 +1058,18 @@ def make_app(state: gateway.State, base: str, token: str | None,
914
1058
  web.delete("/{scene}/{stream}/{type}/{version}/{key}",
915
1059
  delete_instance),
916
1060
  web.put(gateway.RESOLVE_PATH, put_manifest),
1061
+ # The preflight, on every path that can be asked for cross-origin
1062
+ # with a credential or a JSON body. Mirrors the routes above rather
1063
+ # than using a catch-all, so a path this gateway does not serve still
1064
+ # 404s on OPTIONS instead of promising a method nobody implements.
1065
+ web.options(f"{WELL_KNOWN}/schemas", preflight),
1066
+ web.options(f"{WELL_KNOWN}/schemas/{{type}}", preflight),
1067
+ web.options(f"{WELL_KNOWN}/search", preflight),
1068
+ web.options(f"{WELL_KNOWN}/bootstrap", preflight),
1069
+ web.options(f"{WELL_KNOWN}/resolver", preflight),
1070
+ web.options(gateway.RESOLVE_PATH, preflight),
1071
+ web.options(f"{ASSET_PATH}/{{name}}", preflight),
1072
+ web.options("/{scene}/{stream}/{type}/{version}", preflight),
1073
+ web.options("/{scene}/{stream}/{type}/{version}/{key}", preflight),
917
1074
  ])
918
1075
  return app
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spatialdds
3
- Version: 0.5.1
3
+ Version: 0.5.3
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,41 @@ 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
- ## Not in 0.5.1
422
+ 0.5.2 made `notify` safe from the thread its own docstring invited. It put
423
+ onto the subscriber queue directly, and `asyncio.Queue.put_nowait` from a
424
+ foreign thread enqueues without waking the loop — so a backend reading a bus
425
+ in a thread fed a parked subscriber nothing until the loop woke for some other
426
+ reason. Busy endpoint, invisible; idle endpoint, silent. Measured: a bridge
427
+ pumping 202 samples into two registered subscriber queues delivered 0 live
428
+ events. If you call `notify` only from the serving loop, 0.5.0 and 0.5.1 were
429
+ correct for you.
430
+
431
+ 0.5.3 lets 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
+ ## Not in 0.5.3
423
457
 
424
458
  Stated so nobody waits for them: no async API, no caching beyond ETag
425
459
  revalidation, no retry policy, no MCAP tooling beyond the journal the server
File without changes