feat(base,app,learn,todo,train,meditate): agent accounts with tokens

ADR 0044's step 6a (D42, D43, D45): accounts for programs, such as AI
assistants, that an identified person makes from the Profile.

halkyon_base keeps them on the instance's register, which its members
ask: an agent is an account (agent_account: its owner, its name, its
public keys, set by itself once) with one token (agent_token: 32
random bytes, kept as their SHA-256 alone, shown once). agents.create,
mine, newToken, revoke, delete and setKeys serve its owner;
agentSignIn.signIn exchanges the token for an ordinary server-side
session of a day, with the scopes identified and agent, at most 60
sign-ins an hour from a network. Revoking a token or making a new one
ends the agent's sessions; a token unused for 90 days lapses in every
service's pruning. Up to 5 agents a person, where the operator didn't
switch them off (AGENTS=off; said on the privacy page and in
instance.basics). An agent makes no agents, creates or owns no shared
space, is invited and raised as a Viewer alone, is never handed a
space on, is named no operator and granted no assistants; each refusal
has its test. Deleting a person deletes their agents first; an agent's
public profile names whose agent it is.

In the apps, every Profile has an Agents card (halkyon_app's
AgentsSection): made with a name and a handle, its token shown once to
copy, a new token, the token taken back, deleted. Learn's Share sheet
marks an agent among the members and offers it no role above Viewer.

