The Atsign FoundationThe Atsign Foundation

pub package pub points gitHub license

at_onboarding_cli

The command-line tools, and a small library, for registering, activating and enrolling atSigns from a terminal or a headless program. The lifecycle itself is at_client's — Atsign.activate, Atsign.open and Atsign.enroll do the work — and this package wraps those verbs in two binaries, plus an AtOnboardingService for programs written against earlier versions.

If you're new to the Atsign Protocol lifecycle (register → activate → APKAM enroll), read at_auth's README first — this package is the CLI concretisation of that model. at_client_flutter is the Flutter-UI equivalent.

Turnkey CLI tools

Both ship as executables when this package is globally activated:

dart pub global activate at_onboarding_cli

at_register — get a free atSign

at_register -e your_email@example.com

Fetches a free atSign, emails you a verification code, then activates the atSign once you paste the code back. The generated .atKeys file lands in ~/.atsign/keys/.

at_activate — the atSign's lifecycle from the terminal

Every invocation should name a command. With none, at_activate still runs onboard, as 1.x did, but prints a deprecation warning, because 3.0 will refuse it.

# Activate a newly registered atSign with the CRAM secret the registrar sent
at_activate onboard -a @alice -c <cram_secret>

# ...or let at_activate fetch it: the registrar emails a verification code,
# which you paste back
at_activate onboard -a @alice

Either form writes the master .atKeys to ~/.atsign/keys/@alice_key.atKeys. These are the root of trust for @alice — back them up.

The remaining commands run on the keyfile -k names (that one by default):

Command What it does
status -a @alice asks where the atSign stands; the exit code says: 0 activated, 1 atServer up but not yet activated, 2 atServer unreachable, 3 no such atSign, 4 atDirectory unreachable
enroll -a @alice --app <app> --device <device> --namespaces <ns:rw,...> --passcode <otp> submits an APKAM enrollment for a new app and device and waits for its approval; run again for the same app and device, it resumes the wait rather than submitting a second request
otp / spp issues a one-time passcode, or sets a semi-permanent one, that a new enrollment quotes
list, fetch the enrollment roster, or one enrollment's record
approve, deny, revoke, unrevoke, delete decides an enrollment by its id; auto listens and approves the requests that match its app and device patterns
interactive a shell over the commands above
decrypt writes a passphrase-protected keyfile out decrypted

--posture legacy|pqReady|pqActive is accepted on every command and decides what a client built for it does post-quantum (see the at_client README). An unnamed --posture is at_client's default on every command (legacy today), and onboard and enroll say which posture they ran at. Approving a post-quantum enrollment needs the post-quantum providers, so under legacy at_client refuses such an approval when it reads the request; pass --posture pqReady to approve one. enroll --key-exchange legacy|pq chooses how the enrollment's symmetric key travels, for the approver that will pick the request up.

APKAM enrollment

A new device / app authenticating as an existing atSign should go through APKAM rather than asking the user for their master keys. The worked example lives under example/apkam_examples/:

  • apkam_enroll.dart — the new device submits an enrollment request scoped to specific namespaces
  • enroll_app_listen.dart — a device holding the master keys listens for and approves / denies incoming requests, through client.enrollments
  • apkam_authenticate.dart — the new device authenticates with its newly-issued scoped keys

Full step-by-step walkthrough: example/README.md.

Library usage

The lifecycle is at_client's: one import, and the verbs are on the atSign. AtOnboardingPreference extends AtClientPreference with where the keyfile and the local storage live, and storageFor(atSign) is the store a client for that atSign opens under it.

import 'package:at_client/at_client.dart';
import 'package:at_onboarding_cli/at_onboarding_cli.dart';

final keys = FileAtKeysIo(filePath: (_) => 'storage/@alice_key.atKeys');
final pref = AtOnboardingPreference()
  ..rootDomain = 'root.atsign.org'
  ..namespace = 'my_app'
  ..storagePath = 'storage/hive';

pref.lookUps is the AtLookUpFactory every client opened under the preference builds its connections with, and the commands pass it to the verbs: proxyLookUps() when rootDomain names a proxy (proxy:host:port), which sends from:<atSign> first on every connection so the proxy can route it, and TLS on TCP otherwise. Set it for a transport of your own.

