diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cbc6f485..abce6688 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: jobs: - build: + build-docs: runs-on: ubuntu-latest defaults: run: @@ -35,14 +35,14 @@ jobs: SFTP_HOST: ${{ secrets.SFTP_HOST }} SFTP_USERNAME: ${{ secrets.SFTP_USERNAME }} SFTP_KEY: ${{ secrets.SFTP_KEY }} - SFTP_KNOWN_HOSTS: ${{secrets.SFTP_KNOWN_HOSTS}} + SFTP_KNOWN_HOSTS: ${{ secrets.SFTP_KNOWN_HOSTS }} run: | echo "$SFTP_KEY" > sftp_key chmod 600 sftp_key echo "$SFTP_KNOWN_HOSTS" > known_hosts chmod 600 known_hosts rsync -avz --delete -e "ssh -i sftp_key -o UserKnownHostsFile=known_hosts" dist/ $SFTP_USERNAME@$SFTP_HOST:/var/www/www.setonix - doc: + build-doc-api: runs-on: ubuntu-24.04 defaults: run: @@ -54,12 +54,10 @@ jobs: with: flutter-version-file: app/pubspec.yaml - name: Install dependencies - run: | - flutter pub get + run: flutter pub get - name: Generate documentation - run: | - dart doc - - name: Deploy to SFTP + run: dart doc + - name: Deploy API docs via SFTP if: github.ref == 'refs/heads/develop' env: SFTP_HOST: ${{ secrets.SFTP_HOST }} @@ -72,7 +70,7 @@ jobs: echo "$SFTP_KNOWN_HOSTS" > known_hosts chmod 600 known_hosts rsync -avz --delete -e "ssh -i sftp_key -o UserKnownHostsFile=known_hosts" doc/api/ $SFTP_USERNAME@$SFTP_HOST:/var/www/api.setonix - web: + build-web: runs-on: ubuntu-24.04 defaults: run: @@ -84,9 +82,8 @@ jobs: with: flutter-version-file: app/pubspec.yaml - name: Install dependencies - run: | - flutter pub get - - name: Generate + run: flutter pub get + - name: Generate code run: | cd ../tools dart pub get @@ -104,16 +101,60 @@ jobs: echo "WEB_DIR=web" >> $GITHUB_ENV - name: Build run: flutter build web --wasm --release --no-web-resources-cdn --dart-define=flavor=$SETONIX_FLAVOR - - name: Deploy to SFTP + - name: Deploy Flutter web via SFTP if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' env: SFTP_HOST: ${{ secrets.SFTP_HOST }} SFTP_USERNAME: ${{ secrets.SFTP_USERNAME }} SFTP_KEY: ${{ secrets.SFTP_KEY }} - SFTP_KNOWN_HOSTS: ${{secrets.SFTP_KNOWN_HOSTS}} + SFTP_KNOWN_HOSTS: ${{ secrets.SFTP_KNOWN_HOSTS }} run: | echo "$SFTP_KEY" > sftp_key chmod 600 sftp_key echo "$SFTP_KNOWN_HOSTS" > known_hosts chmod 600 known_hosts rsync -avz --delete -e "ssh -i sftp_key -o UserKnownHostsFile=known_hosts" build/web/ $SFTP_USERNAME@$SFTP_HOST:/var/www/$WEB_DIR.setonix + build-servers: + runs-on: ubuntu-latest + defaults: + run: + working-directory: servers + steps: + - uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + package_json_file: docs/package.json + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: "pnpm" + cache-dependency-path: docs/pnpm-lock.yaml + - name: Install dependencies + run: pnpm install + - name: Build + run: pnpm build + - name: Setup SSH Agent + if: github.ref == 'refs/heads/develop' + uses: webfactory/ssh-agent@v0.9.1 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Add SSH Known Hosts + if: github.ref == 'refs/heads/develop' + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts + + - name: Deploy SSR build via rsync + if: github.ref == 'refs/heads/develop' + run: | + rsync -avz --delete ./dist/ ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:/var/www/servers.setonix + + - name: Restart SSR application + if: github.ref == 'refs/heads/develop' + run: | + ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} << 'EOF' + pm2 stop astro-servers || true + PORT=${{ secrets.SERVERS_PORT }} pm2 start dist/server/entry.mjs --name astro-servers --update-env + EOF diff --git a/api/lib/helpers.dart b/api/lib/helpers.dart new file mode 100644 index 00000000..165c8c85 --- /dev/null +++ b/api/lib/helpers.dart @@ -0,0 +1 @@ +export 'src/helpers/crypto.dart'; diff --git a/api/lib/services.dart b/api/lib/services.dart index 6b73810f..469c9a86 100644 --- a/api/lib/services.dart +++ b/api/lib/services.dart @@ -1,2 +1,4 @@ export 'src/services/asset.dart'; +export 'src/services/challenge.dart'; export 'src/services/network.dart'; +export 'src/services/user.dart'; diff --git a/api/lib/setonix_api.dart b/api/lib/setonix_api.dart index c4f2287c..9f87654b 100644 --- a/api/lib/setonix_api.dart +++ b/api/lib/setonix_api.dart @@ -1,6 +1,7 @@ /// The Linwood Setonix API library; +export 'helpers.dart'; export 'models.dart'; export 'event.dart'; export 'services.dart'; diff --git a/api/lib/src/event/client.dart b/api/lib/src/event/client.dart index 051c7171..94621919 100644 --- a/api/lib/src/event/client.dart +++ b/api/lib/src/event/client.dart @@ -157,3 +157,31 @@ final class ModeChangeRequest extends ClientWorldEvent ModeChangeRequest(this.location); ModeChangeRequest.plain() : location = null; } + +@MappableClass(includeCustomMappers: [Base64Uint8ListHook()]) +final class AuthenticateRequest extends ClientWorldEvent + with AuthenticateRequestMappable { + final Uint8List signature; + final Uint8List publicKey; + + static final _generator = Ed25519(); + + AuthenticateRequest(this.signature, this.publicKey); + + static Future build( + AuthenticatedRequested request, SetonixAccount account) async { + final challenge = request.challenge; + final keyPair = account.keyPair; + final signature = await _generator.sign(challenge, keyPair: keyPair); + return AuthenticateRequest( + Uint8List.fromList(signature.bytes), + account.publicKey, + ); + } + + Future verify(Uint8List challenge) => _generator.verify( + challenge, + signature: Signature(signature, + publicKey: SimplePublicKey(publicKey, type: KeyPairType.ed25519)), + ); +} diff --git a/api/lib/src/event/event.dart b/api/lib/src/event/event.dart index fc9a45d5..fff4ce7a 100644 --- a/api/lib/src/event/event.dart +++ b/api/lib/src/event/event.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:dart_mappable/dart_mappable.dart'; import 'package:networker/networker.dart'; import 'package:setonix_api/event.dart'; diff --git a/api/lib/src/event/event.mapper.dart b/api/lib/src/event/event.mapper.dart index 75873eec..e1933ce9 100644 --- a/api/lib/src/event/event.mapper.dart +++ b/api/lib/src/event/event.mapper.dart @@ -135,6 +135,7 @@ class ServerWorldEventMapper extends SubClassMapperBase { DialogsClosedMapper.ensureInitialized(); ImagesUpdatedMapper.ensureInitialized(); ServerStateUpdatedMapper.ensureInitialized(); + AuthenticatedRequestedMapper.ensureInitialized(); HybridWorldEventMapper.ensureInitialized(); } return _instance!; @@ -1774,6 +1775,139 @@ class _ServerStateUpdatedCopyWithImpl<$R, $Out> _ServerStateUpdatedCopyWithImpl<$R2, $Out2>($value, $cast, t); } +class AuthenticatedRequestedMapper + extends SubClassMapperBase { + AuthenticatedRequestedMapper._(); + + static AuthenticatedRequestedMapper? _instance; + static AuthenticatedRequestedMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = AuthenticatedRequestedMapper._()); + ServerWorldEventMapper.ensureInitialized().addSubMapper(_instance!); + MapperContainer.globals.useAll([Base64Uint8ListHook()]); + } + return _instance!; + } + + @override + final String id = 'AuthenticatedRequested'; + + static Uint8List _$challenge(AuthenticatedRequested v) => v.challenge; + static const Field _f$challenge = + Field('challenge', _$challenge); + static bool _$isRequired(AuthenticatedRequested v) => v.isRequired; + static const Field _f$isRequired = + Field('isRequired', _$isRequired, opt: true, def: true); + + @override + final MappableFields fields = const { + #challenge: _f$challenge, + #isRequired: _f$isRequired, + }; + + @override + final String discriminatorKey = 'type'; + @override + final dynamic discriminatorValue = 'AuthenticatedRequested'; + @override + late final ClassMapperBase superMapper = + ServerWorldEventMapper.ensureInitialized(); + + static AuthenticatedRequested _instantiate(DecodingData data) { + return AuthenticatedRequested(data.dec(_f$challenge), + isRequired: data.dec(_f$isRequired)); + } + + @override + final Function instantiate = _instantiate; + + static AuthenticatedRequested fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static AuthenticatedRequested fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin AuthenticatedRequestedMappable { + String toJson() { + return AuthenticatedRequestedMapper.ensureInitialized() + .encodeJson(this as AuthenticatedRequested); + } + + Map toMap() { + return AuthenticatedRequestedMapper.ensureInitialized() + .encodeMap(this as AuthenticatedRequested); + } + + AuthenticatedRequestedCopyWith + get copyWith => _AuthenticatedRequestedCopyWithImpl< + AuthenticatedRequested, AuthenticatedRequested>( + this as AuthenticatedRequested, $identity, $identity); + @override + String toString() { + return AuthenticatedRequestedMapper.ensureInitialized() + .stringifyValue(this as AuthenticatedRequested); + } + + @override + bool operator ==(Object other) { + return AuthenticatedRequestedMapper.ensureInitialized() + .equalsValue(this as AuthenticatedRequested, other); + } + + @override + int get hashCode { + return AuthenticatedRequestedMapper.ensureInitialized() + .hashValue(this as AuthenticatedRequested); + } +} + +extension AuthenticatedRequestedValueCopy<$R, $Out> + on ObjectCopyWith<$R, AuthenticatedRequested, $Out> { + AuthenticatedRequestedCopyWith<$R, AuthenticatedRequested, $Out> + get $asAuthenticatedRequested => $base.as((v, t, t2) => + _AuthenticatedRequestedCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class AuthenticatedRequestedCopyWith< + $R, + $In extends AuthenticatedRequested, + $Out> implements ServerWorldEventCopyWith<$R, $In, $Out> { + @override + $R call({Uint8List? challenge, bool? isRequired}); + AuthenticatedRequestedCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t); +} + +class _AuthenticatedRequestedCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, AuthenticatedRequested, $Out> + implements + AuthenticatedRequestedCopyWith<$R, AuthenticatedRequested, $Out> { + _AuthenticatedRequestedCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + AuthenticatedRequestedMapper.ensureInitialized(); + @override + $R call({Uint8List? challenge, bool? isRequired}) => + $apply(FieldCopyWithData({ + if (challenge != null) #challenge: challenge, + if (isRequired != null) #isRequired: isRequired + })); + @override + AuthenticatedRequested $make(CopyWithData data) => + AuthenticatedRequested(data.get(#challenge, or: $value.challenge), + isRequired: data.get(#isRequired, or: $value.isRequired)); + + @override + AuthenticatedRequestedCopyWith<$R2, AuthenticatedRequested, $Out2> + $chain<$R2, $Out2>(Then<$Out2, $R2> t) => + _AuthenticatedRequestedCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + class ClientWorldEventMapper extends SubClassMapperBase { ClientWorldEventMapper._(); @@ -1794,6 +1928,7 @@ class ClientWorldEventMapper extends SubClassMapperBase { DialogCloseRequestMapper.ensureInitialized(); ImagesRequestMapper.ensureInitialized(); ModeChangeRequestMapper.ensureInitialized(); + AuthenticateRequestMapper.ensureInitialized(); HybridWorldEventMapper.ensureInitialized(); } return _instance!; @@ -3368,6 +3503,134 @@ class _ModeChangeRequestCopyWithImpl<$R, $Out> _ModeChangeRequestCopyWithImpl<$R2, $Out2>($value, $cast, t); } +class AuthenticateRequestMapper + extends SubClassMapperBase { + AuthenticateRequestMapper._(); + + static AuthenticateRequestMapper? _instance; + static AuthenticateRequestMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = AuthenticateRequestMapper._()); + ClientWorldEventMapper.ensureInitialized().addSubMapper(_instance!); + MapperContainer.globals.useAll([Base64Uint8ListHook()]); + } + return _instance!; + } + + @override + final String id = 'AuthenticateRequest'; + + static Uint8List _$signature(AuthenticateRequest v) => v.signature; + static const Field _f$signature = + Field('signature', _$signature); + static Uint8List _$publicKey(AuthenticateRequest v) => v.publicKey; + static const Field _f$publicKey = + Field('publicKey', _$publicKey); + + @override + final MappableFields fields = const { + #signature: _f$signature, + #publicKey: _f$publicKey, + }; + + @override + final String discriminatorKey = 'type'; + @override + final dynamic discriminatorValue = 'AuthenticateRequest'; + @override + late final ClassMapperBase superMapper = + ClientWorldEventMapper.ensureInitialized(); + + static AuthenticateRequest _instantiate(DecodingData data) { + return AuthenticateRequest(data.dec(_f$signature), data.dec(_f$publicKey)); + } + + @override + final Function instantiate = _instantiate; + + static AuthenticateRequest fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static AuthenticateRequest fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin AuthenticateRequestMappable { + String toJson() { + return AuthenticateRequestMapper.ensureInitialized() + .encodeJson(this as AuthenticateRequest); + } + + Map toMap() { + return AuthenticateRequestMapper.ensureInitialized() + .encodeMap(this as AuthenticateRequest); + } + + AuthenticateRequestCopyWith get copyWith => _AuthenticateRequestCopyWithImpl< + AuthenticateRequest, AuthenticateRequest>( + this as AuthenticateRequest, $identity, $identity); + @override + String toString() { + return AuthenticateRequestMapper.ensureInitialized() + .stringifyValue(this as AuthenticateRequest); + } + + @override + bool operator ==(Object other) { + return AuthenticateRequestMapper.ensureInitialized() + .equalsValue(this as AuthenticateRequest, other); + } + + @override + int get hashCode { + return AuthenticateRequestMapper.ensureInitialized() + .hashValue(this as AuthenticateRequest); + } +} + +extension AuthenticateRequestValueCopy<$R, $Out> + on ObjectCopyWith<$R, AuthenticateRequest, $Out> { + AuthenticateRequestCopyWith<$R, AuthenticateRequest, $Out> + get $asAuthenticateRequest => $base.as( + (v, t, t2) => _AuthenticateRequestCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class AuthenticateRequestCopyWith<$R, $In extends AuthenticateRequest, + $Out> implements ClientWorldEventCopyWith<$R, $In, $Out> { + @override + $R call({Uint8List? signature, Uint8List? publicKey}); + AuthenticateRequestCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t); +} + +class _AuthenticateRequestCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, AuthenticateRequest, $Out> + implements AuthenticateRequestCopyWith<$R, AuthenticateRequest, $Out> { + _AuthenticateRequestCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + AuthenticateRequestMapper.ensureInitialized(); + @override + $R call({Uint8List? signature, Uint8List? publicKey}) => + $apply(FieldCopyWithData({ + if (signature != null) #signature: signature, + if (publicKey != null) #publicKey: publicKey + })); + @override + AuthenticateRequest $make(CopyWithData data) => AuthenticateRequest( + data.get(#signature, or: $value.signature), + data.get(#publicKey, or: $value.publicKey)); + + @override + AuthenticateRequestCopyWith<$R2, AuthenticateRequest, $Out2> + $chain<$R2, $Out2>(Then<$Out2, $R2> t) => + _AuthenticateRequestCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + class HybridWorldEventMapper extends SubClassMapperBase { HybridWorldEventMapper._(); diff --git a/api/lib/src/event/process/client.dart b/api/lib/src/event/process/client.dart index 281ae172..ecee4007 100644 --- a/api/lib/src/event/process/client.dart +++ b/api/lib/src/event/process/client.dart @@ -9,6 +9,7 @@ bool isValidClientEvent( Channel channel, WorldState state, { required AssetManager assetManager, + ChallengeManager? challengeManager, }) => switch (event) { TeamJoinRequest() => state.info.teams.containsKey(event.team), @@ -117,22 +118,38 @@ Set _hybridNeedsUpdate(HybridWorldEvent event, WorldState state) => _ => {}, }; -ServerResponse? processClientEvent( - WorldEvent? event, Channel channel, WorldState state, - {required AssetManager assetManager, bool allowServerEvents = false}) { +Future processClientEvent( + WorldEvent? event, + Channel channel, + WorldState state, { + required AssetManager assetManager, + bool allowServerEvents = false, + ChallengeManager? challengeManager, + UserManager? userManager, +}) async { + buildInitialize() => WorldInitialized( + table: state.protectTable(channel), + info: state.info, + id: channel, + packsSignature: assetManager + .createSignature(state.info.packs.toSet()) + .values + .toList(), + teamMembers: state.teamMembers, + ); + if (event == null) { - return ServerResponse.builder( - WorldInitialized( - table: state.protectTable(channel), - info: state.info, - id: channel, - packsSignature: assetManager - .createSignature(state.info.packs.toSet()) - .values - .toList(), - teamMembers: state.teamMembers, - ), - channel); + if (challengeManager != null) { + final challenge = challengeManager.getChallenge(channel); + return ServerResponse.builder( + AuthenticatedRequested( + challenge, + isRequired: true, + ), + channel); + } + userManager?.addUser(channel); + return ServerResponse.builder(buildInitialize(), channel); } if (!isValidClientEvent(event, channel, state, assetManager: assetManager)) { return null; @@ -279,5 +296,15 @@ ServerResponse? processClientEvent( : assetManager.getPack(location.namespace)?.getMode(location.id); return ServerResponse.builder( WorldInitialized.fromMode(mode, state), channel); + case AuthenticateRequest(): + final challenge = challengeManager?.getChallenge(channel); + if (challenge == null) return null; + final verified = await event.verify(challenge); + if (!verified) { + return ServerResponse.builder( + AuthenticatedRequested(challenge, isRequired: true), channel); + } + userManager?.addUser(channel, event.publicKey); + return ServerResponse.builder(buildInitialize(), channel); } } diff --git a/api/lib/src/event/process/server.dart b/api/lib/src/event/process/server.dart index 884afbf7..1ae4db6d 100644 --- a/api/lib/src/event/process/server.dart +++ b/api/lib/src/event/process/server.dart @@ -103,6 +103,8 @@ ServerProcessed processServerEvent( info: event.info ?? state.info, dialogs: event.clearUserInterface ? [] : state.dialogs, images: event.clearUserInterface ? {} : state.images, + gameState: GameState.play, + authRequest: null, )); case TeamJoined(): return ServerProcessed(state.copyWith( @@ -349,5 +351,9 @@ ServerProcessed processServerEvent( return ServerProcessed(state.copyWith( serverState: event.state, )); + case AuthenticatedRequested(): + return ServerProcessed(state.copyWith( + authRequest: event, + )); } } diff --git a/api/lib/src/event/server.dart b/api/lib/src/event/server.dart index 2a1f3134..591b0b21 100644 --- a/api/lib/src/event/server.dart +++ b/api/lib/src/event/server.dart @@ -187,6 +187,23 @@ final class DialogsClosed extends ServerWorldEvent with DialogsClosedMappable { DialogsClosed.all() : ids = null; } +class Base64Uint8ListHook extends SimpleMapper { + const Base64Uint8ListHook(); + + @override + Uint8List decode(Object value) { + if (value is String) { + return base64Decode(value); + } + return value as Uint8List; + } + + @override + Object? encode(Uint8List self) { + return base64Encode(self); + } +} + class Base64IdMapHook extends SimpleMapper> { const Base64IdMapHook(); @@ -224,3 +241,12 @@ final class ServerStateUpdated extends ServerWorldEvent const ServerStateUpdated(this.state); } + +@MappableClass(includeCustomMappers: [Base64Uint8ListHook()]) +final class AuthenticatedRequested extends ServerWorldEvent + with AuthenticatedRequestedMappable { + final Uint8List challenge; + final bool isRequired; + + const AuthenticatedRequested(this.challenge, {this.isRequired = true}); +} diff --git a/api/lib/src/event/state.dart b/api/lib/src/event/state.dart index 6783edd9..8a4ce56e 100644 --- a/api/lib/src/event/state.dart +++ b/api/lib/src/event/state.dart @@ -11,6 +11,7 @@ import '../models/meta.dart'; import '../models/server.dart'; import '../models/table.dart'; import '../models/vector.dart'; +import 'event.dart'; part 'state.mapper.dart'; @@ -20,6 +21,12 @@ enum WorldOperationMode { boards, } +@MappableEnum() +enum GameState { + configuration, + play, +} + @MappableClass() final class WorldState with WorldStateMappable { final GameTable table; @@ -34,9 +41,12 @@ final class WorldState with WorldStateMappable { final List dialogs; final Map images; final ServerState serverState; + final GameState gameState; + final AuthenticatedRequested? authRequest; const WorldState({ this.name, + this.gameState = GameState.play, this.table = const GameTable(), this.tableName = '', this.info = const GameInfo(), @@ -47,6 +57,7 @@ final class WorldState with WorldStateMappable { this.dialogs = const [], this.images = const {}, this.serverState = const ServerState(), + this.authRequest, required this.data, }); diff --git a/api/lib/src/event/state.mapper.dart b/api/lib/src/event/state.mapper.dart index 65c4266b..31ef0a8d 100644 --- a/api/lib/src/event/state.mapper.dart +++ b/api/lib/src/event/state.mapper.dart @@ -52,6 +52,52 @@ extension WorldOperationModeMapperExtension on WorldOperationMode { } } +class GameStateMapper extends EnumMapper { + GameStateMapper._(); + + static GameStateMapper? _instance; + static GameStateMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = GameStateMapper._()); + } + return _instance!; + } + + static GameState fromValue(dynamic value) { + ensureInitialized(); + return MapperContainer.globals.fromValue(value); + } + + @override + GameState decode(dynamic value) { + switch (value) { + case r'configuration': + return GameState.configuration; + case r'play': + return GameState.play; + default: + throw MapperException.unknownEnumValue(value); + } + } + + @override + dynamic encode(GameState self) { + switch (self) { + case GameState.configuration: + return r'configuration'; + case GameState.play: + return r'play'; + } + } +} + +extension GameStateMapperExtension on GameState { + String toValue() { + GameStateMapper.ensureInitialized(); + return MapperContainer.globals.toValue(this) as String; + } +} + class WorldStateMapper extends ClassMapperBase { WorldStateMapper._(); @@ -59,12 +105,14 @@ class WorldStateMapper extends ClassMapperBase { static WorldStateMapper ensureInitialized() { if (_instance == null) { MapperContainer.globals.use(_instance = WorldStateMapper._()); + GameStateMapper.ensureInitialized(); GameTableMapper.ensureInitialized(); GameInfoMapper.ensureInitialized(); FileMetadataMapper.ensureInitialized(); ChatMessageMapper.ensureInitialized(); GameDialogMapper.ensureInitialized(); ServerStateMapper.ensureInitialized(); + AuthenticatedRequestedMapper.ensureInitialized(); } return _instance!; } @@ -75,6 +123,9 @@ class WorldStateMapper extends ClassMapperBase { static String? _$name(WorldState v) => v.name; static const Field _f$name = Field('name', _$name, opt: true); + static GameState _$gameState(WorldState v) => v.gameState; + static const Field _f$gameState = + Field('gameState', _$gameState, opt: true, def: GameState.play); static GameTable _$table(WorldState v) => v.table; static const Field _f$table = Field('table', _$table, opt: true, def: const GameTable()); @@ -105,12 +156,16 @@ class WorldStateMapper extends ClassMapperBase { static ServerState _$serverState(WorldState v) => v.serverState; static const Field _f$serverState = Field('serverState', _$serverState, opt: true, def: const ServerState()); + static AuthenticatedRequested? _$authRequest(WorldState v) => v.authRequest; + static const Field _f$authRequest = + Field('authRequest', _$authRequest, opt: true); static SetonixData _$data(WorldState v) => v.data; static const Field _f$data = Field('data', _$data); @override final MappableFields fields = const { #name: _f$name, + #gameState: _f$gameState, #table: _f$table, #tableName: _f$tableName, #info: _f$info, @@ -121,12 +176,14 @@ class WorldStateMapper extends ClassMapperBase { #dialogs: _f$dialogs, #images: _f$images, #serverState: _f$serverState, + #authRequest: _f$authRequest, #data: _f$data, }; static WorldState _instantiate(DecodingData data) { return WorldState( name: data.dec(_f$name), + gameState: data.dec(_f$gameState), table: data.dec(_f$table), tableName: data.dec(_f$tableName), info: data.dec(_f$info), @@ -137,6 +194,7 @@ class WorldStateMapper extends ClassMapperBase { dialogs: data.dec(_f$dialogs), images: data.dec(_f$images), serverState: data.dec(_f$serverState), + authRequest: data.dec(_f$authRequest), data: data.dec(_f$data)); } @@ -204,8 +262,11 @@ abstract class WorldStateCopyWith<$R, $In extends WorldState, $Out> MapCopyWith<$R, String, Uint8List, ObjectCopyWith<$R, Uint8List, Uint8List>> get images; ServerStateCopyWith<$R, ServerState, ServerState> get serverState; + AuthenticatedRequestedCopyWith<$R, AuthenticatedRequested, + AuthenticatedRequested>? get authRequest; $R call( {String? name, + GameState? gameState, GameTable? table, String? tableName, GameInfo? info, @@ -216,6 +277,7 @@ abstract class WorldStateCopyWith<$R, $In extends WorldState, $Out> List? dialogs, Map? images, ServerState? serverState, + AuthenticatedRequested? authRequest, SetonixData? data}); WorldStateCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); } @@ -260,8 +322,14 @@ class _WorldStateCopyWithImpl<$R, $Out> ServerStateCopyWith<$R, ServerState, ServerState> get serverState => $value.serverState.copyWith.$chain((v) => call(serverState: v)); @override + AuthenticatedRequestedCopyWith<$R, AuthenticatedRequested, + AuthenticatedRequested>? + get authRequest => + $value.authRequest?.copyWith.$chain((v) => call(authRequest: v)); + @override $R call( {Object? name = $none, + GameState? gameState, GameTable? table, String? tableName, GameInfo? info, @@ -272,9 +340,11 @@ class _WorldStateCopyWithImpl<$R, $Out> List? dialogs, Map? images, ServerState? serverState, + Object? authRequest = $none, SetonixData? data}) => $apply(FieldCopyWithData({ if (name != $none) #name: name, + if (gameState != null) #gameState: gameState, if (table != null) #table: table, if (tableName != null) #tableName: tableName, if (info != null) #info: info, @@ -285,11 +355,13 @@ class _WorldStateCopyWithImpl<$R, $Out> if (dialogs != null) #dialogs: dialogs, if (images != null) #images: images, if (serverState != null) #serverState: serverState, + if (authRequest != $none) #authRequest: authRequest, if (data != null) #data: data })); @override WorldState $make(CopyWithData data) => WorldState( name: data.get(#name, or: $value.name), + gameState: data.get(#gameState, or: $value.gameState), table: data.get(#table, or: $value.table), tableName: data.get(#tableName, or: $value.tableName), info: data.get(#info, or: $value.info), @@ -300,6 +372,7 @@ class _WorldStateCopyWithImpl<$R, $Out> dialogs: data.get(#dialogs, or: $value.dialogs), images: data.get(#images, or: $value.images), serverState: data.get(#serverState, or: $value.serverState), + authRequest: data.get(#authRequest, or: $value.authRequest), data: data.get(#data, or: $value.data)); @override diff --git a/api/lib/src/helpers/crypto.dart b/api/lib/src/helpers/crypto.dart new file mode 100644 index 00000000..a1063d21 --- /dev/null +++ b/api/lib/src/helpers/crypto.dart @@ -0,0 +1,18 @@ +import 'dart:typed_data'; + +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; + +String generateFingerprint(Uint8List publicKeyBytes, [bool short = false]) { + final digest = sha256.convert(publicKeyBytes); + var hexString = hex.encode(digest.bytes); + if (short) { + hexString = hexString.substring(0, 32); + } + final output = hexString.replaceAllMapped( + RegExp(r'.{2}'), + (match) => '${match.group(0)!}:', + ); + + return output.endsWith(':') ? output.substring(0, output.length - 1) : output; +} diff --git a/api/lib/src/models/config.dart b/api/lib/src/models/config.dart index cbfda0ff..b2a08e4b 100644 --- a/api/lib/src/models/config.dart +++ b/api/lib/src/models/config.dart @@ -26,6 +26,9 @@ final class SetonixConfig with SetonixConfigMappable { final String? description; static const String defaultDescription = 'A server for Setonix.'; static const String envDescription = 'SETONIX_DESCRIPTION'; + final String? guestPrefix; + static const String defaultGuestPrefix = 'Guest '; + static const String envGuestPrefix = 'SETONIX_GUEST_PREFIX'; const SetonixConfig({ this.host, @@ -35,6 +38,7 @@ final class SetonixConfig with SetonixConfigMappable { this.multiWorld, this.maxPlayers, this.description, + this.guestPrefix, }); static const defaultConfig = SetonixConfig( @@ -45,6 +49,7 @@ final class SetonixConfig with SetonixConfigMappable { multiWorld: defaultMultiWorld, maxPlayers: defaultMaxPlayers, description: defaultDescription, + guestPrefix: defaultGuestPrefix, ); static SetonixConfig fromEnvironment() { @@ -71,6 +76,10 @@ final class SetonixConfig with SetonixConfigMappable { ? String.fromEnvironment(envDescription, defaultValue: defaultDescription) : null, + guestPrefix: bool.hasEnvironment(envGuestPrefix) + ? String.fromEnvironment(envGuestPrefix, + defaultValue: defaultGuestPrefix) + : null, ); } @@ -82,5 +91,6 @@ final class SetonixConfig with SetonixConfigMappable { multiWorld: other.multiWorld ?? multiWorld, maxPlayers: other.maxPlayers ?? maxPlayers, description: other.description ?? description, + guestPrefix: other.guestPrefix ?? guestPrefix, ); } diff --git a/api/lib/src/models/config.mapper.dart b/api/lib/src/models/config.mapper.dart index c7e10576..567d94f4 100644 --- a/api/lib/src/models/config.mapper.dart +++ b/api/lib/src/models/config.mapper.dart @@ -41,6 +41,9 @@ class SetonixConfigMapper extends ClassMapperBase { static String? _$description(SetonixConfig v) => v.description; static const Field _f$description = Field('description', _$description, opt: true); + static String? _$guestPrefix(SetonixConfig v) => v.guestPrefix; + static const Field _f$guestPrefix = + Field('guestPrefix', _$guestPrefix, opt: true); @override final MappableFields fields = const { @@ -51,6 +54,7 @@ class SetonixConfigMapper extends ClassMapperBase { #multiWorld: _f$multiWorld, #maxPlayers: _f$maxPlayers, #description: _f$description, + #guestPrefix: _f$guestPrefix, }; static SetonixConfig _instantiate(DecodingData data) { @@ -61,7 +65,8 @@ class SetonixConfigMapper extends ClassMapperBase { autosave: data.dec(_f$autosave), multiWorld: data.dec(_f$multiWorld), maxPlayers: data.dec(_f$maxPlayers), - description: data.dec(_f$description)); + description: data.dec(_f$description), + guestPrefix: data.dec(_f$guestPrefix)); } @override @@ -124,7 +129,8 @@ abstract class SetonixConfigCopyWith<$R, $In extends SetonixConfig, $Out> bool? autosave, bool? multiWorld, int? maxPlayers, - String? description}); + String? description, + String? guestPrefix}); SetonixConfigCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); } @@ -144,7 +150,8 @@ class _SetonixConfigCopyWithImpl<$R, $Out> Object? autosave = $none, Object? multiWorld = $none, Object? maxPlayers = $none, - Object? description = $none}) => + Object? description = $none, + Object? guestPrefix = $none}) => $apply(FieldCopyWithData({ if (host != $none) #host: host, if (port != $none) #port: port, @@ -152,7 +159,8 @@ class _SetonixConfigCopyWithImpl<$R, $Out> if (autosave != $none) #autosave: autosave, if (multiWorld != $none) #multiWorld: multiWorld, if (maxPlayers != $none) #maxPlayers: maxPlayers, - if (description != $none) #description: description + if (description != $none) #description: description, + if (guestPrefix != $none) #guestPrefix: guestPrefix })); @override SetonixConfig $make(CopyWithData data) => SetonixConfig( @@ -162,7 +170,8 @@ class _SetonixConfigCopyWithImpl<$R, $Out> autosave: data.get(#autosave, or: $value.autosave), multiWorld: data.get(#multiWorld, or: $value.multiWorld), maxPlayers: data.get(#maxPlayers, or: $value.maxPlayers), - description: data.get(#description, or: $value.description)); + description: data.get(#description, or: $value.description), + guestPrefix: data.get(#guestPrefix, or: $value.guestPrefix)); @override SetonixConfigCopyWith<$R2, SetonixConfig, $Out2> $chain<$R2, $Out2>( diff --git a/api/lib/src/models/data.dart b/api/lib/src/models/data.dart index 6e82693e..03bdaa70 100644 --- a/api/lib/src/models/data.dart +++ b/api/lib/src/models/data.dart @@ -22,6 +22,7 @@ const kPackTranslationsPath = 'translations'; const kPackBackgroundsPath = 'backgrounds'; const kPackScriptsPath = 'scripts'; const kPackModesPath = 'modes'; +const kPackAccountsPath = 'accounts'; const kGameTablePath = 'tables'; const kGameTeamPath = 'teams.json'; @@ -312,6 +313,32 @@ class SetonixData extends ArchiveData { if (mode == null) return null; return MapEntry(e, mode); }).nonNulls); + + SetonixData addAccount(SetonixAccount setonixAccount) { + final accountId = setonixAccount.name; + return setAsset( + '$kPackAccountsPath/$accountId.key', setonixAccount.privateKey) + .setAsset( + '$kPackAccountsPath/$accountId.pub', setonixAccount.publicKey); + } + + Iterable getAccounts() sync* { + const kKeySuffix = '.key'; + final privateKeys = getAssets('$kPackAccountsPath/', true) + .where((e) => e.endsWith(kKeySuffix)); + for (final path in privateKeys) { + final name = path.substring(0, path.length - kKeySuffix.length); + final privateKey = getAsset(path); + if (privateKey == null) continue; + final publicKey = getAsset('$kPackAccountsPath/$name.pub'); + if (publicKey == null) continue; + yield SetonixAccount( + privateKey: privateKey, + publicKey: publicKey, + name: name, + ); + } + } } class SetonixFile { diff --git a/api/lib/src/models/meta.dart b/api/lib/src/models/meta.dart index 4ef78d23..1e701d4d 100644 --- a/api/lib/src/models/meta.dart +++ b/api/lib/src/models/meta.dart @@ -1,4 +1,8 @@ +import 'dart:typed_data'; + +import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:dart_mappable/dart_mappable.dart'; +import 'package:setonix_api/src/helpers/crypto.dart'; part 'meta.mapper.dart'; @@ -9,6 +13,7 @@ enum FileType { pack, game, template, + accounts, } @MappableClass() @@ -65,3 +70,24 @@ final class DataMetadata with DataMetadataMappable { return serversLastUsed.values.fold(addedAt, (a, b) => a.isAfter(b) ? a : b); } } + +final class SetonixAccount { + final Uint8List privateKey; + final Uint8List publicKey; + final String name; + + SetonixAccount({ + required this.privateKey, + required this.publicKey, + required this.name, + }); + + KeyPair get keyPair => SimpleKeyPairData( + privateKey, + publicKey: SimplePublicKey(publicKey, type: KeyPairType.ed25519), + type: KeyPairType.ed25519, + ); + + String getFingerprint([bool short = false]) => + generateFingerprint(publicKey, short); +} diff --git a/api/lib/src/models/meta.mapper.dart b/api/lib/src/models/meta.mapper.dart index b2f71bf8..78540fc0 100644 --- a/api/lib/src/models/meta.mapper.dart +++ b/api/lib/src/models/meta.mapper.dart @@ -31,6 +31,8 @@ class FileTypeMapper extends EnumMapper { return FileType.game; case r'template': return FileType.template; + case r'accounts': + return FileType.accounts; default: throw MapperException.unknownEnumValue(value); } @@ -45,6 +47,8 @@ class FileTypeMapper extends EnumMapper { return r'game'; case FileType.template: return r'template'; + case FileType.accounts: + return r'accounts'; } } } diff --git a/api/lib/src/models/server.dart b/api/lib/src/models/server.dart index 042c7716..6d2bab03 100644 --- a/api/lib/src/models/server.dart +++ b/api/lib/src/models/server.dart @@ -46,6 +46,17 @@ final class ListGameServer extends GameServer with ListGameServerMappable { buildServerAddress(Uri.parse(address), secure, webSockets: webSockets); } +@MappableClass() +final class BrowsedGameServer extends ListGameServer + with BrowsedGameServerMappable { + BrowsedGameServer({ + super.name = '', + required super.address, + super.secure = true, + super.highlighted = false, + }); +} + Uri buildServerAddress(Uri uri, bool secure, {bool webSockets = true}) { // handle plain host without scheme: treat single-segment path as host if (uri.host.isEmpty && uri.pathSegments.length == 1) { @@ -53,10 +64,7 @@ Uri buildServerAddress(Uri uri, bool secure, {bool webSockets = true}) { host: uri.pathSegments.first, pathSegments: uri.pathSegments.skip(1).toList()); } - if (uri.scheme.isEmpty) { - uri = - uri.replace(scheme: (webSockets ? 'ws' : 'http') + (secure ? 's' : '')); - } + uri = uri.replace(scheme: (webSockets ? 'ws' : 'http') + (secure ? 's' : '')); if (!uri.hasPort) { uri = uri.replace(port: kDefaultPort); } diff --git a/api/lib/src/models/server.mapper.dart b/api/lib/src/models/server.mapper.dart index 36993da8..b94a99a0 100644 --- a/api/lib/src/models/server.mapper.dart +++ b/api/lib/src/models/server.mapper.dart @@ -186,6 +186,7 @@ class ListGameServerMapper extends ClassMapperBase { if (_instance == null) { MapperContainer.globals.use(_instance = ListGameServerMapper._()); GameServerMapper.ensureInitialized(); + BrowsedGameServerMapper.ensureInitialized(); } return _instance!; } @@ -311,6 +312,141 @@ class _ListGameServerCopyWithImpl<$R, $Out> _ListGameServerCopyWithImpl<$R2, $Out2>($value, $cast, t); } +class BrowsedGameServerMapper extends ClassMapperBase { + BrowsedGameServerMapper._(); + + static BrowsedGameServerMapper? _instance; + static BrowsedGameServerMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = BrowsedGameServerMapper._()); + ListGameServerMapper.ensureInitialized(); + } + return _instance!; + } + + @override + final String id = 'BrowsedGameServer'; + + static String _$name(BrowsedGameServer v) => v.name; + static const Field _f$name = + Field('name', _$name, opt: true, def: ''); + static String _$address(BrowsedGameServer v) => v.address; + static const Field _f$address = + Field('address', _$address); + static bool _$secure(BrowsedGameServer v) => v.secure; + static const Field _f$secure = + Field('secure', _$secure, opt: true, def: true); + static bool _$highlighted(BrowsedGameServer v) => v.highlighted; + static const Field _f$highlighted = + Field('highlighted', _$highlighted, opt: true, def: false); + + @override + final MappableFields fields = const { + #name: _f$name, + #address: _f$address, + #secure: _f$secure, + #highlighted: _f$highlighted, + }; + + static BrowsedGameServer _instantiate(DecodingData data) { + return BrowsedGameServer( + name: data.dec(_f$name), + address: data.dec(_f$address), + secure: data.dec(_f$secure), + highlighted: data.dec(_f$highlighted)); + } + + @override + final Function instantiate = _instantiate; + + static BrowsedGameServer fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static BrowsedGameServer fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin BrowsedGameServerMappable { + String toJson() { + return BrowsedGameServerMapper.ensureInitialized() + .encodeJson(this as BrowsedGameServer); + } + + Map toMap() { + return BrowsedGameServerMapper.ensureInitialized() + .encodeMap(this as BrowsedGameServer); + } + + BrowsedGameServerCopyWith + get copyWith => + _BrowsedGameServerCopyWithImpl( + this as BrowsedGameServer, $identity, $identity); + @override + String toString() { + return BrowsedGameServerMapper.ensureInitialized() + .stringifyValue(this as BrowsedGameServer); + } + + @override + bool operator ==(Object other) { + return BrowsedGameServerMapper.ensureInitialized() + .equalsValue(this as BrowsedGameServer, other); + } + + @override + int get hashCode { + return BrowsedGameServerMapper.ensureInitialized() + .hashValue(this as BrowsedGameServer); + } +} + +extension BrowsedGameServerValueCopy<$R, $Out> + on ObjectCopyWith<$R, BrowsedGameServer, $Out> { + BrowsedGameServerCopyWith<$R, BrowsedGameServer, $Out> + get $asBrowsedGameServer => $base + .as((v, t, t2) => _BrowsedGameServerCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class BrowsedGameServerCopyWith<$R, $In extends BrowsedGameServer, + $Out> implements ListGameServerCopyWith<$R, $In, $Out> { + @override + $R call({String? name, String? address, bool? secure, bool? highlighted}); + BrowsedGameServerCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t); +} + +class _BrowsedGameServerCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, BrowsedGameServer, $Out> + implements BrowsedGameServerCopyWith<$R, BrowsedGameServer, $Out> { + _BrowsedGameServerCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + BrowsedGameServerMapper.ensureInitialized(); + @override + $R call({String? name, String? address, bool? secure, bool? highlighted}) => + $apply(FieldCopyWithData({ + if (name != null) #name: name, + if (address != null) #address: address, + if (secure != null) #secure: secure, + if (highlighted != null) #highlighted: highlighted + })); + @override + BrowsedGameServer $make(CopyWithData data) => BrowsedGameServer( + name: data.get(#name, or: $value.name), + address: data.get(#address, or: $value.address), + secure: data.get(#secure, or: $value.secure), + highlighted: data.get(#highlighted, or: $value.highlighted)); + + @override + BrowsedGameServerCopyWith<$R2, BrowsedGameServer, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t) => + _BrowsedGameServerCopyWithImpl<$R2, $Out2>($value, $cast, t); +} + class GamePropertyMapper extends ClassMapperBase { GamePropertyMapper._(); diff --git a/api/lib/src/services/challenge.dart b/api/lib/src/services/challenge.dart new file mode 100644 index 00000000..9fc790be --- /dev/null +++ b/api/lib/src/services/challenge.dart @@ -0,0 +1,25 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:networker/networker.dart'; + +class ChallengeManager { + final Map _challenges = {}; + + Uint8List getChallenge(Channel channel) { + return _challenges[channel] ??= generateChallenge(); + } + + void removeChallenge(Channel channel) { + _challenges.remove(channel); + } +} + +Uint8List generateChallenge() { + final random = Random.secure(); + final bytes = Uint8List(32); + for (var i = 0; i < bytes.length; i++) { + bytes[i] = random.nextInt(256); + } + return bytes; +} diff --git a/api/lib/src/services/user.dart b/api/lib/src/services/user.dart new file mode 100644 index 00000000..efbf0806 --- /dev/null +++ b/api/lib/src/services/user.dart @@ -0,0 +1,64 @@ +import 'dart:typed_data'; + +import 'package:dart_mappable/dart_mappable.dart'; +import 'package:networker/networker.dart'; +import 'package:setonix_api/event.dart'; + +part 'user.mapper.dart'; + +@MappableClass(includeCustomMappers: [Base64Uint8ListHook()]) +final class SetonixUser with SetonixUserMappable { + final Uint8List? publicKey; + final String name; + + const SetonixUser({ + this.publicKey, + required this.name, + }); +} + +final class UserManager { + final Map _users = {}; + final String guestPrefix; + int _nextGuestId = 1; + + UserManager([this.guestPrefix = SetonixConfig.defaultGuestPrefix]); + + bool containsUserName(String name) => + _users.values.any((u) => u.name == name); + + void removeUser(Channel channel) { + _users.remove(channel); + } + + /// Retrieves the user associated with the channel. + SetonixUser? getUser(Channel channel) => _users[channel]; + + /// Retrieves a user by name. + SetonixUser? getUserByName(String name) { + for (var user in _users.values) { + if (user.name == name) { + return user; + } + } + return null; + } + + String _generateGuestName() { + String name; + do { + name = '$guestPrefix$_nextGuestId'; + _nextGuestId++; + } while (containsUserName(name)); + return name; + } + + bool addUser(Channel channel, [Uint8List? publicKey, String? name]) { + name ??= _generateGuestName(); + if (containsUserName(name)) { + return false; + } + _users[channel] = SetonixUser(publicKey: publicKey, name: name); + return true; + } +} diff --git a/api/lib/src/services/user.mapper.dart b/api/lib/src/services/user.mapper.dart new file mode 100644 index 00000000..d7d8b2e3 --- /dev/null +++ b/api/lib/src/services/user.mapper.dart @@ -0,0 +1,120 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member +// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter + +part of 'user.dart'; + +class SetonixUserMapper extends ClassMapperBase { + SetonixUserMapper._(); + + static SetonixUserMapper? _instance; + static SetonixUserMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = SetonixUserMapper._()); + MapperContainer.globals.useAll([Base64Uint8ListHook()]); + } + return _instance!; + } + + @override + final String id = 'SetonixUser'; + + static Uint8List? _$publicKey(SetonixUser v) => v.publicKey; + static const Field _f$publicKey = + Field('publicKey', _$publicKey, opt: true); + static String _$name(SetonixUser v) => v.name; + static const Field _f$name = Field('name', _$name); + + @override + final MappableFields fields = const { + #publicKey: _f$publicKey, + #name: _f$name, + }; + + static SetonixUser _instantiate(DecodingData data) { + return SetonixUser( + publicKey: data.dec(_f$publicKey), name: data.dec(_f$name)); + } + + @override + final Function instantiate = _instantiate; + + static SetonixUser fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static SetonixUser fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin SetonixUserMappable { + String toJson() { + return SetonixUserMapper.ensureInitialized() + .encodeJson(this as SetonixUser); + } + + Map toMap() { + return SetonixUserMapper.ensureInitialized() + .encodeMap(this as SetonixUser); + } + + SetonixUserCopyWith get copyWith => + _SetonixUserCopyWithImpl( + this as SetonixUser, $identity, $identity); + @override + String toString() { + return SetonixUserMapper.ensureInitialized() + .stringifyValue(this as SetonixUser); + } + + @override + bool operator ==(Object other) { + return SetonixUserMapper.ensureInitialized() + .equalsValue(this as SetonixUser, other); + } + + @override + int get hashCode { + return SetonixUserMapper.ensureInitialized().hashValue(this as SetonixUser); + } +} + +extension SetonixUserValueCopy<$R, $Out> + on ObjectCopyWith<$R, SetonixUser, $Out> { + SetonixUserCopyWith<$R, SetonixUser, $Out> get $asSetonixUser => + $base.as((v, t, t2) => _SetonixUserCopyWithImpl<$R, $Out>(v, t, t2)); +} + +abstract class SetonixUserCopyWith<$R, $In extends SetonixUser, $Out> + implements ClassCopyWith<$R, $In, $Out> { + $R call({Uint8List? publicKey, String? name}); + SetonixUserCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); +} + +class _SetonixUserCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, SetonixUser, $Out> + implements SetonixUserCopyWith<$R, SetonixUser, $Out> { + _SetonixUserCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + SetonixUserMapper.ensureInitialized(); + @override + $R call({Object? publicKey = $none, String? name}) => + $apply(FieldCopyWithData({ + if (publicKey != $none) #publicKey: publicKey, + if (name != null) #name: name + })); + @override + SetonixUser $make(CopyWithData data) => SetonixUser( + publicKey: data.get(#publicKey, or: $value.publicKey), + name: data.get(#name, or: $value.name)); + + @override + SetonixUserCopyWith<$R2, SetonixUser, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t) => + _SetonixUserCopyWithImpl<$R2, $Out2>($value, $cast, t); +} diff --git a/api/pubspec.lock b/api/pubspec.lock index db6ef648..2763daf2 100644 --- a/api/pubspec.lock +++ b/api/pubspec.lock @@ -146,7 +146,7 @@ packages: source: hosted version: "1.19.1" convert: - dependency: transitive + dependency: "direct main" description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 @@ -161,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + cryptography_plus: + dependency: "direct main" + description: + name: cryptography_plus + sha256: "34db787df4f4740a39474b6fb0a610aa6dc13a5b5b68754b4787a79939ac0454" + url: "https://pub.dev" + source: hosted + version: "2.7.1" dart_leap: dependency: "direct main" description: diff --git a/api/pubspec.yaml b/api/pubspec.yaml index a8702958..9f58782d 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -28,6 +28,8 @@ dependencies: ref: b7787191b0705ff0a22149409b1b360468d9e06d crypto: ^3.0.5 collection: ^1.18.0 + convert: ^3.1.2 + cryptography_plus: ^2.7.1 # path: ^1.8.0 dev_dependencies: diff --git a/app/lib/api/open.dart b/app/lib/api/open.dart index 8d45eeb9..591fe19d 100644 --- a/app/lib/api/open.dart +++ b/app/lib/api/open.dart @@ -93,29 +93,31 @@ Future importFileData(BuildContext context, SetonixFileSystem fileSystem, final data = file.load(); final metadata = data.getMetadataOrDefault(); final type = metadata.type; + final loc = AppLocalizations.of(context); final result = await showDialog( context: context, builder: (context) => AlertDialog( title: Text(switch (type) { - FileType.pack => AppLocalizations.of(context).importPack, - FileType.game => AppLocalizations.of(context).importGame, - FileType.template => AppLocalizations.of(context).importTemplate, + FileType.pack => loc.importPack, + FileType.game => loc.importGame, + FileType.template => loc.importTemplate, + FileType.accounts => loc.importAccounts, }), content: Text(switch (type) { - FileType.pack => AppLocalizations.of(context).importPackDescription, - FileType.game => AppLocalizations.of(context).importGameDescription, - FileType.template => - AppLocalizations.of(context).importTemplateDescription, + FileType.pack => loc.importPackDescription, + FileType.game => loc.importGameDescription, + FileType.template => loc.importTemplateDescription, + FileType.accounts => loc.importAccountsDescription, }), actions: [ TextButton.icon( onPressed: () => Navigator.of(context).pop(false), - label: Text(AppLocalizations.of(context).cancel), + label: Text(loc.cancel), icon: Icon(PhosphorIconsLight.prohibit), ), ElevatedButton.icon( onPressed: () => Navigator.of(context).pop(true), - label: Text(AppLocalizations.of(context).import), + label: Text(loc.import), icon: Icon(PhosphorIconsLight.boxArrowDown), ), ], @@ -130,5 +132,7 @@ Future importFileData(BuildContext context, SetonixFileSystem fileSystem, await fileSystem.templateSystem.createFile(metadata.name, data); case FileType.game: await fileSystem.worldSystem.createFile(metadata.name, data); + case FileType.accounts: + await fileSystem.importAccountsFromData(data); } } diff --git a/app/lib/api/settings.dart b/app/lib/api/settings.dart index e6e5454d..02120016 100644 --- a/app/lib/api/settings.dart +++ b/app/lib/api/settings.dart @@ -4,18 +4,26 @@ import 'package:flutter/material.dart'; import '../pages/settings/home.dart'; -Future openSettings(BuildContext context) => showGeneralDialog( +Future openSettings( + BuildContext context, { + SettingsView view = SettingsView.general, +}) => + showGeneralDialog( context: context, pageBuilder: (context, animation, secondaryAnimation) => ScaffoldMessenger( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), child: Dialog( - clipBehavior: Clip.antiAlias, - child: ConstrainedBox( - constraints: - const BoxConstraints(maxHeight: 800, maxWidth: 1000), - child: const SettingsPage(isDialog: true))), + clipBehavior: Clip.antiAlias, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 800, maxWidth: 1000), + child: SettingsPage( + isDialog: true, + view: view, + ), + ), + ), ), ), barrierDismissible: true, diff --git a/app/lib/bloc/multiplayer.dart b/app/lib/bloc/multiplayer.dart index a6b32f79..59f4e153 100644 --- a/app/lib/bloc/multiplayer.dart +++ b/app/lib/bloc/multiplayer.dart @@ -33,6 +33,8 @@ sealed class MultiplayerState with MultiplayerStateMappable { bool get isConnected => this is MultiplayerConnectedState; bool get isClient => false; bool get isServer => false; + + UserManager? get userManager => null; } @MappableClass() @@ -62,6 +64,8 @@ final class MultiplayerConnectedState extends MultiplayerState with MultiplayerConnectedStateMappable { final NetworkerBase networker; final SimpleNetworkerPipe pipe; + @override + final UserManager userManager = UserManager(); MultiplayerConnectedState(this.networker, this.pipe); @@ -136,7 +140,8 @@ class MultiplayerCubit extends Cubit { if (base is NetworkerClient) { transformer.connect(pipe); } else if (base is NetworkerServer) { - base.clientConnect.listen(_initController.add); + base.clientConnect.listen(_onJoin); + base.clientDisconnect.listen(_onLeft); transformer.connect(SimpleNetworkerPipe() ..read.listen(_onClientEvent) ..write.listen((e) => _onClientEvent(e, true)) @@ -309,4 +314,15 @@ class MultiplayerCubit extends Cubit { _fatalError = e; disconnect(); } + + void _onLeft((Channel, ConnectionInfo) event) { + state.userManager?.removeUser(event.$1); + } + + void _onJoin((Channel, ConnectionInfo) event) { + _initController.add(event); + state.userManager?.addUser( + event.$1, + ); + } } diff --git a/app/lib/bloc/multiplayer.mapper.dart b/app/lib/bloc/multiplayer.mapper.dart index 3908cbcd..c80fa04c 100644 --- a/app/lib/bloc/multiplayer.mapper.dart +++ b/app/lib/bloc/multiplayer.mapper.dart @@ -445,11 +445,16 @@ class MultiplayerConnectedStateMapper v.pipe; static const Field> _f$pipe = Field('pipe', _$pipe); + static UserManager _$userManager(MultiplayerConnectedState v) => + v.userManager; + static const Field _f$userManager = + Field('userManager', _$userManager, mode: FieldMode.member); @override final MappableFields fields = const { #networker: _f$networker, #pipe: _f$pipe, + #userManager: _f$userManager, }; static MultiplayerConnectedState _instantiate(DecodingData data) { diff --git a/app/lib/bloc/settings.dart b/app/lib/bloc/settings.dart index 5b3c3d57..858ac182 100644 --- a/app/lib/bloc/settings.dart +++ b/app/lib/bloc/settings.dart @@ -31,6 +31,14 @@ enum ThemeDensity { }; } +List getDefaultServerList() { + final env = String.fromEnvironment('server_list', defaultValue: ''); + if (env.isNotEmpty) { + return env.split(',').map((e) => e.trim()).toList(); + } + return ['https://servers.setonix.linwood.dev/data.json']; +} + final class ThemeModeMapper extends SimpleMapper { const ThemeModeMapper(); @@ -55,7 +63,7 @@ class SetonixSettings with SetonixSettingsMappable implements LeapSettings { @override final bool nativeTitleBar; final bool stackedCards; - final bool showConnectYour, showConnectNetwork; + final bool showConnectYour, showConnectBrowse; final GameProperty gameProperty; final List servers; final double zoom; @@ -63,6 +71,8 @@ class SetonixSettings with SetonixSettingsMappable implements LeapSettings { final List swamps; final double scrollSensitivity; final ThemeDensity density; + final List serverList; + final bool showIntro; const SetonixSettings({ this.localeTag = '', @@ -71,7 +81,7 @@ class SetonixSettings with SetonixSettingsMappable implements LeapSettings { this.dataDirectory = '', this.nativeTitleBar = false, this.showConnectYour = true, - this.showConnectNetwork = true, + this.showConnectBrowse = false, this.lastVersion, this.gameProperty = const GameProperty(), this.servers = const [], @@ -81,6 +91,8 @@ class SetonixSettings with SetonixSettingsMappable implements LeapSettings { this.swamps = const [], this.density = ThemeDensity.system, this.scrollSensitivity = 1, + this.serverList = const [], + this.showIntro = true, }); Locale? get locale { @@ -100,7 +112,7 @@ class SetonixSettings with SetonixSettingsMappable implements LeapSettings { nativeTitleBar: prefs.getBool('nativeTitleBar') ?? false, localeTag: prefs.getString('locale') ?? '', showConnectYour: prefs.getBool('showConnectYour') ?? true, - showConnectNetwork: prefs.getBool('showConnectNetwork') ?? true, + showConnectBrowse: prefs.getBool('showConnectBrowse') ?? false, lastVersion: prefs.getString('lastVersion'), gameProperty: prefs.containsKey('gameProperty') ? GamePropertyMapper.fromJson(prefs.getString('gameProperty')!) @@ -118,6 +130,8 @@ class SetonixSettings with SetonixSettingsMappable implements LeapSettings { density: ThemeDensity.values.byName( prefs.getString('density') ?? ThemeDensity.system.name, ), + serverList: prefs.getStringList('serverList') ?? [], + showIntro: prefs.getBool('showIntro') ?? true, ); Future save() async { @@ -128,7 +142,7 @@ class SetonixSettings with SetonixSettingsMappable implements LeapSettings { await prefs.setBool('nativeTitleBar', nativeTitleBar); await prefs.setString('locale', localeTag); await prefs.setBool('showConnectYour', showConnectYour); - await prefs.setBool('showConnectNetwork', showConnectNetwork); + await prefs.setBool('showConnectBrowse', showConnectBrowse); if (lastVersion == null) { if (prefs.containsKey('last_version')) { await prefs.remove('last_version'); @@ -145,6 +159,8 @@ class SetonixSettings with SetonixSettingsMappable implements LeapSettings { await prefs.setStringList('swamps', swamps); await prefs.setDouble('scrollSensitivity', scrollSensitivity); await prefs.setString('density', density.name); + await prefs.setStringList('serverList', serverList); + await prefs.setBool('showIntro', showIntro); } } @@ -183,8 +199,8 @@ class SettingsCubit extends Cubit return save(); } - Future changeShowConnectNetwork(bool value) { - emit(state.copyWith(showConnectNetwork: value)); + Future changeShowConnectBrowse(bool value) { + emit(state.copyWith(showConnectBrowse: value)); return save(); } @@ -269,6 +285,25 @@ class SettingsCubit extends Cubit return save(); } + Future addServersToList(List server, [bool reset = false]) { + final newList = {if (!reset) ...state.serverList, ...server}.toList(); + emit(state.copyWith(serverList: newList)); + return save(); + } + + Future addServerToList(String server) => addServersToList([server]); + + Future removeServerFromList(String server) { + final newList = state.serverList.where((s) => s != server).toList(); + emit(state.copyWith(serverList: newList)); + return save(); + } + + Future changeShowIntro(bool value) { + emit(state.copyWith(showIntro: value)); + return save(); + } + Future importSettings(String data) { final settings = SetonixSettingsMapper.fromJson(data); emit(settings); diff --git a/app/lib/bloc/settings.mapper.dart b/app/lib/bloc/settings.mapper.dart index ace703d4..33f36fdc 100644 --- a/app/lib/bloc/settings.mapper.dart +++ b/app/lib/bloc/settings.mapper.dart @@ -104,9 +104,9 @@ class SetonixSettingsMapper extends ClassMapperBase { static bool _$showConnectYour(SetonixSettings v) => v.showConnectYour; static const Field _f$showConnectYour = Field('showConnectYour', _$showConnectYour, opt: true, def: true); - static bool _$showConnectNetwork(SetonixSettings v) => v.showConnectNetwork; - static const Field _f$showConnectNetwork = - Field('showConnectNetwork', _$showConnectNetwork, opt: true, def: true); + static bool _$showConnectBrowse(SetonixSettings v) => v.showConnectBrowse; + static const Field _f$showConnectBrowse = + Field('showConnectBrowse', _$showConnectBrowse, opt: true, def: false); static String? _$lastVersion(SetonixSettings v) => v.lastVersion; static const Field _f$lastVersion = Field('lastVersion', _$lastVersion, opt: true); @@ -135,6 +135,12 @@ class SetonixSettingsMapper extends ClassMapperBase { static double _$scrollSensitivity(SetonixSettings v) => v.scrollSensitivity; static const Field _f$scrollSensitivity = Field('scrollSensitivity', _$scrollSensitivity, opt: true, def: 1); + static List _$serverList(SetonixSettings v) => v.serverList; + static const Field> _f$serverList = + Field('serverList', _$serverList, opt: true, def: const []); + static bool _$showIntro(SetonixSettings v) => v.showIntro; + static const Field _f$showIntro = + Field('showIntro', _$showIntro, opt: true, def: true); @override final MappableFields fields = const { @@ -144,7 +150,7 @@ class SetonixSettingsMapper extends ClassMapperBase { #dataDirectory: _f$dataDirectory, #nativeTitleBar: _f$nativeTitleBar, #showConnectYour: _f$showConnectYour, - #showConnectNetwork: _f$showConnectNetwork, + #showConnectBrowse: _f$showConnectBrowse, #lastVersion: _f$lastVersion, #gameProperty: _f$gameProperty, #servers: _f$servers, @@ -154,6 +160,8 @@ class SetonixSettingsMapper extends ClassMapperBase { #swamps: _f$swamps, #density: _f$density, #scrollSensitivity: _f$scrollSensitivity, + #serverList: _f$serverList, + #showIntro: _f$showIntro, }; static SetonixSettings _instantiate(DecodingData data) { @@ -164,7 +172,7 @@ class SetonixSettingsMapper extends ClassMapperBase { dataDirectory: data.dec(_f$dataDirectory), nativeTitleBar: data.dec(_f$nativeTitleBar), showConnectYour: data.dec(_f$showConnectYour), - showConnectNetwork: data.dec(_f$showConnectNetwork), + showConnectBrowse: data.dec(_f$showConnectBrowse), lastVersion: data.dec(_f$lastVersion), gameProperty: data.dec(_f$gameProperty), servers: data.dec(_f$servers), @@ -173,7 +181,9 @@ class SetonixSettingsMapper extends ClassMapperBase { zoom: data.dec(_f$zoom), swamps: data.dec(_f$swamps), density: data.dec(_f$density), - scrollSensitivity: data.dec(_f$scrollSensitivity)); + scrollSensitivity: data.dec(_f$scrollSensitivity), + serverList: data.dec(_f$serverList), + showIntro: data.dec(_f$showIntro)); } @override @@ -234,6 +244,7 @@ abstract class SetonixSettingsCopyWith<$R, $In extends SetonixSettings, $Out> ListCopyWith<$R, ListGameServer, ListGameServerCopyWith<$R, ListGameServer, ListGameServer>> get servers; ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get swamps; + ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get serverList; $R call( {String? localeTag, ThemeMode? theme, @@ -241,7 +252,7 @@ abstract class SetonixSettingsCopyWith<$R, $In extends SetonixSettings, $Out> String? dataDirectory, bool? nativeTitleBar, bool? showConnectYour, - bool? showConnectNetwork, + bool? showConnectBrowse, String? lastVersion, GameProperty? gameProperty, List? servers, @@ -250,7 +261,9 @@ abstract class SetonixSettingsCopyWith<$R, $In extends SetonixSettings, $Out> double? zoom, List? swamps, ThemeDensity? density, - double? scrollSensitivity}); + double? scrollSensitivity, + List? serverList, + bool? showIntro}); SetonixSettingsCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( Then<$Out2, $R2> t); } @@ -276,6 +289,10 @@ class _SetonixSettingsCopyWithImpl<$R, $Out> ListCopyWith($value.swamps, (v, t) => ObjectCopyWith(v, $identity, t), (v) => call(swamps: v)); @override + ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get serverList => + ListCopyWith($value.serverList, (v, t) => ObjectCopyWith(v, $identity, t), + (v) => call(serverList: v)); + @override $R call( {String? localeTag, ThemeMode? theme, @@ -283,7 +300,7 @@ class _SetonixSettingsCopyWithImpl<$R, $Out> String? dataDirectory, bool? nativeTitleBar, bool? showConnectYour, - bool? showConnectNetwork, + bool? showConnectBrowse, Object? lastVersion = $none, GameProperty? gameProperty, List? servers, @@ -292,7 +309,9 @@ class _SetonixSettingsCopyWithImpl<$R, $Out> double? zoom, List? swamps, ThemeDensity? density, - double? scrollSensitivity}) => + double? scrollSensitivity, + List? serverList, + bool? showIntro}) => $apply(FieldCopyWithData({ if (localeTag != null) #localeTag: localeTag, if (theme != null) #theme: theme, @@ -300,7 +319,7 @@ class _SetonixSettingsCopyWithImpl<$R, $Out> if (dataDirectory != null) #dataDirectory: dataDirectory, if (nativeTitleBar != null) #nativeTitleBar: nativeTitleBar, if (showConnectYour != null) #showConnectYour: showConnectYour, - if (showConnectNetwork != null) #showConnectNetwork: showConnectNetwork, + if (showConnectBrowse != null) #showConnectBrowse: showConnectBrowse, if (lastVersion != $none) #lastVersion: lastVersion, if (gameProperty != null) #gameProperty: gameProperty, if (servers != null) #servers: servers, @@ -309,7 +328,9 @@ class _SetonixSettingsCopyWithImpl<$R, $Out> if (zoom != null) #zoom: zoom, if (swamps != null) #swamps: swamps, if (density != null) #density: density, - if (scrollSensitivity != null) #scrollSensitivity: scrollSensitivity + if (scrollSensitivity != null) #scrollSensitivity: scrollSensitivity, + if (serverList != null) #serverList: serverList, + if (showIntro != null) #showIntro: showIntro })); @override SetonixSettings $make(CopyWithData data) => SetonixSettings( @@ -319,8 +340,8 @@ class _SetonixSettingsCopyWithImpl<$R, $Out> dataDirectory: data.get(#dataDirectory, or: $value.dataDirectory), nativeTitleBar: data.get(#nativeTitleBar, or: $value.nativeTitleBar), showConnectYour: data.get(#showConnectYour, or: $value.showConnectYour), - showConnectNetwork: - data.get(#showConnectNetwork, or: $value.showConnectNetwork), + showConnectBrowse: + data.get(#showConnectBrowse, or: $value.showConnectBrowse), lastVersion: data.get(#lastVersion, or: $value.lastVersion), gameProperty: data.get(#gameProperty, or: $value.gameProperty), servers: data.get(#servers, or: $value.servers), @@ -330,7 +351,9 @@ class _SetonixSettingsCopyWithImpl<$R, $Out> swamps: data.get(#swamps, or: $value.swamps), density: data.get(#density, or: $value.density), scrollSensitivity: - data.get(#scrollSensitivity, or: $value.scrollSensitivity)); + data.get(#scrollSensitivity, or: $value.scrollSensitivity), + serverList: data.get(#serverList, or: $value.serverList), + showIntro: data.get(#showIntro, or: $value.showIntro)); @override SetonixSettingsCopyWith<$R2, SetonixSettings, $Out2> $chain<$R2, $Out2>( diff --git a/app/lib/bloc/world/bloc.dart b/app/lib/bloc/world/bloc.dart index 4db97c8b..9c3fe216 100644 --- a/app/lib/bloc/world/bloc.dart +++ b/app/lib/bloc/world/bloc.dart @@ -50,6 +50,7 @@ class WorldBloc extends Bloc { String? name, SetonixData? data, GameTable? table, + GameState gameState = GameState.play, }) : super(ClientWorldState( assetManager: GameAssetManager( fileSystem: fileSystem, @@ -62,6 +63,7 @@ class WorldBloc extends Bloc { table: table ?? data?.getTable() ?? const GameTable(), metadata: data?.getMetadata() ?? const FileMetadata(), info: data?.getInfo() ?? const GameInfo(), + gameState: gameState, ), )) { pluginSystem = PluginSystem(server: _WorldServerInterfaceImpl(this)); @@ -151,9 +153,14 @@ class WorldBloc extends Bloc { return state.fileSystem.worldSystem.updateFile(name, data); } - void _processEvent(NetworkerPacket data) { - final value = processClientEvent(data.data, data.channel, state.world, - assetManager: state.assetManager); + Future _processEvent(NetworkerPacket data) async { + final value = await processClientEvent( + data.data, + data.channel, + state.world, + assetManager: state.assetManager, + userManager: state.multiplayer.state.userManager, + ); if (value == null) return; state.multiplayer.sendServerPackets( value.buildPackets(state.world, state.multiplayer.clients)); @@ -167,7 +174,7 @@ class WorldBloc extends Bloc { } } - void process(WorldEvent event) { + Future process(WorldEvent event) async { switch (event) { case LocalWorldEvent e: add(e); @@ -176,7 +183,8 @@ class WorldBloc extends Bloc { if (multiplayer.isConnected) { multiplayer.send(e); } else { - final event = processClientEvent(e, kAuthorityChannel, state.world, + final event = await processClientEvent( + e, kAuthorityChannel, state.world, assetManager: state.assetManager, allowServerEvents: true); if (event != null) { add(event.main.data); diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 4fe34fc2..f6eeffdc 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -250,5 +250,24 @@ "compact": "Compact", "standard": "Standard", "comfortable": "Comfortable", - "stackedCards": "Stacked cards" + "stackedCards": "Stacked cards", + "accounts": "Accounts", + "create": "Create", + "configuringGame": "Configuring game...", + "authenticate": "Authenticate", + "authenticateDescription": "The server requested authentication. Select the account you want to use to authenticate.", + "backupKey": "Backup key", + "importAccounts": "Import accounts", + "importAccountsDescription": "Are you sure you want to import the accounts?", + "backupAllKeys": "Backup all keys", + "noAccount": "There are no accounts available", + "authenticateRequired": "Authentication is required to connect to the server", + "authenticateLoading": "Authenticating...", + "loadDefaultServerList": "Load default server list", + "loadDefaultServerListDescription": "This will load the default server list from the Setonix website when opening the servers dialog.", + "serverLists": "Server lists", + "reset": "Reset", + "confirm": "Confirm", + "showIntro": "Show intro", + "highlighted": "Highlighted" } diff --git a/app/lib/main.dart b/app/lib/main.dart index 9272179b..870edb5d 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -4,16 +4,18 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:setonix/pages/settings/accounts.dart'; +import 'package:setonix/pages/settings/data.dart'; +import 'package:setonix/pages/settings/general.dart'; import 'package:setonix/pages/settings/input.dart'; +import 'package:setonix/pages/settings/personalization.dart'; +import 'package:setonix/pages/settings/servers.dart'; import 'package:setonix/src/generated/i18n/app_localizations.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; import 'package:material_leap/material_leap.dart'; import 'package:setonix/pages/editor/shell.dart'; import 'package:setonix/pages/game/page.dart'; import 'package:setonix/pages/home/page.dart'; -import 'package:setonix/pages/settings/data.dart'; -import 'package:setonix/pages/settings/general.dart'; -import 'package:setonix/pages/settings/personalization.dart'; import 'package:setonix/services/file_system.dart'; import 'package:setonix/services/network.dart'; import 'package:setonix/theme.dart'; @@ -176,6 +178,14 @@ class SetonixApp extends StatelessWidget { path: 'inputs', builder: (context, state) => const InputsSettingsPage(), ), + GoRoute( + path: 'accounts', + builder: (context, state) => const AccountsSettingsPage(), + ), + GoRoute( + path: 'serverlist', + builder: (context, state) => const ServersSettingsPage(), + ), ], ), ], diff --git a/app/lib/pages/game/auth.dart b/app/lib/pages/game/auth.dart new file mode 100644 index 00000000..f94a9895 --- /dev/null +++ b/app/lib/pages/game/auth.dart @@ -0,0 +1,166 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:setonix/api/settings.dart'; +import 'package:setonix/bloc/world/bloc.dart'; +import 'package:setonix/bloc/world/state.dart'; +import 'package:setonix/pages/settings/home.dart'; +import 'package:setonix/services/file_system.dart'; +import 'package:setonix/src/generated/i18n/app_localizations.dart'; +import 'package:setonix_api/setonix_api.dart'; + +class AuthGameView extends StatefulWidget { + const AuthGameView({super.key}); + + @override + State createState() => _AuthGameViewState(); +} + +class _AuthGameViewState extends State { + late final SetonixFileSystem _fileSystem; + Future>? _keysFuture; + + @override + void initState() { + super.initState(); + _fileSystem = context.read(); + _buildKeysFuture(); + } + + void _buildKeysFuture() { + _keysFuture = _fileSystem.getAccounts(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.world.authRequest != current.world.authRequest, + builder: (context, state) { + final authRequest = state.world.authRequest; + if (authRequest == null) { + return const SizedBox.shrink(); + } + bool isLoading = false; + return StatefulBuilder( + builder: (context, setInnerState) => Stack( + alignment: Alignment.center, + children: [ + BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: Container( + color: Colors.black.withValues(alpha: 0.5), + ), + ), + ResponsiveAlertDialog( + title: Text(AppLocalizations.of(context).authenticate), + constraints: BoxConstraints( + maxWidth: LeapBreakpoints.compact, + ), + headerActions: [ + IconButton( + icon: const Icon(PhosphorIconsLight.gear), + onPressed: () async { + await openSettings(context, + view: SettingsView.accounts); + setState(() { + _buildKeysFuture(); + }); + }, + ), + ], + content: isLoading + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + const SizedBox(height: 16.0), + Text(AppLocalizations.of(context) + .authenticateLoading), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(AppLocalizations.of(context) + .authenticateDescription), + if (authRequest.isRequired) + Text( + AppLocalizations.of(context) + .authenticateRequired, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: Colors.red, + ), + ), + const SizedBox(height: 16.0), + Flexible( + child: FutureBuilder>( + future: _keysFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.waiting) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator(), + ); + } else if (snapshot.hasError) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + AppLocalizations.of(context) + .error, + ), + ); + } else if (!snapshot.hasData || + snapshot.data!.isEmpty) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + AppLocalizations.of(context) + .noAccount), + ); + } else { + final accounts = snapshot.data!; + return ListView.builder( + shrinkWrap: true, + itemCount: accounts.length, + itemBuilder: (context, index) { + final account = accounts[index]; + return ListTile( + title: Text( + account.name.substring(1)), + subtitle: Text(account + .getFingerprint(true)), + onTap: () async { + final bloc = + context.read(); + final event = + await AuthenticateRequest + .build(authRequest, + account); + bloc.process(event); + setInnerState(() { + isLoading = true; + }); + }, + ); + }, + ); + } + }, + ), + ), + ], + ), + ) + ], + )); + }); + } +} diff --git a/app/lib/pages/game/page.dart b/app/lib/pages/game/page.dart index 9da267b0..1600a315 100644 --- a/app/lib/pages/game/page.dart +++ b/app/lib/pages/game/page.dart @@ -2,6 +2,7 @@ import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:setonix/api/open.dart'; +import 'package:setonix/pages/game/auth.dart'; import 'package:setonix/pages/game/dialog.dart'; import 'package:setonix/src/generated/i18n/app_localizations.dart'; import 'package:material_leap/material_leap.dart'; @@ -65,6 +66,7 @@ class _GamePageState extends State { name: widget.name, data: data, colorScheme: Theme.of(context).colorScheme, + gameState: address == null ? GameState.play : GameState.configuration, ); await world.state.assetManager.loadPacks(); if (address != null) { @@ -183,7 +185,7 @@ class _GamePageState extends State { DrawerView.notes => const GameNotesDrawer(), }, ), - body: BlocListener( + body: BlocConsumer( listenWhen: (previous, current) => previous.messages.length != current.messages.length, listener: (context, state) { @@ -224,20 +226,53 @@ class _GamePageState extends State { ), ); }, - child: GameWidget( - game: BoardGame( - bloc: context.read(), - settingsCubit: context.read(), - contextMenuController: _contextMenuController, - onEscape: () => Scaffold.of(context).openDrawer(), - ), - focusNode: _focusNode, - initialActiveOverlays: ['dialogs', 'filter'], - overlayBuilderMap: { - 'dialogs': (context, game) => GameDialogOverlay(), - 'filter': (context, game) => GameFilterView(), - }, - ), + buildWhen: (previous, current) => + previous.world.gameState != current.world.gameState, + builder: (context, state) { + return Center( + child: + Stack(alignment: Alignment.center, children: [ + if (state.world.gameState == + GameState.configuration) + Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context) + .configuringGame, + style: Theme.of(context) + .textTheme + .bodyMedium, + ), + ], + ) + else + GameWidget( + game: BoardGame( + bloc: context.read(), + settingsCubit: + context.read(), + contextMenuController: + _contextMenuController, + onEscape: () => + Scaffold.of(context).openDrawer(), + ), + focusNode: _focusNode, + initialActiveOverlays: ['dialogs', 'filter'], + overlayBuilderMap: { + 'dialogs': (context, game) => + GameDialogOverlay(), + 'filter': (context, game) => + GameFilterView(), + }, + ), + AuthGameView(), + ]), + ); + }, ), ); }, diff --git a/app/lib/pages/game/team.dart b/app/lib/pages/game/team.dart index a626defb..72dae490 100644 --- a/app/lib/pages/game/team.dart +++ b/app/lib/pages/game/team.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:setonix/src/generated/i18n/app_localizations.dart'; import 'package:material_leap/material_leap.dart'; import 'package:setonix/bloc/world/bloc.dart'; @@ -90,9 +91,10 @@ class _TeamDialogState extends State { ), ), actions: [ - TextButton( + TextButton.icon( onPressed: () => Navigator.of(context).pop(), - child: Text(AppLocalizations.of(context).cancel), + icon: const Icon(PhosphorIconsLight.prohibit), + label: Text(AppLocalizations.of(context).cancel), ), BlocBuilder( bloc: _bloc, @@ -102,7 +104,7 @@ class _TeamDialogState extends State { listenable: _nameController, builder: (context, child) { final text = _nameController.text; - return ElevatedButton( + return ElevatedButton.icon( onPressed: text.isEmpty || (state.info.teams.containsKey(text) && text != widget.team) @@ -120,7 +122,8 @@ class _TeamDialogState extends State { team)); Navigator.of(context).pop(); }, - child: Text(_isCreate() + icon: const Icon(PhosphorIconsLight.check), + label: Text(_isCreate() ? LeapLocalizations.of(context).create : AppLocalizations.of(context).edit), ); diff --git a/app/lib/pages/home/connect.dart b/app/lib/pages/home/connect.dart index baf0f1a8..62bd9486 100644 --- a/app/lib/pages/home/connect.dart +++ b/app/lib/pages/home/connect.dart @@ -34,6 +34,7 @@ class ConnectEditDialog extends StatelessWidget { String address = initialValue?.address ?? ''; String name = initialValue?.name ?? ''; bool secure = initialValue?.secure ?? true; + bool highlighted = initialValue?.highlighted ?? true; final secureSwitchEnabled = !kIsWeb || Uri.base.isScheme('HTTP'); @@ -74,6 +75,13 @@ class ConnectEditDialog extends StatelessWidget { onFieldSubmitted: index == null ? (_) => connect() : null, ), const SizedBox(height: 8), + StatefulBuilder( + builder: (context, setState) => SwitchListTile( + title: Text(AppLocalizations.of(context).highlighted), + value: highlighted, + onChanged: (value) => setState(() => highlighted = value), + ), + ), if (secureSwitchEnabled) StatefulBuilder( builder: (context, setState) => SwitchListTile( @@ -93,6 +101,7 @@ class ConnectEditDialog extends StatelessWidget { address: address, secure: secure, name: name, + highlighted: highlighted, ); if (index != null) { cubit.updateServer(index!, updated); @@ -123,7 +132,7 @@ class ServersDialog extends StatefulWidget { } class _ServersDialogState extends State { - late final Stream> _servers; + Stream>? _servers; bool _isMobileOpen = false; (GameServer, int)? _selected; @@ -132,13 +141,56 @@ class _ServersDialogState extends State { @override void initState() { super.initState(); - _servers = ValueConnectableStream( - context.read().fetchServersWithProperties()) + _buildServersStream(); + } + + void _buildServersStream([SetonixSettings? settings]) { + settings ??= context.read().state; + _servers = ValueConnectableStream(context + .read() + .fetchServersWithProperties( + browsable: settings.showConnectBrowse, + local: settings.showConnectYour)) .autoConnect(); } - Text _buildDetails(BuildContext context, GameProperty property) => - Text('${property.currentPlayers}/${property.maxPlayers ?? '?'}'); + void _refreshServers(SetonixSettings settings) { + setState(() { + _buildServersStream(settings); + }); + } + + Widget _buildTitle(BuildContext context, GameServer server) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + server.display, + style: Theme.of(context).textTheme.titleLarge, + ), + if (server is ListGameServer && server.name.isNotEmpty) + Text( + server.address, + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ); + } + + Widget _buildDetails( + BuildContext context, bool secure, GameProperty property) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(secure + ? PhosphorIconsLight.shieldCheck + : PhosphorIconsLight.shieldSlash), + const SizedBox(width: 8), + Text('${property.currentPlayers}/${property.maxPlayers ?? '?'}'), + ], + ); + } List _buildDetailsChildren(GameProperty server) => [ ListTile( @@ -148,12 +200,17 @@ class _ServersDialogState extends State { ]; @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocConsumer( + listenWhen: (previous, current) => + previous.serverList != current.serverList || + previous.showConnectYour != current.showConnectYour || + previous.showConnectBrowse != current.showConnectBrowse, buildWhen: (previous, current) => previous.showConnectYour != current.showConnectYour || - previous.showConnectNetwork != current.showConnectNetwork, + previous.showConnectBrowse != current.showConnectBrowse, + listener: (context, state) => _refreshServers(state), builder: (context, settings) => ResponsiveAlertDialog( - title: Text(AppLocalizations.of(context).connect), + title: Text(AppLocalizations.of(context).servers), leading: IconButton.outlined( icon: const Icon(PhosphorIconsLight.x), onPressed: () => Navigator.of(context).pop(), @@ -275,12 +332,41 @@ class _ServersDialogState extends State { itemBuilder: (context, index) { final entry = servers[index]; final current = entry.key; + final primaryColor = + ColorScheme.of(context).primary; + final defaultColor = IconTheme.of(context).color; + final highlighted = current is ListGameServer && + current.highlighted; return ListTile( - title: Text(current.display), - leading: switch (current) { + title: Text(current.display, + style: TextStyle( + fontWeight: highlighted + ? FontWeight.w800 + : FontWeight.normal, + )), + trailing: switch (current) { LanGameServer() => - const Icon(PhosphorIconsLight.globe), - _ => null, + const Icon(PhosphorIconsLight.mapPin), + BrowsedGameServer() => Icon( + PhosphorIcons.globe( + highlighted + ? PhosphorIconsStyle.fill + : PhosphorIconsStyle.light, + ), + color: highlighted + ? primaryColor + : defaultColor, + ), + ListGameServer() => Icon( + PhosphorIcons.puzzlePiece( + highlighted + ? PhosphorIconsStyle.fill + : PhosphorIconsStyle.light, + ), + color: highlighted + ? primaryColor + : defaultColor, + ), }, onTap: () { setState(() { @@ -291,7 +377,7 @@ class _ServersDialogState extends State { showLeapBottomSheet( context: context, titleBuilder: (context) => - Text(server.display), + _buildTitle(context, current), actionsBuilder: (context) => [ if (property != null) ...[ DefaultTextStyle( @@ -299,8 +385,8 @@ class _ServersDialogState extends State { .textTheme .headlineSmall ?? const TextStyle(fontSize: 20), - child: _buildDetails( - context, property), + child: _buildDetails(context, + current.secure, property), ), const SizedBox(width: 8), ], @@ -347,26 +433,26 @@ class _ServersDialogState extends State { onSearchChanged: (value) => setState(() { _search = value; }), - children: const [ - /* InputChip( - label: Text(AppLocalizations.of(context).yourServers), - avatar: const Icon(PhosphorIconsLight.puzzlePiece), - showCheckmark: false, - selected: settings.showConnectYour, - onPressed: () => context - .read() - .changeShowConnectYour(!settings.showConnectYour), - ), - InputChip( - label: Text(AppLocalizations.of(context).inNetwork), - avatar: const Icon(PhosphorIconsLight.globe), - showCheckmark: false, - selected: settings.showConnectNetwork, - onPressed: () => context - .read() - .changeShowConnectNetwork( - !settings.showConnectNetwork), - ), */ + children: [ + InputChip( + label: Text(AppLocalizations.of(context).yourServers), + avatar: const Icon(PhosphorIconsLight.puzzlePiece), + showCheckmark: false, + selected: settings.showConnectYour, + onPressed: () => context + .read() + .changeShowConnectYour(!settings.showConnectYour), + ), + InputChip( + label: Text(AppLocalizations.of(context).browse), + avatar: const Icon(PhosphorIconsLight.globe), + showCheckmark: false, + selected: settings.showConnectBrowse, + onPressed: () => context + .read() + .changeShowConnectBrowse( + !settings.showConnectBrowse), + ), ]), const SizedBox(height: 8), Expanded( @@ -393,18 +479,16 @@ class _ServersDialogState extends State { .titleLarge ?? const TextStyle(), child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Expanded( - child: Text( - server.display, - style: Theme.of(context) - .textTheme - .titleLarge, - ), + child: _buildTitle( + context, server), ), const SizedBox(width: 8), - _buildDetails( - context, property), + _buildDetails(context, + server.secure, property), ], ), ), diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index 542ab94b..c49b18df 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:setonix/pages/settings/intro.dart'; import 'package:setonix/src/generated/i18n/app_localizations.dart'; import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; @@ -12,10 +14,33 @@ import 'package:setonix/pages/home/play.dart'; import '../../api/settings.dart'; -class HomePage extends StatelessWidget { +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { final ScrollController _scrollController = ScrollController(); - HomePage({super.key}); + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _showIntro()); + } + + void _showIntro() { + if (context.read().state.showIntro) { + showDialog(context: context, builder: (context) => IntroDialog()); + } + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } List<(String, IconData, VoidCallback, Widget?, bool)> _getItems( BuildContext context) => diff --git a/app/lib/pages/settings/accounts.dart b/app/lib/pages/settings/accounts.dart new file mode 100644 index 00000000..a3f838c8 --- /dev/null +++ b/app/lib/pages/settings/accounts.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:lw_file_system/lw_file_system.dart'; +import 'package:setonix/api/open.dart'; +import 'package:setonix/api/save.dart'; +import 'package:setonix/src/generated/i18n/app_localizations.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:setonix/services/file_system.dart'; +import 'package:setonix_api/setonix_api.dart'; + +import '../../bloc/settings.dart'; + +class AccountsSettingsPage extends StatefulWidget { + final bool inView; + const AccountsSettingsPage({super.key, this.inView = false}); + + @override + State createState() => _AccountsSettingsPageState(); +} + +class _AccountsSettingsPageState extends State { + late final KeyFileSystem _privateKeyFileSystem, _publicKeyFileSystem; + Future>? _keysFuture; + late final SetonixFileSystem _fileSystem; + + @override + void initState() { + super.initState(); + _fileSystem = context.read(); + _privateKeyFileSystem = _fileSystem.privateKeySystem; + _publicKeyFileSystem = _fileSystem.publicKeySystem; + _buildKeysFuture(); + } + + void _buildKeysFuture() { + _keysFuture = _fileSystem.getAccounts(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: widget.inView ? Colors.transparent : null, + appBar: WindowTitleBar( + inView: widget.inView, + backgroundColor: widget.inView ? Colors.transparent : null, + title: Text(AppLocalizations.of(context).accounts), + actions: [ + IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.arrowSquareIn), + tooltip: AppLocalizations.of(context).import, + onPressed: () async { + await importFile( + context, + _fileSystem, + ); + setState(() { + _buildKeysFuture(); + }); + }, + ), + IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.export), + tooltip: AppLocalizations.of(context).backupAllKeys, + onPressed: () async { + final data = await _fileSystem.exportAccounts(); + if (!context.mounted) return; + exportData(context, data, 'accounts'); + }, + ), + ], + ), + body: FutureBuilder>( + future: _keysFuture, + builder: (context, state) { + if (state.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (state.hasError) { + return Center( + child: Text( + AppLocalizations.of(context).error, + style: Theme.of(context).textTheme.bodyLarge, + ), + ); + } + final accounts = state.data ?? []; + if (accounts.isEmpty) { + return Center( + child: Text( + AppLocalizations.of(context).noAccount, + style: Theme.of(context).textTheme.bodyLarge, + ), + ); + } + return ListView.builder( + itemCount: accounts.length, + itemBuilder: (context, index) { + final account = accounts[index]; + final key = account.name; + final fingerprint = account.getFingerprint(true); + void deleteKey() { + _privateKeyFileSystem.deleteFile(key); + _publicKeyFileSystem.deleteFile(key); + setState(() { + accounts.removeAt(index); + }); + } + + return Dismissible( + key: Key(key), + child: ContextRegion( + builder: (context, button, controller) => ListTile( + title: Text(key.substring(1)), + subtitle: Text(fingerprint), + trailing: button, + ), + menuChildren: [ + MenuItemButton( + leadingIcon: + const PhosphorIcon(PhosphorIconsLight.export), + onPressed: () async { + final data = await _fileSystem.exportAccounts([key]); + if (!context.mounted) return; + exportData(context, data, key); + }, + child: Text(AppLocalizations.of(context).backupKey), + ), + MenuItemButton( + leadingIcon: const PhosphorIcon(PhosphorIconsLight.trash), + onPressed: deleteKey, + child: Text(AppLocalizations.of(context).delete), + ), + ], + ), + onDismissed: (direction) => deleteKey(), + ); + }, + ); + }, + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () async { + final name = await showDialog( + context: context, builder: (context) => NameDialog()); + if (name == null) return; + await _fileSystem.generateKey(name); + setState(() { + _buildKeysFuture(); + }); + }, + label: Text(AppLocalizations.of(context).add), + icon: const PhosphorIcon(PhosphorIconsLight.plus), + ), + ); + } +} diff --git a/app/lib/pages/settings/general.dart b/app/lib/pages/settings/general.dart index 0a90755e..e33d0c5a 100644 --- a/app/lib/pages/settings/general.dart +++ b/app/lib/pages/settings/general.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:material_leap/material_leap.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:setonix/pages/settings/intro.dart'; import 'package:setonix/src/generated/i18n/app_localizations.dart'; import 'package:setonix/bloc/settings.dart'; import 'package:setonix/theme.dart'; @@ -184,6 +185,14 @@ class _GeneralSettingsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + ListTile( + leading: const PhosphorIcon(PhosphorIconsLight.info), + title: Text(AppLocalizations.of(context).showIntro), + onTap: () => showDialog( + context: context, + builder: (context) => IntroDialog(), + ), + ), ListTile( leading: const PhosphorIcon(PhosphorIconsLight.article), diff --git a/app/lib/pages/settings/home.dart b/app/lib/pages/settings/home.dart index 17083857..38c51087 100644 --- a/app/lib/pages/settings/home.dart +++ b/app/lib/pages/settings/home.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:setonix/pages/settings/accounts.dart'; import 'package:setonix/pages/settings/input.dart'; +import 'package:setonix/pages/settings/servers.dart'; import 'package:setonix/src/generated/i18n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:material_leap/material_leap.dart'; @@ -13,7 +15,9 @@ enum SettingsView { general, data, personalization, - inputs; + accounts, + inputs, + servers; bool get isEnabled => true; @@ -22,16 +26,30 @@ enum SettingsView { SettingsView.data => AppLocalizations.of(context).data, SettingsView.personalization => AppLocalizations.of(context).personalization, + SettingsView.accounts => AppLocalizations.of(context).accounts, SettingsView.inputs => AppLocalizations.of(context).inputs, + SettingsView.servers => AppLocalizations.of(context).serverLists, }; IconGetter get icon => switch (this) { SettingsView.general => PhosphorIcons.gear, SettingsView.data => PhosphorIcons.database, SettingsView.personalization => PhosphorIcons.monitor, + SettingsView.accounts => PhosphorIcons.user, SettingsView.inputs => PhosphorIcons.keyboard, + SettingsView.servers => PhosphorIcons.list, }; String get path => '/settings/$name'; + + Widget buildContent({bool inView = false}) => switch (this) { + SettingsView.general => GeneralSettingsPage(inView: inView), + SettingsView.data => DataSettingsPage(inView: inView), + SettingsView.personalization => + PersonalizationSettingsPage(inView: inView), + SettingsView.accounts => AccountsSettingsPage(inView: inView), + SettingsView.inputs => InputsSettingsPage(inView: inView), + SettingsView.servers => ServersSettingsPage(inView: inView), + }; } class SettingsPage extends StatefulWidget { @@ -118,13 +136,7 @@ class _SettingsPageState extends State { if (isMobile) { return navigation; } - final content = switch (_view) { - SettingsView.general => const GeneralSettingsPage(inView: true), - SettingsView.data => const DataSettingsPage(inView: true), - SettingsView.personalization => - const PersonalizationSettingsPage(inView: true), - SettingsView.inputs => const InputsSettingsPage(inView: true), - }; + final content = _view.buildContent(inView: true); return Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox(width: 300, child: navigation), Expanded(child: content), diff --git a/app/lib/pages/settings/intro.dart b/app/lib/pages/settings/intro.dart new file mode 100644 index 00000000..da05f444 --- /dev/null +++ b/app/lib/pages/settings/intro.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:setonix/bloc/settings.dart'; +import 'package:setonix/src/generated/i18n/app_localizations.dart'; + +class IntroDialog extends StatefulWidget { + const IntroDialog({super.key}); + + @override + State createState() => _IntroDialogState(); +} + +class _IntroDialogState extends State { + bool _loadDefaultServers = true; + late final SettingsCubit _settingsCubit; + + @override + void initState() { + super.initState(); + _settingsCubit = context.read(); + } + + @override + void dispose() async { + super.dispose(); + await _settingsCubit.changeShowIntro(false); + if (_loadDefaultServers) { + await _settingsCubit.addServersToList(getDefaultServerList(), true); + } + } + + @override + Widget build(BuildContext context) { + return ResponsiveAlertDialog( + title: Text(AppLocalizations.of(context).welcome), + constraints: const BoxConstraints(maxWidth: LeapBreakpoints.compact), + content: ListView( + shrinkWrap: true, + children: [ + Image.asset( + 'images/logo.png', + width: 100, + height: 100, + ), + Text( + AppLocalizations.of(context).welcomeContent, + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + const Divider(), + SwitchListTile( + value: _loadDefaultServers, + onChanged: (e) => setState(() => _loadDefaultServers = e), + title: Text(AppLocalizations.of(context).loadDefaultServerList), + subtitle: Text( + AppLocalizations.of(context).loadDefaultServerListDescription), + ), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const PhosphorIcon(PhosphorIconsLight.check), + label: Text(AppLocalizations.of(context).confirm), + ), + ], + ), + ); + } +} diff --git a/app/lib/pages/settings/servers.dart b/app/lib/pages/settings/servers.dart new file mode 100644 index 00000000..4bd6795d --- /dev/null +++ b/app/lib/pages/settings/servers.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:setonix/src/generated/i18n/app_localizations.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +import '../../bloc/settings.dart'; + +class ServersSettingsPage extends StatelessWidget { + final bool inView; + const ServersSettingsPage({super.key, this.inView = false}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: inView ? Colors.transparent : null, + appBar: WindowTitleBar( + inView: inView, + backgroundColor: inView ? Colors.transparent : null, + title: Text(AppLocalizations.of(context).servers), + actions: [ + IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.clockCounterClockwise), + tooltip: AppLocalizations.of(context).loadDefaultServerList, + onPressed: () async { + final settingsCubit = context.read(); + showDialog( + context: context, + builder: (context) => AlertDialog( + title: + Text(AppLocalizations.of(context).loadDefaultServerList), + content: Text(AppLocalizations.of(context) + .loadDefaultServerListDescription), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(AppLocalizations.of(context).cancel), + ), + OutlinedButton( + onPressed: () { + settingsCubit.addServersToList( + getDefaultServerList(), true); + Navigator.of(context).pop(true); + }, + child: Text(AppLocalizations.of(context).reset), + ), + ElevatedButton( + onPressed: () { + settingsCubit.addServersToList(getDefaultServerList()); + Navigator.of(context).pop(true); + }, + child: Text(AppLocalizations.of(context).confirm), + ), + ], + ), + ); + }, + ), + ], + ), + body: BlocBuilder( + buildWhen: (previous, current) => + previous.serverList != current.serverList, + builder: (context, state) { + final servers = state.serverList; + if (servers.isEmpty) { + return Center( + child: Text( + AppLocalizations.of(context).noServers, + style: Theme.of(context).textTheme.bodyLarge, + ), + ); + } + return ListView.builder( + itemCount: servers.length, + itemBuilder: (context, index) { + final server = servers[index]; + + void deleteServer() => + context.read().removeServerFromList(server); + + return Dismissible( + key: Key(server), + child: ContextRegion( + builder: (context, button, controller) => ListTile( + title: Text(server), + trailing: button, + ), + menuChildren: [ + MenuItemButton( + leadingIcon: const PhosphorIcon(PhosphorIconsLight.trash), + onPressed: deleteServer, + child: Text(AppLocalizations.of(context).delete), + ), + ], + ), + onDismissed: (direction) => deleteServer(), + ); + }, + ); + }, + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () async { + final settingsCubit = context.read(); + final name = await showDialog( + context: context, builder: (context) => NameDialog()); + if (name == null) return; + var uri = Uri.tryParse(name); + if (uri == null) return; + if (!uri.hasScheme) { + uri = uri.replace(scheme: 'https'); + } + settingsCubit.addServerToList(name); + }, + label: Text(AppLocalizations.of(context).add), + icon: const PhosphorIcon(PhosphorIconsLight.plus), + ), + ); + } +} diff --git a/app/lib/services/file_system.dart b/app/lib/services/file_system.dart index 5d43a310..434a5869 100644 --- a/app/lib/services/file_system.dart +++ b/app/lib/services/file_system.dart @@ -2,6 +2,7 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:typed_data'; +import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:file_selector/file_selector.dart' as fs; import 'package:http/http.dart' as http; import 'package:idb_shim/idb.dart'; @@ -34,12 +35,16 @@ class SetonixFileSystem { worldSystem, editorSystem; final TypedKeyFileSystem dataInfoSystem; + final KeyFileSystem privateKeySystem, publicKeySystem; static Future _onDatabaseUpgrade(VersionChangeEvent event) async { await initStores(event, ['packs', 'templates', 'worlds']); if (event.oldVersion < 2) { event.database.createObjectStore('packs-data'); } + if (event.oldVersion < 3) { + event.database.createObjectStore('accounts'); + } } static const kDatabaseVersion = 2; @@ -116,6 +121,30 @@ class SetonixFileSystem { ), onDecode: SetonixData.fromData, onEncode: (data) => data.exportAsBytes(), + ), + privateKeySystem = KeyFileSystem.fromPlatform( + FileSystemConfig( + passwordStorage: SecureStoragePasswordStorage(), + storeName: 'accounts', + getDirectory: (storage) async => + '${await getSetonixDirectory()}/Accounts', + database: 'setonix.db', + databaseVersion: kDatabaseVersion, + keySuffix: '.key', + onDatabaseUpgrade: _onDatabaseUpgrade, + ), + ), + publicKeySystem = KeyFileSystem.fromPlatform( + FileSystemConfig( + passwordStorage: SecureStoragePasswordStorage(), + storeName: 'accounts', + getDirectory: (storage) async => + '${await getSetonixDirectory()}/Accounts', + database: 'setonix.db', + databaseVersion: kDatabaseVersion, + keySuffix: '.pub', + onDatabaseUpgrade: _onDatabaseUpgrade, + ), ); Future fetchCorePack() async => @@ -197,4 +226,85 @@ class SetonixFileSystem { await updateServerLastUsed(pack, serverAddress); } } + + Future generateKey(String name) async { + final generator = Ed25519(); + final keyPair = await generator.newKeyPair(); + final privateKey = await keyPair.extractPrivateKeyBytes(); + final publicKey = await keyPair.extractPublicKey(); + await privateKeySystem.createFileWithName(Uint8List.fromList(privateKey), + name: name); + await publicKeySystem + .createFileWithName(Uint8List.fromList(publicKey.bytes), name: name); + } + + Future getAccount(String name) async { + final privateKey = await privateKeySystem.getFile(name); + if (privateKey == null) return null; + final publicKey = await publicKeySystem.getFile(name); + if (publicKey == null) return null; + return SetonixAccount( + privateKey: privateKey, + publicKey: publicKey, + name: name, + ); + } + + Future deleteAccount(String name) async { + await privateKeySystem.deleteFile(name); + await publicKeySystem.deleteFile(name); + } + + Future importAccountsFromData(SetonixData data) => + importAccounts(data.getAccounts().toList()); + + Future importAccounts(List accounts) async { + for (final account in accounts) { + final name = await privateKeySystem.createFileWithName( + account.privateKey, + name: account.name, + ); + await publicKeySystem.updateFile( + name, + account.publicKey, + ); + } + } + + Future> getAccounts([List? names]) async { + await privateKeySystem.initialize(); + names ??= await privateKeySystem.getKeys(); + return Future.wait( + names.map((name) => getAccount(name)), + ).then((accounts) => accounts.nonNulls.toList()); + } + + Future exportAccounts( + [List? names, List? accounts]) async { + var data = SetonixData.empty().setMetadata(FileMetadata( + type: FileType.accounts, + )); + final allAccounts = accounts ?? await getAccounts(names); + for (final account in allAccounts) { + final privateKey = account.privateKey; + final publicKey = account.publicKey; + if (privateKey.isEmpty || publicKey.isEmpty) continue; + data = data.addAccount( + SetonixAccount( + privateKey: privateKey, + publicKey: publicKey, + name: account.name, + ), + ); + } + return data; + } + + Future getFingerprint(String key, [bool short = false]) async { + final publicKey = await publicKeySystem.getFile(key); + if (publicKey == null) { + return ''; + } + return generateFingerprint(publicKey, short); + } } diff --git a/app/lib/services/network.dart b/app/lib/services/network.dart index 23badedd..f18e3287 100644 --- a/app/lib/services/network.dart +++ b/app/lib/services/network.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -129,45 +130,109 @@ class NetworkService { } } + Future _fetchServer( + GameServer server, + LanProperty? property, { + Map> cached = const {}, + }) async { + property ??= const LanProperty(); + return cached[server] ?? + switch (server) { + LanGameServer() => + Future.value(GameProperty(description: property.description)), + ListGameServer() => cached[server] = + fetchInfo(server.buildAddress(webSockets: false)) + .onError((_, __) => null), + }; + } + + Stream> _fetchBrowsableServersWithProperties({ + Map>? cached, + required Map returned, + }) { + final lists = settingsCubit.state.serverList; + Future> fetchList(String address) async { + try { + final response = await http.get(Uri.parse(address), + headers: {HttpHeaders.contentTypeHeader: 'application/json'}); + if (response.statusCode != HttpStatus.ok) return []; + final result = jsonDecode(response.body) as Map; + return result['servers'] + .map((e) => BrowsedGameServerMapper.fromMap(e)) + .toList(); + } catch (_) { + return []; + } + } + + final currentCached = cached ?? >{}; + Stream> fetchServers(String address) async* { + final servers = await fetchList(address); + for (final server in servers) { + returned.putIfAbsent( + server, + () => const GameProperty(), + ); + } + yield returned; + for (final server in servers) { + try { + returned[server] = + await _fetchServer(server, null, cached: currentCached); + } catch (_) { + returned[server] = null; + } + yield returned; + } + } + + final streams = lists.map((e) => fetchServers(e)); + return Rx.merge(streams); + } + Stream> fetchServersWithProperties({ - bool list = true, + bool browsable = true, + bool local = true, }) async* { Map> cached = {}; - Future fetch( - GameServer server, LanProperty? property) async { - property ??= const LanProperty(); - return cached[server] ?? - switch (server) { - LanGameServer() => - Future.value(GameProperty(description: property.description)), - ListGameServer() => cached[server] = - fetchInfo(server.buildAddress(webSockets: false)) - .onError((_, __) => null), - }; - } - final returned = {}; yield returned; + fetchLocal() async* { + await for (final event in _servers) { + returned.removeWhere((key, value) => !event.any((e) => e.$1 == key)); + for (final (server, _) in event) { + returned[server] = const GameProperty(); + } + yield returned; - await for (final event in _servers) { - returned.removeWhere((key, value) => !event.any((e) => e.$1 == key)); - for (final (server, _) in event) { - returned[server] = const GameProperty(); - } - yield returned; + if (event.isEmpty) { + continue; + } - if (event.isEmpty) { - continue; + for (final e in event) { + try { + returned[e.$1] = await _fetchServer(e.$1, e.$2, cached: cached); + } catch (_) { + returned[e.$1] = null; + } + yield returned; + } } + } - for (final e in event) { - try { - returned[e.$1] = await fetch(e.$1, e.$2); - } catch (_) { - returned[e.$1] = null; - } - yield returned; + if (browsable) { + if (!local) { + yield* _fetchBrowsableServersWithProperties( + cached: cached, returned: returned); + } else { + yield* Rx.merge([ + fetchLocal(), + _fetchBrowsableServersWithProperties( + cached: cached, returned: returned), + ]); } + } else if (local) { + yield* fetchLocal(); } } } diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg deleted file mode 100644 index cba5ac14..00000000 --- a/docs/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/plugin/pubspec.lock b/plugin/pubspec.lock index 90c7d267..305eb77b 100644 --- a/plugin/pubspec.lock +++ b/plugin/pubspec.lock @@ -177,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + cryptography_plus: + dependency: transitive + description: + name: cryptography_plus + sha256: "34db787df4f4740a39474b6fb0a610aa6dc13a5b5b68754b4787a79939ac0454" + url: "https://pub.dev" + source: hosted + version: "2.7.1" dart_leap: dependency: transitive description: diff --git a/plugin/rust/Cargo.lock b/plugin/rust/Cargo.lock index ce0f13a4..43707683 100644 --- a/plugin/rust/Cargo.lock +++ b/plugin/rust/Cargo.lock @@ -124,15 +124,15 @@ checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" [[package]] name = "byteorder" @@ -142,9 +142,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.26" +version = "1.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" +checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" dependencies = [ "shlex", ] @@ -396,9 +396,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.1" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -436,9 +436,9 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" dependencies = [ "cfg-if", "windows-targets 0.53.0", @@ -446,9 +446,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -496,9 +496,9 @@ dependencies = [ [[package]] name = "mlua" -version = "0.10.5" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1f5f8fbebc7db5f671671134b9321c4b9aa9adeafccfd9a8c020ae45c6a35d0" +checksum = "d3f763c1041eff92ffb5d7169968a327e1ed2ebfe425dac0ee5a35f29082534b" dependencies = [ "anyhow", "bstr", @@ -510,16 +510,15 @@ dependencies = [ "num-traits", "parking_lot", "rustc-hash", - "rustversion", "serde", "serde-value", ] [[package]] name = "mlua-sys" -version = "0.6.8" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380c1f7e2099cafcf40e51d3a9f20a346977587aa4d012eae1f043149a728a93" +checksum = "1901c1a635a22fe9250ffcc4fcc937c16b47c2e9e71adba8784af8bca1f69594" dependencies = [ "cc", "cfg-if", @@ -538,9 +537,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.17.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", @@ -583,9 +582,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -593,9 +592,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", @@ -624,9 +623,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "proc-macro2" @@ -698,9 +697,9 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" @@ -786,9 +785,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "strum" @@ -834,9 +833,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.1" +version = "1.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" dependencies = [ "backtrace", "pin-project-lite", diff --git a/server/lib/src/asset.mapper.dart b/server/lib/src/asset.mapper.dart index 40f7a854..29f32ce9 100644 --- a/server/lib/src/asset.mapper.dart +++ b/server/lib/src/asset.mapper.dart @@ -58,8 +58,8 @@ mixin ServerDataMetadataMappable { ServerDataMetadataCopyWith - get copyWith => _ServerDataMetadataCopyWithImpl( - this as ServerDataMetadata, $identity, $identity); + get copyWith => _ServerDataMetadataCopyWithImpl(this as ServerDataMetadata, $identity, $identity); @override String toString() { return ServerDataMetadataMapper.ensureInitialized() @@ -82,8 +82,8 @@ mixin ServerDataMetadataMappable { extension ServerDataMetadataValueCopy<$R, $Out> on ObjectCopyWith<$R, ServerDataMetadata, $Out> { ServerDataMetadataCopyWith<$R, ServerDataMetadata, $Out> - get $asServerDataMetadata => - $base.as((v, t, t2) => _ServerDataMetadataCopyWithImpl(v, t, t2)); + get $asServerDataMetadata => $base.as( + (v, t, t2) => _ServerDataMetadataCopyWithImpl<$R, $Out>(v, t, t2)); } abstract class ServerDataMetadataCopyWith<$R, $In extends ServerDataMetadata, @@ -118,5 +118,5 @@ class _ServerDataMetadataCopyWithImpl<$R, $Out> @override ServerDataMetadataCopyWith<$R2, ServerDataMetadata, $Out2> $chain<$R2, $Out2>( Then<$Out2, $R2> t) => - _ServerDataMetadataCopyWithImpl($value, $cast, t); + _ServerDataMetadataCopyWithImpl<$R2, $Out2>($value, $cast, t); } diff --git a/server/lib/src/bloc.dart b/server/lib/src/bloc.dart index 84dcc372..231a7056 100644 --- a/server/lib/src/bloc.dart +++ b/server/lib/src/bloc.dart @@ -6,10 +6,16 @@ import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:consoler/consoler.dart'; import 'package:setonix_server/setonix_server.dart'; -Future _computeEvent(ServerWorldEvent event, WorldState state, - List signature) { - return Isolate.run( - () => processServerEvent(event, state, signature: signature)); +Future _computeEvent( + ServerWorldEvent event, + WorldState state, { + required List signature, +}) { + return Isolate.run(() => processServerEvent( + event, + state, + signature: signature, + )); } class WorldBloc extends Bloc @@ -40,8 +46,11 @@ class WorldBloc extends Bloc _serverPlugin = _pluginSystem.registerPlugin('', SetonixPlugin.new); on((event, emit) async { final signature = assetManager.createSignature(); - final processed = - await _computeEvent(event, state, signature.values.toList()); + final processed = await _computeEvent( + event, + state, + signature: signature.values.toList(), + ); final newState = processed.state; processed.responses.forEach(process); if (event is WorldInitialized) { @@ -62,7 +71,7 @@ class WorldBloc extends Bloc Future _loadScript(String? script) async { try { if (script == null) return; - server.pluginSystem.loadLuaPlugin(assetManager, script); + pluginSystem.loadLuaPlugin(assetManager, script); } catch (e) { server.log('Error loading script: $e', level: LogLevel.error); } @@ -89,17 +98,19 @@ class WorldBloc extends Bloc await file.writeAsBytes(bytes); } - void onClientEvent(NetworkerPacket packet, + Future onClientEvent(NetworkerPacket packet, {bool force = false}) async { final data = packet.data; ServerResponse? process; try { - process = processClientEvent( + process = await processClientEvent( data is UserJoined ? null : data, packet.channel, state, assetManager: assetManager, allowServerEvents: packet.isServer, + challengeManager: server.challengeManager, + userManager: server.userManager, ); } catch (e) { server.log('Error processing event: $e', level: LogLevel.error); diff --git a/server/lib/src/config.dart b/server/lib/src/config.dart index d3b34f13..5383c9f7 100644 --- a/server/lib/src/config.dart +++ b/server/lib/src/config.dart @@ -31,8 +31,8 @@ class ConfigManager { _mergedConfig = _mergeConfig(); } else { _config = SetonixConfig.defaultConfig; - await file.writeAsString( - JsonEncoder.withIndent(' ').convert(_config.toJson())); + await file + .writeAsString(JsonEncoder.withIndent(' ').convert(_config.toMap())); } } @@ -53,4 +53,7 @@ class ConfigManager { String get worldFile => _mergedConfig.worldFile ?? SetonixConfig.defaultWorldName; + + String get guestPrefix => + _mergedConfig.guestPrefix ?? SetonixConfig.defaultGuestPrefix; } diff --git a/server/lib/src/server.dart b/server/lib/src/server.dart index 355dae16..413ee6d1 100644 --- a/server/lib/src/server.dart +++ b/server/lib/src/server.dart @@ -26,11 +26,12 @@ String limitOutput(Object? value, [int limit = 500]) { final class SetonixServer { final Consoler consoler; - final ConfigManager configManager = ConfigManager(); + final ConfigManager configManager; final ServerAssetManager assetManager; + final UserManager userManager; + final ChallengeManager? challengeManager; final Map _worlds = {}; final Map _userWorlds = {}; - late final PluginSystem pluginSystem; NetworkerSocketServer? _server; NetworkerPipe? _pipe; @@ -67,7 +68,13 @@ final class SetonixServer { WorldState? getUserWorldState(Channel channel) => getUserWorld(channel)?.state; - SetonixServer._(this.consoler, this.assetManager); + SetonixServer._( + this.consoler, + this.assetManager, + this.configManager, + this.userManager, + this.challengeManager, + ); static Future load({ String? worldFile, @@ -80,7 +87,11 @@ final class SetonixServer { ); await _runStaticLogZone( consoler, () => assetManager.init(console: consoler)); - return SetonixServer._(consoler, assetManager); + final configManager = ConfigManager(); + final userManager = UserManager(configManager.guestPrefix); + final challengeManager = ChallengeManager(); + return SetonixServer._( + consoler, assetManager, configManager, userManager, challengeManager); } void log(Object? message, {LogLevel? level}) => @@ -205,6 +216,10 @@ final class SetonixServer { final (user, info) = event; log('${info.address} ($user) left the game', level: LogLevel.info); getUserWorld(user)?.eventSystem.runLeaveCallback(event.$1, event.$2); + + _userWorlds.remove(user); + userManager.removeUser(user); + challengeManager?.removeChallenge(user); } Future saveAll({bool force = false}) async { diff --git a/server/pubspec.lock b/server/pubspec.lock index 8b890933..5a81376c 100644 --- a/server/pubspec.lock +++ b/server/pubspec.lock @@ -210,6 +210,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + cryptography_plus: + dependency: transitive + description: + name: cryptography_plus + sha256: "34db787df4f4740a39474b6fb0a610aa6dc13a5b5b68754b4787a79939ac0454" + url: "https://pub.dev" + source: hosted + version: "2.7.1" dart_leap: dependency: transitive description: diff --git a/servers/.gitignore b/servers/.gitignore new file mode 100644 index 00000000..016b59ea --- /dev/null +++ b/servers/.gitignore @@ -0,0 +1,24 @@ +# build output +dist/ + +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ diff --git a/servers/README.md b/servers/README.md new file mode 100644 index 00000000..a64188fd --- /dev/null +++ b/servers/README.md @@ -0,0 +1,48 @@ +# Astro Starter Kit: Basics + +```sh +pnpm create astro@latest -- --template basics +``` + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) +[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) + +> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! + +![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) + +## 🚀 Project Structure + +Inside of your Astro project, you'll see the following folders and files: + +```text +/ +├── public/ +│ └── favicon.svg +├── src/ +│ ├── layouts/ +│ │ └── Layout.astro +│ └── pages/ +│ └── index.astro +└── package.json +``` + +To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/). + +## 🧞 Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +| :------------------------ | :----------------------------------------------- | +| `pnpm install` | Installs dependencies | +| `pnpm dev` | Starts local dev server at `localhost:4321` | +| `pnpm build` | Build your production site to `./dist/` | +| `pnpm preview` | Preview your build locally, before deploying | +| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` | +| `pnpm astro -- --help` | Get help using the Astro CLI | + +## 👀 Want to learn more? + +Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/servers/astro.config.mjs b/servers/astro.config.mjs new file mode 100644 index 00000000..4639bb97 --- /dev/null +++ b/servers/astro.config.mjs @@ -0,0 +1,28 @@ +// @ts-check +import { defineConfig, envField } from "astro/config"; + +import node from "@astrojs/node"; + +// https://astro.build/config +export default defineConfig({ + output: "server", + + env: { + schema: { + REMOTE_URLS: envField.string({ + context: "server", + access: "secret", + optional: true, + }), + USE_REMOTE_URLS_STATUS: envField.boolean({ + context: "server", + access: "secret", + default: true, + }), + }, + }, + + adapter: node({ + mode: "standalone", + }), +}); \ No newline at end of file diff --git a/servers/package.json b/servers/package.json new file mode 100644 index 00000000..252f2fde --- /dev/null +++ b/servers/package.json @@ -0,0 +1,18 @@ +{ + "name": "servers", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "packageManager": "pnpm@10.12.1", + "dependencies": { + "@astrojs/node": "^9.2.2", + "@cacheable/node-cache": "^1.5.6", + "@linwooddev/style": "^0.4.2", + "astro": "^5.9.3" + } +} \ No newline at end of file diff --git a/servers/pnpm-lock.yaml b/servers/pnpm-lock.yaml new file mode 100644 index 00000000..1857865f --- /dev/null +++ b/servers/pnpm-lock.yaml @@ -0,0 +1,3303 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@astrojs/node': + specifier: ^9.2.2 + version: 9.2.2(astro@5.9.3(@types/node@24.0.1)(rollup@4.43.0)(typescript@5.8.3)) + '@cacheable/node-cache': + specifier: ^1.5.6 + version: 1.5.6 + '@linwooddev/style': + specifier: ^0.4.2 + version: 0.4.2 + astro: + specifier: ^5.9.3 + version: 5.9.3(@types/node@24.0.1)(rollup@4.43.0)(typescript@5.8.3) + +packages: + + '@astrojs/compiler@2.12.2': + resolution: {integrity: sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw==} + + '@astrojs/internal-helpers@0.6.1': + resolution: {integrity: sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A==} + + '@astrojs/markdown-remark@6.3.2': + resolution: {integrity: sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q==} + + '@astrojs/node@9.2.2': + resolution: {integrity: sha512-PtLPuuojmcl9O3CEvXqL/D+wB4x5DlbrGOvP0MeTAh/VfKFprYAzgw1+45xsnTO+QvPWb26l1cT+ZQvvohmvMw==} + peerDependencies: + astro: ^5.3.0 + + '@astrojs/prism@3.3.0': + resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} + + '@astrojs/telemetry@3.3.0': + resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.5': + resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.27.6': + resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} + engines: {node: '>=6.9.0'} + + '@cacheable/node-cache@1.5.6': + resolution: {integrity: sha512-1sLMrvrWoxrqiVKW1BFDjYrBGSG32miOJfNyPUaWwisvbsu+V2wgYqPyLiw0qpprxtjVZT02omsGgQut+DKw8Q==} + + '@capsizecss/unpack@2.4.0': + resolution: {integrity: sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q==} + + '@emnapi/runtime@1.4.3': + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@keyv/serialize@1.0.3': + resolution: {integrity: sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==} + + '@linwooddev/style@0.4.2': + resolution: {integrity: sha512-ZYDdLnl7D8ttJ9lA3BdZtBw7pCkfwF3EEJ3bCmTibP/10ESQl+RPEUbmKbihudYA+6bX65bZIr4Yaimyh7i15w==} + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.43.0': + resolution: {integrity: sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.43.0': + resolution: {integrity: sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.43.0': + resolution: {integrity: sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.43.0': + resolution: {integrity: sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.43.0': + resolution: {integrity: sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.43.0': + resolution: {integrity: sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.43.0': + resolution: {integrity: sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.43.0': + resolution: {integrity: sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.43.0': + resolution: {integrity: sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.43.0': + resolution: {integrity: sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.43.0': + resolution: {integrity: sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.43.0': + resolution: {integrity: sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.43.0': + resolution: {integrity: sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.43.0': + resolution: {integrity: sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.43.0': + resolution: {integrity: sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.43.0': + resolution: {integrity: sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.43.0': + resolution: {integrity: sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.43.0': + resolution: {integrity: sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.43.0': + resolution: {integrity: sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.43.0': + resolution: {integrity: sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==} + cpu: [x64] + os: [win32] + + '@shikijs/core@3.6.0': + resolution: {integrity: sha512-9By7Xb3olEX0o6UeJyPLI1PE1scC4d3wcVepvtv2xbuN9/IThYN4Wcwh24rcFeASzPam11MCq8yQpwwzCgSBRw==} + + '@shikijs/engine-javascript@3.6.0': + resolution: {integrity: sha512-7YnLhZG/TU05IHMG14QaLvTW/9WiK8SEYafceccHUSXs2Qr5vJibUwsDfXDLmRi0zHdzsxrGKpSX6hnqe0k8nA==} + + '@shikijs/engine-oniguruma@3.6.0': + resolution: {integrity: sha512-nmOhIZ9yT3Grd+2plmW/d8+vZ2pcQmo/UnVwXMUXAKTXdi+LK0S08Ancrz5tQQPkxvjBalpMW2aKvwXfelauvA==} + + '@shikijs/langs@3.6.0': + resolution: {integrity: sha512-IdZkQJaLBu1LCYCwkr30hNuSDfllOT8RWYVZK1tD2J03DkiagYKRxj/pDSl8Didml3xxuyzUjgtioInwEQM/TA==} + + '@shikijs/themes@3.6.0': + resolution: {integrity: sha512-Fq2j4nWr1DF4drvmhqKq8x5vVQ27VncF8XZMBuHuQMZvUSS3NBgpqfwz/FoGe36+W6PvniZ1yDlg2d4kmYDU6w==} + + '@shikijs/types@3.6.0': + resolution: {integrity: sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/fontkit@2.0.8': + resolution: {integrity: sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + + '@types/node@24.0.1': + resolution: {integrity: sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + + astro@5.9.3: + resolution: {integrity: sha512-VReZrpUa/3rfeiVvsQ1A2M3ujDPI+pDGIYOMtXPEZwut8tZoEyealXXLjitgCsJ+3dunKGZbg4Eak6i+r0vniw==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} + hasBin: true + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + blob-to-buffer@1.2.9: + resolution: {integrity: sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA==} + + boxen@8.0.1: + resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} + engines: {node: '>=18'} + + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + cacheable@1.10.0: + resolution: {integrity: sha512-SSgQTAnhd7WlJXnGlIi4jJJOiHzgnM5wRMEPaXAU4kECTAMpBoYKoZ9i5zHmclIEZbxcu3j7yY/CF8DTmwIsHg==} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + ci-info@4.2.0: + resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==} + engines: {node: '>=8'} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + common-ancestor-path@1.0.1: + resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + deterministic-object-hash@2.0.2: + resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} + engines: {node: '>=18'} + + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + flattie@1.1.1: + resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} + engines: {node: '>=8'} + + fontace@0.3.0: + resolution: {integrity: sha512-czoqATrcnxgWb/nAkfyIrRp6Q8biYj7nGnL6zfhTcX+JKKpWHFBnb8uNMw/kZr7u++3Y3wYSYoZgHkCcsuBpBg==} + + fontkit@2.0.4: + resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + h3@1.15.3: + resolution: {integrity: sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ==} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + hookified@1.9.1: + resolution: {integrity: sha512-u3pxtGhKjcSXnGm1CX6aXS9xew535j3lkOCegbA6jdyh0BaAjTbXI4aslKstCr6zUNtoCxFGFKwjbSHdGrMB8g==} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + keyv@5.3.4: + resolution: {integrity: sha512-ypEvQvInNpUe+u+w8BIcPkQvEqXquyyibWE/1NB5T2BTzIpS5cGEV1LZskDzPSTvNAaT4+5FutvzlvnkxOSKlw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + neotraverse@0.6.18: + resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} + engines: {node: '>= 10'} + + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + + node-fetch-native@1.6.6: + resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-mock-http@1.0.0: + resolution: {integrity: sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.3: + resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} + + p-limit@6.2.0: + resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} + engines: {node: '>=18'} + + p-queue@8.1.0: + resolution: {integrity: sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==} + engines: {node: '>=18'} + + p-timeout@6.1.4: + resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} + engines: {node: '>=14.16'} + + package-manager-detector@1.3.0: + resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} + + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + parse-latin@7.0.0: + resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + postcss@8.5.5: + resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==} + engines: {node: ^10 || ^12 || >=14} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + + rehype@13.0.2: + resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-smartypants@3.0.2: + resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==} + engines: {node: '>=16.0.0'} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + restructure@3.0.2: + resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} + + retext-latin@4.0.0: + resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} + + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + + retext-stringify@4.0.0: + resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} + + retext@9.0.0: + resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + + rollup@4.43.0: + resolution: {integrity: sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + server-destroy@1.0.1: + resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shiki@3.6.0: + resolution: {integrity: sha512-tKn/Y0MGBTffQoklaATXmTqDU02zx8NYBGQ+F6gy87/YjKbizcLd+Cybh/0ZtOBX9r1NEnAy/GTRDKtOsc1L9w==} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smol-toml@1.3.4: + resolution: {integrity: sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA==} + engines: {node: '>= 18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + ultrahtml@1.6.0: + resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + + unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unifont@0.5.0: + resolution: {integrity: sha512-4DueXMP5Hy4n607sh+vJ+rajoLu778aU3GzqeTCqsD/EaUcvqZT9wPC8kgK6Vjh22ZskrxyRCR71FwNOaYn6jA==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-modify-children@4.0.0: + resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-children@3.0.0: + resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + unstorage@1.16.0: + resolution: {integrity: sha512-WQ37/H5A7LcRPWfYOrDa1Ys02xAbpPJq6q5GkO88FBXVSQzHd7+BjEwfRqyaSWCv9MbsJy058GWjjPjcJ16GGA==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6.0.3 || ^7.0.0 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/kv': ^1.0.1 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@6.3.5: + resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.0.6: + resolution: {integrity: sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + vite: + optional: true + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-pm-runs@1.1.0: + resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} + engines: {node: '>=4'} + + widest-line@5.0.0: + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + + yocto-spinner@0.2.3: + resolution: {integrity: sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ==} + engines: {node: '>=18.19'} + + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + + zod-to-json-schema@3.24.5: + resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} + peerDependencies: + zod: ^3.24.1 + + zod-to-ts@1.2.0: + resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==} + peerDependencies: + typescript: ^4.9.4 || ^5.0.2 + zod: ^3 + + zod@3.25.64: + resolution: {integrity: sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@astrojs/compiler@2.12.2': {} + + '@astrojs/internal-helpers@0.6.1': {} + + '@astrojs/markdown-remark@6.3.2': + dependencies: + '@astrojs/internal-helpers': 0.6.1 + '@astrojs/prism': 3.3.0 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + hast-util-to-text: 4.0.2 + import-meta-resolve: 4.1.0 + js-yaml: 4.1.0 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.1 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-smartypants: 3.0.2 + shiki: 3.6.0 + smol-toml: 1.3.4 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/node@9.2.2(astro@5.9.3(@types/node@24.0.1)(rollup@4.43.0)(typescript@5.8.3))': + dependencies: + '@astrojs/internal-helpers': 0.6.1 + astro: 5.9.3(@types/node@24.0.1)(rollup@4.43.0)(typescript@5.8.3) + send: 1.2.0 + server-destroy: 1.0.1 + transitivePeerDependencies: + - supports-color + + '@astrojs/prism@3.3.0': + dependencies: + prismjs: 1.30.0 + + '@astrojs/telemetry@3.3.0': + dependencies: + ci-info: 4.2.0 + debug: 4.4.1 + dlv: 1.1.3 + dset: 3.1.4 + is-docker: 3.0.0 + is-wsl: 3.1.0 + which-pm-runs: 1.1.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/parser@7.27.5': + dependencies: + '@babel/types': 7.27.6 + + '@babel/types@7.27.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@cacheable/node-cache@1.5.6': + dependencies: + cacheable: 1.10.0 + hookified: 1.9.1 + keyv: 5.3.4 + + '@capsizecss/unpack@2.4.0': + dependencies: + blob-to-buffer: 1.2.9 + cross-fetch: 3.2.0 + fontkit: 2.0.4 + transitivePeerDependencies: + - encoding + + '@emnapi/runtime@1.4.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.5': + optional: true + + '@esbuild/android-arm64@0.25.5': + optional: true + + '@esbuild/android-arm@0.25.5': + optional: true + + '@esbuild/android-x64@0.25.5': + optional: true + + '@esbuild/darwin-arm64@0.25.5': + optional: true + + '@esbuild/darwin-x64@0.25.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.5': + optional: true + + '@esbuild/freebsd-x64@0.25.5': + optional: true + + '@esbuild/linux-arm64@0.25.5': + optional: true + + '@esbuild/linux-arm@0.25.5': + optional: true + + '@esbuild/linux-ia32@0.25.5': + optional: true + + '@esbuild/linux-loong64@0.25.5': + optional: true + + '@esbuild/linux-mips64el@0.25.5': + optional: true + + '@esbuild/linux-ppc64@0.25.5': + optional: true + + '@esbuild/linux-riscv64@0.25.5': + optional: true + + '@esbuild/linux-s390x@0.25.5': + optional: true + + '@esbuild/linux-x64@0.25.5': + optional: true + + '@esbuild/netbsd-arm64@0.25.5': + optional: true + + '@esbuild/netbsd-x64@0.25.5': + optional: true + + '@esbuild/openbsd-arm64@0.25.5': + optional: true + + '@esbuild/openbsd-x64@0.25.5': + optional: true + + '@esbuild/sunos-x64@0.25.5': + optional: true + + '@esbuild/win32-arm64@0.25.5': + optional: true + + '@esbuild/win32-ia32@0.25.5': + optional: true + + '@esbuild/win32-x64@0.25.5': + optional: true + + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.4.3 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@keyv/serialize@1.0.3': + dependencies: + buffer: 6.0.3 + + '@linwooddev/style@0.4.2': {} + + '@oslojs/encoding@1.1.0': {} + + '@rollup/pluginutils@5.1.4(rollup@4.43.0)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.43.0 + + '@rollup/rollup-android-arm-eabi@4.43.0': + optional: true + + '@rollup/rollup-android-arm64@4.43.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.43.0': + optional: true + + '@rollup/rollup-darwin-x64@4.43.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.43.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.43.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.43.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.43.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.43.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.43.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.43.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.43.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.43.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.43.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.43.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.43.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.43.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.43.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.43.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.43.0': + optional: true + + '@shikijs/core@3.6.0': + dependencies: + '@shikijs/types': 3.6.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.6.0': + dependencies: + '@shikijs/types': 3.6.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.3 + + '@shikijs/engine-oniguruma@3.6.0': + dependencies: + '@shikijs/types': 3.6.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.6.0': + dependencies: + '@shikijs/types': 3.6.0 + + '@shikijs/themes@3.6.0': + dependencies: + '@shikijs/types': 3.6.0 + + '@shikijs/types@3.6.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree@1.0.7': {} + + '@types/estree@1.0.8': {} + + '@types/fontkit@2.0.8': + dependencies: + '@types/node': 24.0.1 + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + + '@types/node@24.0.1': + dependencies: + undici-types: 7.8.0 + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.3.0': {} + + acorn@8.15.0: {} + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-iterate@2.0.1: {} + + astro@5.9.3(@types/node@24.0.1)(rollup@4.43.0)(typescript@5.8.3): + dependencies: + '@astrojs/compiler': 2.12.2 + '@astrojs/internal-helpers': 0.6.1 + '@astrojs/markdown-remark': 6.3.2 + '@astrojs/telemetry': 3.3.0 + '@capsizecss/unpack': 2.4.0 + '@oslojs/encoding': 1.1.0 + '@rollup/pluginutils': 5.1.4(rollup@4.43.0) + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + boxen: 8.0.1 + ci-info: 4.2.0 + clsx: 2.1.1 + common-ancestor-path: 1.0.1 + cookie: 1.0.2 + cssesc: 3.0.0 + debug: 4.4.1 + deterministic-object-hash: 2.0.2 + devalue: 5.1.1 + diff: 5.2.0 + dlv: 1.1.3 + dset: 3.1.4 + es-module-lexer: 1.7.0 + esbuild: 0.25.5 + estree-walker: 3.0.3 + flattie: 1.1.1 + fontace: 0.3.0 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.2.0 + import-meta-resolve: 4.1.0 + js-yaml: 4.1.0 + kleur: 4.1.5 + magic-string: 0.30.17 + magicast: 0.3.5 + mrmime: 2.0.1 + neotraverse: 0.6.18 + p-limit: 6.2.0 + p-queue: 8.1.0 + package-manager-detector: 1.3.0 + picomatch: 4.0.2 + prompts: 2.4.2 + rehype: 13.0.2 + semver: 7.7.2 + shiki: 3.6.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tsconfck: 3.1.6(typescript@5.8.3) + ultrahtml: 1.6.0 + unifont: 0.5.0 + unist-util-visit: 5.0.0 + unstorage: 1.16.0 + vfile: 6.0.3 + vite: 6.3.5(@types/node@24.0.1) + vitefu: 1.0.6(vite@6.3.5(@types/node@24.0.1)) + xxhash-wasm: 1.1.0 + yargs-parser: 21.1.1 + yocto-spinner: 0.2.3 + zod: 3.25.64 + zod-to-json-schema: 3.24.5(zod@3.25.64) + zod-to-ts: 1.2.0(typescript@5.8.3)(zod@3.25.64) + optionalDependencies: + sharp: 0.33.5 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - db0 + - encoding + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - uploadthing + - yaml + + axobject-query@4.1.0: {} + + bail@2.0.2: {} + + base-64@1.0.0: {} + + base64-js@1.5.1: {} + + blob-to-buffer@1.2.9: {} + + boxen@8.0.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 8.0.0 + chalk: 5.4.1 + cli-boxes: 3.0.0 + string-width: 7.2.0 + type-fest: 4.41.0 + widest-line: 5.0.0 + wrap-ansi: 9.0.0 + + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + cacheable@1.10.0: + dependencies: + hookified: 1.9.1 + keyv: 5.3.4 + + camelcase@8.0.0: {} + + ccount@2.0.1: {} + + chalk@5.4.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + ci-info@4.2.0: {} + + cli-boxes@3.0.0: {} + + clone@2.1.2: {} + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + optional: true + + color-name@1.1.4: + optional: true + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + + comma-separated-tokens@2.0.3: {} + + common-ancestor-path@1.0.1: {} + + cookie-es@1.2.2: {} + + cookie@1.0.2: {} + + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.2.0: + dependencies: + character-entities: 2.0.2 + + defu@6.1.4: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + + detect-libc@2.0.4: + optional: true + + deterministic-object-hash@2.0.2: + dependencies: + base-64: 1.0.0 + + devalue@5.1.1: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dfa@1.2.0: {} + + diff@5.2.0: {} + + dlv@1.1.3: {} + + dset@3.1.4: {} + + ee-first@1.1.1: {} + + emoji-regex@10.4.0: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + entities@6.0.1: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.25.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 + + escape-html@1.0.3: {} + + escape-string-regexp@5.0.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + etag@1.8.1: {} + + eventemitter3@5.0.1: {} + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fdir@6.4.6(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + flattie@1.1.1: {} + + fontace@0.3.0: + dependencies: + '@types/fontkit': 2.0.8 + fontkit: 2.0.4 + + fontkit@2.0.4: + dependencies: + '@swc/helpers': 0.5.17 + brotli: 1.3.3 + clone: 2.1.2 + dfa: 1.2.0 + fast-deep-equal: 3.1.3 + restructure: 3.0.2 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + get-east-asian-width@1.3.0: {} + + github-slugger@2.0.0: {} + + h3@1.15.3: + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.5 + defu: 6.1.4 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.0 + radix3: 1.1.2 + ufo: 1.6.1 + uncrypto: 0.1.3 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.2 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + + hookified@1.9.1: {} + + html-escaper@3.0.3: {} + + html-void-elements@3.0.0: {} + + http-cache-semantics@4.2.0: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + ieee754@1.2.1: {} + + import-meta-resolve@4.1.0: {} + + inherits@2.0.4: {} + + iron-webcrypto@1.2.1: {} + + is-arrayish@0.3.2: + optional: true + + is-docker@3.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-plain-obj@4.1.0: {} + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + keyv@5.3.4: + dependencies: + '@keyv/serialize': 1.0.3 + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + longest-streak@3.1.0: {} + + lru-cache@10.4.3: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 + source-map-js: 1.2.1 + + markdown-table@3.0.4: {} + + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdn-data@2.12.2: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.1 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + mime-db@1.54.0: {} + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + neotraverse@0.6.18: {} + + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + + node-fetch-native@1.6.6: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-mock-http@1.0.0: {} + + normalize-path@3.0.0: {} + + ofetch@1.4.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.6 + ufo: 1.6.1 + + ohash@2.0.11: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.3: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.0.1 + regex-recursion: 6.0.2 + + p-limit@6.2.0: + dependencies: + yocto-queue: 1.2.1 + + p-queue@8.1.0: + dependencies: + eventemitter3: 5.0.1 + p-timeout: 6.1.4 + + p-timeout@6.1.4: {} + + package-manager-detector@1.3.0: {} + + pako@0.2.9: {} + + parse-latin@7.0.0: + dependencies: + '@types/nlcst': 2.0.3 + '@types/unist': 3.0.3 + nlcst-to-string: 4.0.0 + unist-util-modify-children: 4.0.0 + unist-util-visit-children: 3.0.0 + vfile: 6.0.3 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + postcss@8.5.5: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prismjs@1.30.0: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + property-information@6.5.0: {} + + property-information@7.1.0: {} + + radix3@1.1.2: {} + + range-parser@1.2.1: {} + + readdirp@4.1.2: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.0.1: + dependencies: + regex-utilities: 2.3.0 + + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + unified: 11.0.5 + + rehype@13.0.2: + dependencies: + '@types/hast': 3.0.4 + rehype-parse: 9.0.1 + rehype-stringify: 10.0.1 + unified: 11.0.5 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remark-smartypants@3.0.2: + dependencies: + retext: 9.0.0 + retext-smartypants: 6.2.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + restructure@3.0.2: {} + + retext-latin@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + parse-latin: 7.0.0 + unified: 11.0.5 + + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.0.0 + + retext-stringify@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unified: 11.0.5 + + retext@9.0.0: + dependencies: + '@types/nlcst': 2.0.3 + retext-latin: 4.0.0 + retext-stringify: 4.0.0 + unified: 11.0.5 + + rollup@4.43.0: + dependencies: + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.43.0 + '@rollup/rollup-android-arm64': 4.43.0 + '@rollup/rollup-darwin-arm64': 4.43.0 + '@rollup/rollup-darwin-x64': 4.43.0 + '@rollup/rollup-freebsd-arm64': 4.43.0 + '@rollup/rollup-freebsd-x64': 4.43.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.43.0 + '@rollup/rollup-linux-arm-musleabihf': 4.43.0 + '@rollup/rollup-linux-arm64-gnu': 4.43.0 + '@rollup/rollup-linux-arm64-musl': 4.43.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.43.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.43.0 + '@rollup/rollup-linux-riscv64-gnu': 4.43.0 + '@rollup/rollup-linux-riscv64-musl': 4.43.0 + '@rollup/rollup-linux-s390x-gnu': 4.43.0 + '@rollup/rollup-linux-x64-gnu': 4.43.0 + '@rollup/rollup-linux-x64-musl': 4.43.0 + '@rollup/rollup-win32-arm64-msvc': 4.43.0 + '@rollup/rollup-win32-ia32-msvc': 4.43.0 + '@rollup/rollup-win32-x64-msvc': 4.43.0 + fsevents: 2.3.3 + + semver@7.7.2: {} + + send@1.2.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + server-destroy@1.0.1: {} + + setprototypeof@1.2.0: {} + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + optional: true + + shiki@3.6.0: + dependencies: + '@shikijs/core': 3.6.0 + '@shikijs/engine-javascript': 3.6.0 + '@shikijs/engine-oniguruma': 3.6.0 + '@shikijs/langs': 3.6.0 + '@shikijs/themes': 3.6.0 + '@shikijs/types': 3.6.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + + sisteransi@1.0.5: {} + + smol-toml@1.3.4: {} + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + statuses@2.0.1: {} + + statuses@2.0.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + tiny-inflate@1.0.3: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + + toidentifier@1.0.1: {} + + tr46@0.0.3: {} + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + tsconfck@3.1.6(typescript@5.8.3): + optionalDependencies: + typescript: 5.8.3 + + tslib@2.8.1: {} + + type-fest@4.41.0: {} + + typescript@5.8.3: {} + + ufo@1.6.1: {} + + ultrahtml@1.6.0: {} + + uncrypto@0.1.3: {} + + undici-types@7.8.0: {} + + unicode-properties@1.4.1: + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unifont@0.5.0: + dependencies: + css-tree: 3.1.0 + ohash: 2.0.11 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-modify-children@4.0.0: + dependencies: + '@types/unist': 3.0.3 + array-iterate: 2.0.1 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-children@3.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + unstorage@1.16.0: + dependencies: + anymatch: 3.1.3 + chokidar: 4.0.3 + destr: 2.0.5 + h3: 1.15.3 + lru-cache: 10.4.3 + node-fetch-native: 1.6.6 + ofetch: 1.4.1 + ufo: 1.6.1 + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + vite@6.3.5(@types/node@24.0.1): + dependencies: + esbuild: 0.25.5 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.5 + rollup: 4.43.0 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 24.0.1 + fsevents: 2.3.3 + + vitefu@1.0.6(vite@6.3.5(@types/node@24.0.1)): + optionalDependencies: + vite: 6.3.5(@types/node@24.0.1) + + web-namespaces@2.0.1: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-pm-runs@1.1.0: {} + + widest-line@5.0.0: + dependencies: + string-width: 7.2.0 + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + xxhash-wasm@1.1.0: {} + + yargs-parser@21.1.1: {} + + yocto-queue@1.2.1: {} + + yocto-spinner@0.2.3: + dependencies: + yoctocolors: 2.1.1 + + yoctocolors@2.1.1: {} + + zod-to-json-schema@3.24.5(zod@3.25.64): + dependencies: + zod: 3.25.64 + + zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.25.64): + dependencies: + typescript: 5.8.3 + zod: 3.25.64 + + zod@3.25.64: {} + + zwitch@2.0.4: {} diff --git a/servers/public/favicon.svg b/servers/public/favicon.svg new file mode 100644 index 00000000..fbf392b3 --- /dev/null +++ b/servers/public/favicon.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/servers/src/assets/astro.svg b/servers/src/assets/astro.svg new file mode 100644 index 00000000..8cf8fb0c --- /dev/null +++ b/servers/src/assets/astro.svg @@ -0,0 +1 @@ + diff --git a/servers/src/assets/background.svg b/servers/src/assets/background.svg new file mode 100644 index 00000000..4b2be0ac --- /dev/null +++ b/servers/src/assets/background.svg @@ -0,0 +1 @@ + diff --git a/servers/src/components/LoadingServerEntry.astro b/servers/src/components/LoadingServerEntry.astro new file mode 100644 index 00000000..0a2fe6dd --- /dev/null +++ b/servers/src/components/LoadingServerEntry.astro @@ -0,0 +1,16 @@ +--- +import { fetchServerStatus } from "../scripts/servers/info"; +import type { Server } from "../scripts/servers/utils"; +import ServerEntry from "./ServerEntry.astro"; + + +interface Props { + server: Server; +} + +const { server } = Astro.props; +--- + + + + diff --git a/servers/src/components/ServerEntry.astro b/servers/src/components/ServerEntry.astro new file mode 100644 index 00000000..f5455e57 --- /dev/null +++ b/servers/src/components/ServerEntry.astro @@ -0,0 +1,63 @@ +--- +import { fetchServerStatus, type ServerStatus } from "../scripts/servers/info"; +import type { Server } from "../scripts/servers/utils"; + +interface Props { + server: Server; + serverStatus?: boolean; +} + +const { server } = Astro.props; +let serverStatus: ServerStatus | undefined | null = undefined; +if (Astro.props.serverStatus) { + serverStatus = await fetchServerStatus(server); +} + +function buildJoinURL(): string { + return `https://launch.setonix.linwood.dev/connect?address=${encodeURIComponent(server.address)}&secure=${encodeURIComponent(server.secure ?? 'true')}`; +} + +const secureLabel = server.secure ? "Secure server" : "Unsecure server"; +const playerText = serverStatus == null + ? "Offline" + : serverStatus.currentPlayers + + (serverStatus.maxPlayers == null ? "" : `/${serverStatus.maxPlayers}`); +--- + +
  • + +
    +

    {server.address}

    +
    + {serverStatus !== undefined && ( +

    + {playerText} +

    + )} + + {server.secure ? "🔒" : "🔓"} + +
    +
    + +

    + {serverStatus === undefined + ? "Loading…" + : serverStatus?.description || "No description available."} +

    +
    +
  • diff --git a/servers/src/data/servers.json b/servers/src/data/servers.json new file mode 100644 index 00000000..421ad1ad --- /dev/null +++ b/servers/src/data/servers.json @@ -0,0 +1,3 @@ +[ + {"address": "127.0.0.1", "secure": false} +] \ No newline at end of file diff --git a/servers/src/layouts/Layout.astro b/servers/src/layouts/Layout.astro new file mode 100644 index 00000000..6fd6384e --- /dev/null +++ b/servers/src/layouts/Layout.astro @@ -0,0 +1,24 @@ +--- +import '@linwooddev/style/css/main.css'; +--- + + + + + + + Setonix Serverlist + + + + + + + diff --git a/servers/src/pages/data.json.ts b/servers/src/pages/data.json.ts new file mode 100644 index 00000000..b6d3212e --- /dev/null +++ b/servers/src/pages/data.json.ts @@ -0,0 +1,10 @@ +import type { APIRoute } from 'astro'; +import { loadServersFromConfig } from '../scripts/servers/list'; + +export async function GET() { + const servers = await loadServersFromConfig(); + return new Response(JSON.stringify({servers}), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); +}; \ No newline at end of file diff --git a/servers/src/pages/index.astro b/servers/src/pages/index.astro new file mode 100644 index 00000000..ff13cfac --- /dev/null +++ b/servers/src/pages/index.astro @@ -0,0 +1,35 @@ +--- +import Layout from "../layouts/Layout.astro"; +import { loadServersFromConfig } from "../scripts/servers/list"; +import ServerEntry from "../components/ServerEntry.astro"; +import LoadingServerEntry from "../components/LoadingServerEntry.astro"; + +const servers = await loadServersFromConfig(); +--- + + + +
    +
    +

    Server List

    +
    +
    +
      + { + servers.map((e) => ( + + )) + } +
    +
    +
    +
    diff --git a/servers/src/scripts/servers/info.ts b/servers/src/scripts/servers/info.ts new file mode 100644 index 00000000..bcc00c21 --- /dev/null +++ b/servers/src/scripts/servers/info.ts @@ -0,0 +1,56 @@ +import NodeCache from "@cacheable/node-cache"; +import { buildServerURL, type Server } from "./utils"; +import { getRemoteServerStatus } from "./list"; +import { USE_REMOTE_URLS_STATUS } from "astro:env/server"; + +const cache = new NodeCache({ stdTTL: 60 * 10 }); +const failedTTL = 60 * 5; // 5 minutes for failed requests + +export type ServerStatus = { + description: string; + maxPlayers?: number; + currentPlayers: number; + packsSignature: Record; +}; + +export async function fetchServerStatus( + server: Server, +): Promise { + if (server.remote && USE_REMOTE_URLS_STATUS) { + return getRemoteServerStatus(server); + } + const cacheKey = `server-status:${server.address}`; + const cached = cache.get(cacheKey); + if (cached) { + return Promise.resolve(cached as ServerStatus); + } + const url = buildServerURL(server); + const response = (await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-Setonix-Method": "info", + }, + }) + .then((res) => { + if (!res.ok) { + throw new Error( + `Server ${server.address} returned status ${res.status}` + ); + } + return res.json(); + }) + .catch((error) => { + console.error( + `Error fetching status for server ${server.address}:`, + error + ); + return null; + })) as ServerStatus | null; + cache.set(cacheKey, response, response ? undefined : failedTTL); + if (!response) { + console.warn(`Invalid response from server ${server.address}:`, response); + return null; + } + return response; +} diff --git a/servers/src/scripts/servers/list.ts b/servers/src/scripts/servers/list.ts new file mode 100644 index 00000000..b37ca56d --- /dev/null +++ b/servers/src/scripts/servers/list.ts @@ -0,0 +1,66 @@ +import { NodeCache } from "@cacheable/node-cache"; +import { promises as fs } from "fs"; +import type { Server } from "./utils"; +import { REMOTE_URLS, USE_REMOTE_URLS_STATUS } from "astro:env/server"; +import type { ServerStatus } from "./info"; + +// Cache with 5 minutes TTL +const cache = new NodeCache({ stdTTL: 60 * 10 }); +// Cache for local servers list +let localCache: Server[] | null = null; + +export async function getRemoteServerStatus(server: Server) { + return ((server as any)["status"] as ServerStatus) || null; +} + +/** + * Loads the local servers.json and merges it with all remote lists. + */ +export async function loadServers( + localPath: string, + remoteUrls: string[] = [] +): Promise { + if (!localCache) { + const localData = await fs.readFile(localPath, "utf-8"); + localCache = JSON.parse(localData); + } + const local = localCache!; + + async function fetchRemote(url: string): Promise { + const cacheKey = `servers:${url}`; + const cached = cache.get(cacheKey); + if (cached) return cached; + + const res = await fetch(url); + console.log(`Fetching remote servers from ${url}`); + if (!res.ok) { + console.warn(`Fetch from ${url} failed: ${res.status}`); + return []; + } + + const data = await res.json(); + if (!("servers" in data)) { + console.warn(`Invalid data format from ${url}:`, data); + return []; + } + const servers = data["servers"].map((e: any) => ({ + remote: true, + ...e, + })) as Server[]; + cache.set(cacheKey, servers); + return servers; + } + + const remoteLists = await Promise.all(remoteUrls.map(fetchRemote)); + + return [...local, ...remoteLists.flat()]; +} + +export async function loadServersFromConfig() { + const localPath = "src/data/servers.json"; + const remotes = REMOTE_URLS?.split(","); + return await loadServers( + localPath, + remotes?.filter((url) => url.trim() !== "") || [] + ); +} diff --git a/servers/src/scripts/servers/utils.ts b/servers/src/scripts/servers/utils.ts new file mode 100644 index 00000000..630f03b2 --- /dev/null +++ b/servers/src/scripts/servers/utils.ts @@ -0,0 +1,18 @@ +import { z } from "astro:content"; + +export const ServerObject = z.object({ + address: z.string().url(), + secure: z.boolean().optional(), + highlighted: z.boolean().optional(), + remote: z.boolean().optional(), +}); + +export type Server = z.infer; + +export function buildServerURL(server: Server, webSocket?: boolean): string { + const protocol = (webSocket ? "ws": "http") + (server.secure ? "s" : ""); + const [hostPort, ...segments] = server.address.split("/"); + const path = segments.length ? `/${segments.join("/")}` : ""; + const hostWithPort = hostPort.includes(":") ? hostPort : `${hostPort}:28006`; + return `${protocol}://${hostWithPort}${path}`; +} diff --git a/servers/tsconfig.json b/servers/tsconfig.json new file mode 100644 index 00000000..8bf91d3b --- /dev/null +++ b/servers/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +}