Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 47 additions & 5 deletions crates/frameshift-cli/src/cmd/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ use clap::{ArgGroup, Args, Subcommand, ValueEnum};
use frameshift_catalog::{AccountInviteStatus, AccountStatus, PlatformRole};
use frameshift_client::account::{
assign_account_platform_role, create_publisher_profile, get_account, get_auth_config,
issue_account_invite, list_account_invite_requests, login_local_account, logout_local_account,
register_local_account, review_account_invite_request, revoke_account_platform_role,
set_account_status, update_account_profile, update_publisher_profile,
AccountInviteReviewStatus, AccountView, IssuedAccountInvite, LocalAccountSession,
NativeAuthClient,
get_publisher_profile, issue_account_invite, list_account_invite_requests, login_local_account,
logout_local_account, register_local_account, review_account_invite_request,
revoke_account_platform_role, set_account_status, update_account_profile,
update_publisher_profile, AccountInviteReviewStatus, AccountView, IssuedAccountInvite,
LocalAccountSession, NativeAuthClient,
};
use frameshift_client::session::{AuthenticatedSession, SessionClient, SessionClientConfig};
use frameshift_client::session_store::{
Expand Down Expand Up @@ -73,6 +73,15 @@ pub enum AccountCommand {
#[arg(long)]
display_name: Option<String>,
},
/// Fetch and print one public publisher profile without authentication.
ShowPublisher {
/// Registry API base URL.
#[arg(long)]
server: String,
/// Existing public publisher handle.
#[arg(long)]
handle: String,
},
/// Create a pending publisher profile owned by the authenticated account.
CreatePublisher {
/// Registry API base URL.
Expand Down Expand Up @@ -344,6 +353,7 @@ pub fn run_account(args: AccountArgs) -> Result<(), CliError> {
email,
display_name,
} => update_profile(&server, email.as_deref(), display_name.as_deref()),
AccountCommand::ShowPublisher { server, handle } => show_publisher(&server, &handle),
AccountCommand::CreatePublisher {
server,
handle,
Expand Down Expand Up @@ -406,6 +416,15 @@ fn update_profile(
Ok(())
}

/// Print one public publisher profile without resolving an account session.
fn show_publisher(server: &str, handle: &str) -> Result<(), CliError> {
validate_server_url(server)?;
let publisher = get_publisher_profile(server, handle)
.map_err(|error| CliError::Account(error.to_string()))?;
println!("{}", serde_json::to_string_pretty(&publisher)?);
Ok(())
}

/// Create one publisher profile through the current authenticated session.
fn create_publisher(
server: &str,
Expand Down Expand Up @@ -1233,6 +1252,29 @@ mod tests {
.is_err());
}

/// Public publisher lookup parsing requires only registry and handle metadata.
#[test]
fn parses_public_publisher_profile_lookup() {
let parsed = TestCli::try_parse_from([
"frameshift",
"account",
"show-publisher",
"--server",
"https://registry.example",
"--handle",
"gatekeeper",
])
.expect("publisher lookup arguments");
let TestCommand::Account(AccountArgs {
command: AccountCommand::ShowPublisher { server, handle },
}) = parsed.command
else {
panic!("expected publisher profile lookup");
};
assert_eq!(server, "https://registry.example");
assert_eq!(handle, "gatekeeper");
}

/// Publisher creation parsing preserves its public profile fields.
#[test]
fn parses_publisher_profile_creation() {
Expand Down
45 changes: 45 additions & 0 deletions crates/frameshift-client/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,38 @@ pub fn get_account(
}
}

/// Fetch one public publisher profile by handle.
///
/// This endpoint is intentionally unauthenticated because publisher profiles
/// are public registry metadata.
///
/// # Errors
///
/// Returns a registry URL, transport, status, size, or JSON error.
pub fn get_publisher_profile(
server_url: &str,
handle: &str,
) -> Result<PublisherProfileRecord, ClientError> {
let url = crate::publisher::registry_endpoint_url(server_url, &["v1", "publishers", handle])?;
let request = ureq::AgentBuilder::new()
.redirects(0)
.timeout(std::time::Duration::from_secs(15))
.build()
.get(url.as_str());
match request.call() {
Ok(response) => crate::registry::response_json_bounded(response, url.as_str()),
Err(ureq::Error::Status(status, response)) => Err(ClientError::RegistryRejected {
url: url.to_string(),
status,
message: crate::registry::response_text_bounded(response, url.as_str()),
}),
Err(error) => Err(ClientError::RegistryHttp {
url: url.to_string(),
detail: error.to_string(),
}),
}
}

/// Update mutable metadata for the authenticated account.
///
/// Omitted fields retain their current values. Text validation remains owned
Expand Down Expand Up @@ -856,6 +888,19 @@ mod tests {
assert_eq!(request.matches("test-access-token").count(), 1);
}

/// Public publisher lookup preserves base paths without adding authority.
#[test]
fn fetches_public_publisher_profile_without_authorization() {
let (server, handle) = serve_json_response(publisher_json());
let server = format!("{server}/registry");
let publisher =
get_publisher_profile(&server, "gatekeeper").expect("publisher profile response");
assert_eq!(publisher.handle, "gatekeeper");
let request = handle.join().expect("publisher request thread");
assert!(request.starts_with("GET /registry/v1/publishers/gatekeeper HTTP/1.1\r\n"));
assert!(!request.to_ascii_lowercase().contains("authorization:"));
}

/// Profile mutations preserve base paths, bearer authority, and exact JSON fields.
#[test]
fn sends_account_and_publisher_profile_mutations() {
Expand Down
8 changes: 8 additions & 0 deletions docs/wiki/Accounts-and-Publisher-Identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ A publisher profile is the durable owner shown with account-backed releases.
Its handle is the public name; its UUID is the stable identity used for
ownership checks even if display metadata changes.

Anyone can inspect the public profile without an account session:

```bash
frameshift account show-publisher \
--server https://frameshift-api.syntheos.dev \
--handle PUBLISHER_HANDLE
```

Publisher operations require an active account membership with the appropriate
role. Create the profile from the authenticated CLI session:

Expand Down
16 changes: 9 additions & 7 deletions docs/wiki/CLI-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,22 @@ interactive prompts. `login --first-party` selects password login when the
registry also advertises OIDC.

Use `update-profile` to replace account metadata. At least one of `--email` or
`--display-name` is required. Use `create-publisher` to create a pending public
profile and active owner membership, then use `update-publisher` to replace its
display metadata. Omit biography options to retain the current biography, or
use `--clear-biography` to remove it.
`--display-name` is required. `show-publisher` reads public profile metadata and
does not require a saved account session. Use `create-publisher` to create a
pending public profile and active owner membership, then use `update-publisher`
to replace its display metadata. Omit biography options to retain the current
biography, or use `--clear-biography` to remove it.

```bash
frameshift account update-profile --server <url> [--email <email>] [--display-name <name>]
frameshift account show-publisher --server <url> --handle <handle>
frameshift account create-publisher --server <url> --handle <handle> --display-name <name> [--biography <text>]
frameshift account update-publisher --server <url> --handle <handle> --display-name <name> [--biography <text> | --clear-biography]
```

These profile commands resolve the saved bearer authority for the exact
registry and do not accept a token argument. Publisher updates require an
active owner membership and a fresh authentication session.
The write commands resolve the saved bearer authority for the exact registry
and do not accept a token argument. Publisher updates require an active owner
membership and a fresh authentication session.

Active administrators can grant or revoke global platform roles and transition
account lifecycle states. These commands resolve bearer authority for the exact
Expand Down
Loading