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 docs/api_direct.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,8 @@ Create a group (with optional permitted actions and PKPs). Then add an action (I
-H "Content-Type: application/json" \
-H "X-Api-Key: KEY" \
-d '{"group_name":"My Group","group_description":"","pkp_ids_permitted":[],"cid_hashes_permitted":[]}'

# List groups to get group_id (use the "id" from the response)
curl -s "http://localhost:8000/core/v1/list_groups?page_number=0&page_size=10" \
-H "X-Api-Key: KEY"

# Response: {"success":true,"group_id":"1"}

# Register action metadata (replace IPFS_CID)
curl -s -X POST "http://localhost:8000/core/v1/add_action" \
-H "Content-Type: application/json" \
Expand Down
7 changes: 2 additions & 5 deletions docs/management/api_direct.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,9 @@ Create a group, then add an action (IPFS CID) to scope which keys can run it.
"pkp_ids_permitted": [],
"cid_hashes_permitted": []
}'
# Response: {"success":true,"group_id":"1"}

# List groups to get group_id
curl -s "http://localhost:8000/core/v1/list_groups?page_number=0&page_size=10" \
-H "X-Api-Key: KEY"

# Add action to group (replace GROUP_ID and IPFS_CID)
# Add action to group (use group_id from the response above)
curl -s -X POST "http://localhost:8000/core/v1/add_action_to_group" \
-H "Content-Type: application/json" \
-H "X-Api-Key: KEY" \
Expand Down
38 changes: 8 additions & 30 deletions k6/correctness/api-key-security.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,23 +125,15 @@ export function setup(): SecuritySetupData {
adminA,
);
if (!assertOk("setup/addGroupX", "POST /add_group", addGroupXRes)) throw new Error("setup failed: addGroupX");
const groupIdX = parseInt((addGroupXRes.data as { group_id: string }).group_id);

// Create group Y
const addGroupYRes = client.addGroup(
{ group_name: `k6-sec-groupY-${K6_RUN_ID}`, group_description: "Security test group Y", pkp_ids_permitted: [], cid_hashes_permitted: [] },
adminA,
);
if (!assertOk("setup/addGroupY", "POST /add_group", addGroupYRes)) throw new Error("setup failed: addGroupY");

// List groups to extract IDs
const listGroupsRes = client.listGroups({ page_number: 0, page_size: 100 }, adminA);
if (!assertOk("setup/listGroups", "GET /list_groups", listGroupsRes)) throw new Error("setup failed: listGroups");
const groups = listGroupsRes.data as Array<{ id: string; name: string }>;
const groupX = groups.find((g) => g.name === `k6-sec-groupX-${K6_RUN_ID}`);
const groupY = groups.find((g) => g.name === `k6-sec-groupY-${K6_RUN_ID}`);
if (!groupX || !groupY) throw new Error("setup failed: could not find created groups");
const groupIdX = parseInt(groupX.id);
const groupIdY = parseInt(groupY.id);
const groupIdY = parseInt((addGroupYRes.data as { group_id: string }).group_id);

// Get IPFS CID for hello-world action
const ipfsRes = client.getLitActionIpfsId(HELLO_WORLD_CODE);
Expand Down Expand Up @@ -209,13 +201,7 @@ export function setup(): SecuritySetupData {
adminB,
);
if (!assertOk("setup/addGroupB", "POST /add_group", addGroupBRes)) throw new Error("setup failed: addGroupB");

const listGroupsBRes = client.listGroups({ page_number: 0, page_size: 100 }, adminB);
if (!assertOk("setup/listGroupsB", "GET /list_groups", listGroupsBRes)) throw new Error("setup failed: listGroupsB");
const groupsB = listGroupsBRes.data as Array<{ id: string; name: string }>;
const groupB = groupsB.find((g) => g.name === `k6-sec-groupB-${K6_RUN_ID}`);
if (!groupB) throw new Error("setup failed: could not find Account B group");
const groupIdB = parseInt(groupB.id);
const groupIdB = parseInt((addGroupBRes.data as { group_id: string }).group_id);

