feat(client): reloadAuth() wrapper
Some checks failed
Security / Security check (push) Failing after 1s

Studio + pilot apps can now trigger a hub-side TokenStore
swap without restarting the daemon:

  await client.reloadAuth();  // returns new token count

Throws when the new config is invalid; the existing store
stays in place server-side so a botched rotation can't lock
the operator out.

Bindings regenerated for the new
HubAdmin.ReloadAuth + ReloadAuthResponse + LoggedEvent
.caller_name proto additions.

dart analyze clean; dart test green (4 tests).

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-29 18:40:20 +02:00
parent 023adcec31
commit db13bdb4a7
5 changed files with 137 additions and 2 deletions

View file

@ -1056,6 +1056,7 @@ class LoggedEvent extends $pb.GeneratedMessage {
$fixnum.Int64? durationMs,
$core.String? error,
$core.String? detail,
$core.String? callerName,
}) {
final result = create();
if (eventId != null) result.eventId = eventId;
@ -1070,6 +1071,7 @@ class LoggedEvent extends $pb.GeneratedMessage {
if (durationMs != null) result.durationMs = durationMs;
if (error != null) result.error = error;
if (detail != null) result.detail = detail;
if (callerName != null) result.callerName = callerName;
return result;
}
@ -1098,6 +1100,7 @@ class LoggedEvent extends $pb.GeneratedMessage {
..aInt64(10, _omitFieldNames ? '' : 'durationMs')
..aOS(11, _omitFieldNames ? '' : 'error')
..aOS(12, _omitFieldNames ? '' : 'detail')
..aOS(13, _omitFieldNames ? '' : 'callerName')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -1230,6 +1233,21 @@ class LoggedEvent extends $pb.GeneratedMessage {
$core.bool hasDetail() => $_has(11);
@$pb.TagNumber(12)
void clearDetail() => $_clearField(12);
/// Operator-friendly name of the token whose holder
/// triggered this event. Empty when the call was anonymous
/// (auth.tokens: not configured), when the event was emitted
/// by hub-internal code (boot housekeeping, federation
/// discovery), or when the row pre-dates the audit-
/// attribution migration (0006).
@$pb.TagNumber(13)
$core.String get callerName => $_getSZ(12);
@$pb.TagNumber(13)
set callerName($core.String value) => $_setString(12, value);
@$pb.TagNumber(13)
$core.bool hasCallerName() => $_has(12);
@$pb.TagNumber(13)
void clearCallerName() => $_clearField(13);
}
class SaveFlowRequest extends $pb.GeneratedMessage {
@ -5967,6 +5985,63 @@ class DefaultScopeResponse extends $pb.GeneratedMessage {
$pb.PbList<$core.String> get knownProviders => $_getList(1);
}
class ReloadAuthResponse extends $pb.GeneratedMessage {
factory ReloadAuthResponse({
$core.int? tokenCount,
}) {
final result = create();
if (tokenCount != null) result.tokenCount = tokenCount;
return result;
}
ReloadAuthResponse._();
factory ReloadAuthResponse.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ReloadAuthResponse.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ReloadAuthResponse',
package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'),
createEmptyInstance: create)
..aI(1, _omitFieldNames ? '' : 'tokenCount', fieldType: $pb.PbFieldType.OU3)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ReloadAuthResponse clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ReloadAuthResponse copyWith(void Function(ReloadAuthResponse) updates) =>
super.copyWith((message) => updates(message as ReloadAuthResponse))
as ReloadAuthResponse;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ReloadAuthResponse create() => ReloadAuthResponse._();
@$core.override
ReloadAuthResponse createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ReloadAuthResponse getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ReloadAuthResponse>(create);
static ReloadAuthResponse? _defaultInstance;
/// Number of tokens loaded into the live store after the
/// swap. Operators sanity-check this against the number
/// they expected to have in `auth.tokens:`.
@$pb.TagNumber(1)
$core.int get tokenCount => $_getIZ(0);
@$pb.TagNumber(1)
set tokenCount($core.int value) => $_setUnsignedInt32(0, value);
@$pb.TagNumber(1)
$core.bool hasTokenCount() => $_has(0);
@$pb.TagNumber(1)
void clearTokenCount() => $_clearField(1);
}
class McpClientConfigEntry extends $pb.GeneratedMessage {
factory McpClientConfigEntry({
$core.String? name,

View file

@ -378,6 +378,20 @@ class HubAdminClient extends $grpc.Client {
return $createUnaryCall(_$setDefaultScope, request, options: options);
}
/// Re-read `auth.tokens:` from operator config + env vars and
/// atomically swap the live token store. Used by operators to
/// rotate tokens without restarting the daemon. Returns the
/// new token count. Errors if the new config is invalid (env
/// var missing, duplicate secret, no scopes, ) the
/// existing store stays in place so a botched rotation can't
/// lock the operator out.
$grpc.ResponseFuture<$0.ReloadAuthResponse> reloadAuth(
$1.Empty request, {
$grpc.CallOptions? options,
}) {
return $createUnaryCall(_$reloadAuth, request, options: options);
}
/// List host services declared in the operator config. Each
/// entry carries name + endpoint; reachability is operator-side
/// (on-demand `fai service status` or Studio probe).
@ -721,6 +735,11 @@ class HubAdminClient extends $grpc.Client {
'/fai.v1.HubAdmin/SetDefaultScope',
($0.SetDefaultScopeRequest value) => value.writeToBuffer(),
$0.DefaultScopeResponse.fromBuffer);
static final _$reloadAuth =
$grpc.ClientMethod<$1.Empty, $0.ReloadAuthResponse>(
'/fai.v1.HubAdmin/ReloadAuth',
($1.Empty value) => value.writeToBuffer(),
$0.ReloadAuthResponse.fromBuffer);
static final _$listServices = $grpc.ClientMethod<$1.Empty, $0.ServiceList>(
'/fai.v1.HubAdmin/ListServices',
($1.Empty value) => value.writeToBuffer(),
@ -1024,6 +1043,13 @@ abstract class HubAdminServiceBase extends $grpc.Service {
($core.List<$core.int> value) =>
$0.SetDefaultScopeRequest.fromBuffer(value),
($0.DefaultScopeResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$1.Empty, $0.ReloadAuthResponse>(
'ReloadAuth',
reloadAuth_Pre,
false,
false,
($core.List<$core.int> value) => $1.Empty.fromBuffer(value),
($0.ReloadAuthResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$1.Empty, $0.ServiceList>(
'ListServices',
listServices_Pre,
@ -1392,6 +1418,14 @@ abstract class HubAdminServiceBase extends $grpc.Service {
$async.Future<$0.DefaultScopeResponse> setDefaultScope(
$grpc.ServiceCall call, $0.SetDefaultScopeRequest request);
$async.Future<$0.ReloadAuthResponse> reloadAuth_Pre(
$grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async {
return reloadAuth($call, await $request);
}
$async.Future<$0.ReloadAuthResponse> reloadAuth(
$grpc.ServiceCall call, $1.Empty request);
$async.Future<$0.ServiceList> listServices_Pre(
$grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async {
return listServices($call, await $request);

View file

@ -356,6 +356,7 @@ const LoggedEvent$json = {
{'1': 'duration_ms', '3': 10, '4': 1, '5': 3, '10': 'durationMs'},
{'1': 'error', '3': 11, '4': 1, '5': 9, '10': 'error'},
{'1': 'detail', '3': 12, '4': 1, '5': 9, '10': 'detail'},
{'1': 'caller_name', '3': 13, '4': 1, '5': 9, '10': 'callerName'},
],
};
@ -367,7 +368,8 @@ final $typed_data.Uint8List loggedEventDescriptor = $convert.base64Decode(
'52b2NhdGlvbklkEhsKCWZsb3dfbmFtZRgGIAEoCVIIZmxvd05hbWUSFwoHc3RlcF9pZBgHIAEo'
'CVIGc3RlcElkEh8KC21vZHVsZV9uYW1lGAggASgJUgptb2R1bGVOYW1lEiUKDm1vZHVsZV92ZX'
'JzaW9uGAkgASgJUg1tb2R1bGVWZXJzaW9uEh8KC2R1cmF0aW9uX21zGAogASgDUgpkdXJhdGlv'
'bk1zEhQKBWVycm9yGAsgASgJUgVlcnJvchIWCgZkZXRhaWwYDCABKAlSBmRldGFpbA==');
'bk1zEhQKBWVycm9yGAsgASgJUgVlcnJvchIWCgZkZXRhaWwYDCABKAlSBmRldGFpbBIfCgtjYW'
'xsZXJfbmFtZRgNIAEoCVIKY2FsbGVyTmFtZQ==');
@$core.Deprecated('Use saveFlowRequestDescriptor instead')
const SaveFlowRequest$json = {
@ -1391,6 +1393,18 @@ final $typed_data.Uint8List defaultScopeResponseDescriptor = $convert.base64Deco
'ChREZWZhdWx0U2NvcGVSZXNwb25zZRIUCgVzY29wZRgBIAMoCVIFc2NvcGUSJwoPa25vd25fcH'
'JvdmlkZXJzGAIgAygJUg5rbm93blByb3ZpZGVycw==');
@$core.Deprecated('Use reloadAuthResponseDescriptor instead')
const ReloadAuthResponse$json = {
'1': 'ReloadAuthResponse',
'2': [
{'1': 'token_count', '3': 1, '4': 1, '5': 13, '10': 'tokenCount'},
],
};
/// Descriptor for `ReloadAuthResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List reloadAuthResponseDescriptor = $convert.base64Decode(
'ChJSZWxvYWRBdXRoUmVzcG9uc2USHwoLdG9rZW5fY291bnQYASABKA1SCnRva2VuQ291bnQ=');
@$core.Deprecated('Use mcpClientConfigEntryDescriptor instead')
const McpClientConfigEntry$json = {
'1': 'McpClientConfigEntry',

View file

@ -188,6 +188,18 @@ class HubClient {
);
}
/// Re-read `auth.tokens:` from operator config + env vars
/// and atomically swap the hub's live token store. Used by
/// operators to rotate tokens without restarting the daemon.
/// Returns the new token count after the swap. Throws when
/// the new config is invalid; the existing store stays in
/// place in that case so a botched rotation can't lock the
/// operator out.
Future<int> reloadAuth() async {
final r = await _admin.reloadAuth(Empty());
return r.tokenCount;
}
/// Server-streaming audit-log subscription. The hub first
/// replays up to [backfill] historical events (oldest-last so
/// the receiver sees them in chronological order), then keeps

View file

@ -1,6 +1,6 @@
name: fai_client_sdk
description: gRPC client SDK for the F∆I Platform hub. Used by F∆I Studio and Tier-3 domain apps.
version: 0.17.0
version: 0.17.1
publish_to: 'none'
repository: https://git.flemming.ai/fai/client-sdk-dart