The migrations, the module's and each host's (Learn, Meditate, Todo,
Train), are additive alone: CREATE TABLE "agent_account" with a unique
index on "agentId" (one row an agent account) and one on "ownerId" (a
person's agents, and deleting them with the person), and CREATE TABLE
"agent_token" with unique indexes on "agentId" (one token an agent)
and "tokenHash" (the sign-in finds the token by its hash). No existing
table is touched, so the server before the deploy keeps working on the
migrated database.

Whats-New: Make agents in your Profile: accounts for programs such as AI assistants, which sign in with a token and see only what you share with them.
Whats-New-De: Leg in deinem Profil Agenten an: Konten für Programme wie KI-Assistenten, die sich mit einem Token anmelden und nur sehen, was du mit ihnen teilst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-11 19:55:27 +01:00
commit 7aee78bd87
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
55 changed files with 113257 additions and 50 deletions

View file

@ -17,6 +17,15 @@ export 'src/admission/admission.dart'
unapprovedSignUpLifetime,
waitingPassLifetime;
export 'src/admission/sign_up_config.dart';
export 'src/agent/agents.dart'
show
agentSessionLifetime,
agentSignInMethod,
agentTokenLapsesAfter,
agentTokenPrefix,
forgetLapsedAgentTokens,
maxAgentsPerPerson;
export 'src/agent/agents_config.dart';
export 'src/assistant/assistant_conversation.dart'
show
assistantImageMaxBytes,

View file

@ -0,0 +1,26 @@
### An agent (Halkyon's ADR 0044, D6, D45): an account of the register that
### its owner made from their Profile, signing in with a token alone
### (`agent_token`), never a password. Its public keys are its own, set by
### the agent once where it runs; it has no others.
class: AgentAccount
serverOnly: true
table: agent_account
fields:
### The agent's account: its AuthUser's id.
agentId: UuidValue
### The person who made it, and whose it is.
ownerId: UuidValue
### What its owner called it.
name: String
createdAt: DateTime
### X25519's public key, which shared keys are sealed to; null until the
### agent set its keys.
encryptionPublicKey: ByteData?
### Ed25519's public key, which its signatures check against.
signingPublicKey: ByteData?
indexes:
agent_account_agent_idx:
fields: agentId
unique: true
agent_account_owner_idx:
fields: ownerId

View file

@ -0,0 +1,14 @@
### An agent as its owner's Profile lists it (Halkyon's ADR 0044, D45).
class: AgentInfo
fields:
### The agent's account.
id: UuidValue
name: String
handle: String
createdAt: DateTime
### When it last signed in with its token; null while never.
lastUsedAt: DateTime?
### Whether it has a token: revoked, it has none until a new one is made.
hasToken: bool
### Whether it set its public keys.
hasKeys: bool

View file

@ -0,0 +1,5 @@
### An agent just made, or given a new token: the token, shown this once.
class: AgentMade
fields:
agent: AgentInfo
token: String

View file

@ -0,0 +1,19 @@
### Why a call about agents was refused (Halkyon's ADR 0044, D45).
enum: AgentRefusal
serialized: byName
values:
### The operator switched agents off (`AGENTS=off`).
- agentsOff
### The person has as many agents as they may (5).
- tooMany
### An agent asked for what only people do, or a person for what only
### agents do.
- notForAgents
### No agent of yours is that: another person's answers alike.
- noSuchAgent
### No token is that, or it lapsed.
- notValid
### The agent set its keys already.
- keysSet
### Too many sign-ins from this network this hour.
- tooManyAttempts

View file

@ -0,0 +1,4 @@
### A call about agents was refused; nothing changed.
exception: AgentRefusedException
fields:
reason: AgentRefusal

View file

@ -0,0 +1,21 @@
import 'package:halkyon_base_server/src/agent/agents.dart';
import 'package:halkyon_base_server/src/common/network.dart';
import 'package:halkyon_base_server/src/register/account_register.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';
/// An agent signing in with its token (Halkyon's ADR 0044, D43): the
/// register checks the token and stamps its use, and this server makes a
/// session lasting a day, as an agent.
class AgentSignInEndpoint extends Endpoint {
/// A session for the agent [token] is of. Refused for a token that is
/// none or lapsed (`AgentRefusal.notValid`), where the operator switched
/// agents off (`agentsOff`), and past this network's sign-ins this hour
/// (`tooManyAttempts`).
Future<AuthSuccess> signIn(Session session, {required String token}) async {
final agent = await AccountRegister.of(
session.serverpod,
).agentWithToken(session, token: token.trim(), network: networkOf(session));
return await issueAgentSession(session, agent);
}
}

View file

@ -0,0 +1,21 @@
### The token an agent signs in with (Halkyon's ADR 0044, D12, D43, D45):
### one an agent, 32 random bytes shown to its owner once, kept as their
### SHA-256 alone. It lapses 90 days after it was last used.
class: AgentToken
serverOnly: true
table: agent_token
fields:
### The agent's account.
agentId: UuidValue
### SHA-256 of the token as the agent sends it, in hex.
tokenHash: String
createdAt: DateTime
### When the agent last signed in with it; null while never.
lastUsedAt: DateTime?
indexes:
agent_token_agent_idx:
fields: agentId
unique: true
agent_token_hash_idx:
fields: tokenHash
unique: true

View file

@ -0,0 +1,525 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:clock/clock.dart';
import 'package:crypto/crypto.dart';
import 'package:halkyon_base_server/src/agent/agents_config.dart';
import 'package:halkyon_base_server/src/auth/account.dart';
import 'package:halkyon_base_server/src/auth/scopes.dart';
import 'package:halkyon_base_server/src/generated/protocol.dart';
import 'package:halkyon_base_server/src/profile/first_handle.dart';
import 'package:halkyon_base_server/src/profile/profile_rules.dart';
import 'package:halkyon_base_server/src/register/people.dart';
import 'package:halkyon_base_server/src/register/sessions_ended.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';
// Agents as the register keeps them (Halkyon's ADR 0044, D6, D12, D43,
// D45): what `LocalRegister` answers, and a member asks it for.
/// How many agents one person may have.
const maxAgentsPerPerson = 5;
/// How long a token stays valid after it was last used (D12), or made.
const agentTokenLapsesAfter = Duration(days: 90);
/// How long a session an agent signed in for lasts: it signs in at each
/// start, so a stolen session is good for a day at most.
const agentSessionLifetime = Duration(days: 1);
/// What a token begins with, so that it is recognised as one, by people and
/// by secret scanners alike.
const agentTokenPrefix = 'hkagent_';
/// The method an agent's sessions are made by, as Serverpod names it.
const agentSignInMethod = 'agent';
/// How many times one network (IP address) signs an agent in within an
/// hour. A token has 256 random bits, so nobody guesses one: this only
/// keeps a flood of sign-ins off the database.
final _signInsPerNetwork = DatabaseRateLimiter(
RateLimiterConfig(
domain: 'agent_sign_in',
source: 'per_network',
maxAttempts: 60,
timeframe: const Duration(hours: 1),
),
);
Never _refused(AgentRefusal reason) =>
throw AgentRefusedException(reason: reason);
/// A new token: [agentTokenPrefix] and 32 random bytes, base64url.
String _newToken() {
final random = Random.secure();
return agentTokenPrefix +
base64Url
.encode([for (var i = 0; i < 32; i++) random.nextInt(256)])
.replaceAll('=', '');
}
/// What the register keeps of [token]: SHA-256, in hex. The token is random
/// enough that no pepper is needed.
String _hashOf(String token) => sha256.convert(utf8.encode(token)).toString();
/// Whether [token], last used or made as it says, has lapsed at [now].
bool _lapsed(AgentToken token, DateTime now) =>
!(token.lastUsedAt ?? token.createdAt)
.add(agentTokenLapsesAfter)
.isAfter(now);
/// Makes an agent of [owner] called [name] with [handle] (D45), and its
/// token, which the answer alone holds. Refused where the operator
/// switched agents off, for an agent, past [maxAgentsPerPerson], and for a
/// handle or name a profile couldn't have (`ProfileRefusedException`); an
/// answer about a handle counts as one (`countHandleCheck`, from
/// [network]).
Future<AgentMade> createAgent(
Session session, {
required UuidValue owner,
required String name,
required String handle,
required String network,
}) async {
if (!agentsOfferedOn(session.serverpod)) {
_refused(AgentRefusal.agentsOff);
}
final displayName = name.trim().isEmpty ? handle : name.trim();
if (profileRefusal(
handle: handle,
displayName: displayName,
bio: '',
links: const [],
)
case final reason?) {
throw ProfileRefusedException(reason: reason);
}
await _refuseAgentOwner(session, owner);
await _refuseTooMany(session, owner);
if (await refusalOfNewHandle(
session,
handle,
asker: owner.uuid,
network: network,
)
case final reason?) {
throw ProfileRefusedException(reason: reason);
}
final token = _newToken();
final agent = const Uuid().v7obj();
final now = clock.now().toUtc();
await session.db.transaction((transaction) async {
// The owner's row, locked, so that two agents made at once count each
// other.
// SELECT … FROM "serverpod_auth_core_user" WHERE "id" = $1 FOR UPDATE
await AuthUser.db.findById(
session,
owner,
transaction: transaction,
lockMode: LockMode.forUpdate,
);
await _refuseTooMany(session, owner, transaction: transaction);
await ensurePerson(
session,
agent,
scopes: {identified, agentScope},
transaction: transaction,
);
// Its profile is public from the start, under the name its owner gave
// it: members see whom they invite.
// INSERT INTO "public_profile" (…) VALUES (…)
await PublicProfile.db.insertRow(
session,
PublicProfile(
authUserId: agent,
handle: handle,
handleKey: handle.toLowerCase(),
displayName: displayName,
bio: '',
links: const [],
verifiedLinks: const [],
updatedAt: now,
),
transaction: transaction,
);
// INSERT INTO "agent_account" (…) VALUES (…)
await AgentAccount.db.insertRow(
session,
AgentAccount(
agentId: agent,
ownerId: owner,
name: displayName,
createdAt: now,
),
transaction: transaction,
);
// INSERT INTO "agent_token" (…) VALUES (…)
await AgentToken.db.insertRow(
session,
AgentToken(agentId: agent, tokenHash: _hashOf(token), createdAt: now),
transaction: transaction,
);
});
return AgentMade(agent: await _infoOf(session, owner, agent), token: token);
}
/// Refuses [person] where they are an agent: agents make none.
Future<void> _refuseAgentOwner(Session session, UuidValue person) async {
// SELECT count(*) FROM "agent_account" WHERE "agentId" = $1
if (await AgentAccount.db.count(
session,
where: (t) => t.agentId.equals(person),
) >
0) {
_refused(AgentRefusal.notForAgents);
}
}
/// Refuses [owner] where they have [maxAgentsPerPerson] agents.
Future<void> _refuseTooMany(
Session session,
UuidValue owner, {
Transaction? transaction,
}) async {
// SELECT count(*) FROM "agent_account" WHERE "ownerId" = $1
if (await AgentAccount.db.count(
session,
where: (t) => t.ownerId.equals(owner),
transaction: transaction,
) >=
maxAgentsPerPerson) {
_refused(AgentRefusal.tooMany);
}
}
/// [owner]'s agents, the first made first.
Future<List<AgentInfo>> agentsOf(Session session, UuidValue owner) async {
// SELECT … FROM "agent_account" WHERE "ownerId" = $1
// ORDER BY "createdAt", "agentId"
final rows = await AgentAccount.db.find(
session,
where: (t) => t.ownerId.equals(owner),
orderBy: (t) => t.createdAt,
);
return await _infos(session, rows);
}
Future<List<AgentInfo>> _infos(
Session session,
List<AgentAccount> rows,
) async {
if (rows.isEmpty) {
return const [];
}
final ids = <UuidValue>{for (final row in rows) row.agentId};
// SELECT … FROM "agent_token" WHERE "agentId" = ANY($1)
final tokens = {
for (final token in await AgentToken.db.find(
session,
where: (t) => t.agentId.inSet(ids),
))
token.agentId: token,
};
// SELECT … FROM "public_profile" WHERE "authUserId" = ANY($1)
final handles = {
for (final profile in await PublicProfile.db.find(
session,
where: (t) => t.authUserId.inSet(ids),
))
profile.authUserId: profile.handle,
};
return [
for (final row in rows)
AgentInfo(
id: row.agentId,
name: row.name,
handle: handles[row.agentId] ?? '',
createdAt: row.createdAt,
lastUsedAt: tokens[row.agentId]?.lastUsedAt,
hasToken: tokens.containsKey(row.agentId),
hasKeys: row.encryptionPublicKey != null,
),
];
}
/// [owner]'s agent [agent]; refused as none where it is no agent of theirs.
Future<AgentAccount> _ownedAgent(
Session session,
UuidValue owner,
UuidValue agent, {
Transaction? transaction,
}) async =>
// SELECT … FROM "agent_account" WHERE "agentId" = $1 AND "ownerId" = $2
await AgentAccount.db.findFirstRow(
session,
where: (t) => t.agentId.equals(agent) & t.ownerId.equals(owner),
transaction: transaction,
) ??
_refused(AgentRefusal.noSuchAgent);
Future<AgentInfo> _infoOf(
Session session,
UuidValue owner,
UuidValue agent,
) async =>
(await _infos(session, [await _ownedAgent(session, owner, agent)])).single;
/// Gives [owner]'s agent [agent] a new token, which the answer alone holds:
/// the one before ends, with every session of the agent's, here and on the
/// members.
Future<AgentMade> newAgentToken(
Session session, {
required UuidValue owner,
required UuidValue agent,
}) async {
await _ownedAgent(session, owner, agent);
final token = _newToken();
await _endTokenAndSessions(
session,
agent,
then: (transaction) async {
// INSERT INTO "agent_token" (…) VALUES (…)
await AgentToken.db.insertRow(
session,
AgentToken(
agentId: agent,
tokenHash: _hashOf(token),
createdAt: clock.now().toUtc(),
),
transaction: transaction,
);
},
);
return AgentMade(agent: await _infoOf(session, owner, agent), token: token);
}
/// Takes [owner]'s agent [agent]'s token back: it signs in no more until
/// it gets a new one, and its sessions end, here and on the members.
Future<void> revokeAgentToken(
Session session, {
required UuidValue owner,
required UuidValue agent,
}) async {
await _ownedAgent(session, owner, agent);
await _endTokenAndSessions(session, agent);
}
Future<void> _endTokenAndSessions(
Session session,
UuidValue agent, {
Future<void> Function(Transaction transaction)? then,
}) async {
await session.db.transaction((transaction) async {
// DELETE FROM "agent_token" WHERE "agentId" = $1
await AgentToken.db.deleteWhere(
session,
where: (t) => t.agentId.equals(agent),
transaction: transaction,
);
await endSessionsEverywhere(session, agent, transaction: transaction);
await then?.call(transaction);
});
await announceSessionsEnded(session, agent);
}
/// Deletes [owner]'s agent [agent], as an account is deleted.
Future<void> deleteAgent(
Session session, {
required UuidValue owner,
required UuidValue agent,
}) async {
await _ownedAgent(session, owner, agent);
await deletePerson(session, agent);
}
/// Deletes [owner]'s agents, as accounts are deleted: before the owner.
Future<void> deleteAgentsOf(Session session, UuidValue owner) async {
// SELECT … FROM "agent_account" WHERE "ownerId" = $1
for (final row in await AgentAccount.db.find(
session,
where: (t) => t.ownerId.equals(owner),
)) {
await deletePerson(session, row.agentId);
}
}
/// Forgets what the register keeps of [agent] as an agent, as its account
/// goes, in [transaction].
Future<void> forgetAgent(
Session session,
UuidValue agent, {
required Transaction transaction,
}) async {
// DELETE FROM "agent_token" WHERE "agentId" = $1
await AgentToken.db.deleteWhere(
session,
where: (t) => t.agentId.equals(agent),
transaction: transaction,
);
// DELETE FROM "agent_account" WHERE "agentId" = $1
await AgentAccount.db.deleteWhere(
session,
where: (t) => t.agentId.equals(agent),
transaction: transaction,
);
}
/// The agent [token] signs in, its last use stamped now; refused for a
/// token that is none or lapsed, where the operator switched agents off,
/// and past [network]'s sign-ins this hour.
Future<UuidValue> agentWithToken(
Session session, {
required String token,
required String network,
}) async {
if (!agentsOfferedOn(session.serverpod)) {
_refused(AgentRefusal.agentsOff);
}
if (!await _signInsPerNetwork.tryRecordAttempt(session, key: network)) {
_refused(AgentRefusal.tooManyAttempts);
}
if (!token.startsWith(agentTokenPrefix)) {
_refused(AgentRefusal.notValid);
}
final now = clock.now().toUtc();
return await session.db.transaction((transaction) async {
// SELECT … FROM "agent_token" WHERE "tokenHash" = $1 FOR UPDATE
final row = await AgentToken.db.findFirstRow(
session,
where: (t) => t.tokenHash.equals(_hashOf(token)),
transaction: transaction,
lockMode: LockMode.forUpdate,
);
if (row == null || _lapsed(row, now)) {
_refused(AgentRefusal.notValid);
}
// UPDATE "agent_token" SET "lastUsedAt" = $1 WHERE "id" = $2
await AgentToken.db.updateRow(
session,
row.copyWith(lastUsedAt: now),
columns: (t) => [t.lastUsedAt],
transaction: transaction,
);
return row.agentId;
});
}
/// Sets [agent]'s public keys (D6), once; refused for a person, and for an
/// agent whose keys are set. Throws an [ArgumentError] for a key that isn't
/// 32 bytes.
Future<void> setAgentKeys(
Session session, {
required UuidValue agent,
required ByteData encryption,
required ByteData signing,
}) async {
if (encryption.lengthInBytes != 32 || signing.lengthInBytes != 32) {
throw ArgumentError("An agent's public keys are 32 bytes each.");
}
await session.db.transaction((transaction) async {
// SELECT … FROM "agent_account" WHERE "agentId" = $1 FOR UPDATE
final row =
await AgentAccount.db.findFirstRow(
session,
where: (t) => t.agentId.equals(agent),
transaction: transaction,
lockMode: LockMode.forUpdate,
) ??
_refused(AgentRefusal.notForAgents);
if (row.encryptionPublicKey != null) {
_refused(AgentRefusal.keysSet);
}
// UPDATE "agent_account" SET "encryptionPublicKey" = $1,
// "signingPublicKey" = $2 WHERE "id" = $3
await AgentAccount.db.updateRow(
session,
row.copyWith(encryptionPublicKey: encryption, signingPublicKey: signing),
columns: (t) => [t.encryptionPublicKey, t.signingPublicKey],
transaction: transaction,
);
});
}
/// The public keys of those of [people] that are agents with keys set.
Future<Map<UuidValue, AccountPublicKeys>> agentPublicKeysOf(
Session session,
Set<UuidValue> people,
) async {
if (people.isEmpty) {
return const {};
}
// SELECT … FROM "agent_account" WHERE "agentId" = ANY($1)
return {
for (final row in await AgentAccount.db.find(
session,
where: (t) => t.agentId.inSet(people),
))
if ((row.encryptionPublicKey, row.signingPublicKey) case (
final encryption?,
final signing?,
))
row.agentId: AccountPublicKeys(
encryption: encryption,
signing: signing,
),
};
}
/// Those of [people] that are agents, each with its owner.
Future<Map<UuidValue, UuidValue>> agentOwnersOf(
Session session,
Set<UuidValue> people,
) async {
if (people.isEmpty) {
return const {};
}
// SELECT … FROM "agent_account" WHERE "agentId" = ANY($1)
return {
for (final row in await AgentAccount.db.find(
session,
where: (t) => t.agentId.inSet(people),
))
row.agentId: row.ownerId,
};
}
/// Forgets the tokens unused for [agentTokenLapsesAfter]; how many. The
/// hourly pruning runs it; their agents stay, for a new token.
Future<int> forgetLapsedAgentTokens(Session session) async {
final cutoff = clock.now().toUtc().subtract(agentTokenLapsesAfter);
// DELETE FROM "agent_token" WHERE "lastUsedAt" <= $1
// OR ("lastUsedAt" IS NULL AND "createdAt" <= $1)
final gone = await AgentToken.db.deleteWhere(
session,
where: (t) =>
(t.lastUsedAt <= cutoff) |
(t.lastUsedAt.equals(null) & (t.createdAt <= cutoff)),
);
return gone.length;
}
/// A session for the agent [agent] here (D43), made a person here first
/// where it isn't one, with its scopes, lasting [agentSessionLifetime].
Future<AuthSuccess> issueAgentSession(
Session session,
UuidValue agent,
) async {
final scopes = {identified, agentScope};
await ensurePerson(session, agent, scopes: scopes);
final managers = AuthServices.instance.tokenManager;
final sessions = switch (managers) {
MultiTokenManager(
primaryTokenManager: final ServerSideSessionsTokenManager primary,
) =>
primary.serverSideSessions,
_ => throw StateError('Agents sign in to server-side sessions alone.'),
};
return await sessions.createSession(
session,
authUserId: agent,
method: agentSignInMethod,
scopes: scopes,
expiresAt: clock.now().toUtc().add(agentSessionLifetime),
);
}

View file

@ -0,0 +1,29 @@
import 'dart:io';
import 'package:serverpod/serverpod.dart';
/// Whether people may make agents on [pod]'s instance (Halkyon's ADR 0044,
/// D45): unless its operator switched them off (`AGENTS=off`). The
/// register's alone counts: a member asks the register. Throws a
/// [StateError] for a value it doesn't know ([agentsOfferedFrom]).
bool agentsOfferedOn(Serverpod pod) => switch (_offeredOf[pod]) {
(:final offered?) => offered,
_ => agentsOfferedFrom(Platform.environment['AGENTS']),
};
/// Whether [value] (`AGENTS`), whatever its case, offers agents: `on`, or
/// none (unset or empty, as Compose passes an unset variable); `off`
/// refuses them. Throws a [StateError] for anything else.
bool agentsOfferedFrom(String? value) =>
switch ((value ?? '').trim().toLowerCase()) {
'' || 'on' => true,
'off' => false,
_ => throw StateError('AGENTS is "$value": it is on or off.'),
};
/// Puts [offered] in place of the operator's `AGENTS` on [pod], null
/// meaning what the environment says: tests do.
void useAgents(Serverpod pod, bool? offered) =>
_offeredOf[pod] = (offered: offered);
final _offeredOf = Expando<({bool? offered})>('Agents offered');

View file

@ -0,0 +1,90 @@
import 'dart:typed_data';
import 'package:halkyon_base_server/src/auth/scopes.dart';
import 'package:halkyon_base_server/src/common/network.dart';
import 'package:halkyon_base_server/src/generated/protocol.dart';
import 'package:halkyon_base_server/src/register/account_register.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';
/// A person's agents (Halkyon's ADR 0044, D6, D12, D45), made and taken
/// back from their Profile, and an agent's own keys. Everything goes
/// through the register, which keeps agents.
class AgentsEndpoint extends Endpoint {
@override
Set<Scope> get requiredScopes => {identified};
/// Makes an agent called [name] with [handle], with you its owner; its
/// token is in the answer alone (D45). Refused where the operator
/// switched agents off (`AgentRefusal.agentsOff`), past 5 agents
/// (`tooMany`), to an agent (`notForAgents`), and for a handle or name a
/// profile couldn't have (`ProfileRefusedException`); counts as an
/// answer about a handle.
Future<AgentMade> create(
Session session, {
required String name,
required String handle,
}) => _register(session).createAgent(
session,
owner: _person(session),
name: name,
handle: handle.trim(),
network: networkOf(session),
);
/// Your agents, the first made first.
Future<List<AgentInfo>> mine(Session session) =>
_register(session).agentsOf(session, _person(session));
/// A new token for your agent [agent], in the answer alone: the one
/// before ends, and the agent's sessions with it. Refused for an agent
/// not yours (`noSuchAgent`).
Future<AgentMade> newToken(Session session, {required UuidValue agent}) =>
_register(
session,
).newAgentToken(session, owner: _person(session), agent: agent);
/// Takes your agent [agent]'s token back: it signs in no more, and its
/// sessions end. Refused for an agent not yours (`noSuchAgent`).
Future<void> revoke(Session session, {required UuidValue agent}) => _register(
session,
).revokeAgentToken(session, owner: _person(session), agent: agent);
/// Deletes your agent [agent], as an account is deleted. Refused for an
/// agent not yours (`noSuchAgent`).
Future<void> delete(Session session, {required UuidValue agent}) => _register(
session,
).deleteAgent(session, owner: _person(session), agent: agent);
/// Sets your public keys, as an agent, once (D6): what members seal
/// shared keys to, pinning them on first sight. Refused for a person
/// (`notForAgents`) and once set (`keysSet`).
Future<void> setKeys(
Session session, {
required ByteData encryption,
required ByteData signing,
}) async {
final caller = session.authenticated!;
if (!caller.scopes.contains(agentScope)) {
throw AgentRefusedException(reason: AgentRefusal.notForAgents);
}
await _register(session).setAgentKeys(
session,
agent: caller.authUserId,
encryption: encryption,
signing: signing,
);
}
/// The person calling; refused for an agent, which makes no agents.
UuidValue _person(Session session) {
final caller = session.authenticated!;
if (caller.scopes.contains(agentScope)) {
throw AgentRefusedException(reason: AgentRefusal.notForAgents);
}
return caller.authUserId;
}
AccountRegister _register(Session session) =>
AccountRegister.of(session.serverpod);
}

View file

@ -4,6 +4,7 @@ import 'package:clock/clock.dart';
import 'package:halkyon_base_server/src/assistant/assistants_config.dart';
import 'package:halkyon_base_server/src/auth/membership.dart';
import 'package:halkyon_base_server/src/generated/protocol.dart';
import 'package:halkyon_base_server/src/register/account_register.dart';
import 'package:halkyon_llm/halkyon_llm.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';
@ -491,8 +492,9 @@ Future<void> grantAssistants(
required List<String> assistants,
required AssistantLimits budget,
}) async {
final granted = _checked(session, assistants, budget);
final people = accounts.toSet();
await _refuseAgents(session, people);
final granted = _checked(session, assistants, budget);
await session.db.transaction((transaction) async {
await _knownOrRefused(session, people, transaction);
for (final person in people) {
@ -502,6 +504,16 @@ Future<void> grantAssistants(
});
}
/// Refuses where any of [people] is an agent (Halkyon's ADR 0044, D45),
/// which uses none of the operator's assistants.
Future<void> _refuseAgents(Session session, Set<UuidValue> people) async {
if ((await AccountRegister.of(
session.serverpod,
).agentOwners(session, people)).isNotEmpty) {
_refusedForOperator(OperatorRefusal.isAgent);
}
}
/// Gives [account] exactly [assistants], taking back the others, the
/// agreements of those kept kept, and [budget]; with no assistants, the
/// budget goes too. Refused as [grantAssistants] is.
@ -511,6 +523,7 @@ Future<void> changeAssistants(
required List<String> assistants,
required AssistantLimits budget,
}) async {
await _refuseAgents(session, {account});
final kept = _checked(session, assistants, budget);
await session.db.transaction((transaction) async {
await _knownOrRefused(session, {account}, transaction);

View file

@ -1,3 +1,4 @@
import 'package:halkyon_base_server/src/agent/agents.dart';
import 'package:halkyon_base_server/src/assistant/assistant_grants.dart';
import 'package:halkyon_base_server/src/auth/scopes.dart';
import 'package:halkyon_base_server/src/generated/protocol.dart';
@ -38,6 +39,11 @@ Future<void> deletePerson(
HoldsRegister(:final members) => members,
MemberOfRegister() => const <Uri>[],
};
// Their agents first, each as an account is deleted (Halkyon's ADR 0044,
// D6): the register keeps them, and its members learn of each.
if (base.accounts is HoldsRegister) {
await deleteAgentsOf(session, authUserId);
}
var noticesKept = false;
final afterwards = await session.db.transaction((transaction) async {
// SELECT … FROM "serverpod_auth_core_user" WHERE "id" = $1
@ -84,6 +90,8 @@ Future<void> deletePerson(
await forgetAvatar(session, hash, transaction: transaction);
}
}
// What the register keeps of them as an agent: its owner and its token.
await forgetAgent(session, authUserId, transaction: transaction);
// Their address's hash: the way they signed in.
// DELETE FROM "email_identity" WHERE "authUserId" = $1
await EmailIdentity.db.deleteWhere(

View file

@ -12,3 +12,8 @@ const identified = Scope('identified');
/// register: they approve, refuse and suspend accounts, and name other
/// operators. The first is made by SQL; an operator never waits.
const instanceOperator = Scope('operator');
/// Agents (Halkyon's ADR 0044, D6, D45): accounts a person made, signing in
/// with a token alone. Their sessions carry it beside [identified], and the
/// module refuses them what only people do.
const agentScope = Scope('agent');

View file

@ -0,0 +1,731 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:serverpod/serverpod.dart' as _is;
/// An agent (Halkyon's ADR 0044, D6, D45): an account of the register that
/// its owner made from their Profile, signing in with a token alone
/// (`agent_token`), never a password. Its public keys are its own, set by
/// the agent once where it runs; it has no others.
abstract class AgentAccount
implements _is.TableRow<int?>, _is.ProtocolSerialization {
AgentAccount._({
this.id,
required this.agentId,
required this.ownerId,
required this.name,
required this.createdAt,
this.encryptionPublicKey,
this.signingPublicKey,
});
factory AgentAccount({
int? id,
required _is.UuidValue agentId,
required _is.UuidValue ownerId,
required String name,
required DateTime createdAt,
_idt.ByteData? encryptionPublicKey,
_idt.ByteData? signingPublicKey,
}) = _AgentAccountImpl;
factory AgentAccount.fromJson(Map<String, dynamic> jsonSerialization) {
return AgentAccount(
id: jsonSerialization['id'] as int?,
agentId: _is.UuidValueJsonExtension.fromJson(
jsonSerialization['agentId'],
),
ownerId: _is.UuidValueJsonExtension.fromJson(
jsonSerialization['ownerId'],
),
name: jsonSerialization['name'] as String,
createdAt: _is.DateTimeJsonExtension.fromJson(
jsonSerialization['createdAt'],
),
encryptionPublicKey: jsonSerialization['encryptionPublicKey'] == null
? null
: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['encryptionPublicKey'],
),
signingPublicKey: jsonSerialization['signingPublicKey'] == null
? null
: _is.ByteDataJsonExtension.fromJson(
jsonSerialization['signingPublicKey'],
),
);
}
static final t = AgentAccountTable();
static const db = AgentAccountRepository._();
@override
int? id;
/// The agent's account: its AuthUser's id.
_is.UuidValue agentId;
/// The person who made it, and whose it is.
_is.UuidValue ownerId;
/// What its owner called it.
String name;
DateTime createdAt;
/// X25519's public key, which shared keys are sealed to; null until the
/// agent set its keys.
_idt.ByteData? encryptionPublicKey;
/// Ed25519's public key, which its signatures check against.
_idt.ByteData? signingPublicKey;
@override
_is.Table<int?> get table => t;
/// Returns a shallow copy of this [AgentAccount]
/// with some or all fields replaced by the given arguments.
@_is.useResult
AgentAccount copyWith({
int? id,
_is.UuidValue? agentId,
_is.UuidValue? ownerId,
String? name,
DateTime? createdAt,
_idt.ByteData? encryptionPublicKey,
_idt.ByteData? signingPublicKey,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.AgentAccount',
if (id != null) 'id': id,
'agentId': agentId.toJson(),
'ownerId': ownerId.toJson(),
'name': name,
'createdAt': createdAt.toJson(),
if (encryptionPublicKey != null)
'encryptionPublicKey': encryptionPublicKey?.toJson(),
if (signingPublicKey != null)
'signingPublicKey': signingPublicKey?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {};
}
static AgentAccountInclude include() {
return AgentAccountInclude._();
}
static AgentAccountIncludeList includeList({
_is.WhereExpressionBuilder<AgentAccountTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<AgentAccountTable>? orderBy,
_is.OrderByListBuilder<AgentAccountTable>? orderByList,
AgentAccountInclude? include,
}) {
return AgentAccountIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(AgentAccount.t),
orderByList: orderByList?.call(AgentAccount.t),
include: include,
);
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _Undefined {}
class _AgentAccountImpl extends AgentAccount {
_AgentAccountImpl({
int? id,
required _is.UuidValue agentId,
required _is.UuidValue ownerId,
required String name,
required DateTime createdAt,
_idt.ByteData? encryptionPublicKey,
_idt.ByteData? signingPublicKey,
}) : super._(
id: id,
agentId: agentId,
ownerId: ownerId,
name: name,
createdAt: createdAt,
encryptionPublicKey: encryptionPublicKey,
signingPublicKey: signingPublicKey,
);
/// Returns a shallow copy of this [AgentAccount]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
AgentAccount copyWith({
Object? id = _Undefined,
_is.UuidValue? agentId,
_is.UuidValue? ownerId,
String? name,
DateTime? createdAt,
Object? encryptionPublicKey = _Undefined,
Object? signingPublicKey = _Undefined,
}) {
return AgentAccount(
id: id is int? ? id : this.id,
agentId: agentId ?? this.agentId,
ownerId: ownerId ?? this.ownerId,
name: name ?? this.name,
createdAt: createdAt ?? this.createdAt,
encryptionPublicKey: encryptionPublicKey is _idt.ByteData?
? encryptionPublicKey
: this.encryptionPublicKey?.clone(),
signingPublicKey: signingPublicKey is _idt.ByteData?
? signingPublicKey
: this.signingPublicKey?.clone(),
);
}
}
class AgentAccountUpdateTable extends _is.UpdateTable<AgentAccountTable> {
AgentAccountUpdateTable(super.table);
_is.ColumnValue<_is.UuidValue, _is.UuidValue> agentId(_is.UuidValue value) =>
_is.ColumnValue(
table.agentId,
value,
);
_is.ColumnValue<_is.UuidValue, _is.UuidValue> ownerId(_is.UuidValue value) =>
_is.ColumnValue(
table.ownerId,
value,
);
_is.ColumnValue<String, String> name(String value) => _is.ColumnValue(
table.name,
value,
);
_is.ColumnValue<DateTime, DateTime> createdAt(DateTime value) =>
_is.ColumnValue(
table.createdAt,
value,
);
_is.ColumnValue<_idt.ByteData, _idt.ByteData> encryptionPublicKey(
_idt.ByteData? value,
) => _is.ColumnValue(
table.encryptionPublicKey,
value,
);
_is.ColumnValue<_idt.ByteData, _idt.ByteData> signingPublicKey(
_idt.ByteData? value,
) => _is.ColumnValue(
table.signingPublicKey,
value,
);
}
class AgentAccountTable extends _is.Table<int?> {
AgentAccountTable({super.tableRelation}) : super(tableName: 'agent_account') {
updateTable = AgentAccountUpdateTable(this);
agentId = _is.ColumnUuid(
'agentId',
this,
);
ownerId = _is.ColumnUuid(
'ownerId',
this,
);
name = _is.ColumnString(
'name',
this,
);
createdAt = _is.ColumnDateTime(
'createdAt',
this,
);
encryptionPublicKey = _is.ColumnByteData(
'encryptionPublicKey',
this,
);
signingPublicKey = _is.ColumnByteData(
'signingPublicKey',
this,
);
}
late final AgentAccountUpdateTable updateTable;
/// The agent's account: its AuthUser's id.
late final _is.ColumnUuid agentId;
/// The person who made it, and whose it is.
late final _is.ColumnUuid ownerId;
/// What its owner called it.
late final _is.ColumnString name;
late final _is.ColumnDateTime createdAt;
/// X25519's public key, which shared keys are sealed to; null until the
/// agent set its keys.
late final _is.ColumnByteData encryptionPublicKey;
/// Ed25519's public key, which its signatures check against.
late final _is.ColumnByteData signingPublicKey;
@override
List<_is.Column> get columns => [
id,
agentId,
ownerId,
name,
createdAt,
encryptionPublicKey,
signingPublicKey,
];
}
class AgentAccountInclude extends _is.IncludeObject {
AgentAccountInclude._();
@override
Map<String, _is.Include?> get includes => {};
@override
_is.Table<int?> get table => AgentAccount.t;
}
class AgentAccountIncludeList extends _is.IncludeList {
AgentAccountIncludeList._({
_is.WhereExpressionBuilder<AgentAccountTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(AgentAccount.t);
}
@override
Map<String, _is.Include?> get includes => include?.includes ?? {};
@override
_is.Table<int?> get table => AgentAccount.t;
}
class AgentAccountRepository {
const AgentAccountRepository._();
/// Returns a list of [AgentAccount]s matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order of the items use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// The maximum number of items can be set by [limit]. If no limit is set,
/// all items matching the query will be returned.
///
/// [offset] defines how many items to skip, after which [limit] (or all)
/// items are read from the database.
///
/// ```dart
/// var persons = await Persons.db.find(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.firstName,
/// limit: 100,
/// );
/// ```
Future<List<AgentAccount>> find(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<AgentAccountTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<AgentAccountTable>? orderBy,
_is.OrderByListBuilder<AgentAccountTable>? orderByList,
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.find<AgentAccount>(
where: where?.call(AgentAccount.t),
orderBy: orderBy?.call(AgentAccount.t),
orderByList: orderByList?.call(AgentAccount.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [AgentAccount] matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// [offset] defines how many items to skip, after which the next one will be picked.
///
/// ```dart
/// var youngestPerson = await Persons.db.findFirstRow(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.age,
/// );
/// ```
Future<AgentAccount?> findFirstRow(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<AgentAccountTable>? where,
int? offset,
_is.OrderByBuilder<AgentAccountTable>? orderBy,
_is.OrderByListBuilder<AgentAccountTable>? orderByList,
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<AgentAccount>(
where: where?.call(AgentAccount.t),
orderBy: orderBy?.call(AgentAccount.t),
orderByList: orderByList?.call(AgentAccount.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [AgentAccount] by its [id] or null if no such row exists.
Future<AgentAccount?> findById(
_is.DatabaseSession session,
int id, {
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findById<AgentAccount>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [AgentAccount]s in the list and returns the inserted rows.
///
/// The returned [AgentAccount]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails to
/// insert, none of the rows will be inserted.
///
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
/// rows are silently skipped, and only the successfully inserted rows are
/// returned.
///
/// If [noReturn] is set to `true`, the inserted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<AgentAccount>> insert(
_is.DatabaseSession session,
List<AgentAccount> rows, {
_is.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<AgentAccount>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [AgentAccount] and returns the inserted row.
///
/// The returned [AgentAccount] will have its `id` field set.
Future<AgentAccount> insertRow(
_is.DatabaseSession session,
AgentAccount row, {
_is.Transaction? transaction,
}) async {
return session.db.insertRow<AgentAccount>(
row,
transaction: transaction,
);
}
/// Upserts all [AgentAccount]s in the list and returns the resulting rows.
///
/// If a row conflicts on the given [conflictColumns], the existing row is
/// updated with the new values. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies to rows matching the
/// given expression. Conflicting rows that don't match are skipped and not
/// returned, so the resulting list may be shorter than [rows].
///
/// The returned [AgentAccount]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails,
/// none of the rows will be affected.
///
/// If [noReturn] is set to `true`, the resulting rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<AgentAccount>> upsert(
_is.DatabaseSession session,
List<AgentAccount> rows, {
required _is.ColumnSelections<AgentAccountTable> conflictColumns,
_is.ColumnSelections<AgentAccountTable>? updateColumns,
_is.WhereExpressionBuilder<AgentAccountTable>? updateWhere,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<AgentAccount>(
rows,
conflictColumns: conflictColumns(AgentAccount.t),
updateColumns: updateColumns?.call(AgentAccount.t),
updateWhere: updateWhere?.call(AgentAccount.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [AgentAccount] and returns the resulting row.
///
/// If the row conflicts on the given [conflictColumns], the existing row is
/// updated. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies when the existing
/// row matches the expression. Returns `null` if no row was affected — for
/// example when [updateWhere] does not match the conflicting row.
///
/// The returned [AgentAccount] will have its `id` field set.
Future<AgentAccount?> upsertRow(
_is.DatabaseSession session,
AgentAccount row, {
required _is.ColumnSelections<AgentAccountTable> conflictColumns,
_is.ColumnSelections<AgentAccountTable>? updateColumns,
_is.WhereExpressionBuilder<AgentAccountTable>? updateWhere,
_is.Transaction? transaction,
}) async {
return session.db.upsertRow<AgentAccount>(
row,
conflictColumns: conflictColumns(AgentAccount.t),
updateColumns: updateColumns?.call(AgentAccount.t),
updateWhere: updateWhere?.call(AgentAccount.t),
transaction: transaction,
);
}
/// Updates all [AgentAccount]s in the list and returns the updated rows. If
/// [columns] is provided, only those columns will be updated. Defaults to
/// all columns.
/// This is an atomic operation, meaning that if one of the rows fails to
/// update, none of the rows will be updated.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<AgentAccount>> update(
_is.DatabaseSession session,
List<AgentAccount> rows, {
_is.ColumnSelections<AgentAccountTable>? columns,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<AgentAccount>(
rows,
columns: columns?.call(AgentAccount.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [AgentAccount]. The row needs to have its id set.
/// Optionally, a list of [columns] can be provided to only update those
/// columns. Defaults to all columns.
Future<AgentAccount> updateRow(
_is.DatabaseSession session,
AgentAccount row, {
_is.ColumnSelections<AgentAccountTable>? columns,
_is.Transaction? transaction,
}) async {
return session.db.updateRow<AgentAccount>(
row,
columns: columns?.call(AgentAccount.t),
transaction: transaction,
);
}
/// Updates a single [AgentAccount] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<AgentAccount?> updateById(
_is.DatabaseSession session,
int id, {
required _is.ColumnValueListBuilder<AgentAccountUpdateTable> columnValues,
_is.Transaction? transaction,
}) async {
return session.db.updateById<AgentAccount>(
id,
columnValues: columnValues(AgentAccount.t.updateTable),
transaction: transaction,
);
}
/// Updates all [AgentAccount]s matching the [where] expression with the specified [columnValues].
/// Returns the list of updated rows.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<AgentAccount>> updateWhere(
_is.DatabaseSession session, {
required _is.ColumnValueListBuilder<AgentAccountUpdateTable> columnValues,
required _is.WhereExpressionBuilder<AgentAccountTable> where,
int? limit,
int? offset,
_is.OrderByBuilder<AgentAccountTable>? orderBy,
_is.OrderByListBuilder<AgentAccountTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<AgentAccount>(
columnValues: columnValues(AgentAccount.t.updateTable),
where: where(AgentAccount.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(AgentAccount.t),
orderByList: orderByList?.call(AgentAccount.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [AgentAccount]s in the list and returns the deleted rows.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// This is an atomic operation, meaning that if one of the rows fail to
/// be deleted, none of the rows will be deleted.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<AgentAccount>> delete(
_is.DatabaseSession session,
List<AgentAccount> rows, {
_is.OrderByBuilder<AgentAccountTable>? orderBy,
_is.OrderByListBuilder<AgentAccountTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<AgentAccount>(
rows,
orderBy: orderBy?.call(AgentAccount.t),
orderByList: orderByList?.call(AgentAccount.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [AgentAccount].
Future<AgentAccount> deleteRow(
_is.DatabaseSession session,
AgentAccount row, {
_is.Transaction? transaction,
}) async {
return session.db.deleteRow<AgentAccount>(
row,
transaction: transaction,
);
}
/// Deletes all rows matching the [where] expression.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<AgentAccount>> deleteWhere(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<AgentAccountTable> where,
_is.OrderByBuilder<AgentAccountTable>? orderBy,
_is.OrderByListBuilder<AgentAccountTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<AgentAccount>(
where: where(AgentAccount.t),
orderBy: orderBy?.call(AgentAccount.t),
orderByList: orderByList?.call(AgentAccount.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<AgentAccountTable>? where,
int? limit,
_is.Transaction? transaction,
}) async {
return session.db.count<AgentAccount>(
where: where?.call(AgentAccount.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [AgentAccount] rows matching the [where] expression.
Future<void> lockRows(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<AgentAccountTable> where,
required _is.LockMode lockMode,
required _is.Transaction transaction,
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
}) async {
return session.db.lockRows<AgentAccount>(
where: where(AgentAccount.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -0,0 +1,162 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod/serverpod.dart' as _is;
/// An agent as its owner's Profile lists it (Halkyon's ADR 0044, D45).
abstract class AgentInfo
implements _is.SerializableModel, _is.ProtocolSerialization {
AgentInfo._({
required this.id,
required this.name,
required this.handle,
required this.createdAt,
this.lastUsedAt,
required this.hasToken,
required this.hasKeys,
});
factory AgentInfo({
required _is.UuidValue id,
required String name,
required String handle,
required DateTime createdAt,
DateTime? lastUsedAt,
required bool hasToken,
required bool hasKeys,
}) = _AgentInfoImpl;
factory AgentInfo.fromJson(Map<String, dynamic> jsonSerialization) {
return AgentInfo(
id: _is.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
name: jsonSerialization['name'] as String,
handle: jsonSerialization['handle'] as String,
createdAt: _is.DateTimeJsonExtension.fromJson(
jsonSerialization['createdAt'],
),
lastUsedAt: jsonSerialization['lastUsedAt'] == null
? null
: _is.DateTimeJsonExtension.fromJson(jsonSerialization['lastUsedAt']),
hasToken: _is.BoolJsonExtension.fromJson(jsonSerialization['hasToken']),
hasKeys: _is.BoolJsonExtension.fromJson(jsonSerialization['hasKeys']),
);
}
/// The agent's account.
_is.UuidValue id;
String name;
String handle;
DateTime createdAt;
/// When it last signed in with its token; null while never.
DateTime? lastUsedAt;
/// Whether it has a token: revoked, it has none until a new one is made.
bool hasToken;
/// Whether it set its public keys.
bool hasKeys;
/// Returns a shallow copy of this [AgentInfo]
/// with some or all fields replaced by the given arguments.
@_is.useResult
AgentInfo copyWith({
_is.UuidValue? id,
String? name,
String? handle,
DateTime? createdAt,
DateTime? lastUsedAt,
bool? hasToken,
bool? hasKeys,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.AgentInfo',
'id': id.toJson(),
'name': name,
'handle': handle,
'createdAt': createdAt.toJson(),
if (lastUsedAt != null) 'lastUsedAt': lastUsedAt?.toJson(),
'hasToken': hasToken,
'hasKeys': hasKeys,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.AgentInfo',
'id': id.toJson(),
'name': name,
'handle': handle,
'createdAt': createdAt.toJson(),
if (lastUsedAt != null) 'lastUsedAt': lastUsedAt?.toJson(),
'hasToken': hasToken,
'hasKeys': hasKeys,
};
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _Undefined {}
class _AgentInfoImpl extends AgentInfo {
_AgentInfoImpl({
required _is.UuidValue id,
required String name,
required String handle,
required DateTime createdAt,
DateTime? lastUsedAt,
required bool hasToken,
required bool hasKeys,
}) : super._(
id: id,
name: name,
handle: handle,
createdAt: createdAt,
lastUsedAt: lastUsedAt,
hasToken: hasToken,
hasKeys: hasKeys,
);
/// Returns a shallow copy of this [AgentInfo]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
AgentInfo copyWith({
_is.UuidValue? id,
String? name,
String? handle,
DateTime? createdAt,
Object? lastUsedAt = _Undefined,
bool? hasToken,
bool? hasKeys,
}) {
return AgentInfo(
id: id ?? this.id,
name: name ?? this.name,
handle: handle ?? this.handle,
createdAt: createdAt ?? this.createdAt,
lastUsedAt: lastUsedAt is DateTime? ? lastUsedAt : this.lastUsedAt,
hasToken: hasToken ?? this.hasToken,
hasKeys: hasKeys ?? this.hasKeys,
);
}
}

View file

@ -0,0 +1,96 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_base_server/src/generated/protocol.dart' as _ibewtqgw;
import 'package:serverpod/serverpod.dart' as _is;
import '../agent/agent_info.dart' as _idw6egxv;
/// An agent just made, or given a new token: the token, shown this once.
abstract class AgentMade
implements _is.SerializableModel, _is.ProtocolSerialization {
AgentMade._({
required this.agent,
required this.token,
});
factory AgentMade({
required _idw6egxv.AgentInfo agent,
required String token,
}) = _AgentMadeImpl;
factory AgentMade.fromJson(Map<String, dynamic> jsonSerialization) {
return AgentMade(
agent: _ibewtqgw.Protocol().deserialize<_idw6egxv.AgentInfo>(
jsonSerialization['agent'],
),
token: jsonSerialization['token'] as String,
);
}
_idw6egxv.AgentInfo agent;
String token;
/// Returns a shallow copy of this [AgentMade]
/// with some or all fields replaced by the given arguments.
@_is.useResult
AgentMade copyWith({
_idw6egxv.AgentInfo? agent,
String? token,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.AgentMade',
'agent': agent.toJson(),
'token': token,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.AgentMade',
'agent': agent.toJsonForProtocol(),
'token': token,
};
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _AgentMadeImpl extends AgentMade {
_AgentMadeImpl({
required _idw6egxv.AgentInfo agent,
required String token,
}) : super._(
agent: agent,
token: token,
);
/// Returns a shallow copy of this [AgentMade]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
AgentMade copyWith({
_idw6egxv.AgentInfo? agent,
String? token,
}) {
return AgentMade(
agent: agent ?? this.agent.copyWith(),
token: token ?? this.token,
);
}
}

View file

@ -0,0 +1,67 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod/serverpod.dart' as _is;
/// Why a call about agents was refused (Halkyon's ADR 0044, D45).
enum AgentRefusal implements _is.SerializableModel {
/// The operator switched agents off (`AGENTS=off`).
agentsOff,
/// The person has as many agents as they may (5).
tooMany,
/// An agent asked for what only people do, or a person for what only
/// agents do.
notForAgents,
/// No agent of yours is that: another person's answers alike.
noSuchAgent,
/// No token is that, or it lapsed.
notValid,
/// The agent set its keys already.
keysSet,
/// Too many sign-ins from this network this hour.
tooManyAttempts;
static AgentRefusal fromJson(String name) {
switch (name) {
case 'agentsOff':
return AgentRefusal.agentsOff;
case 'tooMany':
return AgentRefusal.tooMany;
case 'notForAgents':
return AgentRefusal.notForAgents;
case 'noSuchAgent':
return AgentRefusal.noSuchAgent;
case 'notValid':
return AgentRefusal.notValid;
case 'keysSet':
return AgentRefusal.keysSet;
case 'tooManyAttempts':
return AgentRefusal.tooManyAttempts;
default:
throw ArgumentError(
'Value "$name" cannot be converted to "AgentRefusal"',
);
}
}
@override
String toJson() => name;
@override
String toString() => name;
}

View file

@ -0,0 +1,76 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod/serverpod.dart' as _is;
import '../agent/agent_refusal.dart' as _igo9c1fx;
/// A call about agents was refused; nothing changed.
abstract class AgentRefusedException
implements
_is.SerializableException,
_is.SerializableModel,
_is.ProtocolSerialization {
AgentRefusedException._({required this.reason});
factory AgentRefusedException({required _igo9c1fx.AgentRefusal reason}) =
_AgentRefusedExceptionImpl;
factory AgentRefusedException.fromJson(
Map<String, dynamic> jsonSerialization,
) {
return AgentRefusedException(
reason: _igo9c1fx.AgentRefusal.fromJson(
(jsonSerialization['reason'] as String),
),
);
}
_igo9c1fx.AgentRefusal reason;
/// Returns a shallow copy of this [AgentRefusedException]
/// with some or all fields replaced by the given arguments.
@_is.useResult
AgentRefusedException copyWith({_igo9c1fx.AgentRefusal? reason});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.AgentRefusedException',
'reason': reason.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.AgentRefusedException',
'reason': reason.toJson(),
};
}
@override
String toString() {
return 'AgentRefusedException(reason: $reason)';
}
}
class _AgentRefusedExceptionImpl extends AgentRefusedException {
_AgentRefusedExceptionImpl({required _igo9c1fx.AgentRefusal reason})
: super._(reason: reason);
/// Returns a shallow copy of this [AgentRefusedException]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
AgentRefusedException copyWith({_igo9c1fx.AgentRefusal? reason}) {
return AgentRefusedException(reason: reason ?? this.reason);
}
}

View file

@ -0,0 +1,659 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod/serverpod.dart' as _is;
/// The token an agent signs in with (Halkyon's ADR 0044, D12, D43, D45):
/// one an agent, 32 random bytes shown to its owner once, kept as their
/// SHA-256 alone. It lapses 90 days after it was last used.
abstract class AgentToken
implements _is.TableRow<int?>, _is.ProtocolSerialization {
AgentToken._({
this.id,
required this.agentId,
required this.tokenHash,
required this.createdAt,
this.lastUsedAt,
});
factory AgentToken({
int? id,
required _is.UuidValue agentId,
required String tokenHash,
required DateTime createdAt,
DateTime? lastUsedAt,
}) = _AgentTokenImpl;
factory AgentToken.fromJson(Map<String, dynamic> jsonSerialization) {
return AgentToken(
id: jsonSerialization['id'] as int?,
agentId: _is.UuidValueJsonExtension.fromJson(
jsonSerialization['agentId'],
),
tokenHash: jsonSerialization['tokenHash'] as String,
createdAt: _is.DateTimeJsonExtension.fromJson(
jsonSerialization['createdAt'],
),
lastUsedAt: jsonSerialization['lastUsedAt'] == null
? null
: _is.DateTimeJsonExtension.fromJson(jsonSerialization['lastUsedAt']),
);
}
static final t = AgentTokenTable();
static const db = AgentTokenRepository._();
@override
int? id;
/// The agent's account.
_is.UuidValue agentId;
/// SHA-256 of the token as the agent sends it, in hex.
String tokenHash;
DateTime createdAt;
/// When the agent last signed in with it; null while never.
DateTime? lastUsedAt;
@override
_is.Table<int?> get table => t;
/// Returns a shallow copy of this [AgentToken]
/// with some or all fields replaced by the given arguments.
@_is.useResult
AgentToken copyWith({
int? id,
_is.UuidValue? agentId,
String? tokenHash,
DateTime? createdAt,
DateTime? lastUsedAt,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.AgentToken',
if (id != null) 'id': id,
'agentId': agentId.toJson(),
'tokenHash': tokenHash,
'createdAt': createdAt.toJson(),
if (lastUsedAt != null) 'lastUsedAt': lastUsedAt?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {};
}
static AgentTokenInclude include() {
return AgentTokenInclude._();
}
static AgentTokenIncludeList includeList({
_is.WhereExpressionBuilder<AgentTokenTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<AgentTokenTable>? orderBy,
_is.OrderByListBuilder<AgentTokenTable>? orderByList,
AgentTokenInclude? include,
}) {
return AgentTokenIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(AgentToken.t),
orderByList: orderByList?.call(AgentToken.t),
include: include,
);
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _Undefined {}
class _AgentTokenImpl extends AgentToken {
_AgentTokenImpl({
int? id,
required _is.UuidValue agentId,
required String tokenHash,
required DateTime createdAt,
DateTime? lastUsedAt,
}) : super._(
id: id,
agentId: agentId,
tokenHash: tokenHash,
createdAt: createdAt,
lastUsedAt: lastUsedAt,
);
/// Returns a shallow copy of this [AgentToken]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
AgentToken copyWith({
Object? id = _Undefined,
_is.UuidValue? agentId,
String? tokenHash,
DateTime? createdAt,
Object? lastUsedAt = _Undefined,
}) {
return AgentToken(
id: id is int? ? id : this.id,
agentId: agentId ?? this.agentId,
tokenHash: tokenHash ?? this.tokenHash,
createdAt: createdAt ?? this.createdAt,
lastUsedAt: lastUsedAt is DateTime? ? lastUsedAt : this.lastUsedAt,
);
}
}
class AgentTokenUpdateTable extends _is.UpdateTable<AgentTokenTable> {
AgentTokenUpdateTable(super.table);
_is.ColumnValue<_is.UuidValue, _is.UuidValue> agentId(_is.UuidValue value) =>
_is.ColumnValue(
table.agentId,
value,
);
_is.ColumnValue<String, String> tokenHash(String value) => _is.ColumnValue(
table.tokenHash,
value,
);
_is.ColumnValue<DateTime, DateTime> createdAt(DateTime value) =>
_is.ColumnValue(
table.createdAt,
value,
);
_is.ColumnValue<DateTime, DateTime> lastUsedAt(DateTime? value) =>
_is.ColumnValue(
table.lastUsedAt,
value,
);
}
class AgentTokenTable extends _is.Table<int?> {
AgentTokenTable({super.tableRelation}) : super(tableName: 'agent_token') {
updateTable = AgentTokenUpdateTable(this);
agentId = _is.ColumnUuid(
'agentId',
this,
);
tokenHash = _is.ColumnString(
'tokenHash',
this,
);
createdAt = _is.ColumnDateTime(
'createdAt',
this,
);
lastUsedAt = _is.ColumnDateTime(
'lastUsedAt',
this,
);
}
late final AgentTokenUpdateTable updateTable;
/// The agent's account.
late final _is.ColumnUuid agentId;
/// SHA-256 of the token as the agent sends it, in hex.
late final _is.ColumnString tokenHash;
late final _is.ColumnDateTime createdAt;
/// When the agent last signed in with it; null while never.
late final _is.ColumnDateTime lastUsedAt;
@override
List<_is.Column> get columns => [
id,
agentId,
tokenHash,
createdAt,
lastUsedAt,
];
}
class AgentTokenInclude extends _is.IncludeObject {
AgentTokenInclude._();
@override
Map<String, _is.Include?> get includes => {};
@override
_is.Table<int?> get table => AgentToken.t;
}
class AgentTokenIncludeList extends _is.IncludeList {
AgentTokenIncludeList._({
_is.WhereExpressionBuilder<AgentTokenTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(AgentToken.t);
}
@override
Map<String, _is.Include?> get includes => include?.includes ?? {};
@override
_is.Table<int?> get table => AgentToken.t;
}
class AgentTokenRepository {
const AgentTokenRepository._();
/// Returns a list of [AgentToken]s matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order of the items use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// The maximum number of items can be set by [limit]. If no limit is set,
/// all items matching the query will be returned.
///
/// [offset] defines how many items to skip, after which [limit] (or all)
/// items are read from the database.
///
/// ```dart
/// var persons = await Persons.db.find(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.firstName,
/// limit: 100,
/// );
/// ```
Future<List<AgentToken>> find(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<AgentTokenTable>? where,
int? limit,
int? offset,
_is.OrderByBuilder<AgentTokenTable>? orderBy,
_is.OrderByListBuilder<AgentTokenTable>? orderByList,
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.find<AgentToken>(
where: where?.call(AgentToken.t),
orderBy: orderBy?.call(AgentToken.t),
orderByList: orderByList?.call(AgentToken.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [AgentToken] matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// [offset] defines how many items to skip, after which the next one will be picked.
///
/// ```dart
/// var youngestPerson = await Persons.db.findFirstRow(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.age,
/// );
/// ```
Future<AgentToken?> findFirstRow(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<AgentTokenTable>? where,
int? offset,
_is.OrderByBuilder<AgentTokenTable>? orderBy,
_is.OrderByListBuilder<AgentTokenTable>? orderByList,
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<AgentToken>(
where: where?.call(AgentToken.t),
orderBy: orderBy?.call(AgentToken.t),
orderByList: orderByList?.call(AgentToken.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [AgentToken] by its [id] or null if no such row exists.
Future<AgentToken?> findById(
_is.DatabaseSession session,
int id, {
_is.Transaction? transaction,
_is.LockMode? lockMode,
_is.LockBehavior? lockBehavior,
}) async {
return session.db.findById<AgentToken>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [AgentToken]s in the list and returns the inserted rows.
///
/// The returned [AgentToken]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails to
/// insert, none of the rows will be inserted.
///
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
/// rows are silently skipped, and only the successfully inserted rows are
/// returned.
///
/// If [noReturn] is set to `true`, the inserted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<AgentToken>> insert(
_is.DatabaseSession session,
List<AgentToken> rows, {
_is.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<AgentToken>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [AgentToken] and returns the inserted row.
///
/// The returned [AgentToken] will have its `id` field set.
Future<AgentToken> insertRow(
_is.DatabaseSession session,
AgentToken row, {
_is.Transaction? transaction,
}) async {
return session.db.insertRow<AgentToken>(
row,
transaction: transaction,
);
}
/// Upserts all [AgentToken]s in the list and returns the resulting rows.
///
/// If a row conflicts on the given [conflictColumns], the existing row is
/// updated with the new values. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies to rows matching the
/// given expression. Conflicting rows that don't match are skipped and not
/// returned, so the resulting list may be shorter than [rows].
///
/// The returned [AgentToken]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails,
/// none of the rows will be affected.
///
/// If [noReturn] is set to `true`, the resulting rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<AgentToken>> upsert(
_is.DatabaseSession session,
List<AgentToken> rows, {
required _is.ColumnSelections<AgentTokenTable> conflictColumns,
_is.ColumnSelections<AgentTokenTable>? updateColumns,
_is.WhereExpressionBuilder<AgentTokenTable>? updateWhere,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<AgentToken>(
rows,
conflictColumns: conflictColumns(AgentToken.t),
updateColumns: updateColumns?.call(AgentToken.t),
updateWhere: updateWhere?.call(AgentToken.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [AgentToken] and returns the resulting row.
///
/// If the row conflicts on the given [conflictColumns], the existing row is
/// updated. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies when the existing
/// row matches the expression. Returns `null` if no row was affected — for
/// example when [updateWhere] does not match the conflicting row.
///
/// The returned [AgentToken] will have its `id` field set.
Future<AgentToken?> upsertRow(
_is.DatabaseSession session,
AgentToken row, {
required _is.ColumnSelections<AgentTokenTable> conflictColumns,
_is.ColumnSelections<AgentTokenTable>? updateColumns,
_is.WhereExpressionBuilder<AgentTokenTable>? updateWhere,
_is.Transaction? transaction,
}) async {
return session.db.upsertRow<AgentToken>(
row,
conflictColumns: conflictColumns(AgentToken.t),
updateColumns: updateColumns?.call(AgentToken.t),
updateWhere: updateWhere?.call(AgentToken.t),
transaction: transaction,
);
}
/// Updates all [AgentToken]s in the list and returns the updated rows. If
/// [columns] is provided, only those columns will be updated. Defaults to
/// all columns.
/// This is an atomic operation, meaning that if one of the rows fails to
/// update, none of the rows will be updated.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<AgentToken>> update(
_is.DatabaseSession session,
List<AgentToken> rows, {
_is.ColumnSelections<AgentTokenTable>? columns,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<AgentToken>(
rows,
columns: columns?.call(AgentToken.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [AgentToken]. The row needs to have its id set.
/// Optionally, a list of [columns] can be provided to only update those
/// columns. Defaults to all columns.
Future<AgentToken> updateRow(
_is.DatabaseSession session,
AgentToken row, {
_is.ColumnSelections<AgentTokenTable>? columns,
_is.Transaction? transaction,
}) async {
return session.db.updateRow<AgentToken>(
row,
columns: columns?.call(AgentToken.t),
transaction: transaction,
);
}
/// Updates a single [AgentToken] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<AgentToken?> updateById(
_is.DatabaseSession session,
int id, {
required _is.ColumnValueListBuilder<AgentTokenUpdateTable> columnValues,
_is.Transaction? transaction,
}) async {
return session.db.updateById<AgentToken>(
id,
columnValues: columnValues(AgentToken.t.updateTable),
transaction: transaction,
);
}
/// Updates all [AgentToken]s matching the [where] expression with the specified [columnValues].
/// Returns the list of updated rows.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<AgentToken>> updateWhere(
_is.DatabaseSession session, {
required _is.ColumnValueListBuilder<AgentTokenUpdateTable> columnValues,
required _is.WhereExpressionBuilder<AgentTokenTable> where,
int? limit,
int? offset,
_is.OrderByBuilder<AgentTokenTable>? orderBy,
_is.OrderByListBuilder<AgentTokenTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<AgentToken>(
columnValues: columnValues(AgentToken.t.updateTable),
where: where(AgentToken.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(AgentToken.t),
orderByList: orderByList?.call(AgentToken.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [AgentToken]s in the list and returns the deleted rows.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// This is an atomic operation, meaning that if one of the rows fail to
/// be deleted, none of the rows will be deleted.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<AgentToken>> delete(
_is.DatabaseSession session,
List<AgentToken> rows, {
_is.OrderByBuilder<AgentTokenTable>? orderBy,
_is.OrderByListBuilder<AgentTokenTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<AgentToken>(
rows,
orderBy: orderBy?.call(AgentToken.t),
orderByList: orderByList?.call(AgentToken.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [AgentToken].
Future<AgentToken> deleteRow(
_is.DatabaseSession session,
AgentToken row, {
_is.Transaction? transaction,
}) async {
return session.db.deleteRow<AgentToken>(
row,
transaction: transaction,
);
}
/// Deletes all rows matching the [where] expression.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<AgentToken>> deleteWhere(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<AgentTokenTable> where,
_is.OrderByBuilder<AgentTokenTable>? orderBy,
_is.OrderByListBuilder<AgentTokenTable>? orderByList,
_is.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<AgentToken>(
where: where(AgentToken.t),
orderBy: orderBy?.call(AgentToken.t),
orderByList: orderByList?.call(AgentToken.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_is.DatabaseSession session, {
_is.WhereExpressionBuilder<AgentTokenTable>? where,
int? limit,
_is.Transaction? transaction,
}) async {
return session.db.count<AgentToken>(
where: where?.call(AgentToken.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [AgentToken] rows matching the [where] expression.
Future<void> lockRows(
_is.DatabaseSession session, {
required _is.WhereExpressionBuilder<AgentTokenTable> where,
required _is.LockMode lockMode,
required _is.Transaction transaction,
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
}) async {
return session.db.lockRows<AgentToken>(
where: where(AgentToken.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

Some files were not shown because too many files have changed in this diff Show more