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
7 changes: 2 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ Start with [`CONTRIBUTING.md`](CONTRIBUTING.md) for repository boundaries, devel
- Official TypeScript, Python, and Rust SDKs cover Runtime Project Auth only. Server API has a generated OpenAPI document but no official SDK and must never be imported into hosted-web code.
- Keep Auth and Control listeners independently configurable. Keep Runtime, Server API, and Control internal authority boundaries independently configurable while all three surfaces remain bound to one PostgreSQL server/database authority. The Runtime Hosted Authentication UI and Control Management Console are the only browser surfaces.
- PostgreSQL is the durable authority. Do not add browser credential persistence, file-store fallbacks, unreviewed secret delivery, or dependencies from CLI/SDKs into the server implementation.
- Edit Rust DTOs in `owlauth-types`; regenerate derived OpenAPI/hosted-web contracts rather than hand-editing generated files. Until the first deployed schema is declared, rebuild clean module baselines instead of preserving pre-release migration history; after deployment, freeze those baselines and add ordered migrations only.
- Edit Rust DTOs in `owlauth-types`; regenerate derived OpenAPI/hosted-web contracts rather than hand-editing generated files.
- The PostgreSQL schema has been deployed. Existing migration files are immutable compatibility artifacts: never edit, reorder, squash, or replace them. Every schema change must use a new ordered migration and must be tested both as a clean install and as an upgrade from the deployed migration set.

## Development quick start

Expand Down Expand Up @@ -76,7 +77,3 @@ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for changelog scopes, SemVer rules, sha
- [`scripts/`](scripts/) — repository, package, release, installer, SDK, and container checks
- [`dev/`](dev/README.md) — local PostgreSQL development service
- [`.github/workflows/`](.github/workflows/) — CI, documentation, image, and component release automation

## AnyCap