Activate a new atSign with its CRAM secret (OnboardingUtil fetches one from the registrar against an emailed verification code):

final owner = await Atsign('@alice').activate(
    cramSecret: secret, keys: keys, preference: pref,
    storage: pref.storageFor('@alice'));

Open a client on keys already held. It comes back online, offline or refused, and connection says which:

final client = await Atsign('@alice').open(
    keys: keys, preference: pref, storage: pref.storageFor('@alice'));
print(client.connection.current);

Enrol a new device, quoting a passcode an enrolled client issued, and wait for that client to approve. The keyfile is the resume record: a request already in it is picked up by resumeEnrollment rather than repeated.

final pending = await Atsign('@alice').enroll(
    otp: otp, app: 'my_app', device: 'laptop',
    namespaces: {'my_app': 'rw'}, keys: keys, preference: pref);
final enrolled = await pending.client(pref, storage: pref.storageFor('@alice'));

The approving side, on an enrolled client:

for (final request in await owner.enrollments.pending()) {
  await owner.enrollments.approve(request.enrollmentId!);
}
final passcode = await owner.enrollments.otp();

Two helpers stay for programs written against earlier versions of this package. AtOnboardingServiceImpl('@alice', pref).authenticate() opens the client from pref.atKeysFilePath, makes it AtClientManager.getInstance().atClient, and answers whether it is online; createAtClient(atSign: '@alice', atKeysFilePath: ..., rootDomain: ...) does the same from bare arguments and waits for the connection with a budget of maxConnectAttempts tries.

Worked examples covering each flow: example/ and example/legacy_examples/.

Most app developers don't need this library directly — they use at_cli_commons' CLIBase, which opens the client through at_client.

Migrating from 1.x

2.0 moves everything AtOnboardingService orchestrated to at_client, keeps authenticate() and atClient for the programs that call them, and makes at_activate name its command. The .atKeys file a 1.x tool wrote is read unchanged.

1.x 2.0
at_activate -a @alice -c <secret> (no command) at_activate onboard -a @alice -c <secret>; an invocation naming no command still runs onboard, with a deprecation warning
(no signing-algorithm flag) --posture legacy|pqReady|pqActive, honoured on every command and defaulting to at_client's posture; enroll --key-exchange legacy|pq for how the enrollment's key travels
AtOnboardingServiceImpl(atSign, pref).onboard() Atsign(atSign).activate(cramSecret: ..., keys: ..., preference: pref, storage: pref.storageFor(atSign))
.authenticate() unchanged: opens the client through Atsign.open, makes it current, and answers true only when its connection is online; an offline client is still held, atClient.connection says why
.authenticate(enrollmentId: ...) removed: the keyfile decides which enrollment authenticates
.enroll(...), .sendEnrollRequest(...), .awaitApproval(...), .createAtKeysFile(...) Atsign(atSign).enroll(...) and PendingEnrollment.client(...); the keyfile named on enroll is the resume record, and Atsign.resumeEnrollment picks a pending request up after a restart
the *.enrollment.checkpoint file gone; the keyfile holds the pending keys
.close() atClient.stop()
.isOnboarded() at_activate status, or at_lookup's checkAtSignServer over a lookup the app builds
.atLookUp, .atChops, .atAuth, .completeActivation() none; the client's own connection does what they exposed
.getAtClient() .atClient; the deprecated getter is gone
AtOnboardingPreference.hiveStoragePath, .commitLogPath .storagePath, or .storage for a bundle of your own; commitLogPath was never read
AtOnboardingPreference()..signingAlgoType = ... AtOnboardingPreference(posture: ..., authenticationKeyAlgorithm: ..., dataSigningKeyAlgorithms: ...), fixed at construction
package:at_onboarding_cli/src/activate_cli/activate_cli.dart removed; run the at_activate binary
authenticate() copying the keyfile's keys into the client's local storage the client reads them from its key source
the keyfile onboard writes at_auth's own document: the self-encryption key is no longer duplicated under the atSign, and a passphrase-protected file uses a salted envelope that 1.x tooling cannot read

