Compare commits

...
Author SHA1 Message Date
Edison Jwa 609c4cee8c docs(security): regenerate license inventories
Cargo inventory: pick up chanora_resolver bump from 0.1.0 to
0.2.0-beta.1 so it matches the workspace; also adds a trailing newline
so 'cargo about generate' is idempotent in CI license-drift checks.

Flutter inventory: pick up flutter_local_notifications (+ platform
interfaces) and timezone pulled in by the prior notification
permission work.
2026-06-09 19:29:04 +09:00
Edison Jwa c7e51c2e48 fix(ios-audio): keep session active when already-in-channel
The 'already in channel' server response (code 0x0302) is treated as a
successful join by _onJoinChannel: the user stays in the channel and
local state is updated to reflect the joined target. But the underlying
voiceJoin call still raises BridgeError_ServerRejected, which the
joinVoiceChannelWithIosAudioSession helper used to interpret as a join
failure and deactivate the iOS audio session. Result: the UI shows the
user as joined while the audio session is dead and capture/playback
remain silent.

Add an isJoinSuccess predicate to the ordering helper. When the
predicate matches, the helper rethrows (so the caller can still run its
success-on-already-joined branch) without deactivating the session.
Wire _onJoinChannel to pass _isAlreadyInChannel as the predicate so the
0x0302 path keeps the session active.