This project uses [AnyCap](https://anycap.ai) for multimodal capabilities, web search and crawling, file sharing, and static-page hosting. For these tasks, use the installed AnyCap skills and verify availability with `anycap status`. AnyCap evolves rapidly, so check or update its skills when commands or capabilities appear stale; submit `anycap feedback` for service or capability issues.
12 changes: 10 additions & 2 deletions crates/owlauth-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Every authenticated command repeats discovery validation before reading the refe

The self-hosted client supports typed commands for:

- Project list/get/create/disable, token/session policy get/set, and Project-user authoritative search/filter/sort/page, exact-email lookup, get/identity/session inspection, disable, and exact session revoke;
- Project list/get/create/disable/enable/permanent-delete, token/session policy get/set, and Project-user authoritative search/filter/sort/page, exact-email lookup, get/identity/session inspection, disable, and exact session revoke;
- Application list/get/create/disable and cursor-bounded immutable user-event history;
- provider Project-egress get/set, Custom OIDC preflight, and list/create/disable/assign/unassign for the closed `oidc`, `google`, and `github` kinds;
- signing-key list/rotate/revoke, with provisioning, publication, activation, and retirement handled by the automatic lifecycle;
Expand All @@ -58,6 +58,14 @@ owlauth --profile local project list
owlauth --profile local project create \
--display-name 'Example' \
--idempotency-key project_create_20260803
owlauth --profile local project enable \
11111111-1111-4111-8111-111111111111 \
--expected-security-revision 2 \
--yes
owlauth --profile local project delete \
11111111-1111-4111-8111-111111111111 \
--expected-security-revision 3 \
--yes
owlauth --profile local application list \
11111111-1111-4111-8111-111111111111
owlauth --profile local project user list \
Expand All @@ -81,7 +89,7 @@ owlauth --profile local server-key acknowledge \
--yes
```

All Control path identifiers must be canonical lowercase hyphenated UUIDs. Create commands require an explicit 8–128 character `--idempotency-key`; retain and reuse that key when reconciling an ambiguous transport outcome instead of submitting the same normalized create under a new key. `server-key create` leaves replacement creation blocked until the emitted credential is durably stored and the exact returned key revision is passed to `server-key acknowledge`, or the key is revoked. Acknowledgement is an explicit assertion about external secret-manager storage; successful stdout delivery alone does not make that assertion. Revision-fenced trust, visibility, activation, disable, retirement, revoke, assignment, unassignment, endpoint-test, and policy changes require explicit `--yes` where exposed. The CLI rejects the operation before authentication when confirmation is absent; when present, it prints a redacted preview containing the selected profile, pinned endpoint/instance, exact target, operation, and bounded effect before authenticating. Full-replacement booleans such as `--browser-session-reuse` require an explicit `true` or `false` value.
All Control path identifiers must be canonical lowercase hyphenated UUIDs. Create commands require an explicit 8–128 character `--idempotency-key`; retain and reuse that key when reconciling an ambiguous transport outcome instead of submitting the same normalized create under a new key. `server-key create` leaves replacement creation blocked until the emitted credential is durably stored and the exact returned key revision is passed to `server-key acknowledge`, or the key is revoked. Acknowledgement is an explicit assertion about external secret-manager storage; successful stdout delivery alone does not make that assertion. Revision-fenced trust, visibility, activation, disable, retirement, revoke, assignment, unassignment, endpoint-test, and policy changes require confirmation. Project disable, enable, and permanent delete require `--yes`. The CLI rejects an unconfirmed operation before authentication and prints a redacted preview containing the selected profile, pinned endpoint/instance, exact target, operation, and bounded effect before the mutation. Full-replacement booleans such as `--browser-session-reuse` require an explicit `true` or `false` value.

Provider client secrets and webhook signing secrets, including candidate rotation generations, are accepted only through named environment-variable references:

Expand Down
127 changes: 103 additions & 24 deletions crates/owlauth-cli/src/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,30 @@ enum ProjectCommand {
#[arg(long)]
idempotency_key: String,
},
/// Monotonically disable a Project.
/// Disable a Project until it is explicitly enabled again.
Disable {
project_id: String,
#[arg(long)]
expected_security_revision: i64,
#[arg(long)]
yes: bool,
},
/// Enable a disabled Project.
Enable {
project_id: String,
#[arg(long)]
expected_security_revision: i64,
#[arg(long)]
yes: bool,
},
/// Permanently delete a Project and all of its live authority data.
Delete {
project_id: String,
#[arg(long)]
expected_security_revision: i64,
#[arg(long)]
yes: bool,
},
/// Read or replace Project token/session policy.
Policy(ProjectPolicyArgs),
/// Inspect and administer Project users and their sessions.
Expand Down Expand Up @@ -707,6 +723,67 @@ enum WebhookDeliveryCommand {
},
}

fn run_project_transition(
profile: Option<&str>,
project_id: &str,
expected_security_revision: i64,
confirmed: bool,
action: &str,
effect: &str,
) -> Result<(), RemoteError> {
resource(project_id)?;
let target = format!("projects/{project_id}/{action}");
let stored = require_confirmation(
confirmed,
profile,
&format!("project.{action}"),
&target,
&serde_json::json!({
"effect": effect,
"expected_security_revision": expected_security_revision,
}),
)?;
let value: Project = authenticated_server_snapshot(stored)?.send(
Method::POST,
&target,
&ExpectedSecurityRevision {
expected_security_revision,
},
None,
)?;
print_json(&value)
}

fn run_project_delete(
profile: Option<&str>,
project_id: &str,
expected_security_revision: i64,
confirmed: bool,
) -> Result<(), RemoteError> {
resource(project_id)?;
let target = format!("projects/{project_id}");
let stored = require_confirmation(
confirmed,
profile,
"project.delete",
&target,
&serde_json::json!({
"effect": "permanently delete the Project after provider cleanup",
"expected_security_revision": expected_security_revision,
"recoverable": false,
}),
)?;
let value: Project = authenticated_server_snapshot(stored)?.send(
Method::DELETE,
&target,
&ExpectedSecurityRevision {
expected_security_revision,
},
None,
)?;
print_json(&value)
}

pub(crate) fn run_project(profile: Option<&str>, args: ProjectArgs) -> Result<(), RemoteError> {
match args.command {
ProjectCommand::List { belongs_to } => {
Expand Down Expand Up @@ -744,29 +821,31 @@ pub(crate) fn run_project(profile: Option<&str>, args: ProjectArgs) -> Result<()
project_id,
expected_security_revision,
yes,
} => {
resource(&project_id)?;
let target = format!("projects/{project_id}/disable");
let stored = require_confirmation(
yes,
profile,
"project.disable",
&target,
&serde_json::json!({
"effect": "monotonically disable the Project",
"expected_security_revision": expected_security_revision,
}),
)?;
let value: Project = authenticated_server_snapshot(stored)?.send(
Method::POST,
&target,
&ExpectedSecurityRevision {
expected_security_revision,
},
None,
)?;
print_json(&value)
}
} => run_project_transition(
profile,
&project_id,
expected_security_revision,
yes,
"disable",
"disable the Project until it is explicitly enabled",
),
ProjectCommand::Enable {
project_id,
expected_security_revision,
yes,
} => run_project_transition(
profile,
&project_id,
expected_security_revision,
yes,
"enable",
"enable the Project",
),
ProjectCommand::Delete {
project_id,
expected_security_revision,
yes,
} => run_project_delete(profile, &project_id, expected_security_revision, yes),
ProjectCommand::Policy(args) => run_project_policy(profile, args),
ProjectCommand::User(args) => run_project_user(profile, args),
}
Expand Down
22 changes: 22 additions & 0 deletions crates/owlauth-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,28 @@ mod tests {
);
}

#[test]
fn project_lifecycle_commands_require_security_revisions() {
const PROJECT: &str = "11111111-1111-4111-8111-111111111111";
for transition in ["disable", "enable", "delete"] {
assert!(
Cli::try_parse_from([
"owlauth",
"project",
transition,
PROJECT,
"--expected-security-revision",
"2",
"--yes",
])
.is_ok()
);
assert!(
Cli::try_parse_from(["owlauth", "project", transition, PROJECT, "--yes",]).is_err()
);
}
}

#[test]
fn project_user_directory_commands_parse_closed_arguments() {
const PROJECT: &str = "11111111-1111-4111-8111-111111111111";
Expand Down
Loading