feat(base)!: sign-ups as the operator says, waiting accounts, operators (ADR 0021, step 1)

SIGN_UP takes open (unset), approval or closed; anything else stops the
server as it starts. Closed refuses a SimpleLogin sign-up before its
handle is counted, an address sign-up only once the code has proved the
address, so nothing tells whether an address has an account. Approval
lets a new account wait, at most ten at once: no session is issued for a
waiting or suspended account, only a pass (32 random bytes, kept as its
SHA-256, 30 days, five per account) and four BIP-39 words the operator
reads out to match it. The pass looks, forgets, and deletes an account
that was never approved; nothing else.

Operators hold the scope operator, given first by SQL (D32), then named
in the app: InstanceOperatorEndpoint lists, approves and refuses waiting
accounts, finds accounts, suspends them (ending their sessions), and
names and takes back operators, never the last. account.operating()
counts the waiting for the badge. Members ask the register (five new
operations, a refusal kind signUp), and say closed while it is out of
reach. Never-approved accounts go after 14 days in the daily pruning.

The migration only adds the tables waiting_account and waiting_pass, so
the server before it keeps working on the new schema.

BREAKING CHANGE: a register and its members must run this version
together: a member asks the register operations an older one doesn't
know.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-25 15:04:21 +01:00
commit a060fbc21d
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -14,6 +14,8 @@
import 'dart:async' as _ida;
import 'dart:io' as _idi;
import 'dart:typed_data' as _idt;
import 'package:halkyon_base_server/src/generated/admission/waiting_state.dart'
as _iuvj70hb;
import 'package:halkyon_base_server/src/generated/auth/email_code_purpose.dart'
as _i5775c0l;
import 'package:halkyon_base_server/src/generated/auth/sign_in_ways.dart'
@ -32,6 +34,8 @@ import 'package:halkyon_base_server/src/generated/keys/keys_proof.dart'
as _iy592cuk;
import 'package:halkyon_base_server/src/generated/keys/keys_setup.dart'
as _ii64ejy6;
import 'package:halkyon_base_server/src/generated/operator/account_overview.dart'
as _iiuvgbjo;
import 'package:halkyon_base_server/src/generated/password/authenticator.dart'
as _ip34xal6;
import 'package:halkyon_base_server/src/generated/password/authenticator_enrolment.dart'
@ -200,6 +204,8 @@ void withServerpod(
}
class TestEndpoints {
late final _WaitingEndpoint waiting;
late final _AccountEndpoint account;
late final _EmailCodeEndpoint emailCode;
@ -208,6 +214,8 @@ class TestEndpoints {
late final _KeysEndpoint keys;
late final _InstanceOperatorEndpoint instanceOperator;
late final _AccountRecoveryEndpoint accountRecovery;
late final _AuthenticatorEndpoint authenticator;
@ -230,6 +238,10 @@ class _InternalTestEndpoints extends TestEndpoints
_is.SerializationManager serializationManager,
_is.EndpointDispatch endpoints,
) {
waiting = _WaitingEndpoint(
endpoints,
serializationManager,
);
account = _AccountEndpoint(
endpoints,
serializationManager,
@ -246,6 +258,10 @@ class _InternalTestEndpoints extends TestEndpoints
endpoints,
serializationManager,
);
instanceOperator = _InstanceOperatorEndpoint(
endpoints,
serializationManager,
);
accountRecovery = _AccountRecoveryEndpoint(
endpoints,
serializationManager,
@ -277,6 +293,110 @@ class _InternalTestEndpoints extends TestEndpoints
}
}
class _WaitingEndpoint {
_WaitingEndpoint(
this._endpointDispatch,
this._serializationManager,
);
final _is.EndpointDispatch _endpointDispatch;
final _is.SerializationManager _serializationManager;
_ida.Future<_iuvj70hb.WaitingState> look(
_ist.TestSessionBuilder sessionBuilder, {
required String pass,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'waiting',
method: 'look',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'waiting',
methodName: 'look',
parameters: _ist.testObjectToJson({'pass': pass}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_iuvj70hb.WaitingState>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<bool> deleteAccount(
_ist.TestSessionBuilder sessionBuilder, {
required String pass,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'waiting',
method: 'deleteAccount',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'waiting',
methodName: 'deleteAccount',
parameters: _ist.testObjectToJson({'pass': pass}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<bool>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<void> forget(
_ist.TestSessionBuilder sessionBuilder, {
required String pass,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'waiting',
method: 'forget',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'waiting',
methodName: 'forget',
parameters: _ist.testObjectToJson({'pass': pass}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
}
class _AccountEndpoint {
_AccountEndpoint(
this._endpointDispatch,
@ -351,6 +471,34 @@ class _AccountEndpoint {
}
});
}
_ida.Future<int?> operating(_ist.TestSessionBuilder sessionBuilder) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'account',
method: 'operating',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'account',
methodName: 'operating',
parameters: _ist.testObjectToJson({}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<int?>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
}
class _EmailCodeEndpoint {
@ -785,6 +933,263 @@ class _KeysEndpoint {
}
}
class _InstanceOperatorEndpoint {
_InstanceOperatorEndpoint(
this._endpointDispatch,
this._serializationManager,
);
final _is.EndpointDispatch _endpointDispatch;
final _is.SerializationManager _serializationManager;
_ida.Future<List<_iiuvgbjo.AccountOverview>> waiting(
_ist.TestSessionBuilder sessionBuilder,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'instanceOperator',
method: 'waiting',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'instanceOperator',
methodName: 'waiting',
parameters: _ist.testObjectToJson({}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<List<_iiuvgbjo.AccountOverview>>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<void> approve(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue account,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'instanceOperator',
method: 'approve',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'instanceOperator',
methodName: 'approve',
parameters: _ist.testObjectToJson({'account': account}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<void> refuse(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue account,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'instanceOperator',
method: 'refuse',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'instanceOperator',
methodName: 'refuse',
parameters: _ist.testObjectToJson({'account': account}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<List<_iiuvgbjo.AccountOverview>> accounts(
_ist.TestSessionBuilder sessionBuilder, {
required String startingWith,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'instanceOperator',
method: 'accounts',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'instanceOperator',
methodName: 'accounts',
parameters: _ist.testObjectToJson({'startingWith': startingWith}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<List<_iiuvgbjo.AccountOverview>>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<void> suspend(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue account,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'instanceOperator',
method: 'suspend',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'instanceOperator',
methodName: 'suspend',
parameters: _ist.testObjectToJson({'account': account}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<List<_iiuvgbjo.AccountOverview>> operators(
_ist.TestSessionBuilder sessionBuilder,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'instanceOperator',
method: 'operators',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'instanceOperator',
methodName: 'operators',
parameters: _ist.testObjectToJson({}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<List<_iiuvgbjo.AccountOverview>>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<void> appoint(
_ist.TestSessionBuilder sessionBuilder, {
required String handle,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'instanceOperator',
method: 'appoint',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'instanceOperator',
methodName: 'appoint',
parameters: _ist.testObjectToJson({'handle': handle}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<void> takeBack(
_ist.TestSessionBuilder sessionBuilder, {
required _is.UuidValue account,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'instanceOperator',
method: 'takeBack',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'instanceOperator',
methodName: 'takeBack',
parameters: _ist.testObjectToJson({'account': account}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
}
class _AccountRecoveryEndpoint {
_AccountRecoveryEndpoint(
this._endpointDispatch,