Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
4b1083f
feat: introduce np_activate_params class and np_activate_utils
srieteja Nov 6, 2025
99a0d12
feat: introduce np_activate_impl containing actual cram/enroll wrappers
srieteja Nov 6, 2025
e84fd0c
feat: introduce np_activate_cli
srieteja Nov 6, 2025
7990ba9
build: update buildBinaries to compile noports_cli
srieteja Nov 13, 2025
6d726f2
feat: reintroduce NPActivate impl
srieteja Nov 13, 2025
2b5b727
feat: introduce NPIssueKeys impl
srieteja Nov 13, 2025
75f4a85
feat: introduce noports_cli utils/
srieteja Nov 13, 2025
060c86f
feat: introduce CLI entrypoint 'noports' in sshnoports/bin
srieteja Nov 13, 2025
f45b60e
chore: temp pubspec overrides for at_client and onboarding_cli
srieteja Nov 13, 2025
5b4a778
Merge branch 'trunk' into noports_activate
srieteja Nov 13, 2025
4a0d2c7
docs: added method docs
srieteja Nov 13, 2025
3d56f0e
Merge remote-tracking branch 'origin/noports_activate' into noports_a…
srieteja Nov 13, 2025
7a01f69
build(deps): add at_auth dep in sshnoports
srieteja Nov 13, 2025
d4638d9
Merge branch 'trunk' into noports_activate
srieteja Nov 13, 2025
0bbf4ce
refactor: readability improvements + bug fixes + docs
srieteja Nov 17, 2025
a36b39e
build: consume at_onboarding_cli v1.14.1
srieteja Nov 17, 2025
6c88288
Merge remote-tracking branch 'origin/noports_activate' into noports_a…
srieteja Nov 17, 2025
68b0a94
Merge branch 'trunk' into noports_activate
srieteja Nov 17, 2025
f4c2cac
fix: do not pass atkeys path to atClient
srieteja Nov 17, 2025
f250cda
Merge remote-tracking branch 'origin/noports_activate' into noports_a…
srieteja Nov 17, 2025
f3ecf17
Merge branch 'trunk' into noports_activate
srieteja Nov 23, 2025
93f0b41
feat: introduce cli_logging_handler.dart
srieteja Nov 26, 2025
324f2e6
feat: refactor utils/
srieteja Nov 26, 2025
bf3ccae
feat: support for stateFile to allow resuming issue-keys command
srieteja Nov 26, 2025
ed60b50
feat: support for stateFile to allow resuming issue-keys command
srieteja Nov 26, 2025
ad51fe4
Merge branch 'trunk' into noports_activate
srieteja Nov 26, 2025
8befdb2
Merge remote-tracking branch 'origin/noports_activate' into noports_a…
srieteja Nov 26, 2025
984a5a0
feat: refactoring for readability
srieteja Nov 27, 2025
f98b80a
Merge branch 'trunk' into noports_activate
srieteja Nov 27, 2025
6cfe1a9
fix: help bug
srieteja Nov 27, 2025
5d8a5b8
Merge remote-tracking branch 'origin/noports_activate' into noports_a…
srieteja Nov 27, 2025
5007ec7
Merge branch 'trunk' into noports_activate
gkc Dec 2, 2025
8c7f4a3
refactor: move noports_cli from sshnoports -> noports_core
srieteja Dec 5, 2025
d66b030
build(deps): update at_onboarding_cli to v1.14.2
srieteja Dec 5, 2025
c6a1fa3
chore: dart formatter code cleanup
srieteja Dec 5, 2025
9588a2a
build(deps): remove at_auth dep from sshnoports
srieteja Dec 5, 2025
11c6307
Merge branch 'trunk' into noports_activate
srieteja Dec 5, 2025
d4dc431
build(deps): update pubspec.lock in np_admin
srieteja Dec 5, 2025
cbce19d
Merge remote-tracking branch 'origin/noports_activate' into noports_a…
srieteja Dec 5, 2025
fcad752
Merge branch 'trunk' into noports_activate
gkc Dec 9, 2025
a441257
Merge branch 'trunk' into noports_activate
gkc Dec 16, 2025
f7527bc
feat: Introduce separate parser for issue-keys and consume it
srieteja Dec 16, 2025
64ea4a6
feat: activate command uses a hybrid parser. relocated to noports_cor…
srieteja Dec 16, 2025
eabd000
feat: move utils to noports_core/src/commands/util/
srieteja Dec 16, 2025
8ba58bc
Merge branch 'trunk' into noports_activate
srieteja Dec 16, 2025
492755c
feat: introduce HelpRequestedException use it
srieteja Dec 16, 2025
12cedd7
Merge remote-tracking branch 'origin/noports_activate' into noports_a…
srieteja Dec 16, 2025
3a7ddbd
fix: changes from self review
srieteja Dec 16, 2025
90a00f6
fix: constructors made public + minor formatting
srieteja Dec 17, 2025
402c948
Merge branch 'trunk' into noports_activate
srieteja Dec 17, 2025
8a4a9d7
docs: updated usage messages + minor fixes
srieteja Dec 17, 2025
c59f870
Merge remote-tracking branch 'origin/noports_activate' into noports_a…
srieteja Dec 17, 2025
a882ef9
Merge branch 'trunk' into noports_activate
srieteja Dec 18, 2025
56faa5d
Merge branch 'trunk' into noports_activate
gkc Dec 19, 2025
edd41b2
chore: minor tweaks and cleanup
srieteja Dec 19, 2025
5070f49
Merge remote-tracking branch 'origin/noports_activate' into noports_a…
srieteja Dec 19, 2025
0a3b19e
Merge branch 'trunk' into noports_activate
srieteja Dec 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/admin/admin_api/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ packages:
dependency: transitive
description:
name: at_onboarding_cli
sha256: "264be4a871a4d6cc417ae74886eebac36defc72297f4a9dd4dda37b720bdfbc0"
sha256: "9e8317662dffb447e5dd41e82d5825eea0d44460e01c106570902224a97abd44"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.14.2"
at_persistence_secondary_server:
dependency: transitive
description:
Expand Down
10 changes: 10 additions & 0 deletions packages/dart/noports_core/lib/commands.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
library noports_core_activate;

