use std::{
collections::BTreeSet,
fs,
net::{IpAddr, Ipv4Addr, SocketAddr},
path::Path,
process::{Command, Stdio},
time::Duration,
};
use axum::body::Bytes;
use base64::{Engine as _, engine::general_purpose};
use k256::schnorr::SigningKey;
use reqwest::{
Method, Response, StatusCode,
header::{AUTHORIZATION, CONTENT_TYPE, COOKIE, HOST, ORIGIN, SET_COOKIE},
};
use serde::Serialize;
use serde_json::{Value, json};
use sha2::{Digest as _, Sha256};
use time::OffsetDateTime;
use uuid::Uuid;
use zeroize::Zeroizing;
use super::process_harness::{CapturedChild, Daemon};
mod account_workflows;
mod agent_workflows;
mod profile_workflows;
use maincopy_shared::{
auth::AdminScope,
auth_api::{
ADMIN_AGENT_CREDENTIALS_PATH, ADMIN_AUDIT_EVENTS_PATH, ADMIN_SESSIONS_PATH,
ADMIN_USERS_PATH, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, CreateAdminSessionRequest,
SESSION_COOKIE_NAME, SecretString,
},
publication::IDEMPOTENCY_KEY_HEADER,
source::{SOURCE_PATH, SOURCE_SYNCS_PATH},
};
const ADMIN_ORIGIN: &str = "https://admin.example.test";
const ADMIN_AUTHORITY: &str = "admin.example.test";
const OWNER_USERNAME: &str = "first-owner";
const OWNER_PASSWORD: &str = "correct horse battery staple";
const NIP98_EVENT_KIND: u64 = 27_235;
const BOOTSTRAP_LIMIT: Duration = Duration::from_secs(20);
const SERVER_START_LIMIT: Duration = Duration::from_secs(20);
const REQUEST_LIMIT: Duration = Duration::from_secs(10);
const MAX_RESPONSE_BODY_BYTES: usize = 64 * 1024;
const READY_MESSAGE: &str = "authenticated admin backend listener bound";
const TEST_PUBLICATION: &str = "[site]\n\
title = \"Admin identity integration test\"\n\
base_url = \"https://publication.example.test\"\n\
description = \"A self-contained publication fixture.\"\n\
[author]\n\
name = \"Integration Tester\"\n";
#[test]
fn response_body_budget_accepts_the_exact_limit_and_rejects_the_next_byte() {
let mut body = Vec::new();
assert!(append_response_chunk(&mut body, &vec![0; MAX_RESPONSE_BODY_BYTES - 1]).is_ok());
assert!(append_response_chunk(&mut body, &[0]).is_ok());
assert_eq!(body.len(), MAX_RESPONSE_BODY_BYTES);
assert!(append_response_chunk(&mut body, &[0]).is_err());
assert_eq!(body.len(), MAX_RESPONSE_BODY_BYTES);
}
#[test]
fn admin_readiness_ignores_the_public_listener_and_rejects_unsafe_addresses() {
assert_eq!(
admin_address_from_ready_line("INFO public listener bound bind=127.0.0.1:1234"),
Ok(None)
);
assert_eq!(
admin_address_from_ready_line(
"INFO authenticated admin backend listener bound bind=127.0.0.1:4321"
),
Ok(Some("127.0.0.1:4321".parse().unwrap()))
);
assert!(
admin_address_from_ready_line(
"INFO authenticated admin backend listener bound bind=0.0.0.0:4321"
)
.is_err()
);
}
#[tokio::test]
async fn release_controls_require_the_release_management_agent_scope() {
let harness = AdminProcessHarness::start().await;
let operator = SigningKey::from_bytes(&[4_u8; 32]).unwrap();
let observer = SigningKey::from_bytes(&[5_u8; 32]).unwrap();
for (key, scope) in [
(&operator, AdminScope::ReleaseManage),
(&observer, AdminScope::ContentRead),
] {
let registered = harness.send_json(Method::POST, ADMIN_AGENT_CREDENTIALS_PATH, json!({
"owner_user_id":harness.owner_user_id, "public_key":public_key(key), "label":"release scope test", "scopes":[scope], "expires_at":null
})).await;
assert_eq!(registered.status(), StatusCode::CREATED);
}
let path = "/api/admin/v1/releases/11111111-1111-4111-8111-111111111111";
for (key, expected_status, expected_code) in [
(&operator, StatusCode::NOT_FOUND, "release_not_found"),
(&observer, StatusCode::FORBIDDEN, "insufficient_scope"),
] {
let response = harness
.send_json_as(
key,
Method::POST,
path,
json!({"action":"cancel", "expected_version":1}),
)
.await;
assert_eq!(response.status(), expected_status);
assert_problem(response, expected_code).await;
}
harness.stop();
}
#[tokio::test]
async fn identity_mutations_persist_a_complete_user_and_agent_lifecycle() {
let harness = AdminProcessHarness::start().await;
let users = harness.get(ADMIN_USERS_PATH).await;
assert_eq!(users.status(), StatusCode::OK);
let users = response_json(users).await;
assert_eq!(users["users"][0]["user_id"], harness.owner_user_id);
let first_human_key = test_public_key(6);
let created = harness
.send_json(
Method::POST,
ADMIN_USERS_PATH,
json!({
"status": "disabled",
"roles": ["publisher"],
"credentials": [{
"provider": "nostr",
"public_key": first_human_key,
}],
}),
)
.await;
assert_eq!(created.status(), StatusCode::CREATED);
let created = response_json(created).await;
let user_id = created["user_id"].as_str().unwrap().to_owned();
assert_eq!(created["version"], 1);
let roles = harness
.send_json(
Method::PUT,
&format!("{ADMIN_USERS_PATH}/{user_id}/roles"),
json!({
"expected_version": 1,
"roles": ["administrator"],
}),
)
.await;
assert_eq!(roles.status(), StatusCode::OK);
assert_eq!(response_json(roles).await["version"], 2);
let replacement_human_key = test_public_key(7);
let credential = harness
.send_json(
Method::PUT,
&format!("{ADMIN_USERS_PATH}/{user_id}/credentials/nostr"),
json!({
"mode": "replace",
"expected_version": 1,
"credential": {
"provider": "nostr",
"public_key": replacement_human_key,
},
}),
)
.await;
assert_eq!(credential.status(), StatusCode::OK);
assert_eq!(response_json(credential).await["version"], 3);
let removed = harness
.send_json(
Method::DELETE,
&format!("{ADMIN_USERS_PATH}/{user_id}/credentials/nostr"),
json!({ "expected_version": 2 }),
)
.await;
assert_eq!(removed.status(), StatusCode::OK);
assert_eq!(response_json(removed).await["version"], 4);
let registered = harness
.send_json(
Method::POST,
ADMIN_AGENT_CREDENTIALS_PATH,
json!({
"owner_user_id": harness.owner_user_id,
"public_key": test_public_key(8),
"label": "release helper",
"scopes": ["content_read", "preview_read"],
"expires_at": null,
}),
)
.await;
let registered_status = registered.status();
let registered = response_json(registered).await;
assert_eq!(
registered_status,
StatusCode::CREATED,
"unexpected registration response: {registered}"
);
let agent_id = registered["agent_credential_id"]
.as_str()
.unwrap()
.to_owned();
assert_eq!(registered["version"], 1);
let scopes = harness
.send_json(
Method::PUT,
&format!("{ADMIN_AGENT_CREDENTIALS_PATH}/{agent_id}/scopes"),
json!({
"expected_version": 1,
"scopes": ["content_read"],
}),
)
.await;
assert_eq!(scopes.status(), StatusCode::OK);
assert_eq!(response_json(scopes).await["version"], 2);
let revoked = harness
.send_json(
Method::DELETE,
&format!("{ADMIN_AGENT_CREDENTIALS_PATH}/{agent_id}"),
json!({ "expected_version": 2 }),
)
.await;
assert_eq!(revoked.status(), StatusCode::OK);
assert_eq!(response_json(revoked).await["version"], 3);
let user = harness.get(&format!("{ADMIN_USERS_PATH}/{user_id}")).await;
assert_eq!(user.status(), StatusCode::OK);
let user = response_json(user).await;
assert_eq!(user["status"], "disabled");
assert_eq!(user["roles"], json!(["administrator"]));
assert_eq!(user["credentials"], json!([]));
assert_eq!(user["version"], 4);
let agent = harness
.get(&format!("{ADMIN_AGENT_CREDENTIALS_PATH}/{agent_id}"))
.await;
assert_eq!(agent.status(), StatusCode::OK);
let agent = response_json(agent).await;
assert_eq!(agent["scopes"], json!(["content_read"]));
assert_eq!(agent["effective_scopes"], json!(["content_read"]));
assert!(agent["revoked_at"].is_string());
assert_eq!(agent["version"], 3);
let audit = harness.get(ADMIN_AUDIT_EVENTS_PATH).await;
assert_eq!(audit.status(), StatusCode::OK);
let audit = response_json(audit).await;
let successful_actions = audit["audit_events"]
.as_array()
.unwrap()
.iter()
.filter(|event| event["outcome"] == "succeeded")
.filter_map(|event| event["action"].as_str())
.collect::<BTreeSet<_>>();
for action in [
"identity.user.create",
"identity.user.roles.replace",
"identity.user.credential.put",
"identity.user.credential.remove",
"identity.agent.register",
"identity.agent.scopes.replace",
"identity.agent.revoke",
] {
assert!(
successful_actions.contains(action),
"missing successful audit event for {action}"
);
}
harness.stop();
}
#[tokio::test]
async fn identity_mutations_reject_ambiguous_paths_and_unsafe_inputs() {
let harness = AdminProcessHarness::start().await;
for body in [
json!({
"status": "disabled",
"roles": [],
"credentials": [],
}),
json!({
"status": "disabled",
"roles": ["publisher"],
"credentials": [
{ "provider": "nostr", "public_key": test_public_key(9) },
{ "provider": "nostr", "public_key": test_public_key(10) },
],
}),
json!({
"status": "disabled",
"roles": ["publisher"],
"credentials": [
{ "provider": "nostr", "public_key": test_public_key(11) },
{ "provider": "nostr", "public_key": test_public_key(12) },
{ "provider": "nostr", "public_key": test_public_key(13) },
],
}),
] {
let response = harness
.send_json(Method::POST, ADMIN_USERS_PATH, body)
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
assert_problem(response, "invalid_identity_request").await;
}
let password = harness
.send_json(
Method::POST,
ADMIN_USERS_PATH,
json!({
"status": "enabled",
"roles": ["publisher"],
"credentials": [{
"provider": "password",
"username": "writer",
"password": "correct horse battery staple",
}],
}),
)
.await;
assert_eq!(password.status(), StatusCode::FORBIDDEN);
assert_problem(password, "fresh_authentication_required").await;
let noncanonical_user_id = Uuid::new_v4().hyphenated().to_string().to_uppercase();
let invalid_user = harness
.send_json(
Method::PUT,
&format!("{ADMIN_USERS_PATH}/{noncanonical_user_id}/credentials/nostr"),
json!({
"mode": "create",
"credential": {
"provider": "nostr",
"public_key": test_public_key(14),
},
}),
)
.await;
assert_eq!(invalid_user.status(), StatusCode::BAD_REQUEST);
assert_problem(invalid_user, "invalid_identity_identifier").await;
let invalid_provider = harness
.send_json(
Method::DELETE,
&format!("{ADMIN_USERS_PATH}/{}/credentials/email", Uuid::new_v4()),
json!({ "expected_version": 1 }),
)
.await;
assert_eq!(invalid_provider.status(), StatusCode::BAD_REQUEST);
assert_problem(invalid_provider, "invalid_identity_identifier").await;
let invalid_key = harness
.send_json(
Method::POST,
ADMIN_AGENT_CREDENTIALS_PATH,
json!({
"owner_user_id": harness.owner_user_id,
"public_key": "not-a-public-key",
"label": "invalid",
"scopes": ["content_read"],
"expires_at": null,
}),
)
.await;
assert_eq!(invalid_key.status(), StatusCode::BAD_REQUEST);
assert_problem(invalid_key, "invalid_identity_request").await;
let users = harness.get(ADMIN_USERS_PATH).await;
assert_eq!(users.status(), StatusCode::OK);
assert_eq!(
response_json(users).await["users"]
.as_array()
.unwrap()
.len(),
1
);
let agents = harness.get(ADMIN_AGENT_CREDENTIALS_PATH).await;
assert_eq!(agents.status(), StatusCode::OK);
assert_eq!(
response_json(agents).await["agent_credentials"]
.as_array()
.unwrap()
.len(),
1
);
harness.stop();
}
#[tokio::test]
async fn managed_source_endpoints_enforce_their_agent_scopes() {
let harness = AdminProcessHarness::start().await;
let source_operator =
SigningKey::from_bytes(&[4_u8; 32]).expect("the fixed source operator key must be valid");
let source_observer =
SigningKey::from_bytes(&[5_u8; 32]).expect("the fixed source observer key must be valid");
for (signing_key, label, scopes) in [
(
&source_operator,
"source sync contract test agent",
vec![AdminScope::SourceSync],
),
(
&source_observer,
"source status contract test agent",
vec![AdminScope::StatusRead],
),
] {
let registered = harness
.send_json(
Method::POST,
ADMIN_AGENT_CREDENTIALS_PATH,
json!({
"owner_user_id": harness.owner_user_id,
"public_key": public_key(signing_key),
"label": label,
"scopes": scopes,
"expires_at": null,
}),
)
.await;
assert_eq!(registered.status(), StatusCode::CREATED);
}
let status = harness
.send_as(
&source_observer,
Method::GET,
SOURCE_PATH,
Vec::new(),
false,
)
.await;
assert_eq!(status.status(), StatusCode::OK);
for path in [
"/api/admin/v1/source/configuration",
"/api/admin/v1/source/deploy-key",
] {
let method = if path.ends_with("configuration") {
Method::PUT
} else {
Method::GET
};
let denied = harness
.send_as(&source_operator, method, path, b"{}".to_vec(), true)
.await;
assert_eq!(denied.status(), StatusCode::FORBIDDEN);
assert_problem(denied, "insufficient_scope").await;
}
let sync = harness
.send_json_as(&source_operator, Method::POST, SOURCE_SYNCS_PATH, json!({}))
.await;
assert_eq!(sync.status(), StatusCode::CONFLICT);
assert_problem(sync, "source_sync_unsupported").await;
for (signing_key, method, path, body, json_body) in [
(
&source_operator,
Method::GET,
SOURCE_PATH,
Vec::new(),
false,
),
(
&source_observer,
Method::POST,
SOURCE_SYNCS_PATH,
b"{}".to_vec(),
true,
),
] {
let denied = harness
.send_as(signing_key, method, path, body, json_body)
.await;
assert_eq!(denied.status(), StatusCode::FORBIDDEN, "{path}");
assert_problem(denied, "insufficient_scope").await;
}
harness.stop();
}
struct AdminProcessHarness {
daemon: Daemon,
_root: tempfile::TempDir,
client: reqwest::Client,
admin_url: String,
owner_user_id: String,
signing_key: SigningKey,
}
impl AdminProcessHarness {
async fn start() -> Self {
let root = tempfile::tempdir().expect("admin process root must be created");
write_host_file(root.path());
bootstrap_password_owner(root.path());
let (daemon, admin_address) = start_admin_daemon(root.path());
let client = reqwest::Client::builder()
.no_proxy()
.redirect(reqwest::redirect::Policy::none())
.timeout(REQUEST_LIMIT)
.build()
.expect("admin integration client must build");
let admin_url = format!("http://{admin_address}");
let (owner_user_id, session) = password_login(&client, &admin_url).await;
let signing_key = SigningKey::from_bytes(&[3_u8; 32])
.expect("the fixed agent integration key must be valid");
register_test_agent(&client, &admin_url, &owner_user_id, &session, &signing_key).await;
Self {
daemon,
_root: root,
client,
admin_url,
owner_user_id,
signing_key,
}
}
async fn get(&self, path: &str) -> Response {
self.send_as(&self.signing_key, Method::GET, path, Vec::new(), false)
.await
}
async fn send_json(&self, method: Method, path: &str, body: Value) -> Response {
self.send_json_as(&self.signing_key, method, path, body)
.await
}
async fn send_json_as(
&self,
signing_key: &SigningKey,
method: Method,
path: &str,
body: Value,
) -> Response {
self.send_as(
signing_key,
method,
path,
serde_json::to_vec(&body).expect("admin request fixture must serialize"),
true,
)
.await
}
async fn send_as(
&self,
signing_key: &SigningKey,
method: Method,
path: &str,
body: Vec<u8>,
json_body: bool,
) -> Response {
let idempotency_key = Uuid::new_v4().hyphenated().to_string();
let authorization =
agent_authorization(signing_key, &method, path, &body, &idempotency_key);
let mut request = self
.client
.request(method, format!("{}{path}", self.admin_url))
.header(HOST, ADMIN_AUTHORITY)
.header(ORIGIN, ADMIN_ORIGIN)
.header(AUTHORIZATION, authorization)
.header(IDEMPOTENCY_KEY_HEADER, idempotency_key)
.body(body);
if json_body {
request = request.header(CONTENT_TYPE, "application/json");
}
request
.send()
.await
.expect("admin integration request must complete")
}
fn stop(self) {
self.daemon.stop();
}
}
struct HumanSession {
cookie: Zeroizing<String>,
csrf: Zeroizing<String>,
}
async fn password_login(client: &reqwest::Client, admin_url: &str) -> (String, HumanSession) {
let login = CreateAdminSessionRequest::Password {
username: OWNER_USERNAME.into(),
password: SecretString::new(OWNER_PASSWORD),
};
let body = Bytes::from_owner(Zeroizing::new(
serde_json::to_vec(&login).expect("password login fixture must serialize"),
));
drop(login);
let request = client
.post(format!("{admin_url}{ADMIN_SESSIONS_PATH}"))
.header(HOST, ADMIN_AUTHORITY)
.header(ORIGIN, ADMIN_ORIGIN)
.header(CONTENT_TYPE, "application/json")
.body(body)
.build()
.expect("password login request must build");
let response = client
.execute(request)
.await
.expect("password login request must complete");
let status = response.status();
let session = session_cookies(response.headers());
let body = response_json(response).await;
assert_eq!(status, StatusCode::CREATED, "password login failed: {body}");
let owner_user_id = body["user_id"]
.as_str()
.expect("password login must identify the owner")
.to_owned();
(owner_user_id, session)
}
async fn register_test_agent(
client: &reqwest::Client,
admin_url: &str,
owner_user_id: &str,
session: &HumanSession,
signing_key: &SigningKey,
) {
let body = serde_json::to_vec(&json!({
"owner_user_id": owner_user_id,
"public_key": public_key(signing_key),
"label": "protected process contract test agent",
"scopes": AdminScope::ALL,
"expires_at": null,
}))
.expect("agent registration fixture must serialize");
let idempotency_key = Uuid::new_v4().hyphenated().to_string();
let cookie = Zeroizing::new(format!(
"{SESSION_COOKIE_NAME}={}; {CSRF_COOKIE_NAME}={}",
session.cookie.as_str(),
session.csrf.as_str()
));
let response = client
.post(format!("{admin_url}{ADMIN_AGENT_CREDENTIALS_PATH}"))
.header(HOST, ADMIN_AUTHORITY)
.header(ORIGIN, ADMIN_ORIGIN)
.header(CONTENT_TYPE, "application/json")
.header(COOKIE, cookie.as_str())
.header(CSRF_HEADER_NAME, session.csrf.as_str())
.header(IDEMPOTENCY_KEY_HEADER, idempotency_key)
.body(body)
.send()
.await
.expect("agent registration request must complete");
let status = response.status();
let body = response_json(response).await;
assert_eq!(
status,
StatusCode::CREATED,
"agent registration failed: {body}"
);
}
fn session_cookies(headers: &reqwest::header::HeaderMap) -> HumanSession {
let mut session = None;
let mut csrf = None;
for value in headers.get_all(SET_COOKIE) {
let pair = value
.to_str()
.expect("session cookie must be visible ASCII")
.split(';')
.next()
.expect("session cookie must contain a name and value");
let (name, value) = pair
.split_once('=')
.expect("session cookie must contain a separator");
match name {
SESSION_COOKIE_NAME => session = Some(value.to_owned()),
CSRF_COOKIE_NAME => csrf = Some(value.to_owned()),
_ => {}
}
}
HumanSession {
cookie: Zeroizing::new(session.expect("password login must set the session cookie")),
csrf: Zeroizing::new(csrf.expect("password login must set the CSRF cookie")),
}
}
fn write_host_file(root: &Path) {
let content_root = root.join("content");
fs::create_dir(&content_root).expect("admin integration content directory must be created");
fs::write(content_root.join("publication.toml"), TEST_PUBLICATION)
.expect("admin integration publication fixture must be written");
fs::write(
root.join("maincopy.toml"),
format!(
"[paths]\n\
content_root = \"content\"\n\
state_root = \"state\"\n\
runtime_root = \"run\"\n\
[public]\n\
bind = \"127.0.0.1:0\"\n\
[metrics]\n\
bind = \"127.0.0.1:0\"\n\
[admin]\n\
bind = \"127.0.0.1:0\"\n\
origin = \"{ADMIN_ORIGIN}\"\n"
),
)
.expect("admin integration host file must be written");
}
fn bootstrap_password_owner(root: &Path) {
let child = Command::new(env!("CARGO_BIN_EXE_maincopyd"))
.args([
"--config",
"maincopy.toml",
"identity",
"bootstrap",
"password",
"--username",
OWNER_USERNAME,
])
.current_dir(root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("identity bootstrap process must start");
let mut process = CapturedChild::new(child);
let password = Zeroizing::new(format!("{OWNER_PASSWORD}\n"));
process
.write_stdin(password.as_bytes())
.expect("identity bootstrap password must be written");
let (completion, stdout, stderr) = process.wait(BOOTSTRAP_LIMIT);
let diagnostic = captured_process_diagnostic(&stdout, &stderr);
assert!(
completion.wait_error.is_none(),
"identity bootstrap wait failed: {}: {diagnostic}",
completion
.wait_error
.as_deref()
.unwrap_or("unknown wait failure")
);
assert!(
!completion.timed_out,
"identity bootstrap exceeded {BOOTSTRAP_LIMIT:?}: {diagnostic}"
);
assert!(
completion.termination_error.is_none(),
"identity bootstrap could not be killed after its timeout: {}: {diagnostic}",
completion
.termination_error
.as_deref()
.unwrap_or("unknown termination failure")
);
assert!(
completion
.status
.as_ref()
.unwrap_or_else(|error| panic!("identity bootstrap could not be reaped: {error}"))
.success(),
"identity bootstrap failed: {diagnostic}"
);
}
fn captured_process_diagnostic(stdout: &[u8], stderr: &[u8]) -> String {
format!(
"stdout: {}; stderr: {}",
redacted_output(stdout),
redacted_output(stderr)
)
}
fn redacted_output(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).replace(OWNER_PASSWORD, "<redacted>")
}
fn start_admin_daemon(root: &Path) -> (Daemon, SocketAddr) {
let mut command = Command::new(env!("CARGO_BIN_EXE_maincopyd"));
command
.args(["--config", "maincopy.toml"])
.current_dir(root);
Daemon::start_with_readiness(
command,
SERVER_START_LIMIT,
admin_address_from_ready_line,
redacted_output,
)
}
fn admin_address_from_ready_line(line: &str) -> Result<Option<SocketAddr>, &'static str> {
if !line.contains(READY_MESSAGE) {
return Ok(None);
}
let encoded = line
.split_whitespace()
.find_map(|field| field.strip_prefix("bind="))
.ok_or("admin readiness log omitted its bound address")?;
let address = encoded
.parse::<SocketAddr>()
.map_err(|_| "admin readiness log contained an invalid bound address")?;
if address.ip() != IpAddr::V4(Ipv4Addr::LOCALHOST) || address.port() == 0 {
return Err("admin readiness log contained an unsafe bound address");
}
Ok(Some(address))
}
async fn assert_problem(response: Response, expected_code: &str) {
let request_id = response.headers()["x-request-id"]
.to_str()
.expect("problem request ID must be visible ASCII")
.to_owned();
let body = response_json(response).await;
assert_eq!(body["error"]["code"], expected_code);
assert_eq!(body["error"]["request_id"], request_id);
}
async fn response_json(mut response: Response) -> Value {
if response
.content_length()
.is_some_and(|length| length > MAX_RESPONSE_BODY_BYTES as u64)
{
panic!(
"admin integration response body declared more than {MAX_RESPONSE_BODY_BYTES} bytes"
);
}
let mut body = Vec::new();
while let Some(chunk) = response
.chunk()
.await
.expect("admin integration response body must be readable")
{
append_response_chunk(&mut body, &chunk).unwrap_or_else(|_| {
panic!("admin integration response body exceeded {MAX_RESPONSE_BODY_BYTES} bytes")
});
}
serde_json::from_slice(&body).expect("admin integration response must be valid JSON")
}
struct ResponseBodyTooLarge;
fn append_response_chunk(body: &mut Vec<u8>, chunk: &[u8]) -> Result<(), ResponseBodyTooLarge> {
let received = body
.len()
.checked_add(chunk.len())
.ok_or(ResponseBodyTooLarge)?;
if received > MAX_RESPONSE_BODY_BYTES {
return Err(ResponseBodyTooLarge);
}
body.extend_from_slice(chunk);
Ok(())
}
fn test_public_key(seed: u8) -> String {
let signing_key =
SigningKey::from_bytes(&[seed; 32]).expect("the fixed test signing key must be valid");
public_key(&signing_key)
}
fn public_key(signing_key: &SigningKey) -> String {
lower_hex(&signing_key.verifying_key().to_bytes())
}
fn lower_hex(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
#[derive(Serialize)]
struct SignedEvent {
id: String,
pubkey: String,
created_at: i64,
kind: u64,
tags: Vec<Vec<String>>,
content: String,
sig: String,
}
fn agent_authorization(
signing_key: &SigningKey,
method: &Method,
path: &str,
body: &[u8],
idempotency_key: &str,
) -> String {
let created_at = OffsetDateTime::now_utc().unix_timestamp();
let tags = vec![
vec!["u".into(), format!("{ADMIN_ORIGIN}{path}")],
vec!["method".into(), method.as_str().into()],
vec!["payload".into(), lower_hex(&Sha256::digest(body))],
vec!["idempotency".into(), idempotency_key.into()],
];
let pubkey = public_key(signing_key);
let content = String::new();
let encoded = serde_json::to_vec(&(0, &pubkey, created_at, NIP98_EVENT_KIND, &tags, &content))
.expect("the test NIP-01 event must serialize");
let event_id: [u8; 32] = Sha256::digest(encoded).into();
let signature = signing_key
.sign_raw(&event_id, &[7_u8; 32])
.expect("the fixed test key must sign")
.to_bytes();
let event = SignedEvent {
id: lower_hex(&event_id),
pubkey,
created_at,
kind: NIP98_EVENT_KIND,
tags,
content,
sig: lower_hex(&signature),
};
format!(
"Nostr {}",
general_purpose::STANDARD
.encode(serde_json::to_vec(&event).expect("the signed test event must serialize"))
)
}