@@ -149,6 +149,63 @@ pub(super) fn dispatch(
149149 & fuota_multicast_key ( labels) ,
150150 labels. get ( "MulticastGroupId" ) . map ( String :: as_str) ,
151151 ) ) ) ,
152+ // Wireless device/gateway <-> IoT thing. Sets ThingArn/ThingName on
153+ // the stored record so GetWirelessDevice/GetWirelessGateway reflect it.
154+ "AssociateWirelessDeviceWithThing" => Ok ( Some ( associate_thing (
155+ svc,
156+ ctx,
157+ "wireless-devices" ,
158+ labels. get ( "Id" ) . map ( String :: as_str) ,
159+ body,
160+ ) ) ) ,
161+ "DisassociateWirelessDeviceFromThing" => Ok ( Some ( disassociate_thing (
162+ svc,
163+ ctx,
164+ "wireless-devices" ,
165+ labels. get ( "Id" ) . map ( String :: as_str) ,
166+ ) ) ) ,
167+ "AssociateWirelessGatewayWithThing" => Ok ( Some ( associate_thing (
168+ svc,
169+ ctx,
170+ "wireless-gateways" ,
171+ labels. get ( "Id" ) . map ( String :: as_str) ,
172+ body,
173+ ) ) ) ,
174+ "DisassociateWirelessGatewayFromThing" => Ok ( Some ( disassociate_thing (
175+ svc,
176+ ctx,
177+ "wireless-gateways" ,
178+ labels. get ( "Id" ) . map ( String :: as_str) ,
179+ ) ) ) ,
180+
181+ // Wireless gateway <-> IoT certificate.
182+ "AssociateWirelessGatewayWithCertificate" => Ok ( Some ( associate_gateway_certificate (
183+ svc,
184+ ctx,
185+ labels. get ( "Id" ) . map ( String :: as_str) ,
186+ body,
187+ ) ) ) ,
188+ "DisassociateWirelessGatewayFromCertificate" => Ok ( Some ( disassociate_thing_field (
189+ svc,
190+ ctx,
191+ "wireless-gateways" ,
192+ labels. get ( "Id" ) . map ( String :: as_str) ,
193+ & [ "IotCertificateId" ] ,
194+ ) ) ) ,
195+ "GetWirelessGatewayCertificate" => {
196+ Ok ( Some ( get_wireless_gateway_certificate ( svc, ctx, labels) ) )
197+ }
198+
199+ // AWS-account <-> Sidewalk partner account.
200+ "AssociateAwsAccountWithPartnerAccount" => {
201+ Ok ( Some ( associate_partner_account ( svc, ctx, body) ) )
202+ }
203+ "DisassociateAwsAccountFromPartnerAccount" => Ok ( Some ( disassociate_partner_account (
204+ svc,
205+ ctx,
206+ labels. get ( "PartnerAccountId" ) . map ( String :: as_str) ,
207+ ) ) ) ,
208+
152209 "ListMulticastGroupsByFuotaTask" => {
153210 Ok ( Some ( list_multicast_groups_by_fuota_task ( svc, ctx, labels) ) )
154211 }
@@ -704,6 +761,184 @@ fn disassociate(
704761 ( ok_json ( Value :: Object ( Map :: new ( ) ) ) , true )
705762}
706763
764+ /// Set `ThingArn` (+ derived `ThingName`) on a stored wireless device/gateway
765+ /// record so the matching Get reflects the association.
766+ fn associate_thing (
767+ svc : & IotWirelessService ,
768+ ctx : & Ctx ,
769+ rtype : & str ,
770+ id : Option < & str > ,
771+ body : & Map < String , Value > ,
772+ ) -> ( AwsResponse , bool ) {
773+ let ( Some ( id) , Some ( thing_arn) ) = ( id, body. get ( "ThingArn" ) . and_then ( Value :: as_str) ) else {
774+ return ( ok_json ( Value :: Object ( Map :: new ( ) ) ) , false ) ;
775+ } ;
776+ let thing_name = thing_arn
777+ . rsplit_once ( "thing/" )
778+ . map ( |( _, n) | n)
779+ . unwrap_or ( "" ) ;
780+ let mut g = svc. state . write ( ) ;
781+ let data = g. get_or_create ( & ctx. account ) ;
782+ if let Some ( record) = data. get_resource ( rtype, id) . cloned ( ) {
783+ let mut record = record;
784+ if let Some ( obj) = record. as_object_mut ( ) {
785+ obj. insert ( "ThingArn" . to_string ( ) , json ! ( thing_arn) ) ;
786+ obj. insert ( "ThingName" . to_string ( ) , json ! ( thing_name) ) ;
787+ }
788+ data. put_resource ( rtype, id, record) ;
789+ }
790+ ( ok_json ( Value :: Object ( Map :: new ( ) ) ) , true )
791+ }
792+
793+ /// Clear `ThingArn`/`ThingName` on a wireless device/gateway record.
794+ fn disassociate_thing (
795+ svc : & IotWirelessService ,
796+ ctx : & Ctx ,
797+ rtype : & str ,
798+ id : Option < & str > ,
799+ ) -> ( AwsResponse , bool ) {
800+ disassociate_thing_field ( svc, ctx, rtype, id, & [ "ThingArn" , "ThingName" ] )
801+ }
802+
803+ /// Remove the named fields from a stored record (used by the disassociate ops).
804+ fn disassociate_thing_field (
805+ svc : & IotWirelessService ,
806+ ctx : & Ctx ,
807+ rtype : & str ,
808+ id : Option < & str > ,
809+ fields : & [ & str ] ,
810+ ) -> ( AwsResponse , bool ) {
811+ if let Some ( id) = id {
812+ let mut g = svc. state . write ( ) ;
813+ let data = g. get_or_create ( & ctx. account ) ;
814+ if let Some ( mut record) = data. get_resource ( rtype, id) . cloned ( ) {
815+ if let Some ( obj) = record. as_object_mut ( ) {
816+ for f in fields {
817+ obj. remove ( * f) ;
818+ }
819+ }
820+ data. put_resource ( rtype, id, record) ;
821+ }
822+ }
823+ ( ok_json ( Value :: Object ( Map :: new ( ) ) ) , true )
824+ }
825+
826+ /// Store `IotCertificateId` on the wireless-gateway record.
827+ fn associate_gateway_certificate (
828+ svc : & IotWirelessService ,
829+ ctx : & Ctx ,
830+ id : Option < & str > ,
831+ body : & Map < String , Value > ,
832+ ) -> ( AwsResponse , bool ) {
833+ let ( Some ( id) , Some ( cert) ) = ( id, body. get ( "IotCertificateId" ) . and_then ( Value :: as_str) ) else {
834+ return ( ok_json ( Value :: Object ( Map :: new ( ) ) ) , false ) ;
835+ } ;
836+ let mut g = svc. state . write ( ) ;
837+ let data = g. get_or_create ( & ctx. account ) ;
838+ if let Some ( mut record) = data. get_resource ( "wireless-gateways" , id) . cloned ( ) {
839+ if let Some ( obj) = record. as_object_mut ( ) {
840+ obj. insert ( "IotCertificateId" . to_string ( ) , json ! ( cert) ) ;
841+ }
842+ data. put_resource ( "wireless-gateways" , id, record) ;
843+ }
844+ ( ok_json ( json ! ( { "IotCertificateId" : cert } ) ) , true )
845+ }
846+
847+ /// GetWirelessGatewayCertificate (a Verb::Action, so not served by the generic
848+ /// engine): project the cert id stored on the gateway record.
849+ fn get_wireless_gateway_certificate (
850+ svc : & IotWirelessService ,
851+ ctx : & Ctx ,
852+ labels : & HashMap < String , String > ,
853+ ) -> ( AwsResponse , bool ) {
854+ let id = labels. get ( "Id" ) . map ( String :: as_str) . unwrap_or ( "" ) ;
855+ let g = svc. state . read ( ) ;
856+ let cert = g
857+ . get ( & ctx. account )
858+ . and_then ( |d| d. get_resource ( "wireless-gateways" , id) )
859+ . and_then ( |r| r. get ( "IotCertificateId" ) )
860+ . and_then ( Value :: as_str)
861+ . unwrap_or ( "" )
862+ . to_string ( ) ;
863+ ( ok_json ( json ! ( { "IotCertificateId" : cert } ) ) , false )
864+ }
865+
866+ /// A deterministic 64-character hex digest of an input, standing in for the
867+ /// SHA-256 fingerprint AWS reports for a Sidewalk app-server private key. Built
868+ /// from eight salted FNV-1a folds so the same key always maps to the same
869+ /// digest without pulling in a crypto dependency.
870+ fn fingerprint_hex ( input : & str ) -> String {
871+ let mut out = String :: with_capacity ( 64 ) ;
872+ for salt in 0u8 ..8 {
873+ let mut hash: u64 = 0xcbf2_9ce4_8422_2325 ;
874+ hash ^= salt as u64 ;
875+ for b in input. bytes ( ) {
876+ hash ^= b as u64 ;
877+ hash = hash. wrapping_mul ( 0x0000_0100_0000_01b3 ) ;
878+ }
879+ out. push_str ( & format ! ( "{hash:016x}" ) ) ;
880+ }
881+ out
882+ }
883+
884+ /// Persist a Sidewalk partner-account association keyed by its AmazonId so
885+ /// GetPartnerAccount / ListPartnerAccounts (generic reads) reflect it.
886+ fn associate_partner_account (
887+ svc : & IotWirelessService ,
888+ ctx : & Ctx ,
889+ body : & Map < String , Value > ,
890+ ) -> ( AwsResponse , bool ) {
891+ let sidewalk = body. get ( "Sidewalk" ) . cloned ( ) . unwrap_or ( json ! ( { } ) ) ;
892+ let amazon_id = sidewalk
893+ . get ( "AmazonId" )
894+ . and_then ( Value :: as_str)
895+ . unwrap_or ( "" )
896+ . to_string ( ) ;
897+ // AWS returns a fingerprint of the app-server private key, never the key
898+ // itself. Derive a deterministic 64-hex digest so Get/List round-trip.
899+ let private_key = sidewalk
900+ . get ( "AppServerPrivateKey" )
901+ . and_then ( Value :: as_str)
902+ . unwrap_or ( "" ) ;
903+ let fingerprint = fingerprint_hex ( private_key) ;
904+ // The SidewalkAccountInfoWithFingerprint the reads project carries the
905+ // AmazonId + Fingerprint (never the private key).
906+ let sidewalk_with_fp = json ! ( {
907+ "AmazonId" : amazon_id,
908+ "Fingerprint" : fingerprint,
909+ } ) ;
910+ // Store the projected list-element members (AmazonId/Fingerprint/Arn) at the
911+ // top level so the generic ListPartnerAccounts projection finds them, and
912+ // keep the nested Sidewalk object for GetPartnerAccount.
913+ let record = json ! ( {
914+ "AmazonId" : amazon_id,
915+ "Fingerprint" : fingerprint,
916+ "Sidewalk" : sidewalk_with_fp,
917+ "PartnerAccountId" : amazon_id,
918+ "PartnerType" : "Sidewalk" ,
919+ } ) ;
920+ let mut g = svc. state . write ( ) ;
921+ let data = g. get_or_create ( & ctx. account ) ;
922+ data. put_resource ( "partner-accounts" , & amazon_id, record. clone ( ) ) ;
923+ ( ok_json ( record) , true )
924+ }
925+
926+ /// Remove a partner-account association.
927+ fn disassociate_partner_account (
928+ svc : & IotWirelessService ,
929+ ctx : & Ctx ,
930+ id : Option < & str > ,
931+ ) -> ( AwsResponse , bool ) {
932+ if let Some ( id) = id {
933+ let mut g = svc. state . write ( ) ;
934+ let data = g. get_or_create ( & ctx. account ) ;
935+ data. resources
936+ . get_mut ( "partner-accounts" )
937+ . map ( |m| m. remove ( id) ) ;
938+ }
939+ ( ok_json ( Value :: Object ( Map :: new ( ) ) ) , true )
940+ }
941+
707942/// `ListMulticastGroupsByFuotaTask`: read the FUOTA-task -> multicast-group
708943/// edges persisted by `AssociateMulticastGroupWithFuotaTask`.
709944fn list_multicast_groups_by_fuota_task (
0 commit comments