// Add action to Account B's group
const addActionBRes = client.addAction(
Expand Down Expand Up @@ -281,13 +267,9 @@ export default function (data: SecuritySetupData) {
assertOk("2-createGroup-positive", "POST /add_group", posRes);

// Clean up: delete the created group with admin key
const listRes = client.listGroups({ page_number: 0, page_size: 100 }, adminA);
if (assertOk("2-listGroups-cleanup", "GET /list_groups", listRes)) {
const gs = listRes.data as Array<{ id: string; name: string }>;
const created = gs.find((g) => g.name === `k6-sec-t2-pos-${K6_RUN_ID}`);
if (created) {
client.removeGroup({ group_id: created.id }, adminA);
}
const createdGroupId = (posRes.data as { group_id: string }).group_id;
if (createdGroupId) {
client.removeGroup({ group_id: createdGroupId }, adminA);
}

// Negative: key with can_create_groups=false
Expand All @@ -306,15 +288,11 @@ export default function (data: SecuritySetupData) {
adminA,
);
assertOk("3-createTempGroup", "POST /add_group", tempRes);
const listRes = client.listGroups({ page_number: 0, page_size: 100 }, adminA);
assertOk("3-listGroups", "GET /list_groups", listRes);
const gs = listRes.data as Array<{ id: string; name: string }>;
const temp = gs.find((g) => g.name === `k6-sec-t3-temp-${K6_RUN_ID}`);
if (!temp) { console.error("3-deleteGroup: temp group not found"); return; }
const tempGroupId = (tempRes.data as { group_id: string }).group_id;

// Positive: key with can_delete_groups=true
const posRes = client.removeGroup(
{ group_id: temp.id },
{ group_id: tempGroupId },
authHeaders(data.usageKey_deleteGroups),
);
assertOk("3-deleteGroup-positive", "POST /remove_group", posRes);
Expand Down
12 changes: 4 additions & 8 deletions k6/correctness/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,16 @@ export default function (data: IntegrationSetupData) {
checkAndLog(addGroupRes.response, {
"addGroup success": (r) => {
try {
return JSON.parse(r.body as string).success === true;
const body = JSON.parse(r.body as string);
return body.success === true && typeof body.group_id === "string";
} catch {
return false;
}
},
}, "addGroup");
const groupId = (addGroupRes.data as { success: boolean; group_id: string }).group_id;

// ── 7. listGroups — extract groupId for subsequent tests ──────────────────
// ── 7. listGroups — verify group appears ──────────────────────────────────
const listGroupsRes = client.listGroups(
{ page_number: 0, page_size: 10 },
authHeaders,
Expand All @@ -145,12 +147,6 @@ export default function (data: IntegrationSetupData) {
}
},
}, "listGroups");
const groups = listGroupsRes.data as Array<{ id: string; name: string; description: string }>;
if (!groups || groups.length === 0) {
console.error("listGroups returned empty array after addGroup");
return;
}
const groupId = groups[groups.length - 1].id; // use the most recently created group

// ── 8. updateGroup — called while group is empty so the full-replace is safe ───
const updateGroupRes = client.updateGroup(
Expand Down
12 changes: 10 additions & 2 deletions k6/litApiServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,20 @@ export interface LitActionRequest {
}

/**
* Response for account config operations (add_group, add_pkp_to_group, remove_pkp_from_group, add_usage_api_key, remove_usage_api_key).
* Response for account config operations (add_pkp_to_group, remove_pkp_from_group, add_usage_api_key, remove_usage_api_key).
*/
export interface AccountOpResponse {
success: boolean;
}

/**
* Response for add_group, includes the on-chain group ID.
*/
export interface AddGroupResponse {
success: boolean;
group_id: string;
}

/**
* Request for add_group. permitted_actions and pkps are keccak256 hashes as hex strings (with or without 0x). API key via header.
*/
Expand Down Expand Up @@ -398,7 +406,7 @@ export type AddGroupHeaders = {
"X-Api-Key": string;
};

export type AddGroupDefault = AccountOpResponse | ErrMessage;
export type AddGroupDefault = AddGroupResponse | ErrMessage;

export type RemoveGroupHeaders = {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ contract WritesFacet {
string memory description,
uint256[] memory cidHashes,
address[] memory pkpIds
) public {
) public returns (uint256) {
SecurityLib.revertIfNoAccountAccess(accountApiKeyHash, msg.sender);
SecurityLib.revertIfNotMasterAccount(accountApiKeyHash);
AppStorage.AccountConfigStorage storage s = AppStorage.getStorage();
Expand All @@ -124,6 +124,7 @@ contract WritesFacet {
for (uint256 i = 0; i < pkpIds.length; i++) {
group.pkpId.add(pkpIds[i]);
}
return account.groupCount;
}

function updateGroup(
Expand Down
8 changes: 7 additions & 1 deletion lit-api-server/src/accounts/contracts/AccountConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,13 @@
}
],
"name": "addGroup",
"outputs": [],
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2230,7 +2230,7 @@ pub mod account_config {
description: ::std::string::String,
cid_hashes: ::std::vec::Vec<::ethers::core::types::U256>,
pkp_ids: ::std::vec::Vec<::ethers::core::types::Address>,
) -> ::ethers::contract::builders::ContractCall<M, ()> {
) -> ::ethers::contract::builders::ContractCall<M, ::ethers::core::types::U256> {
self.0
.method_hash(
[43, 18, 222, 230],
Expand Down
23 changes: 21 additions & 2 deletions lit-api-server/src/accounts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,37 @@ pub async fn add_group(
description: &str,
cid_hashes: Vec<U256>,
pkp_ids: Vec<Address>,
) -> Result<bool> {
) -> Result<U256> {
let (contract, signer_address, client) =
get_signable_account_config_contract(signer_pool.clone()).await?;
let account_api_key_hash = api_key_hash(api_key);

// Simulate the call first to obtain the returned group ID.
let sim_call = contract.add_group(
account_api_key_hash,
name.to_string(),
description.to_string(),
cid_hashes.clone(),
pkp_ids.clone(),
);
let group_id = match sim_call.call().await {
Ok(id) => id,
Err(e) => {
// Release the signer back to the pool before propagating.
signer_pool.release(signer_address).await?;
return Err(e.into());
}
};

let function_call = contract.add_group(
account_api_key_hash,
name.to_string(),
description.to_string(),
cid_hashes,
pkp_ids,
);
send_transaction(function_call, signer_pool, signer_address, client).await
send_transaction(function_call, signer_pool, signer_address, client).await?;
Ok(group_id)
}

/// Create a new action entry with name, description, and IPFS CID hash in the account's actionMetadata mapping.
Expand Down
4 changes: 2 additions & 2 deletions lit-api-server/src/accounts/signable_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ pub(crate) async fn get_read_only_account_config_contract()
Ok(contract)
}

pub async fn send_transaction(
mut function_call: ContractCall<SigningClient, ()>,
pub async fn send_transaction<T: ethers::abi::Detokenize>(
mut function_call: ContractCall<SigningClient, T>,
signer_pool: Arc<SignerPool>,
signer_address: H160,
client: Arc<SigningClient>,
Expand Down
15 changes: 9 additions & 6 deletions lit-api-server/src/core/account_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ use crate::core::v1::models::request::{
UpdateUsageApiKeyRequest,
};
use crate::core::v1::models::response::{
AccountOpResponse, AddUsageApiKeyResponse, ApiKeyItem, ChainConfigKeysResponse,
CreateWalletResponse, ListMetadataItem, NewAccountResponse, NodeChainConfigResponse,
WalletItem,
AccountOpResponse, AddGroupResponse, AddUsageApiKeyResponse, ApiKeyItem,
ChainConfigKeysResponse, CreateWalletResponse, ListMetadataItem, NewAccountResponse,
NodeChainConfigResponse, WalletItem,
};
use crate::dstack::v1::get_client_key;
use crate::stripe::StripeState;
Expand Down Expand Up @@ -151,11 +151,11 @@ pub async fn add_group(
signer_pool: Arc<SignerPool>,
api_key: &str,
req: Json<AddGroupRequest>,
) -> Result<AccountOpResponse, ApiStatus> {
) -> Result<AddGroupResponse, ApiStatus> {
let cid_hashes = hex_array_to_u256_array(&req.cid_hashes_permitted)?;
let pkp_ids = hex_array_to_h160_array(&req.pkp_ids_permitted)?;

accounts::add_group(
let group_id = accounts::add_group(
signer_pool,
api_key,
&req.group_name,
Expand All @@ -165,7 +165,10 @@ pub async fn add_group(
)
.await
.map_err(|e| ApiStatus::internal_server_error(e, "add_group failed"))?;
Ok(AccountOpResponse { success: true })
Ok(AddGroupResponse {
success: true,
group_id: group_id.to_string(),
})
}

pub async fn add_action(
Expand Down
8 changes: 4 additions & 4 deletions lit-api-server/src/core/v1/endpoints/account_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use crate::core::v1::models::request::{
UpdateUsageApiKeyRequest,
};
use crate::core::v1::models::response::{
AccountOpResponse, AddUsageApiKeyResponse, ApiKeyItem, ChainConfigKeysResponse,
CreateWalletResponse, ListMetadataItem, NewAccountResponse, NodeChainConfigResponse,
WalletItem,
AccountOpResponse, AddGroupResponse, AddUsageApiKeyResponse, ApiKeyItem,
ChainConfigKeysResponse, CreateWalletResponse, ListMetadataItem, NewAccountResponse,
NodeChainConfigResponse, WalletItem,
};
use crate::stripe::StripeState;
use rocket::State;
Expand Down Expand Up @@ -191,7 +191,7 @@ pub(super) async fn add_group(
signer_pool: &State<Arc<SignerPool>>,
api_key: BilledManagementApiKey,
req: Json<AddGroupRequest>,
) -> OpenApiResponse<AccountOpResponse, ErrMessage> {
) -> OpenApiResponse<AddGroupResponse, ErrMessage> {
OpenApiResponse {
response: ApiResult(
account_management::add_group(signer_pool.inner().clone(), api_key.0.as_str(), req)
Expand Down
9 changes: 8 additions & 1 deletion lit-api-server/src/core/v1/models/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,19 @@ pub struct AddUsageApiKeyResponse {
pub usage_api_key: String,
}

/// Response for account config operations (add_group, add_pkp_to_group, remove_pkp_from_group, add_usage_api_key, remove_usage_api_key).
/// Response for account config operations (add_pkp_to_group, remove_pkp_from_group, add_usage_api_key, remove_usage_api_key).
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct AccountOpResponse {
pub success: bool,
}
Comment thread
GTC6244 marked this conversation as resolved.

/// Response for add_group, includes the on-chain group ID.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct AddGroupResponse {
pub success: bool,
pub group_id: String,
}

/// Mirrors AccountConfig.sol Group struct (groupName, groupDescription, plus ids/hashes when returned).
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct GroupResponse {
Expand Down
2 changes: 1 addition & 1 deletion lit-static/core_sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ export class LitNodeSimpleApiClient {
* POST /core/v1/add_group
* Add a group to an account with permitted action hashes and PKP hashes.
* @param {AddGroupOptions} options
* @returns {Promise<AccountOpResponse>}
* @returns {Promise<AddGroupResponse>} Response with `success` and `group_id`.
*/
Comment on lines 406 to 410

Copilot AI Mar 27, 2026

Copy link

Choose a reason for hiding this comment

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

addGroup() now documents returning AddGroupResponse, but this type isn’t defined anywhere in the SDK JSDoc, so consumers relying on JSDoc/TS inference will see an unknown type. Add a @typedef {Object} AddGroupResponse (with success and group_id) alongside the other response typedefs, and consider updating the existing AccountOpResponse typedef description to no longer mention add_group since it no longer matches this endpoint’s response shape.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Done in commit 0facc5c:

  • Added @typedef {Object} AddGroupResponse (with success boolean and group_id string) alongside the other response typedefs in core_sdk.js.
  • Updated AccountOpResponse typedef to remove the stale add_group mention — it now reads "Response for operations that return only success" with the full list of applicable endpoints.
  • Updated the AccountOpResponse doc comment in response.rs to match the same generic description.

Note: the commit is ready locally but the branch protection rule is blocking direct pushes — a repo admin will need to allow the push or relax the rule for this branch.

async addGroup({ apiKey, groupName, groupDescription = '', pkpIdsPermitted = [], cidHashesPermitted = [] }) {
const body = {
Expand Down
6 changes: 4 additions & 2 deletions lit-static/dapps/dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -915,16 +915,18 @@ function openGroupModal(item = null) {
hideStatus('groups-status');
try {
const client = await getClient();
let createdGroupId;
if (isEdit) {
const id = String(Number(item.id));
showActionProgress('Updating group', `Updating group "${name}".`);
await client.updateGroup({ apiKey, groupId: id, name, description: desc, pkpIdsPermitted, cidHashesPermitted });
} else {
showActionProgress('Creating group', `Creating group "${name}".`);
await client.addGroup({ apiKey, groupName: name, groupDescription: desc, pkpIdsPermitted, cidHashesPermitted });
const result = await client.addGroup({ apiKey, groupName: name, groupDescription: desc, pkpIdsPermitted, cidHashesPermitted });
createdGroupId = result.group_id;
}
await loadGroups();
showStatus('groups-status', isEdit ? 'Group updated.' : 'Group created.', 'success');
showStatus('groups-status', isEdit ? 'Group updated.' : `Group created (ID: ${createdGroupId}).`, 'success');
} catch (e) {
showStatus('groups-status', 'Error: ' + (e && e.message ? e.message : String(e)), 'error');
} finally {
Expand Down
Loading