halkyon/packages/halkyon_base_server/lib/base.dart

149 lines
5.2 KiB
Dart
Raw Permalink Normal View History

/// halkyon_base for a host's own code: its configuration
/// (`HalkyonBase.configure`), how people authenticate, the account's
/// deletion and the pruning, email sign-in's helpers, the instance's
/// basics, people's public profiles and their pages, the instance's register
feat(auth): a password with TOTP on the register, and resets The register's side of signing in with an address, a password that never leaves the device (SRP-6a) and a code from an authenticator (ADR 0017), with members forwarding every step through AccountRegister: - password.set: salt, Argon2id's costs, SRP's verifier, the public keys and the private keys wrapped twice, all made on the device; with the address a mailed code proves, an anonymous person becomes identified under their ID. It signs in once an authenticator is confirmed. - passwordSignIn.begin and complete: SRP's two steps with a TOTP or recovery code. An address nobody has is answered from a decoy key as an account would be; every wrong credential is refused alike. - authenticator: secrets sealed under totpKey (XChaCha20-Poly1305, bound to their row), each code accepted once, ten recovery codes with the first, the last one kept. - accountRecovery: a lost authenticator reset 72 hours after the password and a mailed code or the recovery phrase ask, a lost password set anew with the phrase's signature, or the account reset with new keys 72 hours after a mailed code asks; a mail at each start, a sign-in calling it off. - An account with a password no longer signs in by a mailed code or GitHub; no new GitHub account is linked where passwords are offered, and from GITHUB_SIGN_IN_ENDS GitHub signs nobody in and its IDs go. Five tables, added by the module's migration; deletePerson deletes their rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 13:32:12 +02:00
/// (ADR 0016), signing in with a password and TOTP and getting back into an
/// account (ADR 0017), signing in with SimpleLogin and its forwarding page
feat(base,learn,todo): the operator's assistants, and the instance's privacy page (ADR 0021, step 2) The server half of the assistants (points 4 to 8, D27 to D46): the operator sets language models in deploy/assistants.env, grants them to people with limits, and the module forwards their questions with the operator's key, counting what each used, never what was asked. - assistantsFrom: ASSISTANTS, ASSISTANT_ID_KEY, ASSISTANT_<ID>_NAME, _PROTOCOL (anthropic, openai, openai-responses), _BASE_URL, _MODEL, _KEY, _IMAGES, _PRICE_INPUT and _PRICE_OUTPUT (per million tokens), ASSISTANT_CURRENCY, ASSISTANT_OWN_KEYS; anything unusable stops the server as it starts, naming the variable. - The assistant endpoint (identified): offered, agree (D40), ask as a Serverpod stream (D35): the text as the model writes it, then the whole turn, sent once the request is counted; figures, the caller's own. Checked in order: at most 2 questions on their way per person (tooManyAtOnce), the grant, the agreement, limits in the pricing's unit now (limitOutdated), the month's and the day's limits (with when they reset, UTC), the question (malformed, imagesNotRead, tooLarge past 400,000 characters, an image over 3,932,160 bytes or more than 20 images); output capped at 16,384 tokens. Each request carries the person's opaque ID (HMAC-SHA256 under ASSISTANT_ID_KEY, D30). Once sent, the provider's reply is read to its end even if the app cancels, and counted on a session of its own; a provider's failure is logged by kind and status, told as providerKey, providerBusy (with its wait), providerRefused or providerFailed. - The assistantOperator endpoint (identified and operator, on the register): setUp, grants, grant (assistantNotSet, invalidLimit, notFound), revoke, figures of a UTC month. - Tables assistant_grant and assistant_figures (per person, assistant and UTC day), in the module's, Learn's and Todo's migrations. The daily pruning keeps this month and the two before (forgetOldAssistantFigures, D41); deletePerson deletes both. A member offers no assistant (D43). - instance.basics() gains assistantOwnKeys. - /privacy (D39, addPrivacyPageRoute, on the register's web server, before the profile pages' route): made from the instance's settings, English or German, no JavaScript: who may sign up, where the data and backups are, what the module keeps (the account, sessions, IP addresses, the server's log), the host's privacyPagePoints (Learn's: studysets synced, private ones encrypted on the devices with what stays readable, what is published, dictionary lookups, the learner count), each assistant with its model and host, what goes and what is counted, and whether personal keys are allowed. Never a key. Tests: 70 on the assistants (grants, limits, refusals in order, streaming, cancelling, counting, concurrency, the operator's denials, deletion, pruning), 19 on the settings, 8 on the privacy page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 18:07:01 +01:00
/// (ADR 0020), the ways in an operator offers, the operator's assistants
feat(base,learn): spaces people share, kept by halkyon_base ADR 0044, step 2 (D1, D2, D11, D13, and D15 to D17, decided today). halkyon_base keeps the spaces several people share, for every app: shared_space, its members with their roles (Owner, Editor, Viewer), and invitations by handle waiting for someone with no account on the service yet. The sharedSpace endpoint creates a space, lists its members, invites by handle, changes roles, removes members, hands the space on, leaves and deletes it. Who may do what: - Only people with an identity reach the endpoint. - A space the caller isn't in can't be told from one that doesn't exist. - Members are named by their membership, never by their account's ID. - Only the Owner manages the members, unless the space lets members invite. - The Owner's role passes only by handing the space on, and the Owner leaves only naming who takes it on. - An invitation counts as an answer about a handle, as it tells whether one exists. Every rule has a test asserting the refusal. The module stays free of the sync engine: a host hands it a SpaceGrants, and every call runs in the module's transaction, so the engine's grants follow the roles or nothing changes. The Owner and an Editor write; a Viewer reads. Learn hands over SyncSpaceGrants on OfflineSyncSpaces. Todo hands over none yet, so its server refuses until its projects are shared. Deleting an account hands each space the person owns to the Editor who joined first, else the Viewer who joined first. A space with no other member goes. This runs before the host's own deletions. A person's first sign-in on a service makes their waiting invitations memberships, in the same transaction that makes them a person there. Migrations: the module's and every host's (Learn, Todo, Train, Meditate) create the three tables, their indexes and the two cascading foreign keys to shared_space, and nothing else. The old server keeps working. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 10:33:12 +01:00
/// (Halkyon Learn's ADR 0021), people's files (ADR 0043), the spaces people
/// share (ADR 0044), and the module's models.
library;
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>
2026-10-25 15:04:21 +01:00
export 'src/admission/admission.dart'
show
forgetUnapprovedSignUps,
maxWaitingPasses,
maxWaitingSignUps,
unapprovedSignUpLifetime,
waitingPassLifetime;
export 'src/admission/sign_up_config.dart';
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>
2026-11-11 19:55:27 +01:00
export 'src/agent/agents.dart'
show
agentSessionLifetime,
agentSignInMethod,
agentTokenLapsesAfter,
agentTokenPrefix,
forgetLapsedAgentTokens,
maxAgentsPerPerson;
export 'src/agent/agents_config.dart';
feat(base,learn,todo): the operator's assistants, and the instance's privacy page (ADR 0021, step 2) The server half of the assistants (points 4 to 8, D27 to D46): the operator sets language models in deploy/assistants.env, grants them to people with limits, and the module forwards their questions with the operator's key, counting what each used, never what was asked. - assistantsFrom: ASSISTANTS, ASSISTANT_ID_KEY, ASSISTANT_<ID>_NAME, _PROTOCOL (anthropic, openai, openai-responses), _BASE_URL, _MODEL, _KEY, _IMAGES, _PRICE_INPUT and _PRICE_OUTPUT (per million tokens), ASSISTANT_CURRENCY, ASSISTANT_OWN_KEYS; anything unusable stops the server as it starts, naming the variable. - The assistant endpoint (identified): offered, agree (D40), ask as a Serverpod stream (D35): the text as the model writes it, then the whole turn, sent once the request is counted; figures, the caller's own. Checked in order: at most 2 questions on their way per person (tooManyAtOnce), the grant, the agreement, limits in the pricing's unit now (limitOutdated), the month's and the day's limits (with when they reset, UTC), the question (malformed, imagesNotRead, tooLarge past 400,000 characters, an image over 3,932,160 bytes or more than 20 images); output capped at 16,384 tokens. Each request carries the person's opaque ID (HMAC-SHA256 under ASSISTANT_ID_KEY, D30). Once sent, the provider's reply is read to its end even if the app cancels, and counted on a session of its own; a provider's failure is logged by kind and status, told as providerKey, providerBusy (with its wait), providerRefused or providerFailed. - The assistantOperator endpoint (identified and operator, on the register): setUp, grants, grant (assistantNotSet, invalidLimit, notFound), revoke, figures of a UTC month. - Tables assistant_grant and assistant_figures (per person, assistant and UTC day), in the module's, Learn's and Todo's migrations. The daily pruning keeps this month and the two before (forgetOldAssistantFigures, D41); deletePerson deletes both. A member offers no assistant (D43). - instance.basics() gains assistantOwnKeys. - /privacy (D39, addPrivacyPageRoute, on the register's web server, before the profile pages' route): made from the instance's settings, English or German, no JavaScript: who may sign up, where the data and backups are, what the module keeps (the account, sessions, IP addresses, the server's log), the host's privacyPagePoints (Learn's: studysets synced, private ones encrypted on the devices with what stays readable, what is published, dictionary lookups, the learner count), each assistant with its model and host, what goes and what is counted, and whether personal keys are allowed. Never a key. Tests: 70 on the assistants (grants, limits, refusals in order, streaming, cancelling, counting, concurrency, the operator's denials, deletion, pruning), 19 on the settings, 8 on the privacy page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 18:07:01 +01:00
export 'src/assistant/assistant_conversation.dart'
show
assistantImageMaxBytes,
assistantQuestionMaxCharacters,
assistantQuestionMaxImages;
export 'src/assistant/assistant_forwarding.dart'
show assistantUsageCounted, maxQuestionsAtOnce, useLlmClient;
export 'src/assistant/assistant_grants.dart'
show assistantFiguresMonths, forgetOldAssistantFigures;
export 'src/assistant/assistants_config.dart';
export 'src/auth/account.dart';
export 'src/auth/auth_config.dart';
export 'src/auth/code_mail.dart';
export 'src/auth/email.dart';
export 'src/auth/identified.dart';
export 'src/auth/offered_ways.dart';
export 'src/auth/scopes.dart';
export 'src/common/network.dart';
export 'src/common/short_code.dart';
refactor(base,learn): people's files kept by halkyon_base ADR 0043, D2, D4, D6 and D7, the first step towards Todo's attachments: Learn's file store moves into the module, for every server that keeps files, and Learn becomes its first host. halkyon_base gets the store as Learn had it: uploads into the caller's own space by SHA-256, downloads by a row the caller may read, each person's room (FILE_LIMIT_IDENTIFIED_MB, FILE_LIMIT_INSTANCE_MB, the operator's file_allowance, which moves into the module unchanged) and the pruning of files no live row names. It stays free of the sync engine: a host hands it a FileStore saying which of its rows name a file and what it takes (HalkyonBase.files); without one the files endpoint refuses. Its tests, with refusals, run on a stand-in host. Learn hands it learnFiles: its asset rows, read by their space's members, a readable file over 20 MB only as a PDF, up to 100 MB. Its app calls the module; its own asset endpoint stays for released apps alone, forwarding to the module in Learn's own models, whose names are on the wire. Its file tests still pass through it, end to end. Migrations: the module's creates file_allowance. Learn's server migration holds version rows alone, as tables are matched by name (ADR 0014); its device migration creates the module's empty table, as every module's. Todo's, Train's and Meditate's create the empty table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 00:11:36 +01:00
export 'src/files/file_limits.dart'
show FileLimits, Room, checkRoom, effectiveLimit, fileLimits, roomOf;
export 'src/files/file_pruning.dart'
show filePruningGrace, pruneUnreferencedFiles;
export 'src/files/file_store.dart';
export 'src/files/stored_files.dart'
show
downloadFile,
fileStoreOf,
fileUsageOf,
finishFileUpload,
startFileUpload;
export 'src/generated/protocol.dart' hide Protocol;
export 'src/halkyon_base.dart';
export 'src/instance/data_location.dart';
feat(base,learn,todo): the operator's assistants, and the instance's privacy page (ADR 0021, step 2) The server half of the assistants (points 4 to 8, D27 to D46): the operator sets language models in deploy/assistants.env, grants them to people with limits, and the module forwards their questions with the operator's key, counting what each used, never what was asked. - assistantsFrom: ASSISTANTS, ASSISTANT_ID_KEY, ASSISTANT_<ID>_NAME, _PROTOCOL (anthropic, openai, openai-responses), _BASE_URL, _MODEL, _KEY, _IMAGES, _PRICE_INPUT and _PRICE_OUTPUT (per million tokens), ASSISTANT_CURRENCY, ASSISTANT_OWN_KEYS; anything unusable stops the server as it starts, naming the variable. - The assistant endpoint (identified): offered, agree (D40), ask as a Serverpod stream (D35): the text as the model writes it, then the whole turn, sent once the request is counted; figures, the caller's own. Checked in order: at most 2 questions on their way per person (tooManyAtOnce), the grant, the agreement, limits in the pricing's unit now (limitOutdated), the month's and the day's limits (with when they reset, UTC), the question (malformed, imagesNotRead, tooLarge past 400,000 characters, an image over 3,932,160 bytes or more than 20 images); output capped at 16,384 tokens. Each request carries the person's opaque ID (HMAC-SHA256 under ASSISTANT_ID_KEY, D30). Once sent, the provider's reply is read to its end even if the app cancels, and counted on a session of its own; a provider's failure is logged by kind and status, told as providerKey, providerBusy (with its wait), providerRefused or providerFailed. - The assistantOperator endpoint (identified and operator, on the register): setUp, grants, grant (assistantNotSet, invalidLimit, notFound), revoke, figures of a UTC month. - Tables assistant_grant and assistant_figures (per person, assistant and UTC day), in the module's, Learn's and Todo's migrations. The daily pruning keeps this month and the two before (forgetOldAssistantFigures, D41); deletePerson deletes both. A member offers no assistant (D43). - instance.basics() gains assistantOwnKeys. - /privacy (D39, addPrivacyPageRoute, on the register's web server, before the profile pages' route): made from the instance's settings, English or German, no JavaScript: who may sign up, where the data and backups are, what the module keeps (the account, sessions, IP addresses, the server's log), the host's privacyPagePoints (Learn's: studysets synced, private ones encrypted on the devices with what stays readable, what is published, dictionary lookups, the learner count), each assistant with its model and host, what goes and what is counted, and whether personal keys are allowed. Never a key. Tests: 70 on the assistants (grants, limits, refusals in order, streaming, cancelling, counting, concurrency, the operator's denials, deletion, pruning), 19 on the settings, 8 on the privacy page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 18:07:01 +01:00
export 'src/instance/privacy_page.dart' show addPrivacyPageRoute, privacyPage;
export 'src/instance/source_code.dart';
export 'src/password/account_resets.dart'
show forgetLapsedResets, resetDelay, resetLapsesAfter;
feat(auth): a password with TOTP on the register, and resets The register's side of signing in with an address, a password that never leaves the device (SRP-6a) and a code from an authenticator (ADR 0017), with members forwarding every step through AccountRegister: - password.set: salt, Argon2id's costs, SRP's verifier, the public keys and the private keys wrapped twice, all made on the device; with the address a mailed code proves, an anonymous person becomes identified under their ID. It signs in once an authenticator is confirmed. - passwordSignIn.begin and complete: SRP's two steps with a TOTP or recovery code. An address nobody has is answered from a decoy key as an account would be; every wrong credential is refused alike. - authenticator: secrets sealed under totpKey (XChaCha20-Poly1305, bound to their row), each code accepted once, ten recovery codes with the first, the last one kept. - accountRecovery: a lost authenticator reset 72 hours after the password and a mailed code or the recovery phrase ask, a lost password set anew with the phrase's signature, or the account reset with new keys 72 hours after a mailed code asks; a mail at each start, a sign-in calling it off. - An account with a password no longer signs in by a mailed code or GitHub; no new GitHub account is linked where passwords are offered, and from GITHUB_SIGN_IN_ENDS GitHub signs nobody in and its IDs go. Five tables, added by the module's migration; deletePerson deletes their rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 13:32:12 +02:00
export 'src/password/authenticators.dart'
show
forgetUnconfirmedAuthenticators,
maxAuthenticators,
unconfirmedAuthenticatorLifetime;
export 'src/password/key_proof.dart'
show
KeyProofPurpose,
PasswordProofPurpose,
authenticatorDigest,
canonicalInstanceUrl,
isAccountSignature,
keyProofMessage,
feat(base): mending a broken SimpleLogin link with the handle and the recovery phrase (ADR 0020, D7) Someone whose SimpleLogin identity is new to the register but who has an account ("I already have one") names their handle and proves the account's recovery phrase; the new identity takes the old one's place. - `simpleLogin.beginMend(attempt, handle)`: the keys wrapped under the phrase's key, the public keys and a nonce, for the account with that handle (whatever its case). Only an account with keys and a SimpleLogin identity is mended, a link it had, as nobody adds SimpleLogin signed out (D10); a handle nobody has, or any other account's, gets a decoy from `signInDecoyKey`, the same each time. At most 10 an hour per handle and 30 per network. - `simpleLogin.mend(attempt, proof)`: the phrase's proof for `mendLink`, binding the attempt (`mendLinkDigest`, on both sides), so a proof relayed from another sign-in links nobody's identity but the device's own; checked for the waiting identity first, which leaves the proof unspent. Every session of the account ends, in every service, and the sign-in stands ten minutes for a change, such as a lost password. - SimpleLogin needs `SIGN_IN_DECOY_KEY` alone of ADR 0017's three keys (`signInDecoyKeyOf`); `addInstanceRoutes` stops a server with a SimpleLogin app and without it (`checkSimpleLoginMending`). - Members ask the register for both; `RemoteRegister` and the instance API carry them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 22:26:58 +02:00
mendLinkDigest,
feat(base): the changes of SimpleLogin accounts' keys, each with the password and a fresh sign-in (ADR 0020, D20) Every change takes the password's proof by its own proof key (the recovery phrase's for a lost password) and a SimpleLogin sign-in of the last ten minutes on the device (`KeysOwnerProof`, its attempt), as ADR 0017's take the password and a code. The sign-in is checked before the proof and taken with the change, so a wrong proof leaves it standing and each sign-in serves one change. - `keys.set(attempt, setup, handle?)`: the first keys of an account that SimpleLogin was added to (anonymous before, or signing in with GitHub or a code), with the handle where it has none; the sign-in alone, as there is no password to prove yet. - `keys.changePassword`, `keys.beginPhrase` and `keys.resetPassword` (a lost password, with the phrase): every other session ends, in every service, as ADR 0017's change has it. - `keys.setRecoveryKeys`: a new phrase. - `account.delete(keysProof)`: an account without SRP's verifier deletes itself with the proof and the sign-in; `requireOwnerToDelete` no longer asks such an account for SRP. - `newKeysPasswordDigest` on both sides, held to each other by a test; a challenge purpose of its own for the phrase's proof while signed in. - Members ask the register for all of it; `RemoteRegister` and the instance API carry the five new operations and the deletion's proof. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 22:11:49 +02:00
newKeysPasswordDigest,
newPasswordDigest,
newPhraseDigest,
passwordIdentity,
proofKeyDigest,
publicInstanceUrlOf;
feat(auth): a password with TOTP on the register, and resets The register's side of signing in with an address, a password that never leaves the device (SRP-6a) and a code from an authenticator (ADR 0017), with members forwarding every step through AccountRegister: - password.set: salt, Argon2id's costs, SRP's verifier, the public keys and the private keys wrapped twice, all made on the device; with the address a mailed code proves, an anonymous person becomes identified under their ID. It signs in once an authenticator is confirmed. - passwordSignIn.begin and complete: SRP's two steps with a TOTP or recovery code. An address nobody has is answered from a decoy key as an account would be; every wrong credential is refused alike. - authenticator: secrets sealed under totpKey (XChaCha20-Poly1305, bound to their row), each code accepted once, ten recovery codes with the first, the last one kept. - accountRecovery: a lost authenticator reset 72 hours after the password and a mailed code or the recovery phrase ask, a lost password set anew with the phrase's signature, or the account reset with new keys 72 hours after a mailed code asks; a mail at each start, a sign-in calling it off. - An account with a password no longer signs in by a mailed code or GitHub; no new GitHub account is linked where passwords are offered, and from GITHUB_SIGN_IN_ENDS GitHub signs nobody in and its IDs go. Five tables, added by the module's migration; deletePerson deletes their rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 13:32:12 +02:00
export 'src/password/password_config.dart';
export 'src/password/passwords.dart'
show
isKeysPassword,
isKeysSetup,
isNewPassword,
isPasswordSetup,
feat(auth): a password with TOTP on the register, and resets The register's side of signing in with an address, a password that never leaves the device (SRP-6a) and a code from an authenticator (ADR 0017), with members forwarding every step through AccountRegister: - password.set: salt, Argon2id's costs, SRP's verifier, the public keys and the private keys wrapped twice, all made on the device; with the address a mailed code proves, an anonymous person becomes identified under their ID. It signs in once an authenticator is confirmed. - passwordSignIn.begin and complete: SRP's two steps with a TOTP or recovery code. An address nobody has is answered from a decoy key as an account would be; every wrong credential is refused alike. - authenticator: secrets sealed under totpKey (XChaCha20-Poly1305, bound to their row), each code accepted once, ten recovery codes with the first, the last one kept. - accountRecovery: a lost authenticator reset 72 hours after the password and a mailed code or the recovery phrase ask, a lost password set anew with the phrase's signature, or the account reset with new keys 72 hours after a mailed code asks; a mail at each start, a sign-in calling it off. - An account with a password no longer signs in by a mailed code or GitHub; no new GitHub account is linked where passwords are offered, and from GITHUB_SIGN_IN_ENDS GitHub signs nobody in and its IDs go. Five tables, added by the module's migration; deletePerson deletes their rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 13:32:12 +02:00
newPasswordCosts,
passwordGroup,
passwordMethod,
signInChallengeLifetime,
signsInWithPassword,
wrappedKeysLength;
export 'src/profile/handle_checks.dart';
export 'src/profile/link_verification.dart';
export 'src/profile/profile_page.dart' show addProfilePageRoute, profilePage;
export 'src/profile/profile_rules.dart'
show
accountDomain,
avatarPath,
avatarStorage,
isHandle,
maxAvatarBytes,
operatorAccountDomain,
profilePageUrl,
reservedHandles;
export 'src/profile/profiles.dart';
export 'src/register/account_register.dart';
export 'src/register/accounts_mode.dart';
export 'src/register/deletion_notices.dart'
show MemberNotifier, deliverDeletionNotices;
export 'src/register/instance_routes.dart';
export 'src/register/member_pruning.dart';
export 'src/register/people.dart' show issueSession;
export 'src/register/sessions_ended.dart'
show AccountSessionsEnded, deliverSessionNotices;
feat(base,app,learn): invitations to shared spaces by mail and by link The instance's half of Halkyon's ADR 0044, step 5 (D37 to D40). An invitation by mail (sharedSpace.inviteByEmail) goes out through the register (send-space-invitation), naming who invites by their handle and nothing of what is shared, with a link to /join/<token>. The instance keeps only the address's keyed hash, as for sign-in, and the SHA-256 of the token. Whoever opens it joins with a code mailed to that address (EmailCodePurpose.joinSpace, proven through the register, prove-address-code), so a forwarded mail admits nobody else; a code proving another address is refused. It lapses after 14 days, and the hourly pruning forgets it; inviting the address again replaces it. A space's own link (setJoinLink, the Owner's) admits whoever opens it as a Viewer (joinByLink) while it is on; switched off and on, it gets a new token. Its token is kept as it is, so that whoever may invite copies the link again (joinLink). The page at /join/<token>, in English and German, says who invites and as whom; preview tells the app the same. A membership someone else made is new to its member until they mark it seen (isNew, addedBy; mine names who shared it), for the Inbox. halkyon_app learns the two new refusals (handleNeeded, otherAddress), which Learn reports as not allowed until its screens offer these ways in. The ADR records D37 to D40 and how the instance's half is built. The migration, the same in the module and in each server including it (Learn, Meditate, Todo, Train): ALTER TABLE "shared_space" ADD COLUMN "joinToken" text; CREATE UNIQUE INDEX "shared_space_join_token_idx" ON "shared_space" ("joinToken"); CREATE TABLE "shared_space_email_invitation" (…), with its unique indexes on the space and address hash, and on the token's hash, and its foreign key to "shared_space" ON DELETE CASCADE; ALTER TABLE "shared_space_member" ADD COLUMN "addedBy" uuid; ALTER TABLE "shared_space_member" ADD COLUMN "isNew" boolean NOT NULL DEFAULT false; Every space gets no token, and the unique index allows any number of nulls; every membership gets no one who added it and isn't new, so nothing already shared shows in an Inbox. The new table starts empty. None of it rewrites a table, and the server before the switch names its own columns, so it keeps working. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 18:28:13 +01:00
export 'src/shared_space/join_page.dart' show addJoinPageRoute, joinPage;
feat(base,learn): suggest changes within a shared studyset ADR 0044's step 6b (D44, D46 to D48): a Viewer, and an agent, suggest changes within a shared studyset, which its Owner and Editors accept or decline. A suggestion or report is written as Explore's are, in its writer's own synced space, with kinds of its own (space-suggestion, space-report) and the space's id in feedback_thread's publicationId, so no synced table changes and older apps, skipping kinds they don't know, keep syncing. Where the studyset is private it is sealed with its key, bound to the space; a reply to it is sealed for its thread. Learn's server gathers a space's threads for its members alone (feedback.threadsInSpace, NotFound for anyone else): only threads and replies by members now, decisions only from rows in the space itself, where the engine lets its Owner and Editors alone write; sealed payloads pass in base64 with their key and nonce. The inbox adds the threads its caller decides on or wrote. The module tells a host a person's spaces and each space's members (sharedSpacesOf, membersOfSpaces). No migration: no table changes. In the app, a Viewer's card and note offer Suggest a change: the fields write into the suggestion alone, sent with a message. A shared studyset's menu lists its Suggestions, where its Owner and Editors accept a change as a revision, or a new card or note into the studyset, and decide in the space. A suggestion naming a file that isn't the studyset's, or bringing files of its own, isn't taken. Whats-New: In a studyset shared with you as a viewer, suggest a change to a card or note; its owner and editors find your suggestions under Suggestions in its menu and accept or decline them. Whats-New-De: In einem Lernset, das du nur ansehen darfst, schlägst du Änderungen an Karteikarten und Notizen vor; Besitzer:in und Bearbeiter:innen finden sie im Menü unter „Vorschläge“ und nehmen sie an oder lehnen sie ab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 20:34:06 +01:00
export 'src/shared_space/shared_spaces.dart'
show forgetLapsedSpaceInvitations, membersOfSpaces, sharedSpacesOf;
feat(base,learn): spaces people share, kept by halkyon_base ADR 0044, step 2 (D1, D2, D11, D13, and D15 to D17, decided today). halkyon_base keeps the spaces several people share, for every app: shared_space, its members with their roles (Owner, Editor, Viewer), and invitations by handle waiting for someone with no account on the service yet. The sharedSpace endpoint creates a space, lists its members, invites by handle, changes roles, removes members, hands the space on, leaves and deletes it. Who may do what: - Only people with an identity reach the endpoint. - A space the caller isn't in can't be told from one that doesn't exist. - Members are named by their membership, never by their account's ID. - Only the Owner manages the members, unless the space lets members invite. - The Owner's role passes only by handing the space on, and the Owner leaves only naming who takes it on. - An invitation counts as an answer about a handle, as it tells whether one exists. Every rule has a test asserting the refusal. The module stays free of the sync engine: a host hands it a SpaceGrants, and every call runs in the module's transaction, so the engine's grants follow the roles or nothing changes. The Owner and an Editor write; a Viewer reads. Learn hands over SyncSpaceGrants on OfflineSyncSpaces. Todo hands over none yet, so its server refuses until its projects are shared. Deleting an account hands each space the person owns to the Editor who joined first, else the Viewer who joined first. A space with no other member goes. This runs before the host's own deletions. A person's first sign-in on a service makes their waiting invitations memberships, in the same transaction that makes them a person there. Migrations: the module's and every host's (Learn, Todo, Train, Meditate) create the three tables, their indexes and the two cascading foreign keys to shared_space, and nothing else. The old server keeps working. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 10:33:12 +01:00
export 'src/shared_space/space_grants.dart';
export 'src/simplelogin/handle_suggestion.dart'
show handleFromName, handlesToTry, suggestable;
export 'src/simplelogin/simplelogin.dart' show forgetUnfinishedSimpleLogins;
feat(base): SimpleLogin's forwarding page and Learn's App Links statement (ADR 0020, step 1) SimpleLogin takes only https redirect addresses, each registered exactly, so the instance's SimpleLogin app registers one page on the register's web server, /simplelogin-callback, which sends the browser on to the app that began the sign-in: a 303 to the desktop's loopback (127.0.0.1, a port per app) or the phone's scheme, chosen by the marker the state begins with (`<app>-<platform>.<rest>`, the rest 1 to 512 characters of base64url), from a fixed table, so it sends nobody anywhere else. It forwards the code or SimpleLogin's error (`deny`) with the state and nothing else, refuses anything malformed, repeated or not a GET with a page that repeats nothing it was given, and keeps and logs nothing (no-store, no referrer). It is built ahead of the sign-in itself so development can sign in through learn.halkyon.eu's page, which the development app registers too. Android's assetlinks.json for Halkyon Learn signed with the release key is answered by the Caddyfile on learn.halkyon.eu alone: the one domain the project's builds name, and the server knows no app's key. A root test holds its fingerprint to tool/release/android-certificate.sha256. The app's intent filter comes with the sign-in; iOS's association waits for an Apple team ID. The profile page's language choice and look move to common/web_page.dart, shared by both pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 15:27:56 +02:00
export 'src/simplelogin/simplelogin_callback.dart'
show
addSimpleLoginCallbackRoute,
simpleLoginCallbackPath,
simpleLoginReturns;
export 'src/simplelogin/simplelogin_config.dart';
export 'src/srp/srp_group.dart';
export 'src/srp/srp_math.dart' show numberOf, padded;
export 'src/srp/srp_server.dart'
show
SrpAccepted,
SrpChallenge,
SrpRefusal,
SrpRefused,
SrpVerification,
srpChallenge,
srpVerify;
export 'src/totp/base32.dart';
export 'src/totp/recovery_codes.dart';
export 'src/totp/totp.dart';