Adds two regression tests covering the success-on-rethrow and the
predicate-false-still-deactivates paths.
2026-06-09 19:28:54 +09:00
Edison Jwa 703f44d731 docs(ios-audio): align activation lifecycle comments 2026-06-09 02:16:20 +09:00
Edison Jwa 7be3934c86 fix(ios-audio): activate session before voice joins 2026-06-09 02:16:08 +09:00
Edison Jwa f224347836 fix(ios-audio): add voice join session coordinator 2026-06-09 02:15:55 +09:00
7 changed files with 374 additions and 8 deletions
@@ -46,7 +46,8 @@ import AVFoundation
// //
// VoIP configuration is engaged on voice-channel join via the // VoIP configuration is engaged on voice-channel join via the
// `chanora/ios_audio_session` MethodChannel, driven from Dart // `chanora/ios_audio_session` MethodChannel, driven from Dart
// by the BridgeEvent::AudioStarted / AudioStopped lifecycle. // before `voiceJoin` starts VoiceProcessingIO and again as an
// idempotent guard on the AudioStarted lifecycle.
do { do {
try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default) try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default)
logAudioSessionState(context: "launch-ambient") logAudioSessionState(context: "launch-ambient")
@@ -79,8 +80,8 @@ import AVFoundation
} }
/// Activate the VoIP audio session. Called from Dart via the /// Activate the VoIP audio session. Called from Dart via the
/// `chanora/ios_audio_session` channel when a voice channel join /// `chanora/ios_audio_session` channel before a voice channel join
/// reaches the `BridgeEvent::AudioStarted` stage. Configures /// starts VoiceProcessingIO. Configures
/// .playAndRecord + .voiceChat with .mixWithOthers so other apps /// .playAndRecord + .voiceChat with .mixWithOthers so other apps
/// (Spotify, podcasts) can keep playing alongside the voice /// (Spotify, podcasts) can keep playing alongside the voice
/// channel matching the Telegram group-call UX. Idempotent: /// channel matching the Telegram group-call UX. Idempotent:
+13 -1
View File
@@ -34,6 +34,7 @@ import 'services/prefetch_debouncer.dart';
import 'services/snapshot_state_mapper.dart'; import 'services/snapshot_state_mapper.dart';
import 'services/ts3_server_link.dart'; import 'services/ts3_server_link.dart';
import 'services/ui_preferences_service.dart'; import 'services/ui_preferences_service.dart';
import 'services/voice_join_ordering.dart';
import 'src/rust/api.dart' as rust; import 'src/rust/api.dart' as rust;
import 'src/rust/frb_generated.dart'; import 'src/rust/frb_generated.dart';
import 'src/rust/lib.dart' as rust_err; import 'src/rust/lib.dart' as rust_err;
@@ -1301,7 +1302,18 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
}); });
} }
} }
await rust.voiceJoin(channelId: ch.id, password: password ?? ''); await joinVoiceChannelWithIosAudioSession(
channelId: ch.id,
password: password ?? '',
voiceJoin: rust.voiceJoin,
activateIosAudioSession: iosAudioSessionController.activate,
deactivateIosAudioSession: iosAudioSessionController.deactivate,
// Server says we are already in the target channel: the user is
// still joined to a voice channel, so the iOS audio session must
// stay active. The catch below converts this rethrow into the
// success-on-already-joined branch.
isJoinSuccess: _isAlreadyInChannel,
);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_currentVoiceChannelId = ch.id; _currentVoiceChannelId = ch.id;
@@ -10,9 +10,9 @@ const iosAudioSessionChannelName = 'chanora/ios_audio_session';
/// launch and leaves it inactive. The session is only switched to /// launch and leaves it inactive. The session is only switched to
/// `.playAndRecord` + `.voiceChat` (with `.mixWithOthers`) while a /// `.playAndRecord` + `.voiceChat` (with `.mixWithOthers`) while a
/// voice channel is actually active. This controller is the Dart /// voice channel is actually active. This controller is the Dart
/// side of that contract — call [activate] when the Rust engine /// side of that contract — call [activate] before the Rust engine
/// emits `BridgeEvent::AudioStarted` and [deactivate] on /// starts VoiceProcessingIO and [deactivate] on
/// `BridgeEvent::AudioStopped`. /// `BridgeEvent::AudioStopped` or failed joins.
/// ///
/// On non-iOS platforms both methods are no-ops; the platforms /// On non-iOS platforms both methods are no-ops; the platforms
/// handle their own session lifecycle elsewhere (Android via /// handle their own session lifecycle elsewhere (Android via
@@ -0,0 +1,36 @@
typedef VoiceJoinCallback = Future<void> Function({
required BigInt channelId,
required String password,
});
typedef IosVoiceSessionActivation = Future<void> Function();
typedef IosVoiceSessionDeactivation = Future<void> Function();
/// Predicate used to recognise `voiceJoin` errors that the caller treats as a
/// successful join outcome (e.g. the server replied "already in channel").
///
/// When this returns `true` for a thrown error, the iOS audio session is kept
/// active because the user is still considered joined to the channel. The
/// error is still rethrown so the caller can run its success-on-already-joined
/// branch and update local state.
typedef VoiceJoinSuccessPredicate = bool Function(Object error);
Future<void> joinVoiceChannelWithIosAudioSession({
required BigInt channelId,
required String password,
required VoiceJoinCallback voiceJoin,
required IosVoiceSessionActivation activateIosAudioSession,
required IosVoiceSessionDeactivation deactivateIosAudioSession,
VoiceJoinSuccessPredicate? isJoinSuccess,
}) async {
await activateIosAudioSession();
try {
await voiceJoin(channelId: channelId, password: password);
} catch (e) {
if (isJoinSuccess != null && isJoinSuccess(e)) {
rethrow;
}
await deactivateIosAudioSession();
rethrow;
}
}
@@ -0,0 +1,123 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/services/voice_join_ordering.dart';
void main() {
group('joinVoiceChannelWithIosAudioSession', () {
test('activates the iOS audio session before Rust voiceJoin', () async {
final calls = <String>[];
await joinVoiceChannelWithIosAudioSession(
channelId: BigInt.from(42),
password: 'secret',
activateIosAudioSession: () async {
calls.add('activateIosAudioSession');
},
deactivateIosAudioSession: () async {
calls.add('deactivateIosAudioSession');
},
voiceJoin: ({required channelId, required password}) async {
expect(channelId, BigInt.from(42));
expect(password, 'secret');
calls.add('voiceJoin');
},
);
expect(calls, ['activateIosAudioSession', 'voiceJoin']);
});
test('deactivates the iOS audio session when Rust voiceJoin fails',
() async {
final calls = <String>[];
await expectLater(
joinVoiceChannelWithIosAudioSession(
channelId: BigInt.from(42),
password: '',
activateIosAudioSession: () async {
calls.add('activateIosAudioSession');
},
deactivateIosAudioSession: () async {
calls.add('deactivateIosAudioSession');
},
voiceJoin: ({required channelId, required password}) async {
calls.add('voiceJoin');
throw StateError('join rejected');
},
),
throwsStateError,
);
expect(calls, [
'activateIosAudioSession',
'voiceJoin',
'deactivateIosAudioSession',
]);
});
test(
'keeps the iOS audio session active when voiceJoin throws but the '
'error is recognised as already-in-channel (treated as success); '
'still rethrows so the caller runs its success-on-already-joined branch',
() async {
final calls = <String>[];
await expectLater(
joinVoiceChannelWithIosAudioSession(
channelId: BigInt.from(42),
password: '',
activateIosAudioSession: () async {
calls.add('activateIosAudioSession');
},
deactivateIosAudioSession: () async {
calls.add('deactivateIosAudioSession');
},
voiceJoin: ({required channelId, required password}) async {
calls.add('voiceJoin');
throw _FakeAlreadyInChannel();
},
isJoinSuccess: (error) => error is _FakeAlreadyInChannel,
),
throwsA(isA<_FakeAlreadyInChannel>()),
);
expect(calls, ['activateIosAudioSession', 'voiceJoin']);
},
);
test(
'deactivates the iOS audio session when isJoinSuccess returns false '
'for a non-success error',
() async {
final calls = <String>[];
await expectLater(
joinVoiceChannelWithIosAudioSession(
channelId: BigInt.from(42),
password: '',
activateIosAudioSession: () async {
calls.add('activateIosAudioSession');
},
deactivateIosAudioSession: () async {
calls.add('deactivateIosAudioSession');
},
voiceJoin: ({required channelId, required password}) async {
calls.add('voiceJoin');
throw StateError('join rejected');
},
isJoinSuccess: (error) => error is _FakeAlreadyInChannel,
),
throwsStateError,
);
expect(calls, [
'activateIosAudioSession',
'voiceJoin',
'deactivateIosAudioSession',
]);
},
);
});
}
class _FakeAlreadyInChannel implements Exception {}
+193
View File
@@ -49,6 +49,11 @@ terms.
| `flutter` | 0.0.0 | sdk | yes | | `flutter` | 0.0.0 | sdk | yes |
| `flutter_foreground_task` | 9.2.2 | hosted | yes | | `flutter_foreground_task` | 9.2.2 | hosted | yes |
| `flutter_lints` | 6.0.0 | hosted | yes | | `flutter_lints` | 6.0.0 | hosted | yes |
| `flutter_local_notifications` | 22.0.0 | hosted | yes |
| `flutter_local_notifications_linux` | 8.0.1 | hosted | yes |
| `flutter_local_notifications_platform_interface` | 12.0.0 | hosted | yes |
| `flutter_local_notifications_web` | 1.0.0 | hosted | yes |
| `flutter_local_notifications_windows` | 3.1.0 | hosted | yes |
| `flutter_localizations` | 0.0.0 | sdk | yes | | `flutter_localizations` | 0.0.0 | sdk | yes |
| `flutter_rust_bridge` | 2.12.0 | hosted | yes | | `flutter_rust_bridge` | 2.12.0 | hosted | yes |
| `flutter_test` | 0.0.0 | sdk | yes | | `flutter_test` | 0.0.0 | sdk | yes |
@@ -117,6 +122,7 @@ terms.
| `string_scanner` | 1.4.1 | hosted | yes | | `string_scanner` | 1.4.1 | hosted | yes |
| `term_glyph` | 1.2.2 | hosted | yes | | `term_glyph` | 1.2.2 | hosted | yes |
| `test_api` | 0.7.11 | hosted | yes | | `test_api` | 0.7.11 | hosted | yes |
| `timezone` | 0.11.0 | hosted | yes |
| `typed_data` | 1.4.0 | hosted | yes | | `typed_data` | 1.4.0 | hosted | yes |
| `url_launcher` | 6.3.2 | hosted | yes | | `url_launcher` | 6.3.2 | hosted | yes |
| `url_launcher_android` | 6.3.30 | hosted | yes | | `url_launcher_android` | 6.3.30 | hosted | yes |
@@ -1869,6 +1875,166 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
``` ```
### flutter_local_notifications 22.0.0
```
Copyright 2018 Michael Bui. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
### flutter_local_notifications_linux 8.0.1
```
Copyright 2018 Michael Bui. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
### flutter_local_notifications_platform_interface 12.0.0
```
Copyright 2020 Michael Bui. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
### flutter_local_notifications_web 1.0.0
```
Copyright 2020 Michael Bui. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
### flutter_local_notifications_windows 3.1.0
```
Copyright 2024 Michael Bui. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
### flutter_localizations 0.0.0 ### flutter_localizations 0.0.0
``` ```
@@ -4673,6 +4839,33 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
``` ```
### timezone 0.11.0
```
Copyright (c) 2014, timezone project authors.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
### typed_data 1.4.0 ### typed_data 1.4.0
``` ```
+2 -1
View File
@@ -291,7 +291,7 @@ those terms.
| chanora_diagnostics | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> | | chanora_diagnostics | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> |
| chanora_prefetch | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> | | chanora_prefetch | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> |
| chanora_protocol | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> | | chanora_protocol | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> |
| chanora_resolver | 0.1.0 | `Apache License 2.0` | — | | chanora_resolver | 0.2.0-beta.1 | `Apache License 2.0` | — |
| chanora_state | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> | | chanora_state | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> |
| chanora_storage | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> | | chanora_storage | 0.2.0-beta.1 | `Apache License 2.0` | <https://github.com/anomalyco/opencode> |
| alsa | 0.11.0 | `Apache License 2.0` | <https://github.com/diwic/alsa-rs> | | alsa | 0.11.0 | `Apache License 2.0` | <https://github.com/diwic/alsa-rs> |
@@ -10967,3 +10967,4 @@ cargo about generate --output-file docs/security/license-inventory.html about.hb
This artefact supports the DEC-012 legal review handoff at This artefact supports the DEC-012 legal review handoff at
`docs/governance/legal-review-readiness.md`. `docs/governance/legal-review-readiness.md`.