-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
example for approvals per spending limits
- Loading branch information
Showing
12 changed files
with
1,746 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
ADMIN_USER_ADDR=0xaaa... | ||
MEMBER_USER_ADDR=-0xbbb... | ||
SYSTEM_MANAGER_KEY=0xddd... | ||
MEMBER_USER_CRED=0xeee... | ||
|
||
AUTH_HOST=https://auth.armory.narval.xyz | ||
AUTH_CLIENT_ID=narval-example | ||
VAULT_HOST=https://vault.armory.narval.xyz | ||
VAULT_CLIENT_ID=narval-example | ||
|
||
AUTH_API_KEY=armory-admin-api-key | ||
VAULT_API_KEY=vault-admin-api-key |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Collect approval before authorizing a request |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { AuthClient, AuthConfig, buildSignerEip191, privateKeyToJwk } from "@narval/armory-sdk" | ||
import { hexSchema } from "@narval/policy-engine-shared" | ||
import minimist from "minimist" | ||
import 'dotenv/config' | ||
|
||
const main = async () => { | ||
console.log('Starting...') | ||
|
||
const args = minimist(process.argv.slice(2)) | ||
|
||
const userType = args.user | ||
const authId = args._[0] | ||
|
||
// Check if userType is provided | ||
if (!userType) { | ||
console.error('Please specify the user type: --user=member or --user=admin') | ||
process.exit(1) | ||
} | ||
|
||
// Check if authId is provided | ||
if (!authId) { | ||
console.error('Please provide the authId as the second argument') | ||
process.exit(1) | ||
} | ||
|
||
const CRED = | ||
userType === 'admin' ? hexSchema.parse(process.env.ADMIN_USER_CRED) : hexSchema.parse(process.env.MEMBER_USER_CRED) | ||
|
||
const AUTH_HOST = process.env.AUTH_HOST | ||
const AUTH_CLIENT_ID = process.env.AUTH_CLIENT_ID | ||
|
||
if ( | ||
!AUTH_HOST || | ||
!AUTH_CLIENT_ID | ||
) { | ||
console.error('Missing environment variables') | ||
return | ||
} | ||
|
||
const authJwk = privateKeyToJwk(CRED) | ||
|
||
const authConfig: AuthConfig = { | ||
host: AUTH_HOST, | ||
clientId: AUTH_CLIENT_ID, | ||
signer: { | ||
jwk: authJwk, | ||
alg: 'EIP191', | ||
sign: buildSignerEip191(CRED) | ||
} | ||
} | ||
|
||
const auth = new AuthClient(authConfig) | ||
|
||
const authRequest = await auth.getAuthorizationById(authId) | ||
console.log('### authRequestBeforeApproval', JSON.stringify(authRequest, null, 2)) | ||
|
||
await auth.approve(authId) | ||
|
||
const approvedAuthorizationRequest = await auth.getAuthorizationById(authId) | ||
console.log('### approvedAuthorizationRequest', JSON.stringify(approvedAuthorizationRequest, null, 2)) | ||
console.log('Done') | ||
} | ||
|
||
main().catch(console.error) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { | ||
AuthClient, | ||
AuthConfig, | ||
DataStoreConfig, | ||
EntityStoreClient, | ||
PolicyStoreClient, | ||
VaultClient, | ||
VaultConfig | ||
} from '@narval-xyz/armory-sdk' | ||
|
||
export const armoryClient = (configs: { | ||
auth: AuthConfig | ||
vault: VaultConfig | ||
entityStore: DataStoreConfig | ||
policyStore: DataStoreConfig | ||
}) => { | ||
const authClient = new AuthClient(configs.auth) | ||
const vaultClient = new VaultClient(configs.vault) | ||
const entityStoreClient = new EntityStoreClient(configs.entityStore) | ||
const policyStoreClient = new PolicyStoreClient(configs.policyStore) | ||
|
||
return { | ||
authClient, | ||
vaultClient, | ||
entityStoreClient, | ||
policyStoreClient | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
import { | ||
AuthClient, | ||
EntityStoreClient, | ||
Permission, | ||
PolicyStoreClient, | ||
PublicKey, | ||
VaultClient | ||
} from '@narval-xyz/armory-sdk' | ||
import { AccountEntity, CredentialEntity } from '@narval-xyz/armory-sdk/policy-engine-shared' | ||
import { addressToKid, publicKeySchema } from '@narval-xyz/armory-sdk/signature' | ||
import { Action, Criterion, EntityType, Policy, Then, UserEntity, UserRole, ValueOperators } from '@narval/policy-engine-shared' | ||
import { Curves, KeyTypes, SigningAlg, jwkEoaSchema, privateKeyToJwk } from '@narval/signature' | ||
import { v4 } from 'uuid' | ||
import { Hex } from 'viem' | ||
|
||
const setPolicies = async (policyStoreClient: PolicyStoreClient) => { | ||
const policies: Policy[] = [ | ||
{ | ||
id: v4(), | ||
description: 'Allows admin to do anything', | ||
when: [ | ||
{ | ||
criterion: Criterion.CHECK_PRINCIPAL_ROLE, | ||
args: [UserRole.ADMIN] | ||
} | ||
], | ||
then: Then.PERMIT | ||
}, | ||
{ | ||
id: v4(), | ||
description: 'Allows managers to read, create and import wallets', | ||
when: [ | ||
{ | ||
criterion: Criterion.CHECK_PRINCIPAL_ROLE, | ||
args: [UserRole.MANAGER] | ||
}, | ||
{ | ||
criterion: Criterion.CHECK_PERMISSION, | ||
args: ['wallet:read', 'wallet:create', 'wallet:import'] | ||
} | ||
], | ||
then: Then.PERMIT | ||
}, | ||
{ | ||
id: v4(), | ||
description: 'Allows members to read wallets', | ||
when: [ | ||
{ | ||
criterion: Criterion.CHECK_PRINCIPAL_ROLE, | ||
args: [UserRole.MEMBER] | ||
}, | ||
{ | ||
criterion: Criterion.CHECK_PERMISSION, | ||
args: ['wallet:read'] | ||
} | ||
], | ||
then: Then.PERMIT | ||
}, | ||
{ | ||
id: 'members-can-transfer-1-eth-per-minute', | ||
description: 'members can transfer 1 ETH', | ||
when: [ | ||
{ | ||
criterion: 'checkAction', | ||
args: ['signTransaction'] | ||
}, | ||
{ | ||
criterion: 'checkPrincipalRole', | ||
args: ['member'] | ||
}, | ||
{ | ||
criterion: 'checkIntentType', | ||
args: ['transferNative'] | ||
}, | ||
{ | ||
criterion: 'checkIntentToken', | ||
args: ['eip155:1/slip44:60'] | ||
}, | ||
{ | ||
criterion: 'checkSpendingLimit', | ||
args: { | ||
limit: '1000000000000000000', | ||
operator: 'lt' as ValueOperators, | ||
timeWindow: { | ||
type: 'rolling', | ||
value: 30 | ||
}, | ||
filters: { | ||
perPrincipal: true, | ||
tokens: ['eip155:1/slip44:60'] | ||
} | ||
} | ||
} | ||
], | ||
then: 'permit' | ||
}, | ||
{ | ||
id: 'members-can-transfer-gt-1-eth-per-minute-with-approval', | ||
description: 'members transfers for more than 1 ETH per day requires an admin approval', | ||
when: [ | ||
{ | ||
criterion: 'checkAction', | ||
args: ['signTransaction'] | ||
}, | ||
{ | ||
criterion: 'checkPrincipalRole', | ||
args: ['member'] | ||
}, | ||
{ | ||
criterion: 'checkIntentType', | ||
args: ['transferNative'] | ||
}, | ||
{ | ||
criterion: 'checkIntentToken', | ||
args: ['eip155:1/slip44:60'] | ||
}, | ||
{ | ||
criterion: 'checkSpendingLimit', | ||
args: { | ||
limit: '1000000000000000000', | ||
operator: 'gte' as ValueOperators, | ||
timeWindow: { | ||
type: 'rolling', | ||
value: 30 | ||
}, | ||
filters: { | ||
perPrincipal: true, | ||
tokens: ['eip155:1/slip44:60'] | ||
} | ||
} | ||
}, | ||
{ | ||
criterion: 'checkApprovals', | ||
args: [ | ||
{ | ||
approvalCount: 1, | ||
countPrincipal: false, | ||
approvalEntityType: 'Narval::UserRole' as EntityType, | ||
entityIds: ['admin'] | ||
} | ||
] | ||
} | ||
], | ||
then: 'permit' | ||
} | ||
] | ||
await policyStoreClient.signAndPush(policies) | ||
} | ||
|
||
export const createPublicKey = (credInput: Hex): PublicKey => { | ||
return credInput.length === 42 | ||
? jwkEoaSchema.parse({ | ||
kty: KeyTypes.EC, | ||
crv: Curves.SECP256K1, | ||
alg: SigningAlg.ES256K, | ||
kid: addressToKid(credInput), | ||
addr: credInput | ||
}) | ||
: publicKeySchema.parse(privateKeyToJwk(credInput, 'ES256K')) | ||
} | ||
|
||
const setEntities = async ( | ||
entityStoreClient: EntityStoreClient, | ||
userAndCredentials: { credential: Hex; role: UserRole; id?: string }[], | ||
accounts: AccountEntity[] | ||
) => { | ||
const entitiesInput = userAndCredentials.reduce( | ||
(acc, { credential: credInput, role, id }) => { | ||
const user: UserEntity = { | ||
id: id || v4(), | ||
role | ||
} | ||
|
||
const publicKey = createPublicKey(credInput) | ||
|
||
const cred: CredentialEntity = { | ||
id: publicKey.kid, | ||
key: publicKey, | ||
userId: user.id | ||
} | ||
|
||
acc.users.push(user) | ||
acc.credentials.push(cred) | ||
|
||
return acc | ||
}, | ||
{ | ||
users: [] as UserEntity[], | ||
credentials: [] as CredentialEntity[], | ||
accounts | ||
} | ||
) | ||
|
||
await entityStoreClient.signAndPush(entitiesInput) | ||
} | ||
|
||
export const setInitialState = async ({ | ||
armory, | ||
userAndCredentials | ||
}: { | ||
armory: { | ||
vaultClient: VaultClient | ||
authClient: AuthClient | ||
entityStoreClient: EntityStoreClient | ||
policyStoreClient: PolicyStoreClient | ||
} | ||
userAndCredentials: { credential: Hex; role: UserRole; id?: string }[] | ||
}) => { | ||
const { vaultClient, authClient, entityStoreClient, policyStoreClient } = armory | ||
|
||
await setPolicies(policyStoreClient) | ||
await setEntities(entityStoreClient, userAndCredentials, []) | ||
|
||
const accessToken = await authClient.requestAccessToken({ | ||
action: Action.GRANT_PERMISSION, | ||
resourceId: 'vault', | ||
nonce: v4(), | ||
permissions: [Permission.WALLET_IMPORT, Permission.WALLET_CREATE, Permission.WALLET_READ] | ||
}) | ||
|
||
const { account } = await vaultClient.generateWallet({ accessToken }) | ||
|
||
await setEntities(entityStoreClient, userAndCredentials, [ | ||
{ | ||
id: account.id, | ||
accountType: 'eoa', | ||
address: account.address as Hex | ||
} | ||
]) | ||
return account | ||
} |
Oops, something went wrong.