Files
IKAR/tool/verify_ghost_mode.dart
sevenhill 5423355d83
Tests / test (push) Canceled after 0s
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Native crypto / native-crypto (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
Add verified server rollout and live ghost-mode checks
2026-10-07 22:22:04 +03:00

192 lines
6.2 KiB
Dart

import 'dart:convert';
import 'dart:io';
import '../lib/backend/ikar_transport.dart';
import '../lib/core/transport/ikar_network.dart';
import 'live_ikar_smoke.dart' as live;
Future<void> main() async {
final peers = <live.Device>[];
IkarTransport? visibleConnection;
final base = Uri.parse('https://ikar.kusoft.xyz');
final checks = <String>[];
final client = IkarNetwork.client();
try {
final healthResponse = await (await client.getUrl(
base.resolve('/health'),
)).close();
final health = jsonDecode(await utf8.decodeStream(healthResponse)) as Map;
live.check(
healthResponse.statusCode == 200 && health['status'] == 'ok',
'Public TLS health failed',
);
checks.add('public HTTPS health with certificate validation');
final stamp = DateTime.now().microsecondsSinceEpoch.toString().substring(6);
peers.add(
await live.Device.create(
base,
'node',
'+999${stamp}0',
'Ghost verification',
interactive: false,
),
);
peers.add(
await live.Device.create(
base,
'node',
'+999${stamp}1',
'Presence observer',
),
);
final hidden = peers[0], observer = peers[1];
final chatId = hidden.id ^ observer.id;
await hidden.ok(64, {
'chatId': chatId,
'message': {'text': 'Ghost initial message'},
});
Future<Map> presence(int status) async {
for (var attempt = 0; attempt < 30; attempt++) {
final result = await observer.ok(35, {
'contactIds': [hidden.id],
});
final entry = result['presence']['${hidden.id}'] as Map;
if (entry['status'] == status) return entry;
await Future<void>.delayed(const Duration(milliseconds: 100));
}
throw StateError('Presence failed to reach $status');
}
final initial = await presence(0);
live.check(
initial['seen'] == 0,
'Invisible first connection exposed activity',
);
checks.add(
'invisible login and SignalR connection expose neither online nor last seen',
);
await hidden.ok(1, {'interactive': true});
final active = await presence(1);
live.check(
(active['seen'] as num) > 0,
'Visible ping failed to publish activity',
);
await Future<void>.delayed(const Duration(milliseconds: 1100));
await hidden.ok(1, {'interactive': false});
final inactive = await presence(0);
live.check(
inactive['seen'] == active['seen'],
'Ghost ping changed last seen',
);
checks.add('switching to ghost freezes the previous public timestamp');
hidden.transport.interactive = false;
await hidden.transport.connectEvents();
await hidden.ok(1, {'interactive': false});
live.check(
(await presence(0))['seen'] == active['seen'],
'Reconnect exposed activity',
);
checks.add('invisible event reconnection retains hidden presence');
final delivered = hidden.transport.events
.firstWhere(
(event) =>
event['opcode'] == 128 &&
event['payload']?['message']?['text'] == 'Delivery to ghost',
)
.timeout(const Duration(seconds: 15));
final sent = await observer.ok(64, {
'chatId': chatId,
'message': {'text': 'Delivery to ghost'},
});
await delivered;
final history = await hidden.ok(49, {'chatId': chatId});
live.check(
(history['messages'] as List).any(
(message) => message['id'] == sent['message']['id'],
),
'Ghost history missed delivered message',
);
await hidden.ok(64, {
'chatId': chatId,
'message': {'text': 'Reply from ghost'},
});
live.check(
(await presence(0))['seen'] == active['seen'],
'Ghost messaging exposed security session activity',
);
checks.add(
'invisible client receives messages, reads history and replies without exposing activity',
);
final incoming = hidden.transport.events
.firstWhere((event) => event['opcode'] == 137)
.timeout(const Duration(seconds: 15));
final call = await observer.ok(78, {
'calleeIds': [hidden.id],
'isVideo': false,
'conversationId': 'ikar-ghost-$stamp',
});
final invitation = await incoming;
live.check(
invitation['payload']['callerId'] == observer.id,
'Ghost call invitation has wrong caller',
);
final endpoint =
jsonDecode(call['internalCallerParams'])['endpoint'] as String;
final callSocket = await IkarNetwork.connectWebSocket(endpoint);
final callSubscription = callSocket.listen((_) {});
try {
callSocket.add(
jsonEncode({'command': 'hangup', 'sequence': 1, 'reason': 'FINISHED'}),
);
await Future<void>.delayed(const Duration(milliseconds: 200));
} finally {
await callSocket.close();
await callSubscription.cancel();
}
live.check(
(await presence(0))['seen'] == active['seen'],
'Incoming call exposed ghost activity',
);
checks.add(
'invisible client receives a call invitation without exposing activity',
);
visibleConnection = IkarTransport(base)
..token = hidden.transport.token
..interactive = true;
await visibleConnection.connectEvents();
await presence(1);
await visibleConnection.close();
visibleConnection = null;
await presence(0);
checks.add(
'disconnecting a visible connection preserves the invisible connection',
);
await hidden.ok(1, {'interactive': true});
final restored = await presence(1);
live.check(
(restored['seen'] as num) > (active['seen'] as num),
'Visible activity did not resume',
);
checks.add('disabling ghost resumes online and public last seen');
final report = {
'passed': true,
'checks': checks,
'timestamp': DateTime.now().toUtc().toIso8601String(),
};
await File(
'artifacts/ghost-live-verification.json',
).writeAsString(const JsonEncoder.withIndent(' ').convert(report));
stdout.writeln(jsonEncode(report));
} finally {
await visibleConnection?.close();
client.close(force: true);
for (final peer in peers.reversed) {
try {
await peer.ok(199, {});
} finally {
await peer.close();
}
}
}
}