Before and after

The things a 1.x program commonly did, each as it was and as it is now.

Authenticate an onboarded atSign and get the client

// 1.x
final pref = AtOnboardingPreference()
  ..rootDomain = 'root.atsign.org'
  ..namespace = 'my_app'
  ..hiveStoragePath = 'storage/hive'
  ..commitLogPath = 'storage/commitLog'
  ..isLocalStoreRequired = true
  ..atKeysFilePath = 'storage/@alice_key.atKeys';
final svc = AtOnboardingServiceImpl('@alice', pref);
await svc.authenticate();
final AtClient? atClient = await svc.atClient;
final AtLookUp? atLookup = svc.atLookUp;

// 2.0, smallest change: the service still authenticates and holds the client
final pref = AtOnboardingPreference()
  ..rootDomain = 'root.atsign.org'
  ..namespace = 'my_app'
  ..storagePath = 'storage/hive'
  ..atKeysFilePath = 'storage/@alice_key.atKeys';
final svc = AtOnboardingServiceImpl('@alice', pref);
final online = await svc.authenticate();   // false: held, but offline; atClient.connection says why
final AtClient atClient = svc.atClient!;

// 2.0, the verb itself: no service
final keys = FileAtKeysIo(filePath: (_) => 'storage/@alice_key.atKeys');
final client = await Atsign('@alice').open(
    keys: keys, preference: pref, storage: pref.storageFor('@alice'));
print(client.connection.current);          // online | offline | refused

Anything the program reached through svc.atLookUp it does on the client's own connection now: client.getRemoteSecondary() for a verb, and the connection for whether the atServer was reached.

Onboard with a CRAM secret

// 1.x
final pref = AtOnboardingPreference()
  ..cramSecret = secret
  ..atKeysFilePath = 'storage/@alice_key.atKeys'
  ..rootDomain = rootDomain;
final ok = await AtOnboardingServiceImpl('@alice', pref).onboard();
if (!ok) exit(1);

// 2.0: the verb writes the keys to `keys` and hands back the client it opened
final client = await Atsign('@alice').activate(
    cramSecret: secret, keys: keys, preference: pref,
    storage: pref.storageFor('@alice'));
await client.stop();                        // a tool that wanted the keys, not a session

Enroll a device, and wait for the approval

// 1.x: the response said whether it was approved
final AtEnrollmentResponse response = await svc.enroll(
    'my_app', 'laptop', otp, {'my_app': 'rw'}, atKeysFile: File(path));
if (response.enrollStatus != EnrollmentStatus.approved) exit(1);

// 2.0: `enroll` files the request in `keys` as pending; `client` waits for
// the approval and hands back the enrolled client, or throws on a denial.
final pending = await Atsign('@alice').enroll(
    otp: otp, app: 'my_app', device: 'laptop',
    namespaces: {'my_app': 'rw'}, keys: keys, preference: pref);
try {
  final enrolled = await pending.client(pref, storage: pref.storageFor('@alice'));
} on AtEnrollmentException catch (e) {
  print('not approved: ${e.message}');
  exit(1);
}
// After a restart, the pending request is picked up rather than repeated:
final resumed = await Atsign('@alice').resumeEnrollment(
    app: 'my_app', device: 'laptop', keys: keys, preference: pref);

Finish

// 1.x
await svc.close();

// 2.0
await client.stop();

The command line

# 1.x
at_activate -a @alice -c <cram_secret>

# 2.0: the command is named; the posture is one flag on every command
at_activate onboard -a @alice -c <cram_secret>
at_activate onboard -a @alice -c <cram_secret> --posture pqReady

Where to go next

  • at_auth — the keyfile format and key stores, and the protocol layer under at_client
  • at_cli_commons — thin layer that gets you from already-onboarded atKeys to an authenticated AtClient in one line
  • at_client_flutter — the Flutter-UI equivalent of this package

Open source usage and contributions

BSD3-licensed. See CONTRIBUTING.md for guidance on setting up tools, running tests, and raising a PR.

Libraries

at_onboarding_cli