// Implementations
export 'package:noports_core/src/commands/activate/activate_impl.dart';
export 'package:noports_core/src/commands/issue_keys/issue_keys_impl.dart';

// Utilities
export 'package:noports_core/src/commands/utils/console.dart';
export 'package:noports_core/src/commands/utils/usage_messages.dart';
export 'package:noports_core/src/common/help_requested_exception.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import 'dart:io';

import 'package:at_auth/at_auth.dart';
import 'package:at_client/at_client.dart';
import 'package:at_onboarding_cli/at_onboarding_cli.dart';
import 'package:at_utils/at_logger.dart';
import 'package:noports_core/commands.dart';
import 'package:noports_core/src/commands/activate/activate_params.dart';
import 'package:noports_core/utils.dart';

class Activate {
final ActivateParams _params;

final AtOnboardingService _onboardingService;

final logger = AtSignLogger('Activate', loggingHandler: CLILoggingHandler())
..level = 'info';

Activate(this._onboardingService, this._params);

factory Activate.fromArgs(List<String> args) {
if (args.isEmpty) {
throw ArgumentError('At least one argument is required.');
}

ActivateParams params = ActivateParams.fromArgs(args);

final preference = AtOnboardingPreference()
..cramSecret = params.cramSecret
..atKeysFilePath = params.atKeysFilePath;
AtOnboardingService service = AtOnboardingServiceImpl(
params.atsign,
preference,
);

return Activate(service, params);
}

/// Entry point for the activate command
Future<int> wrappedMain() async {
switch (_params.type) {
case ActivateType.cram:
return await cramAuthenticate();
case ActivateType.enroll:
return await enroll();
}
}

/// Authenticates an existing atSign using CRAM credentials
///
/// Requires [_params.cramSecret] to be set
///
/// Returns: 0 if authentication succeeds, 1 in case of failure
/// Throws: [ArgumentError] if cram credentials are missing
Future<int> cramAuthenticate() async {
if (_params.cramSecret == null) {
throw ArgumentError('Cannot perform CRAM auth without secret');
}
logger.info('Activating atsign: ${_params.atsign}');

final success = await _onboardingService.onboard();

if (!success) {
logger.shout('Activation Failed');
return 1;
}
logger.info('Activated');
return 0;
}

/// Enrolls a new device using APKAM enrollment
///
/// Requires [_params.otp] and [_params.device] to be set.
/// Optionally uses [_params.atKeysFilePath] if provided.
///
/// Returns: [AtEnrollmentResponse] containing enrollment status and details
/// Throws: [ArgumentError] if otp is missing
Future<int> enroll() async {
if (_params.otp == null) {
throw ArgumentError('Cannot create enrollment without otp');
}
logger.info(
'Creating new enrollment with deviceName: ${_params.deviceName}',
);

await _validateAndPrepareKeysFile();

final response = await _onboardingService.enroll(
_params.appName,
_params.deviceName!,
_params.otp!,
_params.namespaces,
atKeysFile: _getKeysFile(),
);

return response.enrollStatus == EnrollmentStatus.approved ? 0 : 1;
}

/// Validates and prepares the atKeys file location before enrollment.
///
/// This method checks if a keys file already exists at the target location
/// (either specified by the user or at the default path) and prompts the user
/// to provide an alternate location if a file is found. This prevents
/// accidentally overwriting existing authentication keys during enrollment.
///
/// The validation flow:
/// 1. If [_params.atKeysFilePath] is null, checks the default keys file path
/// from the [AtOnboardingPreference]
/// 2. If [_params.atKeysFilePath] is provided, checks that custom path
/// 3. If a file exists at either location, prompts the user for an alternate
/// path and updates [_params.atKeysFilePath]
/// 4. If no file exists, returns without modification (safe to proceed)
///
///
/// TODO: Replace [AtOnboardingServiceImpl] after ensuring that
/// [AtOnboardingService] throws a specific exception when keyFile exists at defaultPath
Future<void> _validateAndPrepareKeysFile() async {
final String? keysFilePath;

if (_params.atKeysFilePath == null) {
// Use the default path from onboarding service preferences
Copy link
Contributor Author

@srieteja srieteja Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workaround to avoid conflicts with creating atKeys files. Currently fetches the defaultPath from OnboardingServiceImpl.

Changes required: OnboardingService should throw a dedicated exception for key file name collisions, which can then be caught and handled.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to address this issue fully in this PR. Find the places that MUST have file existence checking (onboard, enroll), ensure the checking always happens there. Remove file existence checking from elsewhere and verify that all the ways we create atKeys files do end up having a file existence check so we don't ever overwrite keys files. As a final safeguard, put a check into the place we finally write an AtKeys file and do not permit overwriting.

keysFilePath = (_onboardingService as AtOnboardingServiceImpl)
.atOnboardingPreference
.atKeysFilePath;
} else {
// Use the user-provided path
keysFilePath = _params.atKeysFilePath;
}

if (keysFilePath != null && !File(keysFilePath).existsSync()) {
return;
}

logger.shout('KeysFile exists at $keysFilePath');
_params.atKeysFilePath = promptUser(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if the user puts the same path in again?

'Please provide alternate location to store keyfile',
);
logger.info('Sending updated enrollment request');
}

File? _getKeysFile() {
final path = _params.atKeysFilePath;
return path != null ? File(path) : null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import 'package:args/args.dart';
import 'package:at_commons/atsign.dart';
import 'package:noports_core/commands.dart';
import 'package:noports_core/src/commands/utils/constants.dart';
import 'package:noports_core/src/commands/utils/regex.dart';

enum ActivateType {
cram,
enroll;

static ActivateType parse(String input) {
try {
return values.firstWhere((type) => input.contains(type.name));
} on Exception {
throw ArgumentError(
'Invalid activation type in: $input (expected "cram" or "enroll")',
);
}
}
}

class ActivateParams {
final String atsign;
final ActivateType type;
final String? cramSecret;
final String? otp;
final String? deviceName;
final appName = defaultAppName;
final namespaces = defaultEnrollmentNamespaces;

String? atKeysFilePath;

// Static parser for consistent usage and help generation
static final ArgParser argParser = _createArgParser();

ActivateParams({
required this.atsign,
required this.type,
this.cramSecret,
this.otp,
this.deviceName,
this.atKeysFilePath,
});

factory ActivateParams.fromArgs(List<String> args) {
final results = argParser.parse(args);

if (results.wasParsed('help')) {
throw HelpRequestedException();
}

if (results.rest.isEmpty) {
throw ArgumentError(
'Activation string is required (e.g. @alice:cram:secret or'
' @alice:enroll:otp:123456)',
);
}

final activationString = results.rest.single;
final type = ActivateType.parse(activationString);

final atsign = _parseAtsign(activationString, type);
if (atsign == null || atsign.isEmpty) {
throw ArgumentError('atsign is required in activation string');
}

// parse from arg parser results
final keyfile = results['target-keyfile'] as String?;

return ActivateParams(
atsign: atsign,
type: type,
cramSecret: _parseCramSecret(activationString),
otp: _parseOtp(activationString),
deviceName: _parseDeviceName(activationString),
atKeysFilePath: keyfile,
);
}

static ArgParser _createArgParser() {
return ArgParser()
..addOption(
'target-keyfile',
abbr: 't',
mandatory: false,
help: 'Destination path for atKeys file',
)
..addFlag(
'help',
abbr: 'h',
negatable: false,
help: 'Show this usage info',
);
}

static String? _parseAtsign(String input, ActivateType type) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return type should be Atsign? not String?

final regex = type == ActivateType.cram
? ActivateRegex.cram
: ActivateRegex.enroll;

final match = regex.firstMatch(input);
final atsign = match?.namedGroup(ActivateRegexGroups.atsign);

return atsign != null && atsign.isNotEmpty ? atsign.toAtsign() : null;
}

static String? _parseCramSecret(String input) {
final match = ActivateRegex.cram.firstMatch(input);
return match?.namedGroup(ActivateRegexGroups.cram);
}

static String? _parseOtp(String input) {
final match = ActivateRegex.enroll.firstMatch(input);
return match?.namedGroup(ActivateRegexGroups.otp);
}

static String? _parseDeviceName(String input) {
final match = ActivateRegex.enroll.firstMatch(input);
return match?.namedGroup(ActivateRegexGroups.deviceName);
}

Map<String, String?> toJson() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is type not part of the json?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are missing a test somewhere to ensure this kind of problem doesn't happen

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after further discussion we concluded that toJson is no longer needed and can be removed from this class

return {
'atsign': atsign,
'cramSecret': cramSecret,
'otp': otp,
'deviceName': deviceName,
'atKeysFilePath': atKeysFilePath,
};
}
}
Loading